C++ 从基础到进阶之五

大纲

C++ 面向对象浅析

继承中的构造顺序、访问权限、函数隐藏

继承中的构造顺序

  • 在 C++ 的继承体系中
    • 构造函数的调用顺序
      • 先构造基类,再构造派生类,否则派生类无法安全使用基类成员
    • 析构函数的调用顺序
      • 先析构派生类,再析构基类,否则派生类的析构函数可能访问已经被销毁的基类成员
    • 记忆口诀:先构造后析构,后构造先析构

继承与组合混搭情况下的构造与析构顺序

  • 继承与组合对象混搭使用的情况下,构造函数与析构函数的调用原则如下:
  • (1) 构造函数的调用顺序:先构造父类,再构造成员变量,最后构造自身。
  • (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
#include <iostream>
using namespace std;

class Base {
public:
Base() {
cout << "Base constructor" << endl;
}

~Base() {
cout << "Base destructor" << endl;
}
};

class Derived : public Base {
public:
Derived() {
cout << "Derived constructor" << endl;
}

~Derived() {
cout << "Derived destructor" << endl;
}
};

int main() {
Derived d;
return 0;
}

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

1
2
3
4
Base constructor
Derived constructor
Derived destructor
Base destructor

继承中的访问权限

继承中的函数隐藏

什么是函数隐藏

  • 在 C++ 的继承体系中,如果子类声明了一个与父类同名的成员函数(无论参数列表是否相同),那么在子类作用域内,父类中所有同名函数都会被隐藏,无法通过子类普通对象调用方式访问。这种现象称为函数隐藏(Function Hiding),也称为 “函数遮蔽”。

  • 造成函数隐藏的本质原因是:在 C++ 编译期的名字查找(Name Lookup)过程中,如果在派生类作用域中找到了某个函数名,编译器将停止向基类作用域继续查找该名称,从而导致基类中所有同名函数(包括不同参数列表的重载函数)都会被隐藏。

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
#include <iostream>
using namespace std;

class Base {
public:
void func() {
cout << "Base::func()" << endl;
}

void func(int x) {
cout << "Base::func(int): " << x << endl;
}

void func(double d) {
cout << "Base::func(double): " << d << endl;
}
};

class Derived : public Base {
public:
void func(double d) {
cout << "Derived::func(double): " << d << endl;
}
};

int main() {
Derived d;

d.func(3.14); // ✅ 编译成功,会调用子类同名函数 Derived::func(double)

d.func(); // ❌ 编译失败,无法调用父类同名函数 Base::func()
d.func(10); // ❌ 编译失败,无法调用父类同名函数 Base::func(int)

return 0;
}
  • 上述代码为什么会报错?

    • 因为只要子类 Derived 声明了 func 这个成员函数名称,那么 Base::func()Base::func(int) 全部都会被隐藏(不是重写,是隐藏)。
  • 函数隐藏的本质

    • C++ 编译器的函数查找规则是:
      • 先在子类作用域查找
      • 如果找到同名函数
      • 则停止查找父类作用域
    • 不管参数是否匹配
    • 这叫做:名字查找(Name Lookup)规则
  • 函数隐藏的特征

    • 只要基类与派生类中有相同函数名,那么就会发生函数隐藏
    • 函数隐藏与函数参数列表是否相同无关
    • 函数隐藏与函数是否被 virtual 修饰无关
    • 函数隐藏发生在 C++ 编译期名字查找阶段
    • 函数隐藏不是多态行为

函数隐藏解决方案一

  • 使用 using 关键字重新引入父类函数(推荐),也就是让父类同名函数在子类中以重载的方式来使用
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 <iostream>
using namespace std;

class Base {
public:
void func() {
cout << "Base::func()" << endl;
}

void func(int x) {
cout << "Base::func(int): " << x << endl;
}

void func(double d) {
cout << "Base::func(double): " << d << endl;
}
};

class Derived : public Base {
public:
using Base::func; // 解决函数隐藏问题

void func(double d) {
// 子类内部调用父类的同名函数
func(); // ✅ 会调用 Base::func()
func(20); // ✅ 会调用 Base::func(int)
func(2.63); // ❌ 因为函数重载的原因,无法调用 Base::func(double),调用的是 Derived::func(double),这里相当于递归调用
cout << "Derived::func(double): " << d << endl;
}
};

int main() {
Derived d;

d.func(3.14); // ✅ 会调用 Derived::func(double)

// 子类外部调用父类的同名函数
d.func(); // ✅ 会调用 Base::func()
d.func(10); // ✅ 会调用 Base::func(int)

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
#include <iostream>
using namespace std;

class Base {
public:
void func() {
cout << "Base::func()" << endl;
}

void func(int x) {
cout << "Base::func(int): " << x << endl;
}

void func(double d) {
cout << "Base::func(double): " << d << endl;
}
};

class Derived : public Base {
public:
void func(double d) {
// 子类内部调用父类的同名函数
Base::func(); // ✅ 会调用 Base::func()
Base::func(20); // ✅ 会调用 Base::func(int)
Base::func(6.19); // ✅ 会调用 Base::func(double)
cout << "Derived::func(double): " << d << endl;
}
};

int main() {
Derived d;

d.func(3.14); // ✅ 会调用 Derived::func(double)

// 子类外部调用父类的同名函数
d.Base::func(); // ✅ 会调用 Base::func()
d.Base::func(10); // ✅ 会调用 Base::func(int)
d.Base::func(3.14); // ✅ 会调用 Base::func(double)

return 0;
}

函数隐藏 VS 函数重写

概念条件是否需要 virtual 修饰是否隐藏
重写(override)函数签名完全相同必须不隐藏
隐藏(hiding)只要名字相同不需要会隐藏

函数隐藏高频面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
virtual void show(int x) {
cout << "Base show(int)" << endl;
}
};

class Derived : public Base {
public:
void show(double x) {
cout << "Derived show(double)" << endl;
}
};
  • 上述代码:
    • 不是函数重写(因为函数签名不同),也不是函数重载(因为同名函数不在同一个类),而是函数隐藏
    • virtual 关键字不会生效,多态不会发生

继承中的构造函数继承、多继承、虚继承

构造函数继承

关键点

  • 通过 using A::A;,派生类可以继承直接基类的有参构造函数(主要是非特殊的有参构造函数),但基类的默认构造函数、默认拷贝构造函数、默认移动构造函数不会被继承,它们仍由编译器按规则为派生类自动生成(或被删除)。
  • 这样可以避免重复编写构造函数、提高复用性,但如果派生类中定义了相同参数列表的构造函数,会覆盖(隐藏)从基类继承而来的构造函数。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A(int i, int j) {}

A(int i, int j, int k) {}
};

