(GeekBand)Fourth class

一、虚表、虚指针、动态绑定

这一部分,我们介绍下 继承体系下,类和对象的存储形式。

1.1 vptr 虚指针 和 vtable 虚表

对于虚指针和虚表的定义,这里引用一段quora上的一个回复(这里我已经翻译成中文): 如果一个类存在一个或多个虚函数,编译器会为这个类的实例 (对象) 创建一个隐藏的成员变量,即虚指针(virtual-pointer),简称 vptr。 vptr 指向一个包含一组函数指针的表,我们称之为 虚表 (virtual table),简称 vtable。虚表由编译器创建,虚表中的每一项均是 一个指向对应虚函数的指针。

为了实现动态绑定 (dynamic binding),编译器为每一个拥有虚函数的类 (和它的子类) 创建一张虚表。编译器将虚函数的地址存放到对应 类的虚表中。 当通过基类指针 (或父类指针,Base * pb) 调用虚函数时,编译器插入一段在虚表中查找虚函数地址和获取 vptr 的代码。 所以才能够调用到”正确”的函数,实现动态绑定。

关于 vptr 和 vtable 的调用,这里用侯捷老师 PPT 上的一张图表示:

关于 类 A、B、C 的结构声明参考下面的代码 (注意这里不包含具体实现):

// 上图中 类 A、B、C 的声明classA{public:virtualvoidvfunc1();virtualvoidvfunc2();voidfunc1();voidfunc2();private:intm_data1,m_data2;}classB:publicA{public:virtualvoidvfunc1();voidfunc2();private:intm_data3;}classC:publicB{public:virtualvoidvfunc1();voidfunc2();private:intm_data1,m_data4;}

1.2 this pointer (template method)

在继承体系中,子类对象调用一个方法时,如果该类本身这个函数,那么会调用这个函数;如果本身没有,那么编译器会沿着继承树向上查找父类中是否有该方法。

侯捷老师PPT中的一张图很好地体现了这种调用机制:

1.3 dynamic binding 动态绑定

1.3.1 什么是动态绑定?

动态绑定是编程语言的一种特性(或机制),它允许程序在运行时决定执行操作的细节,而不是在编译时就确定。在设计一个软件时,通常会出现下面两类情况:

类的接口已经确定,但是还不知道具体怎么实现

开发者知道需要什么算法,但是不知道具体的操作

这两种情况下,开发者都需要延迟决定,延迟到什么时候呢?延迟到已经有足够的信息去做一个正确的决策。此时如果能不修改原先的实现,我们的目标就达到了。

动态绑定正是为了满足这些需求而存在,结果就是更灵活和可伸缩的软件架构。比如在软件开发初期,不需要做出所有设计决策。这里我们讨论下灵活性和可伸缩性:

flexibility (灵活性): 很容易将现存组件和新的配置合并到一起

extensibility (扩展性): 很容易添加新组件

C++ 通过 虚表和虚指针机制 实现对动态绑定的支持,具体的机制我们在上面已经谈到,这里不再赘述。

1.3.2 动态绑定在 C++ 中的体现

在 C++ 中,动态绑定的标志是在声明类方法时,在方法名前面显式地添加 virtual 关键字。比如下面这样:

