[崩溃时间]C/C++深拷贝、浅拷贝构造、赋值运算符重载的一系列崩溃时间(全解 参数、返回值类型等)

大量文本警告!!!干货警告!!!我崩溃了一下午的收获…/心累

大量C/C++、数据结构电子书籍资料以及重难点总结请看公众号:同年新月

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

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

·系统内置的拷贝构造函数和赋值运算符缺陷:(-》重载“=”问题:为何有指针成员时一定要重载?)(浅拷贝与深拷贝 见附录1)

当存在指针成员时,进行拷贝构造操作时,会使两个对象的指针成员指向同一处空间(new 出来的空间),析构时会对同一处空间进行释放两次。
当存在指针成员时,进行对象赋值运算,相当于将右值完全不变地赋值给左值,这时不仅使两个对象的指针成员指向同一处空间,析构时会对同一处空间进行析构两次;同时左值原来的指针成员指向的空间(new 出来的空间)也没有被释放,会出现内存泄漏问题!

·补充一个:为什么建议拷贝构造函数的参数是const Class& c;

先说为什么是引用,拷贝构造函数就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。如果参数采用的是值传递方法,那么必然涉及将实参传递给形参,需要对形参进行拷贝构造,而我们正在编写拷贝构造,这样不就矛盾了吗。
再说为什么写const,因为一些函数会返回对象类型,这些临时对象默认是const Class型的,如果实现像Class c3=c1+c2;(假设c1,c2是定义好的Class类对象,+已经重载,返回值为Class类型),就会将c1+c2返回的临时对象(const Class型)拷贝给c3,如果拷贝构造函数形参是Class& 型,就会将const Class型赋给Class&型,这样显然是错误的。

-》》》再谈赋值运算符

好了,说过一些浅层次的赋值运算符重载的应用问题了,再谈谈深层次的。

一、为什么函数的形参要是const Class&

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是因为:
①我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。
②加上const,对于const的和非const的实参,函数就能接受;如果不加,就只能接受非const的实参。
③个人感觉这是关键:因为进行赋值运算时会很容易出现将一个临时对象赋值给对象的状况,例如c3=c1+c2;,因为临时对象是const型,所以形参一定要是const型才能接受实参。
用引用是因为:
这样可以避免在函数调用时对实参的一次拷贝,提高了效率。

二、重载函数的返回值类型是不是引用与链式运算没有关系!

返回值不是引用时,将返回一个临时对象(const Class 型),这时你=的重载函数的形参类型是const Class&型的,可以接受const Class型,所以进行连续的赋值运算是正确的,可见返回值类型引用与否不影响连续赋值。

三、赋值运算符重载函数要避免自赋值

对于赋值运算符重载函数,我们要避免自赋值情况(即自己给自己赋值)的发生,一般地,我们通过比较赋值者与被赋值者的地址是否相同来判断两者是否是同一对象(if (this != &str))。
为什么要避免自赋值呢?
①为了效率。显然,自己给自己赋值完全是毫无意义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。如果我们一旦判定是自赋值,就立即return *this,会避免对其它函数的调用。
②如果类的数据成员中含有指针,自赋值有时会导致灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为什么要这么做呢?因为指针p所指的空间通常是new来的,如果在为p重新分配空间前没有将p原来的空间delete掉,会造成内存泄露),然后再为p重新分配空间,将_p所指的内容拷贝到p所指的空间。如果是自赋值,那么p和_p是同一指针,在赋值操作前对p的delete操作,将导致p所指的数据同时被销毁。那么重新赋值时,拿什么来赋?
所以,对于赋值运算符重载函数,一定要先检查是否是自赋值,如果是,直接return *this。

四、提供默认赋值运算符重载函数的时机

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。注意我们的限定条件,不是说只要程序中有了显式的赋值运算符重载函数,编译器就一定不再提供默认的版本,而是说只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。可见,所谓默认,就是“以本类或本类的引用为参数”的意思。
见下面的例2

#include<iostream>#include<string>using namespace std;
​
​
class Data{private:int data;public:Data() {};Data(int _data):data(_data){
​
       cout << "constructor" << endl;}
​
   Data& operator=(const int _data){
​
       cout << "operator=(int _data)" << endl;
​
       data = _data;return *this;}};int main()
{
​
   Data data1(1);
​
   Data data2,data3;
​
   cout << "=====================" << endl;
​
   data2 = 1;
​
   cout << "=====================" << endl;
​
   data3 = data2;return 0;}

