C++在布局以及存取时间上主要的额外负担是由virtual引起的。包括:
- Virtual function 机制 。用以支持一个有效率的“执行期绑定”(runtime binding)。
- Virtual base class。用以实现“多次出现在继承体系中的base class,有一个单一而共享的实体”。
C++对象模式:
在C++中,有两种class data members : static 和 nonstatic
有三种class member functions: static 、nonstatic 和 virtual
C++模型的演变的过程如下:
简单对象模型(Simple Object Model)
表格驱动对象模型(Member Table Object Model)
该模型将所有的与members相关的信息抽出来,放在一个 data member table 和一个 member function table 之中,class object 本身则含有指向这两个表格的指针。Member function table 是一系列的 slots,每一个 slot 指出一个 member function;Data member table 则直接含有 data 本身。
所以这里相当于 class 指向了一个 Funciton Member Table,Function Member Table 中的每个 slot 又指向了函数地址,多了一层访问的间接性。
C++对象模型(The C++ Object Model)
此模型中,Nostatic data members 被配置于每一个 class object 之内,static data members 也被放在所有的 class object 之外, Virtual function 则以两个步骤支持之。
- 每一个 class 产生出一堆指向 virtual functions 的指针,放在表格之中,这个表格被称为 virtual table(vtbl)。
- 每一个 class object 被添加了一个指针,指向相关的 virtual table 。通常这个指针被称为 vptr,vptr 的设定(setting)和重置(resetting)都由每一个 class 的constructor、destructor 和 copy assignment 运算符自动完成,每一个 class 所关联的 type_info object (用以支持 runtime type identigication,RTTI)也经由 virtual table 被指出来,通常是放在表格的第一个 slot 处。
对象的差异(An Object Distinction)
程序模型(procedural model)、抽象数据类型模型(ADT),面向对象模型(object-oriented model)
class base {
public:
base() = default;
virtual void foo() { std::cout<<"base"<<std::endl; }
virtual ~base() {}
private:
int int_;
};
class derived : public base {
public:
derived() = default;
~derived() {}
void foo() { std::cout<<"derived"<<std::endl; }
private:
int int_;
};
//void do_polymorphic(base ptr) //输出:base
void do_polymorphic(base& ptr) //引用或指针将输出:derived
{
ptr.foo();
}
int main()
{
derived dr;
do_polymorphic(dr);
return 0;
}
上述程序中,使用父类类型接收子类,将会发生切片,会调用父类自己的 foo() 函数。对于 object 的多态操作,object 必须可以经由一个 pointer 或 referrnce 来存取。
1.经由一组隐含的转化操作。例如把一个 derived class 指针转化为一个指向其 public base type 的指针:
shape *ps = new circle();
2.经由 virtual function 机制:
3.经由 dynamic_cast 和 typeid 运算符:ps->rotate();
if ( circle * pc = dynamic_cast< circle* > ( ps ) ) ...
多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象 base class 中。可以在执行期根据 object 的真正类型解析出到底是哪一个函数实体被调用。
需要多少内存才能够表现一个 class object ? 一般而言要有:
- 其 nonstatic data members 的总和大小
- 加上任何由于 alignment(alignment 就是将数值调整到某数的倍数。在 32 位计算机上,通常 alignment 为 4 bytes(32位),以使 bus 的“运输量”达到最高效率)的需求而填补(padding)上去的空间(可能存在于 members 之间,也可能存在于集合体边界)
- 加上为了支持 virtual 而由内部产生的任何额外负担(overhead)
Bear b;
ZooAnimal *pz = &b;
Bear *pb = &b;
- ( ( Bear *) pz )->cell_block // ok,经过一个明白的 down_cast就没问题
- if ( Bear* pb2 = dynamic_cast< Bear* >( pz ) ) pz->cell_block; //这样更好,但它是一个 run-time operation,成本较高
#include <iostream>
#include <stdio.h>
class base {
public:
base(int i) : int_base_(i) {}
virtual void foo() { std::cout<<"base"<<std::endl; }
virtual ~base() = default;
protected:
int int_base_; //data member also can be overrided
};
class derived : public base {
public:
derived(int i) : int_(i), base(4) {
//this->int_base_ = 4;
//std::cout<<this->int_base_<<std::endl;
}
~derived() = default;
void foo() { std::cout<<"derived"<<std::endl; }
private:
int int_;
};
void do_polymorphic(base* ptr)
{
ptr->foo();
}
int main()
{
derived dr(5);
base b(1);
void *ptr = static_cast<void *>(&dr);
( (void(*)()) (*(int *)(*((int *)ptr))) )(); //调用derived的虚函数表中对应的第一个虚函数
//typedef void(*fun)(); //这种方式是上面的简化版
//fun f = fun( (*(int *)(*((int *)ptr))) );
//f();
printf("%x\n", *(int *)ptr);
void *base_ptr = static_cast<void *>(&b);
printf("%x\n", *(int *)base_ptr);
( (void(*)()) (*(int *)(*((int *)base_ptr))) )();
return 0;
}
注意点有以下几点:
- ( void (*)() )X,左边void (*)()类似于上面的fun,用来强制转化X为函数指针类型,然后代码中右侧再加 (),构成函数调用 ;而 ( void )(* X)(),代表X的返回值为 void() 类型,此处X为表示所用符号,不代表X就是一个整型变量。
- 注意 override 重写(重写=覆盖),如果子类重写了父类的虚函数 fun(),重写就是所有参数都一样,那么子类调用将调用自己的 fun() 函数,如果没有,会调用父类的 fun() 函数。
- 子类在构造函数时,如果父类构造函数不是 default,应该在成员初始化列表中对父类进行显示初始化。当然使用this->的方式也可以,不过不推荐。
- 父类和子类的虚函数表不是共用的,子类继承时会首先拷贝一份父类的 virtual table,如果子类对某个 virtual function 进行了 override,那么子类会在自己的 virtable 中该 virtual function 所在位置写上自己的新函数的地址。
- 相同的范围(在同一个类中)
- 函数名字相同
- 参数不同
- virtual关键字可有可无
- 不同的范围,分别位于基类和派生类中
- 函数的名字相同
- 参数相同
- 基类函数必须有virtual关键字
- 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。