C++-继承-菱形继承-菱形虚拟继承-虚函数表
Posted 天津 唐秙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++-继承-菱形继承-菱形虚拟继承-虚函数表相关的知识,希望对你有一定的参考价值。
文章目录
1. 继承的概念和定义
1.1 继承的概念
继承机制是对象对象程序设计使代码可以复用的重要手段,允许程序员在保持原有类特性的基础上进行扩展,产生新类,称为派生类,以前的学的都是函数复用,继承是类设计层次的复用。
1.2 继承的定义
Base类是父类,也称为基类,D类称为子类,也称为派生类。继承方式有public继承,protected继承,private继承。
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected | 派生类的private成员 |
基类的protected成员 | 派生类的protected | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
1.基类的private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见指的是基类的私有成员被继承到了派生类对象当中,但是无论是在派生类的类内还是类外都是不能去访问的。
2.基类的成员不想在类外被直接访问,但需要在派生类当中能访问,就定义为protected。保护成员限定符就是因为继承才出现的。
3.public>protected>private
4.使用关键字class时默认的继承方式时private,使用struct时默认的继承方式是public,不过最好显示写出继承方式。
5.实际运用中一般都是使用public继承,很少也不提倡用protected/private继承。
1.3 代码
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
~Base()
{
cout << "Base::~Base()" << endl;
}
};
class D : public Base
{
public:
D()
{
cout << "D::D()" << endl;
}
~D()
{
cout << "D::~D()" << endl;
}
};
int main()
{
D d;
return 0;
}
2. 基类和派生类对象的赋值转换(赋值兼容原则)
1.派生类对象可以赋值给基类的对象/基类的指针/基类的引用(切片或者切割)。
2.基类对象不能赋值给派生类对象
3.基类的指针可以通过强制类型转换赋值给派生类的指针,但必须是基类的指针指向派生类对象的时候才是安全的。如果基类是多态类型,可以使用RTT的dynamic cast来进行识别后进行安全转换。
代码
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
~Base()
{
cout << "Base::~Base()" << endl;
}
private:
int a;
};
class D : public Base
{
public:
D()
{
cout << "D::D()" << endl;
}
~D()
{
cout << "D::~D()" << endl;
}
private:
int b;
};
int main()
{
D d;
Base* b = &d;
return 0;
}
3. 继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域。
2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏,也叫重定义,在子类成员函数中,可以使用基类::基类成员显示访问。
3.成员函数的隐藏,只需要函数名相同。
4.在实际中在继承体系里最好不要定义同名的成员。
代码
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
~Base()
{
cout << "Base::~Base()" << endl;
}
void fun(int, int)
{}
private:
int a;
};
class D : public Base
{
public:
D()
{
cout << "D::D()" << endl;
}
~D()
{
cout << "D::~D()" << endl;
}
void fun(int)
{}
private:
int b;
};
int main()
{
D d;
Base b;
d.fun(3);
b.fun(2, 3);
//d.fun(2, 3); //被隐藏了
d.Base::fun(2, 3);
return 0;
}
4. 派生类的默认成员函数
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的析构函数会在被调用完成之后自动调用基类的析构函数清理基类的成员,这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
4.派生类的operator=必须要调用基类的operator=完成基类的复制
5.派生类对象初始化先调用基类构造再调用派生类构造。
6.派生类对象析构清理先调用派生类析构再调用基类的析构。
5. 继承与友元
1.友元关系不能继承,基类友元不能访问子类私有和保护成员。
6. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论多少个子类,都只有一个static成员实例。
7. 菱形继承及菱形虚拟继承
1.单继承:一个子类只有一个直接父亲时称这个继承关系为单继承。
2.多继承:一个子类有两个或者以上直接父类时称这个继承关系为多继承。
3.菱形继承:菱形继承是多继承的一种特殊情况。
4.菱形继承的问题:从对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。
5.虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如果在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。
代码
class A
{
public:
A()
{
cout << "A::A()" << endl;
}
~A()
{
cout << "~A::A()" << endl;
}
void fun(int)
{
cout << "A::fun()" << endl;
}
private:
int a = 1;
};
class B : public A
{
public:
B()
{
cout << "B::B()" << endl;
}
~B()
{
cout << "~B::B()" << endl;
}
virtual void fun(double)
{
cout << "B::fun()" << endl;
}
private:
int b = 2;
};
class C : public A
{
public:
C()
{
cout << "C::C()" << endl;
}
~C()
{
cout << "C::C()" << endl;
}
virtual void fun(double)
{
cout << "C::fun()" << endl;
}
private:
int c = 3;
};
class D : public B, public C
{
public:
D()
{
cout << "D::D()" << endl;
}
~D()
{
cout << "D::~D()" << endl;
}
void fun(double)
{
cout << "D::fun()" << endl;
}
private:
int d = 4;
};
int main()
{
D d;
return 0;
}
这里时通过了B和C两个指针,指向了一张表,这两个指针叫做虚基表指针,这两个表叫做虚基表,虚基表中存的偏移量,通过偏移量可以找到下面的A。
8. 继承的总结和反思
1.有了多继承,就存在菱形继承,有了菱形继承就有了菱形虚拟继承,底层实现上就很复杂,因此一般不建议设计出菱形继承,否则在复杂度及性能上会有问题。
2.继承和组合
- public继承是一种is-a的关系,每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系,假设B组合了A,每个对象中都有一个A对象。
- 优先使用对象组合,而不是类继承。
- 继承允许根据基类的实现来定义派生类的实现,通过生成派生类的复用通常称为白箱复用,术语“白箱”是相对可视化而言的,在继承方式中,基类的内部细节对子类可见,继承一定程度破坏了基类的封装,基类的改变,对派生类有很大影响,派生类和基类间的依赖关系很强,耦合度高。(目标:高内聚,低耦合)
- 对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或者组合对象来获得,对象组合要求被组合的对象具有良好定义的接口,这种复用风格被称为黑箱复用,对象的内部细节是不可见的,组合类之间没有很强的依赖关系,耦合度低,优先使用对象组合有助于保持每个类被封装。
9. 虚函数表
对于了解C++的人来说,应该都知道虚函数是通过一张虚函数表实现的,简称V-Table。在这个白哦中,主要是一个类的虚函数的地址表,这个表解决了继承和覆盖的问题,当我们用父类指针操作子类的时候,可以通过这张虚函数表指明了实际所应该调用的函数。
在C++的标准规格说明书中说到,编译器必须要保证虚函数表的指针存在于对象实例中最前面的位置,为了获取正确的虚函数偏移量,通过变量里面的函数指针,就可以调用相应的函数。
当存在多重继承有虚函数覆盖的时候,类实例中的虚函数表的图。
10. 面试题
1.什么是菱形继承?菱形继承的问题是什么?
2.什么是菱形虚拟继承?如果解决数据冗余和二义性?
3.继承和组合的区别?什么时候用继承?什么时候用组合?
以上是关于C++-继承-菱形继承-菱形虚拟继承-虚函数表的主要内容,如果未能解决你的问题,请参考以下文章