class B : public A {
public:
// 继承 A 类的全部有参构造函数,除了 A 类的默认构造函数、默认拷贝构造函数、默认移动构造函数(如果存在)
using A::A;
};

int main() {
B b1(1, 2); // 编译通过
B b2(1, 2, 3); // 编译通过
return 0;
}
  • using A::A; 使派生类 B 继承基类 A 的全部有参构造函数,除了基类的默认构造函数、默认拷贝构造函数、默认移动构造函数(如果存在)。
  • 编译器会为 B 自动生成与 A 对应的有参构造函数。

等价效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A(int i, int j) {}

A(int i, int j, int k) {}
};

class B : A {
B(int i, int j) : A(i, j) {}

B(int i, int j, int k) : A(i, j, k) {}
};

int main() {
B b1(1, 2); // 编译通过
B b2(1, 2, 3); // 编译通过
return 0;
}
  • 本质上相当于为每个父类构造函数在子类中生成一个 “转发构造函数”。

参数规则限制

  • 派生类继承的构造函数参数列表必须与基类一致,不能减少或增加参数。
  • 但可以在派生类中额外定义自己的构造函数。

默认构造函数情况

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
A(int i, int j, int k) {
}
};

class B : A {
using A::A;
};

int main() {
// B b; // 编译失败,因为 A 没有默认构造函数,编译器也就不会为 B 生成默认构造函数
return 0;
}
  • 如果 A(基类)没有默认构造函数,且 B(派生类)也没有定义默认构造函数,那么编译器不会为 B 自动生成默认构造函数,反之亦然。

继承构造函数的限制

  • 派生类只能继承直接基类的有参构造函数,且不包括基类的默认构造函数、默认拷贝构造函数、默认移动构造函数(如果存在)。
  • 如果派生类中定义了相同参数列表的构造函数,那么派生类会覆盖(隐藏)从基类继承来的构造函数。

多重继承

多重继承的使用

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

using namespace std;

