【拷贝构造和赋值运算符】C++ 拷贝构造函数和赋值运算符函数

本文前面主要介绍了拷贝构造函数和赋值运算符函数的区别,以及在什么时候调用拷贝构造函数、什么情况下调用赋值运算符函数。最后,分析了下深拷贝和浅拷贝的问题,即拷贝构造函数和赋值运算符函数的必要性和意义。

1、拷贝构造函数和赋值运算符

在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符函数(缺省的)。

class Person
{
  public:
  ...
    Person(const Person& p) = delete; 
    Person& operator=(const Person& p) = delete;

  private:
    int age;
    string name;
};

用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。上面的定义的类 Person 显式地删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示无法调用该函数,它是已删除的函数。

如果我们不想编写拷贝构造函数和赋值运算符函数,又不允许别人使用编译器隐式生成的缺省函数,同时也不想显式地删除拷贝构造函数和赋值运算符函数,我们还可以通过将拷贝构造函数和赋值运算符函数声明成类私有函数的方式来实现。如下所示:

class Person
{
  public:
  ...

  private:
    Person(const Person& p); //以常量引用的方式传递参数
    Person& operator=(const Person& p);//返回值类型为该类型的引用

    int age;
    string name;
};

还有两点需要注意的是:

  • 拷贝构造函数必须以引用的方式传递参数,基本上都是传常量引用的方式传递函数参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。

  • **赋值运算符函数的返回值类型要声明为该类型的引用,并在函数结束前返回实例自身的的引用(*this)加粗样式,只有返回一个引用,才能进行连续赋值。否则,如果函数的返回值是void,则应用改赋值运算符将不能进行连续赋值。假设有3个Person对象:p1、p2、p3,在程序中语句p1=p2=p3将不能通过编译。

2、两者分别在何时调用

拷贝构造函数和赋值运算符函数的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符函数是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符函数是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符函数,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符函数。

调用拷贝构造函数主要有以下场景:

  • 对象作为函数的参数,以值传递的方式传给函数。
  • 对象作为函数的返回值,以值的方式从函数返回。
  • 使用一个对象给另一个对象初始化。
class Person
{
  public:
    Person(){}
    Person(const Person& p)
    {
        cout << "Copy Constructor" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Assign" << endl;
        return *this;
    }

  private:
    int age;
    string name;
};

void f(Person p)
{
    return;
}

Person f1()
{
    Person p;
    return p;
}

int main()
{
    Person p;
    Person p1 = p;    // 1
    Person p2;
    p2 = p;           // 2
    f(p2);            // 3
    p2 = f1();        // 4
    Person p3 = f1(); // 5
    getchar();
    return 0;
}

上面代码中定义了一个类Person,显式地定义了拷贝构造函数和赋值运算符函数。然后定义了一个f函数,以值的方式参传入Person对象;f1函数,以值的方式返回Person对象。在main中模拟了5个中场景,测试调用的是拷贝构造函数还是赋值运算符函数。执行结果如下:
在这里插入图片描述

分析如下:

  • 这是虽然使用了”=”,但是实际上使用对象p来创建一个新的对象p1。也就是产生了新的对象,所以调用的是拷贝构造函数。
  • 首先声明一个对象p2,然后使用赋值运算符”=”,将p的值复制给p2,显然是调用赋值运算符函数,为一个已经存在的对象赋值 。
  • 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
  • 这条语句拷贝构造函数和赋值运算符函数都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符函数将临时对象tmp赋值给p2.
  • 按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。

3、深拷贝、浅拷贝:拷贝构造函数和赋值运算符函数的必要性和意义

说到拷贝构造函数,就不得不提深拷贝和浅拷贝。通常,默认生成的拷贝构造函数和赋值运算符函数,只是简单的进行值的复制。例如:上面的Person类,字段只有int和string两种类型,这在拷贝或者赋值时进行值复制创建的出来的对象和源对象也是没有任何关联,对源对象的任何操作都不会影响到拷贝出来的对象。反之,有如下一个CExample类:

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; 
}; 

CExample类的特点是包含指向其他资源的指针,即有一个对象为char *,pBuffer指向堆中分配的一段内存空间。

例1:

int main(int argc, char* argv[]) 
{ 
CExample theObjone; 
theObjone.Init40); 

//现在需要另一个对对象2:theObjtwo,需要将他初始化成对象1:theObjone的状态 
CExample theObjtwo=theObjone; 
... 
} 

由上面的分析可知,例1是使用一个对象给另一个对象初始化,所以要调用拷贝构造函数,由于没有显式地定义拷贝构造函数,故调用编译器隐式生成的缺省的拷贝构造函数,对类对象进行简单的值复制。其完成方式是内存拷贝,复制所有成员的值(包括指针的值,即地址)。完成后,theObjtwo.pBuffer==theObjone.pBuffer(地址相同)。即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。这样不符合要求,对象之间不独立了,并为空间的删除带来隐患。任何一个对象对该值的修改都会影响到另一个对象,这种情况就是浅拷贝。

