拷贝构造函数与等号赋值

拷贝构造函数与赋值函数

1.从概念上区分:

  • 复制构造函数是构造函数
  • 赋值操作符属于操作符重载范畴,它通常是类的成员函数

2.从原型上来区分:

  • 复制构造函数原型ClassType(const ClassType &)——>无返回值
  • 赋值操作符原型ClassType& operator = (const ClassType &)——>返回值为ClassType的引用,便于连续赋值操作

3.从使用的场合来区分:
复制构造函数用于产生对象,它用于以下几个地方:
⭐ 函数参数为类的值类型时、函数返回值为类类型时以及初始化语句,例如(示例了初始化语句,函数参数与函数返回值为类的值类型时较简单,这里没给出示例)

ClassType a;         //
ClassType b(a);     //调用复制构造函数
ClassType c = a;    //调用复制构造函数
//而赋值操作符要求‘=’的左右对象均已存在,它的作用就是把‘=’右边的对象的值赋给左边的对象
ClassType e;
ClassType f;
f = e;              //调用赋值操作符

⭐复制构造函数是去完成对未初始化的存储区的初始化,而赋值操作符则是处理一个已经存在的对象

⭐对一个对象赋值,当它一次出现时,它将调用复制构造函数,以后每次出现,都调用赋值操作符。

构造函数

主要用来在创建对象时, 即为对象成员变量赋初始值

拷贝构造函数

形式为X(X&) ,用来完成基于同一类的其他对象的构建及初始化:构造函数第一个参数为自身类类型的引用,且任何额外参数都具有默认值,则此构造函数为拷贝构造函数。

class Foo
{
public:
    Foo();              //默认构造函数
    Foo(const Foo&)     //拷贝构造函数
};
  • 一个对象需要通过另外一个对象进行初始化,调用拷贝构造函数。

  • 初始化和赋值的不同含义是构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的

  • 拷贝构造函数是一种特殊构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。为啥形参必须是对该类型的引用呢?试想一下,假如形参是该类的一个实例,由于是传值参数,我们把形参复制到实参会调用拷贝构造函数,如果允许拷贝构造函数传值,就会在拷贝构造函数内调用拷贝构造函数,从而形成无休止的递归调用导致栈溢出。

构造函数、析构函数、赋值函数