class Grand {
public:
Grand(int i) : m_value_grand(i) {
cout << "Grand(int)" << endl;
}

virtual ~Grand() {
cout << "~Grand()" << endl;
}

void info() {
cout << m_value_grand << endl;
}

public:
int m_value_grand;
};

// 单继承
class A : public Grand {
public:
A(int i) : Grand(i), m_value_a(i) {
cout << "A(int)" << endl;
}

virtual ~A() {
cout << "~A()" << endl;
}

void info() {
cout << m_value_a << endl;
}

public:
int m_value_a;
};

class B {
public:
B(int i) : m_value_b(i) {
cout << "B(int)" << endl;
}

virtual ~B() {
cout << "~B()" << endl;
}

void info() {
cout << m_value_b << endl;
}

public:
int m_value_b;
};

// 多重继承
class C : public A, public B {
public:
C(int i, int j, int k) : A(i), B(j), m_value_c(k) {
cout << "C(int, int, int)" << endl;
}

virtual ~C() {
cout << "~C()" << endl;
}

void infoA() {
// 在类的内部,显式调用父类 A 的 info() 函数
A::info();
}

void infoB() {
// 在类的内部,显式调用父类 B 的 info() 函数
B::info();
}

public:
int m_value_c;
};

int main() {
C c1(10, 20, 30);

// c1.info(); // 编译失败,因为编译器不知道调用的式父类 A 还是父类 B 的 info() 函数,存在二义性
c1.A::info(); // 编译通过,通过作用域,在类的外部,显式调用父类 A 的 info() 函数,输出 10

c1.infoA(); // 输出 10
c1.infoB(); // 输出 20

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
Grand(int)
A(int)
B(int)
C(int, int, int)
10
10
20
~C()
~B()
~A()
~Grand()

派生类的构造函数与析构函数

  • 构造一个派生类对象时,会自动调用其所有基类的构造函数
  • 派生类的构造函数必须负责:
    • 初始化直接基类
    • 初始化自身成员变量
  • 基类的初始化优先于派生类,顺序如下:
    • (1) 先调用基类构造函数
    • (2) 再调用派生类构造函数
  • 如果存在多继承:
    • 基类的构造顺序按照继承列表中的声明顺序执行(与初始化列表顺序无关)
    • 比如:class C : public A, public B { },这里会根据继承列表先构造 A,然后再构造 B
  • 析构函数执行顺序与构造函数相反:
    • (1) 先执行派生类析构函数
    • (2) 再执行基类析构函数
多继承中的静态变量

关键点

static 变量属于类,在整个继承体系中只有一份共享实例;父类修改后,子类访问到的一定是修改后的值,不存在子类单独拥有一份 static 副本的情况。

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

using namespace std;

class Grand {
public:
Grand(int i) : m_value_grand(i) {
}

virtual ~Grand() {
}

void info() {
cout << m_value_grand << endl;
}

public:
int m_value_grand;
static int m_value_static; // 声明静态成员变量
};

// 类外定义静态成员变量
int Grand::m_value_static = 2;

// 单继承
class A : public Grand {
public:
A(int i) : Grand(i), m_value_a(i) {
}

virtual ~A() {
}

void info() {
cout << m_value_a << endl;
}

public:
int m_value_a;
};

class B {
public:
B(int i) : m_value_b(i) {
}

virtual ~B() {
}

void info() {
cout << m_value_b << endl;
}

public:
int m_value_b;
};

// 多重继承
class C : public A, public B {
public:
C(int i, int j, int k) : A(i), B(j), m_value_c(k) {
}

virtual ~C() {
}

void infoA() {
cout << m_value_c << endl;
}

public:
int m_value_c;
};

int main() {
// static 变量属于类,在继承体系中只有一份,父类修改后,子类访问到的一定是修改后的值(不存在子类自己拥有 static 副本的说法)
A::m_value_static = 15;
cout << A::m_value_static << endl;
cout << C::m_value_static << endl;
return 0;
}

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

1
2
15
15

这里还有一特殊种情况,子类可以 “隐藏” 父类的 static 变量(但不推荐)

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

using namespace std;

class Grand {
public:
Grand(int i) : m_value_grand(i) {
}

virtual ~Grand() {
}

void info() {
cout << m_value_grand << endl;
}

public:
int m_value_grand;
static int m_value_static; // 声明静态成员变量
};

// 类外定义静态成员变量
int Grand::m_value_static = 2;

// 单继承
class A : public Grand {
public:
A(int i) : Grand(i), m_value_a(i) {
}

virtual ~A() {
}

void info() {
cout << m_value_a << endl;
}

public:
int m_value_a;
};

class B {
public:
B(int i) : m_value_b(i) {
}

virtual ~B() {
}

void info() {
cout << m_value_b << endl;
}

public:
int m_value_b;
};

// 多重继承
class C : public A, public B {
public:
C(int i, int j, int k) : A(i), B(j), m_value_c(k) {
}

virtual ~C() {
}

void infoA() {
cout << m_value_c << endl;
}

public:
int m_value_c;
static int m_value_static; // 声明静态成员变量
};

// 类外定义静态成员变量
// C 自己重新定义的一份 static 变量
int C::m_value_static = 4;

int main() {
// 这里访问的是 Grand::m_value_static(A 继承自 Grand,但没有定义自己的 static 变量)
A::m_value_static = 15;

cout << A::m_value_static << endl; // 输出 15,本质等价于 Grand::m_value_static
cout << C::m_value_static << endl; // 输出 4,访问的是 C 自己定义的 static 变量

// C 修改自己的 static 变量,不影响 Grand(也不影响 A)
C::m_value_static = 30;

cout << A::m_value_static << endl; // 仍然是 15(Grand::m_value_static 没变)
cout << C::m_value_static << endl; // 变为 30(C::m_value_static 被修改)

return 0;
}

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

1
2
3
4
15
4
15
30
从多个基类继承构造函数

关键点

如果派生类通过 using 关键字继承了多个基类中签名相同(参数列表相同)的构造函数,会产生二义性并导致编译错误;此时需要在派生类中显式定义对应的构造函数来消除歧义;需要注意的是,构造函数不能被覆盖(override),这里只是重新定义并明确调用哪个基类的构造函数。

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 A {
public:
A(int i) {
cout << "A()" << endl;
}

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

class B {
public:
B(int i) {
cout << "B()" << endl;
}

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

class C : public A, public B {
public:
// 继承 A 类的全部有参构造函数,除了 A 类的默认构造函数、默认拷贝构造函数、默认移动构造函数(如果存在)
using A::A;

// 继承 B 类的全部有参构造函数,除了 B 类的默认构造函数、默认拷贝构造函数、默认移动构造函数(如果存在)
using B::B;

// 如果派生类从它的多个基类中继承了相同参数的构造函数,那么这个派生类必须为该构造函数显式定义自己的版本,否则会有二义性,导致编译失败
C(int i) : A(i), B(i) {
}
};

int main() {
C c1(10);
return 0;
}

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

1
2
3
4
A()
B()
~B()
~A()

虚继承

虚继承的概念
  • 主要用途:

    • 虚继承主要用于解决菱形继承中的数据冗余和二义性问题,使基类在继承体系中只保留一份实例
  • 定义方式:

    • 在继承时,使用 virtual 关键字,比如:class A : virtual public Base {};
  • 核心特性:

    • 多个子类共享同一份基类对象
    • 最终在派生类中,只会存在一份基类实例
  • 不使用虚继承时:

    • 基类被重复继承,导致派生类中出现多份基类数据
    • 当派生类访问基类成员时,会产生二义性
  • 使用虚继承后:

    • 消除数据冗余,在派生类中只会存在一份基类实例
    • 访问基类成员不再出现二义性
  • 构造规则:

    • 虚基类由最底层的派生类负责初始化
    • 中间类即使写了虚基类的构造调用,也会被忽略
  • 底层原理:

    • 虚继承的底层是靠虚基类指针(vbptr)和虚基类表(vbtable)来实现。
  • 适用场景:

    • 虚继承只适用于有共同基类(公共基类)的多继承场景(比如菱形继承),如图所示
  • 不适用场景:

    • 对于 V 字形的多继承场景,虚继承是没办法解决二义性问题的,如图所示

总结

  • 在 C++ 中,虚继承的声明需要使用关键字 virtual,被虚继承的类通常称作为虚基类。
  • 如果一个派生类从多个基类继承,而这些基类又有一个共同的基类(公共基类),则在对该基类中声明的成员进行访问时,可能会产生二义性,需要使用虚继承来解决二义性问题。
  • 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象。要使这个公共基类在派生类中只产生一个子对象,必须对这个基类的继承声明为虚继承,使这个基类成为 虚基类
虚继承的使用

菱形继承存在的二义性问题

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

using namespace std;

class Grand {
public:
Grand(int i) : m_value_grand(i) {
cout << "Grand(int)" << endl;
}

virtual ~Grand() {
cout << "~Grand()" << endl;
}

void info() {
cout << m_value_grand << endl;
}

public:
int m_value_grand;
};

// 单继承
class A : public Grand {
public:
A(int i) : Grand(i), m_value_a(i) {
cout << "A(int)" << endl;
}

virtual ~A() {
cout << "~A()" << endl;
}

void info() {
cout << m_value_a << endl;
}

public:
int m_value_a;
};

// 单继承
class A2 : public Grand {
public:
A2(int i) : Grand(i), m_value_a2(i) {
cout << "A2(int)" << endl;
}

virtual ~A2() {
cout << "~A2()" << endl;
}

void info() {
cout << m_value_a2 << endl;
}

public:
int m_value_a2;
};

class B {
public:
B(int i) : m_value_b(i) {
cout << "B(int)" << endl;
}

virtual ~B() {
cout << "~B()" << endl;
}

void info() {
cout << m_value_b << endl;
}

public:
int m_value_b;
};

// 多重继承
class C : public A, public A2, public B {
public:
C(int i, int j, int k) : A(i), A2(i), B(j), m_value_c(k) {
cout << "C(int, int, int)" << endl;
}

virtual ~C() {
cout << "~C()" << endl;
}

void infoA() {
cout << m_value_c << endl;
}

public:
int m_value_c;
};

int main() {
// 在不使用虚继承的时候,这里 Grand 类会被构造两次
C c1(10, 20, 30);

// 在不使用虚继承的时候,这行代码会编译失败,因为存在二义性
// c1.m_value_grand = 20;

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
Grand(int)
A(int)
Grand(int)
A2(int)
B(int)
C(int, int, int)
~C()
~B()
~A2()
~Grand()
~A()
~Grand()

使用虚继承来解决菱形继承的二义性问题

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

using namespace std;

// 虚基类
class Grand {
public:
Grand(int i) : m_value_grand(i) {
cout << "Grand(int)" << endl;
}

virtual ~Grand() {
cout << "~Grand()" << endl;
}

void info() {
cout << m_value_grand << endl;
}

public:
int m_value_grand;
};

// 虚继承
class A : virtual Grand {
public:
A(int i) : Grand(i), m_value_a(i) {
cout << "A(int)" << endl;
}

virtual ~A() {
cout << "~A()" << endl;
}

void info() {
cout << m_value_a << endl;
}

public:
int m_value_a;
};

// 虚继承
class A2 : virtual public Grand {
public:
A2(int i) : Grand(i), m_value_a2(i) {
cout << "A2(int)" << endl;
}

virtual ~A2() {
cout << "~A2()" << endl;
}

void info() {
cout << m_value_a2 << endl;
}

public:
int m_value_a2;
};

class B {
public:
B(int i) : m_value_b(i) {
cout << "B(int)" << endl;
}

virtual ~B() {
cout << "~B()" << endl;
}

void info() {
cout << m_value_b << endl;
}

public:
int m_value_b;
};

// 多重继承
class C : public A, public A2, public B {
public:
// 这里需要手动调用虚基类(Grand)的构造函数
C(int i, int j, int k) : Grand(i), A(i), A2(i), B(j), m_value_c(k) {
cout << "C(int, int, int)" << endl;
}

virtual ~C() {
cout << "~C()" << endl;
}

void infoA() {
cout << m_value_c << endl;
}

public:
int m_value_c;
};

int main() {
// 使用虚继承后,这里 Grand 类只会被构造一次
C c1(10, 20, 30);

// 使用虚继承后,这行代码可以编译成功,不存在二义性
c1.m_value_grand = 20;

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
Grand(int)
A(int)
A2(int)
B(int)
C(int, int, int)
~C()
~B()
~A2()
~A()
~Grand()
虚继承的初始化规则
  • 在继承体系中,如果存在虚基类(如 Grand):

    • 虚基类由最底层的派生类负责初始化
    • 中间层(如 A、A2)即使写了对 Grand 的构造调用,也不会真正生效
  • 换句话说:

    • 谁是最终创建对象的类,谁就负责初始化虚基类
  • 初始化顺序规则:

    • (1) 先初始化所有虚基类
    • (2) 再按照继承列表顺序,初始化非虚基类
    • (3) 最后初始化当前类的自身成员

总结

  • 虚基类只初始化一次,并且由最底层的派生类统一负责初始化。简而言之,虚基类的初始化顺序优先于所有普通基类。

友元函数、友元类、友元成员函数

特别注意

友元关系不能被继承;友元关系是单向的;友元关系没有传递性。

友元类的使用

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

using namespace std;

class Human {
public:
Human() {
cout << "Human::Human()" << endl;
}

Human(const string& name, int age) : m_Name(name), m_Age(age) {
cout << "Human::Human(string, int)" << endl;
}

virtual ~Human() {
cout << "Human::~Human()" << endl;
}

void eat() const {
cout << m_Name << "Human eat food" << endl;
}

public:
// 声明友元类
friend class Game;

private:
int m_Age;
string m_Name;
};

class Game {
public:
explicit Game(shared_ptr<Human> h) : m_Human(std::move(h)) {
cout << "Game::Game()" << endl;
}

~Game() {
cout << "Game::~Game()" << endl;
}

void getHuman() const {
if (m_Human) {
// 在友元类中,可以访问其他类的私有成员变量或者私有成员函数
cout << "name = " << m_Human->m_Name << ", age = " << m_Human->m_Age << endl;
}
}

private:
shared_ptr<Human> m_Human;
};

int main() {
shared_ptr<Human> human = make_shared<Human>("Jim", 28);
Game game(human);
game.getHuman();
return 0;
}

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

1
2
3
4
5
Human::Human(string, int)
Game::Game()
name = Jim, age = 28
Game::~Game()
Human::~Human()

友元函数的使用

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

using namespace std;

class Human {
public:
Human() {
cout << "Human::Human()" << endl;
}

Human(const string& name, int age) : m_Name(name), m_Age(age) {
cout << "Human::Human(string, int)" << endl;
}

virtual ~Human() {
cout << "Human::~Human()" << endl;
}

void eat() const {
cout << m_Name << "Human eat food" << endl;
}

public:
// 声明友元函数
friend void show(const Human& human);

private:
int m_Age;
string m_Name;
};

// 友元函数
void show(const Human& human) {
// 在友元函数中,可以访问其他类的私有成员变量或者私有成员函数
cout << "name = " << human.m_Name << ", age = " << human.m_Age << endl;
}

int main() {
Human human("Jim", 18);
show(human);
return 0;
}

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

1
2
3
Human::Human(string, int)
name = Jim, age = 18
Human::~Human()

友元成员函数的使用

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

using namespace std;

// 类前向声明
class Human;

// 先完整声明 Game 类
class Game {
public:
Game(shared_ptr<Human> h);

~Game();

void getHuman() const;

private:
shared_ptr<Human> m_Human;
};

// 声明与定义 Human 类
class Human {
public:
Human(const string& name, int age) : m_Name(name), m_Age(age) {
cout << "Human::Human(string, int)" << endl;
}

virtual ~Human() {
cout << "Human::~Human()" << endl;
}

private:
int m_Age;
string m_Name;

// 声明友元成员函数
friend void Game::getHuman() const;
};

/////////////// 定义 Game 类 ///////////////

Game::Game(shared_ptr<Human> h) : m_Human(h) {
cout << "Game::Game()" << endl;
}

Game::~Game() {
cout << "Game::~Game()" << endl;
}

void Game::getHuman() const {
if (m_Human) {
// 在友元成员函数中,可以访问其他类的私有成员变量或者私有成员函数
cout << "name = " << m_Human->m_Name << ", age = " << m_Human->m_Age << endl;
}
}

int main() {
shared_ptr<Human> human = make_shared<Human>("Jim", 28);
Game game(human);
game.getHuman();
}

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

1
2
3
4
5
Human::Human(string, int)
Game::Game()
name = Jim, age = 28
Game::~Game()
Human::~Human()
  • 关键点
    • Game 类必须先完整声明成员函数
      • getHuman() 必须先声明,再在 Human 里声明为友元成员函数
    • 前向声明不能直接友元成员函数
      • 如果 Human 还没见过 Game 的完整声明,就无法指定友元成员函数
    • 友元成员函数可以访问 Human 私有成员,无需把整个 Game 类声明为友元

特别注意

在 C++ 中,有一个关键限制:只能把全局函数、其他类的成员函数、或者模板函数声明为友元;但是前向声明的类的成员函数不能直接作为友元,除非类完整声明后才可用。

RTTI、dynamic_cast、typeid、虚函数表

RTTI 的核心概念

  • RTTI 的概述

    • RTTI(Run Time Type Identification,运行时类型识别)是 C++ 提供的一种机制,用于在程序运行期间确定对象的实际类型。
    • 当通过基类指针或引用操作派生类对象时,可以借助 RTTI 查询对象的真实派生类型。
  • RTTI 的实现机制

    • 主要通过以下机制实现:
      • dynamic_cast —— 安全地进行向下类型转换(基类 → 派生类)
      • typeid —— 获取对象的实际类型信息
  • RTTI 的注意事项:

    • RTTI 仅对包含至少一个虚函数的多态类型(即必须至少有一个虚函数的基类)有效。
    • 必须通过基类的指针或引用访问对象,才能获取运行时的真实类型。

dynamic_cast 的使用

dynamic_cast 的概述

  • dynamic_cast 是动态类型转换,用于如父类和子类之间的多态类型转换。
  • dynamic_cast 在运行期会基于 RTTI 进行类型检查,但只能用于多态类型(即继承体系中必须存在虚函数),能保证下行转换的安全性。
  • dynamic_cast 可以用于类层次结构中的上行转换和下行转换,但是不支持基本数据类型的转换。
  • 在类层次结构中进行上行转换(将派生类的指针或者引用转换成基类表示)时,dynamic_caststatic_cast 的效果一样。
  • 在类层次结构中进行下行转换(将基类的指针或者引用转换成派生类表示)时,dynamic_cast 具有运行时类型检查的功能,比 static_cast 更安全。
  • 对于指针,如果 dynamic_cast 转换失败,会返回一个空指针(nullptr)。对于引用,如果 dynamic_cast 转换失败,会抛出 std::bad_cast 异常。

特别注意

在 C++ 的四种显式类型转换中,static_castconst_cast 会在编译期进行类型检查;reinterpret_cast 仅在编译期会做最基本的语法检查,不进行类型安全检查;dynamic_cast 会在运行期基于 RTTI 机制进行类型检查(仅适用于多态类型)。

dynamic_cast 的使用

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

using namespace std;

class Human {
public:
Human() {
cout << "Human::Human()" << endl;
}

Human(const string& name, int age) : m_Name(name), m_Age(age) {
cout << "Human::Human(string, int)" << endl;
}

virtual ~Human() {
cout << "Human::~Human()" << endl;
}

// 虚函数
virtual void sleep() {
cout << "Human::sleep()" << endl;
}

protected:
int m_Age;
string m_Name;
};

// 派生类
class Men : public Human {
public:
Men() {
cout << "Men::Men()" << endl;
}

Men(const string& name, int age) {
this->m_Age = age;
this->m_Name = name;
cout << "Men::Men(string, int)" << endl;
}

~Men() {
cout << "Men::~Men()" << endl;
}

virtual void sleep() override {
cout << "Men::sleep()" << endl;
}

void eat() {
cout << "Men::eat()" << endl;
}
};

int main() {
// 父类的指针指向子类的对象
Human* human = new Men("Jim", 18);

// 多态调用,最终调用子类的成员函数 Men::sleep()
human->sleep();

// 安全的向下类型转换(父类 --> 子类)
Men* men = dynamic_cast<Men*>(human);

// 判断类型转换是否成功
if (men != nullptr) {
// 向下类型转换后,最终调用子类的成员函数 Men::eat()
men->eat();
}

delete human;

return 0;
}

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

1
2
3
4
5
6
Human::Human()
Men::Men(string, int)
Men::sleep()
Men::eat()
Men::~Men()
Human::~Human()

typeid、type_info 的使用

typeid 的介绍

  • typeid 的概述

    • typeid 用于获取类型信息,返回值类型为 const std::type_info&,即返回一个 std::type_info 常量对象的引用。
    • 对于多态类型,通过基类指针或引用可在运行时获取对象的真实类型。
  • typeid 的用法

    • 语法一:typeid(类型)
    • 语法二:typeid(表达式)

typeid 的使用情况一

  • 情况一:基础类型
    • typeid(表达式) 在编译期确定类型
    • 不会发生运行时类型识别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

using namespace std;

int main() {
int a = 10;
char b[10] = {5, 1};
float c = 0.32;
double d = 3.14;
string e = "abc";

cout << typeid(a).name() << endl; // int
cout << typeid(b).name() << endl; // char [10]
cout << typeid(c).name() << endl; // float
cout << typeid(d).name() << endl; // double
cout << typeid(e).name() << endl; // class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char>>
}

typeid 的使用情况二

  • 情况二:非多态类型(没有虚函数)
    • typeid(表达式) 在编译期确定类型
    • 不会发生运行时类型识别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

using namespace std;

class A {

};

int main() {
A a;

// 编译期确定类型为 A
cout << typeid(a).name() << endl;

// 判断是什么类型
if (typeid(a) == typeid(A)) {
cout << "a 是 A 类型" << endl;
} else {
cout << "a 是其他类型" << endl;
}

return 0;
}

typeid 的使用情况三

  • 情况三:多态类型(存在虚函数)
    • 如果:
      • 基类中存在至少一个虚函数
      • 通过基类指针或引用访问派生类对象
    • 那么:
      • typeid(表达式) 会在运行时获取对象的真实类型
      • 依赖 RTTI 机制(运行时类型识别机制)
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
#include <iostream>

using namespace std;

class Base {
public:
// 虚析构函数
virtual ~Base() {
}
};

class Derived : public Base {

};

int main() {
// 父类的指针指向子类的对象
Base* p = new Derived();

// 运行时识别类型为 Derived
cout << typeid(*p).name() << endl;

// 判断是什么类型
if (typeid(*p) == typeid(Base)) {
std::cout << "指针 p 指向 Base 类型" << endl;
} else if (typeid(*p) == typeid(Derived)) {
std::cout << "指针 p 指向 Derived 类型" << endl;
} else {
std::cout << "指针 p 指向未知类型" << endl;
}

return 0;
}

特别注意

比较 typeid 返回的类型时,不要使用 std::type_info::name() 进行字符串比较,因为 std::type_info::name() 返回的是实现相关的字符串(可能经过修饰),不同编译器返回的结果不相同,不具备可移植性,不能作为可靠的类型判断依据。应该使用 std::type_info::operator== 进行类型比较,例如 typeid(*p) == typeid(Derived)。如果目的是判断对象是否属于某个派生类并进行安全的向下类型转换,则更推荐使用 dynamic_cast,如 if (Derived* d = dynamic_cast<Derived*>(p)) { },这样既能判断类型,又能安全使用派生类。

RTTI 与虚函数表的关系

什么是虚函数表

  • 虚函数表的概述

    • 在 C++ 中,如果一个类中包含至少一个虚函数,编译器通常会为该类生成一个虚函数表(vtable)。
  • 虚函数表的本质

    • 虚函数表本质上是一个函数指针数组
    • 表中的每一项都是一个函数入口地址
    • 每个包含虚函数的类,通常都会对应一个虚函数表
    • 每个对象内部会保存一个指向虚函数表的指针(通常称为 vptr

虚函数表的结构

1
2
3
4
5
6
7
8
对象内存布局:

+-------------------+
| vptr -----------+----> 虚函数表(vtable)
+-------------------+ |
| 成员变量1 | |--> 虚函数1地址
| 成员变量2 | |--> 虚函数2地址
+-------------------+ |--> ...

虚函数表中的内容

  • 虚函数表中通常包含:
    • 各个虚函数的入口地址
    • 析构函数的地址
    • 与 RTTI 相关的信息

RTTI 与虚函数表的关系

  • 当类是多态类型(包含虚函数)时:

    • 编译器不仅会生成虚函数表
    • 还会为该类生成一个 type_info 对象
    • 该对象记录类的类型信息
    • 虚函数表中通常会包含一个指向该 type_info 对象的指针
  • 因此:

    • RTTI 机制通常依赖虚函数表来实现运行时类型识别
  • 当调用:

    1
    typeid(*p)
  • 如果 p 是基类指针且类为多态类型:

    • 程序会通过对象中的虚函数表指针(vptr
    • 找到虚函数表
    • 再获取与之关联的 type_info
    • 从而得到对象的真实运行时类型
  • 关键结论

    • 没有虚函数 → 没有多态 → 不会产生完整的 RTTI 运行时行为
    • 必须通过基类指针或引用访问对象,RTTI 才能识别真实派生类型
    • RTTI 的底层实现通常依赖虚函数表

总结

在 C++ 中,只要类包含虚函数,编译器就会生成虚函数表;RTTI 机制通常依赖虚函数表中的类型信息指针来实现运行时类型识别。