21.12 — 重载赋值运算符

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

相关内容

自 C++11 起,C++ 还支持“移动赋值”。我们在课程 22.3 -- 移动构造函数和移动赋值中讨论移动赋值。

拷贝赋值与拷贝构造函数

拷贝构造函数和拷贝赋值运算符的目的几乎相同——两者都将一个对象拷贝到另一个对象。然而,拷贝构造函数初始化新对象,而赋值运算符替换现有对象的内容。

拷贝构造函数和拷贝赋值运算符之间的区别对于新程序员来说常常会造成很多困惑,但实际上并没有那么难。总结如下:

  • 如果在拷贝发生之前必须创建新对象,则使用拷贝构造函数(注意:这包括按值传递或返回对象)。
  • 如果在拷贝发生之前不需要创建新对象,则使用赋值运算符。

重载赋值运算符

重载拷贝赋值运算符 (operator=) 相当直接,只有一个我们将要讨论的特殊注意事项。拷贝赋值运算符必须作为成员函数重载。

#include <cassert>
#include <iostream>

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

public:
	// Default constructor
	Fraction(int numerator = 0, int denominator = 1 )
		: m_numerator { numerator }, m_denominator { denominator }
	{
		assert(denominator != 0);
	}

	// Copy constructor
	Fraction(const Fraction& copy)
		: m_numerator { copy.m_numerator }, m_denominator { copy.m_denominator }
	{
		// no need to check for a denominator of 0 here since copy must already be a valid Fraction
		std::cout << "Copy constructor called\n"; // just to prove it works
	}

	// Overloaded assignment
	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;
}

// A simplistic implementation of operator= (see better implementation below)
Fraction& Fraction::operator= (const Fraction& fraction)
{
    // do the copy
    m_numerator = fraction.m_numerator;
    m_denominator = fraction.m_denominator;

    // return the existing object so we can chain this operator
    return *this;
}

int main()
{
    Fraction fiveThirds { 5, 3 };
    Fraction f;
    f = fiveThirds; // calls overloaded assignment
    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; // chained assignment

    return 0;
}

自赋值引起的问题

事情在这里开始变得更有趣了。C++ 允许自赋值

int main()
{
    Fraction f1 { 5, 3 };
    f1 = f1; // self assignment

    return 0;
}

这将调用 f1.operator=(f1),在上述简单实现中,所有成员都将赋值给自己。在这个特定的例子中,自赋值导致每个成员被赋值给自己,除了浪费时间,没有整体影响。在大多数情况下,自赋值根本不需要做任何事情!

然而,在赋值运算符需要动态分配内存的情况下,自赋值实际上可能很危险。

#include <algorithm> // for std::max and 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); // copy length elements of data into m_data
		}
	}
	~MyString()
	{
		delete[] m_data;
	}

	MyString(const MyString&) = default; // some compilers (gcc) warn if you have pointer members but no declared copy constructor

	// Overloaded assignment
	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;
}

// A simplistic implementation of operator= (do not use)
MyString& MyString::operator= (const MyString& str)
{
	// if data exists in the current string, delete it
	if (m_data) delete[] m_data;

	m_length = str.m_length;
	m_data = nullptr;

	// allocate a new array of the appropriate length
	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); // copies m_length elements of str.m_data into m_data

	// return the existing object so we can chain this operator
	return *this;
}

int main()
{
	MyString alex("Alex", 5); // Meet Alex
	MyString employee;
	employee = alex; // Alex is our newest employee
	std::cout << employee; // Say your name, employee

	return 0;
}

首先,按原样运行程序。您会看到程序按预期打印“Alex”。

现在运行以下程序

int main()
{
    MyString alex { "Alex", 5 }; // Meet Alex
    alex = alex; // Alex is himself
    std::cout << alex; // Say your name, Alex

    return 0;
}

您可能会得到乱码输出。发生了什么?

考虑在重载的 operator= 中,当隐式对象和传入参数 (str) 都是变量 alex 时会发生什么。在这种情况下,m_data 与 str.m_data 相同。首先发生的是函数检查隐式对象是否已经有一个字符串。如果是,它需要删除它,这样我们就不会导致内存泄漏。在这种情况下,m_data 已分配,因此函数会删除 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)
{
	// self-assignment check
	if (this == &str)
		return *this;

	// if data exists in the current string, delete it
	if (m_data) delete[] m_data;

	m_length = str.m_length;
	m_data = nullptr;

	// allocate a new array of the appropriate length
	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); // copies m_length elements of str.m_data into m_data

	// return the existing object so we can chain this operator
	return *this;
}

通过检查我们的隐式对象的地址是否与作为参数传入的对象的地址相同,我们可以让我们的赋值运算符立即返回,而不做任何其他工作。

由于这只是一个指针比较,它应该很快,并且不需要重载 operator==。

何时不处理自赋值

通常,拷贝构造函数会跳过自赋值检查。因为被拷贝构造的对象是新创建的,新创建的对象等于被拷贝对象的唯一情况是当您尝试用自身初始化一个新定义的对象时

someClass c { c };

在这种情况下,编译器应该警告您 c 是一个未初始化的变量。

其次,在能够自然处理自赋值的类中,可以省略自赋值检查。考虑这个带有自赋值保护的 Fraction 类赋值运算符

// A better implementation of operator=
Fraction& Fraction::operator= (const Fraction& fraction)
{
    // self-assignment guard
    if (this == &fraction)
        return *this;

    // do the copy
    m_numerator = fraction.m_numerator; // can handle self-assignment
    m_denominator = fraction.m_denominator; // can handle self-assignment

    // return the existing object so we can chain this operator
    return *this;
}

如果不存在自赋值保护,此函数在自赋值期间仍将正确运行(因为函数执行的所有操作都可以正确处理自赋值)。

由于自赋值是罕见事件,一些著名的 C++ 大师建议即使在受益于自赋值保护的类中也省略它。我们不建议这样做,因为我们认为防御性编程,然后选择性优化是更好的实践。

拷贝并交换惯用法

处理自赋值问题的更好方法是使用所谓的拷贝并交换惯用法。关于此惯用法如何工作的精彩文章可以在Stack Overflow上找到。

隐式拷贝赋值运算符

与其他运算符不同,如果您不提供用户定义的拷贝赋值运算符,编译器将为您的类提供一个隐式公共拷贝赋值运算符。此赋值运算符执行逐成员赋值(这与默认拷贝构造函数执行的逐成员初始化本质上相同)。

就像其他构造函数和运算符一样,您可以通过将拷贝赋值运算符设置为私有或使用 delete 关键字来阻止赋值。

#include <cassert>
#include <iostream>

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

public:
    // Default constructor
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator { numerator }, m_denominator { denominator }
    {
        assert(denominator != 0);
    }

	// Copy constructor
	Fraction(const Fraction &copy) = delete;

	// Overloaded assignment
	Fraction& operator= (const Fraction& fraction) = delete; // no copies through assignment!

	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; // compile error, operator= has been deleted
    std::cout << f;

    return 0;
}

请注意,如果您的类包含 const 成员,编译器将改为将隐式 operator= 定义为 deleted。这是因为 const 成员不能被赋值,因此编译器会假定您的类不应该是可赋值的。

如果您希望一个包含 const 成员的类是可赋值的(对于所有非 const 成员),您将需要显式重载 operator= 并手动赋值每个非 const 成员。

guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
212 评论
最新
最早 最多投票
内联反馈
查看所有评论