C++:类的六个默认函数之一 —— 赋值重载


运算符重载概念

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符
    比如:operator@

  • 重载操作符必须有一个类类型或者枚举类型的操作数

  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义 ,内置类型语言已经定义了其逻辑功能,不能对其做改变。运算符重载相当于在自定义类型中定义另一种逻辑。

  • 作为类成员的重载函数时,其形参看起来比操作数数目少1个,成员函数的操作符有一个默认的形参this,限定为第一个形参。

  • 注意以下的运算符不能重载

1. 作用域操作符:::

2. 条件操作符: ?  :  

3. 成员访问操作符:.

4. 指向成员操作的指针操作符:.*

5. 长度运算符:sizeof
    
..* 运算符不能重载是为了保证访问成员的功能不能被改变,
域运算符和sizeof运算符的运算对象是类型而不是变量或一般表达式,不具备重载的特征。

演示代码:

#include <iostream>
using namespace std;

// 全局的operator== 
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //private:
    int _year;
    int _month;
    int _day;
};

// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证? 
// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

void Test() {
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
}

int main() {
    Test();	// 输出 0

    return 0;
}
#include <iostream>
using namespace std;

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // bool operator==(const Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this指向的调用函数的对象;在函数末尾加const相当于对this加const
    bool operator==(const Date& d2)const
    {
        return _year == d2._year && _month == d2._month && _day == d2._day;
    }
    private:
        int _year;
        int _month;
        int _day;
};

void Test() {
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
}

int main() {
    Test();	// 输出 0

    return 0;
}

两者的区别:

  1. 全局函数运算符需要几个参数就得输入几个参数。

  2. 成员函数运算符个数时所需参数再减1,其中第一个参数被this指针所代替。

赋值运算符重载

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

String  a(“hello”);
String  b(“world”);
String  c = a;	// 调用了拷贝构造函数,最好写成 c(a);
		c = b; 	// 调用了赋值函数

本例中第三个语句的风格较差,宜改写成String c(a) 以区别于第四个语句。

重载赋值运算符要点

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    Date(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
    Date& operator=(const Date& d)
    {
        if (this != &d)	// 仅仅减少运算量,没有也没太大关系
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }
		return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};
  1. 返回值的类型需为该类型的引用,并在函数结束前 返回实例自身的引用( *this )参数类型。(只有返回一个引用,才可以连续赋值)
  2. 传入的参数类型需为常量引用(提高代码效率,避免调用拷贝构造函数),参数还应加上 const 关键字(赋值运算符函数内不会改变传入参数的状态)
  3. 检测是否自己给自己赋值会在释放自身内存的时候出现严重的问题:如果传入的参数与*this 是同一个实例,一旦释放了自身的内存,传入的参数的内存也同时被释放了)
  4. 是否释放实例自身已有的内存(如果忘记释放已有的空间,程序会出现内存泄漏)
  5. 分配新的内存资源,并复制字符串。注意函数strlen返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy则连‘\0’一起复制。
  6. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝(浅拷贝)。
#include <iostream>
using namespace std;

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void print() {
        cout << _year << ' ' << _month << ' ' << _day << endl;
    }
};

int main() {
    Date d1;
    Date d2(2018, 10, 1);

    // 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。 
    d1 = d2;
    d1.print();	//输出 2018 10 1
    return 0;
}

编译器生成的默认赋值重载函数可以完成字节序的值拷贝了,但是这样的拷贝只是 浅拷贝

默认赋值运算符针对String类引发浅拷贝问题: 点击进入——>深拷贝

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String {
public:
    String(const char* str = "jack") {
        _str = (char*)malloc(strlen(str) + 1);
        strcpy(_str, str);
    }
 
    ~String() {
        cout << "~String()" << endl;
        free(_str);
    }
private:
    char* _str;
};
 
int main() {
    String s1("hello");
    String s2("world");
    
    s1 = s2;

	return 0;
}

