C++继承总结
继承是什么?
继承是面向对象复用的重要手段,通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西。
通过继承联系在一起的类构成一种层次关系,通常在层次关系的根部有一个基类,其他类则直接或间接的从基类继承而来,这些继承得到的类称为派生类。
基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
继承的时候,派生类通过使用派生类列表明确指出它是从哪个基类继承而来。
派生类列表的使用形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类可以有访问说明符,如:
class Person{
public:
Person(){}
protected:
//身份证号和姓名
string _id;
string _name;
};
class Student : public Person{
public:
Student(){}
protected:
//学号
string _num;
};
如上面的代码,
一个Student类继承一个父类(基类)Person类,public为访问说明符,为公有继承,除此之外还有protected继承和private继承
继承方式和访问限定符的关系
在介绍几种继承方式时,先介绍一下protected访问限定符,protected限定符可以看做是public和private中和之后的产物:
·和私有成员类似,受保护的成员对于类的用户来说是不可访问的
·和公有成员类似,受保护的成员对于派生类的成员和友元来说是可以访问的
接下来我们看几种继承方式:
1.公有继承(public inheritance)是一个接口继承,保持is-a原则,每个父类可用的成员对子类可用,因为每个子类对象也是一个父类对象。总的来说,公有继承时,基类的公有接口是派生类的公有接口的组成部分。
公有继承例子:
class Person{
public:
//身份证号和姓名
string _id;
string _name;
};
class Student : public Person{
public:
//学号
string _num;
};
创建一个Student对象,尝试在外部访问父类Person的公有成员,如图,父类的公有成员被子类公有继承后,它们还是公有的,在类外部是可以访问的
接下来,我们修改一些代码
class Person{
public:
Person() {
_id = "0001";
_name = "regi";
}
protected:
//身份证号和姓名
string _id;
string _name;
};
class Student : public Person{
public:
Student()
:Person(){
//学号
_num = "15060204";
_id = "0002";
}
void DisPlay() {
cout << "身份证号" << _id << endl;
cout << "姓名" << _name << endl;
cout << "学号" << _num << endl;
}
//学号
string _num;
};
在派生类Student的构造函数中,访问了父类的protected成员,然后进行修改,打印的结果如下:
我们可以发现身份证号已经被修改了,这是在类的内部访问基类的protected成员,那么类外部呢?
我们发现,基类的protected成员在类外部是不可以访问的,说明父类的受保护成员在公有继承后,其访问限定符还是protected
所以,派生类中,公有继承的基类的公有成员也是派生类的公有成员,受保护的成员也是派生类的受保护成员,至于基类的私有成员是不允许外部访问的,除了基类的内部可以访问之外都不能访问。
2.受保护的继承(protected inheritance)是一个实现继承,基类的部分成员并未完全成为子类接口的一部分,是一种has-a继承,在受保护的继承中,基类的公有成员和受保护成员,都是派生类的受保护成员。
举个栗子:
class Person{
public:
Person() {
_id = "0001";
_name = "regi";
}
//身份证号和姓名
string _id;
string _name;
};
class Student : protected Person{
public:
Student()
:Person(){
//学号
_num = "15060204";
_id = "0002";
}
void DisPlay() {
cout << "身份证号" << _id << endl;
cout << "姓名" << _name << endl;
cout << "学号" << _num << endl;
}
//学号
string _num;
};
还是刚才的代码,不过变成了受保护的继承,我们调用Display函数,结果如下:
我们发现,受保护的继承,在类的内部是可以访问基类的公有成员的,那么类外部呢?
结果发现是不可以继承的,所以基类的公有成员在受保护继承后,其访问限定符被更改为受保护的(protected)
如果把基类的两个成员的访问说明符改为protected,则结果如下:
所以,受保护继承时,基类的公有成员和受保护成员都是派生类的保护成员
3.私有继承(private inheritance)和受保护的继承一样也是一种has-a继承,总的来说,在私有继承中,基类的公有成员和受保护成员是派生类的私有成员。
还是上面的代码,无论基类的两个成员变量的访问说明符是public还是protected,在类的内部都是可以访问的,而在类外部是不可以访问的,我找到了一张表,对比了继承方式和访问限定符之间的关系,如图:
小结:不管哪种继承方式,在派生类内部都可以访问类的公有成员和受保护成员,但是基类的私有成员在子类中不可见。
基类和派生类的转换或赋值——public继承
在介绍基类和派生类的赋值之前,我们先画出一个图示,介绍派生类的组成:
我们发现,公有继承时,派生类继承了父类的成员,因为派生类对象中含有与其基类对应的组成部分,所以我们能把派生类对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上.
接下来就解释基类和派生类的赋值兼容规则,先定义两个类:
class Person{
public:
Person() {
_id = "0001";
_name = "regi";
}
void Display() {
cout << "身份证号:" << _id << endl;
cout << "姓名:" << _name << endl;
}
protected:
//身份证号和姓名
string _id;
string _name;
};
class Student : public Person{
public:
Student()
:Person(){
//学号
_num = "15060204";
_id = "0002";
}
void DisPlay() {
cout << "身份证号" << _id << endl;
cout << "姓名" << _name << endl;
cout << "学号" << _num << endl;
}
//学号
string _num;
};
分情况讨论:
1.子类对象可以赋值给父类对象(切割/切片)
//子类对象赋值给父类对象
Person p;
Student s;
//赋值前
p.Display();
p = s;
//赋值后
p.Display();
结果如下:
我们发现,赋值后,基类对象的成员变量和子类的成员变量相同,这个动作叫切片,图示如下图:
注意,切片不是类型转换,实际上在C++中,对象之间也不存在类型转换
2.父类对象不能赋值给子类对象
我们发现,编译器是不支持的,显然错误,其实也很好理解,子类对象在继承了父类对象的基础之上又有自己的成员变量,两者不对等。
3.父类的指针/引用可以指向子类对象
代码如下:
//父类的指针/引用可以指向子类对象
Student s;
Person* p1 = &s;
Person& p2 = s;
s.DisPlay();
p1->Display();
p2.Display();
运行结果如图:
由结果得知,基类对象p1指向的是派生类s的基类部分,p2引用的也是派生类s的基类部分
4.子类的指针/引用不可以指向父类对象(可以强制类型转换)
如图,我们发现这种操作是不被允许的,但是如果加上强制类型转换呢?
//子类的指针/引用不能指向父类对象
Person p;
Student* s1 = (Student*)&p;
Student& s2 = (Student&)p;
s2.DisPlay();
进行强制转换以后,编译器没有报错,但是如果调用DisPlay()函数:
程序崩了,因为子类指针或对象,指向的是父类的地址空间,当我们进行强制类型转换之后如图:
编译器把父类的内存空间看做是子类的内存空间对内存空间进行读取,但是这个派生类对象除基类之外的内存空间所存储的内容是未知的,如果对派生类的成员进行访问时,程序就会崩溃
小结:将基类的指针或引用绑定到派生类对象中的基类部分上,这种转换是派生类到基类的类型转化,跟其他类型转换一样,编译器会隐式的执行类型转换。
继承中的类作用域
每个类定义自己的作用域,在这个定义域内我们定义类的成员,当存在继承时,派生类的作用域嵌套在其基类的作用域之内。
在派生类中,如果一个名字在派生类的作用域之内无法正确解析,则编译器将继续在外层的基类作用域寻找该名字的定义,正因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。
名字冲突与继承
和其他的作用域一样,派生类也能重定义在其直接基类或者间接基类中的名字,此时定义在内层的作用域(即派生类)的名字将隐藏定义在外层作用域(基类)的名字。
即派生类的成员将隐藏同名的基类成员。
class Car {
public:
void f1() {
cout << "Car::f1()" << endl;
}
void f2() {
cout << "Car::f2()" << endl;
}
string _name;
};
class new_Car : public Car{
public:
void f1() {
cout << "new_Car::f1()" << endl;
}
void f2() {
cout << "new_Car::f2()" << endl;
}
string _name;
};
int main() {
Car c1;
c1._name = "Car";
new_Car c2;
c2._name = "new_Car";
system("pause");
return 0;
}
上面这一段代码,定义了一个基类Car和一个派生类new_Car,他们的每个成员都是相同的,我们先来看看他们的成员变量:
如图,在监视窗口里面,我们发现派生类对象和基类对象的成员变量_name是完全不同的,虽然他们的名字相同,为什么?
这里因为派生类的成员变量的名称和基类的成员变量的名称相同,派生类隐藏了基类中的_name变量,虽然它是继承了基类的_name变量了的。
我们再测试一下同名的成员函数,加入这两行代码:
c2.f1();
c2.f2();
结果如下:
我们可以发现,派生类把基类中的和自己成员函数名称相同的函数都隐藏了
基类的成员函数,在派生类中如果同名,注意,只要是同名,就可以构成隐藏,只有当成员函数的函数名,参数列表相同,返回值相同,而且加了virtual关键字时,才会构成重写(加了virtual关键字的成员函数是虚函数),其余的情况,只要名称相同就可以构成隐藏。
在派生类中,如果想调用基类的成员,可以使用:"基类::基类成员"的方式来访问。
例如在上面代码的派生类new_Car中,加入这样一个函数:
void Display() {
Car::_name = "BMW";
cout << "Car::_name = " << Car::_name << endl;
Car::f1();
}
就像这样就可以调用基类中的成员变量,打印结果如下:
派生类的默认成员函数
在继承关系里,在派生类中如果没有显式的定义六个默认的成员函数,编译器则会默认合成这六个默认的成员函数
我们来看一个例子:
class Person
{
public:
//构造函数
Person(string id = "000001", string name = "")
:_id(id)
, _name(name)
{
cout << "Person()" << endl;
}
//拷贝构造函数
Person(const Person& p)
:_id(p._id)
, _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
//赋值运算符重载
Person& operator=(const Person& p)
{
if (this != &p)
{
_id = p._id;
_name = p._name;
}
}
//析构函数
~Person()
{
cout << "~Person()" << endl;
}
protected:
//身份证号和姓名
string _id;
string _name;
};
我们把上面的类当做基类,来写一个派生类Student:
class Student : public Person
{
public:
//待完成的默认成员函数
protectd:
//学号
string _num;
}
我们先来分析一下派生类里都有哪些东西,如图
我们发现,派生类Student中,有从基类中继承下来的_id和_name变量,还有自身的_num变量,那么我们该如何构造一个派生类呢?
因为基类是派生类的一部分,所以要创建派生类就必须先创建其中基类的部分,在C++中我们使用成员初始化列表来完成这项工作,构造函数代码如下:
Student(string id, string name, string num)
:Person(id, name)
, _num(num)
{
cout << "Student()" << endl;
}
如果不调用基类的构造函数,程序将使用默认的基类构造函数,我们尝试构造基类对象和派生类对象,代码如下:
Person p1("000002", "Jack");
Student s2("000003", "Tom", "060204111");
构造成功了,接下来我们分析拷贝构造函数,拷贝构造函数和构造函数的思路很相似,先构造出基类对象,再构造出派生类对象,拷贝构造函数代码如下:
//拷贝构造函数
Person(const Person& p)
:_id(p._id)
, _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
派生类在赋值运算符重载上和普通的类没有多大区别,代码如下:
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);
_num = s._num;
}
}
接下来分析一下派生类的析构函数,在派生类中, 构造函数在构造派生类之前先调用了基类的构造函数,即先构造派生类中的基类部分,再构造自己的部分,那么析构函数呢?
答案是不用调用基类的析构函数,编译器会在派生类的析构函数完成后,自动调用基类的析构函数,如图:
这段汇编代码在派生类的构函数结束之后,我们发现先编译器会在派生类的析构函数之后自动调用基类的析构函数
派生类的析构函数和基类的析构函数构成隐藏
我们知道,基类和派生类中,函数名相同的成员函数构成隐藏(重定义),那么为什么析构函数也构成了隐藏呢?
我们显示的调用一下基类和派生类的析构函数
Person p;
Student s;
p.~Person();
s.~Student()
我们看一下汇编层次的代码:
我们发现,无论是基类还是派生类,调用析构函数的时候都是这么一串东西:
这是因为编译器进行了优化,使之构成了隐藏(重定义)。
继承与静态成员
基类如果定义了static成员,则整个继承体系中只有一个这样的成员,无论有多少个派生类,都只有一个static成员的实例
菱形继承
在介绍菱形继承之前,我们先了解一下C++中的继承方式
单继承
一个子类只有一个直接父类时称这个继承关系为单继承
我们上面的例子基本都是单继承
多继承
一个子类有两个或以上的父类时称这个继承关系为多继承
然而,多继承可能会出现这样的情况,比如上面,Assistant类继承了Student类和Teacher类,但是,如果Student类和Teacher类都继承了Person类呢?如图:
这样的继承就是菱形继承,这样一来Assistant类中 会有两份Person类中的成员,这样会造成数据冗余和二义性,存在两份Person类的成员变量,而且Assistant类调用时不知道调用的是哪个类中的Person类成员,虽然可使用访问限定符,但是这样未免太过麻烦。
我再举个栗子,看如下代码:
class Base {
protected:
int _base;
};
class A : public Base {
protected:
int _a;
};
class B : public Base {
protected:
int _b;
};
class C : public A, public B {
protected:
int _c;
};
类C继承了类A和类B,但是类A和类B都继承了类A,那么就会有下面的结果:
如图我们分析类C的成员,有两个Base类的_base成员,这就菱形继承造成的数据的二义性,那么如何解决呢?
虚继承
处理菱形继承的方法就是虚继承,那么什么是虚继承呢?
虚拟继承(Virtual Inheritance),解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。如图:
在继承的时候,让A和B在继承Base的时候加上virtual关键字,这样就实现了虚继承,代码如下:
class Base {
public:
int _base;
};
class A : virtual public Base {
public:
int _a;
};
class B : virtual public Base {
public:
int _b;
};
class C : public A, public B {
public:
int _c;
};
void test() {
C c;
c._a = 10;
c._b = 20;
c._base = 30;
c._c = 40;
}
int main() {
test();
system("pause");
return 0;
}
为了方便测试,我把类中成员变量的访问限定符设置为public,下面我们来调试一下看一下虚继承是如何解决这个问题的:
如图,我们发现对象c的内存模型是这样的:
我们可以发现,在使用虚继承之前,我们有两个_base,现在_base在对象c里面只有一个,而且存储在最底下,为什么呢?接下来我们来看一下这两个地址里面的信息(因为我的电脑是小端,所以左边的是低地址),如图:
在这两个地址里面,当前位置什么都没有,但是下一个位置有都有一个数字,注意14和0c都是16进制,所以a里面的是20,b里面的是12,那么这两个数字又有什么含义呢?我们再看下面这个图
我们发现,地址0x004ffe04存储的是_base的值,所以原本菱形继承存储_base的位置,存储了指针,指针指向的地址里面的下一个位置存储的是偏移量。
总结:虚继承底层实现原理与编译器有关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针和虚基表,虚基类指针指向一个虚基类表,虚基类表中存储了虚基类与本类的偏移地址,通过偏移地址就可以找到虚基类成员,这样就不会造成多继承的数据冗余和数据的二义性,只需要存储一份内容和两个虚基类表,节省了存储空间
好了,关于继承的总结暂时就总结到这里。
end