继承
什么是继承
继承是为了更好的使代码得以复用而产生的,同时呈现了面向对象程序设计中的层次结构,继承会使得我们写好的类可以得到扩展。简单来说继承可以增强我们的代码复用,包括可以复用类的层次结构,同时使程序复用层次和条理。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 <string>
using namespace std;
class Person
{
public:
//如果我们设计一个类考虑到它会被继承那么最好用protected成员取代private成员
//protected成员在类外部仍然是无法使用的,但是在派生类中它是可见的也就是可以使用的
Person()
{
cout << "im a Person" << endl;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
};
//Programer类,这里让其公有继承Person类
//这时继承产生的新类被称为子类,也叫派生类
//被继承的旧类被称为父类,也叫基类
//继承方式为公有(public),其会让基类中的public成员成为派生类中的public成员
//基类中的protected成员成为派生类中的protected成员,基类中的private成员在派生类中不可见
//关于继承方式也在下文讲解
class Programer: public Person
{
//这里我们不再重写几个默认生成的成员函数,他们默认会完成他们应有的功能
//具体派生类的默认成员函数会在下文讲解
protected:
int _workyear;
};
class Teacher: public Person
{
protected:
int _teachyear;
};
int main()
{
Person person;//基类对象
Programer programer;//派生类对象
Teacher teacher;//派生类对象
person.Print();
programer.Print();
teacher.Print();
}
im a Person
im a Person
im a Person
age = 20
name = Misaki
age = 20
name = Misaki
age = 20
name = Misaki
以上这个例子就是简单的利用继承进行了类的扩展扩展出了两个派生类,可以看出派生类中拥有积累的成员函数和成员变量,并且还可以添加新的成员函数和变量。
继承方式
三种继承方式
继承方式一共有三种:public
, protected
,private
,这三种继承方式都可以将基类中的成员继承到派生类中,但是继承方式的不同也决定着在派生类中继承过来的成员的访问权限的不同,具体关于各个继承方式中对访问权限的改变可以参考下图。
这里提到基类中的private
成员无论何种继承方式在派生类中都是不可见的,所谓不可见指成员依然已经继承了过来并且作为了私有成员,但是在派生类中限制这些成员无论在类外还是类内都是无法访问的。
因此为了让一个成员在派生类中依然可以访问便出现了protected
访问权限,所以才说如果一个类为了方便继承最好将其private
访问权限用protected
来替代,这样即使继承产生派生类也依然可以在派生类中访问该成员。
如果我们在继承时不写出继承方式,则class
默认使用private
继承,struct
默认使用public
继承,不过为了可读性最好还是显示的写出继承方式。
最为经常使用的继承方式是public
继承,它可以让派生类中各个访问权限下的成员在派生类中还为相应的访问权限,是最为方便便于理解的继承方式,并且其他继承方式的扩展和维护性也并不强,因此除非特殊情况也不建议使用除public
外的其他继承方式。
切割
在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#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
Person()
{
cout << "im a Person" << endl;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
};
//派生类
class Programer: public Person
{
protected:
int _workyear;
};
//派生类
class Teacher: public Person
{
protected:
int _teachyear;
};
int main()
{
Person person;
//Programer programer;
Teacher teacher;
//person.Print();
//programer.Print();
//teacher.Print();
//teacher是派生类对象,person是基类对象,这样的隐式类型转换是允许的
person = teacher;
//也可以用派生类对象来拷贝构造基类对象
Person person2 = teacher;
person.Print();
person2.Print();
}
im a Person
im a Person
age = 20
name = Misaki
age = 20
name = Misaki
以上这段代码我们将派生类对象赋值给了基类对象,并且用派生类对象拷贝构造了基类对象,在这其中得以隐式转换最重要的原因就是编译器进行了切割处理,即将派生类对象中属于基类的那一部分成员保留,而将属于派生类的成员舍弃,由此才能完成类型转换。
上图过程即为切割的过程,在派生类的对象/指针/引用赋值给父类的对象/指针/引用时都会发生隐式类型转换,过程中都会发生切割。但是基类对象/指针引用不可以赋值给派生类对象/指针/引用。当然也有例外,基类指针可以通过强制类型转换也可以赋值给派生类指针,当然只有在基类指针指向了派生类对象时才是安全的。如果基类构成多态也可以用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#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
void Print()
{
cout << "Person::Print()" << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
};
//派生类
class Teacher: public Person
{
public:
void Print()
{
cout << "Teacher::Print()" << endl;
}
void PrintAge()
{
cout << "Teacher::_age:" << _age << endl;//重名成员构成隐藏
cout << "Person::_age:" << Person::_age << endl;//显示调用基类成员
}
protected:
int _age = 19;
};
int main()
{
Person person;
person.Print();//Person::Print()
Teacher teacher;
teacher.Print();//Teacher::Print(),派生类重名成员构成隐藏
teacher.PrintAge();
}
Person::Print()
Teacher::Print()
Teacher::_age:19
Person::_age:20
值得注意的是,关于函数,函数重载指的是在相同作用域内函数名相同参数不同构成重载,而重定义/隐藏是指在基类和派生类的作用域内函数名相同就会构成隐藏。所以我们最好还是不要在派生类中定义和基类同名的成员,避免隐藏和重定义的发生。
派生类的默认成员函数
派生类也有6个默认成员函数,但是着6个默认成员函数既要兼顾到基类也需要兼顾到派生类,因此在写法上与常规的并不相同。
构造函数
在派生类的构造函数中我们需要先显示调用基类的构造函数对基类成员进行初始化然后才能对派生类成员进行初始化。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#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
//这里构造函数就不选择传参了方便起见我都初始化为定值
Person()
:_age(20)
,_name("Misaki")
{
cout << "Person()" << endl;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age;
string _name;
};
//派生类
class Teacher: public Person
{
public:
//派生类构造函数,先调用基类构造函数,在初始化派生类成员
//默认生成的构造函数也是这样实现的
Teacher()
:Person()
,_teachyear(3)
{
cout << "Teacher()" << endl;
}
void Print()
{
Person::Print();
cout << "teachyear = " << _teachyear << endl;
}
static int _count;
protected:
int _teachyear;
};
int Teacher::_count = 2;
int main()
{
Teacher teacher;
teacher.Print();
}
Person()
Teacher()
age = 20
name = Misaki
teachyear = 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#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
//这里构造函数就不选择传参了方便起见我都初始化为定值
Person()
:_age(20)
,_name("Misaki")
{
cout << "Person()" << endl;
}
Person(const Person& person)
:_age(person._age)
,_name(person._name)
{
cout << "Person(const Person&)" << endl;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age;
string _name;
};
//派生类
class Teacher: public Person
{
public:
//派生类构造函数,先调用基类构造函数,在初始化派生类成员
//默认生成的构造函数也是这样实现的
Teacher()
:Person()
,_teachyear(3)
{
cout << "Teacher()" << endl;
}
//拷贝构造函数,先调用基类构造函数对基类成拷贝构造,再拷贝构造派生类成员
//默认生成的拷贝构造也是这样的实现方法
Teacher(const Teacher& teacher)
:Person(teacher)//这里利用派生类对象可以隐式转换为基类对象来调用基类拷贝构造
,_teachyear(3)
{
cout << "Teacher(const Teacher&)" << endl;
}
void Print()
{
Person::Print();
cout << "teachyear = " << _teachyear << endl;
}
static int _count;
protected:
int _teachyear;
};
int Teacher::_count = 2;
int main()
{
Teacher teacher1;
Teacher teacher2 = teacher1;
teacher2.Print();
}
Person()
Teacher()
Person(const Person&)
Teacher(const Teacher&)
age = 20
name = Misaki
teachyear = 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
87
88
89
90
91
92
93
94
95
96
97
98
99#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
//这里构造函数就不选择传参了方便起见我都初始化为定值
Person()
:_age(20)
,_name("Misaki")
{
cout << "Person()" << endl;
}
Person(const Person& person)
:_age(person._age)
,_name(person._name)
{
cout << "Person(const Person&)" << endl;
}
Person& operator=(const Person& person)
{
if(&person != this)
{
_age = person._age;
_name = person._name;
}
cout << "Person::operator=(const Person&)" << endl;
return *this;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age;
string _name;
};
//派生类
class Teacher: public Person
{
public:
//派生类构造函数,先调用基类构造函数,在初始化派生类成员
//默认生成的构造函数也是这样实现的
Teacher()
:Person()
,_teachyear(3)
{
cout << "Teacher()" << endl;
}
//拷贝构造函数,先调用基类构造函数对基类成拷贝构造,再拷贝构造派生类成员
//默认生成的拷贝构造也是这样的实现方法
Teacher(const Teacher& teacher)
:Person(teacher)//这里利用派生类对象可以隐式转换为基类对象来调用基类拷贝构造
,_teachyear(3)
{
cout << "Teacher(const Teacher&)" << endl;
}
//赋值运算符重载,先调用基类的赋值运算符重载对基类成员进行赋值,再赋值派生类成员
//默认生成的赋值运算符重载也是这样的实现方法
Teacher& operator=(const Teacher& teacher)
{
if(&teacher != this)
{
Person::operator=(teacher);//调用基类的赋值运算符重载,并且用隐式类型转换传参
_teachyear = teacher._teachyear;
}
cout << "Teacher::operator=(const Teacher&)" << endl;
return *this;
}
void Print()
{
Person::Print();
cout << "teachyear = " << _teachyear << endl;
}
static int _count;
protected:
int _teachyear;
};
int Teacher::_count = 2;
int main()
{
Teacher teacher1;
Teacher teacher2;
teacher2 = teacher1;
teacher2.Print();
}
Person()
Teacher()
Person()
Teacher()
Person::operator=(const Person&)
Teacher::operator=(const Teacher&)
age = 20
name = Misaki
teachyear = 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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
//这里构造函数就不选择传参了方便起见我都初始化为定值
Person()
:_age(20)
,_name("Misaki")
{
cout << "Person()" << endl;
}
Person(const Person& person)
:_age(person._age)
,_name(person._name)
{
cout << "Person(const Person&)" << endl;
}
Person& operator=(const Person& person)
{
if(&person != this)
{
_age = person._age;
_name = person._name;
}
cout << "Person::operator=(const Person&)" << endl;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age;
string _name;
};
//派生类
class Teacher: public Person
{
public:
//派生类构造函数,先调用基类构造函数,在初始化派生类成员
//默认生成的构造函数也是这样实现的
Teacher()
:Person()
,_teachyear(3)
{
cout << "Teacher()" << endl;
}
//拷贝构造函数,先调用基类构造函数对基类成拷贝构造,再拷贝构造派生类成员
//默认生成的拷贝构造也是这样的实现方法
Teacher(const Teacher& teacher)
:Person(teacher)//这里利用派生类对象可以隐式转换为基类对象来调用基类拷贝构造
,_teachyear(3)
{
cout << "Teacher(const Teacher&)" << endl;
}
//赋值运算符重载,先调用基类的赋值运算符重载对基类成员进行赋值,再赋值派生类成员
//默认生成的赋值运算符重载也是这样的实现方法
Teacher& operator=(const Teacher& teacher)
{
if(&teacher != this)
{
Person::operator=(teacher);//调用基类的赋值运算符重载,并且用隐式类型转换传参
_teachyear = teacher._teachyear;
}
cout << "Teacher::operator=(const Teacher&)" << endl;
return *this;
}
//要注意派生类析构函数不需要显示调用基类的析构函数,在调用派生类析构函数释放派生类成员后
//会自动调用基类的析构函数,来满足先释放派生类成员再释放基类成员的顺序
~Teacher()
{
cout << "~Teacher()" << endl;
//Person::~Person();这样调用会报错
}
void Print()
{
Person::Print();
cout << "teachyear = " << _teachyear << endl;
}
static int _count;
protected:
int _teachyear;
};
int Teacher::_count = 2;
int main()
{
Teacher teacher;
}
Person()
Teacher()
~Teacher()
~Person()
派生类析构函数执行时会先释放派生类成员再释放基类成员,并且要注意派生类析构函数会自动调用基类析构函数进行清理,无需手动调用。从执行结果上也可以看出编译器是先释放派生类成员再释放基类成员。
总结
对派生类默认成员函数进行总结。
1、派生类构造函数必须首先调用基类构造函数构造基类成员,之后再构造派生类成员才能成立。
2、派生类拷贝构造函数必须先调用基类拷贝构造函数拷贝构造基类成员,之后再拷贝构造派生类成员才能成立。
3、派生类赋值运算符重载必须先调用基类赋值运算符重载函数对基类成员进行赋值,再对派生类成员进行赋值才能成立。
4、派生类析构函数会在调用后自动调用基类析构函数无需显式调用基类析构函数。
5、派生类对象构造会先构造基类成员再构造派生类成员。
6、派生类对象析构会先释放派生类成员再释放基类成员。
继承与友元
在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#include <iostream>
#include <string>
using namespace std;
class Test;
//基类
class Person
{
friend class Test;
public:
void Print()
{
cout << "age = " << _age << endl;
cout << "name = " << _name << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
};
//派生类
class Teacher: public Person
{
public:
void Print()
{
Person::Print();
cout << "teachyear = " << _teachyear << endl;
}
protected:
int _teachyear = 3;
};
class Test
{
public:
void Print()
{
cout << "age = " << person._age << endl;
cout << "name = " << person._name << endl;
}
void Print2()
{
cout << "age = " << teacher._age << endl;
cout << "name = " << teacher._name << endl;
//cout << "teachyear = " << teacher._teachyear << endl;//无法访问
}
private:
Person person;
Teacher teacher;
};
int main()
{
Test test;
test.Print2();
}
age = 20
name = Misaki
继承和静态成员
关于继承和静态成员,我们所要记住的只有一句话,在整个继承体系中,无论发生多少次继承,静态成员在继承体系中只存在一份。
静态成员函数
关于静态成员函数,这句话将更好理解,在不发生隐藏的情况下我们调用的依然是基类的静态函数。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>
#include <string>
using namespace std;
//基类
class Person
{
public:
static void Print()
{
cout << "Person::_count = " << _count << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
static int _count;
};
int Person::_count = 1;
//派生类
class Teacher: public Person
{
public:
//static void Print()
//{
// cout << "Teacher::_count = " << _count << endl;
//}
protected:
int _age = 19;
};
int main()
{
Person::Print();
Teacher::Print();
}
Person::_count = 1
Person::_count = 1
在发生隐藏的情况下,就会调用派生类的静态函数。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>
#include <string>
using namespace std;
//基类
class Person
{
public:
static void Print()
{
cout << "Person::_count = " << _count << endl;
}
protected:
int _age = 20;
string _name = "Misaki";
static int _count;
};
int Person::_count = 1;
//派生类
class Teacher: public Person
{
public:
static void Print()
{
cout << "Teacher::_count = " << _count << endl;
}
protected:
int _age = 19;
};
int main()
{
Person::Print();
Teacher::Print();
}
Person::_count = 1
Teacher::_count = 1
静态成员变量
关于静态成员变量,我们要理解其在整个继承体系中有且只有一份,我们在基类中将其改变,派生类的也会跟着改变。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>
#include <string>
using namespace std;
//基类
class Person
{
public:
static void Print()
{
cout << "Person::_count = " << _count << endl;
}
static int _count;
protected:
int _age = 20;
string _name = "Misaki";
};
int Person::_count = 1;
//派生类
class Teacher: public Person
{
public:
static void Print()
{
cout << "Teacher::_count = " << _count << endl;
}
protected:
int _age = 19;
};
int main()
{
Person::_count = 3;
Person::Print();
Teacher::Print();
}
Person::_count = 3
Teacher::_count = 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#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
static void Print()
{
cout << "Person::_count = " << _count << endl;
}
static int _count;
protected:
int _age = 20;
string _name = "Misaki";
};
int Person::_count = 1;
//派生类
class Teacher: public Person
{
public:
static void Print()
{
cout << "Teacher::_count = " << _count << endl;
cout << "Person::_count = " << Person::_count << endl;
}
protected:
int _age = 19;
static int _count;
};
int Teacher::_count = 5;
int main()
{
Person::_count = 3;
Person::Print();
Teacher::Print();
}
Person::_count = 3
Teacher::_count = 5
Person::_count = 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#include <iostream>
#include <string>
using namespace std;
//基类
class Person
{
public:
static void Print()
{
cout << "Person::_count = " << _count << endl;
}
static int _count;
protected:
int _age = 20;
string _name = "Misaki";
};
int Person::_count = 1;
//派生类
class Teacher: public Person
{
public:
static void Print()
{
//cout << "Teacher::_count = " << _count << endl;//编不过了,因为静态函数中只能调用静态成员,而此时_count隐藏已经不是静态成员
cout << "Person::_count = " << Person::_count << endl;
}
int _count = 5;
protected:
int _age = 19;
};
int main()
{
Person::_count = 3;
Person::Print();
Teacher::Print();
Teacher teacher;
cout << teacher._count << endl;//可以访问了
}
Person::_count = 3
Person::_count = 3
5
由此我们可以判断一点,如果在派生类中发生隐藏和重定义那么在派生类中该同名成员的生命周期及访问权限将根据在派生类中重定义的情况而决定,因此在基类中即使是静态的成员如果在派生类中重定义为非静态变量进行使用也是可以的,可见重定义和隐藏是十分恐怖的,因此我们最好是尽量防止在派生类中定义和基类重名的成员,避免发生隐藏和重定义。
菱形继承和虚继承
Cpp语法复杂这一特点可以从继承中体现出来,因为其支持多继承,而多继承就有可能会产生菱形继承的情况。
菱形继承
什么是菱形继承呢?其产生原因就要“归功于”Cpp支持多继承这一特点,多继承就是允许一个类继承于多个基类,当派生类多继承于多个基类时,这些基类也有可能继承于更上层的同一个基类,由此就会产生菱形继承的情况。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#include <iostream>
using namespace std;
class A
{
public:
char _a = 'A';
};
//B继承于A
class B: public A
{
public:
char _b = 'B';
};
//C也继承于A
class C: public A
{
public:
char _c = 'C';
};
//D继承于B,C
class D: public B, public C
{
public:
char _d = 'D';
};
以上这种结构就会产生菱形继承,我们用画图的方式具现化表示一下。
这种继承体系就是菱形继承,也是非常直观的可以体现出来的。
菱形继承带来的问题
菱形继承是Cpp多继承所带来的主要问题之一,一旦发生菱形继承,首当其冲的我们就应该考虑到菱形继承带来的数据冗余及数据二义性的问题。
还用上面的例子,因为D同时继承了B,C类,而B,C类又都分别继承了A类这就意味着D中存在着两份A类,一份是从B那里继承来的,另一份是从C继承来的,这就造成了数据冗余,并且当我们通过D想要直接访问A类成员时编译器会报错,因为编译器不知道你要访问的时B中的A类成员还是C中的A类成员,我们必须加上域限定符才能访问,这就造成了数据二义性,但是我们通过域限定符姑且可以解决数据二义性的问题,但是数据冗余却无法解决。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 A
{
public:
char _a = 'A';
};
//B继承于A
class B: public A
{
public:
char _b = 'B';
};
//C也继承于A
class C: public A
{
public:
char _c = 'C';
};
//D继承于B,C
class D: public B, public C
{
public:
char _d = 'D';
};
int main()
{
D d;
//cout << d._a << endl;//报错
d.B::_a = 'E';
d.C::_a = 'F';
cout << d.B::_a << endl;
cout << d.C::_a << endl;
}
E
F
从以上结果中可以看出d中有着两份A类的成员,因此在我们日常程序设计中应该极力避免菱形继承的产生,因为会浪费空间也会产生不必要的错误。
虚拟继承
但是如果在某些场景下一定要产生菱形继承,我们也有办法避免数据冗余及数据二义性的发生,这就要牵扯到虚拟继承。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 A
{
public:
char _a = 'A';
};
//使用虚拟继承使B继承于A
class B: virtual public A
{
public:
char _b = 'B';
};
//使用虚拟继承使C继承于A
class C: virtual public A
{
public:
char _c = 'C';
};
//D继承于B,C
class D: public B, public C
{
public:
char _d = 'D';
};
int main()
{
//一旦使用虚拟继承那么D类中就只存在一份A的成员变量
D d;
//无论用什么作用域进行访问都只能访问到同一份
d.B::_a = 'E';
cout << d._a << endl;
cout << d.B::_a << endl;
cout << d.C::_a << endl;
d.C::_a = 'F';
cout << d._a << endl;
cout << d.B::_a << endl;
cout << d.C::_a << endl;
//我们甚至还可以这样访问,一旦使用虚拟继承我们可以视d间接继承了A,因此也有了A的作用域
//这在不使用虚拟继承的情况下是无法完成的
cout << d.A::_a << endl;
}
E
E
E
F
F
F
F
那么虚拟继承是如何做到的这一切呢?在不适用虚拟继承的情况下类D中可以这样表示。
但是引入虚拟继承后,编译器就会生成一张虚基表,这张表中存放着这个类与其虚拟继承的基类之间的地址偏移量,而在这个类中也会多生成一个指针被称为虚基表指针指向这张虚基表。因此此时D类中由于所继承的两个基类都是虚拟继承自A类,因此B,C类都会有属于自己的虚基表及指向这张表的虚基表指针,而他们的虚基表中都会存储与同一个A类之间的地址偏移量,因此就可以做到D类中就有唯一的一份A类成员。
使用虚拟继承D中可以如下表示。
但是要注意虚基表中存放的并不是直接指向类A的指针,而是与A的地址偏移量,由此可以找到A。这样就确保了D中只有唯一的一份A的成员,解决了数据冗余以及数据二义性的问题。1
2
3
4
5
6
7int main()
{
cout << sizeof(B) << endl;
}
8
我们取一个虚拟继承的类的大小也可以发现其大小变成了8,这是因为多了一个虚基表指针占了4个字节,加上基类个派生类的两个字符型成员各占2字节,还有2个字节的补齐成了8。不过这样我们知道了虚基表指针是存储在对象中的,那么虚基表存储在哪里呢?这个根据每个编译器的不同都有不同的处理,vs是存储在寄存器中的。
这里还有一点要注意的,虽然一个继承体系中虚继承自同一个基类的派生类一共只存储一份基类,这是为了防止数据冗余,但是在sizeof()
计算每个派生类大小时编译器也是会将基类大小的计算加入每个派生类中的,尽管他们一共只存储一份基类。例如上面的例子如果我们将B/C
类属于他们自身的成员去掉我们会发现他们的大小还是有8
,这就是因为编译器在每个派生类中还加入了基类大小的计算,这点在虚继承上也不例外。
总结
在继承这一章,我们透彻的学习了继承的各种知识点,并且还解析了菱形继承以及虚拟继承底层的实现原理,我们不由得产生了一个问题,我什么时候使用继承什么时候使用组合呢?
继承:是一种is a的关系。
组合:是一种has a的关系。
这一点并不难理解,但是实际开发中,我们尽量优先使用组合,因为组合耦合度低,易于开发和维护,我们可以不用过多的考虑基类的实现,而继承不同。但是有一些情况下确实是继承更为符合情景呢么就是用继承,但要小心多继承。继承的使用也是十分广泛的,比如之后要学习的多态,都多亏于继承的语法,当然如果继承和组合都可以使用的话还是使用组合更好。