zoukankan      html  css  js  c++  java
  • C ++ 拷贝构造函数和赋值构造函数 非常经典

    C++ 拷贝构造函数 赋值构造函数

    拷贝构造函数和赋值构造函数的异同
    由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告,在阅读正文时就会多心:如果不主动编写拷贝构造函数和赋值函数,编译器将以“位拷贝”的方式自动生成缺省的函数。倘若类中含有指针变量,那么这两个缺省的函数就隐含了错误。

    以类String 的两个对象a,b 为例,

    Class      String

    {
    public:
    String(constchar*ch=NULL);//默认构造函数
    String(constString&str);//拷贝构造函数
    ~String(void);
    String&operator=(constString&str);//赋值函数
    private:
    char*m_data;
    };

    位拷贝拷贝的是地址,而值拷贝则拷贝的是内容。如果定义两个String对象A和B。A.m_data和B.m_data分别指向一段区域,A.m_data="windows",B.m_data=“linux";

    如果未重写赋值函数,将B赋给A;则编译器会默认进行位拷贝,A.m_data=B.m_data

    则A.m_data和B.m_data指向同一块区域,虽然A.m_data指向的内容会改变成"linux",但是这样容易出现这些问题:

    (1):A.m_data原来指向的内存区域未释放,造成内存泄露。

    (2):A.m_data和B.m_data指向同一块区域,任何一方改变都会影响另一方

               (3):当对象被析构时,B.m_data被释放两次。

    对于编译器,如果不主动编写拷贝函数和赋值函数,它会以“位拷贝”的方式自动生成缺省的函数。

    拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。以下程序中,第三个语句和第四个语句很相似,你分得清楚哪个调用了拷贝构造函数,哪个调用了赋值函数吗?

           String a(“hello”);
      String b(“world”);
      String c = a; // 调用了拷贝构造函数,最好写成 c(a);
      c = b; // 调用了赋值函数
      本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。
      

    类String 的拷贝构造函数与赋值函数
      // 拷贝构造函数
      String::String(const String &other)
      {
      // 允许操作other 的私有成员m_data
      int length = strlen(other.m_data);
      m_data = new char[length+1];
      strcpy(m_data, other.m_data);
      }
      // 赋值函数
      String & String::operator =(const String &other)
      {
      // (1) 检查自赋值
      if(this == &other)
      return *this;
      // (2) 释放原有的内存资源
      delete [] m_data;
      // (3)分配新的内存资源,并复制内容
      int length = strlen(other.m_data);
      m_data = new char[length+1];
      strcpy(m_data, other.m_data);
      // (4)返回本对象的引用
      return *this;
      }
      类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。类String 的赋值函数比构造函数复杂得多,分四步实现:
      (1)第一步,检查自赋值。你可能会认为多此一举,难道有人会愚蠢到写出 a = a 这样的自赋值语句!的确不会。但是间接的自赋值仍有可能出现,例如
      // 内容自赋值
      b = a;
      …
      c = b;
      …
      a = c;
      // 地址自赋值
      b = &a;
      …
      a = *b;
      也许有人会说:“即使出现自赋值,我也可以不理睬,大不了化点时间让对象复制自己而已,反正不会出错!”他真的说错了。看看第二步的delete,自杀后还能复制自己吗?所以,如果发现自赋值,应该马上终止函数。注意不要将检查自赋值的if 语句
      if(this == &other)
      错写成为
      if( *this == other)
      (2)第二步,用delete 释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
      (3)第三步,分配新的内存资源,并复制字符串。注意函数strlen 返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy 则连‘\0’一起复制。
      (4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this 错写成 return this 。那么能否写成return other 呢?效果不是一样吗?不可以!因为我们不知道参数other 的生命期。有可能other 是个临时对象,在赋值结束后它马上消失,那么return other 返回的将是垃圾。
      偷懒的办法处理拷贝构造函数与赋值函数
      如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?
      偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
      例如:
      class A
      { …
      private:
      A(const A &a); // 私有的拷贝构造函数
      A & operator =(const A &a); // 私有的赋值函数
      };
      如果有人试图编写如下程序:
      A b(a); // 调用了私有的拷贝构造函数
      b = a; // 调用了私有的赋值函数
      编译器将指出错误,因为外界不可以操作A 的私有函数。

    一、
    拷贝构造,是一个的对象来初始化一边内存区域,这边内存区域就是你的新对象的内存区域赋值运算,对于一个已经被初始化的对象来进行operator=操作
    class    A;     
    A   a;  
    A   b=a;    //拷贝构造函数调用  
    //或  
    A   b(a);    //拷贝构造函数调用  
    ///     
    A   a;  
    A   b;  
    b =a;    //赋值运算符调用   

    你只需要记住,在C++语言里,  
    String    s2(s1);  
    String    s3    =    s1;  
    只是语法形式的不同,意义是一样的,都是定义加初始化,都调用拷贝构造函数。

    二、
    一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求的 一种是复制指针对象,一种是引用指针对象 copy大多数情况下是复制,=则是引用对象的     
    例子:  
       class    A  
       {  
              int    nLen;  
              char    *    pData;  
       }  
       显然  
       A    a,    b;  
       a=b的时候,对于pData数据存在两种需求  
       第一种copy  
           a.pData    =    new    char    [nLen];  
           memcpy(a.pData,    b.pData,    nLen);  
       另外一种(引用方式):  
           a.pData    =    b.pData  
       
       通过对比就可以看到,他们是不同的  
       往往把第一种用copy使用,第二种用=实现
       你只要记住拷贝构造函数是用于类中指针,对象间的COPY  

    三、
       和拷贝构造函数的实现不一样    
       拷贝构造函数首先是一个构造函数,它调用的时候产生一个对象,是通过参数传进来的那个对象来初始化,产生的对象。  
       operator=();是把一个对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话就不做任何操作。

    还要注意的是拷贝构造函数是构造函数,不返回值   

       而赋值函数需要返回一个对象自身的引用,以便赋值之后的操作   

       你肯定知道这个:   

        int    a,    b;   

        b    =    7;   

        Func(    a    =    b    );    //    把i赋值后传给函数Func(    int    )   

       同理:   

        CMyClass    obj1,    obj2;   

           obj1.Initialize();       

           Func2(    obj1    =    obj2    );    //如果没有返回引用,是不能把值传给Func2的   

        

           注:   

        CMyClass    &    CMyClass::    operator    =    (    CMyClass    &    other    )   

           {   

                   if(    this    ==    &other    )   

                          return    *this;   

                   //    赋值操作...   

                   return    *this   

           }

    ==================================================================================

    赋值运算符和复制构造函数都是用已存在的B对象来创建另一个对象A。不同之处在于:赋值运算符处理两个已有对象,即赋值前B应该是存在的;复制构造函数是生成一个全新的对象,即调用复制构造函数之前A不存在。
      CTemp a(b); //复制构造函数,C++风格的初始化
      CTemp a=b; //仍然是复制构造函数,不过这种风格只是为了与C兼容,与上面的效果一样,在这之前a不存在,或者说还未构造好。
      CTemp a;
      a=b; //赋值运算符
      在这之前a已经通过默认构造函数构造完成。
      实例总结:
      重点:包含动态分配成员的类 应提供拷贝构造函数,并重载"="赋值操作符。
      以下讨论中将用到的例子:
      class CExample
      {
      public:
      CExample(){pBuffer=NULL; nSize=0;}
      ~CExample(){delete pBuffer;}
      void Init(int n){ pBuffer=new char[n]; nSize=n;}
      private:
      char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
      int nSize;
      };
      这个类的主要特点是包含指向其他资源的指针。
      pBuffer指向堆中分配的一段内存空间。
      一、拷贝构造函数
      调用拷贝构造函数1
      int main(int argc, char* argv[])
      {
      CExample theObjone;
      theObjone.Init(40);
      //现在需要另一个对象,需要将他初始化称对象一的状态
      CExample theObjtwo=theObjone;//拷贝构造函数
      ...
      }
      语句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
      其完成方式是内存拷贝,复制所有成员的值。
      完成后,theObjtwo.pBuffer==theObjone.pBuffer。
      即它们将指向同样的地方(地址空间),指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。
      所以需要采用必要的手段来避免此类情况。
      回顾以下此语句的具体过程:通过拷贝构造函数(系统默认的)创建新对象theObjtwo,并没有调用theObjtwo的构造函数(vs2005试验过)。
      可以在自定义的拷贝构造函数中添加输出的语句测试。
      注意:
      对于含有在自由空间分配的成员时,要使用深度复制,不应使用浅复制。
      调用拷贝构造函数2
      当对象直接作为参数传给函数时,函数将建立对象的临时拷贝,这个拷贝过程也将调同拷贝构造函数。
      例如
      BOOL testfunc(CExample obj);
      testfunc(theObjone); //对象直接作为参数。
      BOOL testfunc(CExample obj)
      {
      //针对obj的操作实际上是针对复制后的临时拷贝进行的
      }
      调用拷贝构造函数3
      当函数中的局部对象被被返回给函数调者时,也将建立此局部对象的一个临时拷贝,拷贝构造函数也将被调用
      CTest func()
      {
      CTest theTest;
      return theTest
      }
      二、赋值符的重载
      下面的代码与上例相似
      int main(int argc, char* argv[])
      {
      CExample theObjone;
      theObjone.Init(40);
      CExample theObjthree;
      theObjthree.Init(60);
      //现在需要一个对象赋值操作,被赋值对象的原内容被清除,并用右边对象的内容填充。
      theObjthree=theObjone;
      return 0;
      }
      也用到了"="号,但与"一、"中的例子并不同,"一、"的例子中,"="在对象声明语句中,表示初始化。更多时候,这种初始化也可用括号表示。
      例如 CExample theObjone(theObjtwo);
      而本例子中,"="表示赋值操作。将对象theObjone的内容复制到对象theObjthree;,这其中涉及到对象theObjthree原有内容的丢弃,新内容的复制。
      但"="的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。
      由于对象内包含指针,将造成不良后果:为了避免内存泄露,指针成员将释放指针所指向的空间,以便接受新的指针值,这正是由赋值运算符的特征所决定的。但如果是"x=x"即自己给自己赋值,会出现什么情况呢?x将释放分配给自己的内存,然后,从赋值运算符右边指向的内存中复制值时,发现值不见了。
      因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载"="赋值操作符号。
      类定义变为:
      class CExample
      {
      ...
      CExample(const CExample&); //拷贝构造函数
      CExample& operator = (const CExample&); //赋值符重载
      ...
      };
      //赋值操作符重载
      CExample & CExample::operator = (const CExample& RightSides)
      {
      nSize=RightSides.nSize; //复制常规成员
      char *temp=new char[nSize]; //复制指针指向的内容
      memcpy(temp, RightSides.pBuffer, nSize*sizeof(char));
      delete []pBuffer; //删除原指针指向内容 (将删除操作放在后面,避免X=X特殊情况下,内容的丢失)
      pBuffer=temp; //建立新指向
      return *this
      }
      三、拷贝构造函数使用赋值运算符重载的代码。
      CExample::CExample(const CExample& RightSides)
      {
      pBuffer=NULL;
      *this=RightSides //调用重载后的"="
      }

    最后来一个完整的例子

    #include<iostream>
    #include<cwchar>
    #include<windows.h>
    #include<vector>
    #include<string>
    #include <fstream>
    using namespace std;
    class CExample
    {
    public:
        CExample()
        {
            cout << "构造函数被调用!!!" << endl;
            pBuffer = NULL;
            nSize = 0;
        }
        CExample(char *p)
        {
            cout << "带参数构造函数被调用!!!" << endl;
            pBuffer = NULL;
     
            //
            pBuffer = p;
            
            nSize = strlen(p);
     
        }
        ~CExample()
        {
            cout << "析构函数被调用!!!" << endl;
        }
        void Print()
        {
            cout << nSize << endl;
        }
        //拷贝构造函数和赋值运算符的写法
        CExample(const CExample& test)
        {
            cout << "拷贝构造函数被调用!!!" << endl;
            int length = strlen(test.pBuffer);
            pBuffer = new char[length+1];
            strcpy(pBuffer, test.pBuffer);
        }
        CExample& operator =(const CExample& Rightside)
        {
            cout << "赋值函数被调用!!!" << endl;
            //不要返回临时变量的引用或者指针,不要对常量的地址使用delete
            // (1) 检查自赋值
            if (this == &Rightside)//代表的不是引用,代表的是地址
                return *this;
            // (2) 释放原有的内存资源
            delete pBuffer;
            pBuffer = NULL;
            // (3)分配新的内存资源,并复制内容
            int length = strlen(Rightside.pBuffer);
            pBuffer = new char[length + 1];
            strcpy(pBuffer, Rightside.pBuffer);
            // (4)返回本对象的引用
            return *this;
        }
    private:
        char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源
        int nSize;
    };
    int main()
    {
     
        {
            char *w = new char[8];//之所以这样进行转化,是因为,在重载=运算符的时候,
     
    //要进行delete原来的指针,所以传进来的得是new出来的
            strcpy(w,"windows");
            CExample theObjone(w);
     
     
            char *w1 = new char[6];
            strcpy(w1, "linux");
            CExample theObjthree(w1);
            //现在需要另一个对象,需要将他初始化称对象一的状态
            CExample theObjtwo = theObjone;//拷贝构造函数
     
            cout << "theObjtwo.Print()!!!" << endl;
            theObjtwo.Print();
     
            theObjthree = theObjone;
            cout << "theObjthree.Print()!!!" << endl;
            theObjthree.Print();
        }
        cout << "作用域结束!!!" << endl;
        while (1);
        return 0;
    }
     

  • 相关阅读:
    平衡二叉树之RB树
    平衡二叉树之AVL树
    实现哈希表
    LeetCode Median of Two Sorted Arrays
    LeetCode Minimum Window Substring
    LeetCode Interleaving String
    LeetCode Regular Expression Matching
    PAT 1087 All Roads Lead to Rome
    PAT 1086 Tree Traversals Again
    LeetCode Longest Palindromic Substring
  • 原文地址:https://www.cnblogs.com/lidabo/p/15667646.html
Copyright © 2011-2022 走看看