一、C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于
C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中
约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,
C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个
重点去学习。
二、统一的列表初始化
1、{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型(包括定位new表达式),使用初始化列表时,可添加等号(=),也可不添加。
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2{ 2 };
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
2.2 std::initializer_list
无论是对象、内置类型、数组可以用{ }其实还相对能理解,但是更为关键的是只要是有迭代器的容器都可以用{}进行初始化,
三、声明类型
3.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将
其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初
始化值的类型。
3.2 nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
3.3 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
cpp
int main()
{
const int x = 1;
double y = 2.2;
cout << typeid(x * y).name() << endl;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是const int*
cout << typeid(ret).name() << endl;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
// vector存储的类型跟x*y表达式返回值类型一致
// decltype推导表达式类型,用这个类型实例化模板参数或者定义对象
vector<decltype(x* y)> v;
return 0;
}
decltype和表达式的返回结果密切相关,即返回表达式结果对应的类型。
aotu在大多数情况下可以替代decltype,但是有一个区别就是,aotu通过已有的变量或者表达式推出类型后必须去创建对应的变量,但是decltype通过返回值返回之后可以创建也可以不创建,因此在传模版参数的时候,auto无法替代decltype,因为我们可能只是想要单纯去传一个类型。
四、STL的一些变化
容器中的一些新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得 比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是 可以返回const迭代器的,这些都是属于锦上添花的操作。
实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:
https://cplusplus.com/reference/vector/vector/emplace_back/
https://cplusplus.com/reference/vector/vector/push_back/
https://cplusplus.com/reference/map/map/insert/
https://cplusplus.com/reference/map/map/emplace/
但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的? 请看下面的右值引用和移动语义章节的讲解。另外emplace还涉及模板的可变参数,
五、右值引用和移动语义
5.1 区分左值引用和右值引用
传统C++语法就有引用的概念,而在C++11之后新增了一个右值引用的语法特性,在我们区分左值和右值之前,我们心里都要知道一个概念就是:无论是左值引用还是右值引用,本质上都是给对象起别名。
**(1)左值:**一个表示数据的表达式(如变量名或解引用的指针)
他和右值引用的最大区别就是->我们可以获取的他的地址(最关键)+可以对他赋值,左值可以出现在赋值符号的左边,但是右值不能出现在赋值符号的左边。 而左值引用,就是给左值起别名。
例子:
cpp
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
其中const修饰的左值虽然不能给他赋值,但是可以取地址。
通过以上我们会发现,其实左值引用就是我们之前所学的知识。
(2)**右值:**也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址(最关键) 右值引用就是对右值的引用,给右值取别名。
例子:
cpp
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: "=": 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
//但是取了别名后 可以修改 也可以取地址 如果我不想修改,就const修饰
cout << &rr1 << endl;
++rr1;
cout << &rr2 << endl;
++rr3;
return 0;
}
**右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:**不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用
5.2 左值引用和右值引用的比较
左值引用:
(1)左值可以引用左值,但是不能引用右值
(2)const左值引用既可以引用左值又可以引用右值
cpp
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
思考:为什么const修饰的左值引用才可以引用右值呢??
右值引用:
(1)右值引用只能引用右值,不能引用左值
(2)但是右值引用可以引用move后的左值
例子:
cpp
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: "初始化": 无法从"int"转换为"int &&"
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = move(a);
return 0;
}
在我们没有提供右值引用版本func的时候,我们如果传右值的话,可以传给const修饰的左值引用,因为这样才可以保证权限不放大
当我们提供了右值引用版本的func的时候,我们传右值的时候,编译器会优先传给右值引用的func函数。
5.3 右值引用的意义
下面就进入我们的关键点了,右值引用究竟有什么样的意义??为了能够方便我们观察,我们封装一个简单的string类,并在其构造函数内增加打印功能。
cpp
namespace cyx
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
/* string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}*/
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
左值引用可以直接减少拷贝
(1)引用传参
(2)引用返回
但是当函数的返回值是一个局部变量的时候,那么局部变量出了作用域就不存在了,就不能用左值引用返回,那么就只能用传值返回,在一些深拷贝的场景,传值返回的代价很高,影响效率。 而右值引用的出现,就是为了解决左值引用解决不了的两个问题:
(1)局部对象的返回问题
(2)插入接口,对象拷贝的问题(比如链表,每插入一个节点都需要开空间)
cpp
cyx::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
cyx::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
int main()
{
cyx::string ret = cyx::to_string(1234);
//cout << valStr.c_str() << endl;
//std::string s1("hello");
//std::string s2 = s1;
//std::string s3 = move(s1);
//move(s1);
//std::string s3 = s1;
return 0;
}
右值引用和移动语义解决上述问题:
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
不仅仅有移动构造,还有移动赋值:
cpp
cyx::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
cyx::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
int main()
{
cyx::string ret1;
ret1= cyx::to_string(1234);
//cyx::string ret2= cyx::to_string(-1234);
/*cout << valStr.c_str() << endl;
std::string s1("hello");
std::string s2 = s1;
std::string s3 = move(s1);*/
//move(s1);
//std::string s3 = s1;
return 0;
}
STL中的容器都是增加了移动构造和移动赋值
关于插入接口的对象拷贝问题,最最常见的就是链表,每插入一个对象就要拷贝一个新的节点(要开空间)
5.4 右值引用引用左值及其一些更深入的使用场景分析
右值(将亡值)在赋值和拷贝的时候,为了节省空间,我们可以通过右值引用+移动语义将资源转移。但是有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用(只是暂时的),然后实现移动语义。
通过上图我们可以发现,move将s1变成右值后,触发了移动构造,s1的资源直接被ret3掠夺了!s1被置空
而STL容器插入接口函数也增加了右值引用版本
5.5 完美转发(模版中的&&万能引用)
(1)模板中的&&不代表右值引用,而是万能引用 ,其既能接收左值又能接收右值。
(2)模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力(传左值就是左值引用,传右值就是右值引用)
(3)但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
(4)由于上面的原因,在有些情况下一旦出现嵌套调用,但是我们并不希望改变他右值的特性。我们希望能够在传递过程中保持它的右值的属性, 就需要用我们下面学习的完美转发
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
std::forward 完美转发在传参的过程中保留对象原生类型属性
在多层嵌套的场景下,则需要不断地利用 std::forward来帮助我们保持类型。
cpp
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<cyx::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}
六、类与对象相关特性
6.1 移动构造函数和移动赋值运算符重载
默认成员函数:
原来C++类中,有6个默认成员函数:
-
构造函数
-
析构函数
-
拷贝构造函数
-
拷贝赋值重载
-
取地址重载
-
const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
而C++11 新增了两个:移动构造函数和移动赋值运算符重载
(1)如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造(因为移动构造涉及到资源掠夺,是一件很严格的事情,所以条件会比较苛刻,编译器认为我们应该自己去控制)。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
(2)如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
(3)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
cyx::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
假设我们写了析构,那么默认移动构造就不会有。
6.2 强制生成默认函数的关键字default
类似上面的情景,我们希望默认的移动拷贝和默认的移动赋值能够 在我们提供了析构函数之后还可以生成,这个时候就可以用我们的default------强制生成相应的默认构造
6.3 禁止生成默认函数的关键字delete
既然有办法让编译器强制生成默认函数,自然也有办法让编译器禁止生成默认函数
以上是故意为之,但是实际上确实有相关的类似类需要用到这个delete关键字。
6.4 继承和多态中的final与override关键字
final和override关键字在C++中提供了对类继承和函数重写行为的额外控制。 final关键字可以保护基类不被修改,防止滥用继承,而override关键字可以明确标识派生类中对基类的虚函数的重写,并进行编译时类型检查。 然而,final关键字的使用可能限制了代码的扩展性和灵活性,而override关键字需要手动添加,并且只对虚函数有效。
具体的一些用法分析,请参照多态文章。
七、可变参数模版
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
下面就是一个基本可变参数的函数模板
cpp
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变
参数,所以我们的用一些奇招来一一获取参数包的值。
sizeof...(args)可以帮助我们打印出可变参数包的参数数量。
所以以下有两种方法来解析可变参数包
7.1 递归函数方式展开参数包
cpp
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
7.2 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性------初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包(因为我们没有指定数组的个数,所以需要展开去进行推断开多少空间)
cpp
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... }; //利用了arr数组创建的时候必须推断个数
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
其实我们用逗号表达式本质上是通过这个式子前面在进行推断的通过,后面的0是最终返回给arr用来初始化的。 所以还有一种方法就是将PrintArg的返回值设置成0,这样在解析结束的时候正好将0返回回来初始化arr数组。
cpp
template <class T>
int PrintArg(T t)
{
cout << t << " ";
return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { PrintArg(args)... }; //利用了arr数组创建的时候必须推断个数
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
7.3 STL中的emplace相关接口
emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert
emplace系列接口的优势到底在哪里呢?
cpp
int main()
{
// 深拷贝的类
std::list<cyx::string> mylist;
// 没区别 对于已经构造好了的对象 没有区别
cyx::string s1("1111");
mylist.push_back(s1);
mylist.emplace_back(s1);
cout << endl;
cyx::string s2("2222");
mylist.push_back(move(s1));
mylist.emplace_back(move(s2));
// 开始有区别 传一些还没构造好的对象
cout << endl;
mylist.push_back("3333"); // 构造匿名对象 + 移动构造
mylist.emplace_back("3333");// 直接构造
return 0;
}
但是由于有了移动构造的存在,所以其实深拷贝的消耗可以忽略不计,因此其实两者的效率差异不大。
那究竟在什么地方有差异呢???
我们知道其实移动构造只对需要深拷贝的类是右意义的,但是如果该类不需要深拷贝,那么就emplace的优势就体现出来了!!!!
cpp
struct A
{
int _x;
int _y;
A(int x, int y)
:_x(x)
,_y(y)
{
cout << "调用构造"<<endl;
}
A(const A& a)
{
_x = a._x;
_y = a._y;
cout << "深拷贝"<<endl;
}
};
int main()
{
//浅拷贝的类差异会出现,因为省略了对内置类型的再拷贝
std::list<A> mylist2;
mylist2.push_back(A(1,2));
mylist2.push_back({1,2});
mylist2.emplace_back(A(1, 2));
mylist2.emplace_back(1,2);
for (auto&e : mylist2) cout << e._x << " " << e._y << endl;
}
总的来说就是emplace_back和push_back 在处理已经构建好的对象时,是没有差异的,因为无论是左值还是右值,push_back都有对应的版本。 差异体现在对于一些还没构建好的对象,push_back必须要先构造匿名对象,然后再进行拷贝,而emplace_back可以直接通过传过来的参数去推断并调用其构造函数,省略了拷贝的这一步。
push系列:匿名构造+拷贝
emplace系列:直接构造
但是由于有了移动构造的存在,所以对于深拷贝的类来说,其实移动构造的损耗并不是很大,但是对于浅拷贝的类来说,差距就出现了!!!
因此我们平时可以无脑用empalce系列!整体来说会比较好一点。