您的位置:首页 > 编程语言 > C语言/C++

C++对象模型和虚函数表分析以及重载、重写、隐藏的区别

2016-12-22 20:16 561 查看
C++在布局以及存取时间上主要的额外负担是由virtual引起的。包括:

Virtual function 机制 。用以支持一个有效率的“执行期绑定”(runtime binding)。
Virtual base class。用以实现“多次出现在继承体系中的base class,有一个单一而共享的实体”。

此外,还有一些多重继承下的额外负担。发生在“一个derived class和其第二或后继之base class的转换”之间。

C++对象模式:

在C++中,有两种class data members : static 和 nonstatic

有三种class member functions: static 、nonstatic 和 virtual

C++模型的演变的过程如下:

简单对象模型(Simple Object Model)




该模型是最简单的模型,只是为了降低设计复杂度,不具备空间和执行器的效率。在这个模型中,每个object是一系列的slots(槽),它的function member和data member依次对应在slot中。

不过,members本身不在object中,在slot中的是它们的pointer,这样可以避免members有不同的类型,因此需要不同的存储空间所导致的问题。这个模型object的大小很容易计算,指针大小,乘以class中声明的members数目便是。

表格驱动对象模型(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 处。

从以上信息,我的认识时,一个 class 如果有 virtual function,那么它就会把自己的 virtual function 信息”上交“给 virtual table(vtbl)。如果其他 class 比如 class A 继承它,或者拷贝构造它,那么这个 virtual table 就会被share,A 就会得到该 virtual table 的 vptr,并且如果 A 自己有virtual function,也会产生新的 virtual function 指针放入 vptr 所指的virtual
table 之中。然后这个 virtual table 会放在class A 的地址的首部。



这个模型的主要优点在于它的空间和存取时间的效率;主要缺点则是,如果应用程序代码未改变,但所用到的 class objects 的 noostatic data members 有所修改(可能增加、移除或更改),那么这些应用程序代码将重新编译。关于这一点,前面的双表模型就提供了较大的弹性,因为它多提供了一层间接性,不过也因此付出空间和执行效率方面的代价。

C++支持单一继承,多继承,以及虚拟继承。在虚拟继承的情况下,base class 不管在继承串链中被派生(derived)多少次,永远只会存在一个实体(称为subject)。

对象的差异(An Object Distinction)

C++程序设计直接支持三种 programming paradigms(程序设计典范)

程序模型(procedural model)、抽象数据类型模型(ADT),面向对象模型(object-oriented model)

使用一种paradigms编程有利于整体行为的良好稳固,如果混合可能带来不好的结果。比如实现多态却发生了sliced(切片)问题,比如:
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 来存取。
(void *是没有语言支持的多态,因为操作对象不是 class object)。C++中,多态只存在与一个个的 public class 体系中。

C++以下列方法支持多态

1.经由一组隐含的转化操作。例如把一个 derived class 指针转化为一个指向其 public base type 的指针:

shape *ps = new circle();

2.经由 virtual function 机制:

ps->rotate();

3.经由 dynamic_cast 和 typeid 运算符:

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)

现有一个父类 ZonAnimal ,派生类 Bear,并且
Bear b;
ZooAnimal *pz = &b;
Bear *pb = &b;


如果 cell_block() 是 Bear 的member function,但不是 ZooAimal 的 member,直接使用 pz->cell_block 不合法。有两种方案使用 pz 去访问该 member function:

( ( 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 所在位置写上自己的新函数的地址。

上述程序运行结果:



打印出 base class 和 derived class 的 vtbl 首地址不同,并且访问结果是相同的。证明了它们没有共用 virtual table,但是它们的 virtual table 中 virtual function 没有 override 的情况下,是指向同一个函数地址的。

另外关于重写(override,也叫覆盖)、重载(overload)和重定义(redefine,也叫隐藏)的区别:

1.成员函数重载特征

相同的范围(在同一个类中)
函数名字相同
参数不同
virtual关键字可有可无

2 重写(覆盖)是指派生类函数覆盖基类函数,特征是:

不同的范围,分别位于基类和派生类中
函数的名字相同
参数相同
基类函数必须有virtual关键字

3 重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。

有关 virtual table 的更多可以参考陈皓的博客:C++ 虚函数表解析,以及这个博客对于父类和子类是否共用 virtual table 的分析:C++类对象内存结构
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息