目录
新增容器
编辑
新增语法
变量类型推导
auto
存储类型
分类
自动存储类型
静态存储类型
寄存器存储类型
外部链接存储类型
decltype
typeid(c++98)
type_info
{ }初始化
initializer_list
介绍
使用
模拟实现
nullptr
final与override
范围for
右值引用
引入
介绍
左值引用
右值引用
纯右值
将亡值
move函数
用途
引入(左值引用返回的缺陷)
介绍
移动
介绍
移动构造函数
使用
移动赋值函数
使用
新增容器
框起来的都是c++11中新增的容器
- 其中哈希系列的set和map都很有用
- 而array对标的是静态数组,但是比起来只是多了个下标检测,很鸡肋
- forward_list其实就是单链表,但也没啥用,指定位置的插入/删除都只能操作给定位置的后一个结点
新增语法
变量类型推导
auto
存储类型
用来描述变量的存储、生命周期和作用域的属性
分类
自动存储类型,静态存储类型,寄存器存储类型和外部链接存储类型
自动存储类型
- 是局部变量的默认存储类别
- 这些变量在函数或块内部声明,它们的生命周期与包含它们的函数或块的执行周期相关联
- 当函数或块退出时,自动存储变量会被销毁
静态存储类型
- 在程序的生命周期内都存在,而不仅仅在其声明的作用域内
- 它们只被初始化一次
寄存器存储类型
- 用于请求编译器将变量存储在CPU的寄存器中,以加速对其的访问
- 关键字是register
- 但现代编译器通常会智能地管理寄存器,所以这个类型一般不再使用了
外部链接存储类型
- 这些变量可以在不同的文件中访问,它们通常被用于全局变量,以在不同的源文件之间共享数据
- 关键字是extern
但是 [局部域中定义局部的变量] 默认就是自动存储类型,所以auto就没啥用了
但c++11对auto进行了改变,让他可以根据初始化值的类型自动推导变量类型
注意:auto仅能通过初始值推导类型,而不能直接作为类型定义对象,所以传参/没有初始值的情况下,不能使用auto
decltype
delcltype的用处就体现了出来,auto不能做的,他可以做
他可以根据[decltype( 变量 )中变量的类型]作为一种类型,来声明/定义变量
int main() { const int x = 1; double y = 2.2; decltype(x * y) ret; // ret的类型是double decltype(&x) p; // p的类型是int* return 0; }
typeid(c++98)
- typeid是c++98引入的,但鉴于这里都是讲类型的,所以也介绍一下
- 它可以用来确定一个对象或表达式的实际运行时类型
- typeid返回一个type_info对象的引用,该对象包含有关表达式的类型信息
- 头文件:<typeinfo>
type_info
- 用于获取和比较对象的类型信息
{ }初始化
也被称为统一初始化/列表初始化
原先,只有数组可以使用{ }进行初始化
但c++11进行了扩展,让所有的内置类型和自定义的类型都可以使用{ }进行初始化 :
int x = {42}; double y{3.14}; //加不加=都可以 int arr[] = {1, 2, 3, 4}; struct Point { int x; int y; }; Point p = {10, 20}; int* pa = new int[4]{ 0 }; //定义自定义对象,会调用其构造函数 Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 };
上面除了数组外,传入的都是[构造函数参数]个数的参数,也就是每次插入一个元素
而{ }初始化通过某个新类的支持,可以实现下面的操作(传入多个元素到容器中):
std::vector<int> vec = {1, 2, 3, 4};
而这个新类就是下面要介绍的initializer_list
initializer_list
介绍
是 C++11 标准引入的一种容器,用于初始化容器类对象或用户自定义类型的对象
允许以初始化列表的形式传递多个值给对象的构造函数
使用
- std::initializer_list作为参数的构造函数,方便初始化容器对象
- 也可以作为operator=的参数,这样就可以用大括号赋值
auto il = { 10, 20, 30 }; cout << typeid(il).name() << endl;
模拟实现
比如说,为vector增加初始化列表的构造和赋值重载
myvector(initializer_list<T>& l) : _start(nullptr), _finish(nullptr), _endOfStorage(nullptr) { reserve(l.size()); for(auto& it: l){ push_back(it); } } myvector<T> &operator=(initializer_list<T>& l) // v传进来首先会进行拷贝构造,所以形参和实参空间不同 { myvector<T> tmp(l); swap(tmp); return *this; }
这样它也能实现c++11新增的操作
nullptr
是 c++11 标准引入的关键字,用于表示空指针
- 在之前都是用NULL来表示空指针,但在之前,NULL在c++实际上是整型值0
- 普通使用的时候,确实可以正常的被类型转换
- 但如果遇到函数重载(一个是指针类型,一个是int类型),传入NULL的话,就会匹配int那个函数,不符合我们的预期
- 所以c++11引入了一个新的关键字
final与override
这两个关键字实际上在多态那里已经介绍过了
范围for
范围for我们都快用烂了,非常好用的一个语法糖
底层就是调用了迭代器
右值引用
引入
在之前,我们就已经接触了引用的概念,但c++进一步增加了右值引用的概念,所以之前使用的引用就被叫做左值引用了
但无论左值引用还是右值引用,都是给对象取别名,只不过对象类型不同
介绍
左值引用
- 左值是一个表示数据的表达式,我们可以获取它的地址+可以对它赋值
int* p = new int(0); int b = 1; const int c = 2;
- 赋值符号左边只能是左值
- 定义时const修饰符后的左值,既可以引用左值,也可以引用右值
- 左值引用就是给左值的引用,给左值取别名
int*& rp = p; int& rb = b; const int& rc = c; int& pvalue = *p;
右值引用
右值分为纯右值和将亡值(将亡值实际上也属于左值)
纯右值
是字面常量/表达式返回值/匿名的临时对象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);
move函数
- 是 C++ 标准库中的一个函数模板,用于将左值转换为右值引用
- 所以右值引用也可以引用move后的左值
用途
引入(左值引用返回的缺陷)
既然c++11引入了右值引用,说明之前只有左值引用时存在一些缺陷
众所周知,用引用作为参数,可以减少拷贝次数,尤其是深拷贝,对需要深拷贝的提升的效率更大
但是,如果涉及到临时变量,是绝对不能使用左值引用的:
- (在之前引用那里就有介绍过,不能传临时变量的引用作为返回值,也不能使用引用接收返回值)
- 所以,一般这里,就不会使用传引用返回,而是传值返回,以及用值接收返回值
- 但就又会出现拷贝的问题
- 因为传参会先创建一个临时变量,再将临时变量拷贝给ret2,这就会出现2次深拷贝,代价很大
- 当然,新一点的编译器会做出优化,会将返回值直接拷贝给接收变量,但依然会有1次深拷贝
介绍
但是,利用移动语义可以补上这块短板
移动语义的核心就是通过使用右值引用和移动构造函数来实现资源的有效转移
移动
介绍
在C++中,"移动" 是指将资源(如动态分配的内存、文件句柄、对象等)的所有权从一个对象转移到另一个对象,而不进行不必要的数据复制
移动构造函数
接受一个右值引用参数,用于实现资源的移动
在移动构造函数内部,资源的所有权被转移到新对象,同时原对象进入有效但未定义的状态,以确保原对象不再持有资源,防止原对象被析构后出现问题
void swap(myvector<T> &v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_endOfStorage, v._endOfStorage); } myvector(myvector<T> &&v) : _start(nullptr), _finish(nullptr), _endOfStorage(nullptr) { cout << "移动构造" << endl; swap(v); }
防止出现下面情况时,发生不必要的拷贝:
使用
bit::myvector<int> func_move() { cout << "func_move" << endl; auto it = {1, 2, 3, 4}; myvector<int> tmp(it); return tmp; } void test7() { bit::myvector<int> s1(func_move()); }
- 深拷贝是构造tmp时候的
- 本身应该是先深拷贝一个临时对象
- 然后这个临时对象作为函数返回值(它是将亡值,当这个返回值完成拷贝工作后,就没了),调用移动构造初始化ret1
- 但编译器做出了优化,跳过那个中间状态,直接对str进行移动构造,也就是直接将str识别成将亡值
- 将tmp的资源直接给s1,使tmp成为有效但未定义的状态,之后随着func_move的结束而销毁
移动赋值函数
myvector<T> &operator=(myvector<T>&& v) { cout << "移动赋值" << endl; swap(v); return *this; }
和移动拷贝非常像,都是转移资源
使用
bit::myvector<int> func_move() { cout << "func_move" << endl; auto it = { 1, 2, 3, 4 }; bit::myvector<int> tmp(it); //深拷贝 return tmp; } void test8() { bit::myvector<int> s2; s2 = func_move(); }
- 这里因为s2的赋值和定义不在一行,所以没有优化
- 因为要返回tmp赋值给s2,所以要先创建临时变量作为函数返回值
- 但和上面一样,直接将tmp认为是将亡值,且有移动构造,所以使用移动构造构建临时对象
- 然后将这个临时对象使用[=重载]赋值给s2
- 而因为临时对象也是将亡值(匿名对象,且马上就要被销毁) ,且有移动赋值的存在,所以直接调用移动版本的,把临时对象的资源交给s2