目录
一、列表初始化
内置类型的列表初始化
自定义类型的列表初始化
二、变量类型推导
2.1为什么需要类型推导
2.2 decltype类型推导
三、基于范围for的循环
四、final与override
五、智能指针
六、新增加容器---静态数组array、forward_list以及unordered系列
七、委派构造函数
八、默认函数控制
8.1 显式缺省函数
8.2删除默认函数
九、右值引用
9.1 什么是右值?什么是左值?什么是右值引用?什么是左值引用?
9.2右值引用与左值引用
9.3 std::move()
9.4 左值引用注意知识点:
9.5 完美转发
十、lambda表达式
10.1 C++98中的一个例子
10.2lambda表达式各部分说明
10.3 测试捕捉列表
10.4测试 mutable
10.5 底层实现
十一.、线程库
11.1 线程的启动
11.2 线程的结束
11.3 原子性操作库(atomic)
int main()
{
// 内置类型变量
int x1 = { 10 };
int x2{ 10 };
int x3 = 1 + 2;
int x4 = { 1 + 2 };
int x5{ 1 + 2 };
// 数组
int arr1[5] {1, 2, 3, 4, 5};
int arr2[]{1, 2, 3, 4, 5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1, 2, 3, 4, 5};
// 标准容器
vector v{ 1, 2, 3, 4, 5 };
map m{ { 1, 1 }, { 2, 2, }, { 3, 3 }, { 4, 4 } };
return 0;
}
1.标准库支持单个对象的列表初始化
class A
{
public:
A(int a = 10)
:_a(a)
{}
A(int a, char c)
:_a(a)
, _c(c)
{}
private:
int _a;
char _c;
};
void test2()
{
A a;
A a0 = 10;//单参构造函数隐式类型转换: 10会先调用默认构造函数成为一个匿名对象,再使用拷贝构造进行拷贝赋值
//A a2 = 10,'a';//报错,不支持
//C++11: 自定义类型支持列表初始化
A a1 = { 1, 'c' };
}
2. 多个对象的列表初始化
template
//自定义容器类如果要支持列表初始化,需要实现initializer_list接口
class Vector
{
public:
Vector(const initializer_list& lst)//实现initializer_list接口
:_array(new T[lst.size()])
{
int idx = 0;
for (const auto& e : lst)
{
_array[idx++] = e;
}
}
private:
T* _array;
};
void test4()
{
//自定义容器类无法天然支持列表初始化
//Vectorvec0 = { 1, 2, 3, 30 };
//实现initializer_list接口就可以
Vectorvec = { 1, 2, 3, 30 };
}
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。例如:
#include
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。例如:
#include
2.2.1为什么需要decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
template
T1 Add(const T1& left, const T2& right)
{
return left + right;
}
2.2.2 decltype测试(包含推演表达式类型作为变量的定义类型,推演函数返回值的类型)
int add(int x,int y){
return x+y;
}
int main(){
double i=0;
decltype(i) a; // double
decltype(add()) b; //int 注意括号。不带括号就是函数指针了。
}
int fun1(int a)
{
return a;
}
void test5()
{
auto a = 10; //编译时类型推导
decltype(a + 10) c;//c的类型是在运行时类型推导:通过表达式结果确定的,是RTTI思想
decltype(test4) d;//推导test4函数类型
decltype(fun1) e;
decltype(fun1(a)) f;
cout << typeid(e).name() << endl;//int _cdecl-->函数类型
cout << typeid(f).name() << endl;//int-->int类型
}
参考:https://mp.csdn.net/console/editor/html/102709550
类 + fina: 不能被继承的类
虚函数+fina: 不能被重写的函数 override: 强制重写父类的虚函数override用法:强制重写父类的方法,强制实现多态,体现了接口继承
参考:https://mp.csdn.net/console/editor/html/104690689
参考:https://mp.csdn.net/console/editor/html/105661575
array:静态数组。在栈上开辟的固定大小数组
forward_list : 不带头单向非循环链表
参考:https://mp.csdn.net/console/editor/html/106152063
委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写更加容易。
class Info {
public:
Info() : _type(0), _name('a')
{
InitRSet();
}
Info(int type) : _type(type), _name('a')
{
InitRSet();
}
Info(char a) : _type(0), _name(a)
{
InitRSet();
}
private:
void InitRSet() {//初始化其他变量}
private:
int _type;
char _name;
//...
};
上述构造函数除了初始化列表不同之外,其他部分都是类似的,代码重复
因此我们可以使用委派构造完成这项工作。
//委派构造 : 减少构造函数定义
class B
{
public:
B()
:_a(1)
, _b(2)
, _c(3)
{}
B(int a)
:B()
{
_a = a;
}
B(int a, int b)
:B(a)
{
_b = b;
}
private:
int _a;
int _b;
int _c;
};
void test7()
{
B b;
B b2(10);
B b3(10, 20);
}
但在使用委派构造时要防止相互无限次调用,导致程序异常,例如:
class B
{
public:
B()
:_a(1)
, _b(2)
, _c(3)
{}
//构造函数相互调用,无法终止,死循环
B(int a)
:B(a,10)
{
_a = a;
}
B(int a, int b)
:B(a)
{
_b = b;
}
private:
int _a;
int _b;
int _c;
};
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A
{
public:
A(int a) : _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class B
{
public:
B()
:_a(1)
, _b(2)
, _c(3)
{}
B(int a)
:B()
{
_a = a;
}
B(int a, int b)
:B(a)
{
_b = b;
}
B& operator=(const B& b) = delete;//删除默认函数
private:
int _a;
int _b;
int _c;
};
class A
{
public:
A(int a) : _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
//A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
A a3(20);
a3 = a2;
return 0;
}
右值:常量,临时变量,将亡值
左值:除过右值以为
右值引用:既可以引用左值,也可以引用右值(一般加const)
左值引用:只能引用右值
int fun2()
{
int a = 10;
return a;
}
//左值 右值 介绍
void test9()
{
//左值:可以出现在赋值符号的两边
int a = 10;
int b = a;
int c = b;
//右值:只可以出现在赋值符号的右边
//右值: 常量
//10 = a;
//右值:临时对象
//右值:将亡值 声明周期马上结束的值
//fun2() = b;
}
int fun2()
{
int a = 10;
return a;
}
//左值引用+右值引用
void test10()
{
//左值引用:既可以引用左值,也可以引用右值
int a = 10;
int& ra = a;
const int& ra2 = 10;
const int& f22 = fun2();
//右值引用:只能引用右值
int&& rra = 10;
int&& f2 = fun2();
//int&& rra2 = a;//右值引用不能引用左值
}
int main()
{
test10();
return 0;
}
class String
{
public:
//构造函数
String(char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
cout << "String(char*)" << endl;
}
//左值引用拷贝构造 需要开辟空间
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
cout << "String(const String&)" << endl;
}
//右值引用拷贝构造 : 资源可以进行二次利用,不需要开辟空间---》从而提高代码效率,节省开销
String(String&& s)
:_str(s._str)
{
s._str = nullptr;
cout << "String(const String&&)" << endl;
}
//赋值
String& operator=(const String& s)
{
if (this != &s)
{
if (_str)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
}
cout << "operator=(const String&)" << endl;
return *this;
}
//移动赋值
String& operator=(String&& s)
{
if (this != &s)
{
char* tmp = _str;
_str = s._str;
s._str = tmp;
}
cout << "operator=(String&&)" << endl;
return *this;
}
char* c_str()
{
return _str;
}
~String()
{
if (_str)
delete[] _str;
}
private:
char* _str;
};
String getString(char* str)
{
String strOut(str);
return strOut; //调用拷贝构造
}
void test11()
{
//先调用构造在调用拷贝构造
char* str = "123";
getString(str);
char* str1 = "123";
String copy = getString(str1);//执行一次构造+两次拷贝构造--》编译器优化执行一次构造+一次拷贝构造
}
void test12()
{
//String str("123");
String str2("456");
String str3("-10");
str3 = str2;//赋值
cout << endl;
str2 = String("789");//构造+赋值
}
void test13()
{
String str("123");
String str2("456");
cout << str.c_str() << endl;
//移动语义move:错误使用场景,str本身为左值,且str在后面被继续访问时值已经被改变
//只有完全确定左值在后面不会被使用,才能使用移动语义把左值转化为右值,
str2 = move(str);//改变str的属性: str->右值 移动赋值
cout << str.c_str() << endl;
str2 = String("789");//构造+赋值
}
C++11中,std::move()函数位于 头文件中,这个函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。 注意:被转化的左值,其声明周期并没有随着左右值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
class E
{
public:
E(const String& name, const String& id)
:_name(name)
, _id(id)
{
cout << "E(const String& name, const String& id)" << endl;
}
E(E&& e)//右值 移动构造
:_name(move(e._name))
, _id(move(e._id))
{
cout << "E(E&&)" << endl;
}
E(E& e)
:_name(e._name)
, _id(e._id)
{
cout << "E(E&)" << endl;
}
E& operator=(const E& e)
{
cout << "E& operator=(const E& e)" << endl;
if (this != &e)
{
_name = e._name;
_id = e._id;
}
return *this;
}
E& operator=(E&& e)
{
cout << "E& operator=(E&& e)" << endl;
if (this != &e)
{
/*_name = e._name;
_id = e._id;*/
_name = move(e._name);
_id = move(e._id);
}
return *this;
}
private:
String _name;
String _id;
};
void test14()
{
E e("123", "46");
cout << endl;
E cope(e);
cout << endl;
E copy2 = E("999", "999");
cout <<" "<< endl;
e = cope;
copy2 = E("1000", "1000");
}
注意:为了保证移动语义的传递,程序员在编写移动构造函数时,最好使用std::move转移拥有资源的 成员为右值
class D
{
public:
//左值引用
D(const int& d)//必须加const
:_d(d)
{}
D(int&& d)
:_d(d)
{}
private:
int _d;
};
void test15()
{
D d(2);//2常量具有常性,因此使用左值引用时必需加const
//当然可以直接使用右值引用。
}
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发, 比如:
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
#include
#include
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater());
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C11语法中出现了Lambda表达式。
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情,是最简单的lambda表达式。
//测试捕捉列表
void test16()
{
//捕捉列表: 捕捉父作用域(祖先作用域)中的变量,供labmda表达式的函数体使用
int a = 10;
int b = 1;
//以传值的形式捕捉父作用域中的变量a,b
[a, b]()->int{return a + b; };
//以传值的形式捕捉父作用域中的所有变量(已经声明或者定义的)
[=]()->int{return a + b; };
/*
//使用错误: 在labmda表达式之前没有定义变量c
[=]()->int{return a + b + c; };
int c = 100;
*/
//[&a, &b]:以传引用的形式捕捉父作用域中的变量a,b
[&a, &b]()->int{return a + b; };
//[&]:以传引用的形式捕捉父作用域中的所有变量
[&]()->int{return a + b; };
//[=, &b]:以传值的形式捕捉父作用域中除b以外的所有变量(已经声明或者定义的) 以传引用形式捕捉b
[=, &b]()->int{return a + b; };
//[&, b]:以传引用的形式捕捉父作用域中除b以外的所有变量(已经声明或者定义的) 以传值形式捕捉b
[&, b]()->int{return a + b; };
//[=,b]:此种写法不对,捕捉方式不能重复
//[&,&b]:此种写法不对,捕捉方式不能重复
//[=,b]()->int{return a + b;};
//[&,&b]()->int{return a + b;};
}
改变从捕捉列表中捕捉的变量的常性
int glable = 1000;
//mutable:改变从捕捉列表中捕捉的变量的常性,即变量可以修改
void test17()
{
/*
int a = 10;
int b = 1;
[=]()->int{
//a,b都为const变量,不可修改
a = 100;
b = 20;
return a + b; };
*/
int a = 10;
int b = 1;
int c = 100;
[](int c, int d)->int{
c = 100;
d = 1;
c = glable;//gloable 全局变量
return c;
};
for (int i = 0; i < 10; ++i)
{
int f = 10;
[=](int c, int d)->int{
c = 100;
d = 1;
c = glable;//gloable 全局变量
c = a;
return c + f;
};
}
[=]()mutable->int{
//mutable使 a,b都为非const变量,可修改
a = 100;
b = 20;
return a + b; };
}
函数指针和lambda表达式之间可以赋值
void(*ptr)();//函数指针
void testLam()
{
auto fun = [](){int a = 10; int b = 20; return a + b; };
cout << fun() << endl;
auto fun2 = [](){int a = 10; int b = 20; return a + b; };
//lambda表达式之间不能赋值
//fun = fun2;
auto fun3 = [](){int a = 10; int b = 20; cout << a << " " << b << endl; };
//函数指针和lambda表达式之间可以赋值
ptr = fun3;
fun3();
ptr();
}
void testLam2()
{
//lambda表达式底层实现:实际上就是一个仿函数类,仿函数类中去重载()运算符
Com com;
int a = 1;
int b = 2;
com(a, b);
auto fun = [](int a, int b)->bool{
return a > b;
};
fun(a, b);
}
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件,该头文件声明了std::thread 线程类。
C++线程库通过构造一个线程对象来启动一个线程,该线程对象中就包含了线程运行时的上下文环境,比如:线程函数、线程栈、线程起始状态等以及线程ID等,所有操作全部封装在一起,最后在底层统一传递给_beginthreadex() 创建线程函数来实现 (注意:_beginthreadex是windows中创建线程的底层c函数)。std::thread()创建一个新的线程可以接受任意的可调用对象类型(带参数或者不带参数),包括lambda表达式(带变量捕获或者不带),函数,函数对象,以及函数指针。
启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择:
加入式:join()
分离式:detach()
c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。
为了解决上面问题,于是我们进行加锁,确保++是原子性操作
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。因此C++11中引入了原子操作。
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦。