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

using namespace std;

// 定义基类(父类)
class Parent {

public:
Parent(int a = 0, int b = 0) {
this->a = a;
this->b = b;
}

void print() {
cout << "a=" << this->a << ", b=" << this->b << endl;
}

public:
int a;
int b;
};

// 定义派生类(子类),继承方式是 public
class Child : public Parent {

public:

Child(int a = 0, int b = 0, int c = 0) {
// 直接访问基类(父类)的 public 成员变量
this->a = a;
this->b = b;
this->c = c;
}

void echo() {
cout << "a=" << this->a << ", b=" << this->b << ", c=" << this->c << endl;
}

private:
int c;
};

int main() {
Child child(1, 2, 3);
child.print(); // 直接调用基类(父类)的 public 成员函数
child.echo(); // 直接调用派生类(子类)的 public 成员函数
return 0;
}

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

1
2
a=1, b=2
a=1, b=2, c=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
#include <iostream>

using namespace std;

class Base {

public:
Base(int data) : ma(data) {
cout << "Base()" << endl;
}

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

protected:
int ma;

};

class Device : public Base {

public:
// 显式调用 Base 的构造函数初始化 ma
Device(int data) : Base(data), mb(data) {
cout << "Device()" << endl;
}

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

private:
int mb;

};

int main() {
Device d(2); // 先构造的后析构,即后构造的先析构
return 0;
}

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

1
2
3
4
Base()
Device()
~Device()
~Base()

重载、重写、隐藏

  • 重载关系

    • 一组函数要重载,必须处在同一个作用域当中,且函数名字相同,但参数列表不同。
  • 隐藏关系

    • 在继承结构当中,派生类的同名成员会将基类的同名成员给隐藏掉,这里的隐藏是指作用域的隐藏。
  • 重写关系

    • 基类和派生类的函数,其函数名、返回值以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就会被编译器自动处理成虚函数,它们之间成为重写(覆盖)关系。

案例代码一

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

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

void show() {
cout << "Base::show()" << endl;
}

void show(int a) {
cout << "Base::show(int)" << endl;
}

private :
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device;
device.show();
// device.show(20); // 错误写法,派生类的 show() 函数将基类的 show(int a) 函数隐藏了,无法正常调用基类的同名函数
device.Base::show(20); // 正确写法,派生类加上作用域可以正常调用基类的同名函数
return 0;
}

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

1
2
Device::show()
Base::show()

案例代码二

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

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

void show() {
cout << "Base::show()" << endl;
}

void show(int a) {
cout << "Base::show(int)" << endl;
}

private :
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

void test01() {
cout << "=========== test01() ===========" << endl;
Base base;
Device device;
base = device; // 正确写法,类型从下到上的转换
// device = base; // 错误写法,类型从上到下的转换
base.show();
base.show(30);
}

void test02() {
cout << "=========== test02() ===========" << endl;
Base base;
Device device;

Base *_base = &device; // 正确写法,类型从下到上的转换
// Device *_device = &base; // 错误写法,类型从上到下的转换
_base->show();
_base->show(40);
}

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

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

1
2
3
4
5
6
=========== test01() ===========
Base::show()
Base::show(int)
=========== test02() ===========
Base::show()
Base::show(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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <typeinfo>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

void show() {
cout << "Base::show()" << endl;
}

void show(int num) {
cout << "Base::show(int num)" << endl;
}

private:
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device(50);

Base *pb = &device;
pb->show(); // 静态(编译时期)绑定(函数的调用)
pb->show(20); // 静态(编译时期)绑定(函数的调用)

cout << sizeof(Base) << endl;
cout << sizeof(Device) << endl;

cout << typeid(pb).name() << endl; // class Base *
cout << typeid(*pb).name() << endl; // class Base

return 0;
}

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

1
2
3
4
5
6
Base::show()
Base::show(int num)
4
8
class Base *
class Base

动态绑定(带虚函数)

  • 动态绑定(使用虚函数)
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
#include <iostream>
#include <typeinfo>

using namespace std;

class Base {

public:
Base(int data = 10) : ma(data) {

}

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

// 虚函数
virtual void show(int num) {
cout << "Base::show(int num)" << endl;
}

private:
int ma;

};

class Device : public Base {

public:
Device(int data = 20) : Base(data), mb(data) {

}

void show() {
cout << "Device::show()" << endl;
}

private:
int mb;

};

int main() {
Device device(50);

Base *pb = &device;
pb->show(); // 动态(运行时期)绑定(函数的调用)
pb->show(20); // 动态(运行时期)绑定(函数的调用)

cout << sizeof(Base) << endl;
cout << sizeof(Device) << endl;

cout << typeid(pb).name() << endl; // class Base *
cout << typeid(*pb).name() << endl; // class Device

return 0;
}

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

1
2
3
4
5
6
Device::show()
Base::show(int num)
16
16
class Base *
class Device

查看类的内存布局

在 Visual Studio 开发人员命令提示窗口内,可以使用以下命令查看类的内存布局信息,其中 YYY 是类的名称,xxx 是 C++ 源文件的名称。

1
cl /d1 reportSingleClassLayoutYYY xxx.cpp
  • 在上述动态绑定(带虚函数)的代码中,Base 类的内存布局如下:

  • 在上述动态绑定(带虚函数)的代码中,Device 类的内存布局如下:

提示

更多关于 Visual Studio 命令的详细使用教程,请阅读 《使用 VS 查看对象模型以及结构体内存对齐》

虚函数的使用总结

  • (1) 当一个类里面定义了虚函数,那么在编译阶段,编译器会给这个类的类型产生一个唯一的 vftable 虚函数表,虚函数表中主要存储的内容是 RTTI 指针和虚函数的地址(如图所示)。当程序运行时,每一张虚函数表都会加载到内存的 .rodata 区。

  • (2) 当一个类里面定义了虚函数,那么这个类定义的对象在其运行时,内存中开始的部分会多存储一个 vfptr 虚函数指针(占 4 字节大小),它指向相应类型的虚函数表 vftable。一个类定义 N 个对象,它们的 vfptr 虚函数指针指向的都是同一张虚函数表。

  • (3) 如果派生类中的函数和从基类继承来的某个函数,其函数名、返回值、参数列表都相同,而且基类的函数是 virtua1 关键字修饰的,那么派生类的这个函数会被编译器自动处理成虚函数。

  • (4) 一个类里面虚函数的个数,不影响类对象的内存大小(vfptr 虚函数指针永远只占用 4 个字节大小),影响的是虚函数表的大小。