复制构造函数和赋值构造函数

C++自动提供的成员函数有:默认构造函数,复制构造函数,默认析构函数,赋值操作符,地址操作符即this指针,这五种函数如果用户没有定义,则系统会自动创建一个。

复制构造函数:用一个对象复制一个新的对象时被调用,声明为:类名(类名&对象名);

下面这段话很清楚的说明了很多概念,需要细细体会,故粘了下来:

把参数传递给函数有三种方法,一种是值传递,一种是传地址,还有一种是传引用。前者与后两者不同的地方在于:当使用值传递的时候,会在函数里面生成传递参数的一个副本,这个副本的内容是按位从原始参数那里复制过来的,两者的内容是相同的。当原始参数是一个类的对象时,它也会产生一个对象的副本,不过在这里要注意。一般对象产生时都会触发构造函数的执行,但是在产生对象的副本时却不会这样,这时执行的是对象的复制构造函数。为什么会这样?嗯,一般的构造函数都是会完成一些成员属性初始化的工作,在对象传递给某一函数之前,对象的一些属性可能已经被改变了,如果在产生对象副本的时候再执行对象的构造函数,那么这个对象的属性又再恢复到原始状态,这并不是我们想要的。所以在产生对象副本的时候,构造函数不会被执行,被执行的是一个默认的构造函数(复制构造函数)。当函数执行完毕要返回的时候,对象副本会执行析构函数,如果你的析构函数是空的话,就不会发生什么问题,但一般的析构函数都是要完成一些清理工作,如释放指针所指向的内存空间。这时候问题就可能要出现了。假如你在构造函数里面为一个指针变量分配了内存,在析构函数里面释放分配给这个指针所指向的内存空间,那么在把对象传递给函数至函数结束返回这一过程会发生什么事情呢?首先有一个对象的副本产生了,这个副本也有一个指针,它和原始对象的指针是指向同块内存空间的。函数返回时,对象的析构函数被执行了,即释放了对象副本里面指针所指向的内存空间,但是这个内存空间对原始对象还是有用的啊,就程序本身而言,这是一个严重的错误。然而错误还没结束,当原始对象也被销毁的时候,析构函数再次执行,对同一块系统动态分配的内存空间释放两次是一个未知的操作,将会产生严重的错误。

上面说的就是我们会遇到的问题。解决问题的方法是什么呢?首先我们想到的是不要以传值的方式来传递参数,我们可以用传地址或传引用。没错,这样的确可以避免上面的情况,而且在允许的情况下,传地址或传引用是最好的方法,但这并不适合所有的情况,有时我们不希望在函数里面的一些操作会影响到函数外部的变量。那要怎么办呢?可以利用复制构造函数来解决这一问题。复制构造函数就是在产生对象副本的时候执行的,我们可以定义自己的复制构造函数。在复制构造函数里面我们申请一个新的内存空间来保存构造函数里面的那个指针所指向的内容。这样在执行对象副本的析构函数时,释放的就是复制构造函数里面所申请的那个内存空间。
除了将对象传递给函数时会存在以上问题,还有一种情况也会存在以上问题,就是当函数返回对象时,会产生一个临时对象,这个临时对象和对象的副本性质差不多。拷贝构造函数,经常被称作X(X&),是一种特殊的构造函数,他由编译器调用来完成一些基于同一类的其他对象的构件及初始化。它的唯一的一个参数(对象的引用)是不可变的(因为是const型的)。这个函数经常用在函数调用期间于用户定义类型的值传递及返回。拷贝构造函数要调用基类的拷贝构造函数和成员函数。如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用。

在C++中,下面三种对象需要拷贝的情况。因此,拷贝构造函数将会被调用。

1). 一个对象以值传递的方式传入函数体

2). 一个对象以值传递的方式从函数返回

3). 一个对象需要通过另外一个对象进行初始化

以上的情况需要拷贝构造函数的调用。如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作赋共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。
拷贝构造函数不可以改变它所引用的对象,其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象,这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环。

除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。换句话说,你从函数返回得到的只是对象的一份拷贝。但是同样的,拷贝构造函数被正确的调用了,你不必担心。

如果在类中没有显式的声明一个拷贝构造函数,那么,编译器会私下里为你制定一个函数来进行对象之间的位拷贝(bitwise copy)。这个隐含的拷贝构造函数简单的关联了所有的类成员。显式声明的拷贝构造函数关联的只是被实例化的类成员的缺省构造函数除非另外一个构造函数在类初始化或者在构造列表的时候被调用。

