面向对象语言的三大特征分别是封装继承和多态。本文带大家了解一下C++中的继承。
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
如下代码,Student类和Teacher类继承了Person类中的成员变量和成员函数。在权限允许的情况下,可以通过Student类或Teacher类来访问Person中成员变量或函数。
class Person
{
public:
void Print()
{
cout << _name << endl;
cout << _age << endl;
}
protected:
string _name = "张三"; //名字
int _age = 10; //年龄
};
class Student : public Person
{
protected:
int _stuid; // 学号
};
class Teacher : public Person
{
protected:
int _jobid; // 工号
};
int main()
{
Student s;
Teacher t;
s.Print();
t.Print();
return 0;
}
通过监视窗口,我们可以发现,s和t类都各自从Person类中继承到了自己的成员_name和_age。
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。通过如下方式进行派生类对基类的继承。
在基类中,成员函数和成员变量会可能会被三种的访问限定符所修饰,而在继承的过程中也有三种继承方式。如下图:
对他们进行匹配就会有九种情况,我们用如下表格来表示:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
- 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
对于成员变量:如下图所示,Person类和Student类中有相同名称的成员函数_num,直接访问Student类中的_num就会访问其自身的成员变量,而继承过来的会被隐藏。如需访问,需要用”基类::基类成员“的方法来显示访问。
对于成员函数:如下图所示,A类和B类中都有成员函数fun。虽然两者的参数不同,但是只要名字相同,两者就是隐藏关系。两者在不同的作用域中,所以不构成重载关系。
- 子类的成员,如果是内置类型不做处理,自定义类型会去调用这个类型的自己的构造函数。
- 父类继承的成员,必须调用父类的构造函数完成初始化。
注意:子类一定会在初始化列表调用父类的构造函数。如果没有初始化列表没有明确写出,编译器会调用默认无参的构造函数。
- 子类的成员,如果是内置类型不做处理,自定义类型会去调用这个类型的自己的析构函数。
- 父类继承的成员,会去调用父类的析构函数完成析构。
注意:由于多态中的需要,析构函数名字会被默认统一处理为“destructor( )”。所以父类和子类的构造函数会由于同名而形成隐藏关系。如果在子类中要调用父类的析构函数,需要显示调用。但是编译器为了保持构造和析构的顺序正确,会自动调用父类析构,一般不需要自行调用父类的析构函数。构造和析构顺序如下图:
- 子类的成员,如果是内置类型完成值拷贝,自定义类型会调用这个类型自生的拷贝构造或赋值
父类继承的成员,必须调用父类的拷贝构造或赋值运算符重载。
- 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
当一个子类只有一个父类的时候为单继承,继承方式如下图:
当一个子类有多个父类的时候为多继承,继承方式如下图:
由于C++中允许多继承的存在,就可能会出现如下图的菱形继承:
如下代码是上面菱形继承的实现:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
由于采用菱形继承的方式,会有如下两点问题:
存在冗余的问题
由于Assistant继承了Student和Teacher两个类,而这两个类都继承了Person类中的成员变量"_name",所以Assistant类中会有两份Person中的成员变量。很明显,一个人不会有两个名字,所以有一份数据冗余的。经过调试可以清晰的出:
存在二义性问题
由于有两个相同的继承过来的成员变量,在调用的时候就会存在歧义,编译器无法分辨你想要使用的是哪一个,调用时需要通过指定是哪个父类的成员才能解决这个问题。如下图的调用:
假设现在有如下的菱形继承:
通过调试可以观察到,在内存中他的结构如下图:
为了解决此问题,要用到菱形虚拟继承,在继承方式前加上“virtual”,就不会再有上文的两种问题。如下代码:
class Person
{
public :
string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
//如此访问不会存在问题
a._name = "peter";
}
在存储时,从B类继承的、C类继承的、和自己本身的值都会被正常存储,而这个对象的最后会另外开辟一块空间,用来存储A类中继承的数据。而B类和C类中原本存储A类数据的地方会存储一个地址,对这个地址解引用会得到一个偏移量,通过这个偏移量可以找到存放在此对象末尾的A类中继承的数据。其底层原理如下图: