C++面向对象(二) —— 继承

本文最后更新于:2021年1月27日 下午

概览:C++面向对象之继承,继承实例、覆盖、多继承与虚继承、基类派生类对象兼容性赋值。


代码全部运行于VS2019

为简化考虑,部分源码省略了#include<iostream>以及using namespace std

博客后续会持续更新补充。

面向对象

类的基本思想是数据抽象( data abstraction) 和封装(encapsulation)。 数据抽象是一种依赖于接口 (interface) 和 实现 (implementation) 分离的编程(以及设计)技术。类的接口包括用户所能执行的操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。

面向对象程序设计(object oriented programming)的核心思想是数据抽象、继承和动态绑定。通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。

—— 《C++ Primer 第五版》

简单的继承

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

class Animal
{
public:
Animal() :name(""), age(0), sex(true) {}
Animal(string name, int age, bool sex) :
name(name), age(age), sex(sex) {}
~Animal() {}

void showInfo()
{
cout << "name: " << name << ",age: " << age << ", sex: "
<< (sex ? "male" : "female") << endl;
}

static string message;
private:
string name;
int age;
bool sex;
};

//静态变量赋值
string Animal::message = "on Earth";

class Cat :public Animal
{
public:
Cat() :Animal(), lovething("") {}
Cat(string name, int age, bool sex, string lthing) :
Animal(name, age, sex), lovething(lthing) {}
~Cat() {}

void makeSound()
{
cout << "meow~" << endl;
}
private:
string lovething;
};

int main()
{
Animal a("Bob",3,true);
a.showInfo(); //name: Bob,age: 3, sex: male
cout << a.message << endl; //on Earth

Cat b("kelly",7,false,"fish");
b.showInfo(); //name: kelly,age: 7, sex: female
//派生类并未定义这个函数,但是依旧可以访问
b.makeSound(); //meow~
cout << b.message << endl; //on Earth

a.message = "Earth";
cout << b.message << endl; //Earth

return 0;
}

继承是一种将C++的类联系在一起构成一种层级关系。在层次关系的底部的是基类(base class),而其他的类都是直接或者间接地从基类继承而来,这些由继承而得到的类称为派生类(derived class)。基类负责定义在层次关系中所有类都共同拥有的成员,而派生类则额外定义自己特有的成员。

另外,基类希望它的某些函数派生类各自定义适合自身的版本,此时就会将这些函数声明为虚函数(virtual function)。

例如定义一个基类动物类Animal,定义一个派生类Cat。Animal中包含一些动物拥有的特性,而Cat则是猫这种动物的特性,Cat就是Animal中的一种。是Cat is Animal的这种关系。

可以说Cat继承于Animal,或者说从Animal派生Cat。

此外也称Animal基类为父类,Cat派生类为子类

  • 子类拥有父类的全部成员与函数!
  • 继承的形式:派生类后接:,然后接类派生列表,即 继承权限 类名,要继承多个就使用逗号分隔。
  • 如果基类定义了一个静态成员则在这个继承体系之中只存在这这个成员的唯一实例。
  • 在派生类中基类的私有数据成员要使用基类的构造函数来初始化。
  • 友元不能被继承

派生类对象的内存空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A
{
private:
int a;
int b;
};

class B:public A
{
private:
int c;
};

int main()
{
A a;
cout << sizeof(a) << endl;//8

B b;
cout << sizeof(b) << endl;//12
}

派生类对象之中包含着基类对象,且基类对象存储位置位于派生类对象新增的成员变量之前。就相当于基类对象是头部。

  • 派生类对象的大小 = 基类对象成员大小 + 派生类对象自己成员变量的大小。

  • 即使基类中的私有成员派生类不能访问,但是依然存在于派生类之中。

派生类继承的访问权限

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
class Animal
{
public:
Animal() :age(0){}
Animal(int age) : age(age){}
~Animal() {}

void testPbulicFun()
{
cout << "Public function" << endl;
}

protected:
void testProtecteFun()
{
cout << "Protected function" << endl;
}

private:
int age;
void testPrivateFun()
{
cout << "Private function" << endl;
}
};

//公有继承
class Cat :public Animal
{
public:
Cat() :Animal() {}
Cat(int age) :Animal(age) {}
~Cat() {}

void testPublicInher()
{
this->testPbulicFun();
this->testProtecteFun(); //正确,可在类内访问基类的保护成员
//this->testPrivateFun(); //错误,无法访问基类的私有成员
}
};

//保护继承
class Dog :protected Animal
{
public:
Dog() :Animal() {}
Dog(int age):Animal(age) {}
~Dog() {}

void testProtecteInher()
{
this->testPbulicFun(); //正确,可在类内访问基类的共有成员
this->testProtecteFun(); //正确,可在类内访问基类的保护成员
//this->testPrivateFun(); //错误,无法访问基类的私有成员
}
};

