C++ 杂记之四从基础到进阶

大纲

C++ 面向对象

默认构造函数的使用

在 C++ 中,只要类中显式声明了任意构造函数(包括有参构造、拷贝构造、移动构造),编译器就不会再隐式(自动)生成默认构造函数,反之就会自动生成默认构造函数。如果需要默认构造函数,则必须显式声明(比如可以使用 = default)。

拷贝构造函数的自动生成

在 C++ 中,编译器除了可能自动生成默认构造函数外,还可能自动生成拷贝构造函数等特殊成员函数。

  • 只要声明了任意构造函数,编译器就不会自动生成默认构造函数
1
2
3
4
5
6
7
8
9
class A {
public:
A(int x) {} // 有参构造函数
};

int main() {
A a; // 错误写法(编译失败):没有默认构造函数
A b(10); // 正确写法
}
  • 以下情况编译器也不会自动生成默认构造函数:
1
2
3
4
class A {
public:
A(const A&) = default; // 拷贝构造函数
};
  • 如果想要有参构造函数,又想要无参构造函数,可以补一个默认构造函数:
1
2
3
4
5
class A {
public:
A() {} // 默认构造函数
A(int x) {} // 有参构造函数
};
  • 从 C++ 11 起,默认构造函数的推荐写法:
1
2
3
4
5
class A {
public:
A() = default; // 明确告诉编译器:要有默认构造函数
A(int x) {}
};

构造函数的多种调用方式

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

using namespace std;

// 定义类
class Student {
public:
// 无参构造函数
Student() : m_age(0), m_name("") {
cout << "Student()" << endl;
}

// 有参构造函数
Student(int age, string name) : m_age(age), m_name(name) {
cout << "Student(int, string)" << endl;
}

// 拷贝构造函数
Student(const Student& s) {
cout << "Student(const Student &)" << endl;
}

// 析构函数
~Student() {
cout << "~Student" << endl;
}

// 成员函数
void show() {
cout << "age: " << m_age << ", name: " << m_name << endl;
}

private:
// 私有成员变量
int m_age;
string m_name;
};

