写在前面
继承小节相对来说非常重要,因为继承多态和封装是面向对象编程的三大特性。
继承(inheritance) 机制是面向对象程序设计是代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行拓展,增加功能,这样产生新的类,叫做派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触到的复用是函数层次的复用,但是继承是类设计层次的复用。
继承
定义格式
现在先举一个例子:
class Person{
public:
void Print(){
cout << "name:" << name_ << endl;
cout << "age:" << age_ << endl;
}
protected:
string name_ = "ZS";
int age_ = 18;
}
class Student : public: Person{
protected:
int stuId_;
};
定义格式:
class DerivedClass : public BaseClass
继承方式有三种,public、protected、private继承,和访问限定符相对。这三种继承方式的区别是基类成员在派生类中的访问权限。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 不可见 | 不可见 |
- 基类的private 成员在派生类中无论以什么方式继承都不可见。但是不可见指的是,基类的私有成员还是被继承到了派生类对象中,但是语法上限制了派生类对象不管在类里面还是类外面都不能去访问他。
- 如果基类成员不想在类外直接被访问而可以在派生类中访问,就定义为protected。可以看出protected 成员限定符是因为继承才出现的。
- 基类private 成员在派生类中都不可见,除了private 成员,其他成员的访问方式 = Min(成员在基类中的访问限定符,继承方式)。(顺序:public > protected > private)
- 使用关键字class 时默认的继承方式时private,使用struct 时默认的继承方式是public,不过最好显式的写出继承方式。
- 在实际生活中,一般使用的都是public 继承,几乎很少使用protected/ private继承,也不提倡使用后两种继承,因为维护性不强。
基类和派生类对象赋值转换
- 切片:派生类对象可以赋值给基类的对象/ 基类的指针/ 基类的引用。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。
- 但是必须是基类的指针是指向派生类对象时才是安全的。如果基类是多态类型,可以使用RTTI (run-time type information)的 dynamic_cast 来进行识别后安全转换。
class Person{
protected:
string name_;
string sex_;
int age_;
};
class Student : public Person
{
public:
int id_;
};
void Test(){
Student sobj;
//1. 子类可以赋值给父类对象/指针/引用
Person pobj = sobj;
Person* pp = &sobj;
Person& rp = sobj;
//2. 基类对象不能赋值给派生类对象
sobj = pobj;
//3. 基类的指针可以通过强制类型转换赋值给派生类的指针
pp = &pobj;
Student* ps1 = (Student*) pp;
}
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 如果子类和父类有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。
- 在子类成员函数中,可以使用 基类名 :: 基类成员显式访问。
- 如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不需要参数(类型/个数等)完全相同。
- 注意实际中在继承体系里最好不要定义同名的成员。
举个例子:
class Person{
protected:
string name_;
string sex_;
int age_;
public:
void func(){
cout << "func()" << endl;
}
};
class Student : public Person
{
public:
int id_;
string name_;
public:
void func(int i){
cout << "func(int i)" << endl;
}
};
//可以发现Student 和 Person类中都有name_ 成员,这时会构成隐藏,在子类中如果cout<< name_ ,输出的是子类成员的name_.
//如果想访问父类成员的name_,使用:Person::name_.
//可以发现Student 和 Person类中都有func 成员函数,但是因为不在同一个作用域中,所以不构成重载。
//但是因为二者同名,构成隐藏。也就是说如果直接调用父类中的func()函数,不能调用到。
派生类的默认成员函数
- 派生类的构造函数,必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,就必须在派生类的构造函数的初始化列表中显式调用。
- 派生类的拷贝构造函数必须要调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator= 必须要调用基类的operator= 来完成基类的赋值。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造函数再调用派生类构造。
- 派生类对象清理析构时先调用派生类析构再调用基类析构。
- 因为后续场景析构函数需要构成重写,重写的条件之一是函数名相同,编译器会对析构函数名进行特殊处理,在编译器中被处理为destructor(), 所以父类析构函数不加virtual 的情况下,子类析构函数和父类析构函数会构成隐藏。
继承和其他
继承和友元
友元关系不能继承,也就是说基类的友元函数不能访问子类的私有和保护成员。
继承和静态成员
如果基类定义一个static 静态成员,则整个 继承体系中 只有一个这样的成员,无论派生出多少个子类,都只有一个static 成员实例。
菱形继承和菱形虚拟继承
单继承:一个子类只有一个直接父类的继承关系。
多继承:一个子类有两个或以上直接父类的继承关系。
菱形继承:多继承的一种特殊情况。
菱形继承的问题:数据冗余和二义性。
因为在上面的例子中,Assistant 创建出的对象,Person 成员会有两份,所以会造成数据冗余和二义性。
解决问题:虚拟继承。
如果在Student 和Teacher 在继承Person时使用虚拟继承,就可以解决问题。注意,虚拟继承只适用于这种菱形继承的场景,如图。
虚拟继承可以解决数据冗余和二义性。原理是:
通过使用虚拟继承,不存放冗余数据,而是放了虚基表的指针,这个虚基表中存放了偏移量,通过偏移量可以找到数据(原来重复的数据),这个数据分属于不同的父类(Student 和Teacher)。就完成了虚拟继承。因为确定了这个成员到底继承自哪个父类。
小结
-
多继承是C++ 语法复杂的体现。正因为多继承,才有菱形继承,正因为有菱形继承,才有菱形虚拟继承。所以不建议设计出多继承,复杂度提升,性能下降。
-
多继承是C++的缺陷。
-
继承和组合:
- public 继承是一种 is-a 的关系,每个派生类对象都是一个基类对象。
- 组合是 has-a 的关系,假设B 组合了A,每个B对象中都有一个A对象。
- A 继承自B可以说是:A is kind of B.
- A 和B组合可以说 : A is part of B.
-
优先使用对象组合而不是类继承。
- 继承允许你根据基类的实现来定义派生类的实现。这种复用叫做白箱复用(white - box reuse)。因为,在继承方式中,基类的内部细节对对子类可见。继承一定程度上破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类之间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另外一种复用关系。可以通过组装或者组合对象来获得更复杂的功能。对象组合要求被组合对象有良好定义的接口,这种复用叫黑箱复用(black-box reuse),因为对象内部细节是不可见的。组合类之间没有很强依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。
- 尽量使用组合,组合耦合度低,代码维护性好。但继承也有用,因为有的关系适合继承;此外,实现多态也必须要继承。类之间的关系可以用继承,如果可以组合就用组合。
-
组合和继承的代码例子
//继承 class PeopleInfo{ public: int age_; string name_; }; class TeacherInfo : public PeopleInfo { public: int teaId_; } //组合 class InformationSheet{ private: vector<PeopleInfo> ppInfo_; ... }
本小节完。注意继承和多态的重要性,所以我们应该着重训练这方面的题目,加深理解。