//私有继承
class Bull :private Animal
{
public:
Bull() :Animal() {}
Bull(int age) :Animal(age) {}
~Bull() {}

void testPrivateInher()
{
this->testPbulicFun(); //正确,可在类内访问基类的共有成员
this->testProtecteFun(); //正确,可在类内访问基类的保护成员
//this->testPrivateFun(); //错误,无法访问基类的私有成员
}

};

int main()
{
/*----------------------------基类------------------------------*/
Animal a(1);
a.testPbulicFun(); //正确,可以在类外访问类的公有成员
//a.testProtecteFun(); //错误,无法在类外访问类的保护成员
//a.testPrivateFun(); //错误,无法在类外访问类的私有成员

/*-------------------------公有继承类---------------------------*/
Cat b(2);
b.testPublicInher();
b.testPbulicFun(); //正确,可以在类外访问基类的公有成员
//b.testProtecteFun(); //错误,无法在类外访问基类的保护成员
//b.testPrivateFun(); //错误,无法在类外访问基类的私有成员

/*-------------------------保护继承类---------------------------*/
Dog c(3);
c.testProtecteInher();
//c.testPbulicFun(); //错误,无法在类外访问基类的公有成员
//c.testProtecteFun(); //错误,无法在类外访问基类的保护成员
//c.testPrivateFun(); //错误,无法在类外访问基类的私有成员

/*-------------------------私有继承类---------------------------*/
Bull d(4);
d.testPrivateInher();
//d.testPbulicFun(); //错误,无法在类外访问基类的公有成员
//d.testProtecteFun(); //错误,无法在类外访问基类的保护成员
//d.testPrivateFun(); //错误,无法在类外访问基类的私有成员

return 0;
}

  • 继承有public、protected和private三种继承方式.

  • 只要是基类中的private成员,无论如何继承基类,派生类都无法访问。

  • 公有(public)继承,基类的访问权限不变。

  • 保护(protected)继承,基类除了private成员,其余访问权限都变成protected。

  • 私有(private)继承,基类除了private成员,其余访问权限都变成private。

  • 私有的成员一直都是无法访问的,在派生类中想给基类成员赋值需要通过父类构造函数来完成。

构造函数与析构函数

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
class Parent
{
public:
Parent()
{
cout << "Parent()" << endl;
this->a = 0;
}
Parent(int a)
{
cout << "Parent(int)" << endl;
this->a = a;
}
~Parent()
{
cout << "~Parent()" << endl;
}
private:
int a;
};

class Child:public Parent
{
public:
Child()
{
cout << "Child()" << endl;
this->b = 0;
}
Child(int a, int b):Parent(a)
{
cout << "Child(int,int)" << endl;
this->b = b;
}
~Child()
{
cout << "~Child()" << endl;
}
private:
int b;
};

int main()
{
{
Child ch;
}

{
Child ch1(1, 2);
}
}

执行结果

1
2
3
4
5
6
7
8
Parent()
Child()
~Child()
~Parent()
Parent(int)
Child(int,int)
~Child()
~Parent()
  • 派生类的构造函数在构造对象时一定会调用基类的构造函数。即使是无参构造也会调用父类的构造函数。
  • 执行构造函数时,会先调用基类的构造函数,父类还有父类就继续向上。
  • 当需要调用基类的有参构造函数时,需要写在初始化列表的位置
  • 析构函数则是先触发派生类的,再触发基类的,辈分由小到大式的触发,即与构造函数调用顺序相反。

覆盖 —— 派生类成员与基类成员重名

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
class Parent
{
public:
Parent() :a(0) {}
void func()
{
cout << "Parent func" << endl;
}
public:
int a;//公有权限
};

class Child:public Parent
{
public:
Child():a(0) {}
//与基类函数同名
void func()
{
cout << "Child func" << endl;
}

void myFunc()
{
a = 10;
Parent::a = 100;//可以通过这种方式访问基类的同名成员
func();
Parent::func();
}
private:
int a;//与基类成员同名
};

int main()
{
Child ch;
ch.func();
ch.myFunc();
}

执行结果:

Child func
Child func
Parent func

  • 当派生类中的成员与基类成员同名时,这就是覆盖
  • 当在派生类中访问这一类成员时,默认访问的是派生类自己定义的成员。想要访问基类定义的同名成员时需要加上类名以及作用域符号::来区分。
  • 即使基类中又多个参数不同构成重载的成员函数,只要派生类有同名的成员函数,派生类就会隐藏全部的基类函数,必须通过基类名::函数名()的方式调用。
  • 基类派生类的同名函数没有重载关系

多继承

多继承是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
class Wood {
public:
int m = 0;
};

class Bed :public Wood {};

class Sofa :public Wood {};

