Effective C++ 第二版 15) operator=返回值 16) operator=赋值
条款15: 让operator=返回*this的引用
试图让用户自定义类型尽可能和固定类型的工作方式相似;
固定类型赋值可以:
int
w, x, y, z;
w = x = y = z = 0;
用户自定义类型也可以:
12string 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
12string& operator=(
const
string& rhs);
// 将一个 string 赋给一个string
string& operator=(
const
char
*rhs);
// 将一个 char*赋给一个string
>即使是重载, 返回类型也要是类的对象的引用;
Note C++dev 经常犯的错误是将operator=返回void, 这样妨碍了连续(链式)赋值操作;
另一个常犯的错误是让operator=返回const对象的引用:
123456class
Widget {
public
:
...
const
Widget& operator=(
const
Widget& rhs);
...
};
>这样做通常是为了防止程序以下的无意义操作:
Widget w1, w2, w3;
...
(w1 = w2) = w3;
// w2 赋给w1, 然后w3 赋给其结果 (给operator=一个const 返回值 就使这个语句不能通过编译)
但是对于固定类型, 这样操作也是可以的; 所以没必要改变设定, 和固定类型的常规做法不兼容;
int
i1, i2, i3;
...
(i1 = i2) = i3;
// 合法! i2 赋给i1 然后 i3 赋给i1!
缺省形式定义的赋值运算符里, 对象返回值有两个候选, 1)赋值语句左边的对象(this指针指向的对象) 2) 赋值语句右边的对象(参数表中被命名的对象);
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) { ... }, 调用时又会出现问题:
1x =
"Hello"
;
// 和x.op=("Hello");相同
>赋值语句的右边参数是一个字符数组, 不是String, 编译器会产生一个临时String对象(通过String构造, 除非显式地定义了需要的构造函数):
12const
String temp(
"Hello"
);
// 产生临时String
x = temp;
// 临时String 传给operator=
>临时值是一个const [由常量构造], 这样的情况, 如果String的operator=声明的参数是非const的String参数, 编译无法通过: 将const对象传递给非const参数是非法的;
结论: 当定义赋值运算符时, 必须返回赋值运算符左边参数的引用, *this; 否则将不能连续赋值, 或导致调用时的隐式类型转换不能进行;
条款16: 在operator=中对所有数据成员赋值
编译器会自动生成缺省的赋值运算符; 当重写赋值运算符时, 必须对对象的每一个数据成员赋值;
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
}
>当类里增加新的数据成员时, 需要更新赋值运算符函数和构造函数;
当涉及继承时, 派生类的赋值运算符也必须处理基类成员的赋值:
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的赋值运算符是这样:
1234567// 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.
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部分赋值;
12345678// 正确的赋值运算符
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=
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的所有数据成员赋值;
另一个经常发生的和继承有关的问题是在实现派生类的拷贝构造函数时;
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指定一个初始化值:
12345class
Derived:
public
Base {
public
:
Derived(
const
Derived& rhs): Base(rhs), y(rhs.y) {}
...
};
>这样, 当用已有的同类型对象来拷贝创建一个Derived对象时, Base部分也会被拷贝