08.C++ 优化

本节分为六部分:

  1. 对象使用时调用了哪些方法
  2. 函数使用时调用了哪些方法
  3. 三条对象优化规则
  4. 右值引用
  5. move 移动语义
  6. 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;
/*
C++ 编译器对于对象构造的优化:用临时对象生成新对象的时候,临时对象
就不产生了,直接构造新对象就可以了
*/
// 和Test t4(20);没有区别的! 仅调用一次默认构造函数
Test t4 = Test(20);

return 0;
}

image.png

调用赋值函数,因为t4原本已存在

1
2
cout << "\n5. t4 = t2;" << endl;
t4 = t2;

image.png

显式生成临时对象,临时对象生成后,给 t4 赋值,出语句后,临时对象析构 (默认构造函数,赋值运算符,析构函数)

用临时对象赋值给已存在的对象的时候,要产生临时对象,再调用 operator=

1
2
3
4
cout << "\n6. t4 = Test(20);" << endl;
// 显式生成临时对象,临时对象生成后
// 给t4赋值,出语句后,临时对象析构 (默认构造函数,赋值运算符,析构函数)
t4 = Test(20);

image.png

构造函数完成类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 cout << "\n7.t4 = (Test)30;" << endl;
/************************************************************************/
/*
把其他类型转成类类型的时候,编译器就看这个类的类型
有没有合适的构造函数 把整型转成 Test,就看这个类的类型有没有
带 int 类型参数的构造函数 ,有,就可以显式生成临时对象,然后
赋值给 t4 出语句后,临时对象析构
*//************************************************************************/
// 把 30 强转成 Test 类型 int->Test(int) 同 t4 = Test(20);
t4 = (Test)30;

cout << "\n8.t4 = 40;" << endl;
// 隐式生成临时对象,然后赋值给t4,出语句后,临时对象析构 同7
t4 = 40;

image.png

临时对象生存周期:所在的语句

而引用就是别名,相当于给这块内存又给了个名字,所以用引用来引用临时对象,临时对象的生命周期就变成引用变量的生命周期了。

所以用指针指向临时变量是不安全的,而用引用引用临时对象是安全的。

1
2
3
4
5
6
cout << "\n9. Test *p = &Test(40);" << endl;
// 指针指向临时对象,这个临时对象肯定是要生成的
Test* p = &Test(40);
// 然后p指向这个临时对象的地址
// 出语句后,临时对象析构
// 此时p指向的是一个已经析构的临时对象,p相当于野指针了
1
2
3
4
5
6
cout << "\n10. const Test &ref = Test(50);" << endl;
// 引用一个临时对象,这个临时对象也是要生成的
const Test& ref = Test(50);
// 出语句后,临时对象不析构,因为引用相当于是别名,临时对象出语句析构是因为没有名字
// 用引用变量引用临时对象是安全的,临时对象就是有名字了,临时对象的生存周期就变成引用变量的
// 生存周期了。引用变量是这个函数的局部变量,return完,这个临时对象才析构

image.png

程序运行,对象构造顺序以及背后调用总结

  • 先全局
  • 再进入 main

注意:

  • 静态局部变量,内存分配是在程序运行之前就分配好的,因为有初值的静态局部变量存储在 .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:
//因为a,b有默认值所以构造有3种方式:
//Test() Test(10) Test(10, 10)
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;
};

// 在mian函数之前构造,在main函数结束后释放,存储在.data段
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;
// (50,50)是逗号表达式,(表达式1,表达式2,表达式n)
// (50,50)的最后的结果是最后一个表达式n的结果 50
// (50, 50) = (Test)50;
t2 = (Test)(50, 50); // Test(int,int) operator= 出语句调用~Test()

cout << "\n 5 Test* p1 = new Test(70, 70);" << endl;
Test* p1 = new Test(70, 70); // Test(int,int) 要调用delete才析构对象
cout << "\n 6 Test* p2 = new Test[2];" << endl;
Test* p2 = new Test[2]; // Test(int,int) Test(int,int) 要调用delete才析构对象
cout << "\n 7 Test* p3 = &Test(80, 80);" << endl;
Test* p3 = &Test(80, 80); // Test(int,int) 出语句调用~Test()
cout << "\n 8 const Test& p4 = Test(90, 90);" << endl;
const Test& p4 = Test(90, 90); // Test(int,int)

// ~Test()
delete p1;
// ~Test() ~Test()
delete[]p2;

cout << "\n---------------------------finish" << endl;
return 0;
}