构造函数、析构函数、赋值函数是每个类最基本的的函数。每个类只有一个析构函数和一个赋值函数。但是有很多构造函数(一个为复制构造函数,其他为普通构造函数。对于一个类A,如果不编写上述四个函数,c++编译器将自动为A产生四个默认的函数,即:

A(void)                               //默认无参数构造函数
A(const A &a)                         //默认复制构造函数
~A(void);                             //默认的析构函数
A & operator = (const A &a); 		//默认的赋值函数

既然能自动生成函数,为什么还需要自定义?原因之一是“默认的复制构造函数”和"默认的赋值函数“均采用”位拷贝“而非”值拷贝“

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

int main( )  
 {   
	CExample theObjone;           //对象创建,调用构造函数
	theObjone.Init(40); 
	CExample theObjtwo=theObjone; //对象复制(因初始化),调用拷贝构造函数
	//(有指针成员,使用默认拷贝构造有缺点(仅复制指针),故要用自定义的)
	CExample theObjthree;  
	theObjthree.Init(60);   
	theObjthree=theObjone;        //对象赋值操作(因非初始化),调用赋值运算符
	//(有指针成员,使用默赋值符“=”有缺点(旧指针被丢弃),故要重载运算符)
}

注意: “=” 号的两种不同使用,初始化和赋值

第三行也用到了 “=” 号,该 “=” 在对象声明语句中,表示初始化。更多时候,这种初始化也可用括号()表示。而最后一行的 “=” 号是赋值符,使用默认赋值符 “=” ,是把被赋值对象的原内容被清除,并用右边对象的内容填充。

深拷贝(值拷贝)、浅拷贝(位拷贝)

⭐位拷贝(浅拷贝)拷贝的是地址,而值拷贝(深拷贝)拷贝的是内容

  • 拷贝构造函数和赋值函数并非每个对象都会使用,另外如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。在类的设计当中,“位拷贝”是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。
    拷贝有两种:深拷贝,浅拷贝

  • 当出现类的等号赋值 时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。

  • 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。

  • 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的

  • 简而言之,当数据成员中有指针时,必须要用深拷贝。

  • 如果一个类拥有资源,当这个类的对象发生复制过程时,资源重新分配,就叫做深拷贝。

    • 为便于说明,以自定义String类为例,先定义类,而不去实现
#include <iostream>
using namespace std;

class String  
{
    public:
        String(void);
        String(const String &other);
        ~String(void);
        String & operator =(const String &other);
    private:
 
        char *m_data;
        int val;
};
  • 类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL

  • 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。(这是引用与指针的一个重要区别)。然后需要注意的就是深复制了。

  • 如果定义两个 String 对象a, b。当利用位拷贝时,a=b,其中的a.val=b.val;但是a.m_data=b.m_data就错了:a.m_data和b.m_data指向同一个区域。这样出现问题:

    • a.m_data原来的内存区域未释放,造成内存泄露
    • a.m_data和b.m_data指向同一块区域,任何一方改变,会影响到另一方
    • 当对象释放时,b.m_data会释放掉两次
      因此

当类中还有指针变量时,复制构造函数和赋值函数就隐含了错误。此时需要自己定义。

结论:

  • 赋值操作符和复制构造函数可以看成一个单元,当需要其中一个时,我们几乎也肯定需要另一个
  • 如果类需要析构函数,则它也需要赋值操作符和复制构造函数
  • 当拷贝对象状态中包含其他对象的引用时,如果需要复制的是引用对象指向的内容,而不是引用内存地址,则是深复制,否则是浅复制。
  • 浅复制就是成员数据之间的赋值,当值拷贝时,两个对象就有共同的资源。而深拷贝是先将资源复制一份,是对象拥有不同的资源(内存区域),但资源内容(内存里面的数据)是相同的。
  • 与浅复制不同,深复制在处理引用时,如果改变新对象内容将不会影响到原对象内容
  • 与深复制不同,浅复制资源后释放资源时可能会产生资源归属不清楚的情况(含指针时,释放一方的资源,其实另一方的资源也随之释放了),从而导致程序运行出错
  • 深复制和浅复制还有个区别就是执行的时候,浅复制是直接复制内存地址的,而深复制需要重新开辟同样大小的内存区域,然后复制整个资源。

注意:

  • 如果没定义复制构造函数(别的不管),编译器会自动生成默认复制构造函数
  • 如果定义了其他构造函数(包括复制构造函数),编译器绝不会生成默认构造函数
  • 即使自己写了析构函数,编译器也会自动生成默认析构函数
  • 因此此时如果写String s是错误的,因为定义了其他构造函数,就不会自动生成无参默认构造函数。
//复制构造函数  v.s.  赋值函数
#include <iostream>
#include <cstring>
using namespace std;

class String  
{
    public:
        String(const char *str);
        String(const String &other);
        String & operator=(const String &other);
        ~String(void); 
    private:
        char *m_data;
};

String::String(const char *str)
{
    cout << "自定义构造函数" << endl;
    if (str == NULL)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        int length = strlen(str);
        m_data = new char[length + 1];
        strcpy(m_data, str);
    }
}

String::String(const String &other)
{
    cout << "自定义拷贝构造函数" << endl;
    int length = strlen(other.m_data);
    m_data = new char[length + 1];
    strcpy(m_data, other.m_data);
}

String & String::operator=(const String &other)
{
    cout << "自定义赋值函数" << endl; 

    if (this == &other)    //  在赋值函数中一定要记得比较
    {
        return *this;
    }
    else
    {
        delete [] m_data;
        int length = strlen(other.m_data);
        m_data = new char[length + 1];
        strcpy(m_data, other.m_data);
        return *this;
    }
}

