为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

 在effective c++中第十一条写到这样的话,可是我今天又忘了。
   在写一个类的时候,里面用到了指针和动态分配内存的东西。我定义了一个全局的变量来存放这些类的对象,在程序里用了临时一个对象来循环的取出这些对象,第一次退出函数的时候没有出错,可是当第二次退出的时候,却在其析构函数中delete[] p;出错了,其根本原因还是在这里。
class
  func()
{
    CTMR_RTU current_rtu=pTmrNet->m_RTU[TmrSta->currentItem()];
       CTMR_MP tmpmp;
        for(int i=0; i<current_rtu->m_MPCount; i++)
        {
            tmpmp=current_rtu->m_MP[i];
            TmrEqu->insertItem(QString(tr(tmpmp.m_MingZi)));
            mp_nb[mp_nb_len++]=tmpmp.m_ID;

        }
}
  class CTMR_RTU
{
    public:
  
CTMR_RTU();
  ~
CTMR_RTU()
   {
    delete[] m_MP;
   }
 
CTMR_MP* m_MP;
  int m_MPCount;
}
  在上面这段代码中,current_rtu为一个临时变量,将全局变量中的rtu拷贝出来,而此时拷贝的只是指针的值,而指针所指向的内存并没有拷贝,因此当该函数结束的时候,current_rtu调用其构造函数,delete[] m_MP这句话也同时将全局变量中的m_MP也一同释放掉了,因此当再次访问这个函数的时候,就会出现不可知的错误。所以在访问数组的时候,最好还是用指针来访问,这样既省去了一个临时变量的开销,而且在退出的时候也不用调用其析构函数。上面的代码改为:
CTMR_RTU* current_rtu=&pTmrNet->m_RTU[TmrSta->currentItem()];这样就不会出错了,函数结束的时候,释放的只是指针的内容。
     另外一种方法是如effective c++讲到的为类声明一个拷贝构造函数和一个赋值操作符。
 

    几乎所有的类都有一个或多个构造函数,一个析构函数和一个赋值操作符。这没什么奇怪的,因为它们提供的都是一些最基本的功能。构造函数控制对象生成时的基本操作,并保证对象被初始化;析构函数摧毁一个对象并保证它被彻底清除;赋值操作符则给对象一个新的值。在这些函数上出错就会给整个类带来无尽的负面影响,所以一定要保证其正确性。本章我将指导如何用这些函数来搭建一个结构良好的类的主干。


条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

看下面一个表示string对象的类:

// 一个很简单的string类
class string {
public:
  string(const char *value);
  ~string();

  ...                           // 没有拷贝构造函数和operator=

private:
  char *data;
};

string::string(const char *value)
{
  if (value) {
    data = new char[strlen(value) + 1];
    strcpy(data, value);
  }
  else {
    data = new char[1];
    *data = '/0';
  }
}

inline string::~string() { delete [] data; }

请注意这个类里没有声明赋值操作符和拷贝构造函数。这会带来一些不良后果。

如果这样定义两个对象:

string a("hello");
string b("world");

其结果就会如下所示:

a:  data——> "hello/0"
b:  data——> "world/0"

对象a的内部是一个指向包含字符串"hello"的内存的指针,对象b的内部是一个指向包含字符串"world"的内存的指针。如果进行下面的赋值:

b = a;

因为没有自定义的operator=可以调用,c++会生成并调用一个缺省的operator=操作符(见条款45)。这个缺省的赋值操作符会执行从a的成员到b的成员的逐个成员的赋值操作,对指针(a.data和b.data) 来说就是逐位拷贝。赋值的结果如下所示:

a:  data --------> "hello/0"
            /
b:  data --/       "world/0"

这种情况下至少有两个问题。第一,b曾指向的内存永远不会被删除,因而会永远丢失。这是产生内存泄漏的典型例子。第二,现在a和b包含的指针指向同一个字符串,那么只要其中一个离开了它的生存空间,其析构函数就会删除掉另一个指针还指向的那块内存。

string a("hello");                 // 定义并构造 a

{                                  // 开一个新的生存空间
  string b("world");               // 定义并构造 b

  ...

  b = a;                           // 执行 operator=,
                                   // 丢失b的内存

}                                  // 离开生存空间, 调用
                                   // b的析构函数

string c = a;                      // c.data 的值不能确定!
                                   // a.data 已被删除

例子中最后一个语句调用了拷贝构造函数,因为它也没有在类中定义,c++以与处理赋值操作符一样的方式生成一个拷贝构造函数并执行相同的动作:对对象里的指针进行逐位拷贝。这会导致同样的问题,但不用担心内存泄漏,因为被初始化的对象还不能指向任何的内存。比如上面代码中的情形,当c.data用 a.data的值来初始化时没有内存泄漏,因为c.data没指向任何地方。不过,假如c被a初始化后,c.data和a.data指向同一个地方,那这个地方会被删除两次:一次在c被摧毁时,另一次在a被摧毁时。

拷贝构造函数的情况和赋值操作符还有点不同。在传值调用的时候,它会产生问题。当然正如条款22所说明的,一般很少对对象进行传值调用,但还是看看下面的例子:

void donothing(string localstring) {}

string s = "the truth is out there";

donothing(s);

一切好象都很正常。但因为被传递的localstring是一个值,它必须从s通过(缺省)拷贝构造函数进行初始化。于是localstring拥有了一个s内的指针的拷贝。当donothing结束运行时,localstring离开了其生存空间,调用析构函数。其结果也将是:s包含一个指向 localstring早已删除的内存的指针。

顺便指出,用delete去删除一个已经被删除的指针,其结果是不可预测的。所以即使s永远也没被使用,当它离开其生存空间时也会带来问题。

解决这类指针混乱问题的方案在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者你可以采用某种引用计数机制(见条款 m29)去跟踪当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。

对于有些类,当实现拷贝构造函数和赋值操作符非常麻烦的时候,特别是可以确信程序中不会做拷贝和赋值操作的时候,去实现它们就会相对来说有点得不偿失。前面提到的那个遗漏了拷贝构造函数和赋值操作符的例子固然是一个糟糕的设计,那当现实中去实现它们又不切实际的情况下,该怎么办呢?很简单,照本条款的建议去做:可以只声明这些函数(声明为private成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。关于这个俏皮的小技巧的细节,参见条款27。

关于本条款中所用到的那个string类,还要注意一件事。构造函数体内,在两个调用new的地方都小心地用了[],尽管有一个地方实际只需要单个对象。正如条款5所说,在配套使用new和delete时一定要采用相同的形式,所以这里也这么做了。一定要经常注意,当且仅当相应的new用了[]的时候, delete才要用[]。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值