wly603

从一道面试题看深拷贝、浅拷贝构造函数问题

《程序员面试宝典》P99
请看下面的程序,说说会出现什么问题?

View Code
#include <iostream>
#include <cstdlib>
#include <vector> 
using   namespace   std; 

class   CDemo   { 
public: 
    CDemo():str(NULL){}; 
    ~CDemo() 
    { 
        if(str)   delete[]   str; 
    }; 
    char*   str; 
}; 

int   main(int   argc,   char**   argv)   { 
    CDemo   d1; 
    d1.str=new   char[32]; 
    strcpy(d1.str, "trend   micro"); 

    vector<CDemo>   *a1=new   vector<CDemo>(); 
   
    a1->push_back(d1); 
    delete   a1; 
   
    return EXIT_SUCCESS;
}

 




这个程序在退出时,会出问题,什么问题?重复delete同一片内存,程序崩溃。
我们把析构函数改为如下,可以更清楚的看到这一点:

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

 


运行时我们发现打印如下信息:
&CDemo0=000309D8,       str=000307A8
&CDemo1=0013FF70,       str=000307A8
也就是说,发生了CDemo类的两次析构,两次析构str所指向的同一内存地址空间(两次str值相同=000307A8)。
为什么?

《程序员面试宝典》第二版,P99,有句解释“vector对象指针能够自动析构,所以不需要调用delete a1,否则会造成两次析构对象”

我切以为这句话说的有点不妥。任何对象如果是通过new操作符申请了空间,必须显示的调用delete来销毁这个对象。所以“delete   a1; ”这条语句是没有错误的。
这句话“vector<CDemo>   *a1=new   vector<CDemo>(); ”定一个指针,指向 vector<CDemo>,病用new操作符进行了初始化, 我们必须在适当的时候释放a1所占的内存空间,所以“delete   a1; ”这句话是没有错误的。另外,我们必须明白一点,释放vector对象,vector所包含的元素也同时被释放。

那到底那里错误?

这句a1的声明和初始化语句“vector<CDemo>   *a1=new   vector<CDemo>(); ”说明a1所含元素是“CDemo”类型的,在执行“a1->push_back(d1); ”这条语句时,会调用CDemo的拷贝构造函数,虽然CDemo类中没有定义拷贝构造函数,但是编译器会为CDemo类构建一个默认的拷贝构造函数(浅拷贝),这就好像任何对象如果没有定义构造函数,编译器会构建一个默认的构造函数一样。

正是这里出了问题。a1中的所有CDemo元素的str成员变量没有初始化,只有一个四字节(32位机)指针空间。
“a1->push_back(d1);”这句话执行完后,a1里的CDemo元素与d1是不同的对象,但是a1里的CDemo元素的str与d1.str指向的是同一块内存,这从后来的打印信息就可以看出来。

我们知道,局部变量,如“CDemo   d1; ” 在main函数退出时,自动释放所占内存空间,
那么会自动调用CDemo的析构函数“~CDeme”,问题就出在这里。

前面的“delete   a1;”已经把 d1.str 释放了(因为a1里的CDemo元素的str与d1.str指向的是同一块内存),main函数退出时,又要释放已经释放掉的 d1.str 内存空间,所以程序最后崩溃。

解释清楚了。
这里最核心的问题归根结底就是浅拷贝和深拷贝的问题。如果CDemo类添加一个这样的拷贝构造函数就可以解决问题:
    CDemo(const   CDemo   &cd) 
    { 
        this->str   =   new   char[strlen(cd.str)+1]; 
        strcpy(str,cd.str); 
    };
这就是深拷贝。

或者这样用:
    vector<CDemo*>   *a1=new   vector<CDemo*>(); 
    a1->push_back(&d1); 
那么在    “delete   a1;” a1释放,同时a1里面包含的元素(”CDemo*“类型,仍然是一个指针,4字节空间)。

 

总结一下
1. vector <CDemo> *a1 = new vector <CDemo>(); a1是new出来的,所以必须要手工delete.这是对a1本身而言,而与a1内存储的数据无关。
2. a1 -> push_back(d1); 这部操作比较复杂,因为你的vector是存储类,而不是类指针。所以首先会在栈上创建d1的一个拷贝d1_1,压入栈,作为参数传递给push_back。然后在push_back中,创建d1_1的拷贝d1_2,d1_2是存储在a1管理的内存中。然后push_back return,d1_1出栈,调用d1_1的析构。
3. delete a1; a1中存有d1_2,所以会删除d1_2,自然会调用d1_2的析构函数。
4. 在main中return 0, d1被自动删除,此时调用d1的析构函数。
5. 因为class CDemo没有拷贝构造函数,所以创建拷贝时只是简单的把新对象中每个成员变量的值设置成与原来的对象相等。相当于运行memcpy。这时问题就来了,因为你的一个成员是char *str; 这样d1,d1_1,d1_2的str都是指向同一个地址。所以只有第一次调用CDemo的析构函数时能运行正确,以后的都会出错。因为一个地址只能释放一次。
6. 如果你的vector改为vector <CDemo*> *a1 = new vector <CDemo*>(); 即存储类指针,那么在执行delete a1之前,还要手工去删除vector中的每个元素。
7. 如何验证:在析构函数中用cout输出字符串。

posted on 2012-04-11 10:43  wly603  阅读(1209)  评论(0编辑  收藏  举报

导航