String::~String(void)
{
    cout << "自定义析构函数" << endl; 
    delete [] m_data;
}
int main()
{
    cout << "a(\"abc\")" << endl;
    String a("abc");

    cout << "b(\"cde\")" << endl;
    String b("cde");
    
    cout << " d = a" << endl;
    String d = a;

    cout << "c(b)" << endl;
    String c(b);

    cout << "c = a" << endl;
    c = a;

    cout << endl;

在这里插入代码片
说明几点

  1. 赋值函数中,上来比较 this == &other 是很必要的,因为防止自复制,这是很危险的,因为下面有delete []m_data,如果提前把m_data给释放了,指针已成野指针,再赋值就错了

  2. 赋值函数中,接着要释放掉m_data,否则就没机会了(下边又有新指向了)

  3. 拷贝构造函数是对象被创建时调用,赋值函数只能被已经存在了的对象调用

    注意:String a(“hello”); String b(“world”); 调用自定义构造函数

          String c=a;调用拷贝构造函数,因为c一开始不存在,最好写成String c(a);
    

相比而言,对于类String 的赋值函数则要复杂的多:

1、首先需要执行检查自赋值

这是防止自复制以及间接复制,如 b=a; c=b; a=c;之类,如果不进行自检的话,那么后面的 delete
将会进行自杀操作,后面随之的拷贝操作也会出错,所以这是关键的一步。还需要注意的是,自检是检查地址,而不是内容,内存地址是唯一的。必须是
if(this==&rhs)

2、释放原有的内存资源

必须要用 delete 释放掉原有的内存资源,如果此时不释放,该变量指向的内存地址将不再是原有内存地址,英语考级也就无法进行内存释放,造成内存泄露。

3、分配新的内存资源,并复制资源

这样变量指向的内存地址变了,但是里面的资源是一样的

4、返回本对象的引用

这样的目的是为了实现像 a=b=c; 这样的链式表达,注意返回的是 *this 。

但仔细一想,上面的程序没有考虑到异常安全性,我们在分配内存之前用delete
释放了原有实例的内存,如果后面new 出现内存不足抛出异常,那么之前delete 的 m_data
将是一个空指针,这样很容易引起程序崩溃,所以我们可以调换下顺序,即先 new 一个实例内存,成功后再用 delete
释放原有内存空间,最后用 m_data 赋值为new后的指针。

接下来说说拷贝构造函数和赋值函数之间的区别。

拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建是调用的,而赋值函数只能在已经存在了的对象调用。看下面代码:

上面说明出现“=”的地方未必调用的都是赋值函数(算术符重载函数),也有可能拷贝构造函数,那么什么时候是调用拷贝构造函数,什么时候是调用赋值函数你?判断的标准其实很简单:如果临时变量是第一次出现,那么调用的只能是拷贝构造函数,反之如果变量已经存在,那么调用的就是赋值函数。

默认拷贝构造函数(浅拷贝)

CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。

调用默认拷贝构造含, 其完成方式是内存拷贝(浅拷贝),复制所有成员的值。

完成后,theObjtwo.pBuffer==theObjone.pBuffer。 即它们将指向同样的地方,指针虽然复制了,但所指向的空间并没有复制,而是由两个对象共用了。

这样不符合要求,对象之间不独立了,并为空间的删除带来隐患(产生野指针)

自定义拷贝构造函数(深拷贝)

因此可以在构造函数中添加操作来解决指针成员的问题,即自定义的拷贝构造函数

//自定义拷贝构造函数
CExample::CExample(const CExample& RightSides)
{
nSize=RightSides.nSize; //复制常规成员
pBuffer=new char[nSize]; //复制指针指向的内容
memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}
这样,定义新对象,并用已有对象初始化新对象时,CExample(const CExample& RightSides)将被调用,而已有对象用别名RightSides传给构造函数,以用来作复制。

原则上,应该为所有包含动态分配成员的类都提供自定义的拷贝构造函数。

注意:内存拷贝函数void *memcpy(void *dest, constvoid *src, size_t n)

默认赋值操作符"="

赋值函数,也是赋值操作符重载,因为赋值必须作为类成员,那么它的第一个操作数隐式绑定到 this 指针,也就是 this 绑定到指向左操作数的指针。因此,赋值操作符接受单个形参,且该形参是同一类类型的对象。右操作数一般作为const 引用传递。

//赋值操作符重载
CExample & CExample::operator = (const CExample& RightSides)
{
nSize=RightSides.nSize; //复制常规成员
char temp=new char[nSize];//复制指针指向的内容(默认=的缺点)
memcpy(temp,RightSides.pBuffer,nSize
sizeof(char));
delete []pBuffer; //删除原指针指向内容(避免内存泄露)

pBuffer=NULL; //同时把原指针置为NULL(避免野指针)
pBuffer=temp; //建立新指向
return *this ;
}
"="的缺省(默认)操作只是将成员变量的值相应复制,旧的值被自然丢弃

最后一行的"="表示赋值操作,将对象theObjone的内容复制到对象theObjthree,这其中涉及到对象theObjthree原有内容的丢弃,新内容的复制。

由于对象内包含指针,将造成不良后果:指针的值被丢弃了,但指针指向的内容并未释放(导致内存泄露)。指针的值被复制了,但指针所指内容并未复制(导致新指针指的内容丢了)

即产生两方面的问题:丢弃旧指针(导致内存泄露),仅复制新指针(导致内容丢了)

因此,包含动态分配成员的类除了提供拷贝构造函数外,还应该考虑重载"="赋值操作符号

重载"="赋值操作符

1. 何时调用复制构造函数
  复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中,而不是常规的赋值过程中。类的复制构造函数原型通常如下:

  class_name(const class_name&);

它接受一个指向类对象的常量引用作为参数。例如,String类的复制构造函数的原型如下:

  String(const String&);

新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。这在很多情况下都可能发生,最常见的情况是将新对象显示地初始化为现有的对象。例如,假设motto是一个String对象,则下面4种声明都将调用复制构造函数:

  String ditto(motto);
  String metoo = motto;
  String also = String(motto);
  String *pString = new String(motto);

其中中间的2种声明可能会使用复制构造函数直接创建metto和also,也可能会使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给 metoo和also,这取决于具体的实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pString指针。
2. 何时调用赋值构造函数
  赋值构造函数是通过重载赋值操作符实现的,这种操作符的原型如下:

  Class_name& Class_name::operator=(const Class_name&);

它接受并返回一个指向类对象的引用。例如,String 类的赋值操作符的原型如下:

  String& String::operator=(const String&);

将已有的对象赋给另一个对象时,将使用重载的赋值操作符:

  String headline1("test");
  String knot;
  knot = headline1;
  初始化对象时,并不一定会使用赋值操作符:
  String metoo = knot;

这里,metoo是一个新创建的对象,被初始化为knot的值,此时使用复制构造函数。不过,正如前面指出的,实现时也可能分两步来处理这条语句:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说,初始化总是会调用复制构造函数,而使用=操作符时也可能调用赋值操作符。

3、总结:复制构造函数和赋值操作符的区别

同样是利用现有对象的值,生成/更新另一个对象的值。区别在于:复制构造函数是去完成对未初始化的存储区的初始化,而赋值操作符则是处理一个已经存在的对象。对一个对象赋值,当它一次出现时,它将调用复制构造函数,以后每次出现,都调用赋值操作符。

4.拷贝构造函数的标准写法如下:

class Base
{
public:
  Base(){}
  Base(const Base &b){..}
  //
}

拷贝构造函数必须是引用传递传递地址,不可以值传递,否则会陷入无限循环创建的过程!!!。

那么如果我们不写成引用传递呢,而是值传递,那么会怎样?

class Base
{
public:
  Base(){}
  Base(const Base b){}
  //
}
编译出错:error C2652: 'Base' : illegal copy constructor: first parameter must not be a 'Base'

事实上,你可以从这个小小的问题认真搞清楚2件事:

    1. 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。其实,个人认为不应该叫这些constructor (default constructor, copy constructor…)为构造函数,更佳的名字应该是"初始化函数"
    1. 参数传递过程到底发生了什么?
      将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!
    • i)值传递:

      • 对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);
      • 对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用
    • ii)引用传递:
      无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

