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 之后,函数返回局部对象的场景会强制进行拷贝消除,不再触发拷贝构造函数。

类型转换构造函数和运算符

类型转换构造函数

关键点

在 C++ 中,类型转换构造函数是一种特殊的构造函数,它的主要作用是将某个其他数据类型的对象转换成该类类型的对象。通常,它只有一个参数,该参数不是本类的 const 引用,而是待转换的数据类型。在类型转换构造函数中,需要明确指定转换的方法,以便正确完成从源类型到目标类型的转换。

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

using namespace std;

class MyInt {
public:
// 有参构造函数,同时也称为类型转换构造函数
MyInt(int i = 0) : m_num(i) {
cout << "MyInt(int)" << endl;
}

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

int getNumber() {
return m_num;
}

private:
int m_num;
};

int main() {
// 这里会发生隐式类型转换,调用的是类型转换构造函数
// 编译器用 10 这个数字通过调用 MyInt 类的构造函数创建一个临时 MyInt 对象,并将这个临时对象构造到 i 这个预留空间里面去
MyInt i = 10;
cout << i.getNumber() << endl;

// 等效于上面的写法,会发生隐式类型转换,调用的是类型转换构造函数
// MyInt i = MyInt(10);

// 这里不会发生隐式类型转换,调用的是有参构造函数(即类型转换构造函数)
MyInt i2(30);
cout << i2.getNumber() << endl;

return 0;
}

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

1
2
3
4
5
6
MyInt(int)
10
MyInt(int)
30
~MyInt()
~MyInt()

类型转换运算符

关键点

C++ 中的类型转换运算符(又称类型转换函数)是一种特殊的成员函数,其作用与类型转换构造函数相反,用于将类类型对象转换为其他数据类型。其基本语法为 operator type() const;,其中 type 表示要转换的目标类型,可以是任何有效的返回类型(包括数组指针、函数指针、引用等)。该函数不能指定返回类型,但实际返回 type 类型的值;它没有形参,因为类型转换是隐式执行的,无法传递参数;函数通常定义为类的成员函数,const 修饰符为可选项,表示转换过程中不应修改待转换对象的内容。

类型转换运算符的使用
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
#include <iostream>

using namespace std;

class MyInt {
public:
// 有参构造函数,同时也称为类型转换构造函数
MyInt(int i = 0) : m_num(i) {
}

~MyInt() {
}

// 类型转换运算符,将类类型转换为其他类型
operator int() const {
cout << "operator int() const" << endl;
return m_num;
}

private:
int m_num;
};

int main() {
MyInt i(30);

// 将类类型转换为其他类型(隐式调用类型转换运算符)
int i2 = i + 10;
cout << i2 << endl;

// 将类类型转换为其他类型(显式调用类型转换运算符)
int i3 = i.operator int() + 15;
cout << i3 << endl;

// 将类类型转换为其他类型(显式调用类型转换运算符)
int i4 = static_cast<int>(i) + 25;
cout << i4 << endl;

return 0;
}

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

1
2
3
4
5
6
operator int() const
40
operator int() const
45
operator int() const
55
类对象转化为函数指针
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
#include <functional>
#include <iostream>

using namespace std;

class MyInt {
public:
// 定义函数指针类型,代表函数只有一个int形参,没有返回值类型
// typedef void (*tfpoint)(int);
using tfpoint = void (*)(int);

// 静态成员函数
static void func(int i) {
cout << "func(int)" << endl;
}

MyInt(int i = 0) : m_num(i) {
}

~MyInt() {
}

// 类型转换运算符,将本类的类型对象转换为一个函数指针类型
operator tfpoint() {
return func;
}

private:
int m_num;
};

int main() {
MyInt i(10);

// 这里会隐式调用类型转换运算符,将本类的类型对象转换为一个函数指针类型,最后调用静态成员函数 func()
i(3);

// 显式调用类型转换运算符,将本类的类型对象转换为一个函数指针类型,最后调用静态成员函数 func()
i.operator MyInt::tfpoint()(5);

return 0;
}

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

1
2
func(int)
func(int)
类型转换的二义性问题
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
#include <iostream>

using namespace std;

