【摘要】
要求理解覆盖、重载、隐藏的概念与相互之间的区别;熟记类继承中对象、函数的访问控制;掌握虚函数、虚函数表、虚函数指针的联系;理解区分虚函数和虚继承在虚方法、虚指针在空间分配上的重点与难点;熟练使用多重继承,要求能区分基类的同名函数和基类的空间布局。
【正文】
类继承中的覆盖
#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 ()
{
C c(10);
cout << c.GetData() <<endl;
//C中未定义,故调用B中的,但是B中也未定义,故调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data, 故输出 1。
cout << c.A::GetData() <<endl;
//因为A中的doGetData()是虚函数,所以调用B类中的doGetData(),而B类的doGetData()返回B::m_data,故输出 1。
cout << c.B::GetData() <<endl;
//肯定是B类的返回值 1 了。
cout << c.C::GetData() <<endl;
//C类中未重定义GetData(),故调用从B继承来的GetData(),但是B类也未定义,所以调用A中的GetData(),因为A中的doGetData()是虚函数,所以调用B类的doGetData(),股输出为1
cout << c.doGetData() <<endl;
//B类的返回值 1 了。
cout << c.A::doGetData() <<endl;
//因为直接调用了A的doGetData() ,所以输出0。
cout << c.B::doGetData() <<endl;
//调用了B的doGetData(),所以输出 1。
cout << c.C::doGetData() <<endl;
//调用了B的doGetData(),所以输出 1。
return 0;
}
这里需要注意的是,第二个和第六个输出,此处单独列出并解析
cout << c.A::GetData() <<endl;
cout << c.A::doGetData() <<endl;
这里两个虽然都是调用A类中的函数,但是,一个是成员函数调用虚函数一个是直接调用虚函数;
第一个函数是成员函数调用虚函数,本类中无该虚函数的定义声明,故找到最近的基类系虚函数;
第二个函数直接调用虚函数,直接去调用类中查找,故找到调用类结果即可。
- 这里要注意存在一个就近调用,如果父类存在相关接口则优先调用父类接口,如果父类也不存在相关接口则调用祖父辈接口。
- 关于覆盖的概念:指针的数据类型是实函数的类型,指针指向的对象的数据类型,是虚函数的数据类型。
- 详见:C++ 覆盖 重载 隐藏 浅析
- 详址:http://blog.csdn.net/u013630349/article/details/46706299
类继承中的访问控制
| 公有继承(public) | 保护继承(protected) | 私有继承(private) |
派生类对基类的访问控制 | 公有及保护成员可见 | 公有及保护成员可见 | 公有及保护成员可见 |
派生类对象对基类的访问控制 | 公有成员 | 所有成员不可见 | 所有成员不可见 |
派生类中基类成员的访问控制属性 | 基类公有与保护成员访问控制属性不变 | 基类公有与保护成员作为派生类的保护成员 | 基类公有与保护成员作为派生类的私有成员 |
备注 |
| 基类成员作为派生类的(保护\私有)成员不被子类访问 |
private: 本类函数和友元函数可以访问。
protected: 本类函数、友元函数和子类函数可以访问。
public: 本类函数、友元函数、子类函数和本类的对象可以访问。
和公有继承、保护继承和私有继承没有关系,保护继承和私有继承影响的是子类的继承关系。
例:类B从类A派生,私有继承,只能说基类A的public和protected成员到了子类B后,都变为private,B再往下继承时,其子类C是不能访问A的public和protected成员的,但是对于B来说,A的protected和public是可以访问的。
虚函数继承和虚继承
虚方法(虚函数)
每个含有虚方法(虚函数)对象里有虚表指针,指向虚表。虚表里存放了虚函数的地址,虚函数表是顺序存放虚函数地址的,不需要用到链表。所以,类中的每一个对象都有一个指向顺序表的指针来存虚方法地址(顺序表和链表都是线性表),那就是虚表。
虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定后该对象应该调用哪一个虚函数,典型的情况下,这个信息具有一种被称为 vptr 虚函数指针的指针形式,vptr 指向一个被称为 vtbl 的虚函数表,函数指针数组,每一个虚函数都关联到 vtbl 。
当一个对象调用了虚函数,实际的被调用函数通过下面步骤确定:
1)找到对象的 vptr 指向的 vtbl ;
2)在 vtbl 中寻找合适的函数指针。
虚函数的缺点
虚函数最主要的缺点是执行效率较低,看一看虚拟函数引发的多态性的实现过程,你就能体会到其中的原因,另外就是由于要携带额外的信息(VPTR),所以导致类多占的内存空间也会比较大,对象也是一样的。
虚函数、虚函数表、虚函数指针的联系
每一个具有虚函数的类都有一个虚函数表VTABLE,里面按在类中声明的虚函数的顺序存放着虚函数的地址,这个虚函数表VTABLE是这个类的所有对象所共有的,也就是说无论用户声明了多少个类对象,但是,这个VTABLE虚函数表(数据结构的唯一性)只有一个(可以理解成一个类只有一种表,每个对象都复制一个表,存在一个该对象初始化的内存空间之上)。
详址:http://blog.csdn.net/u013630349/article/details/47068767
在每个具有虚函数的类的对象里面都有一个VPTR虚函数指针,这个指针指向VTABLE的首地址,每个类的对象都有这么一种指针。
每个对象的虚函数表是一样的,指的是虚函数表的数据结构,但是,每个对象各有一个虚函数表,指的是每个对象的虚函数表的内存地址不同。
虚函数的继承
1)空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
2)一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
3)类对象的大小 = 各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个) + vbptr指针(多继承下可能不止一个) + 编译器额外增加的字节;
4)当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable。
#include<iostream>
#include<memory.h>
#include<assert.h>
using namespace std;
class A
{
char k[3]; //所占的大小为3
public:
virtual void aa(){}; //虚指针大小为4
};
class B : public A
{
char j[3];
public:
virtual void bb(){};
};
class C : public virtual A
{
char i[3];
public:
virtual void cc(){};
};
int main(int argc, char *argv[])
{
cout << "sizeof(A): " << sizeof(A) << endl; //大小为4(char)+4(虚表)=8
cout << "sizeof(B): " << sizeof(B) << endl; //大小为8(A副本)+4(char)+4(虚表)=16
cout << "sizeof(C): " << sizeof(C) << endl; //大小为8(A副本)+4(char)+4(虚表)+4(虚继承+虚函数构成指针多一个)=20
return 0;
}
什么是虚继承?它和一般的继承有什么不同?有什么用
虚拟继承是多重继承中特有的概念。虚拟基类是为了解决多重继承而出现的,可以节省内存空间
请看下图:
在图 1中,类D接触自类B和类C,而类B和类C都继承自类A,因此出现了图 2所示的情况。
在图 2中,类D中会出现两次A。为了节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A成了虚拟基类。最后形成了图 3。
区分虚函数和虚继承
虚拟继承是多重继承中特有的概念,是为解决多重继承的。用虚继承可以节省内存空间
虚函数是面向对象多态性的主要方式,通过继承基类中的虚函数在子类中重载实现不同操做。继承的虚函数在子类中不需要加virtual,默认就是虚函数。可以被它的子类覆盖。
如果不是虚继承的类(普通继承),即便有虚函数也不会因此增加存储空间,如果是虚继承的类,没有虚函数就添加一个虚指针空间,有虚函数不论多少个,就添加两个虚指针空间!!!
详址:http://blog.csdn.net/u013630349/article/details/47057929
多重继承
多重继承优缺点
优点:
简单、清晰、更加有利于复用,对象可以调用多个基类中的接口
缺点:
1)二义性,例如类A派生了B和C,而B和C共同派生了D,麻烦就出现了,这种中间大两头小的继承树有个形象的名字:叫做砖石型继承树(DOD);
2)使得父类指针指向子类对象变得很麻烦,得用C++的dynamic_cast来执行强制转换,这个东西也很麻烦,因为它是运行期间而非编译期间进行转换的,它要求编译器允许RTTI;
3)多重继承还会使子类的vtable变得不同寻常,因为子类的vtable中绝对不可能包含完整的有序的两个父类的vtable,因此每个父类对象都添加了一个指针。
多重继承优缺点简版
优点:多种功能,加快任务实现。
缺点:多重性格,易得精神分裂。
多重继承的声明:
声明一个类Jetplane,它是从Rocket和Airplane继承而来的
在多继承的时候,如果一个类继承同时继承自class A和class B,而class A和B中有一个函数叫foo(),如何明确地在子类中指出覆盖的是哪个父类的foo()class JetPlane:public Rocket, public Airplane
#include<iostream> #include<memory.h> #include<assert.h> using namespace std; class A { public: void foo(){}; }; class B { public: <span style="white-space:pre"> </span>void foo(){}; }; class D:public A, public B { }; int main() { D d; d.A::foo(); return 0; }
基类和派生类的地址和布局的问题
#include <iostream> using namespace std; class A { int m_a; }; class B { int m_b; }; class C: public A , public B { int m_c; }; int main(int argc, char* argv[]) { C *pc=new C; B *pb=dynamic_cast<B*>(pc); A *pa=dynamic_cast<A*>(pc); if (pc==pb) { cout<<"equal"<<endl; } else { cout<<"unequal"<<endl; } if ((int)pc==(int)pb) { cout<<"equal"<<endl; } else { cout<<"unequal"<<endl; } delete pc; return 0; }
实验结果:第一个相同是因为父类指针指向子类对象的时候,采用多重继承之后用dynamic_cast,导致相等输出equal,第二指针pc和pb值是不同的,所以,转换为int型也是不同的。输出unequal。???(尚未理解!)