C++ 拷贝构造函数与赋值函数的区别(很严谨和全面)

转载自:https://blog.csdn.net/wenqian1991/article/details/29178649
写得很全面,例子也通俗易懂。


这里我们用类String 来介绍这两个函数:

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

string(const string &s);
//类成员,无返回值

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

string& operator=(const string &s);
//类成员,返回对同一类类型(左操作数)的引用

拷贝构造函数和赋值函数并非每个对象都会使用,另外如果不主动编写的话,编译器将以“位拷贝”的方式自动生成缺省的函数。在类的设计当中,“位拷贝”是应当防止的。倘若类中含有指针变量,那么这两个缺省的函数就会发生错误。这就涉及到深复制和浅复制的问题了。
拷贝有两种:深拷贝,浅拷贝
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。
但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。指向不同的内存空间,但内容是一样的
简而言之,当数据成员中有指针时,必须要用深拷贝。

class A{
    char * c;
}a, b;

//浅复制不会重新分配内存
//将a 赋给 b,缺省赋值函数的“位拷贝”意味着执行
a.c = b.c;
//从这行代码可以看出
//b.c 原有的内存没有释放
//a.c 和 b.c 指向同一块内存,任何一方的变动都会影响到另一方
//对象析构的时候,c 被释放了两次(a.c == b.c 指针一样)

//深复制需要自己处理里面的指针
class A{
    char *c;
    A& operator =(const A &b)
    {
        //隐含 this 指针
        if (this == &b)
            return *this;
        delete c;//释放原有内存资源

        //分配新的内存资源
        int length = strlen(b.c);
        c = new char[length + 1];
        strcpy(c, b.c);

        return *this;
    }
}a, b;
//这个是深复制,它有自定义的复制函数,赋值时,对指针动态分配了内存

这里再总结一下深复制和浅复制的具体区别:

  1. 当拷贝对象状态中包含其他对象的引用时,如果需要复制的是引用对象指向的内容,而不是引用内存地址,则是深复制,否则是浅复制。
  2. 浅复制就是成员数据之间的赋值,当值拷贝时,两个对象就有共同的资源。而深拷贝是先将资源复制一份,是对象拥有不同的资源(内存区域),但资源内容(内存里面的数据)是相同的。
  3. 与浅复制不同,深复制在处理引用时,如果改变新对象内容将不会影响到原对象内容
  4. 与深复制不同,浅复制资源后释放资源时可能会产生资源归属不清楚的情况(含指针时,释放一方的资源,其实另一方的资源也随之释放了),从而导致程序运行出错

深复制和浅复制还有个区别就是执行的时候,浅复制是直接复制内存地址的,而深复制需要重新开辟同样大小的内存区域,然后复制整个资源。

好,有了前面的铺垫,下面开始讲讲拷贝构造函数和赋值函数,其实前面第一部分也已经介绍了许多

这里以string 类为例来进行说明

class String
{
public:
    String(const char *str = NULL);
    String(const String &rhs);
    String& operator=(const String &rhs);
    ~String(void){
        delete[] m_data;
    }

private:
    char *m_data;
};

//构造函数
String::String(const char* str)
{
    if (NULL == str)
    {
        m_data = new char[1];
        *m_data = '\0';
    }
    else
    {
        m_data = new char[strlen(str) + 1];
        strcpy(m_data, str);
    }
}

//拷贝构造函数,无需检验参数的有效性
String::String(const String &rhs)
{
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);
}

//赋值函数
String& String::operator=(const String &rhs)
{
    if (this == &rhs)
        return *this;

    delete[] m_data; m_data = NULL;
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data, rhs.m_data);

    return *this;
}

类String 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。(这是引用与指针的一个重要区别)。然后需要注意的就是深复制了。
相比而言,对于类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后的指针。

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

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

    String a("hello");
    String b("world");

    String c = a;//这里c对象被创建调用的是拷贝构造函数
                 //一般是写成 c(a);这里是与后面比较
    c = b;//前面c对象已经创建,所以这里是赋值函数

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

你可能感兴趣的:(语言——C++——基础,Java转C++之路——C++,新人学习笔记,C++,拷贝构造函数,赋值函数)