Delete动态数组

illustration.png

即使把析构函数定义为virtual

依然会无法调用到派生类的析构函数

因为数组的多态会导致未定义的行为

编译器需要建立起遍历数组来调用析构函数的代码

这样他不得不先确定数组的大小

调用如下语句时

//p为指向基类的指针
delete [] p;

编译器把指针p指向的静态类型的大小析构函数指针一并传给delete运算符

而这二者都与实际不符

所以最终没有调用到派生类的destructor

大家会困惑

为何delete数组时不能像delete单个对象一样

使用虚拟化 动态决议删除的实际对象类型

这里涉及数组的实例化机制

数组的元素数量事实上在初始化时被存储于一个hash map中

hash key就是数组首元素地址

但是 并未保存元素的大小和元素的构造函数和析构函数指针等内容

销毁数组的时候

对数组中的每个元素迭代调用类似如下全局函数

void * vec_delete (
    //数组首地址
    void *array,
    //元素大小
    size_t elem_size,
    //元素个数
    int elem_count,
    //析构函数指针
    void (*destructor)(void *)
);

这里的array传入p

因为p是个基类指针

当它是一个非第一直接基类时

与一个正确的派生类指针的地址相比

会有一定的偏移

elem_size和destructor都是根据p的静态类型来获取

显然都是不符合实际的

elem_count则是通过hash map取出

由于hash key是数组首地址

这个都不一定是对的

那么就不一定能取得正确的elem_count

甚至当对应的hash槽位为空时根本取不到

以上所言种种变数导致了使用基类指针来做delete[]

是一个未定义行为

只需要对题目的示例程序稍作改变立刻能玩崩:

class Base {

public:

Base() {printf("Base\n");}

~Base() {printf("~Base\n");}

int i = 1;

};

class A : virtual  public  Base

{

public:

A() { printf("A\n");}

~A(){ printf("~A\n");}

};

class B : virtual  public  Base

{

public:

B() { printf("B\n");}

~B(){ printf("~B\n");}

};

class C : public  A, public  B

{

public:

C() { printf("C\n");}

~C(){ printf("~C\n");}

};



int main()

{

C *c = new  C[2];

/*

分别将如下各变体拿到注释外执行:

Base *p = c;

A *p = c;

B *p = c;

*/

printf("%p %p\n", p, c);

delete[] p;

}

我们修改了原来的单继承为多继承+虚继承

诸位可以用示例代码中的注释中的变体程序一一尝试

各种崩溃的结果可以玩出花来

原来的A *p如果说还能碰巧因为和C *c的初始地址对齐而勉强执行成功

而新来的非第一直接基类B *p和虚基类Base *p则会带来不一样的初始地址

和酸爽的崩溃

触目惊心之余得来一句逆耳忠言

别用基类指针释放动态数组内存

下面对比一下单个对象的delete

delete p;

这里如果p的析构函数是个非虚函数

那么就直接调用了

如果是个虚析构函数

编译器会直接进入p所指对象的虚表里

通过thunk技术检索得到派生类的析构函数指针

并将this指针完成一定偏移指向派生类对象的初始地址

所以单个变量的析构过程是可以动态化的

进一步思考,假如我们在题目示例代码的程序层面上做些调整;

    const static int N = 2;
    A* c = new B[N];
    for (int i=0; i<N; ++i) {
        delete ((B*)c+i);
    }

这样也是不行的

C++标准表示,对数组调用delete而不是delete[]运算符会导致未定义行为

以上代码会出现一个运行时错误:

ABAB~B~A

TestCPP(1739,0x10012c3c0) malloc: *** error for object 0x1004067e8: pointer being freed was not allocated

可见已经析构到了第一个对象的基类了

但是显示的错误是重复delete

这就是未定义行为

所以在对数组元素执行虚函数时

还是要用派生类的指针来delete

B* p = new B[N];
delete[] p;

2018-04-11 更

感谢 @欧阳大哥2013 的回复
回复内容对new动态分配的数组的大小存储的位置做了质疑
在其空间拜读了《C++的new和delete详解》
写得很好
其中相关内容如下

// CA *p3 = new CA[20]; 这句代码在编译时其实会转化为如下的代码片段 
//64位系统多分配8字节 *p = 20; //这里保存分配的对象的数量。
unsigned long *p = operator new[](20 * sizeof(CA) + sizeof(unsigned long)); 
CA *p3 = (CA*)(p + 1); 
CA *pt = p3; 
for (int i = 0; i < *p; i++) { CA::CA(pt); pt += 1; } 

