目录
第一节:多参数的隐式类型转化
第二节:自动推导类型
2-1.decltype
2-2.typeid
第三节:其他内容
3-1.nullptr
3-2.范围for
3-3.智能指针
3-4.STL中的新容器
第四节:右值引用
4-1.什么是右值
4-2.右值引用
4-3.右值引用的意义
4-3-1.类构造
4-3-2.访问临时对象内存空间
4-3-3.延长生命周期
4-4.总结
第五节:函数模板的万能引用
5-1.万能引用
5-2.完美转发
下期预告:
以下内容不考虑编译器的优化。
第一节:多参数的隐式类型转化
C++11允许多参数的隐式类型转化:
class A a = {1,2};
它实际上先进行了一次构造,创建了一个临时对象,然后使用拷贝构造/移动构造:
其次,"="可以省略:
class A a{1,2}; // 等价于class A a = {1,2};
内置类型也可以使用{}进行初始化:
int a = {1}; //int a{1};
上述进行初始化的{}实际上也是一个类 :std::initializer_list<T>,这个类有两个成员指针。一个指向常量数组的第一个元素,一个指向最后一个元素的后一个位置,支持迭代器。
std::initializer_list<T>可以给多种容器赋值,这是因为它们都有使用std::initializer_list<T>作为参数的构造函数,例如vector:
_CONSTEXPR20 vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc()): _Mypair(_One_then_variadic_args_t{}, _Al) {_Construct_n(_Convert_size<size_type>(_Ilist.size()), _Ilist.begin(), _Ilist.end());}
所以std::vector<int> v = {1,2,3};实际上是先使用{1,2,3}构造出了一个匿名的std::vector<int>({1,2,3})最后给 v 进行构造。
当然也可以直接使用std::initializer_list<T>进行构造,不产生std::vector的匿名对象:
std::vector<int> v({1,2,3});
隐式类型转化也允许嵌套:
第二节:自动推导类型
2-1.decltype
使用这个操作符可以获得变量类型,可以用来创建相同类型的变量,也可以作为模板参数:
#include <iostream>
#include <vector>
using namespace std;
int main()
{int a = 0;decltype(a) b = 1;std::vector<decltype(a)> v;v.push_back(a);v.push_back(b);return 0;
}
2-2.typeid
这个操作符可以得到变量的类型名,以字符串的形式返回:
int main() {int a = 0;std::vector<int> v;printf("%s\n", typeid(a).name());printf("%s\n", typeid(v).name());return 0; }
第三节:其他内容
3-1.nullptr
C++11引入nullptr代替NULL,因为NULL的字面量是0(int),在函数可以重载的C++中会发生错配的问题:
void Func(void* ptr) {std::cout << "传入的是指针" << std::endl; }void Func(int num) {std::cout << "传入的是数字" << std::endl; }int main() {Func(NULL);return 0; }
nullptr本质上是 (void*)0,类型是指针,就不会发生这种问题:
3-2.范围for
范围for就是使用容器的迭代器来遍历容器,这里不再赘述
3-3.智能指针
因为C++引入了new操作,这就让向堆区申请空间变得方便了很多,但是也经常忘记释放不用的指针,造成内存泄漏问题:
int main() {if (true){int* ptr = new int(1);// 其他操作// 忘记释放内存了// ptr生命周期结束}// 外面访问不到ptr,也无法释放内存// 内存泄漏了return 0; }
为了解决这种问题,C++又引入了智能指针,智能指针实际上是一个类,它是对指针的封装,包含在头文件<memory>中。
智能指针作为一个类,出作用域时生命周期结束,会调用析构函数,而析构函数中就有对指针的delete操作,释放内存:
int main() {if (true){std::shared_ptr<int> ptr(new int(1));// 其他操作// 忘记释放内存了// ptr析构,释放内存}return 0; }
3-4.STL中的新容器
主要是unordered_set和unordered_map。
还有array 静态数组类,与C语言的数组相比,它有着更完备的越界检查功能,但是不能扩容。
第四节:右值引用
4-1.什么是右值
简单来区分,右值就是不能被取地址的数据,左值就是可以被取地址的数据。
右值包括:字面常量、匿名对象、临时对象、表达式返回值。
字面常量:&10;
匿名对象:&(class A(1,0));
临时对象:&(std::to_string(123));
表达式返回值:&(x+y);
以上操作都是不被允许的。
不能取地址的原因是右值的生命周期只在当前语句(将亡值),取地址是无意义的。而且它们都具有一定的常性,本身是无法修改的。
可以把右值理解为只可以读取,不能修改的数据。
需要注意:左值不一定就是一个值,也可以是一个表达式:
int *p = a; &(*p); // *p是一个表达式,可以取地址
4-2.右值引用
左值引用是使用&给左值取别名,之前一直在使用:
int a = 0; int& b = a;
右值引用自然是给右值取别名,它使用&&:
int x = 3,y = 5; int&& ref = (x+y);
也可以使用const+左值引用:
int x = 3,y = 5; const int& ref = (x+y);
需要注意的是,右值引用 ref 本身是个左值,因为这样才能转移和修改数据。
右值引用的不能直接给左值取别名,但是可以给 move 以后的左值取别名:
int main() {int x = 3, y = 5;int&& ref = move(x);ref = 0;std::cout << x << std::endl;return 0; }
此时 ref 是 x 的别名,对它的改变就是对 x 的改变:
4-3.右值引用的意义
4-3-1.类构造
右值引用在类的构造的效率上有重大意义,之前构造类的时候,很多情况都会通过匿名对象、临时对象进行拷贝构造,然后匿名/临时对象的出作用域之后,它们的数据就不再使用了。
那么能不能免去这个拷贝的过程,直接使用匿名/临时对象的数据呢?
重载一个右值对象为参数的构造函数即可:
class A { public:A(A&& t){void* tmp = ptr;ptr = t.ptr;t.ptr = tmp;}void* ptr; // 指向数据的指针 };
这样当匿名/临时对象管理的数据就由新构造的类管理起来了,流程如下:
省去了拷贝的过程。
这种使用右值的构造又叫移动构造。
4-3-2.访问临时对象内存空间
使用右值引用给右值取别名后,就可以访问右值的内存空间了:
class A { public:int a; }; int main() {A&& ref = A();ref.a = 2;std::cout << ref.a << std::endl;return 0; }
4-3-3.延长生命周期
将亡值的生命周期本来只在当前语句,因为它是无人管理的内存,出语句之后也不会在访问,系统就会在出语句之后自动回收内存空间,生命周期也就结束了。
但是如果给右值取别名,系统就会判断这块内存是有人管理的,也就不会出语句就回收内存了:
class A { public:~A(){std::cout << "A析构函数" << std::endl;} }; class B { public:~B(){std::cout << "B析构函数" << std::endl;} }; int main() {A&& ref = A(); // 有别名的类AB(); // 无别名的类Bstd::cout << "出语句了!\n";return 0; }
可见,B在出语句前就析构了,A在程序结束时才析构
4-4.总结
实际上,有没有名字也是左值和右值的本质差异,左值天生有一个名字,右值天生没有名字,给右值取别名之后,实际上它们就没有区别了,只是名字的类型区别(左值引用、右值引用)。
左值引用,右值引用就是给类的函数一个区别处理的分类而已,如果参数是左值引用,我就认为你是之后要用的,我就不修改你的数据,而是拷贝你的数据;参数是右值引用,我就认为你之后不再使用了,直接把你的数据拿过来用就行了。
4-2说到的 move 本质是给了内存一个右值引用,然后把这个引用保存起来,如果此时有一个类使用 ref 进行构造,就会调用移动构造,管理x的空间;如果使用 x 进行构造,就会调用拷贝构造,自己开辟一块空间进行管理。
第五节:函数模板的万能引用
5-1.万能引用
在函数模板的类型前添加 && 表示万能引用,它只能接收左值、右值、const左值、const右值:
template<class> void Func1(T&& t) {//... }
传入右值时,t就是右值引用;传入左值时,t就是左值引用。
5-2.完美转发
上述 t 在传入右值时是右值引用,前面说过右值引用本身是一个左值,所以当另一个函数接收 t 时,t就会被识别成左值,那么左值、右值就无法分开处理了:
void Func2(int& t) {std::cout << "收到左值引用" << std::endl; } void Func2(int&& t) {std::cout << "收到右值引用" << std::endl; }template<class T> void Func1(T&& t) {Func2(t); }int main() {int x = 0;Func1(move(x));Func1(x);return 0; }
使用完美转发解决这个问题:
template<class T> void Func1(T&& t) {Func2(std::forward<T>(t)); // 完美转发 }
也可以使用强转,强调 t 的类型:
template<class T> void Func1(T&& t) {Func2((T&&)t); };
下期预告:
讲完左值、右值,接下来是lambda表达式和C++11引入的类的新功能。