《程序员面试宝典》中stl模板与容器中的一个例子:
#include
#include
#include
#include
#include
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 * a1=new vector;
a1->push_back(d1);
delete a1;
getchar();
printf("getchar");
return 0;
}
在release模式下运行时vs可能都崩溃,出现下面的错误:
下面分析这块代码错误的原因。主要是由于push_back函数引起的。这个函数会对传递进来的参数进行一次拷贝(调用拷贝构造函数),并将其添加到vector中。如果对象没有拷贝构造函数,编译器会为其生成一个,但是这个编译器生成的拷贝构造函数只是进行了一次浅拷贝,在本例中就是只是复制了str的值,也就是"strend micro"的地址,即拷贝后的对象和原对象的str都是指向同一块内存区域,但是这个拷贝的对象和原对象的析构函数又都会执行,这里就会delete两次。(注意,即使对于一个空类,编译器也会默认生成4个成员函数:默认构造函数,析构函数,拷贝构造函数,赋值函数。)
对析构函数做如下更改可以清晰看出区别:
~CDemo(){
static int i=0;
cout<<"&CDemo"<
用g++编译执行的结果如下:
可以发现无论是vs和g++析构函数都执行了两次,只不过vs有错误提示,g++没有错误提示而已。但是这段代码本质上还是有错误的。顺便打印出的d1的地址,可以发现在getchar之后即main函数退出后析构的是d1。另外一次析构的就是push_back函数调用时创建的那个CDemo对象。
添加拷贝构造函数,并打印出执行的次序,此处添加的拷贝构造函数就是编译器自动生成的拷贝构造函数,只是进行了简单的复制操作,很容易看出问题所在。代码如下:
#include
#include
#include
#include
#include
using namespace std;
class CDemo{
public:
CDemo():str(NULL){
cout<<"construct invoke!"<str=cd.str;
}
~CDemo(){
static int i=0;
cout<<"deconstruct &CDemo"< * a1=new vector;
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!"<str=new char[strlen(cd.str)+1];
strcpy(str,cd.str);
}
注意strcpy也拷贝了字符串结束符'\0'。
上面的问题解决了,还有就是push_back插入元素时的一些构造和拷贝构造的问题。网上有一篇很好的文章
STL 的 vector push_back 类对象时出现问题,偶尔发现 vector 在 push_back 时的调用类对象的拷贝构造函数和析构函数有点特别,简单做下分析。
#include
#include
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 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;
}
在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);
}