重载赋值运算符

复制赋值运算符(operator=)用于将值从一个对象复制到另一个已存在的对象。

相关内容

自 C++11 起,语言还支持“移动赋值”。我们将在课程《移动构造函数与移动赋值》中讨论移动赋值。

复制赋值 vs 复制构造函数

复制构造函数与复制赋值运算符的目的几乎相同——都是将一个对象复制到另一个对象。然而,复制构造函数用于初始化新对象,而赋值运算符则替换已存在对象的内容。

这一区别常常令新手困惑,实则并不复杂。总结如下:

  • 若必须先创建新对象才能进行复制,则调用复制构造函数(注意:按值传递或返回对象时亦属此类情形)。
  • 若无需先创建新对象即可进行复制,则调用赋值运算符。

重载赋值运算符

重载复制赋值运算符(operator=)较为直接,但有一处需要特别注意,后文将详述。复制赋值运算符必须以成员函数形式重载。

#include <cassert>
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 默认构造函数
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
        assert(denominator != 0);
    }

    // 复制构造函数
    Fraction(const Fraction& copy)
        : m_numerator{ copy.m_numerator }, m_denominator{ copy.m_denominator }
    {
        // 此处无需检查分母为 0,因 copy 必已是有效的 Fraction
        std::cout << "Copy constructor called\n"; // 仅作演示
    }

    // 重载赋值运算符
    Fraction& operator=(const Fraction& fraction);

    friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};

std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
    out << f1.m_numerator << '/' << f1.m_denominator;
    return out;
}

// 一种简单的 operator= 实现(见下文更优实现)
Fraction& Fraction::operator=(const Fraction& fraction)
{
    // 执行复制
    m_numerator = fraction.m_numerator;
    m_denominator = fraction.m_denominator;

    // 返回当前对象,以支持链式赋值
    return *this;
}

int main()
{
    Fraction fiveThirds{ 5, 3 };
    Fraction f;
    f = fiveThirds; // 调用重载的赋值运算符
    std::cout << f;

    return 0;
}

输出:

5/3

至此应已一目了然。重载的 operator= 返回 *this,使我们能够链式赋值:

int main()
{
    Fraction f1{ 5, 3 };
    Fraction f2{ 7, 2 };
    Fraction f3{ 9, 5 };

    f1 = f2 = f3; // 链式赋值

    return 0;
}

自赋值引发的问题

此处开始变得有趣:C++ 允许自赋值:

int main()
{
    Fraction f1{ 5, 3 };
    f1 = f1; // 自赋值

    return 0;
}

上述代码会调用 f1.operator=(f1),在简单实现下,各成员被赋给自身,除了浪费时间外并无实质影响。多数情况下,自赋值根本无需任何操作!

然而,若赋值运算符需要动态分配内存,自赋值可能变得危险:

#include <algorithm> // std::max、std::copy_n
#include <iostream>

class MyString
{
private:
    char* m_data{};
    int m_length{};

public:
    MyString(const char* data = nullptr, int length = 0)
        : m_length{ std::max(length, 0) }
    {
        if (length)
        {
            m_data = new char[static_cast<std::size_t>(length)];
            std::copy_n(data, length, m_data); // 将 data 的 length 个元素复制到 m_data
        }
    }
    ~MyString()
    {
        delete[] m_data;
    }

    MyString(const MyString&) = default; // 某些编译器(如 GCC)在类含指针成员却未显式声明复制构造函数时会发出警告

    // 重载赋值运算符
    MyString& operator=(const MyString& str);

    friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};

std::ostream& operator<<(std::ostream& out, const MyString& s)
{
    out << s.m_data;
    return out;
}

// 一种简单的 operator= 实现(切勿直接使用)
MyString& MyString::operator=(const MyString& str)
{
    // 若当前字符串已有数据,则先删除
    if (m_data) delete[] m_data;

    m_length = str.m_length;
    m_data = nullptr;

    // 分配适当长度的新数组
    if (m_length)
        m_data = new char[static_cast<std::size_t>(str.m_length)];

    std::copy_n(str.m_data, m_length, m_data); // 将 str.m_data 的 m_length 个元素复制到 m_data

    // 返回当前对象,以支持链式赋值
    return *this;
}

int main()
{
    MyString alex("Alex", 5); // 创建 Alex
    MyString employee;
    employee = alex; // Alex 成为新员工
    std::cout << employee; // 输出员工姓名

    return 0;
}