结果:
在这里插入图片描述
上面的例子中,我们提供了一个带int型参数的赋值运算符重载函数,data2 = 1;一句调用了该函数,如果编译器不再提供默认的赋值运算符重载函数,那么,data3 = data2;一句将不会编译通过,但我们看到事实并非如此。所以,这个例子有力地证明了我们的结论。
构造函数还是赋值运算符重载函数?
如果我们将上面例子中的赋值运算符重载函数注释掉,main函数中的代码依然可以编译通过。只不过结论变成了
在这里插入图片描述
可见,当用一个非类A的值(如上面的int型值)为类A的对象赋值时
①如果匹配的构造函数和赋值运算符重载函数同时存在(如例2),会调用赋值运算符重载函数。
②如果只有匹配的构造函数存在,就会调用这个构造函数。

五、赋值运算符重载函数只能是类的非静态的成员函数

C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于原因,有人说,赋值运算符重载函数往往要返回*this,而无论是静态成员函数还是友元函数都没有this指针。这乍看起来很有道理,但仔细一想,我们完全可以写出这样的代码

static friend MyStr& operator=(const MyStr str1,const MyStr str2)

{

   ……

   return str1;

}

可见,这种说法并不能揭露C++这么规定的原因。
其实,之所以不是静态成员函数,是因为静态成员函数只能操作类的静态成员,不能操作非静态成员。如果我们将赋值运算符重载函数定义为静态成员函数,那么,该函数将无法操作类的非静态成员,这显然是不可行的。
在前面的讲述中我们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。现在,假设C++允许将赋值运算符重载函数定义为友元函数并且我们也确实这么做了,而且以类的引用为参数。与此同时,我们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。由于友元函数并不属于这个类,所以,此时编译器一看,类内并没有一个以本类或本类的引用为参数的赋值运算符重载函数,所以会自动提供一个。此时,我们再执行类似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,还是执行我们定义的友元函数版本呢?
为了避免这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就能够判定是否要提供默认版本了,也不会再出现二义性。

六、为什么赋值运算符重载函数不能被继承呢?

因为相较于基类,派生类往往要添加一些自己的数据成员和成员函数,如果允许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供自己的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种情况下,派生类自己的数据成员怎么办?所以,C++规定,赋值运算符重载函数不能被继承。

·附录1 浅拷贝与深拷贝

拷贝构造函数和赋值运算符重载函数都会涉及到这个问题。

所谓浅拷贝,就是说编译器提供的默认的拷贝构造函数和赋值运算符重载函数,仅仅是将对象a中各个数据成员的值拷贝给对象b中对应的数据成员(这里假设a、b为同一个类的两个对象,且用a拷贝出b或用a来给b赋值),而不做其它任何事。

例一

#include<iostream>
#include<string>
using namespace std;class MyStr
{
private:
    char *name;
    int id;
public:
    MyStr() {}
    MyStr(int _id, char *_name)   //constructor
    {
        cout << "constructor" << endl;
        id = _id;
        name = new char[strlen(_name) + 1];
        strcpy_s(name, strlen(_name) + 1, _name);
    }
    MyStr(const MyStr& str)
    {
        cout << "copy constructor" << endl;
        id = str.id;
        if (name != NULL)
            delete[] name;
        name = new char[strlen(str.name) + 1];
        strcpy_s(name, strlen(str.name) + 1, str.name);
    }
    MyStr& operator =(const MyStr& str)//赋值运算符
    {
        cout << "operator =" << endl;
        if (this != &str)
        {
            if (name != NULL)
                delete[] name;
            this->id = str.id;
            int len = strlen(str.name);
            name = new char[len + 1];
            strcpy_s(name, strlen(str.name) + 1, str.name);
        }
        return *this;
    }
    ~MyStr()
    {
        delete[] name;
    }
};int main()
{
    MyStr str1(1, "hhxx");
    cout << "====================" << endl;
    MyStr str2;
    str2 = str1;
    cout << "====================" << endl;
    MyStr str3 = str2;
    return 0;
}

假设我们将例1中显式提供的拷贝构造函数注释掉,然后同样执行MyStr str3 = str2;语句,此时调用默认的拷贝构造函数,它只是将str2的id值和nane值拷贝到str3,这样,str2和str3中的name值是相同的,即它们指向内存中的同一区域(在例1中,是字符串”hhxx”)。如下图
在这里插入图片描述
这样,会有两个致命的错误
①当我们通过str2修改它的name时,str3的name也会被修改!
②当执行str2和str3的析构函数时,会导致同一内存区域释放两次,程序崩溃!
这是万万不可行的,所以我们必须通过显式提供拷贝构造函数以避免这样的问题。就像我们在例1中做的那样,先判断被拷贝者的name是否为空,若否,delete[] name(后面会解释为什么要这么做),然后,为name重新申请空间,再将拷贝者name中的数据拷贝到被拷贝者的name中。执行后,如图
在这里插入图片描述
这样,str2.name和str3.name各自独立,避免了上面两个致命错误。
我们是以拷贝构造函数为例说明的,赋值运算符重载函数也是同样的道理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值