C++11是C++语言的一个重要版本,引入了许多新的特性和改进。接下来进行这些新特性的学习!
1.nullptr的引入
在C语言中,NULL表示空地址。而C++中NULL被定义为字面量0。
这里我们通过打印x的类型名,发现NULL的类型名是int,而对于NULL既能够表示指针值,又可以能表示int值,所以C++11引入了nullptr关键字来表示空指针!
2.列表初始化
2.1.语法层面
我们先通过一段代码来学习一下,列表初始化这个新特性
struct Point
{int _x;int _y;
};class Date
{
public: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;
};// 语法层面上
// 一切都可以用列表初始化
// 并且可以省略掉=
void part1_0()
{int i = 0;int j = { 0 };int k{ 0 };int array1[] = { 1, 2, 3, 4, 5 };int array2[]{ 1, 2, 3, 4, 5 };int array3[5] = { 0 };int array4[5]{ 0 };Point p1 = { 1, 2 };Point p2{ 1, 2 };// 日期类需要传入参数Date d1(2023, 11, 25);// 类型转换 构造+拷贝构造 优化直接构造Date d2 = { 2023, 11, 25 };Date d3{ 2023, 11, 25 };const Date& d4 = { 2023, 11, 25 };Date* pd1 = new Date[3]{ d1, d2, d3 };Date* pd2 = new Date[3]{ {2022, 11, 25}, {2022, 11, 26}, {2022, 11, 27} };}
通过这段代码的学习,我们学习到了以后我们定义变量构造对象时可以不用在借助=操作符,可以直接通过{ }来实现,但是列表初始化只是为了实现这个功能吗?当然不是的,我们接下来通过底层来探讨列表初始化。
2.2.列表初始化的作用
如图:我们在日期类中,定义初始化函数是通过传入3个int类型的参数,当我们传入4个参数时会报错:
- “初始化”: 无法从“initializer list”转换为“Date”
- 无构造函数可以接受源类型,或构造函数重载决策不明确
这两个问题第二个是根本上的问题,因为传入的参数数量不匹配。这里涉及先进行构造再进行拷贝构造,来传入参数实现!
- 第一个报错是什么意思呢?
- 为什么vector类型可以随意的增加参数的数量而不会报错呢?
答:因为vector以及大部分的STL容器中在C++11版本后支持initializer list这个结构,不支持这个结构的结构体,无法进行多参数的调整转化,传入参数需要与构造函数参数个数匹配。
为了进一步验证这个initializer list结构的作用,我们通过手搓一个vector但是不实现initializer list模块,来探讨一下能不能进行随意地增加参数!重生之C++学习:vector-CSDN博客
我们在这个博客中最终的代码,对这段代码进行测试
结果是:我们无法的实现参数的随意增加!!!
当我们在vector模块增加一个由initializer_list支持的构造函数,我们就能够实现我们的随意增加减少我们传入的参数了!
my_vector(initializer_list<T> il)
{reserve(il.size());for (auto& e : il){push_back(e);}
}
讲到这里,我们已经知道了列表初始化的作用:简化代码,提供了更灵活的初始化方式。
map<string, string> dict = {{"insert", "插入"}, {"get","获取"} };
for (auto& kv : dict)
{cout << kv.first << ":" << kv.second << endl;
}
如上,我们通过初始化列表对map的便捷初始化
2.3.initializer list
initializer list本质上就是一个类,内部封装着这几个函数
通过这段代码我们发现il1的类型是初始化列表,也就是我们通过方括号的形式传入参数的本质就是通过传入初始化列表这个对象给对应的容器,来实现容器的构造,那么大部分的容器也就是通过支持传入initializer list这个对象的构造函数来实现{ }的多个参数的传入,而对于没有实现这个构造函数的自然就必须按照自身的构造函数规则进行了。
// the type of il is an initializer_list
auto il1 = { 10, 20, 30, 40, 50 };
cout << typeid(il1).name() << endl;
3.auto和decltype
auto 和 decltype都可以推导对象的类型,而auto是用来定义变量时,作为一个简易的语法糖,不能够用来实例化对象。
// auto推导类型,其中i是已定义的一个int类型,d是double类型auto j = i;auto ret = i * d;// 类型以字符串形式获取到cout << typeid(j).name() << endl;cout << typeid(ret).name() << endl;
打印内容为:int和double,这里auto分别推导出j、ret两个变量的类型
// auto在设置时并没有实例化作为实际类型的能力,auto应用场景不足vector<auto ret> v1;auto (ret) y;
假设我们在某些场景中,需要推导出一个复合类型的实际类型是什么,并且将这个类型用STL容器存放,显然在上面的讲解中我们发现auto无法胜任这个场景。所以C++11提供了decltype这个关键字。
// 用ret的类型去实例化vector// decltype可以推导对象的类型。// 可以用来模板实参,或者再定义对象vector<decltype(ret)> v;v.push_back(1);v.push_back(1.1);for (auto e : v){cout << e << " ";}cout << endl;
4. 右值引用
4.1.左值和右值
左值(lvalue)
左值是指那些可以出现在赋值操作左侧的表达式。它们通常表示一个对象的身份,即它们在内存中的位置。左值有一个持久的存储位置,并且可以通过取地址操作符
&
获得其地址。大部分变量(包括全局变量、局部变量、静态变量等)都是左值。
// 左值 表示可以取地址的值int i = 0;int j = i;
右值(rvalue)
右值是指那些只能出现在赋值操作右侧的表达式。它们通常表示临时对象或即将被销毁的对象,因此没有持久的存储位置。右值不能通过取地址操作符
&
获取其地址。在C++11之前,右值主要包括字面量、临时对象以及函数调用返回的非引用对象。
// 右值10;int func();i + j;func();
一言以蔽之:只要某一个值能取到地址的就是左值,右值无法取到地址,因为临时变量和将要销毁的变量的地址在当前生命周期即将释放。
4.2.左值和右值引用
// 左值引用int& r_i = i;int& r_j = j;// 10虽然是右值,但是const int&属于左值引用,对常量const int& r_c = 10;// 右值不能进行左值引用// int& r1 = 10;// int& r2 = i + j;// 右值引用,对临时值、常量进行引用int&& rr1 = 10;int&& rr2 = i + j;int&& rr3 = func();// 右值引用不能绑定左值// int&& rr4 = i;// 通过move可以将左值进行右值引用int&& rr4 = move(i);
左值引用的语法是一个&,而右值引用是&&,并且左值引用不能绑定右值,右值引用不能绑定左值,但是我们可以通过move函数实现右值引用绑定左值。
4.3.右值引用和移动语义
在C++11中,引入了右值引用(rvalue reference)的概念,使用
&&
符号表示。右值引用允许我们绑定到右值,从而可以高效地处理临时对象或即将被销毁的对象,避免不必要的拷贝操作。这种技术被称为移动语义(move semantics)。通过移动语义,我们可以将资源(如动态分配的内存、文件句柄等)从一个对象“移动”到另一个对象,而不是复制它们。这通常比复制更快,并且可以避免不必要的资源分配和释放。
那么接下来我们通过两个个场景的学习来体会一下右值引用和移动语义……
首先我们要知道为什么我们可以使用左值引用?左值引用需要可以取得到左值的地址,因为左值的在当前模块的声明周期较长。
int func()
{int i = 10;int& j = i;return i;
}
在这个场景中,我们能修改func的返回类型为int&(左值引用返回值)吗,很显然是不行的,因为i变量在函数模块中可以被引用为j,这时他的生命周期较长。而当我们return时,这个i已经变成了即将销毁的对象(将亡值),所以不能够被引用了,这也就是左值引用无法解决的场景。
那么这时我们修改一下,将这个函数变成右值引用返回,这时这个问题就解决了。
int&& func()
{int i = 10;int& j = i;return move(i);
}
int main()
{int&& rr1 = func();
}
联系一下右值引用和移动语义的作用,会减少不必要的拷贝工作,如图我们发现在func中的i的地址最终移动到了rr1中
但是减少不必要的拷贝没有得到较好的体现, 接着我们用新的场景来体现,代码部分还是从以往博客的综合处获得 重生之C++学习:string的实现-CSDN博客
void test()
{myString::my_string s1;s1 = myString::to_string(1234);for (auto& e : s1){cout << e << " ";}
}
我们写一个测试函数,在这个函数中我们想要实现的逻辑创建字符串对象s1,然后通过to_string函数实现数字转化为字符串形式。接下来我们来研究一下实现这个功能的代码
主要是因为我们当前的my_string这个类中,只支持了左值引用的拷贝构造函数,和=操作符重载,而当我们希望将右值的这个ret通过这两个函数传入給s1时,需要进行拷贝构造出一个临时对象,然后在=给s1,这里就多了一步临时变量的创建和销毁,因此在上图中我们发现ret和s1对应的_str存在两份地址,但是明明表示的就是一个东西。
这里的原因是:编译器会默认将右值通过构造一个新的左值来进行左值引用,所以并不是右值可以作为左值引用类型的参数。所以就会出现这个新的左值可能带来不必要的拷贝,尤其是一些复杂的类型(因为内部可能维护了很多变量)。
接下来我们给这个string支持右值引用,这里利用了编译器会自动选择更加符合的类型的函数进行调用,那么对于右值引用类型就不会去创建一个临时变量,来实现左值引用传入了。那么就减少了这一步临时变量的创建和释放
my_string(my_string&& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
my_string& operator=(my_string&& s)
{ std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);return *this;
}
接着我们再次打开调试窗口 发现 我们支持右值引用后,ret和s1的_str共用一块地址了,也就是地址实现了从一个将要释放的快要结束生命周期的临时变量移动到了另一个对象这个就是移动语义。
所以使用“右值引用”可以帮助我们完成一些左值引用存在缺陷的场景,这些场景往往都是需要额外开辟和释放空间来实现,右值往左值的转化,来间接进行左值的操作,而C++11提供右值引用这个语法来进行右值直接向生命周期长的对象进行转移。