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

《程序员面试宝典》中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);
}




阅读更多
个人分类: C++
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