目录
1. 继承和友元
2. 继承与静态成员
3. 菱形继承以及菱形虚拟继承
3.1. 单继承
3.2. 多继承
3.3. 菱形继承
3.4. 菱形虚拟继承
3.5. 菱形继承的底层细节
3.6. 菱形虚拟继承的底层细节
3.7. 虚拟继承
4. 继承的总结
5. 相关继承练习题
5.1. 如何定义一个无法被继承的类?
5.2. 关于切片的问题
5.3. 选择正确选项
1. 继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
测试 demo 如下:
namespace Xq
{class worker;// 基类class person{public:friend void display(const person& pobj, const worker& wobj);protected:std::string _name = "haha";};// 派生类class worker : public person{protected:std::string _job_number = "111";};void display(const person& pobj, const worker& wobj){cout << pobj._name << endl;cout << wobj._job_number << endl;}
}
void Test1(void)
{Xq::person pobj;Xq::worker wobj;Xq::display(pobj, wobj);
}
现象如下:
总而言之, 友元关系不可以从基类继承到派生类中,并且友元要慎用且不宜多用。
2. 继承与静态成员
基类定义了static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个 static 成员实例 。
测试 demo 如下:
namespace Xq
{class person{public:person(const char* name = "lisi"):_name(name){}public:std::string _name;// 静态成员属性static int count;};class worker : public person{public:protected:std::string _job_number = "111";};// 静态成员属性需要在类外定义int person::count = 0;
}void Test2(void)
{Xq::person pobj;Xq::worker wobj;pobj._name = "wangwu";cout << pobj._name << endl;cout << wobj._name << endl;cout << "wobj.count: " << wobj.count << endl;// 基类更改这个静态成员pobj.count = 5;cout << "wobj.count: " << wobj.count << endl;cout << "&pobj.count = " << &pobj.count << endl;cout << "&wobj.count = " << &wobj.count << endl;
}
现象如下:
上面的_name,基类对象和派生类对象各自私有一份,而对于静态成员变量 count,派生类继承的 count 和基类里面的 count 是同一份。
即基类里面的静态成员,无论有多少个派生类,它们都共享同一个静态成员。
如果我们想确定基类和派生类一共实例化了多少个对象,我们可以这样做:
person(const char* name = "lisi")
:_name(name)
{++count;
}
当实例化对象时 (无论是基类对象还是派生类对象),都会调用基类的构造函数,就会++count,且由于这个静态成员 count 是所有 ( 基类/派生类 ) 对象共享的,因此可以得到实例化 ( 基类/派生类 ) 对象的总个数;
3. 菱形继承以及菱形虚拟继承
3.1. 单继承
单继承:一个派生类只有一个直接基类,我们称这个继承关系为单继承。
比如有三个类,teacher 类继承 worker类,worker 类继承 person 类,如下所示:
class Person{};
// Worker 继承 Person 类
class Worker : public Person{};
// Teacher 公有继承 Worker 类
class Teacher : public Worker{};
如图所示:
3.2. 多继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承。
比如 Assistant 类 继承 Student 类和 Teacher 类,如下:
class Student{};
class Teacher{};
// Assistant 类 公有继承 Student 和 Teacher
class Assistant : public Student, public Teacher {};
如图所示:
3.3. 菱形继承
菱形继承:菱形继承是多继承的一种特殊情况。
比如 Assistant 类继承 Student 类和 Teacher 类,同时 Student 类继承 Person 类,Teacher 继承 Person类,就形成了一个菱形继承,如下:
具体如下:
namespace Xq
{class Person{public:std::string _name; //姓名};class Teacher : public Person{protected:std::string _id_card; //职工编号};class Student : public Person{protected:std::string _num; // 学号};class Assistant : public Teacher, public Student{protected:std::string _subject; //学科};
}void Test(void)
{// 实例化一个 Assistant 对象Xq::Assistant obj;
}
菱形继承的问题:从下面的 Assistant类的对象成员模型,可以看出菱形继承有数据冗余和二义性的问题。
可以看到,Assistant 的对象中 person 成员 (_name) 会有两份 (一份是 Teache r类中的,另一份是 Student 类中的),并且访问时会存在二义性问题,如下 demo:
void Test(void)
{Xq::Assistant obj;obj._name = "haha";
}
现象如下:
二义性问题即调用不明确,编译器在这里不知道这个_name 是哪一个类中的_name, 也许是 Student 这个类中的,也许是 Teacher 这个类中的,这就是菱形继承的二义性问题。
当然我们也可以解决这种问题,方案是:指明类域,如下 demo:
void Test(void)
{Xq::Assistant obj;obj.Teacher::_name = "haha";obj.Student::_name = "hehe";
}
此时就可以成功编译,现象如下:
虽然可以通过指明类域解决菱形继承的二义性问题,但是菱形继承的数据冗余的问题没有解决。
因此,人们在此基础上引入了菱形虚拟继承,虚拟继承可以解决菱形继承的二义性问题和数据冗余的问题。
如上面的继承关系,在 Student 和 Teacher 继承 Person 时使用虚拟继承,即可解决二义性问题和数据冗余问题,需要注意的是,虚拟继承不要在其他地方去使用。
3.4. 菱形虚拟继承
菱形虚拟继承:
比如 Assistant 类继承 Student 类和 Teacher 类,同时 Student 类虚拟继承 Person 类,Teacher 虚拟继承 Person类,就形成了一个菱形虚拟继承,如下图所示:
上图对应的 code 如下:
namespace Xq
{class Person{public:std::string _name; //姓名};class Teacher : virtual public Person //虚拟继承{protected:std::string _id_card; //职工编号};class Student : virtual public Person //虚拟继承{protected:std::string _num; // 学号};class Assistant : public Teacher, public Student{protected:std::string _subject; //学科};
}void Test()
{Xq::Assistant obj;obj._name = "haha";
}
此时还会有数据冗余问题和二义性问题吗? 结果如下所示:
通过现象,我们发现:
- 首先,此时不指明类域,也可以成功编译,即菱形虚拟继承可以解决二义性问题;
- 其次,我们发现,在 obj 这个对象中,它们的 _name 是同一个成员,因此,这也就解决了数据冗余的问题。
3.5. 菱形继承的底层细节
为了探究菱形继承的底层细节,我们所用的 demo 具体如下:
根据上面的继承关系,写出如下 code,并通过 Test1 来进行测试:
namespace Xq
{class A{public:int _a;};class B : public A{public:int _b;};class C : public A{public:int _c;};class D : public B, public C{public:int _d;};
}void Test1(void)
{Xq::D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}
运行进程,通过监视窗口,得到下面的结果:
我们也可以观察此时的内存窗口,如下:
因此,结合上面的两个窗口,我们得到此时的菱形继承的d对象模型,具体如下:
可以看到,此时基类A中的成员 _a 在菱形继承的D类模型中出现了两次,即数据冗余问题,也正因为这个原因,当D类实例化的对象访问这个_a成员时,会存在着二义性的问题。
3.6. 菱形虚拟继承的底层细节
为了探究菱形虚拟继承的底层细节,我们所用的 demo 具体如下:
根据上面的继承关系,写出如下 code,并通过 Test2 来进行测试:
namespace Xq
{class A{public:int _a;};class B : virtual public A{public:int _b;};class C : virtual public A{public:int _c;};class D : public B, public C{public:int _d;};
}void Test2(void)
{Xq::D d;d._a = 0;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;
}
运行进程,通过监视窗口,得到下面的结果:
我们也可以观察此时的内存窗口,如下:
因此,结合上面的两个窗口,我们得到此时的菱形虚拟继承的d对象模型,具体如下:
首先,上面的代码可以成功编译,因此,菱形继承解决了二义性问题;
其次, 在菱形虚拟继承的D类对象模型中,只有一份 _a (A类中的),因此也解决了数据冗余的问题 (在这里将_a放在了一个公共的区域)。
但是,我们发现一个事实,与菱形继承相比,多了两个地址,那有人说,这不是空间浪费了吗?
实际上,并没有,如果此时的A很大 (有很多成员),如果没有虚拟继承,那么就会造成大量的重复数据 (数据冗余),进而导致浪费空间,因此,事实上,虚拟继承会将冗余数据变成一份数据 (共享),因此会节省空间。
那现在的问题就是,这两个地址是什么呢?
通过内存窗口查看这个两个地址指向的内容,具体如下:
B类对象中的地址 0x0058dd1c 的内容如下:
C类对象中的地址 0x0058dd38 的内容如下:
我们发现这个地址里面存的是一个数字,B类对象中的地址存储的 0x00000014,即20;C类对象中的地址存储的 0x0000000c,即 12。
这两个数字我们称之为相对距离,有的也叫偏移量。
刚刚我们说了虚拟继承会把这里的 _a 放在一个公共的区域里面,那么对于B和C这两个类它是如何知道这个_a 所在的位置呢?
因此,B和C类就需要借助这个偏移量。
同时,我们发现,
为什么要设计这个偏移量呢? 原因是因为在某些情况下,我们必须借助偏移量找到这个公共的位置。
例如当发生切片的时候,具体如下:
void Test3(void)
{Xq::D d;// 发生切片的时候:// 比如 D 类对象经过切片得到B类(对象/引用/指针)Xq::B b1 = d;Xq::B& b2 = d;Xq::B* b3 = &d;// 也比如 D 类对象经过切片得到C类(对象/引用/指针)Xq::C c1 = d;Xq::C& c2 = d;Xq::C* c3 = &d;
}
因为此时这里的 _a 成员 (A类中的_a) 在菱形虚拟继承中的D类对象只有一份,这一份 _a 被放在了D对象模型中的公共位置。
在非虚拟继承中,_a 会在B类对象中有一份,在C类对象中也有一份,当发生切片时,B和C类很容易找到这个 _a;
但如果是虚拟继承,这个_a就在公共位置,而并没有在B和C类对象中,因此,当发生切片时,B和C类就需要借助偏移量找到这个_a。
我们用下图总结下菱形虚拟继承:
3.7. 虚拟继承
接下来,我们来探讨一下虚拟继承的细节问题,我们所用的 demo 具体如下:
根据上面的继承关系,写出如下 code,并通过 Test4 来进行测试:
class A
{
public:int _a;
};
class B : virtual public A
{
public:int _b;
};void Test4(void)
{Xq::B b;std::cout << "Xq::B b size: " << sizeof(b) << std::endl;
}
首先,有一个问题:我们单看B类,它实例化的对象是多大呢?
单看B类,它是一个单继承,因此里面一定会有_a (继承A类中的),那实例化的大小是8吗?这个虚拟继承是否会有影响?我们看看结果:
结果并不是8,那么肯定就是虚拟继承带来的变化了,我们通过内存窗口,查看一下,为什么是12呢?
不过在这之前,为了查看的更明显一点,我们更改一下测试代码,如下:
void Test5(void)
{Xq::B b;b._a = 1;b._b = 2;std::cout << "Xq::B b size: " << sizeof(b) << std::endl;
}
运行进程,调出内存窗口,现象如下:
我们发现菱形虚拟继承B类对象模型中有一个地址,而运行起来的进程是32位的,故地址是4字节,因此,B类的对象是12个字节。
既然它是一个地址,那么它的内容是什么呢?如下:
结合上面的分析,我们可以得出B对象的模型如下:
事实上,虚拟继承B类中的对象模型中会存储一个指针,这个指针只想一张表,这张表会存储偏移量的值,在这里因为是单虚拟继承,因此只有一个偏移量。
这里的指针 (偏移量地址) 我们称之为虚基表指针,这张表我们称之为虚基表。
4. 继承的总结
- 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,也不建议设计出菱形继承,否则在代码维护以及性能上都有问题;
- 多继承可以认为是 C++ 的缺陷之一,很多后来的oo(object--oriented) 语言都没有多继承,如 Jave;
- 继承和组合:
- public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象;
- 组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象;
- 优先使用对象组合,而不是类继承。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse) ;
- 术语 “ 白箱 ” 是相对可视性而言:在继承方式中,基类的内部细节对子类可见,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高;
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用 (black-box reuse) ,因为对象的内部细节是不可见的。对象只以 “ 黑箱 ” 的形式出现。组合类之间没有很强的依赖关系,耦合度低;
- 优先使用对象组合有助于保持每个类的封装特性;
- 实际中尽量多去使用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
下面用实例说明什么是 is-a (继承),什么是has-a (组合)?demo 具体如下:
is-a关系:
class A
{
public: void func() { cout << _a << endl;}
protected:int _a;
}; // (公有)继承 --- is-a关系
// 由于A中成员的改动可能会影响B类,体现了耦合度高的特点class B : public A
{
public: //...
};
has-a关系:
class C
{
public: void func() {cout << _c << endl;}
protected:int _c;
}; // 组合 --- has-a关系
// 而对于组合而言,C的保护改动基本不影响D类,体现了耦合度低的特点 class D
{
public: // ...
protected: class C _c; // (组合)
};
在未来代码设计中,遵循的设计原则是:低耦合,高内聚。
5. 相关继承练习题
5.1. 如何定义一个无法被继承的类?
第一种方式,将基类的构造私有化,派生类继承这个基类,在实例化对象时,需要调用基类的构造,但由于基类的构造已经私有化,故会编译报错。
namespace Xq
{class A{public://将基类的构造函数私有化private: A(int a = int()):_a(a){cout << "A()" << endl;};protected:int _a;};class B : public A{protected:int _b;};
}void Test3(void)
{Xq::B b;
}
现象如下:
上面的做法是 C++98 的做法,而对于 C++11 的做法是:通过关键字 final,被 final 修饰的类,无法被继承,编译器会强制检查。
namespace Xq
{// 用 final 修饰 A类, 此时A类无法被继承class A final{public:A(int a = int()) :_a(a){std::cout << "A()" << std::endl;};protected:int _a;};class B : public A{protected:int _b;};
}void Test4(void)
{Xq::B b;
}
现象如下:
5.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* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}// A:p1 = p2 = p3
// B: p1 < p2 < p3
// C: p1 == p3 != p2
// D: p1 != p2 != p3
做这种问题,我建议先把 Derive 的对象模型画出来,如下:
此时,我们再代入问题,如下:
现在,我们就得出答案了, p1 == p3 != p2。
不过我需要补充几点:
在多继承场景下,派生类继承多个基类时,是先继承的谁呢?
当派生类继承多个基类时,写在前面的就先继承谁。
诸如上面的例子:
class Derive : public Base1, public Base2 {public: int _d; };
此时就是先继承 Base1,在继承 Base2,Derive 对象模型如下:
如果是下面这样呢?
class Derive : public Base2, public Base1 {public: int _d; };
此时就是先继承 Base2,在继承 Base1,Derive 对象模型如下:
我们发现这个问题的结果是:p1 == p3 != p2,但请记住,虽然 p1 和 p3 指向了同一个位置,但意义不同,因为,指针的类型决定了它能访问内容的大小,如下所示:
同时,我们再看一看对象里面的成员在内存中如何存储的,我们通过下面的 demo 测试:
void Test4()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;p1->_b1 = 1;p2->_b2 = 2;p3->_d = 3;
}
通过内存窗口得到如下现象:
换言之,这三者的地址关系应该是: p1 == p3 < p2。
5.3. 选择正确选项
如下所示:
class A{
public:A(char *s) { std::cout << s << std::endl; }~A(){}
};
class B :virtual public A
{
public:B(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class C :virtual public A
{
public:C(char *s1, char*s2) :A(s1) { std::cout << s2 << std::endl; }
};
class D :public B, public C
{
public:D(char *s1, char *s2, char *s3, char *s4) :C(s1, s3), B(s1, s2), A(s1){std::cout << s4 << std::endl;}
};
int main() {D *p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}// A:class A class B class C class D
// B:class D class B class C class A
// C:class D class C class B class A
// D:class A class C class B class D
首先我们要知道,初始化列表的初始化顺序是由成员声明顺序决定的,而在继承中,谁先被继承,谁就先初始化。
在这里,A类最先被D类继承,然后D类继承B,在继承C,因此A类最先被初始化,下来是B,下来是C,因此最后的答案是A。
结果如下:
继承的宏观内容到此结束。