先按原样运行程序,会正确输出 “Alex”。

再运行以下代码:

int main()
{
    MyString alex{ "Alex", 5 };
    alex = alex; // Alex 赋给自己
    std::cout << alex; // 输出 Alex 的姓名

    return 0;
}
你很可能会得到乱码输出。发生了什么?

考虑重载 operator= 时,隐式对象与形参 str 皆为变量 alex 的情形。此时 m_data 与 str.m_data 指向同一块内存。函数首先检查隐式对象是否已含字符串,若有则将其删除,以避免内存泄漏。在此例中,m_data 已分配,于是函数 delete[] m_data。但因 str 与 *this 为同一对象,欲复制的字符串已被删除,m_data(及 str.m_data)变为悬垂指针。

随后,我们为 m_data(及 str.m_data)分配新内存。当尝试将 str.m_data 中的数据复制到 m_data 时,我们实际复制的是垃圾值,因为 str.m_data 从未被正确初始化。

检测并处理自赋值

好在我们可以检测自赋值。下面是 MyString 类重载 operator= 的更新实现:

MyString& MyString::operator=(const MyString& str)
{
    // 自赋值检查
    if (this == &str)
        return *this;

    // 若当前字符串已有数据,则先删除
    if (m_data) delete[] m_data;

    m_length = str.m_length;
    m_data = nullptr;

    // 分配适当长度的新数组
    if (m_length)
        m_data = new char[static_cast<std::size_t>(str.m_length)];

    std::copy_n(str.m_data, m_length, m_data); // 将 str.m_data 的 m_length 个元素复制到 m_data

    // 返回当前对象,以支持链式赋值
    return *this;
}

通过比较隐式对象的地址与传入参数的地址,若相同,则赋值运算符立即返回,不再执行后续操作。

由于这只是指针比较,开销极小,且无需重载 operator==。

何时可不处理自赋值

通常,复制构造函数无需自赋值检查。因被复制构造的对象是新创建的,唯一可能与新对象相等的情形是用自身初始化新定义的对象:

someClass c{ c };

此时,编译器应会警告 c 为未初始化变量。

其次,若某类天然能妥善处理自赋值,亦可省略自赋值检查。考虑带自赋值守卫的 Fraction 类赋值运算符:

// operator= 的更佳实现
Fraction& Fraction::operator=(const Fraction& fraction)
{
    // 自赋值守卫
    if (this == &fraction)
        return *this;

    // 执行复制
    m_numerator = fraction.m_numerator; // 可安全处理自赋值
    m_denominator = fraction.m_denominator; // 可安全处理自赋值

    // 返回当前对象,以支持链式赋值
    return *this;
}

若移除自赋值守卫,该函数在自赋值时仍能正确运行(因为所有操作均可正确处理自赋值)。

鉴于自赋值极少发生,某些著名 C++ 专家建议在即使会受益的类中也省略自赋值守卫。我们不推荐此做法,认为更合理的实践是先防御式编码,再视情况优化。

复制并交换(copy and swap)惯用法

一种更优的自赋值处理方式是所谓的“复制并交换”惯用法。Stack Overflow 上有关于该惯用法的精彩阐述。

隐式复制赋值运算符

与其他运算符不同,若未提供用户定义的复制赋值运算符,编译器将为你的类生成一个隐式的 public 复制赋值运算符。该运算符执行按成员赋值(实质上与默认复制构造函数的按成员初始化相同)。

与其他构造函数和运算符一样,你可通过将复制赋值运算符设为 private 或使用 delete 关键字来阻止赋值:

#include <cassert>
#include <iostream>

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 默认构造函数
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
        assert(denominator != 0);
    }

    // 复制构造函数
    Fraction(const Fraction& copy) = delete;

    // 重载赋值运算符
    Fraction& operator=(const Fraction& fraction) = delete; // 禁止通过赋值复制!

    friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};

std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
    out << f1.m_numerator << '/' << f1.m_denominator;
    return out;
}

int main()
{
    Fraction fiveThirds{ 5, 3 };
    Fraction f;
    f = fiveThirds; // 编译错误:operator= 已被删除
    std::cout << f;

    return 0;
}

注意: 若类含有 const 成员,编译器会将隐式 operator= 定义为 deleted,因为 const 成员不可赋值,编译器据此推断该类不应可赋值。

若你希望含 const 成员的类仍可赋值(仅对非 const 成员赋值),则需显式重载 operator= 并手动赋值每个非 const 成员。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"