一文说尽C++赋值运算符重载函数(operator=)

写在前面:ios

      关于C++的赋值运算符重载函数(operator=),网络以及各类教材上都有不少介绍,但惋惜的是,内容大多雷同且不全面。面对这一局面,在下在整合各类资源及融入我的理解的基础上,整理出一篇较为全面/详尽的文章,以飨读者。网络

正文:函数

.举例this

例1spa

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

结果:指针

.参数code

通常地,赋值运算符重载函数的参数是函数所在类的const类型的引用(如上面例1),加const是由于对象

①咱们不但愿在这个函数中对用来进行赋值的“原版”作任何修改。blog

②加上const,对于const的和非const的实参,函数就能接受;若是不加,就只能接受非const的实参。继承

用引用是由于

这样能够避免在函数调用时对实参的一次拷贝,提升了效率。

注意

上面的规定都不是强制的,能够不加const,也能够没有引用,甚至参数能够不是函数所在的对象,正如后面例2中的那样。

.返回值

通常地,返回值是被赋值者的引用,即*this(如上面例1),缘由是

①这样在函数返回时避免一次拷贝,提升了效率。

②更重要的,这样能够实现连续赋值,即相似a=b=c这样。若是不是返回引用而是返回值类型,那么,执行a=b时,调用赋值运算符重载函数,在函数返回时,因为返回的是值类型,因此要对return后边的“东西”进行一次拷贝,获得一个未命名的副本(有些资料上称之为“匿名对象”),而后将这个副本返回,而这个副本是右值,因此,执行a=b后,获得的是一个右值,再执行=c就会出错。

注意

这也不是强制的,咱们能够将函数返回值声明为void,而后什么也不返回,只不过这样就不可以连续赋值了。

.调用时机

      当为一个类对象赋值(注意:能够用本类对象为其赋值(如上面例1),也能够用其它类型(如内置类型)的值为其赋值,关于这一点,见后面的例2)时,会由该对象调用该类的赋值运算符重载函数。

如上边代码中

str2 = str1;

一句,用str1为str2赋值,会由str2调用MyStr类的赋值运算符重载函数。

须要注意的是

MyStr str2;

str2 = str1;

MyStr str3 = str2;

在调用函数上是有区别的。正如咱们在上面结果中看到的那样。

      前者MyStr str2;一句是str2的声明加定义,调用无参构造函数,因此str2 = str1;一句是在str2已经存在的状况下,用str1来为str2赋值,调用的是拷贝赋值运算符重载函数;然后者,是用str2来初始化str3,调用的是拷贝构造函数。

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

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

见下面的例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),会调用赋值运算符重载函数。

若是只有匹配的构造函数存在,就会调用这个构造函数。

.显式提供赋值运算符重载函数的时机

用非类A类型的值为类A的对象赋值时(固然,从Ⅵ中能够看出,这种状况下咱们能够不提供相应的赋值运算符重载函数而只提供相应的构造函数来完成任务)。

当用类A类型的值为类A的对象赋值且类A的成员变量中含有指针时,为避免浅拷贝(关于浅拷贝和深拷贝,下面会讲到),必须显式提供赋值运算符重载函数(如例1)。

.浅拷贝和深拷贝

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

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

      假设咱们将例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各自独立,避免了上面两个致命错误。

      咱们是以拷贝构造函数为例说明的,赋值运算符重载函数也是一样的道理。

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

       C++规定,赋值运算符重载函数只能是类的非静态的成员函数,不能是静态成员函数,也不能是友元函数。关于缘由,有人说,赋值运算符重载函数每每要返回*this,而不管是静态成员函数仍是友元函数都没有this指针。这乍看起来颇有道理,但仔细一想,咱们彻底能够写出这样的代码

