在一开始学C++之前我们就简单的了解了一下C++的发展历史,重要的几个结点如下:
| 阶段 | 内容 |
|---|---|
| C withclasses | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
| C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
| C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
| C++20 | 自C++11以来最大的发行版,引入了许多新的特性,比如:**模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)**等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
当然在这些之中还发行了其他的版本,C++还在不断的向后发展。但是:现在公司主流使用还是 C++98和C++11 。
C++11官网:👉 传送门
在我们之前学的C++98中,我们初始化一个变量或者是一个数组或一个对象可以是:
struct Point
{Point(int x = 1, int y = 2): _x(x), _y(y){}int _x;int _y;
};int main()
{int x1 = 1;int x2 = int();int* p1 = new int(1);int* p2 = new int[3]{ int(1),int(3),int(4) };int* p3 = new int[3]{ 1,3,4 };Point p(1, 2);Point();return 0;
}
现在在C++11中我们可以按照如下的方式初始化:
int main()
{int x1 = { 2 };int x2{ 3 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p{ 1, 2 };return 0;
}
上面支持,本质就更好支持new[]的初始化问题:
C++98只能new单个对象,new多个对象没办法很好的初始化了,定义一个对象数组是很不方便的,至少应该如下定义:
int main()
{Point p1, p2, p3, p4;Point* pp1 = new Point[]{ p1, p2, p3, p4 };Point* pp2 = new Point[]{ Point(1, 1), Point(2, 2), Point(3, 3), Point(4, 4) };return 0;
}
而在C++11中我们直接可以:
int main()
{Point p1[] = { {1, 1}, {2, 2}, {3, 3}, {4, 4} };Point p2[]{ {1, 1}, {2, 2}, {3, 3}, {4, 4} };Point* p3 = new Point[]{ {1, 1}, {2, 2}, {3, 3}, {4, 4} };return 0;
}
类比C++98的隐式类型转换:
C++98中我们知道,单参数的构造函数可以直接给个值直接构造,C++11可以说是对C++98这一特性进行了延伸:
class Date
{
public://explicit Date(int year, int month, int day)Date(int year, int month, int day):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}private:int _year;int _month;int _day;
};int main()
{Date d1{ 2023, 3, 7 };Date d2 = { 2023, 3, 7 };return 0;
}

std::initializer_list的介绍文档:

std::initializer_list就像是一个容器一样,我们先来看一下其类型:

底层大概的样子:

为什么突然将到一个容器了呢?

以vector,和map为例,C++11之后就我们之前学的容器可以直接通过{ } 列表初始化了:
int main()
{vector v1 = { { 2023, 3, 7 }, { 2023, 3, 7 }, { 2023, 3, 7 } };vector v2{ { 2023, 3, 7 }, { 2023, 3, 7 }, { 2023, 3, 7 } };map dict1 = { { "string", "字符串" }, { "sort", "排序" } };map dict2{ { "string", "字符串" }, { "sort", "排序" } };return 0;
}
先构造一个initializer_list,再用initializer_list构造一个vector,具体过程:
也可以和之前隐式类型转换联系起来,也是中间产生了一个临时对象(initializer_list),再用临时对象去拷贝构造。
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值,什么是左值引用:
什么是右值,什么是右值引用:
总结:
不能说出现在赋值符号左边的就叫左值,在赋值符号右边的也有可能是左值(int a = 1; int b = a) a 可以赋值和取地址,右值却不能出现在赋值符号的左边,const修饰的对象也叫左值(特例),左值一定可以取地址,但不一定能赋值,右值不能出现在赋值符号左边,右值不能取地址。
int main()
{//左值int a = 1;int& b = a;//右值double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);//这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0;
}
注意:
上述我们讲到右值不能放在 = 符号的左边,即不能赋值,我们可以理解成其是一个临时对象,具有const属性的一个临时对象,我们对其进行详细的分类,分成两类:
左值引用:
右值引用:
注意:
在我们之前的C++学习中,我们学的都是左值引用,左值引用可以提高程序的效率,特别是函数传引用返回的时候。
左值引用复习:👉 传送门
左值引用的短板:
string中的 to_string(int value)函数中可以看到,这里只能使用传值回,传值返回会导致至少1次拷贝构造,编译器会优化(如果是一些旧一点的编译器可能是两次拷贝构造)
很显然to_string是要将数字转换成字符串然后返回,那么这个返回的字符串就是个临时对象,除了函数作用域就销毁了,传引用返回就拿不到了,那块空间已经被销毁了,所以这里只能传值返回。

很显然即使是编译器优化了之后,也是至少有一次拷贝。
众所周知把大象放到冰箱里一共有三步:
那么将大象从一号冰箱放到二号冰箱一共有几步:
C++11提供了一个移动构造,直接用另一个新的对象的指针来接管了原来的对象。右值引用和移动语义解决上述问题:

C++11中编译器会直接将传值返回识别成一个右值,然后调用移动构造:

在我们之前没有移动构造的时候,我们调用的是拷贝构造,之前我们提到过const类型既可以接收右值,也可以接收左值,所以之前我们是能匹配的上的,但是现在现在有了移动构造,编译器会匹配最匹配的那一个构造函数。
将一个左值move之后就成了右值,右值的资源有可能会被转移:


默认成员函数:
C++11 新增了两个默认成员函数:
要求:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
STL的容器,C++11以后,都提供移动构造和移动赋值,右值引用 + 移动构造补齐了C+ +传参和传返回值的最后一块短板。
STL容器中的各种插入也用到了右值引用:==
当这些容器的元素是某个对象的时候,插入的话要new一个新的元素,也会产生深拷贝的问题,所以这里用到右值引用将会非常方便:

int main()
{list lt;YY::string s1("1111");//这里调用的是拷贝构造lt.push_back(s1); //->void push_back (const value_type& val);//如果只是为了插入,下面的效果明显更好//下面调用都是移动构造lt.push_back("2222");lt.push_back(std::move(s1)); //->void push_back (value_type&& val);YY::string s2("hello world");//YY::string s3 = std::move(s2);YY::string s4 = s2;return 0;
}

注意:
