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

本文深入探讨C++中的复制构造函数,解析其在对象复制、函数参数传递和返回值时的作用,以及如何避免深拷贝与浅拷贝引发的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

<think>好的,我现在需要回答用户关于C++复制构造函数赋值运算符的区别及实现示例的问题。首先,我需要回顾一下用户提供的引用内容,确保回答准确并正确引用。 用户提到了五个引用,其中引用[1]、[2]、[3]、[5]都涉及复制构造函数赋值运算符的定义、使用场景示例。引用[4]关于运算符重载的语法,可能对解释赋值运算符的实现有帮助。 首先,我要明确复制构造函数赋值运算符的基本概念。复制构造函数用于创建一个新对象作为现有对象的副本,通常在对象初始化时调用,比如通过另一个对象初始化新对象,或者以值传递参数时。而赋值运算符用于将已有对象的值赋给另一个已经存在的对象,发生在赋值操作时。 根据引用[5],复制构造函数的声明形式是class_name(const class_name &old_obj),而赋值运算符用于已存在的对象。引用[3]中的代码示例展示了如何实现这两个函数:复制构造函数在构造时分配新内存并复制数据,而赋值运算符需要检查自赋值,释放旧数据再复制。 接下来需要区分两者的调用时机。例如,当使用一个对象初始化另一个对象时(如MyClass a(b);或MyClass a = b;),会调用复制构造函数;而如果对象已经存在(如a = b;),则调用赋值运算符。这点在引用[2]中有提到,用户可能容易混淆两者的触发场景。 然后要考虑实现上的不同。复制构造函数因为是构造函数,没有返回值,而赋值运算符返回当前对象的引用以支持链式赋值。此外,赋值运算符必须处理自赋值的情况,避免释放资源后再次访问,如引用[3]中的示例所示,使用了if (this != &other)的判断。 可能还需要提到深拷贝与浅拷贝的问题。默认情况下,编译器生成的复制构造函数赋值运算符执行浅拷贝,这在有动态内存分配时会导致问题,如引用[3]中的例子使用int* data,所以需要手动实现深拷贝。 用户可能还关心何时需要自定义这两个函数。根据引用[3],当类管理动态资源时,必须自定义复制构造函数赋值运算符,以避免浅拷贝导致的双重释放或内存泄漏。 最后,总结区别时,可以列出触发场景、函数声明、返回值、自赋值处理等要点。同时,给出具体的代码示例,帮助用户直观理解两者的不同。 现在需要确保回答结构清晰,分点说明区别,并提供示例代码,正确引用相关文献。同时,根据用户要求,生成相关问题。</think>### C++复制构造函数赋值运算符的区别及实现示例 #### 一、核心区别 1. **触发场景** - **复制构造函数**:在**创建新对象**时使用现有对象初始化,例如: ```cpp MyClass a(b); // 直接初始化 MyClass a = b; // 拷贝初始化(本质仍是构造函数) ``` - **赋值运算符**:在**对象已存在**时赋值,例如: ```cpp MyClass a; a = b; // 赋值操作 ``` 2. **函数声明** - 复制构造函数:`ClassName(const ClassName& other)`[^5] - 赋值运算符:`ClassName& operator=(const ClassName& other)`[^4] 3. **返回值** - 复制构造函数无返回值(构造函数特性)。 - 赋值运算符需返回`*this`(支持链式赋值,如`a = b = c`)[^3]。 4. **自赋值处理** - 赋值运算符必须检查`if (this != &other)`,避免释放自身资源后再次访问[^3]。 --- #### 二、代码示例 ```cpp class MyClass { private: int* data; public: // 构造函数 MyClass(int value) { data = new int(value); } // 复制构造函数(深拷贝) MyClass(const MyClass& other) { data = new int(*other.data); // 分配新内存并复制值 } // 赋值运算符(深拷贝) MyClass& operator=(const MyClass& other) { if (this != &other) { // 防止自赋值 delete data; // 释放旧资源 data = new int(*other.data); // 分配新内存并复制值 } return *this; // 支持链式赋值 } // 析构函数 ~MyClass() { delete data; } }; ``` --- #### 三、关键应用场景 1. **复制构造函数** - 对象作为函数参数**按值传递**时。 - 函数返回对象时(部分编译器优化可能跳过调用)[^2]。 2. **赋值运算符** - 显式赋值操作(如`a = b;`)。 - 容器操作(如`std::vector`的`push_back`后调整容量时重新赋值)[^1]。 --- #### 四、默认行为的风险 若类包含**动态内存**或**资源句柄**(如文件指针),编译器生成的默认拷贝函数会执行浅拷贝,导致: 1. 多个对象指向同一内存。 2. 析构时重复释放内存(引发崩溃)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值