有申请内存空间资源的操作,需要自己定义这两个成员函数。没有的话,若采用编译器默认生成的函数使用:

  • 会产生对同一块内存重复析构的问题
  • 对 s1 原有的内存未释放,造成内存泄漏。

要点总结

  1. 类会自动提供一个赋值运算符的重载,执行的是浅拷贝,跟拷贝构造相同
  2. 浅拷贝:直接复制内存
  3. 深拷贝:当成员中有指向堆的指针,就必须重新给指针分配空间,然后将目标对象指针所指空间的内容拷贝到新分配的空间。(如果不这样做,会导致两个指针指向同一片空间,从而在析构中多次释放,导致出现错误)

实现赋值运算符重载

  • 将运算符看成函数,把他的几目当成参数,通过参数的类型识别出对应的操作方法,相当于函数重载。
  • 运算符重载有制定的规则,规则根据运算符来制定。
// 面试题1:赋值运算符函数
// 题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。

#include<cstring>
#include<cstdio>

class CMyString
{
public:
    CMyString(char* pData = nullptr);
    CMyString(const CMyString& str);
    ~CMyString(void);

    CMyString& operator = (const CMyString& str);

    void Print();
private:
    char* m_pData;
};

CMyString::CMyString(char *pData)
{
    if(pData == nullptr)
    {
        m_pData = new char[1];
        m_pData[0] = '\0';
    }
    else
    {
        int length = strlen(pData);
        m_pData = new char[length + 1];
        strcpy(m_pData, pData);
    }
}

CMyString::CMyString(const CMyString &str)
{
    int length = strlen(str.m_pData);
    m_pData = new char[length + 1];
    strcpy(m_pData, str.m_pData);
}

CMyString::~CMyString()
{
    delete[] m_pData;
}

CMyString& CMyString::operator = (const CMyString& str)
{
    if(this == &str)
        return *this;

    delete []m_pData;
    m_pData = nullptr;

    m_pData = new char[strlen(str.m_pData) + 1];
    strcpy(m_pData, str.m_pData);

    return *this;
}

void CMyString::Print()
{
    printf("%s", m_pData);
}

考虑异常安全性的解法

在前面的函数中,我们在分配内存之前先用 delete 释放了实例 m_pData 的内存。如果此时内存不足导致 new char 抛出异常,则 m_pData 将是一个空指针,这样很容易导致程序崩溃。 也就是说一旦在赋值运算符函数内不抛出一个异常, CMyString的实例不再保持有效的状态,这就违背了异常安全性原则。(不泄漏任何资源、不破坏数据)

​ 要想在赋值运算符中实现异常安全性,我们有两种方法,一个简单的办法是我们先用new分配新内容再用delete释放已有的内容。这样只在分配内容成功之后再释放原来的内容,也就是当分配内存失败时我们能确保CMyString的实例不会被修改。我们还有一个更好的办法,先创建一个临时实例,在交换临时实例和原来的实例。见下面的代码:

CMyString& CMyString::operator =(const CMyString& str)
{
    if(this != &str)
    {
        CMyString strTemp(str);
        
        char *pTemp = strTemp.m_pData;
        strTemp.m_pData = m_pData;
        m_pData = pTemp;
    }
    return *this;
}

在上面的函数中,我们先创建一个临时变量strTemp, 接着把strTemp.m_pData和实例自身的m_pData做交换。由于strTemp是一个局部变量,但程序运行到if的外面时也就出了该变量的作用域, 就会自动调用strTemp的析构函数,把strTemp.m_pData所指向的之前的m_pData的内存,这就相当于自动调用析构函数释放实例的内存。

​ 在新的代码中,我们在CMyString的构造函数里用new分配内存。如果由于内存不足抛出诸如bad_alloc等异常,我们还没有修改原来实例的状态,因此实例的状态还是有效的,这也就保证了异常安全性。


如有不同见解,欢迎留言讨论~~~

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值