classBase{public:virtualvoidvfunc1(){std::cout<<"Base::vfunc1()"<

只有类的成员函数才能被声明为虚函数,下面三种是不可以的:

普通的函数 (不属于任何一个类)

类的成员变量

静态方法 (static 修饰的成员函数)

virtual 修饰的成员函数的接口是固定的,但是子类中的同名成员函数可以修改默认实现,比如像下面这样:

classDerived_1{public:virtualvoidvfunc1(){std::cout<<"Derived_1::vfunc1() "<

注意:上面的代码中, virtual 是可选的,即便不写,它仍然是虚函数!

在程序运行时,虚函数调度机制会根据对象的”动态类型”选择对应的成员函数。 被选择的成员函数依赖于被指针指向的对象,而不是指针的类型。看下面代码:

voidfoo(Base*bp){bp->vf1();/* virtual */}Baseb;Base*bp=&b;bp->vf1();// 打印 "Base::vfunc1()"Derived_1d;bp=&d;bp->vf1();// 打印 "Derived_1::vfunc1()"foo(&b);// 打印 "Base::vfunc1()"foo(&d);// 打印 "Derived_1::vfunc1()",这里存在一个隐式的向上转型

关于动态绑定,更多细节参考C++ dynamic binding

Part 2: const 补充

这个小结中,关于 const 的所有例子均来自于msdn。为了便于理解, 对代码进行了稍微的调整。

2.1 const 修饰指针

下面这个例子中, const 修饰的是指针,因此不能修改指针 aptr 的值,即 aptr 不能指向另一个位置。

// constant_values3.cppintmain(){char*mybuf=0,*yourbuf;char*constaptr=mybuf;*aptr='a';// OKaptr=yourbuf;// C3892}

2.2 const 修饰指针指向的数据

下面这个例子中, const 修饰的是指针指向的数据,因此可以修改指针的值,但是不能修改指针指向的数据。

// constant_values4.cpp#include intmain(){constchar*mybuf="test";char*yourbuf="test2";printf_s("%s\n",mybuf);constchar*bptr=mybuf;// Pointer to constant dataprintf_s("%s\n",bptr);// *bptr = 'a';  // Error}

2.3 const 修饰成员函数

在声明成员函数时,如果在函数末尾使用 const 关键字,那么可以称这个函数是”只读”函数。 const成员函数不能修改任何 非static的成员变量, 也不能调用任何 非const 成员函数。

const成员函数在声明和定义时,都必须带有 const 关键字。看下面这个例子:

// constant_member_function.cppclassDate{public:Date(intmn,intdy,intyr);intgetMonth()const;// A read-only functionvoidsetMonth(intmn);// A write function; can't be constprivate:intmonth;};intDate::getMonth()const{returnmonth;// Doesn't modify anything}voidDate::setMonth(intmn){month=mn;// Modifies data member}intmain(){DateMyDate(7,4,1998);constDateBirthDate(1,18,1953);MyDate.setMonth(4);// OkayBirthDate.getMonth();// OkayBirthDate.setMonth(4);// C2662 Error}

Part 3:new 和 delete

3.1 分解 new 和 delete

new 和 delete 都是表达式,因此不能被重载。它们均有不同步骤组成:

new 的执行步骤:

调用operator new 分配内存 (malloc)

对指针进行类型转换

调用构造函数

delete 的执行步骤:

调用析构函数

调用operator delete释放内存 (free)

虽然,new 和 delete 不能被重载,但是 operator new 和 operator delete 可以被重载。 更多细节查看msdn 上的相关页面。 关于重写 operator new/delete的一些原因,参考Customized Allocators with Operator New and Operator Delete

3.2 重载 operator new 和 operator delete

3.2.1 重载全局 operator new 和 operator delete

用户可以通过重新定义 全局 new 和 delete 操作符,以便通过日志或其它方式记录内存的分配和释放。 其中一个应用场景是用于检查内存泄漏。代码如下:

// 这段代码来自于 msdn:https://msdn.microsoft.com/en-us/library/kftdy56f.aspx// spec1_the_operator_delete_function1.cpp

// compile with: /EHsc

// arguments: 3#include

#include

#include

#include usingnamespacestd;intfLogMemory=0;// Perform logging (0=no; nonzero=yes)?intcBlocksAllocated=0;// Count of blocks allocated.// User-defined operator new.void*operatornew(size_tstAllocateBlock){staticintfInOpNew=0;// Guard flag.if(fLogMemory&&!fInOpNew){fInOpNew=1;clog<<"Memory block "<<++cBlocksAllocated<<" allocated for "<1)for(inti=0;i

编译并运行这段代码,可以看到如下输出:

oscar@ubuntu:~/$ g++ -o main spec1_the_operator_delete_function1.cpp -lm

oscar@ubuntu:~/$ ./main 3

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

Memory block 1 allocated for 10 bytes

Memory block 1 deallocated

故事到这里还没有结束,细心的童鞋可能会发现:创建和释放 char* pMem 时,使用的分别是 operator new[] (size_t) 和 operator delete[] (void*), 并没有调用 operator new 和 operator delete。打印的结果却告诉我:operator new 和 operator delete 确实被调用了(作惊恐状)!!!

这里,我找到了 cpluscplus.com 上关于 operator new[] 的表述。不解释,直接上图:

关于重新定义 operator new[] 和 operator delete[],参考 msdn上new and delete Operators页面最下方类成员函数 operator new[] 和 operator delete[] 的实现,它们是类似的。

3.2.2 重载类的成员函数 operator new 和 operator delete

上面我们介绍了重写全局 operator new、operator new[]、operator delete、operator delete[] 的覆盖 (override)。 下面我们看看 类作用域下这四个函数如何实现,应用场景以及注意事项。

在类中重写 operator new/delete([]) 成员函数时,必须声明它们为 static,因此不能声明为虚函数。

下面给出一个重写类 operator new/delete 方法的例子:

// https://msdn.microsoft.com/en-us/library/kftdy56f.aspx

// spec1_the_operator_new_function1.cpp#include

#include

#include

#include usingnamespacestd;classBlanks{public:Blanks(){}Blanks(intdummy){throw1;}staticvoid*operatornew(size_tstAllocateBlock);staticvoid*operatornew(size_tstAllocateBlock,charchInit);staticvoid*operatornew(size_tstAllocateBlock,doubledInit);staticvoidoperatordelete(void*pvMem);staticvoidoperatordelete(void*pvMem,charchInit);staticvoidoperatordelete(void*pvMem,doubledInit);};void*Blanks::operatornew(size_tstAllocateBlock){clog<<"Blanks::operator new( size_t )\n";void*pvTemp=malloc(stAllocateBlock);returnpvTemp;}void*Blanks::operatornew(size_tstAllocateBlock,charchInit){clog<<"Blanks::operator new( size_t, char )\n";// throw 20;void*pvTemp=malloc(stAllocateBlock);if(pvTemp!=0)memset(pvTemp,chInit,stAllocateBlock);returnpvTemp;}void*Blanks::operatornew(size_tstAllocateBlock,doubledInit){clog<<"Blanks::operator new( size_t, double)\n";returnmalloc(stAllocateBlock);}voidBlanks::operatordelete(void*pvMem){clog<<"Blanks::opeator delete (void*)\n";free(pvMem);}voidBlanks::operatordelete(void*pvMem,charchInit){clog<<"Blanks::opeator delete (void*, char)\n";free(pvMem);}voidBlanks::operatordelete(void*pvMem,doubledInit){clog<<"Blanks::opeator delete (void*, double)\n";free(pvMem);}// For discrete objects of type Blanks, the global operator new function

// is hidden. Therefore, the following code allocates an object of type

// Blanks and initializes it to 0xa5intmain(){Blanks*a5=new('c')Blanks;deletea5;cout<

linux运行上的代码,结果如下:

Blanks::operator new( size_t, char )

Blanks::opeator delete (void*)

Blanks::operator new( size_t )

Blanks::opeator delete (void*)

Blanks::operator new( size_t, double)

terminate called after throwing an instance of 'int'

Aborted (core dumped)

很容易发现,不管我们使用哪个版本的 operator new,最后调用的都是 不含额外的参数的 operator delete。 构造函数抛出异常时,也没有调用对应的 operator delete 成员函数。 那么包含额外参数的 delete什么时候会被调用到,应用场景由有哪些呢?

我们继续找相关的文档,msdn上有这样一段文字:

voidoperatordelete(void*);voidoperatordelete(void*,size_t);

Only one of the preceding two forms can be present for a given class. The first form takes a single argument of type void *, which contains a pointer to the object to deallocate. The second form—sized deallocation—takes two arguments, the first of which is a pointer to the memory block to deallocate and the second of which is the number of bytes to deallocate. The return type of both forms is void (operator delete cannot return a value).

The intent of the second form is to speed up searching for the correct size category of the object to be deleted, which is often not stored near the allocation itself and likely uncached; the second form is particularly useful when an operator delete function from a base class is used to delete an object of a derived class.

这里的解释也有些问题,通过上面的例子,可以推断 operator new/delete 均可以被重载。 创建对象时,可以使用不同版本的operator new,但是销毁时,只调用不包含额外参数的operator delete。 delete 的应用场景之一是:在继承体系中,Base* 指向一个子类对象,调用 delete 销毁该对象时,必须保证销毁父类对象,而不是根据子类对象的大小进行截断销毁。

事实上,上面所说的应用场景也没有得到验证。我对上面的代码进行了修改,销毁时调用的仍然是不含额外参数的 delete:

// https://msdn.microsoft.com/en-us/library/kftdy56f.aspx

// spec1_the_operator_new_function1.cpp#include

#include

#include

#include usingnamespacestd;classBase{public:virtual~Base(){}};classBlanks:publicBase{// ...  没有改变 ...};intmain(){Base*a5=new('c')Blanks;// 打印 Blanks::operator new( size_t, char )deletea5;// 打印 Blanks::opeator delete (void*)}

根据侯捷老师关于 basic_string 的分析,operator delete 并没有传入额外的参数,而是通过 Allocator::deallocate 去删除。 因此 重载 operator delete 没有任何意义,需要时 重新定义 operator delete(void* p)即可。 需要查看 stl 文章和源码的话,可以去Code Projectsgi网站上查看。

注意:为类定义 operator new/delete 成员函数会覆盖 全局默认的 operator new/delete。 如果要使用默认的 operator new/delete,那么在创建对象和销毁对象时,需要使用 ::new 和 ::delete。

课程目录:

一、对象模型:vptr和vtbl


二、对象模型:关于this

三、对象模型:关于Dynamic Binding

四、谈谈const

五、关于new,delete

六、重载operator new,operator delete,operator new[],operator delete[]

七、示例

八、重载new(),delete()$示例

九、Basic_String使用new(extra)扩充申请量

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容