一个double free相关问题的澄清
引言
前一阵定位 Oracle 的 OCI 接口相关的一个内存释放问题,在网上看到了链接如下的这篇文章:
一个C++bug引入的许多知识
看到后面说 vector 里的两个单元里的内部成员指针地址是一样的,感觉很怪异。于是写了个简单程序实际验证一下。
COuter 和 CInner 类
1 #include <stdio.h> 2 #include <vector> 3 4 class CInner 5 { 6 public: 7 CInner(int id) : m_nId(id) { 8 printf(" Inner item %d constructor\n", id); 9 } 10 ~CInner() { 11 printf(" Inner item 0x%X deconstructor\n", m_nId); 12 } 13 private: 14 int m_nId; 15 }; 16 17 class COuter 18 { 19 public: 20 COuter(int id) : m_pInner(NULL), m_nId(id) { 21 printf("Outer item %d constructor\n", id); 22 } 23 ~COuter() { 24 printf("Outer item %d deconstructor\n", m_nId); 25 if (m_pInner) { 26 printf("Inner item %d with address=0x%X will be deleted...\n", m_nId, m_pInner); 27 delete m_pInner; 28 printf("Inner item %d deleting done.\n\n", m_nId); 29 } 30 else { 31 printf("Item %d has no inner item.\n\n", m_nId); 32 } 33 } 34 35 CInner* getInnerItem() { 36 if (NULL == m_pInner) { 37 m_pInner = new CInner(m_nId); 38 printf("A inner item with id=%d and address=0x%X is created.\n", m_nId, m_pInner); 39 } 40 return m_pInner; 41 } 42 private: 43 CInner* m_pInner; 44 int m_nId; 45 };
main
1 int main() 2 { 3 std::vector<COuter> vec; 4 for (int idx = 0; idx < 2; ++idx) { 5 COuter item(idx); 6 printf("Outer item %d will be pushed into vector...\n", idx); 7 vec.push_back(item); 8 printf("Outer item %d is in vector.\n", idx); 9 vec.back().getInnerItem(); 10 } 11 return 0; 12 }
运行结果分析
程序本身很简单,以下以 Windows 平台为例(Linux 下情形类似)就运行输出结果进行分析。
程序运行输出以下信息后报错:
1 Outer item 0 constructor // 局部变量COuter item(0)开始生命周期,其构造函数被调用,这是第一个id为0的Outer实例 2 Outer item 0 will be pushed into vector... 3 Outer item 0 is in vector. // 此时有两个Outer实例,id都为0,第二个是第一个的拷贝构造,并已放入vec里 4 Inner item 0 constructor // 为第二个Outer实例构造inner实例 5 A inner item with id=0 and address=0xD55D98 is created. 6 Outer item 0 deconstructor // COuter item(0)到了生命周期,其析构函数被调用,之后只剩第二个id为0的Outer实例 7 Item 0 has no inner item. 8 9 Outer item 1 constructor // 局部变量COuter item(1)开始生命周期 10 Outer item 1 will be pushed into vector... 11 Outer item 0 deconstructor 12 Inner item 0 with address=0xD55D98 will be deleted... 13 Inner item 0x0 deconstructor 14 Inner item 0 deleting done. 15 16 Outer item 1 is in vector. 17 Inner item 1 constructor 18 A inner item with id=1 and address=0xD55D50 is created. 19 Outer item 1 deconstructor 20 Item 1 has no inner item. 21 22 Outer item 0 deconstructor 23 Inner item 0 with address=0xD55D98 will be deleted... 24 Inner item 0xFEEEFEEE deconstructor
第11行到第14行的输出信息说明:vec里因为要增加新单元,其内存空间要做调整,第三个id为0的Outer实例由第二个id为0的Outer实例拷贝构造而成并加入vec,随后便释放了第二个id为0的Outer实例。由于缺省拷贝函数会对指针成员做简单值拷贝,第三个id为0的Outer实例里的m_pInner指针指向的是第二个id为0的Outer实例所指向的Inner实例,即两个Outer实例内部指向了同一个Inner实例。而这个Inner实例随着第二个id为0的Outer实例的释放而被释放了,第三个id为0的实例还在指向这个被释放了的Inner实例,这就留下了隐患,因为。
第16行到20行的输出,很好理解,此时vec里有两个单元,其中id为1的单元实例所指向的Inner实例也已创建好了。两个Inner实例的所占堆空间的起始地址是不一样的,即0xD55D98和0xD55D50。问题在于0xD55D98所指向的地址已经释放过了。
第22行的输出说明:局部变量vec到了生命周期,在释放第一个单元实例时,由于其指针成员不为NULL,于是对0xD55D98所指空间又做一次释放,从而引发了问题。