Effective C++ 第二版 15) operator=返回值 16) operator=赋值

本文介绍了C++中operator=的实现要点,强调了返回*this引用的重要性,以支持连续赋值操作。同时,讨论了在处理数据成员赋值时的注意事项,特别是在继承场景下如何正确赋值基类成员。文章提醒开发者避免返回void或const对象引用,并解释了为何不能直接赋值基类对象的原因,提出了使用显式基类赋值运算符调用来解决问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款15: 让operator=返回*this的引用

试图让用户自定义类型尽可能和固定类型的工作方式相似;

固定类型赋值可以:

1
2
int w, x, y, z;
w = x = y = z = 0;

用户自定义类型也可以:

1
2
string w, x, y, z; // string 是由标准C++库“自定义”的类型(参见条款49)
w = x = y = z = "Hello";

赋值运算符结合性默认是由右向左: 上面的赋值可以认为是: w = (x = (y = (z = "Hello")));

等价的函数形式:  w.operator=(x.operator=(y.operator=(z.operator=("Hello"))));

w.operator=, x.operator=和y.operator=的参数是前一个operator=调用的返回值; 所以operator=的返回值必须作为一个输入参数被函数本身接受;

缺省版本的operator=的形式: C& C::operator=(const C&);

一般情况下, operator=输入和返回的都是类对象的引用, 有时候需要重载operator=使它接受不同类型的参数;

e.g. string

1
2
string& operator=(const string& rhs); // 将一个 string 赋给一个string
string& operator=(const char *rhs);  // 将一个 char*赋给一个string

>即使是重载, 返回类型也要是类的对象的引用;

Note C++dev 经常犯的错误是将operator=返回void, 这样妨碍了连续(链式)赋值操作;

另一个常犯的错误是让operator=返回const对象的引用: 

1
2
3
4
5
6
class Widget {
public:
...
    const Widget& operator=(const Widget& rhs);
...
};

>这样做通常是为了防止程序以下的无意义操作:

1
2
3
Widget w1, w2, w3;
...
(w1 = w2) = w3; // w2 赋给w1, 然后w3 赋给其结果 (给operator=一个const 返回值 就使这个语句不能通过编译)

但是对于固定类型, 这样操作也是可以的; 所以没必要改变设定, 和固定类型的常规做法不兼容;

1
2
3
int i1, i2, i3;
...
(i1 = i2) = i3; // 合法! i2 赋给i1 然后 i3 赋给i1!


缺省形式定义的赋值运算符里, 对象返回值有两个候选, 1)赋值语句左边的对象(this指针指向的对象) 2) 赋值语句右边的对象(参数表中被命名的对象);

1
2
3
4
5
6
7
8
9
10
String& String::operator=(const String& rhs)
{
...
    return *this// 返回左边的对象
}
String& String::operator=(const String& rhs)
{
...
    return rhs; // 返回右边的对象
}

返回rhs不会通过编译, rhs是const String&, 要返回的是String&;

如果你想通过重新声明operator=来解决这个问题: String& String::operator=(String& rhs) { ... }, 调用时又会出现问题: 

1
x = "Hello"// 和x.op=("Hello");相同

>赋值语句的右边参数是一个字符数组, 不是String, 编译器会产生一个临时String对象(通过String构造, 除非显式地定义了需要的构造函数): 

1
2
const String temp("Hello"); // 产生临时String
x = temp; // 临时String 传给operator=

>临时值是一个const [由常量构造],  这样的情况, 如果String的operator=声明的参数是非const的String参数, 编译无法通过: 将const对象传递给非const参数是非法的;

结论: 当定义赋值运算符时, 必须返回赋值运算符左边参数的引用, *this; 否则将不能连续赋值, 或导致调用时的隐式类型转换不能进行;


条款16: 在operator=中对所有数据成员赋值

编译器会自动生成缺省的赋值运算符; 当重写赋值运算符时, 必须对对象的每一个数据成员赋值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class T> // 名字和指针相关联的类的模板
class NamedPtr { // (源自条款12)
public:
    NamedPtr(const string& initName, T *initPtr);
    NamedPtr& operator=(const NamedPtr& rhs);
private:
    string name;
    T *ptr;
};
template<class T>
NamedPtr<T>& NamedPtr<T>::operator=(const NamedPtr<T>& rhs)
{
    if (this == &rhs)
        return *this// 见条款17
// assign to all data members
    name = rhs.name; // 给name 赋值
    *ptr = *rhs.ptr; // 对于ptr,赋的值是指针所指的值,
// 不是指针本身
    return *this// 见条款15
}

