1. 无法被继承的类
要实现无法被继承的类有两种方式:
C++98及其之前:将父类的构造函数设置为private成员。
C++11及其之后:使用final关键字修饰父类。
将构造函数设置为private是因为:子类的构成必须调用父类的构造函数,但是父类的构成函数私有化以后,子类看不见就不能调用了,那么子类就无法实例化出对象。
// C++11的⽅法class Base final{public:void func5() { cout << "Base::func5" << endl; }protected:int a = 1;private:// C++98的⽅法/*Base(){}*/};class Derive :public Base{void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;};int main(){Base b;Derive d;return 0;}
2. 友元关系与继承
父类的友元关系不能被继承。也就是说,也就是说父类友元不能访问子类私有和保护成员。
这一点很好记忆:
父类进行友元声明 ---> 这个函数/类是我的好友。
子类 ---> 我父亲的好友是我的叔叔而不是好友。
class Student;class Person{public:friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名};class Student : public Person{protected:int _stuNum; // 学号};void Display(const Person& p, const Student& s){cout << p._name << endl;cout << s._stuNum << endl;}int main(){Person p;Student s;// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员// 解决⽅案:Display也变成Student 的友元即可Display(p, s);return 0;}
3. 静态成员与继承
父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
class Person{public:string _name;static int _count;};int Person::_count = 0;class Student : public Person{protected:int _stuNum;};int main(){Person p;Student s;// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份cout << &p._name << endl;cout << &s._name << endl;// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的// 说明派⽣类和基类共⽤同⼀份静态成员cout << &p._count << endl;cout << &s._count << endl;// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;}
4. 多继承以及菱形继承问题
4.1 多继承
单继承:一个子类类只有一个直接父类时称这个继承关系为单继承。
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的父类在前面,后继承的父类在后面,子类成员在放到最后面。
class Assistant : public Student, public Teacher
4.2 菱形继承
菱形继承是多继承的一种特殊情况。
菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。
支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
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; // 主修课程};int main(){// 编译报错:error C2385: 对“_name”的访问不明确Assistant a;a._name = "peter";// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;}
4.3 虚继承
虚继承是为了解决菱形继承而提出的,像Person这样被重复继承的类可以用virtual关键字来进行继承:
// 使用虚继承Person类class Student : virtual public Person{protected:int _num; //学号};// 使用虚继承Person类class Teacher : virtual public Person{protected:int _id; // 职⼯编号};// 教授助理class Assistant : public Student, public Teacher{protected:string _majorCourse; // 主修课程};
当两个虚继承了同一个爷爷类(菱形的上顶点)的类被孙子类(菱形的下顶点)继承时,这两个子类的共同父类(爷爷类)就只会被孙子类继承一次。
此时,Person由于只被继承了一次,所以不再适合放到Student内部或Teacher内部,而是被放到整个对象的最下面。
在调用构造函数时,孙子类的初始化列表中会优先调用爷爷类的构造函数,然后是先继承的类的构造函数,后继承的类的构造函数。
而爸爸类初始化列表中对爷爷类构造函数的调用会失效。
很多人说C++语法复杂,其实多继承就是一个体现。
有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。
多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。
我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。
5. 指针偏移问题
上一篇提到,子类对象可以给父类指针赋值。
在上面,我们已经见识过多继承对象内部成员的分布,当我们用Assistant对象给Teacher对象的指针赋值时,得到的地址实际上是Teacher成员开始的位置。
在下面的这个例子中,我们可以看到p1 == p3 != p2的结果。
class Base1 { public: int _b1; };class Base2 { public: int _b2; };class Derive : public Base1, public Base2 { public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;}
6. IO库中的菱形虚拟继承
template<class CharT, class Traits = std::char_traits<CharT>>class basic_ostream : virtual public std::basic_ios<CharT, Traits>{};template<class CharT, class Traits = std::char_traits<CharT>>class basic_istream : virtual public std::basic_ios<CharT, Traits>{};
7. 继承和组合
• public继承是一种is - a的关系。也就是说每个派生类对象都是一个基类对象。
• 组合是一种has - a的关系。假设B组合了A,每个B对象中都有一个A对象。
• 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white - box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对派生类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
• 对象组合是类继承之外的另⼀种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black - box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
• 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is - a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is - a)也适合组合(has - a),就用组合。