Test t5(100, 100);//Test(int, int)

image.png

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:
//有默认值,可以有2种构造方式:Test() Test(20)
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;//1、调用带整型参数的构造函数
Test t2;//2、调用带整型参数的构造函数

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 析构。

image.png

短短的代码调用了11个函数,可以优化。

3. 三条对象优化规则

  1. 函数参数传递过程中,对象优先按引用传递,这样可以省去一个形参t的拷贝构造调用,形参没有构建新的对象,出作用域也不用析构了,所以不要按值传!
  2. 函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象
  3. 接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收

优化 1:没有 t1 的拷贝构造,形参 t 没有新的对象,出作用域也不用析构。

省去了形参t的拷贝构造和形参t的析构

image.png

 优化 2:函数返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象

image.png

优化 3:接收返回值是对象的函数调用的时候,优先按初始化的方式接收,不要按赋值的方式接收

image.png

函数返回值临时对象给 t2 初始化!用这个临时对象拷贝构造同类型的新对象 t2。C++ 编译器会进行优化,这个 main 函数栈帧上的临时对象都不产生了,直接构造 t2 对象。也就是 return Test(val); 直接构造 t2 对象了

Test t2= GetObject(t1); 在汇编上,除了把 t1 的地址传进去,还把 t2 的地址也传进去了,也压到函数栈帧上,所以 return Test(val); 就可以取到 t2 的地址,就知道在哪块内存上构造一个名为 t2 的对象。

image.png

4. 右值引用

如果有的应用场景必须返回的是定义过的对象,也必须按赋值的方式来接收函数调用,那优化的后两条规则就用不成了。

image.png

解决办法:

  • 给该类添加一个右值引用拷贝构造函数,函数内部不做资源的分配,而是资源的转移,每当有通过右值(临时对象)来构建对象的时候,就调用右值引用拷贝构造函数。

1. 详解

  • 左值:有名字或有内存
  • 右值:没名字(临时量)或没内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main()
{
int a = 10;
int &b = a; // ok
int &c = 10; //error, 10是纯右值,不能拿普通引用引用它,可以拿常引用引用它

const int &c = 10; //ok,因为const做了这两件事:int tmp = 10; const int &c= 10;

int && d = 10; //ok,右值引用,可以把一个右值绑定到右值引用上,底层汇编指令类似于int tmp = 10; int &&d = tmp;

//通过const方式不能改右值的值,通过&&右值引用是可以改右值的值的

//一个右值引用的变量,本身是一个左值,如int && d = 10; d本身 是左值,类型是int,变量值是10,所以不能int &&f =d;
}

记住两句话:

  • 常量、数字、临时量、函数返回值都是右值,要引用它们就要用右值引用&&,将亡值也属于右值
  • 一个右值引用的变量,本身是一个左值

2. 提高效率

image.png

我直接指向你的资源,再把你的指针置为空,你的资源相当于移动给我了

下图中 tmpStr 匹到的就是右值引用的拷贝构造,因为函数返回值属于右值。

image.png

CMyString 的重载加号运算符函数

image.png

3. 给容器里拷贝构造对象(笔试题)

vector 提供了左值引用与右值引用的拷贝构造函数,传的是左值就调用左值引用的拷贝构造函数,传的是右值,就调用右值引用的拷贝构造函数。

image.png

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);
/*
这里a是一个左值,因此vec.push_back(a)会调用左值的
拷贝构造函数,用a拷贝构造vector底层数组中的对象
*/
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;

image.png

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) //t为左值,移动后不能在使用t
{
//通过static_cast将string&强制转换为string&&
return static_cast<string&&>(t);
}

公式二、X&& && 折叠成 X&&,用于处理右值。

1
2
3
4
5
6
7
8
std::move(string("hello")) => std::move(string&&)
//此时:T的类型为string
// remove_reference<T>::type为string
//整个std::move被实例如下
string&& move(string&& t) //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; //定义T的类型别名为type
};

//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }

template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }

//举例如下,下列定义的a、b、c三个变量都是int类型
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
//摘自https://zh.cppreference.com/w/cpp/utility/move
#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";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
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";
}

image.png

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
	// FUNCTION TEMPLATE forward
template<class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return (static_cast<_Ty&&>(_Arg));
}

template<class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept
{ // forward an rvalue as an rvalue
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)

image.png


08.C++ 优化
http://example.com/2023/08/17/03.C++进阶部分/08.C++ 优化/
Author
Yakumo
Posted on
August 17, 2023
Licensed under