C++ 巩固进阶之一

大纲

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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <iostream>

using namespace std;

class Test {

public:
Test(int a = 10) : _a(a) {
cout << "Test(int)" << endl;
}

~Test() {
cout << "~Test()" << endl;
}

Test(const Test &t) : _a(t._a) {
cout << "Test(const Test& t)" << endl;
}

Test &operator=(const Test &t) {
cout << "operator=(const Test& t)" << endl;
this->_a = t._a;
return *this;
}

private:
int _a;
};

void test01() {
cout << "============ test01() ============" << endl;

// 调用普通构造函数
Test t1;

// 调用拷贝构造函数
Test t2(t1);

// 调用拷贝构造函数
Test t3 = t1;

// 调用普通构造函数
// 执行效果相当于 `Test t4(30);`
// Test(30) 通常会显式生成临时对象,临时对象的生存周期是所在语句
// 但是,如果用临时对象去拷贝构造新对象,那么临时对象就不会产生,也就是直接构造新对象就行,这是任意 C++ 编译器都会做的优化
Test t4 = Test(30);

cout << "--------- 1 ---------" << endl;

// 调用赋值运算符重载函数
t4 = t2;

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// Test(20) 会显式生成临时对象,临时对象的生存周期是所在语句
t4 = Test(20);

cout << "--------- 2 ---------" << endl;

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// (Test)20 会显式生成临时对象,临时对象的生存周期是所在语句
// 执行效果相当于 `t4 = Test(20);`,这里要求 Test 类拥有 int 类型的构造函数
t4 = (Test) 20;

cout << "--------- 3 ---------" << endl;

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// 会隐式生成临时对象,临时对象的生存周期是所在语句
// 执行效果相当于 `t4 = Test(20);`,这里要求 Test 类拥有 int 类型的构造函数
t4 = 20;

cout << "--------- 4 ---------" << endl;
}

void test02() {
cout << "\n============ test02() ============" << endl;

// 调用普通构造函数
// (Test)20 会显式生成临时对象,临时对象的生存周期是所在语句
// 当临时对象出了所在语句,会立刻被析构,同时指针 p 会成为野指针
// 结论:使用指针变量指向临时对象是不安全的
Test *p = &Test(20);

// 调用普通构造函数
// (Test)20 会显式生成临时对象,临时对象的生存周期是所在函数
// 但是,这里的临时对象出了所在语句,不会立刻被析构,而是直到出了函数作用域才会被析构
// 在函数作用域内,ref 引用会一直有效
// 结论:使用引用变量指向临时对象是安全的
Test &ref = Test(20);

cout << "--------- 1 ---------" << endl;
}

int main() {
test01();
test02();
return 0;
}

程序运行输出的结果如下:

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
--------- 1 ---------
operator=(const Test& t)
Test(int)
operator=(const Test& t)
~Test()
--------- 2 ---------
Test(int)
operator=(const Test& t)
~Test()
--------- 3 ---------
Test(int)
operator=(const Test& t)
~Test()
--------- 4 ---------
~Test()
~Test()
~Test()
~Test()

============ test02() ============
Test(int)
~Test()
Test(int)
--------- 1 ---------
~Test()

案例代码二

提示

在 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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#include <iostream>

using namespace std;

class Test {

public:
Test(int a = 5, int b = 5) : _a(a), _b(b) {
cout << "Test(" << a << ", " << b << ")" << endl;
}

~Test() {
cout << "~Test(" << this->_a << ", " << this->_b << ")" << endl;
}

Test(const Test &t) : _a(t._a), _b(t._b) {
cout << "Test(const Test& t)" << endl;
}

Test &operator=(const Test &t) {
cout << "operator=(const Test& t)" << endl;
this->_a = t._a;
this->_b = t._b;
return *this;
}

private:
int _a;
int _b;
};

void test01() {
cout << "\n============ test01() ============" << endl;

// 调用普通构造函数
Test t2(20, 20);

// 调用拷贝构造函数
Test t3 = t2;

cout << "--------- 1 ---------" << endl;

// 调用普通构造函数
// 执行效果相当于 `Test t4(30, 30);`
// Test(30, 30) 通常会显式生成临时对象,临时对象的生存周期是所在语句
// 但是,如果用临时对象去拷贝构造新对象,那么临时对象就不会产生,也就是直接构造新对象就行,这是任意 C++ 编译器都会做的优化
// 这里定义的局部静态变量,只在程序结束的时候才会被析构
static Test t4 = Test(30, 30);

cout << "--------- 2 ---------" << endl;

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// Test(40, 40) 会显式生成临时对象,临时对象的生存周期是所在语句
t2 = Test(40, 40);

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// (Test)(50, 50) 会显式生成临时对象,临时对象的生存周期是所在语句
// 执行效果相当于 `t2 = Test(50);`,这里要求 Test 类拥有对应的构造函数
t2 = (Test) (50, 50);

// 先调用普通构造函数,然后再调用赋值运算符重载函数
// 60 会显式生成临时对象,临时对象的生存周期是所在语句
// 执行效果相当于 `t2 = Test(60);`,这里要求 Test 类拥有对应的构造函数
t2 = 60;

cout << "--------- 3 ---------" << endl;
}

