• operator=中自我赋值和异常安全问题


    导言

    我们在类中重载赋值运算符(operatro =)时会出现自我赋值异常安全的问题,下面就来逐步解决这两个问题。

    问题出现

    自我赋值

    引例

    自我赋值发生在对象被赋值给自己时:

    class Person { ... };
    Person p;
    ...
    p = p;
    
    • 1
    • 2
    • 3
    • 4

    这看起来有点蠢,但毫无疑问它是合法的,我们也不要认定这种事情决不会发生。

    此外赋值动作并不总是那么可被一眼辨识出来,例如:

    arr[i] = arr[j]	// 潜在的自我赋值
        
    *pa = *pj		// 指针指向引起的自我赋值
        
    int p = 0;		// 引用引起的自我赋值
    int& rp = p;
    ...
    p = rp;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果我们写出下面这样的代码:

    class Teacher { ... };
    class Student
    {
        ...
    private:
        Teacher* t;
    };
    
    
    Student& Student::operator=(const Student& rt)
    {
        delete t;
        t = new Teacher(*(rt.t));
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这里的自我赋值问题是,opreator=函数的*this(赋值目的端)和 rt有可能是同一个对象。如果这样delete就不只是销毁当前对象的Teacher它也销毁rt的Teacher。在函数最后,Student本不会因为自我赋值而改变,现在却持有一个指针指向一个已被删除的对象。

    方法

    传统的解决方法就是在operator最前面加一个检测达到自我赋值的检验目的:

    Student& Student::operator=(const Student& rt)
    {
        // 这种检测的方法需要保证Student与Teacher是一对一的关系
        // 即不能出现不同Stu的指针指向相同Tea的情况,否则也会引起上述问题
        // if (this != &rt)
        
        if (this->t != rt.t)
        {
            delete t;
        	t = new Teacher(*(rt.t));
        }
        
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这样做确实具有自我赋值安全性,但并不具备异常安全性。

    异常安全

    上述的代码存在异常方面的麻烦,更明确的说,如果new Teacher导致异常(分配时内存不足或因为Teacher的拷贝构造函数抛出异常),Student最终都会持有一个指针指向一块被删除的Teacher。你无法安全地删除它们,甚至无法安全地读取它们。

    解决方法

    我们可以采取先申请空间,再删除原有空间的做法。这样,即使申请空间时抛出异常,原有指针也不会受到影响。

    Student& Student::operator=(const Student& rt)
    {
        // 执行检测可以清晰地显示我们考虑了自我赋值这一情况
        if (this->t != rt.t)
        {
            Teacher* tmp = t;			// 记住原先的t
            t = new Teacher(*(rt.t));	// 令t指向新空间
            delete tmp;					// 删除原先的t
        }
        
        return *this;
    }
    
    // 这个版本可以去掉函数最前面的检测,即使没有检测也可以完成自我赋值
    // 前提同样是不能出现不同Stu的指针指向相同Tea的情况,否则会引起奇怪的问题
    // 这样或许会在自我赋值时效率低,但检测本身就需要成本且发生频率并不高
    Student& Student::operator=(const Student& rt)
    {
        Teacher* tmp = t;			// 记住原先的t
        t = new Teacher(*(rt.t));	// 令t指向新空间
        delete tmp;					// 删除原先的t
        
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    copy and swap技术

    我们也可以利用拷贝构造来完成这一函数:

    class Student
    {
        ...
        void swap(Student& rs);	// 交换*this和rs的数据
        ...
    };
    
    Student& Student::operator=(const Student& rs)
    {
        Student tmp(rs);	// 为rs数据制作一份拷贝
        swap(tmp);			// 将*this数据和tmp的数据交换
        					// tmp会在出了operator=作用域后自动销毁
        return *this;
    }
    
    // 若赋值重载采用的是传值传参
    Student& Student::operator=(const Student rs)
    {
        swap(rs);			// 将*this数据和rs的数据交换
    
        return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
  • 相关阅读:
    费时“吃透”4个月啃烂完了这份Redis高手心法,成功上岸收到字节offer
    苹果全球销量超越小米重回第二,荣耀回归国内手机市场第一梯队
    java项目第85期基于ssm的人才招聘系统
    客服必看:售后话术
    SpringBoot 28 服务注册实战
    在原生HTML页面发起axios请求
    2022-5月报
    uni-app下,页面跳转后wacth持续监听的问题处理
    代码随想录算法训练营第三十一天|理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
    SVN基本使用笔记——广州云科
  • 原文地址:https://blog.csdn.net/qq_40080842/article/details/125841391