this
指针
String
重载 +
为 在上一节中我们为 String
重载了运算符 []
,含义是取出字符串的某一位。现在我们打算重载 +
这个运算符。它的含义是字符串拼接,也就是 "abc" + "def"
得到 "abcdef"
。而类比之前重载 []
的过程可以知道,
a + b
中,当 a
是自定义的对象时,就相当于
a.operator+(b)
即调用 a
的成员函数 operator+
。那么,就来看一下 operator+
是怎么定义的吧:
class String {
public:
// [...]
String operator+(const String& b) { // 最好是 const T&,避免复制开销
char* newstr{new char[len + b.len + 1]}; // 分配新的空间,大小为两个长度相加
std::strcpy(newstr, str); // 把 a 的字符串复制到新空间的前半段
std::strcpy(newstr + len, b.str); // 把 b 的字符串复制到新空间的后半段
String result(newstr); // 从这个新空间建立新的 String 对象
delete[] newstr; // 现在可以删除新空间了(因为数据复制到 result 里去了)
return result; // 把 result 返回就可以
}
};
这段代码相比之前要略微难理解一点。如果没有看懂,我建议你还是稍微多停留一会儿,争取搞明白它的工作原理。
然后就可以试一试:
#include <iostream>
int main() {
String a("abc");
String b("def");
// 下面调用了刚刚定义的 operator+
String c(a + b);
std::cout << c.str << std::endl;
}
然后,就能成功输出 abcdef
。我们的 +
重载看上去很不错!
一个小细节:调用完
operator+
之后,还可能会复制构造函数来构造c
。说“可能会”的原因是,这里很可能会发生返回值优化从而避免复制开销。
String
重载 +=
为 下一步,我们尝试重载 +=
运算符。我们的目标很简单,就是比如当 a
为 "abc"
时,执行 a += "def"
,然后 a
就变成了 "abcdef"
。那么怎么做呢?方法也很简单:为了实现 a += b
,只需要算出 a + b
,然后把这个结果赋值给 a
就可以了。所以代码就这样:
class String {
public:
// [...]
void operator+=(const String& b) {
String result(operator+(b)); // 直接调用 operator+ 成员函数
assign(result); // 调用 assign 成员把结果赋值给自己
}
};
这里直接调用了成员函数 operator+
来生成自己加上 b
得到的结果是什么,存放在对象 result
里。然后调用 assign
成员函数,把自己的值赋为 result
就可以了。测试一下:
#include <iostream>
int main() {
String a("abc");
String b("def");
a += b;
std::cout << a << std::endl;
}
成功输出了 abcdef
。但是这个其实还有一点小问题,就是对于“内置的”类型比如 int
,它的赋值运算符是有结果的。比如:
是可以输出 42
的(也就是把运算后的 a
作为赋值运算的结果值)。但是我们的实现做不到:
#include <iostream>
int main() {
String a("abc"), b("def");
std::cout << (a += b).str << std::endl; // 错误:不许在表达式中运用 void 类型
}
那有人说,简单,就把刚才 +
完之后的结果返回就好了:
class String {
public:
// [...]
String operator+=(const String& b) {
String result(operator+(b));
assign(result);
return result;
}
};
但这样其实还不对。因为“内置”的赋值运算是可以“连起来的”,但我们这个做不到。
int main() {
// 内置类型的表现
int p{1};
(p += 2) += 3; // 相当于 a += 2; a += 3;
std::cout << p << std::endl; // a 当前的值为 6 = 1 + 2 + 3
// 接下来试试我们的
String a("abc"), b("def");
(a += b) += b; // 这句不会报错,但……
std::cout << a.str << std::endl; // 输出为 "abcdef" 而非 "abcdefdef"
}
为什么连着两次赋值第二次就不行呢?请注意 operator+=
的返回值:它返回了一个 String
类型对象……但请思考一下,这个对象和 a
有关系吗?没有关系。也就是说,(a += b)
这个表达式确实得到了一个 String
对象,但它和 a
本身是没有任何联系的。那么,接下来对 (a += b)
在做 +=
运算,实际上是对这个孤零零的对象做赋值运算,这一次赋值并没有反映到 a
上。
所以我们的目标就是,让第二次赋值的操作对象是 a
而不是一个临时的对象。那么方法就是:让 +=
返回绑定到 a
的引用。
class String {
public:
String& operator+=(const String& b) { // 注意返回值类型
// [...] 反正最终返回了绑定到 a 的引用(但我们还不知道怎么写)
}
};
然后,再来看 (a += b)
是什么?回顾引用一词的含义,其实 (a += b)
是 a
的别名。所以,此时再对 (a += b)
做 +=
操作,就相当于对 a
做 +=
操作。那么我们的目标就实现了。
最后一个问题:怎么返回绑定到 a
的引用?这里就需要引入 this
关键字了。this
是在成员函数中可以用到的一个特殊变量名,在这里它是 T*
类型的,其中 T
是这个成员函数所归属的类型;作为一个指针,this
总是指向调用这个函数的那个对象。说来抽象,请看例子:
比如它输出
0x61fe1c
0x61fe1c
0x61fe18
0x61fe18
那么很显然,当 a
调用 printThis
时,打印出的就是 a
的地址 &a
;当 b
调用 printThis
时,打印出的就是 b
的地址 &b
。所以基于此,我们就可以构造绑定到 a
的引用:
class String {
public:
// [...]
String& operator+=(const String& b) {
String result(operator+(b)); //
assign(result);
// 这里,this 指向的就是调用 `operator+=` 的调用者 a ,而 *this 就是 a 本身了
return *this;
}
};
也就是 *this
。接下来再试试之前的代码,相信你就能得出正确的结果了。
在只读成员函数中,
this
是const T*
类型的。严格来讲,this
应当具有顶层只读限定(即不可以更改this
的指向,其类型为T* const
或const T* const
),但文中出于行文简便没有考虑它。