>当类里增加新的数据成员时, 需要更新赋值运算符函数和构造函数; 

当涉及继承时, 派生类的赋值运算符也必须处理基类成员的赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    Base(int initialValue = 0): x(initialValue) {}
private:
    int x;
};
class Derived: public Base {
public:
    Derived(int initialValue): Base(initialValue), y(initialValue) {}
    Derived& operator=(const Derived& rhs);
private:
    int y;
};

逻辑上说, Derived的赋值运算符是这样:

1
2
3
4
5
6
7
// erroneous assignment operator
Derived& Derived::operator=(const Derived& rhs)
{
    if (this == &rhs) return *this// 见条款17
    y = rhs.y; // 给Derived 仅有的数据成员赋值
    return *this// 见条款15
}

>因为Derived对象的Base部分的数据成员x在赋值运算符中未更新, 所以这是错误的;

e.g.

1
2
3
4
5
6
void assignmentTester()
{
    Derived d1(0); // d1.x = 0, d1.y = 0
    Derived d2(1); // d2.x = 1, d2.y = 1
    d1 = d2; // d1.x = 0, d1.y = 1!
}

>d1的Base部分没有被赋值操作所更新;

>解决问题最显然的方法是在Derived::operator=中对x赋值, 但是这样不合法, x是Base的私有成员, 必须在Derived的赋值运算符里显式地对Derived的Base部分赋值;

1
2
3
4
5
6
7
8
// 正确的赋值运算符
Derived& Derived::operator=(const Derived& rhs)
{
    if (this == &rhs) return *this;
    Base::operator=(rhs); // 调用this->Base::operator=
    y = rhs.y;
    return *this;
}

>显式调用Base::operator=, 和一般情况下在成员函数中调用其他成员函数一样, *this作为它的隐式左值; Base::operator=将针对*this的Base部分执行该做的工作;

如果基类赋值运算符是编译器自动生成的, 有些编译器会拒绝对于基类赋值运算的调用; 为了适应这种情况, 必须实现Derived::operator=

1
2
3
4
5
6
7
Derived& Derived::operator=(const Derived& rhs)
{
    if (this == &rhs) return *this;
    static_cast<Base&>(*this) = rhs; // 对*this 的Base 部分 调用operator=
    y = rhs.y;
    return *this;
}

>这段代码将*this强转为Base的引用[No Slice, it is reference], 然后对转换结果赋值; 这里只是对Derived对象的Base部分赋值; 转换的是Base对象的引用, 不是Base对象本身; 如果将*this强制转换为Base对象, 就要调用Base的拷贝构造函数, 创建出新的对象成为赋值的目标, 而*this保持不变, 这不是预期的结果;

不管是哪种方法, 给Derived对象的Base部分赋值后, 接着的是Derived本身的赋值, 即对Derived的所有数据成员赋值;


另一个经常发生的和继承有关的问题是在实现派生类的拷贝构造函数时;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
    Base(int initialValue = 0): x(initialValue) {}
    Base(const Base& rhs): x(rhs.x) {}
private:
    int x;
};
class Derived: public Base {
public:
    Derived(int initialValue) : Base(initialValue), y(initialValue) {}
    Derived(const Derived& rhs) : y(rhs.y) {}  // 错误的拷贝构造函数
private:
    int y;
};

>Derived类展现了一个在所有C++环境下都会产生的bug: 当Derived的拷贝创建时, 没有拷贝基类部分; 这个Derived对象的Base部分是用缺省构造函数创建的, 成员x被初始化为0(缺省构造函数的缺省参数值), 没有把实际拷贝对象的x值拷贝过去;

Note 为避免这个问题, Derived的拷贝构造函数必须保证调用Base的拷贝构造函数而不是Base的缺省构造函数;

要在Derived的拷贝构造函数的成员初始化列表里对Base指定一个初始化值:

1
2
3
4
5
class Derived: public Base {
public:
    Derived(const Derived& rhs): Base(rhs), y(rhs.y) {}
...
};

>这样, 当用已有的同类型对象来拷贝创建一个Derived对象时, Base部分也会被拷贝

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值