上述1) 2)回答了为什么拷贝构造函数使用值传递会产生无限递归调用…

  • 3). 如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题了:因为默认的拷贝构造函数是按成员拷贝构造(浅拷贝),这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存(深拷贝)。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
构造函数、赋值运算符函数和拷贝构造函数都是 C++ 中的重要概念,它们的主要功能和区别如下: 1. 构造函数 构造函数是一种特殊的成员函数,用于创建对象时初始化对象的数据成员。它的主要功能是用来初始化对象的状态。在创建对象时,构造函数自动调用,初始化对象中的成员变量。 2. 赋值运算符函数 赋值运算符函数是一种成员函数,用于将一个对象的值赋给另一个对象。它的主要功能是实现对象之间的赋值操作。赋值运算符函数通常以等号“=”作为符号,例如: ``` class MyClass { public: MyClass& operator=(const MyClass& other) { // 实现赋值操作 } }; ``` 3. 拷贝构造函数 拷贝构造函数是一种特殊的构造函数,用于将一个对象的值复制给另一个对象。它的主要功能是在创建一个新的对象时,使用已有的对象的值来初始化新对象。拷贝构造函数通常以 const 引用作为参数,例如: ``` class MyClass { public: MyClass(const MyClass& other) { // 实现拷贝操作 } }; ``` 区别: 1. 形式不同 构造函数是一种特殊的成员函数,没有返回值,函数名与类名相同,用于初始化对象的状态。赋值运算符函数和拷贝构造函数都是成员函数,但它们的函数名与类名不同,且有返回值。 2. 功能不同 构造函数用于创建对象时初始化对象的数据成员,赋值运算符函数用于将一个对象的值赋给另一个对象,拷贝构造函数用于将一个对象的值复制给另一个对象。 3. 调用时机不同 构造函数在创建对象时自动调用赋值运算符函数在对象赋值时自动调用拷贝构造函数在创建新对象时自动调用。 总的来说,构造函数、赋值运算符函数和拷贝构造函数都是 C++ 中重要的概念,用于实现对象的创建、赋值和复制等操作。需要根据具体的需求来选择使用不同的函数。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值