stl中push_back和浅拷贝和深拷贝的问题

来源:互联网 发布:小小dota2数据库 编辑:程序博客网 时间:2024/05/29 14:35

《程序员面试宝典》中stl模板与容器中的一个例子:

#include <iostream>#include <cstdlib>#include <vector>#include <string.h>#include <stdio.h>using namespace std;class CDemo{public:CDemo():str(NULL){};~CDemo(){if(str) delete[] str;};char * str;};int main(){CDemo d1;d1.str=new char[32];strcpy(d1.str,"trend micro");vector<CDemo> * a1=new vector<CDemo>;a1->push_back(d1);delete a1;getchar();printf("getchar");return 0;}

上面的这个代码是有问题的,CDemo中的析构函数会重复删除同一片内存区域。但是奇怪的是使用mingw中的g++编译上面的代码时能正常执行,就是说没有出现运行时错误,但是使用visual studio 2008编译时没有出错,运行时出错。

在release模式下运行时vs可能都崩溃,出现下面的错误:


下面分析这块代码错误的原因。主要是由于push_back函数引起的。这个函数会对传递进来的参数进行一次拷贝(调用拷贝构造函数),并将其添加到vector中。如果对象没有拷贝构造函数,编译器会为其生成一个,但是这个编译器生成的拷贝构造函数只是进行了一次浅拷贝,在本例中就是只是复制了str的值,也就是"strend micro"的地址,即拷贝后的对象和原对象的str都是指向同一块内存区域,但是这个拷贝的对象和原对象的析构函数又都会执行,这里就会delete两次。(注意,即使对于一个空类,编译器也会默认生成4个成员函数:默认构造函数,析构函数,拷贝构造函数,赋值函数。)

对析构函数做如下更改可以清晰看出区别:

~CDemo(){static int i=0;cout<<"&CDemo"<<i++<<"="<<(int *)this<<",str="<<(int *)str<<endl;if(str) delete[] str;};

在VS 2008中执行后的结果如下:


用g++编译执行的结果如下:


可以发现无论是vs和g++析构函数都执行了两次,只不过vs有错误提示,g++没有错误提示而已。但是这段代码本质上还是有错误的。顺便打印出的d1的地址,可以发现在getchar之后即main函数退出后析构的是d1。另外一次析构的就是push_back函数调用时创建的那个CDemo对象。


添加拷贝构造函数,并打印出执行的次序,此处添加的拷贝构造函数就是编译器自动生成的拷贝构造函数,只是进行了简单的复制操作,很容易看出问题所在。代码如下:

#include <iostream>#include <cstdlib>#include <vector>#include <string.h>#include <stdio.h>using namespace std;class CDemo{public:CDemo():str(NULL){cout<<"construct invoke!"<<endl;};CDemo(const CDemo &cd){cout<<"copy construct invoke!"<<endl;cout<<"address of copy:"<<(int *)this<<endl;this->str=cd.str;}~CDemo(){static int i=0;cout<<"deconstruct &CDemo"<<i++<<"="<<(int *)this<<",str="<<(int *)str<<endl;if(str) delete[] str;};char * str;};int main(){CDemo d1;cout<<"address of d1: "<<&d1<<endl;d1.str=new char[32];strcpy(d1.str,"trend micro");vector<CDemo> * a1=new vector<CDemo>;a1->push_back(d1);delete a1;return 0;}
上面的代码执行后输出如下:

可以看出首先调用了构造函数,并且这个对象的地址位于0x28fef8,然后拷贝构造函数调用,拷贝后的对象的地址位于0x332370。然后析构时首先析构位于0x332370的对象,即拷贝的对象,然后析构位于0x28fef8的对象,即d1。到这里可以很明显看出问题所在了,即位于0x332390的内存区被delete了两次。

注意在delete [] str;后添加str=NULL;这行代码也没有用,因为修改d1对象的str为NULL并不会对拷贝后的对象的str产生影响,拷贝对象的析构函数执行时依然会再次delete这块内存。

修改方法是使用深拷贝:

CDemo(const CDemo &cd){cout<<"copy construct invoke!"<<endl;cout<<"address of copy:"<<(int *)this<<endl;this->str=new char[strlen(cd.str)+1];strcpy(str,cd.str);}
注意strcpy也拷贝了字符串结束符'\0'。


上面的问题解决了,还有就是push_back插入元素时的一些构造和拷贝构造的问题。网上有一篇很好的文章

 STL 的 vector push_back 类对象时出现问题,偶尔发现 vector 在 push_back 时的调用类对象的拷贝构造函数和析构函数有点特别,简单做下分析。

#include <iostream>#include <vector> using namespace std; struct sss{public:    explicit sss(int val) : value(val)    {        cout << "---init sss " << this << ", value:" << value << endl;    }     sss(const sss& org)    {        cout << "---copy " << &org << " to " << this << endl;        value = org.value;    }     ~sss()    {        cout << "---destory sss " << this << ", value:" << value << endl;    }      int value;}; int main(int argc, char ** argv){    sss s_tmp(11);    int i = 0;    vector<sss> vvv;     for (i = 0; i < 5; i++) {        s_tmp.value++;        vvv.push_back(s_tmp);        cout << "size: " << vvv.size() << ", capacity: " << vvv.capacity() << endl;    }     return 0;}

g++中编译的结果如下图:


在visual stdio 2008中执行的结果如下:


结果分析:

vector 每次调用 push_back 时都会拷贝一个新的参数指定的 sss 类对象,这会调用 sss 的拷贝构造函数,第一次的 copy 正常,而且 vector 的实际容量也由 0  变为 1。

第二次调用 push_back,通过输出会发现调用了两次拷贝构造函数,一次析构函数,原来 vector 此时判断容量不够,将容量扩大为原来的两倍,变为 2,并将原来的元素再次拷贝一份存放到新的内存空间,然后拷贝新加的类对象,最后再释放原来的元素。

第三次调用 push_back 时,vector 自动扩大为4,因此拷贝构造函数调用了3次,析构函数调用了2次,程序最终退出了时就析构了 5 次加本身的 sss 类对象一共 6 次。

参考:

由此看来,vector 的 push_back 在发现空间不足时在gcc中自动将空间以 2 的指数增长:0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 …

 在Visual Studio 2008 中发现 vector 的实际空间增加顺序为:1 - 2 - 3 - 4 - 6 - 9 - 13 - 19 - 28 - 42 - 63 - 94 - 141 - 211 …

查找资料后得知,如此设计的主要目的是为了尽可能的减小时间复杂度;如果每次都按实际的大小来增加 vector 的空间,会造成时间复杂度很高,降低 push_back 的速度。

另外关于 push_back 为什么会执行拷贝构造函数,push_back 的原型为:

void push_back(const _Ty& _Val)

参数是以引用方式传递,按说不会拷贝,但 push_back 实际实现中判断空间不足时是调用 insert 函数添加元素:

void push_back(const _Ty& _Val)
{
   // insert element at end
   if (size() < capacity())
   #if _HAS_ITERATOR_DEBUGGING
   {
      // room at end, construct it there
      _Orphan_range(_Mylast, _Mylast);
      _Mylast = _Ufill(_Mylast, 1, _Val);
   }
   #else /* _HAS_ITERATOR_DEBUGGING */
      _Mylast = _Ufill(_Mylast, 1, _Val);
   #endif /* _HAS_ITERATOR_DEBUGGING */
   else
      insert(end(), _Val);
}




0 0
原创粉丝点击