int main() {
// 创建类对象(会调用无参构造函数)的多种写法
Student student1;
Student student2 = Student();
Student student3();
Student student4 = Student{};
Student student5{};
Student student6 = {};

// 创建类对象(会调用有参构造函数)的多种写法
Student student7 = Student(18, "Peter");
Student student8(18, "Peter");
Student student9 = Student{18, "Peter"};
Student student10{18, "Peter"};
Student student11 = {18, "Peter"};

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

using namespace std;

// 定义类
class Student {
public:
// 无参构造函数
Student() : m_age(0), m_name("") {
cout << "Student()" << endl;
}

// 有参构造函数
Student(int age, string name) : m_age(age), m_name(name) {
cout << "Student(int, string)" << endl;
}

// 拷贝构造函数
Student(const Student& s) {
cout << "Student(const Student &)" << endl;
m_age = s.m_age;
m_name = s.m_name;
}

// 析构函数
~Student() {
cout << "~Student" << endl;
}

// 成员函数
void show() {
cout << "age: " << m_age << ", name: " << m_name << endl;
}

private:
// 私有成员变量
int m_age;
string m_name;
};

int main() {
Student student1;

// 拷贝对象(会调用拷贝构造函数)的多种写法
Student student2 = student1;
Student student3(student1);
Student student4{student1};
Student student5 = (student1);
Student student6 = {student1};

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
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

#include <iostream>

using namespace std;

// 定义类
class Student {
public:
// 无参构造函数
Student() : m_age(0), m_name("") {
cout << "Student()" << endl;
}

// 有参构造函数
Student(int age, string name) : m_age(age), m_name(name) {
cout << "Student(int, string)" << endl;
}

// 拷贝构造函数
Student(const Student& s) {
cout << "Student(const Student &)" << endl;
m_age = s.m_age;
m_name = s.m_name;
}

// 析构函数
~Student() {
cout << "~Student" << endl;
}

// 成员函数
void show() {
cout << "age: " << m_age << ", name: " << m_name << endl;
}

private:
// 私有成员变量
int m_age;
string m_name;
};

void func1(Student s) {
s.show();
}

Student func2() {
Student student1(22, "David");
return student1;
}

void test01() {
// 将一个对象作为实参传递给一个非引用类型的形参,也会发生对象拷贝,即调用拷贝构造函数
Student student1(18, "Jim");
func1(student1);
}

void test02() {
// 大多数情况下,这里不会调用拷贝构造函数,因为会发生 NRVO(命名返回值优化),但这是 "允许优化",不是强制
// 在 C++ 17 及之后,函数返回局部对象时会强制进行拷贝消除(RVO/NRVO),对象会被直接构造在调用者的栈空间中,不存在中间临时对象,因此不会调用拷贝构造函数
Student s = func2();
}
  • 拷贝构造函数调用的情形一:以值传递方式将对象作为函数参数

    • 当对象以非引用形式作为函数参数传递时,实参需要拷贝一份用于形参的初始化,因此会调用拷贝构造函数。
  • 拷贝构造函数调用的情形二:对象拷贝初始化,且无法进行拷贝消除优化

    • 当一个对象用于初始化另一个对象,并且编译器无法或不允许进行拷贝消除时,会调用拷贝构造函数。
    • C++ 标准不同版本的说明:
      • 在 C++ 11 / C++ 14 中
        • 如果满足 NRVO(命名返回值优化)条件,通常不会调用拷贝构造函数(允许优化)
        • 若不满足优化条件,则会调用拷贝构造函数(或移动构造函数)
      • 在 C++ 17 及之后
        • 对函数返回局部对象的场景,会强制进行拷贝消除,即一定不会调用拷贝构造函数

总结

在 C++ 中,拷贝构造函数通常在对象以值传递方式作为函数参数,或在对象拷贝初始化且无法进行拷贝消除时被调用;而在 C++17 之后,函数返回局部对象的场景会强制进行拷贝消除,不再触发拷贝构造函数。

隐式类型转换和 explicit 关键字

explicit 关键字的概述

  • 当构造函数被声明为 explicit 时,该构造函数不会参与隐式类型转换。
  • 对于单参数的构造函数,通常都建议声明为 explicit,除非有其他特殊原因。
  • 对于拷贝构造函数,通常不使用 explicit 修饰。
  • 使用 explicit 后,不支持函数参数的隐式类型转换,同时不支持拷贝初始化。
  • 使用 explicit 后,只能使用直接初始化(如 T a(x)),或者使用显式类型转换(如 static_cast<T>(x))来构造对象。
1
2
3
4
5
6
7
8
class A {
explicit A(int x) {}
};

A a1(10); // ✅ 正确写法:允许直接初始化
A a2 = 10; // ❌ 错误写法:禁止拷贝初始化,禁止隐式类型转换
A a3 = A(10); // ✅ 正确写法:允许显式构造
A a4 = static_cast<A>(10); // ✅ 正确写法:允许显式类型转换
1
2
3
4
void foo(A a) {}

foo(10); // ❌ 错误写法:禁止隐式类型转换
foo(A(10)); // ✅ 正确写法:允许显式构造

explicit 关键字的使用

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

using namespace std;

// 定义类
class Student {
public:
// 无参构造函数
Student() : m_age(0), m_name("") {
cout << "Student()" << endl;
}

// 有参构造函数(单个参数)
Student(int age) : m_age(age), m_name("") {
cout << "Student(int)" << endl;
}

// 有参构造函数(两个参数)
Student(int age, string name) : m_age(age), m_name(name) {
cout << "Student(int, string)" << endl;
}

// 拷贝构造函数
Student(const Student& s) {
cout << "Student(const Student &)" << endl;
}

// 析构函数
~Student() {
cout << "~Student" << endl;
}

// 成员函数
int getAge() const {
return m_age;
}

// 成员函数
string getName() const {
return m_name;
}

// 成员函数
void show() const {
cout << "age: " << m_age << ", name: " << m_name << endl;
}

private:
// 私有成员变量
int m_age;
string m_name;
};

// 全局函数(普通函数)
void print(Student stu) {
cout << "age: " << stu.getAge() << ", name: " << stu.getName() << endl;
}

int main() {
// 存在临时对象隐式类型转换,会调用单参数构造函数,相当于 Student student1(20)
Student student1 = 20;

// 存在临时对象隐式类型转换,会调用单参数构造函数,相当于 Student student2(15)
Student student2 = (12, 13, 14, 15);

// 会执行 Student student(25) 将 25 转换成一个临时的 Student 对象,导致 print () 函数可以被调用成功
print(25);

return 0;
}
  • 使用 explicit 禁止 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
#include <iostream>

using namespace std;

// 定义类
class Student {
public:
// 无参构造函数
Student() : m_age(0), m_name("") {
cout << "Student()" << endl;
}

// 有参构造函数(单个参数)
explicit Student(int age) : m_age(age), m_name("") {
cout << "Student(int)" << endl;
}

// 有参构造函数(两个参数)
Student(int age, string name) : m_age(age), m_name(name) {
cout << "Student(int, string)" << endl;
}

// 拷贝构造函数
Student(const Student& s) {
cout << "Student(const Student &)" << endl;
}

// 析构函数
~Student() {
cout << "~Student" << endl;
}

// 成员函数
int getAge() const {
return m_age;
}

// 成员函数
string getName() const {
return m_name;
}

// 成员函数
void show() const {
cout << "age: " << m_age << ", name: " << m_name << endl;
}

private:
// 私有成员变量
int m_age;
string m_name;
};

// 全局函数(普通函数)
void print(Student stu) {
cout << "age: " << stu.getAge() << ", name: " << stu.getName() << endl;
}

int main() {
// 错误写法(编译失败),单参数构造函数使用 explicit 声明,不允许隐式类型转换
// Student student1 = 20;

// 错误写法(编译失败),单参数构造函数使用 explicit 声明,不允许隐式类型转换
// Student student2 = {20};

// 错误写法(编译失败),单参数构造函数使用 explicit 声明,不允许隐式类型转换
// Student student3 = (12, 13, 14, 15);

// 错误写法(编译失败),单参数构造函数使用 explicit 声明,不允许隐式类型转换
// print(25);

return 0;
}

成员函数返回自身对象的引用

  • 成员函数适合返回自身对象的引用的场景

    • 返回当前对象(*this
    • 成员函数支持链式调用
    • 运算符重载(=, +=, ++ 等)
  • 成员函数不适合返回自身对象的引用的场景

    • 返回新创建的对象
    • 返回生命周期不受调用方控制的对象
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
#include <iostream>

class Counter {
public:
Counter(int v = 0) : value(v) {}

// 返回自身对象的引用
Counter& increment() {
++value;
return *this;
}

int get() const {
return value;
}

private:
int value;
};

int main() {
Counter c;
c.increment().increment().increment(); // 链式调用
std::cout << c.get() << std::endl; // 输出 3
return 0;
}
  • 在上面的成员函数 increment() 中:
    • *this 是当前对象本身
    • 返回类型是 Counter&
    • 不会产生对象拷贝
    • 常用于链式调用

总结说明

  • 在非 const 成员函数中,this 是一个指向非 const 对象的指针常量(可类比为 Counter * const this),因此 this 指向的地址不能更改,但可以通过 this 修改对象的状态。
  • const 成员函数中,this 是一个指向 const 对象的指针常量(可类比为 const Counter * const this),因此 this 指向的地址不能更改,且不可以通过 this 修改对象的状态。
  • 成员函数中返回 *this 的引用,是 C++ 中实现链式调用和高效操作的常用技巧,前提是当前对象的生命周期由调用方保证且在使用期间有效。
  • this 指针只存在于非静态成员函数中,全局函数和静态成员函数(static 修饰)不依附于具体的对象实例,因此都不能使用 this 指针。