static friend MyStr& operator=(const MyStr str1,const MyStr str2)
{
    ……
    return str1;
}

      可见,这种说法并不能揭露C++这么规定的缘由。

      其实,之因此不是静态成员函数,是由于静态成员函数只能操做类的静态成员,不能操做非静态成员。若是咱们将赋值运算符重载函数定义为静态成员函数,那么,该函数将没法操做类的非静态成员,这显然是不可行的。

      在前面的讲述中咱们说过,当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动提供一个。如今,假设C++容许将赋值运算符重载函数定义为友元函数而且咱们也确实这么作了,并且以类的引用为参数。与此同时,咱们在类内却没有显式提供一个以本类或本类的引用为参数的赋值运算符重载函数。因为友元函数并不属于这个类,因此,此时编译器一看,类内并无一个以本类或本类的引用为参数的赋值运算符重载函数,因此会自动提供一个。此时,咱们再执行相似于str2=str1这样的代码,那么,编译器是该执行它提供的默认版本呢,仍是执行咱们定义的友元函数版本呢?

       为了不这样的二义性,C++强制规定,赋值运算符重载函数只能定义为类的成员函数,这样,编译器就可以断定是否要提供默认版本了,也不会再出现二义性。

. 赋值运算符重载函数不能被继承

见下面的例3

#include<iostream>
#include<string>
using namespace std;

class A
{
public:
    int X;
    A() {}
    A& operator =(const int x)
    {
        X = x;
        return *this;
    }    
};
class B :public A
{
public:
    B(void) :A() {}
};
int main() { A a; B b; a = 45; //b = 67; (A)b = 67; return 0; }

      注释掉的一句没法编译经过。报错提示:没有与这些操做数匹配的”=”运算符。对于b = 67;一句,首先,没有可供调用的构造函数(前面说过,在没有匹配的赋值运算符重载函数时,相似于该句的代码能够调用匹配的构造函数),此时,代码不能编译经过,说明父类的operator =函数并无被子类继承。

     为何赋值运算符重载函数不能被继承呢?

     由于相较于基类,派生类每每要添加一些本身的数据成员和成员函数,若是容许派生类继承基类的赋值运算符重载函数,那么,在派生类不提供本身的赋值运算符重载函数时,就只能调用基类的,但基类版本只能处理基类的数据成员,在这种状况下,派生类本身的数据成员怎么办?

     因此,C++规定,赋值运算符重载函数不能被继承。

    上面代码中, (A)b = 67; 一句能够编译经过,缘由是咱们将B类对象b强制转换成了A类对象。

Ⅺ.赋值运算符重载函数要避免自赋值

      对于赋值运算符重载函数,咱们要避免自赋值状况(即本身给本身赋值)的发生,通常地,咱们经过比较赋值者与被赋值者的地址是否相同来判断二者是不是同一对象(正如例1中的if (this != &str)一句)。

     为何要避免自赋值呢?

 ①为了效率。显然,本身给本身赋值彻底是毫无心义的无用功,特别地,对于基类数据成员间的赋值,还会调用基类的赋值运算符重载函数,开销是很大的。若是咱们一旦断定是自赋值,就当即return *this,会避免对其它函数的调用。

若是类的数据成员中含有指针,自赋值有时会致使灾难性的后果。对于指针间的赋值(注意这里指的是指针所指内容间的赋值,这里假设用_p给p赋值),先要将p所指向的空间delete掉(为何要这么作呢?由于指针p所指的空间一般是new来的,若是在为p从新分配空间前没有将p原来的空间delete掉,会形成内存泄露),而后再为p从新分配空间,将_p所指的内容拷贝到p所指的空间。若是是自赋值,那么p和_p是同一指针,在赋值操做前对p的delete操做,将致使p所指的数据同时被销毁。那么从新赋值时,拿什么来赋?

      因此,对于赋值运算符重载函数,必定要先检查是不是自赋值,若是是,直接return *this。

结束语:

      至此,本文的全部内容都介绍完了。因为在下才疏学浅,错误纰漏之处在所不免,若是您在阅读的过程当中发现了在下的错误和不足,请您务必指出。您的批评指正就是在下前进的不竭动力! 

相关文章
相关标签/搜索