继承
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
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;
Display(p, s);
return 0;
}
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
class Person
{
public:
Person()
{
++_count;
}
static int _count; // 统计人的个数。
protected:
string _name; // 姓名
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
return 0;
}
// 人数 :4
// 人数 :0
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
- 多个基类之间使用
,
隔开 - 每个基类之前都必须要添加继承权限,如果没有显示给出,则使用的是默认的继承权限
- 在子类对象模型中,基类部分的成员排列次序与继承列表中基类的继承次序一致
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant 的对象中Person成员会有两份。
代码示例:
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";
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承 Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test() {
Assistant a;
a._name = "peter";
}
虚拟继承解决数据冗余和二义性的原理:
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助 内存窗口观察对象成员的模型。
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;
}
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A 同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
虚拟继承和普通继承的区别:
- 虚拟继承比普通继承多了四个字节的大小。
- 如果用户没有显式定义构造函数,则编译器会对子类生成一份默认的构造函数;如果子类显式定义了构造函数,则编译器会对子类构造函数进行修改(向对象前4个字节中填充数据)
- 对象模型布局方式不一样
下面是上面的Person关系菱形虚拟继承的原理解释:
继承与组合
-
很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
-
多继承可以认为是C++的缺陷之一,很多后来的OO语言( 面向对象(Object Oriented))都没有多继承,如Java。
-
继承和组合(聚合)
- public继承是一种 is-a(类的父子继承关系)的关系。也就是说 每个派生类对象都是一个基类对象。
- 组合是一种 has-a(对象和它的成员的从属关系)的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系, 耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
// Car和BMW、Car和Benz构成is-a的关系
class Car{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
// Tire和Car构成has-a的关系
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
面试题系列
- 什么是菱形继承?菱形继承的问题是什么?
一个基类被两个派生类都继承了,又有一个子类继承了这两个派生类
问题是:存在数据的冗余和二义性!
- 什么是菱形虚拟继承?如何解决数据冗余和二义性的?
一个基类被两个派生类继承(虚拟继承),又有一个子类继承了这两个派生类
- 首先 通过添加域的访问限定符解决:数据的二义性
假设有一个类A,它有两个子类,分别为类B和类C,再有一个类D又继承了B类和C类
- 引入虚拟继承机制(虚继承) :可以同时解决菱形继承的二义性和数据冗余的问题。
我们需要在类B和类C继承基类A时加入virtual,这样保证了在子对象创建时,只保存了基类A的一份拷贝。(C++使用虚拟继承,解决了从不同路径继承来的相同基类的数据成员在内存中有不同的拷贝造成数据不一致的问题,将共同基类设置为虚基类,这时从不同路径继承的虚基类在内存就只有一个映射。)
- 虚拟继承中为什么多给4个字节?
在虚拟继承中,构造函数内部将一个指针(指向 虚基表/偏移量表格)放在了对象内存空间的前4个字节(将空间里面存储的内容当成偏移量)。因此派生类多出来的4个字节就是该指针。(在64位系统下指针占8个字节)
虚基表/偏移量表格 存储两个信息:
- 第一行(前4个字节)表示:子类对象相对于自己的偏移量
- 第二行(后4个字节)表示:子类对象相对于基类部分成员起始位置的偏移量
- 为什么对象模型倒着的
首先按照菱形继承顺序:基类(C1、C2)中先继承先存储(基类存储在上面),派生类( D )存储在最下面。此时由于是虚拟菱形继承,基类存储时顶层有虚基表指针,为了继承的公平起见,将基类( B )存储在内存的最下面。
- 如何采用虚拟继承来解决菱形继承二义性问题的?如何解决的?
最底层基类(B)成员只存储了一份。 通过虚基表指针去寻找。
- 继承和组合的区别?什么时候用继承?什么时候用组合?
public继承是一种is-a(类的父子继承关系)的关系;组合是一种has-a(对象和它的成员的从属关系)的关系
- 要实现多态,必须要继承。
- 组合类之间没有很强的依赖关系, 耦合度低。优先使用对象组合有助于你保持每个类被封装。
- 常见面试题:实现一个不能被继承的类。
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11给出了新的关键字final禁止继承
class NonInherit final
{};
- 如何访问父类中被private声明的函数
尽管从各种c++书籍中我们得到的信息是子类从父类继承private成员虽然被子类继承,但是无法被子类访问。但是当父类的private函数是一个虚函数时,我们却可以通过读取VTABLE表中信息,从而找到父类虚函数的地址,进而调用它。
要点总结
一、友元
爸爸的朋友不是我的朋友
※友元所有的逻辑参考现实逻辑
二、静态成员继承
静态成员可以继承,但无论发生任何事,都只有一个拷贝。
三、多重继承
一个派生类可以来源于多个基类,多个基类间用逗号隔开。
如果多个父类中有重名的成员,那么会产生二义,必须用父类名::的方式指明用谁的成员。
如果继承的多个父类中有多个虚表,那么子类将全部继承下来。如果子类出现了新的虚函数,那么会加在第一个虚表(第一个继承的父类的虚表)的后面,如果多个父类中含有相同的虚函数,子类重写后,将会只出现一个虚函数。
四、菱形继承、虚继承
某个类的两个父类拥有一个相同的父类。
冗余性:这个类包含了两份爷爷类
二义性:两个爷爷长得一样,分不清
虚继承:
含有一个虚基类指针(vbptr),指向自己的基类,作用是可以描述自己的父类。当发现被继承的另一个父类中也有这么一个相同的虚基类时,两个基类就会合并,只保留一个。
普通的继承只继承了爷爷的衣钵,不知道爷爷是谁。虚继承都知道,所以,发现我的两个爸爸的爸爸是同一个人的时候,干脆就只要一个爷爷。
虚继承只用于菱形继承的情况
虚继承可以解决菱形继承带来的问题。
继承习题
- 下面程序输出结果是什么?
#include<iostream>
using namespace std;
class A {
public:
A(char* s)
{
cout << s << endl;
}
~A() {}
};
class B :virtual public A
{
public:
B(char* s1, char* s2) :A(s1) {
cout << s2 << endl;
}
};
class C :virtual public A
{
public:
C(char* s1, char* s2) :A(s1) {
cout << s2 << endl;
}
};
class D :public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4) : B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << 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
答案解析:
运行发现必须在class B & class C 以及 class D 中实现对继承的类A的构造,即“ :A(s1) ”等语句
D实例对象时,真正起构造A类作用的是最后在D类中对A的构造,虚继承中对基类的构造是该类类型的实例对象初始化,但D类型的实例对象对A类初始化只与D有关,不会再执行一次B/C中的构造,即ABCD。
至于为什么不是 ACBD呢?
按照继承的顺序来的
若将class D :public B, public C
写为class D :public C, public B
,应该选 D , ACBD!!
- 关于重载和多态正确的是
A. 如果父类和子类都有相同的方法,参数个数不同,将子类对象赋给父类后,由于子类继承于父类,所以使用父类指针调用父类方法时,实际调用的是子类的方法
B. 选项全部都不正确
C. 重载和多态在C++面向对象编程中经常用到的方法,都只在实现子类的方法时才会使用
D.
class A{
void test(float a){cout<<"1";}
};
class B:public A{
void test(int b){cout<<"2";}
};
A *a=new A;
B *b=new B;
a=b;
a.test(1.1);
结果是1
正确答案:
B
答案解析:
失手选了D。。。
选项D确实不该选,做过之后再来看,选D确实太傻了。 首先,class默认访问权限为private,故所有函数都不能在外部被调用。 其实,即使把函数的访问权限声明为public,也还是不对。通过指针调用函数,应该使用a->test(1.1); 错的离谱。 选D确实不该。 正确答案为B。
- 多继承中指针偏移问题?
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;
// 地址的输出不一定每次相同,但是 p1 == p3 != p2 不变
cout << p1 << endl; // 00AFFC54
cout << p2 << endl; // 00AFFC58
cout << p3 << endl; // 00AFFC54
if (p2 == p3)
cout << "p2 == p3" << endl; // p2 == p3
else
cout << "p2 != p3" << endl;
return 0;
}
// A. p1 == p2 == p3
// B. p1 < p2 < p3
// C. p1 == p3 != p2
// D. p1 != p2 != p3
// 答案为:C
首先 多继承会继承Base1 ,
再继承Base2,
再实现自己的Driver
导致 Base1 指针 指向了d对象的Base1部分
Base2 指针 指向了 d中Base2 部分
Driver 指向了d的整体部分。
所以才会导致这样 。
那么问题又来了 ,,既然 p2 != p3 那么后边的打印为什么又变成等于了??
原因:原来是编译器编译时,在遇到 == 时会进行判断,两个地址是不是指向了同一个实例对象。如果是,就会做隐式的类型转换 ,然后再判等。所以结果打了 p2 == p3 感觉这样有点不科学的现象。
如有不同见解,欢迎留言讨论!!