//多继承。这个例子是菱形继承
//
// Wood
// / \
// Sofa Bed
// \ /
// SofaBed
class SofaBed :public Sofa, public Bed {};

void test2() {
//访问数据m
SofaBed sb;

//cout << "SofaBed m: " << sb.m << endl; //错误,不明确
cout << "SofaBed Bed::m: " << sb.Bed::m << endl; //0
cout << "SofaBed Sofa::m: " << sb.Sofa::m << endl; //0
}

但是多继承在特殊情况下会带来一些问题。比如上述例子中的sofabed都是继承于同一个基类wood,那基类wood所有的东西比如材质m,派生类sofabed都会用这份材质m,那么多继承的sofabed就会有两份m了。

但是按照逻辑来说,这份材质还只应该出现一次的,于是就有了虚继承

虚继承

两个基类BedSofa需继承它们的基类,即在继承前加入关键字virtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Wood {
public:
int m = 0;
};

class Bed :virtual public Wood {};

class Sofa :virtual public Wood {};

//多继承
class SofaBed :public Sofa, public Bed {};

void test2() {
//访问数据m
SofaBed sb;

cout << "SofaBed m: " << sb.m << endl; //0
cout << "SofaBed Bed::m: " << sb.Bed::m << endl; //0
cout << "SofaBed Sofa::m: " << sb.Sofa::m << endl; //0
}
  • 虚拟继承是多重继承中特有的概念,虚拟继承是为了解决多重继承而出现的。
  • 一般不建议使用,结构复杂,且内存开销比较大。

虚继承的类对象大小

虚继承下类的大小

普通模式下:

1
2
3
4
5
6
7
8
9
10
11
12
class Wood { int m; };
class Bed :public Wood { };
class Sofa :public Wood { };

class BedSofa :public Bed, public Sofa {};

void test1() { //x86
cout << "Wood sizeof = " << sizeof(Wood) << endl; //4
cout << "Bed sizeof = " << sizeof(Bed) << endl; //4
cout << "Sofa sizeof = " << sizeof(Sofa) << endl; //4
cout << "BedSofa sizeof = " << sizeof(BedSofa) << endl; //8
}

虚继承之后

1
2
3
4
5
6
7
8
9
10
11
12
class Wood { int m; };
class Bed :virtual public Wood{ };
class Sofa :virtual public Wood { };

class BedSofa :public Bed, public Sofa {};

void test1() { //x86
cout << "Wood sizeof = " << sizeof(Wood) << endl; //4
cout << "Bed sizeof = " << sizeof(Bed) << endl; //8
cout << "Sofa sizeof = " << sizeof(Sofa) << endl; //8
cout << "BedSofa sizeof = " << sizeof(BedSofa) << endl; //12
}

类的大小与虚表有关,见多态。

子类与父类对象之间的关系–兼容性赋值原则

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
class Father
{
public:
Father() { num = 0; }
void info() { cout << "hello\n"; }
private:
int num;
};

class Son :public Father
{
public:
Son() { d_num = 0; }
void s_info() { cout << "hi\n"; }
private:
double d_num;
};

int main()
{
Son s;
//1.派生类对象可以当基类对象使用,因为派生类对象拥有基类对象的全部方法。
s.info();

//2.派生类对象可以直接赋值给基类对象
Father f;
f = s;
f.info();

//3.派生类对象可以直接初始化基类对象
Father f1(s);
f1.info();

//4.基类指针可以直接指向派生类对象
Father* f2 = &s;
f2->info();

//5.基类引用可以直接引用派生类对象
Father& f3 = s;
f3.info();
}

当继承方式是public继承时:

  1. 子类对象可以当作父类对象使用。
  2. 子类对象可以直接赋值给父类对象。
  3. 子类对象可以直接初始化父类对象
  4. 父类指针可以直接指向子类对象
  5. 父类引用可以直接引用子类对象。

一个原因,根据父类与子类的内存分布决定,父类的结构子类有,而子类的结构父类只有一部分。子类的内存布局可以满足父类指针的所有需求.

而当继承方式是protected或者private时,上述五条规则均不适用!

基类派生类指针强制转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main()
{
Son s;
Father* f_p = &s;
//public继承方式下,基类指针可以指向派生类对象。

f_p->info(); //ok
//f_p->s_info(); //error
//这个基类指针只能访问基类里有的成员而不能访问派生类独有的成员

Son* s_p = (Son*)f_p;//强制类型转换

s_p->info(); //ok
s_p->s_info(); //ok

Father f;
Son* s1 = (Son*)(&f);
s1->info(); //ok
s1->s_info(); //ok
//留存疑问??这样的方式对于任意类都可以吗?还是我这个类太简单了?
}

  • public继承方式下,基类指针可以指向派生类对象,但是该指针只能访问基类里有的方法和成员而不能访问派生类独有的方法和成员。

参考链接:C++ 一篇搞懂继承的常见特性