4.继承与友元
我们先说结论:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
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; } void main() { Person p; Student s; Display(p, s); }
对于上面的继承关系,person有一个友元函数,这个函数可以访问父类的成员变量,当子类继承父类后,我们用这个函数是会报错的,因为友元关系是不会被继承的:
那么该如何解决这种情况呢?我们只需要在子类中也声明一下友元关系即可:
class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name = "peter"; // 姓名 }; class Student : public Person { friend void Display(const Person& p, const Student& s); protected: int _stuNum = 10086; // 学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; } int main() { Person p; Student s; Display(p, s); return 0; }
5.继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
class Person { public: Person() { ++_count; } protected: string _name; // 姓名 public: static int _count; // 统计人的个数。 }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; // 学号 }; class Graduate : public Student { protected: string _seminarCourse; // 研究科目 }; void TestPerson() { Student s1; Student s2; Student s3; Graduate s4; cout << " 人数 :" << Person::_count << endl; Student::_count = 0; cout << " 人数 :" << Person::_count << endl; } int main() { TestPerson(); return 0; }
我们可以看到,静态成员变量是所以对象所共有的,无论继承多少次,都只有一个count。下面我们验证一下:
通过上图可以看到父类中的name和子类中的name根本不是一个name,那么count呢?
通过验证我们也能发现静态成员变量确实只有一个,不管继承了多少次。
下面我们实现一个不能被继承的类:
class A { private: A() { } }; class B :public A { }; int main() { B bb; return 0; }
一旦我们将构造函数私有化了那么就不能继承了。
这样的情况是因为我们不想我们的类被继承,那我们本类自己如何调用这个私有化的构造函数呢?
class A { public: static A CreateObj() { return A(); } private: A() { } }; class B :public A { }; int main() { //B bb; A::CreateObj(); return 0; }
我们可以实现一个函数让这个函数返回一个A类型的匿名对象即可,由于函数必须由对象去调用我们无法创建一个对象,所以我们将这个函数设为static静态的函数,这样就可以用类名去调用这个函数了。
6.复杂的菱形继承与菱形虚拟继承
我们先看看单继承和多继承的区别:
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
菱形继承有数据冗余和二义性的问题,现在我们通过代码调试来观察:
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; // 主修课程 }; void Test() { // 这样会有二义性无法明确知道访问的是哪一个 Assistant a; a._name = "peter"; // 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
首先在上面的代码中类assistant继承了student和teacher,所以我们创建了assistant对象后发现访问成员变量name无法访问,编译器报错不明确,如下图:
这个问题解决起来很简单,就是将我们的变量前面加上域作用限定符,然后指定访问的是哪一个name变量,如下图:
那么数据冗余是什么呢?当一个子类继承两个父类的时候,这两个父类同样是继承另一个父类的,这样就会有很多相同的数据被继承到了子类中,一旦在项目中代码非常多非常复杂那么这种情况是非常浪费空间的,那么这个问题c++的祖师爷是如何解决的呢?这里就引入了虚继承,虚继承可以解决数据冗余和二义性的问题:
虚继承的语法就是在原先继承方式的前面加上virtual关键字。下面我们通过一个简单的菱形继承模型来看看c++祖师爷是如何解决数据冗余和二义性的问题:
class A { public: int _a; }; // class B : public A class B : virtual public A { public: int _b; }; // class C : public A class C : virtual public A { public: int _c; }; class D : public B, public C { public: int _d; }; int main() { D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; return 0; }
我们先看一下不加virtual继承的情况:
通过上图我们可以看到B中继承的a在内存第一行第二行就是B中的成员b,C中继承的a在内存中第三行,最后一个红色的是d的值。那么我们再看看虚继承的结果:
我们发现虚继承中的内存地址和我们刚刚看到的完全不一样,虚继承里面存放的竟然是指针,B类中有一个指针和3这个值,C类中有一个指针和4这个值,那么这能代表什么呢?我们再开一个内存来看看刚刚里面存放的地址到底是什么东西:
我们发现这个地址里面的开头都是0,这是什么意思呢?
我们通过那个0下面的值发现,16进制转化为十进制分别是20和12,然后我们试着在第一个地址加上这个值发现,上面的地址加上这个值正好指向了A:
也就是说虚继承解决数据冗余和二义性的本质是通过偏移量找到A让其这三个类中的a变量都指向父类的那个变量的地址。
总结:
这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
下面我们做一道多继承的题:
对于上面这道题选哪个选项呢?我们来解释一下:
首先D类指针p开了一个D的空间,然后进入D的构造函数,在D的构造函数中我们发现初始化列表对三个类都进行了初始化,而初始化的顺序是谁先继承谁就先初始化,A先被B继承所以先去调用A的构造函数初始化,随后打印class A,接着B类对象初始化,在B类初始化列表中我们发现又初始化一次A,那么A会成功初始化吗?答案是不会因为我们虚继承了,三份A只用初始化一次A,所以这次直接打印class B,然后初始化C,C与B同理打印class C,走完D的初始化列表后进入构造函数打印class D所以答案选A。最重要的是要知道虚继承后只有一份A走了构造函数。
下面我们再看一下组合和继承的区别:
组合的耦合度低,将类C改了类D不会受很大的影响,而继承一旦A改了B继承的很多成员都会随之改变。
总结
1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
3. 继承和组合
public继承是一种 is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。