继承
一、继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
测试一:
#include <iostream>
#include <string>
using namespace std;
//父类、基类
class Person{
public:
string _name;
string _id;
int _age;
void Print(){
cout << _name << " ";
cout << _id << " ";
cout << _age << endl;
}
};
//子类、派生类
class Student : public Person{
int _stdid;
};
class Teacher : public Person{
int _jobid;
};
void Test1(){
Student s;
s._name = "张三";
s._id = "123456";
s._age = 18;
Teacher t;
t._name = "王老师";
t._id = "78910";
t._age = 30;
s.Print();
t.Print();
}
运行结果:
二、继承的定义
2.1 定义格式
2.2 继承方式和访问限定符
成员权限和继承方式的组合:
-
基类private成员不能被派生类继承;基类的其他成员在子类的访问方式为成员权限和继承方式的最小访问权限,其中public > protected > private。
-
基类private成员不能被派生类继承,不管在类里面还是类外面都不能去访问它。
-
如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
-
使用关键字class时默认的访问权限和继承方式是private,使用struct时默认的访问权限和继承方式是public,不过最好显示的写出继承方式。
-
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
-
子类型:子类型必须是子类继承了父类的所有可继承特性,也即公有继承,才能说是子类型,否则就只是单纯的子类
将基类Person中的3个成员变量改为protected权限,直接编译:
测试二,基类protected成员只能在派生类类内访问:
class Person{
protected:
string _name;
string _id;
int _age;
public:
void Print(){
cout << _name << " ";
cout << _id << " ";
cout << _age << endl;
}
};
class Student : public Person{
int _stdid;
public:
void init(string name, string id, int age){
_name = name;
_id = id;
_age = age;
}
};
class Teacher : public Person{
int _jobid;
public:
void init(string name, string id, int age){
_name = name;
_id = id;
_age = age;
}
};
void Test2(){
Student s;
s.init("张三", "123456", 18);
Teacher t;
t.init("王老师", "78910", 30);
s.Print();
t.Print();
}
运行结果:
如果将基类Person中的3个成员变量改为private权限,发现无论是测试一还是测试二的代码都会编译报错。基类private成员不会被派生类继承,无论在类内还是类外都无法访问!
三、继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽对父类同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(这里是成员函数的隐藏,而非函数重载。函数重载的前提条件是在同一作用域下。)
- 注意在实际中在继承体系里面最好不要定义同名的成员。
测试代码:
class Person{
protected:
string _name= "张三";
string _id = "111"; //身份证号
int _age = 18;
public:
void Print(){
cout << _name << " ";
cout << _id << " ";
cout << _age << endl;
}
};
class Student : public Person{
string _id = "999"; //学号
public:
void Print(){
cout << "学号:" << _id << endl; //Student::_id
cout << "身份证号:" << Person::_id << endl; //Person::_id
}
};
void Test3(){
Student s;
s.Print(); //Student::Print
s.Person::Print(); //Person::Print
}
运行结果:
四、基类和派生类对象赋值转换
-
派生类的对象 / 指针 / 引用可以赋值给 基类的对象 / 指针 / 引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
-
这里虽然类型不同,但不是隐式类型转换,不会构造临时对象,是一个特殊的语法支持。
-
但反过来赋值则不能成立,首先基类对象不能赋值给派生类对象(内容不完整);其次基类的指针/引用可以赋值给派生类的指针/引用,但是需要进行类型转换,且存在越界访问和野指针的风险(建议尽量避免)。
测试代码:
void Test4(){
Student s;
s.init("张三", "123456", 18, "111");//初始化_name,_id,_age,_stuid
Person p = s;
p.Print();
Person *pp = &s;
pp->Print();
Person &rp = s;
rp.Print();
}
运行结果:
五、派生类的默认成员函数
测试代码:
class Person{
protected:
string _name;
string _id;
int _age;
public:
Person(const char* name, const char* id, int age)
:_name(name),
_id(id),
_age(age)
{
cout << "Person(const char* name, const char* id, int age)" << endl;
}
Person(const Person& p)
:_name(p._name),
_id(p._id),
_age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p){
if(this != &p)
{
_name = p._name;
_id = p._id;
_age = p._age;
}
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person{
string _stuid;
public:
Student(const char* name, const char* id, int age, const char* stuid)
:Person(name, id, age),
_stuid(stuid)
{
cout << "Student(const char* name, const char* id, int age, const char* stuid)" << endl;
}
Student(const Student& s)
:Person(s),
_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s){
if(this != &s)
{
//*((Person*)this) = s;
Person::operator=(s);
_stuid = s._stuid;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
~Student(){
//Person::~Person();
cout << "~Student()" << endl;
}
void Print(){
cout << _name << " ";
cout << _id << " ";
cout << _age << " ";
cout << _stuid << endl;
}
};
void Test5(){
Student s("张三", "123456", 18, "111");
s.Print();
cout << endl;
Student s1(s);
s1.Print();
cout << endl;
s1 = s;
cout << endl;
}
运行结果:
5.1 构造
- 派生类是一个合成类,派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。
- 如果基类有默认构造,编译器会自动调用;如果基类没有默认构造,则必须在派生类构造函数的初始化列表阶段显示调用。
- 对于派生类自己的成员。其默认构造的处理方法为:内置类型不处理,自定义类型调用他的默认构造。
Student(const char* name, const char* id, int age, const char* stuid)
:Person(name, id, age), //如果基类没有默认构造,则必须在派生类构造函数的初始化列表阶段显示调用。
_stuid(stuid)
{
cout << "Student(const char* name, const char* id, int age, const char* stuid)" << endl;
}
注意:基类成员不能直接在派生类的初始化列表初始化!
5.2 拷贝构造&赋值重载
-
派生类的拷贝构造函数必须在初始化列表调用基类的拷贝构造完成基类部分的拷贝初始化。
-
派生类的operator=必须要调用基类的operator=完成基类部分的赋值。
-
对于派生类自己的成员。其默认拷贝构造/赋值重载的处理方法为:内置类型做值拷贝,自定义类型调用他的拷贝构造/赋值重载。
Student(const Student& s)
:Person(s), //调用基类的拷贝构造完成基类部分的拷贝初始化
_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s){
if(this != &s)
{
//调用基类的operator=完成基类部分的赋值
//*((Person*)this) = s; //写法一:将this强转成(Person*)再赋值
Person::operator=(s); //写法二:显示调用operator=,并注明基类类域。
_stuid = s._stuid;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
提示:
- 无论是调用基类的拷贝构造还是赋值重载,都是将子类切片赋值给基类的引用。(基类和派生类的赋值转换)
- 写法一和写法二都会调用基类的operator=
- 写法二:父子类operator=函数名相同构成隐藏,想要调父类的operator=需注明类域。
5.3 析构
- 在派生类的析构函数中不需要显示的调用基类的析构函数。派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证“先清理派生类成员,再清理基类成员”的顺序。如果显示调用,基类会被析构两次!
- 对于派生类自己的成员,其默认析构函数的处理方法为:内置类型不处理,自定义类型调用他的析构函数
~Student(){
//不需要显示的调用基类的析构
//派生类的析构函数会在被调用完成后自动调用基类的析构函数。
//Person::~Person();
cout << "~Student()" << endl;
}
注意:由于多态的需要,析构函数的名字会被统一处理成destructor()。父子类的析构函数同名构成隐藏,想要显示的调父类的析构需注明父类类域。
-
派生类对象初始化先调用基类构造再调派生类构造。
-
派生类对象析构清理先调用派生类析构再调基类的析构。
5.4 取地址重载
Student* operator&()
{
return this;
}
const Student* operator&() const
{
return this;
}
派生类的取地址重载不需要考虑父类部分的地址!
六、继承与友元
- 友元关系不能继承,也就是说基类友元不能访问派生类的私有和保护成员,但是基类友元可以访问派生类中继承自基类的成员。
- 派生类友元可以访问派生类的所有成员,包括继承自基类的成员。
测试代码:
class Student;
class Person{
friend void Display(const Person& p, const Student& s); //Display是基类Person的友元函数
protected:
string _name;
string _id;
int _age;
public:
Person(const char* name, const char* id, int age)
:_name(name),
_id(id),
_age(age)
{}
};
class Student : public Person{
//friend void Display(const Person& p, const Student& s);
string _stuid;
public:
Student(const char* name, const char* id, int age, const char* stuid)
:Person(name, id, age),
_stuid(stuid)
{}
};
void Display(const Person& p, const Student& s)
{
cout << "pname:" << p._name << endl;
cout << "sname:" << s._name << endl; //基类友元可以访问派生类中的基类部分成员
cout << "stuid:" << s._stuid << endl; //编译报错,基类友元不能访问派生类私有和保护成员
}
void Test6(){
Person p("xiaoming", "123123", 18);
Student s("LiHua", "4343", 20, "5678");
Display(p, s);
}
编译报错:
提示:想要访问派生类的私有和保护成员,需在派生类中设置友元关系。
七、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
测试代码:
class Student;
class Person{
protected:
string _name;
string _id;
int _age;
public:
static int _scount; //声明static静态成员
Person()
{
++_scount; //每构造一个Person实例,_scount就++
}
~Person()
{
--_scount; //每析构一个Person实例,_scount就--
}
};
int Person::_scount = 0; //在类外定义初始化,要加类域
class Student : public Person{
string _stuid;
};
class Teacher : public Person{
string _jobid;
};
void Test7(){
cout << Person::_scount << endl;
Student s1;
Student s2;
Student s3;
cout << Student::_scount << endl;
Teacher t1;
Teacher t2;
cout << Teacher::_scount << endl;
cout << &Person::_scount << " " << &Student::_scount << " " << &Teacher::_scount << endl;
}
运行结果:
八、菱形继承和虚拟继承
8.1 单继承和多继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
如何定义一个不能被继承的类?
C++98方案:将父类构造函数私有,使子类不可见。子类对象实例化,无法调用构造函数。
class A { private: A(){}; //将父类构造函数私有,使子类不可见 }; class B : public A{}; void Test1(){ B b; //error: ‘A::A()’ is private }
C++11方案:final关键字
class A final{}; //加fianl关键字,禁止继承 class B : public A{}; //error: cannot derive from ‘final’ base ‘A’ in derived type ‘B’ void Test1(){ B b; }
8.2 派生类的内存布局
class Base1{ public: int _b1; };
class Base2{ public: int _b2; };
class Derive : public Base1, public Base2{ public: int _d; }; //继承关系的声明顺序决定基类的空间布局顺序
int main(){
Derive d;
Base1 *pb1 = &d;
Base2 *pb2 = &d;
Derive *pd = &d;
cout << "pb1:" << pb1 << endl;
cout << "pb2:" << pb2 << endl;
cout << "pd:" << pd << endl;
}
选项:A. pb1 == pb2 == pd
B. pb1 < pb2 < pd
C. pb1 == pd != pb2
D. pb1 != pb2 != pd
运行结果:答案选C
由此得出结论:
8.3 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
class Person
{
public :
string _name ; // 姓名
void Func(){
cout << "Person::Func()" << endl;
}
};
class Student : public Person
{
protected :
int _num ; //学号
};
class Teacher : public Person
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
// 这样会有二义性无法明确知道访问的是哪一个
//a._name = "peter";
//a.Func();
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Student::Func();
a.Teacher::_name = "yyy";
a.Teacher::Func();
}
菱形继承的问题:
在菱形继承中,由于派生类同时继承了两个基类,其中一个基类又是另一个基类的公共基类,因此会出现成员变量和成员函数的重复继承问题。
-
对于成员变量,派生类会同时继承两个基类的同名成员变量,这会导致成员变量的访问存在二义性,需要使用作用域解析符来指定访问哪个基类的成员变量。
-
对于成员函数,派生类会同时继承两个基类的同名成员函数,这会导致成员函数的调用存在二义性,需要使用作用域解析符来指定调用哪个基类的成员函数。
-
重复继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。
菱形继承派生类的内存布局
B,C类中重复的存放A类的成员_a,由此造成了菱形继承的二义性和数据冗余的问题。
8.4 虚拟继承
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher继承Person时(继承公共基类的类)使用虚拟继承,即可解决问题。需要注意的是,虚拟继承只解决菱形继承的问题,不要在其他地方使用。
class Person //虚基类
{
public :
string _name ; // 姓名
};
class Student : virtual public Person //菱形腰部加virtual关键字,虚拟继承
{
protected :
int _num ; //学号
};
class Teacher : virtual public Person //菱形腰部加virtual关键字,虚拟继承
{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse ; // 主修课程
};
void Test ()
{
Assistant a ;
a._name = "peter"; //虚基类的成员只继承了一份,解决了菱形继承的二义性和数据冗余的问题
}
虚拟继承派生类的内存布局
不同于普通的菱形继承,菱形虚拟继承时:
- B,C类中不再重复的存放A类的成员,而是存放一个指向虚基表的指针。
- 虚基表中存有虚基类A相对指针地址的偏移量。通过该值即可找到A类的地址。
- B,C类的虚基表指向同一块虚基类A的空间,解决了菱形继承的二义性和数据冗余的问题。
虚拟继承基类的内存布局
- bb对象的大小是12字节
- 虚拟继承下,基类和派生类基类部分的内存布局相同,满足基类和派生类对象的赋值兼容(切片)。
- 虚拟继承虽然解决了菱形继承的问题。但相比普通继承,虚拟继承的访存速度相对较慢(要通过偏移量计算虚基类地址)。
- 因此虚拟继承只解决菱形继承的问题,不要在其他地方使用。
- 在日常编程中,最好尽量避免菱形继承。
更复杂的菱形继承
提示:B类和D类继承了公共的基类A,所以B类和D类使用虚拟继承。
九、总结
- 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的OOP语言都没有多继承,如Java。
继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
笔试面试题
- 什么是菱形继承?菱形继承的问题是什么?
- 什么是菱形虚拟继承?如何解决数据冗余和二义性的
- 继承和组合的区别?什么时候用继承?什么时候用组合?