C++ 入门基础之七

大纲

继承概念

面向对象程序设计有 4 个主要特点:抽象、封装、继承和多态。面向对象程序设计的两个重要特征一数据抽象与封装,两者已经能够设计出基于对象的程序,这是面向对象程序设计的基础。要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特征 —— 继承和多态。继承是面向对象程序设计最重要的特征,可以说,如果没有掌握继承,就等于没有掌握类和对象的精华,就是没有掌握面向对象程序设计的真谛。

类之间的关系

类之间一般有三种关系:

  • has-A:包含关系,用以描述一个类由多个 “部件类” 构成。实现 has-A 关系可以用类成员表示,即一个类中的数据成员是另一种已经定义的类。
  • uses-A:一个类部分地使用另一个类。类之间成员函数的联系,可以通过定义友元或者对象参数传递来实现。
  • is-A:机制称为 “继承” 。关系具有传递性,不具有对称性。

继承关系举例

继承相关概念

派生类的定义

值得一提的是,C++ 中的继承方式(public、private、protected)会影响子类的对外访问属性。

继承重要说明

  • (a) 子类拥有父类的所有成员变量和成员函数
  • (b) 子类可以拥有父类没有的方法和属性
  • (c) 子类就是一种特殊的父类
  • (d) 子类对象可以当作父类对象使用

继承使用案例

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++ 中,类成员变量和类成员函数的访问级别分为:publicprivateprotected

  • private:修饰的成员变量和成员函数,只能在类的内部被访问
  • public:修饰的成员变量和成员函数,可以在类的内部和类的外部被访问
  • protected:修饰的成员变量和成员函数,可以在派生类(子类)的内部访问,不能在派生类(子类)的外部被访问

特别注意

在类中没有声明访问控制级别的成员变量和成员函数,默认都是 private 访问级别的。

继承成员的访问控制

在 C++ 中,不同的继承方式(publicprivateprotected)会改变继承成员的访问属性:

  • public 继承:父类成员在子类中保持原有的访问级别
  • private 继承:父类成员在子类中都变为 private 成员
  • protected 继承:父类中 public 成员会变成 protected,父类中 private 成员仍然为 private,父类中 protected 成员仍然为 protected

特别注意

父类的 private 成员在子类中依然存在,但是无法访问到的,即无论使用哪种方式继承父类,子类都不能直接使用父类的 private 成员。

继承成员访问控制的 “三看” 原则

在 C++ 中,不同的继承方式(publicprivateprotected)会改变继承成员的访问属性,最终可总结为以下三个原则(通过某一个原则,判断是否可以被访问):

  • (a) 看调用语句是写在子类的内部还是外部
  • (b) 看子类如何从父类继承(publicprivateprotected
  • (c) 看父类中的访问级别(publicprivateprotected

派生类成员访问级别的控制原则

对于派生类自身的成员,访问级别的控制原则如下:

  • (a) 需要被外界访问的成员直接设置为 public
  • (b) 只能在当前类中访问的成员设置为 private
  • (c) 只能在当前类和子类中访问的成员设置为 protected

使用工具查看类的内存布局信息

Windows 系统环境

在 Visual Studio 开发人员命令提示窗口内,可以使用以下命令查看类的内存布局信息。

  • C++ 代码中的继承关系
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {

public:
int m_index;

};

class B : public A {

public:
int m_age;

};
  • 使用命令查看类的内存布局信息,其中 YYY 是类的名称,xxx 是 C++ 源文件的名称
1
cl /d1 reportSingleClassLayoutYYY xxx.cpp
  • 命令输出的结果

提示

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

Linux 系统环境

在 Linux 系统中,可以使用 GNU Compiler Collection(GCC)提供的一些工具来查看类的内存布局。其中一个最常用的工具是 g++,它是 GCC 的 C++ 编译器。可以使用 g++ 编译器的 -fdump-class-hierarchy 选项来生成类的内存布局信息,如下所示:

1
g++ -fdump-class-hierarchy xxx.cpp

这将生成一个名为 xxx.cpp.class 的文件,其中包含了类的内存布局信息,可以打开这个文件查看生成的信息。另外,还可以使用 objdump 命令来反汇编一个已编译的程序,然后查看类的内存布局。这种方法更加复杂,但在某些情况下也很有用。

继承中的构造和析构

类型兼容原则

类型兼容规则是指在需要基类(父类)对象的任何地方,都可以使用公有派生类(公有继承)的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。值得一提的是,在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承得到的成员,类型兼容规则是多态的重要基础之一。类型兼容规则中所指的替代包括以下情况:

  • 子类对象可以当作父类对象使用
  • 子类对象可以直接赋值给父类对象
  • 子类对象可以直接初始化父类对象
  • 父类指针可以直接指向子类对象
  • 父类引用可以直接引用子类对象
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
#include <iostream>

using namespace std;

// 父类
class Parent {

public:
void printParent() {
cout << "I'm parent" << endl;
}

private:
int a;
};

// 子类(公有继承)
class Child : public Parent {

public:
void printChild() {
cout << "I'm child" << endl;
}

private:
int c;
};

void howToPrint(Parent* p) {
p->printParent();
}

void howToPrint(Parent& p) {
p.printParent();
}

int main() {
Parent p1;
p1.printParent();

Child c1;
c1.printChild();
c1.printParent();

// 1-1 父类指针可以直接指向子类对象
cout << "1-1" << endl;
Parent* p2 = NULL;
p2 = &c1;
p2->printParent();

// 1-2 父类指针可以直接指向子类对象,指针做函数参数
cout << "1-2" << endl;
howToPrint(&p1);
howToPrint(&c1);

// 2-1 父类引用可以直接引用子类对象
cout << "2-1" << endl;
Parent& p3 = c1;
p3.printParent();

// 2-2 父类引用可以直接引用子类对象,引用做函数参数
cout << "2-2" << endl;
howToPrint(p1);
howToPrint(c1);

// 3-1 子类对象可以直接初始化父类对象,会自动调用父类的拷贝构造函数
cout << "3-1" << endl;
Parent p4 = c1;
p4.printParent();

// 4-1 子类对象可以直接赋值给父类对象
cout << "4-1" << endl;
Parent p5;
p5 = c1;
p5.printParent();

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
I'm parent
I'm child
I'm parent
1-1
I'm parent
1-2
I'm parent
I'm parent
2-1
I'm parent
2-2
I'm parent
I'm parent
3-1
I'm parent
4-1
I'm parent

继承中的对象模型

类在 C++ 编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员得到的。

父类与子类的构造函数、析构函数的关系如下:

  • 在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化
  • 在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理

继承中的构造与析构的调用原则

  • (a) 子类对象在创建时,会首先调用父类的构造函数
  • (b) 父类构造函数执行结束后,再执行子类的构造函数
  • (c) 当父类只存在有参构造函数时,必须在子类的初始化列表中显示调用父类的构造函数
  • (d) 析构函数调用的先后顺序与构造函数相反,即先调用子类的析构函数,再调用父类的析构函数
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
#include <iostream>

using namespace std;

class Parent {

public:

Parent(int a, int b) {
this->a = a;
this->b = b;
cout << "父类的构造函数被调用" << endl;
}

~Parent() {
cout << "父类的析构函数被调用" << endl;
}

void printParent() {
cout << "I'm parent, a = " << this->a << ", b = " << this->b << endl;
}

private:
int a;
int b;
};

class Child : public Parent {

public:

// 当父类只存在有参构造函数时,必须在子类的初始化列表中显示调用
Child(int a, int b, int c) : Parent(a, b) {
this->c = c;
cout << "子类的构造函数被调用" << endl;
}

~Child() {
cout << "子类的析构函数被调用" << endl;
}

void printChild() {
cout << "I'm child, c = " << this->c << endl;
}

private:
int c;
};

int main() {
Child c1(1, 2, 3);
c1.printParent();
c1.printChild();
return 0;
}

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

1
2
3
4
5
6
父类的构造函数被调用
子类的构造函数被调用
I'm parent, a = 1, b = 2
I'm child, c = 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
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
#include <iostream>

using namespace std;

class Object {

public:
Object(int a, int b) {
this->a = a;
this->b = b;
cout << "Object类的构造函数被调用" << endl;
}

~Object() {
cout << "Object类的析构函数被调用" << endl;
}

void printObject() {
cout << "I'm object, a = " << this->a << ", b = " << this->b << endl;
}

protected:
int a;
int b;
};

class Parent : public Object {

public:

// 通过初始化列表,调用父类的构造函数
Parent(char* p) : Object(1, 2) {
this->p = p;
cout << "Parent类的构造函数被调用" << endl;
}

~Parent() {
cout << "Parent类的析构函数被调用" << endl;
}

void printParent() {
cout << "I'm parent, p = " << p << endl;
}

protected:
char* p;
};

class Child : public Parent {

public:

// 通过初始化列表,调用组合对象与父类的构造函数
Child(char* c) : obj1(3, 4), obj2(5, 6), Parent(c) {
this->c = c;
cout << "Child类的构造函数被调用" << endl;
}

~Child() {
cout << "Child类的析构函数被调用" << endl;
}

void printChild() {
cout << "I'm child, p = " << p << endl;
}

protected:
char* c;
// 组合对象
Object obj1;
Object obj2;
};

int main() {
char* str = new char[3];
str[0] = 'J';
str[1] = 'i';
str[2] = 'm';

Child c1(str);
c1.printChild();
c1.printParent();
c1.printObject();

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
Object类的构造函数被调用
Parent类的构造函数被调用
Object类的构造函数被调用
Object类的构造函数被调用
Child类的构造函数被调用
I'm child, p = Jim
I'm parent, p = Jim
I'm object, a = 1, b = 2
Child类的析构函数被调用
Object类的析构函数被调用
Object类的析构函数被调用
Parent类的析构函数被调用
Object类的析构函数被调用

继承中的同名成员的处理方式

  • 当子类成员与父类成员同名时,子类不会覆盖父类的同名成员,子类依旧可以从父类继承同名成员。
  • 在子类中通过作用域限定运算符 :: 进行同名成员的区分(即在子类中使用父类的同名成员时,需要显式地使用类名限定符),其作用类似 Java 中的 super 关键字。
  • 如果子类和父类的成员函数同名,子类会把父类的所有的同名成员函数都隐藏掉,子类必须通过作用域限定运算符 :: 显式调用父类的同名成员函数。
  • 特别注意,同名成员存储在内存中的不同位置。

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>

using namespace std;

class Parent {

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

void print() {
cout << "I'm parent, a = " << a << ", b = " << b << endl;
}

public:
int a;
int b;
};

class Child : public Parent {

public:

Child(int a, int b) : Parent(a, b) {
this->a = a + 5;
this->b = b + 5;
}

void print() {
cout << "I'm child, a = " << a << ", b = " << b << endl;
}

public:
int a;
int b;
};

int main() {
Child child(1, 2);

// 子类访问自身的同名成员函数
child.print();

// 子类访问自身的同名成员变量
cout << "child's a = " << child.a << endl;
cout << "child's b = " << child.b << endl;

// 子类访问父类的同名成员函数
child.Parent::print();

// 子类访问父类的同名成员变量
cout << "parent's a = " << child.Parent::a << endl;
cout << "parent's b = " << child.Parent::b << endl;

return 0;
}

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

1
2
3
4
5
6
I'm child, a = 6, b = 7
child's a = 6
child's b = 7
I'm parent, a = 1, b = 2
parent's a = 1
parent's b = 2

派生类中的 static 关键字使用

在 C++ 的普通类中,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 Parent {

public:
// 声明公有的静态成员函数
static void print() {
cout << "a = " << a << ", b = " << b << endl;
}

public:
// 声明公有的静态成员变量
static int a;

private:
// 声明私有的静态成员变量
static int b;
};

// 定义私有的静态成员变量
int Parent::b = 50;

// 定义公有的静态成员变量,这里不是简单的变量赋值,更重要的是告诉C++编译器,给静态成员变量分配内存, 否则在派生类中用到该变量就会报错
int Parent::a = 30;

class Child : public Parent {

public:
int getA() {
// 访问从基类继承得到的静态成员变量
return this->a;
}

int getA2() {
// 访问基类的静态成员变量
return Parent::a;
}

int getB() {
// return b; 错误写法,基类中静态成员自身的访问特性遵守派生类的访问级别控制原则,因此这里不能访问基类中私有的静态成员变量b
return 0;
}

// 调用从基类继承得到的静态成员函数
void print2() {
this->print();
}

// 调用基类的静态成员函数
void print1() {
Parent::print();
}
};

int main() {

// 在类外访问基类的静态成员变量和静态成员函数
Parent::a++;
Parent::print();
cout << endl;

// 在类外访问派生类的静态成员变量和静态成员函数
cout << "a = " << Child::a << endl;
Child::print();
cout << endl;

Child c1;

cout << "a = " << c1.getA() << endl;
cout << "a = " << c1.getA2() << endl;
cout << "a = " << c1.Parent::a << endl;
cout << endl;

c1.print1();
c1.print2();
c1.Parent::print();

return 0;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
a = 31, b = 50

a = 31
a = 31, b = 50

a = 31
a = 31
a = 31

a = 31, b = 50
a = 31, b = 50
a = 31, b = 50