为了解决这类问题,我们可以显示地在拷贝构造函数中解决指针成员的问题。

增加了显式定义拷贝构造函数后的CExample类定义为:

class CExample 
{ 
  public: 
    CExample(){pBuffer=NULL; nSize=0;} 
    ~CExample(){delete pBuffer;} 
    CExample(const CExample&); //拷贝构造函数 
    void Init(int n){ pBuffer=new char[n]; nSize=n;} 
  private: 
    char *pBuffer; //类的对象中包含指针,指向动态分配的内存资源 
    int nSize; 
};

CExample::CExample(const CExample& RightSides) //拷贝构造函数的定义 
{ 
    nSize=RightSides.nSize; //复制常规成员 
    pBuffer=new char[nSize]; //复制指针指向的内容 
    memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char)); 
} 

在显式定义了CExample类的拷贝构造函数后,在例1中就不会调用缺省的拷贝构造函数了,而是调用显式的拷贝构造函数,就避免了上述浅拷贝的问题了,这就是所谓的深拷贝。

深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,因为对于指针只是简单的值复制并不能分割开两个对象的关联(只是复制了指针的值,及地址,指针指向是同一个内存空间),任何一个对象对该指针的操作都会影响到另一个对象。这时候就需要提供自定义的深拷贝的拷贝构造函数,消除这种影响。通常的原则是:

  • 含有指针类型的成员或者有动态分配内存的成员都应该提供自定义的拷贝构造函数
  • 在提供拷贝构造函数的同时,还应该考虑实现自定义的赋值运算符函数
    例2:
int main(int argc, char* argv[]) 
{ 
    CExample theObjone; 
    theObjone.Init(40); 

    CExample theObjthree; 
    theObjthree.Init(60); 

    theObjthree=theObjone; //对一个已存在的对象赋值 
    return 0; 
} 

注:”=”号的两种不同使用,这里的”=”号是赋值符,使用默认赋值符”=”,是把被赋值对象的原内容被清除,并用右边对象的内容填充。而例1中的”=”号是在对象声明语句中,表示初始化;更多时候,这种初始化也可用括号()表示。

由上面的分析可知,这里是对一个已存在的对象实例(theObjthree)赋值,故要调用赋值运算符函数,由于没有显式地定义赋值运算符函数,故调用编译器隐式生成的缺省的赋值运算符函数。但”=”的缺省操作只是将成员变量的值相应复制。旧的值被自然丢弃。
由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放(delete)。指针的值被复制了,但指针所指内容并未复制。 即:这里的theObjthree.pBuffer 原有的内存没被释放,造成内存泄露;theObjthree.pBuffer 和theObjone.pBuffer 指向同一块内存,和theObjone 或theObjthree任何一方变动都会影响另一方;在对象被析构时,pBuffer 被释放了两次。
因此,包含动态分配成员的类除提供拷贝构造函数外,还应该考虑重载”=”赋值操作符号。

增加了显式定义赋值运算符函数后的CExample类定义为:

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 
} 

对于拷贝构造函数的实现要确保以下几点:

  • 对于值类型的成员进行值复制
  • 对于指针和动态分配的空间,在拷贝中应重新分配分配空间
  • 对于基类,要调用基类合适的拷贝方法,完成基类的拷贝
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
拷贝构造函数是用来创建一个新对象并将其初始化为给定对象的副本的特殊成员函数。它通常用于以下情况: - 当一个对象通过值传递给函数或以值的形式返回时 - 当一个对象用另一个对象进行初始化时 - 当一个对象作为另一个对象的成员进行初始化时 对于类`Person`的拷贝构造函数,它会接受一个`const Person&`类型的参数,并将其成员变量`name_`赋值给新创建的对象的`name_`成员变量。 赋值运算符是用于将一个对象的值分配给另一个已经存在的对象的成员函数。它通常用于以下情况: - 当一个对象被另一个对象赋值时 - 当一个对象作为另一个对象的成员进行赋值时 对于类`Person`的赋值运算符,它会接受一个`const Person&`类型的参数,并将其成员变量`name_`赋值给当前对象的`name_`成员变量。然后,它将返回一个指向左侧运算对象的引用,以支持连续赋值的操作。 如果在类定义中没有显式定义拷贝构造函数赋值运算符,编译器会为类生成默认的拷贝构造函数赋值运算符。此外,我们还可以使用`=default`来显式要求编译器生成合成的拷贝构造函数赋值运算符。这将使用默认的实现来完成拷贝和赋值操作。 总之,拷贝构造函数用于创建一个对象的副本,而赋值运算符用于将一个对象的值赋给另一个已经存在的对象。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++拷贝构造函数与拷贝赋值运算符](https://blog.csdn.net/xiongya8888/article/details/89424224)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值