本节分为六部分:
对象使用时调用了哪些方法
函数使用时调用了哪些方法
三条对象优化规则
右值引用
move 移动语义
forward 完美转义
1. 对象使用时调用了哪些方法 C++ 编译器对于对象构造的优化,用临时对象生成新对象的时候,临时对象不产生了,直接构建新对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream> using namespace std;class Test {public : Test (int a = 10 ) :ma (a) { cout << "Test(int)" << endl; } ~Test () { cout << "~Test()" << endl; } Test (const Test& t) :ma (t.ma) { cout << "Test(const Test&)" << endl; } Test& operator =(const Test& t) { cout << "operator=" << endl; ma = t.ma; return *this ; }private : int ma; };int main () { cout << "1.Test t1;" << endl; Test t1; cout << "\n2.Test t2(t1)" << endl; Test t2 (t1) ; cout << "\n3.Test t3 = t1;" << endl; Test t3 = t1; cout << "\n4.Test t4 = Test(20);" << endl; Test t4 = Test (20 ); return 0 ; }
调用赋值函数,因为t4原本已存在
1 2 cout << "\n5. t4 = t2;" << endl; t4 = t2;
显式生成临时对象,临时对象生成后,给 t4 赋值,出语句后,临时对象析构 (默认构造函数,赋值运算符,析构函数)
用临时对象赋值给已存在的对象的时候,要产生临时对象,再调用 operator=
1 2 3 4 cout << "\n6. t4 = Test(20);" << endl; t4 = Test (20 );
构造函数完成类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 cout << "\n7.t4 = (Test)30;" << endl; t4 = (Test)30 ; cout << "\n8.t4 = 40;" << endl; t4 = 40 ;
临时对象生存周期:所在的语句
而引用就是别名,相当于给这块内存又给了个名字,所以用引用来引用临时对象,临时对象的生命周期就变成引用变量的生命周期了。
所以用指针指向临时变量是不安全的,而用引用引用临时对象是安全的。
1 2 3 4 5 6 cout << "\n9. Test *p = &Test(40);" << endl; Test* p = &Test (40 );
1 2 3 4 5 6 cout << "\n10. const Test &ref = Test(50);" << endl;const Test& ref = Test (50 );
程序运行,对象构造顺序以及背后调用总结
注意:
静态局部变量,内存分配是在程序运行之前就分配好的,因为有初值的静态局部变量存储在 .data
区,.data
区的内存是事先就分配好的;但是静态局部变量的初始化(对象的构造)是在运行到它的时候才初始化,.data
区析构的时候是程序结束(main 结束)的时候析构
new 比 malloc 多的:new 不仅分配内存,还构建对象;delete 比 free 多的:delete 不仅释放内存,释放之前先调用析构函数。
对象底层调用代码示例:(注释是构造顺序与底层调用的方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <iostream> using namespace std;class Test {public : Test (int a = 5 , int b = 5 ) :ma (a), mb (b) { cout << "Test(int, int):" << ma << "," << mb << endl; } ~Test () { cout << "~Test():" << ma << "," << mb << endl; } Test (const Test& src) :ma (src.ma), mb (src.mb) { cout << "Test(const Test&)" << endl; } void operator =(const Test& src) { ma = src.ma; mb = src.mb; cout << "operator=" << endl; }private : int ma; int mb; };Test t1 (10 , 10 ) ;int main () { cout << "\n---------------------------main()" << endl; cout << "\n 1.Test t2(20,20); " << endl; Test t2 (0 , 0 ) ; cout << "\n 2.Test t3 = t2" << endl; Test t3 = t2; cout << "\n 3.Test t3 = t2" << endl; static Test t4 = Test (30 , 30 ); cout << "\n 4 t2 = (Test)(50, 50);" << endl; t2 = (Test)(50 , 50 ); cout << "\n 5 Test* p1 = new Test(70, 70);" << endl; Test* p1 = new Test (70 , 70 ); cout << "\n 6 Test* p2 = new Test[2];" << endl; Test* p2 = new Test[2 ]; cout << "\n 7 Test* p3 = &Test(80, 80);" << endl; Test* p3 = &Test (80 , 80 ); cout << "\n 8 const Test& p4 = Test(90, 90);" << endl; const Test& p4 = Test (90 , 90 ); delete p1; delete []p2; cout << "\n---------------------------finish" << endl; return 0 ; }Test t5 (100 , 100 ) ;
2. 函数使用时调用了哪些方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std; class Test {public : Test (int data = 10 ) :ma (data) { cout << "Test(int):" << ma << endl; } ~Test () { cout << "~Test()" << ma << endl; } Test (const Test &t) :ma (t.ma) { cout << "Test(const Test&)" << endl; } void operator =(const Test &t) { cout << "operator=" << endl; ma = t.ma; } int getData () const { return ma; }private : int ma; }; Test GetObject (Test t) { cout << "\n----------------GetObject1 \n" ; int val = t.getData (); Test tmp (val) ; cout << "\n----------------GetObject2 \n" ; return tmp; }int main () { Test t1; Test t2; cout << "\n----------------main GetObject1 \n" ; t2 = GetObject (t1); cout << "\n----------------main GetObject2 \n" ; return 0 ; }
(1)实参传递给形参:调用 Test(const Test&)
拿 t1 拷贝构造形参 t。 (2)调用 Test(int)
的构造,构造 tmp 对象。 (3)return tmp;
tmp 和 t2 是两个不同函数栈帧上的对象,是不能直接进行赋值的 GetObject 函数完成调用时 tmp 对象作为局部对象就析构 ,为了把返回值带出来, 在 return tmp;
这里,首先要 在main 函数栈帧上构建一个临时对象,目的就是把 tmp 对象带出来。 (4)调用 Test(const Test&)
,tmp 拷贝构造 main 函数栈帧上的临时对象 (5)出 GetObject 作用域,tmp 析构。 (6)形参 t 对象析构。 (7)operator =
,把 main 函数刚才构建的临时对象赋值给 t2,临时对象没名字,出了语句就要析构。 (8)把 main 函数刚才构建的临时对象析构。 (9)main 函数结束,t2 析构。 (10)t1 析构。
短短的代码调用了11个函数,可以优化。
3. 三条对象优化规则
函数参数传递过程中,对象优先按引用传递,这样可以省去一个形参t的拷贝构造调用,形参没有构建新的对象,出作用域也不用析构了,所以不要按值传!
函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
优化 1:没有 t1 的拷贝构造,形参 t 没有新的对象,出作用域也不用析构。
省去了形参t的拷贝构造和形参t的析构
优化 2:函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
优化 3:接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收
函数返回值临时对象给 t2 初始化!用这个临时对象拷贝构造同类型的新对象 t2。C++ 编译器会进行优化,这个 main 函数栈帧上的临时对象都不产生了,直接构造 t2 对象。也就是 return Test(val);
直接构造 t2 对象了 。
Test t2= GetObject(t1);
在汇编上,除了把 t1 的地址传进去,还把 t2 的地址也传进去了,也压到函数栈帧上,所以 return Test(val);
就可以取到 t2 的地址,就知道在哪块内存上构造一个名为 t2 的对象。
4. 右值引用 如果有的应用场景必须返回的是定义过的对象,也必须按赋值的方式来接收函数调用,那优化的后两条规则就用不成了。
解决办法:
给该类添加一个右值引用拷贝构造函数,函数内部不做资源的分配,而是资源的转移,每当有通过右值(临时对象)来构建对象的时候,就调用右值引用拷贝构造函数。
1. 详解
左值:有名字或有内存
右值:没名字(临时量)或没内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main () { int a = 10 ; int &b = a; int &c = 10 ; const int &c = 10 ; int && d = 10 ; }
记住两句话:
常量、数字、临时量、函数返回值 都是右值,要引用它们就要用右值引用&&,将亡值也属于右值
一个右值引用的变量,本身是一个左值
2. 提高效率
我直接指向你的资源,再把你的指针置为空,你的资源相当于移动给我了
下图中 tmpStr 匹到的就是右值引用的拷贝构造,因为函数返回值属于右值。
CMyString 的重载加号运算符函数
3. 给容器里拷贝构造对象(笔试题) vector 提供了左值引用与右值引用的拷贝构造函数,传的是左值就调用左值引用的拷贝构造函数,传的是右值,就调用右值引用的拷贝构造函数。
5. move 移动语义 move:移动语义,将 val 的类型强转右值引用类型继而可以通过右值引用使用该值,以用于移动语义。
std::move
源码:_Ty
是未定的引用类型,remove_reference_t
用于移除_Ty
的引用类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <vector> using namespace std;class A {public : A (int data = 10 ) :ptr (new int (data)) { cout << "A()" << endl; } ~A () { delete ptr; ptr = nullptr ; cout << "~A()" << endl; } A (const A& src) { cout << "A(const A&)" << endl; ptr = new int (*src.ptr); } A (A&& src) { cout << "A(A&&)" << endl; ptr = src.ptr; src.ptr = nullptr ; }private : int * ptr; };int main () { vector<A> vec; vec.reserve (10 ); cout << "--------------------begin" << endl; for (int i = 0 ; i < 2 ; ++i) { A a (i) ; vec.push_back (a); } cout << "--------------------endl" << endl; return 1 ; }
每次循环都需要首先构造 A,调用 A 的默认构造函数,然后 调用左值引用的拷贝构造函数,看上面的代码,A a(i)
在 for 循环中其实算是局部对象,在 vec.push_back(a)
完成后,a 对象调用析构函数。
在 vec.push_back(a)
时,应该把对象 a 的资源直接移动给 vector 容器底层的对象,也就是调用右值引用参数的拷贝构造函数,怎么做到呢?这时候就用到了带移动语义的 std::move
函数,main 函数代码修改如下:
1 2 3 4 5 6 7 cout << "--------------------begin" << endl;for (int i = 0 ; i < 2 ; ++i){ A a (i) ; vec.push_back (std::move (a)); } cout << "--------------------endl" << endl;
1 2 3 4 _EXPORT_STD template <class _Ty >_NODISCARD _MSVC_INTRINSIC constexpr remove_reference_t <_Ty>&& move (_Ty&& _Arg) noexcept { return static_cast <remove_reference_t <_Ty>&&>(_Arg); }
首先,函数参数 T&&
是一个指向模板类型参数的右值引用,通过引用折叠,此参数可以与任何类型的实参匹配(可以传递左值或右值,这是 std::move
主要使用的两种场景)。关于引用折叠如下:
公式一. X& &
、X&& &
、X& &&
都折叠成 X&
,用于处理左值。
1 2 3 4 5 6 7 8 9 10 string s ("hello" ) ; std::move (s) => std::move (string& &&) => 折叠后 std::move (string& ) 此时:T的类型为string&typename remove_reference<T>::type为string 整个std::move被实例化如下 string&& move (string& t) { return static_cast <string&&>(t); }
公式二、X&& &&
折叠成 X&&
,用于处理右值。
1 2 3 4 5 6 7 8 std::move (string ("hello" )) => std::move (string&&) string&& move (string&& t) { return static_cast <string&&>(t); }
简单来说,右值经过 T&&
传递类型保持不变还是右值,而左值经过 T&&
变为普通的左值引用
remove_reference
是通过类模板的部分特例化进行实现的,其实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 template <typename T> struct remove_reference { typedef T type; }; template <class T > struct remove_reference <T&> { typedef T type; } template <class T > struct remove_reference <T&&> { typedef T type; } int i; remove_refrence<decltype (42 )>::type a; remove_refrence<decltype (i)>::type b; remove_refrence<decltype (std::move (i))>::type b;
std::move
实现:
首先,通过右值引用传递模板实现,利用引用折叠原理将右值经过 T&&
传递类型保持不变还是右值,而左值经过 T&&
变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变。然后我们通过 static_cast<>
进行强制类型转换返回 T&&
右值引用,而 static_cast
之所以能使用类型转换,是通过 remove_refrence::type
模板移除 T&&
,T&
的引用,获取具体类型 T。
std::move
函数可以以非常简单的方式将左值引用转换为右值引用
C++ 标准库使用比如 vector::push_back
等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数 push_back
进去就行了,通 std::move
,可以避免不必要的拷贝操作。
std::move
是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
对指针类型的标准库对象并不需要这么做.
使用 std::move
后,左值的内容将会被转移,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> #include <utility> #include <vector> #include <string> int main () { std::string str = "Hello" ; std::vector<std::string> v; v.push_back (str); std::cout << "After copy, str is \"" << str << "\"\n" ; v.push_back (std::move (str)); std::cout << "After move, str is \"" << str << "\"\n" ; std::cout << "The contents of the vector are \"" << v[0 ] << "\", \"" << v[1 ] << "\"\n" ; }
6. forward 完美转义 std::forward
通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。一个经典的完美转发的场景是
1 2 3 4 template <class ... Args>void forward (Args&&... args) { f (std::forward<Args>(args)...); }
需要注意的有 2 点:
输入参数的类型是 Args&&...
, && 的作用是引用折叠
std::forward
的模板参数必须是 <Args>
,而不能是 <Args...>
,这是由于我们不能对 Args 进行解包之后传递给 std::forward
,而解包的过程必须在调用 std::forward
之后.
其实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 template <class _Ty> _NODISCARD constexpr _Ty&& forward (remove_reference_t <_Ty>& _Arg) noexcept { return (static_cast <_Ty&&>(_Arg)); } template <class _Ty> _NODISCARD constexpr _Ty&& forward (remove_reference_t <_Ty>&& _Arg) noexcept { static_assert (!is_lvalue_reference_v<_Ty>, "bad forward call" ); return (static_cast <_Ty&&>(_Arg)); }
std::remove_reference_t
是一个模板类的类型别名,用于去掉 T 的引用属性。
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> #include <utility> #include <vector> #include <string> struct A { int value; A (int value=0 ) : value (value) { std::cout << "construct" << std::endl; } A (const A&a) : value (a.value) { std::cout << "A(const A&a):" << a.value << std::endl; } A (const A&&a) : value (a.value) { std::cout << "A(const A&&a):" << a.value << std::endl; } ~A () { std::cout << "deconstruct" << std::endl; } }; void test (A&& a, double b) { std::cout << "完美转发 右值引用: " << a.value << " " << b << std::endl; } void test (A& a, double b) { std::cout << "完美转发 左值引用: " << a.value << " " << b << std::endl; } template <class ... Args>void test_forward (Args&&... args) { test (std::forward<Args>(args)...); } int main () { A a (1 ) ; float b = 2.1 ; test_forward (a, b); test_forward (std::move (a), b); return 0 ; }
test_forward
第一个参数通过 forward 完美转发到 void test(A& a, double b)
以及 void test(A&& a, double b);
首先传入左值 test_forward(a,b)
-> 调用 void test(A& a, double b)
。
之后传入传入左值 test_forward(std::move(a),b)
->调用 void test(A&& a, double b)
。