欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Boolan C++面向对象高级编程(下)第五周笔记

程序员文章站 2024-03-21 23:44:40
...

因个人水平有限,希望各路大神发现错误和不足之处时,能不吝指教。

虚指针、虚表、动态绑定和静态绑定的定义参考What & How & Why同学的笔记(点击打开链接


1. 虚指针和虚表(虚函数调用机制)


通过上周的作业来记录对虚指针和虚函数表的一些理解。

class Fruit{test
   int no;
   double weight;
   char key;
public:
   void print() {   }
   virtual void process(){   }
};
   
class Apple: public Fruit{
   int size;
   char type;
public:
   void save() {   }
   virtual void process(){   }
};
测试代码:http://rextester.com/YLUNC24589

当一个类中有虚函数的声明时候,该类的对象里就会多一个指针,这个指针称为虚指针Virtual Pointer;同时,类中还会多出一张表用于保存该类中所有虚函数定义所在的入口地址。这个表被称作为虚函数表Virtual Table,以下简称虚表)。虚指针是以对象为单位,指向对应类的虚表的入口;而虚表是以类为单位;该类的所有对象共享一张虚表;也就是说,只要是通过这个类实例化的对象,他们包含的虚函数的入口地址,都存在这张表里。 如下面的对象模型示意图所示,Fruit类和Apple类中存在虚函数,因此对象里多了一个虚指针,分别指向各自类的虚表。只要父类中有虚函数,子类一定会继承父类虚函数的调用权,一定会有虚指针。

Boolan C++面向对象高级编程(下)第五周笔记


既然知道了为什么会存在虚指针和虚表,那么现在有了新的问题:虚指针在对象内存中的存储位置,以及它和虚表,虚函数的关系是怎样的?

首先来解决虚指针在对象中位置的问题,根据前面的测试代码我们可以得到Fruit类和Apple类对象的内存布局,如下图所示。

Boolan C++面向对象高级编程(下)第五周笔记

根据以上结果,我们可以看到虚指针(vptr)位于对象内存布局的第一个位置,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。虚指针的大小和编译器有关,32位机器是4个字节大小,64位机器是8个字节。

Boolan C++面向对象高级编程(下)第五周笔记

现在问题变成了虚指针,虚表和虚函数三者之间的关系。我们还是结合对象模型图来解释三者之间的关系,

Boolan C++面向对象高级编程(下)第五周笔记

由上图可知,类对象内存的第一个位置存放的是一个地址,也就是虚指针,这个地址指向了对应的虚表,虚表中存储的是一系列虚函数的地址z,因此在调用虚函数的时候,我们可以采取通过虚指针查询虚表,来获取虚函数的地址,从而实现虚函数的调用。

虚表中存储的虚函数地址如下图所示,作业中只有一个虚函数,若有多个,虚函数出现的顺序与类中虚函数声明顺序一致。

Boolan C++面向对象高级编程(下)第五周笔记


2.this指针和动态联编


我们可以从1中了解到虚函数调用的机制,虚函数存在的意义是为了实现多态性,也就是动态联编。虚函数,多态性,动态联编实际上讲的是同一个东西。

在探讨动态绑定之前,先了解下静态绑定和动态绑定的定义。

一个源程序需要经过编译、连接才能形成可执行文件,在这个过程中要把调用函数的名称和对应函数在内存中的区域关联在一起,这个过程就是绑定Binding),又称联编

绑定分为静态绑定动态绑定。这两者的根本目的都是在为被调用函数寻找其所处内存的位置,只是寻址的方式有所不同。 
静态绑定又称静态联编,是指在编译程序时候,被调用函数的信息中就包含了函数在内存中所处的位置。
动态绑定又称动态联编,也就是虚函数绑定其函数在内存中的地址的过程。跟静态绑定不同的是,虚函数无法在编译的时候确定我们需要具体调用哪个虚函数;因为虚函数的调用存在于对象里,而对象必须要在 run-time 中才能建立。因此,我们不能在编译的时候直接得到对应虚函数的地址,而是在程序运行中通过对象的创建,才能通过虚指针去找到对应的虚函数。对于虚函数的绑定来说,这个过程是动态的,因此称为动态绑定。 

动态绑定需要满足三个条件:

(1)调用函数的是一个指针;

(2)子类对象向上转型;

(3)调用的函数为虚函数;

举个例子:

Fruit* p = new Apple;//Apple的对象向上转型为Fruit类对象;
p->process();//动态绑定,调用Apple::process();

实际过程相当于(*(p->vptr)[n])(p)或者(* p->vptr[n])(p);

以上过程可以这么理解:

(1)建立一个指向子类Apple对象的指针p,向上转型为Fruit类;

(2)通过p->vptr找到Apple类的虚表;

(3)通过(p->vptr)[n]找到存放于虚表中的目标虚函数;

(4)通过(*(p->vptr)[n])(p)调用存放于虚表中的虚函数,同时将p传入虚函数中,完成Apple类指针p对Apple类虚函数的调用;

这里的p指针其实就是this指针,类中的每个成员函数都隐藏着一个this指针。

那么这么做有什么好处吗?举个课程中的实际例子:

Boolan C++面向对象高级编程(下)第五周笔记

子类对象myDoc可以调用父类CDocument的OnFileOpen函数中的其他所有函数,但是其中的虚函数Seriallize子类对它进行了重写,需要调用自身定义的Seriallize函数,这个时候动态绑定就发挥作用了,上述情况满足动态绑定的三个条件,当myDoc调用到Seriallize函数时,会调用子类CMyDoc重写的Seriallize函数,具体流程如前面所讲的那样。通过动态绑定大大简化了代码量,不需要像C语言那样分情况讨论,这也是C++多态性的魅力所在。


3.const


const这块的内容在前面的课程中已经提到过,这里不再另外记录,附上课程PPT,应该可以基本囊括这次的内容。

Boolan C++面向对象高级编程(下)第五周笔记

4.new和delete


首先回顾之前课程提到的new和delete动作的分解

Boolan C++面向对象高级编程(下)第五周笔记

Boolan C++面向对象高级编程(下)第五周笔记


4.1重载全局operator new/delete、new[]/delete[]

void* myAlloc(size_t size){return malloc(size);}
void myFree(void* ptr){return free(ptr); }
.....
//它们不可以被声明于一个namespace内
inline void* operator new (size_t size){
cout << "global new() \n"; 
return myAlloc(size);
}
inline void* operator new[](size_t size){
cout << "operator new[]\n";
return myAlloc(size);
}
inline void operator delete(void* ptr){
cout << "global delete()\n";
myFree(ptr);
}
inline void operator delete[](void* ptr){
cout << "global delete[]\n";
myFree(ptr);
}

4.2重载member operator new/delete、new[]/delete[]

Boolan C++面向对象高级编程(下)第五周笔记

Boolan C++面向对象高级编程(下)第五周笔记

4.3重载new()和delete()

我们可以重载class member operator new(), 写出多个版本,前提是每一个版本声明都必须有独立的参数列,其中第一参数必须是size_t,(其实就是unsign int),其余参数以new所指定的placement arguments为初值。出现new(....)小括号便是所谓的placement arguments。

我们也可以重载class member operator delete(),写出多个版本。但他们绝对不会被delete调用。只当new锁掉用的ctor抛出异常,才会调用这些重载版本的operator delete()。他们可能这样被调用,主要用来归还未完成创建功能的object所占用的内存。