如果一个类中有指针成员,使用缺省的复制构造函数初始化对象就会出现问题。为了说明存在的问题,我们假定对象A与对象B是相同的类,有一个指针成员,指向对象C。当用对象B初始化对象A时,缺省的复制构造函数将B中每一个成员的值复制到A的对应的成员当中,但并没有复制对象C。也就是说,对象A和对象B中的指针成员均指向对象C,实际上,我们希望对象C也被复制,得到C的对象副本D。否则,当对象A和B销毁时,会对对象C的内存区重复释放,而导致错误。为了使对象C也被复制,就必须显式定义复制构造函数。见下面的string类的复制构造函数。class String

{

public:

String(); //构造函数

String(const String &s); //复制构造函数

~String(); //析构函数

// 接口函数

void set(char const data);

char const
get(void);

private:

char *str; //数据成员ptr指向分配的字符串

};

String ::String(const String &s)

{

str = new char[strlen(s.str) + 1];

strcpy(str, s.str);

}
下面是有关复制构造函数的两个小例子:(1)在类的定义中,如果没有显式定义复制构造函数,C++编译器会自动地定义一个缺省的复制构造函数。

#include <iostream>
#include <string.h>
using namespace std;

class withCC
{
public:
    withCC(){}
    withCC(const withCC&aa)
    {
        aa.print();
        cout<<"withCC(withCC&)"<<endl;
    }
    void print(const char* msg=0)const
    {
        cout<<"adsg"<<endl;
    }
};
class woCC
{
    enum{bsz=100};
    char buf[bsz];
public:
    woCC(const char* msg=0)
    {
        memset(buf,0,bsz);
        if(msg)strncpy(buf,msg,bsz);
    }
    void print(const char* msg=0)const
    {
        if(msg)
            cout<<msg<<":";
        cout<<buf<<endl;
    }
};
//类composite既含有withCC类的成员对象又含有woCC类的成员对象,
//它使用无参的构造函数创建withCC类的对象WITHCC(注意内嵌的对象WOCC的初始化方法)
class composite
{
    withCC WITHCC;
    woCC WOCC;
public:
    composite():WOCC("composite()"){}
    void print(const char* msg=0)
    {
        WOCC.print(msg);
    }
};

void main()
{
    composite c;
    c.print("contents of c");
    cout<<"calling composite copy-constructor"<<endl;
    composite c2=c;//通过对象C初始化对象c2,缺省的复制构造函数被调用
    c2.print("constents of c2");
}

(2)

class CStr
{
public:
  CStr(); //默认构造函数
  CStr(const char* psz); //一种广义拷贝构造函数,不过也有人不认同我的看法
    CStr(const CStr& str); //拷贝构造函数
    const CStr& operator=(const CStr& str); //赋值构造函数
    size_t GetSize() const; //这里的const是什么意思?它必须吗?
    operator const char*() const { return m_pdata; }
protected:
    const CStr* _Copy(const CStr& str);
private:
        char* m_pdata;
        size_t m_size;};
CStr::CStr()
{
    m_pdata = NULL;
    m_size = 0;
}
size_t CStr::GetSize() const
{
    return m_size;
}
const CStr* CStr::_Copy(const CStr& str)
{
   if(this != &str)
   {
      if(m_pdata)      {
          delete[] m_pdata;
      }
      m_size = str.GetSize();
      m_pdata = new char[m_size + 1]; assert(m_pdata);
      strcpy(m_pdata, str);
   }
   return this;}
CStr::CStr(const char* psz) : m_pdata(NULL), m_size(0)
{
    assert(psz);
    if(m_pdata != psz)
    {
        if(m_pdata)        {
           delete[] m_pdata;
         }
         m_size = strlen(psz);
         m_pdata = new char[m_size + 1]; assert(m_pdata);
         strcpy(m_pdata, psz);
      }
}
CStr::CStr(const CStr& str): m_pdata(NULL), m_size(0)
{
    _Copy(str);
}
const CStr& CStr::operator=(const CStr& str)
{
    return *(_Copy(str));
}
int main()
{
    const char* psz = "test";
    const char* psz1 = "me";
    CStr str(psz); //拷贝构造函数,此处调用的是CStr(const char* psz)。     #1
    CStr str2(str); // 拷贝构造函数,此处调用的是 CStr(const CStr& str)   #2
    CStr str1 = psz1; // 拷贝构造,str1此前并不存在,现在要先构造它。    #3
    str = str1; // 真正的赋值构造函数    #4
    return 0;
}

对于#3,我想着重说明的是:赋值构造的实际用意是修改一个已有的对象,而现在str1还没有被定义,所以此处还是必须要调用拷贝构造函数先产生一个CStr对象。
原文链接: https://www.cnblogs.com/xyl-share-happy/archive/2012/08/05/2623715.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/58005

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月9日 上午8:30
下一篇 2023年2月9日 上午8:31

相关推荐