据此我发表自己的以下看法:

首先引用以下Lippman(C++标准委员会成员之一,《C++ Primer》等著作的作者)所著的《Inside the C++ object model》一书中 “6.2 Operators new and delete”这一章节中的子内容。

1.png
2.png
3.png
4.png

建议大家看英文版,因为中文版的翻译不好,并且错漏百出
这本书讲了Lippman在贝尔实验室写第一代C++编译器的故事
引用的这一节说的正是new[]和delete[]的设计和演变思路
下面引用里面一些关键部分

How is this caching of the element count implemented? One obvious way is to allocate an additional word of memory with each chunk of memory returned by the vec_new() operator, tucking the element count in that word (generally, the value tucked away is called a cookie). However, Jonathan and the Sun implementation chose to keep an associative array of pointer values and the array size. (Sun also tucks away the address of the destructor—see [CLAM93n].)

简单翻译如下:

动态数组里的元素数量怎么存呢?一个显而易见的方式就是多分配一个字(由数据总线宽度决定,根据架构基本上32位芯片一个字为4字节,64位芯片一个字为8字节)的存储空间给vec_new()运算符(其实就是operator new),然后把元素个数存在这个字里(这个元素个数值绰号叫cookie)。但是Sun公司的总设计师Jonathan在实现时选择保存一个关联数组(associative array),这个数组保存了动态数组的首指针和数组大小,甚至还存入了析构函数的地址。

所以 @欧阳大哥2013 的代码中所表现的思想正是Lippman文中所述的“obvious way”
这个思路清晰简洁
但是我曾经做过如下实验

#include <iostream>
using namespace std;

int main()
{
    unsigned long *p = new unsigned long[10]; 
    cout << sizeof(unsigned long) << endl;
    cout << *(p-1) << endl;
   return 0;
}

得出的结果并非数组大小,而是脏数据
其实细想一下,这种方式的编译器实现是危险的
因为数组大小暴露在用户可以掌控的范围
那么用户完全可以对数组大小进行篡改
从而达到数组溢出攻击的目的

那么我们来看看Jonathan的做法
也就是关联数组
以下就是关联数组所用到的关键外部函数

// array_key is the address of the new array which mustn't either be 0 or already entered  
// elem_count is the count; it may be 0  
 
typedef void *PV;  
extern int __insert_new_array(PV array_key, int elem_count); 

这个函数实现了把数组和其元素个数插入缓存的目的
当然这个cache具体实现Lippman没说 也没有公布 __insert_new_array的细节
我根据自己的所看过的系统内部源码实现的经验认为
最有可能的是Hash map
当然 诸君想要用单纯的链表作cache也无可厚非
这块的实现具体细节大家可以把这一节都看完
聊了很多有趣的细节
只是最后一段我做了clarify
循环调用delete销毁动态数组我做实验
是会报错的 正如我在正文中所言属于未定义行为
做了以上论述并非说 @欧阳大哥2013 的论述是错误的
确切地说 编译器一直在发展 没有永恒不变的东西
也许Jonathan的设计也在某个时间点被更新的编译器推翻了
所以 设计思路没有对错之分 一切只是顺着历史潮流而已
所以 大家只是一直在“实验-求证-实验”的循环往复中前进
日后有时间考虑再转为汇编代码看看内部实现的蛛丝马迹
如果大家有其他编译器的实现方式不妨在留言中告诉我
如果我的以上实验有不妥之处 请不吝赐教

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

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy阅读 9,515评论 1 51
  • C++文件 例:从文件income. in中读入收入直到文件结束,并将收入和税金输出到文件tax. out。 检查...
    SeanC52111阅读 2,772评论 0 3
  • const 引用 const 引用是指向 const 对象的引用:const int ival = 1024;co...
    rogerwu1228阅读 633评论 0 1
  • 3. 类设计者工具 3.1 拷贝控制 五种函数拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数拷贝和移...
    王侦阅读 1,800评论 0 1
  • 梅子黄,江南雨。共氤氲,天与地。粉如烟,飘万里。钟声遥,寒山寂。访古拙,生留意。思临安,踵宋迹。谒武穆,步苏堤。望...
    北京眠石阅读 296评论 23 30