看这一篇文章之前强烈建议先看以下我之前公布的
虚指针,虚函数分析
例1: 以下代码输入什么?
#include <iostream>using namespace std;class A {protected: int m_data;public: A(int data = 0) {m_data=data;} int GetData() { return doGetData(); } virtual int doGetData() { return m_data; }};class B : public A{protected: int m_data;public: B(int data = 1) { m_data = data; } int doGetData() { return m_data; }};class C: public B{protected: int m_data;public: C(int data=2) { m_data = data; }};int main(int argc, char const *argv[]){ C c(10); cout << c.GetData() << endl; cout << c.A::GetData() << endl; cout << c.B::GetData() << endl; cout << c.C::GetData() << endl; cout << c.doGetData() << endl; cout << c.A::doGetData() << endl; cout << c.B::doGetData() << endl; cout << c.C::doGetData() << endl; return 0;}
构造函数从最初始的基类开始结构,各个类的同名变量没有造成笼罩,都是独自的变量。
了解这两个重要的C++个性后解决这个问题就比拟轻松了。 上面咱们详解这几条输入语句。
cout << c.GetData() << endl;
原本是要调用C类的 GetData(), C中未定义, 故调用 B 中的, 然而 B 中也未定义, 故调用 A 中的 GetData(), 因为 A 中的 doGetData()是虚函数,所以调用 B 类中的 doGetData(),而 B 类的 doGetData() 返回 B::m_data, 故输入1。
cout << c.A::GetData() << endl;
因为 A 中的 doGetData() 是虚函数,又因为 C 类中未重定义该接口,所以调用 B 类中的 doGetData(), 而 B 类的 doGetData() 返回 B::m_data, 故输入 l 。
cout << c.B::GetData() << endl;
C调用哪一个GetData() 实质上都是调用的A::GetData(), 调用到 doGetData() 虚函数,再调用父类B笼罩后的虚函数,返回B::m_data, 所以前5个都是1
cout << c.A::doGetData() << endl;
显示调用A::doGetData(), 返回 A::m_data, 是0
cout << c.B::doGetData() << endl;
, cout << c.C::doGetData() << endl;
都将调用B::doGetData(), 返回B::m_data, 是1
所以后果为: 1 1 1 1 1 0 1 1
不便排版,请疏忽掉换行。
最初附上内存结构图:
例2: 为什么虚函数效率低?
因为虚函数须要一次间接的寻址,而一般的函数能够在编译时定位到函数的地址,虚函数是要依据虚指针定位到函数的地址。多减少了一个过程,效率必定低一些,但带来了运行时的多态。
C++反对多重继承,从而大大加强了面向对象程序设计的能力。多重继承是一个类从多个基类派生而来的能力,派生类实际上获取了所有基类的个性。当一个类是两个或多个基类的派生类时,必须在派生类名和冒号之后,列出所有基类的类名,基类间用逗号隔开。 派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们。派生类能够是另一个类的基类,这样,相当于造成了一个继承链。当派生类的构造函数被激活时,它的所有基类的构造函数也都会被激活。
在面向对象的程序设计中,继承和多重继承个别指公共继承。 在无继承的类中,protected 和 private 控制符是没有差异的,在继承中,基类的 private 对所有的外界都屏蔽(包含本人的派生类), 基类的 protected 控制符对应用程序是屏蔽的, 但对其派生类是可拜访的。
虚继承
什么是虚继承?它与个别的继承有什么不同?它有什么用?
虚构继承是多重继承中特有的概念。 虚构基类是为解决多重继承而呈现的。 请看下图:
类D继承自类B和类C, 而类B和类C都继承自类A.
在类D中会两次呈现A。为了节俭内存空间,能够将B、C对A 的继承定义为虚构继承,而A就成了虚构基类。 最初造成如下图所示的状况:
代码如下:class A; class B : public virtual A;class C : public virtual A;class D : public B, public C;
留神: 虚函数继承和虚继承是齐全不同的两个概念.
多重继承
例3: 请评估多重继承的长处和缺点。
多重继承在语言上并没有什么很重大的问题,然而规范自身只对语义做了规定,而对编译器的细节没有做规定。所以在应用时(即便是继承),最好不要对内存布局等有什么假如。为了防止由此带来的复杂性,通常举荐应用复合。
- 多重继承自身并没有问题,不过大多数零碎的类档次往往有一个公共的基类,而这样的构造如果应用多重继承,稍有不慎,将会呈现一个重大景象————菱形继承,这样的继承形式会使得类的拜访构造非常复杂。 但并非不可解决,能够用virtual继承(并非惟一的办法)
- 从哲学上来说,C++多重继承必须要存在,这个世界原本就不是单根的。从理论用处上来说,多重继承不是必须的。
- 多重继承在面向对象实践中并非是必要的————因为它不提供新的语义,能够通过单继承与复合构造来取代。 而Java则放弃了多重继承,应用简略的interface取代。 因为C++中没有interface这个关键字,所以不存在所谓的“接口”技术。然而C++能够很轻松地做到这样的模仿,因为C++中的不定义属性的抽象类就是接口。
- 多重继承自身并不简单,对象布局也不凌乱,语言中都有明确的定义。真正简单的是应用了运行时多态(virtual)的多重继承(因为语言对于多态的实现没有明确的定义)。
- 要理解C++,就要明确有很多概念是C++ 试图思考然而最终放弃的设计。你会发现很多Java、C#中的货色都是C++思考后放弃的。
不是说这些货色不好,而是在C++中它将毁坏C++作为一个整体的谐和性,或者C++ 并不需要这样的货色。
举个例子来阐明,C#中有一个关键字base用来示意该类的父类,C++却没有对应的关键字。为什么没有?其实C++中已经有人提议用一个相似的关键字 inherited, 来示意被继承的类,即父类。 这样一个好的倡议为什么没有被驳回呢?因为这样的关键字既不必须又不充沛。 不必须是因为 C++有一个 typedef* inherited
,不充沛是因为有多个基类,你不可能晓得 inherited 指的是哪个基类。
例4: 在多继承的时候,如果一个类继承同时继承自 class A 和 class B, 而 class A 和 B 中都有一个函数叫 foo(), 如何明确地在子类中指出调用是哪个父类的 foo()?
class A{public: void foo() { cout << "A foo" << endl; }};class B{public: void foo() { cout << "B foo" << endl; }};class C : public A, public B{};int main(int argc, char const* argv[]){ C c; c.A::foo(); return 0;}
C 继承自 A 和 B, 如果呈现了雷同的函数foo(), 那么C.A::foo(), C.B::foo() 就别离代表从 A 类中继承的 foo 函数和从 B 类中继承的 foo 函数。
例5: 以下代码输入什么?
class A{ int m_nA;};class B{ int m_nB;};class C : public A, public B{ int m_nC;};int main(int argc, char const* argv[]){ C* pC = new C; B* pB = dynamic_cast<B*>(pC); A* pA = dynamic_cast<A*>(pC); cout << (pC == pB) << endl; cout << (pC == pA) << endl; cout << ((int)pC == (int)pB) << endl; cout << ((int)pC == (int)pA) << endl; return 0;}
当进行pC=pB比拟时,实际上是比拟pC指向的对象和隐式转换pB后pB 指向的对象 (pC指向的对象)的局部,这个是同一部分,是相等的。
然而,pB实际上指向的地址是对象C中的父类B局部,从地址上跟pC不一样,所以间接比拟地址数值的时候是不相等的。
内存结构图如下:
例6: 如果鸟是能够飞的,那么驼鸟是鸟么?驼鸟如何继承鸟类?
鸟是能够飞的。 也就是说,当鸟航行时,它的高度是大于0的。 驼鸟是鸟类(生物学上)的一种, 但它的飞行高度为0(驼鸟不能飞)。
不要把可替代性和子集相混同。 即便驼鸟集是鸟集的一个子集(每个驼鸟集都在鸟集内),但并不意味着鸵鸟的行为可能代替鸟的行为。 可替代性与行为无关,与子集没有关系。 当评估一个潜在的继承关系时,重要的因素是可代替的行为,而不是子集。
如果肯定要让驼鸟来继承鸟类, 能够采取组合的方法, 把鸟类中的能够被驼鸟继承的函数筛选进去,这样驼鸟就不是"a kind of"鸟了,而是"has some kind of"鸟的属性而已。
class bird{public: void eat(); void sleep(); void fly();};class ostrich{public: void eat(); void sleep();};
例6: C++中如何阻止一个类被实例化?
应用抽象类,或者构造函数被申明成private。
最初补充两个知识点:
函数的暗藏和笼罩
- 函数的暗藏: 没有定义多态的状况下,即没有加virtual的前提下,如果定义了父类和子类,父类和子类呈现了同名的函数,就称子类的函数把同名的父类的函数给暗藏了。
- 函数的笼罩:是针对多态来说的。 如果定义了父类和子类,父类中定义了公共的虚函数,如果此时子类中没有定义同名的虚函数,那么在子类的虚函数表中将会写上父类的该虚函数的函数入口地址,如果在子类中定义了同名虚函数的话,那么在子类的虚函数表中将会把原来的父类的虚函数地址笼罩掉,笼罩成子类的虚函数的函数地址。
总结: 本文的重点还是承接之前“虚指针,虚表分析”的内容,对于多重继承,没有探索其内存构造,并且也不是很好弄清楚,其性能大多数能够被组合(composition)的形式实现,C++规范没有给出编译器具体的多继承的实现细节,不同的编译器有不同的做法。