class MyInt {
public:
// 有参构造函数,同时也称为类型转换构造函数
MyInt(int i = 0) : m_num(i) {
}

~MyInt() {
}

// 类型转换运算符,将类类型转换为其他类型
operator int() const {
cout << "operator int() const" << endl;
return m_num;
}

// 类型转换运算符,将类类型转换为其他类型
operator double() const {
cout << "operator int() const" << endl;
return m_num;
}

private:
int m_num;
};

int main() {
MyInt i(30);

// 编译出错,存在二义性,编译器不知道调用 operator int() 还是 operator double() 运算符
// int i2 = i + 10;

return 0;
}

隐式类型转换和 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;
}

类成员指针的使用

类成员函数指针

关键点

  • C++ 的类成员函数指针是一种特殊指针,用于指向类中的成员函数。对于非静态成员函数(包括虚函数)的函数指针,声明时需要使用 类名::* 指针变量名 的格式,获取函数地址时则通过 & 类名::成员函数名 来获得,这个地址是真正在内存中的函数地址。
  • 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
#include <iostream>

using namespace std;

class MyTest {
public:
// 普通成员函数
void commonFunc(int i) {
cout << "commonFunc(" << i << ")" << endl;
}
};

int main() {
// 声明一个类成员函数指针变量,其中 ptrCommonFunc 是变量名
void (MyTest::*ptrCommonFunc)(int);

// 类成员函数指针变量赋值
ptrCommonFunc = &MyTest::commonFunc;

MyTest t1, *ptr1;
ptr1 = &t1;

// 通过对象本身调用类成员函数指针
(t1.*ptrCommonFunc)(10);

// 通过对象指针调用类成员函数指针
(ptr1->*ptrCommonFunc)(20);

return 0;
}

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

1
2
commonFunc(10)
commonFunc(20)

针对虚函数的使用

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

using namespace std;

class MyTest {
public:
// 虚函数
virtual void virtualFunc(int i) {
cout << "virtualFunc(" << i << ")" << endl;
}
};

// 对于类成员函数指针的使用方式,虚函数跟上面的普通成员函数是一样的,没有任何区别
int main() {
// 声明一个类成员函数指针变量,其中 ptrVirtualFunc 是变量名
void (MyTest::*ptrVirtualFunc)(int);

// 类成员函数指针变量赋值
ptrVirtualFunc = &MyTest::virtualFunc;

MyTest t1, *ptr1;
ptr1 = &t1;

// 通过对象本身调用类成员函数指针
(t1.*ptrVirtualFunc)(10);

// 通过对象指针调用类成员函数指针
(ptr1->*ptrVirtualFunc)(20);

return 0;
}

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

1
2
virtualFunc(10)
virtualFunc(20)

针对静态成员函数的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

class MyTest {
public:
// 静态成员函数
static void staticFunc(int i) {
cout << "staticFunc(" << i << ")" << endl;
}
};

int main() {
// 声明一个类成员函数指针变量,其中 ptrStaticFunc 是变量名
void (*ptrStaticFunc)(int);

// 类成员函数指针变量赋值
ptrStaticFunc = &MyTest::staticFunc;

// 直接调用类成员函数指针
ptrStaticFunc(20);

return 0;
}

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

1
staticFunc(20)

类成员变量指针

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

using namespace std;

class MyTest {
public:
int m_common = 0; // 普通成员变量
static int m_static; // 静态成员变量
};

// 初始化静态成员变量
int MyTest::m_static = 0;

void test01() {
// 针对普通成员变量定义一个类成员变量指针,本质并不是指向内存中的某个地址,而是该类成员变量与该类对象指针之间的地址偏移量
int MyTest::*ptr1 = &MyTest::m_common;

// 通过对象本身 + 类成员变量指针修改普通成员变量的值
MyTest t1;
t1.*ptr1 = 20;
cout << "address: " << ptr1 << ", value: " << t1.m_common << endl;
}

void test02() {
// 针对静态成员变量定义一个类成员变量指针,指向的是真正的内存地址
int *ptr1 = &MyTest::m_static;

// 直接通过类成员变量指针修改静态成员变量的值,等价于 MyTest::m_static = 40
*ptr1 = 40;
cout << "address: " << ptr1 << ", value: " << *ptr1 << endl;
}

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

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

1
2
address: 1, value: 20
address: 0x55f59236e194, value: 40

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

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

    • 返回当前对象(*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 指针。