void test02() {
cout << "\n============ test02() ============" << endl;

// 调用普通构造函数
Test *p1 = new Test(70, 70);

// 调用(两次)普通构造函数
Test *p2 = new Test[2];

cout << "--------- 4 ---------" << endl;

// 调用普通构造函数
// Test(80, 80) 会显式生成临时对象,临时对象的生存周期是所在语句
// 当临时对象出了所在语句,会立刻被析构,同时指针 p 会成为野指针
// 结论:使用指针变量指向临时对象是不安全的
Test *p3 = &Test(80, 80);

// 调用普通构造函数
// Test(90, 90) 会显式生成临时对象,临时对象的生存周期是所在函数
// 但是,这里的临时对象出了所在语句,不会立刻被析构,而是直到出了函数作用域才会被析构
// 在函数作用域内,ref 引用会一直有效
// 结论:使用引用变量指向临时对象是安全的
Test &p4 = Test(90, 90);

cout << "--------- 5 ---------" << endl;

// 调用析构函数
delete p1;

// 调用(两次)析构函数
delete[] p2;

cout << "--------- 6 ---------" << endl;
}

// 第一个执行(t1 是最先构造的,而且又是最后析构)
Test t1(10, 10);

// 第三个执行
int main() {
cout << "\n============ start main()============" << endl;
test01();
test02();
cout << "\n============ end main()============" << endl;
return 0;
}

// 第二个执行
Test t5(100, 100);

程序运行输出的结果如下:

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
Test(10, 10)
Test(100, 100)

============ start main()============

============ test01() ============
Test(20, 20)
Test(const Test& t)
--------- 1 ---------
Test(30, 30)
--------- 2 ---------
Test(40, 40)
operator=(const Test& t)
~Test(40, 40)
Test(50, 5)
operator=(const Test& t)
~Test(50, 5)
Test(60, 5)
operator=(const Test& t)
~Test(60, 5)
--------- 3 ---------
~Test(20, 20)
~Test(60, 5)

============ test02() ============
Test(70, 70)
Test(5, 5)
Test(5, 5)
--------- 4 ---------
Test(80, 80)
~Test(80, 80)
Test(90, 90)
--------- 5 ---------
~Test(70, 70)
~Test(5, 5)
~Test(5, 5)
--------- 6 ---------
~Test(90, 90)

============ end main()============
~Test(30, 30)
~Test(100, 100)
~Test(10, 10)

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
#include <iostream>

using namespace std;

class Test {

public:
Test(int a = 10) : _ma(a) {
cout << "Test(int a)" << endl;
}

~Test() {
cout << "~Test()" << endl;
}

Test(const Test &t) {
cout << "Test(const Test& t)" << endl;
this->_ma = t._ma;
}

void operator=(const Test &t) {
cout << "operator=(const Test& t)" << endl;
this->_ma = t._ma;
}

int getData() {
return this->_ma;
}

private:
int _ma;
};

Test getObject(Test t) {
int value = t.getData();
Test tmp(value);
return tmp;
}

int main() {
Test t1;
Test t2;
t2 = getObject(t1);
return 0;
}

程序运行输出的结果如下(详细分析图解请看这里

1
2
3
4
5
6
7
8
9
10
11
Test(int a)                 // main() 函数中构造 t1 对象
Test(int a) // main() 函数中构造 t2 对象
Test(const Test& t) // getObject() 函数中拷贝构造 t 对象
Test(int a) // getObject() 函数中构造 tmp 对象
Test(const Test& t) // getObject() 函数返回执行结果值时,调用拷贝构造函数来拷贝 tmp 对象给 main() 函数栈帧上的临时对象
~Test() // 析构 getObject() 函数中的 tmp 对象
~Test() // 析构 getObject() 函数中的 t 对象
operator=(const Test& t) // main() 函数中,执行赋值运算符重载函数来将 main() 函数栈帧上的临时对象赋值给 t2 对象
~Test() // 析构 main() 函数栈帧上的临时对象,临时对象的生存周期是所在语句
~Test() // 析构 main() 函数中的 t2 对象
~Test() // 析构 main() 函数中的 t1 对象

案例代码二

  • 上述 案例一 的代码执行效率比较低,主要原因有以下几个:

    • 第一个原因:getObject() 函数的参数是按值传递,而不是按引用传递,这会多调用一次拷贝构造函数。
    • 第二个原因:在 getObject() 函数中先构造了一个 tmp 对象,然后再作为函数的返回值,这会多调用一次构造函数。
    • 第三个原因:在 main() 函数中,将临时对象赋值给 t2 对象,这会多调用一次赋值运算符重载函数。
  • C++ 对象的优化原则

    • (1) 函数参数传递的过程中,对象应该优先按引用传递,而不是按值传递。
    • (2) 函数调用结果返回对象的时候,应该优先返回一个临时对象,而不要返回一个定义过的对象。
    • (3) 当函数返回值是对象的时候,优先按初始化的方式来接收返回值,而不是按赋值的方式来接收返回值。
      • 换言之,尽量使用临时对象去拷贝构造新对象,因为这样临时对象就不会产生,也就是直接构造新对象就行,这是任意 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
#include <iostream>

using namespace std;

class Test {

public:
Test(int a = 10) : _ma(a) {
cout << "Test(int a)" << endl;
}

~Test() {
cout << "~Test()" << endl;
}

Test(const Test &t) {
cout << "Test(const Test& t)" << endl;
this->_ma = t._ma;
}

void operator=(const Test &t) {
cout << "operator=(const Test& t)" << endl;
this->_ma = t._ma;
}

int getData() {
return this->_ma;
}

private:
int _ma;
};

Test getObject(Test &t) {
int value = t.getData();
return Test(value);
}

int main() {
Test t1;
Test t2 = getObject(t1);
return 0;
}

程序运行输出的结果如下:

1
2
3
4
Test(int a)                 // main() 函数中构造 t1 对象
Test(int a) // main() 函数中构造 t2 对象(使用临时对象去拷贝构造新对象时,临时对象就不会产生,也就是直接构造新对象就行,这是任意 C++ 编译器都会做的优化)
~Test() // 析构 main() 函数中的 t2 对象
~Test() // 析构 main() 函数中的 t1 对象