本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这是本文档旧的修订版!
第 12 章笔记
运算符重载是指在 C++ 中基于 operator
关键字引入对运算符的重载。经过重载的运算符可以被视作函数,典型的结构(以减法为例):
return Type + operator - + (parameter list)
operator()
,其他重载的运算符都不能设置默认参数运算符重载可以以成员函数或非成员函数的方式来实现:
*this
作为第一个 operand==
与 ⇔
并不遵循此例
// 非成员函数重载
Str operator+ (Str x, Str y)
{
Str z;
z.x = x.x + y.x;
return z;
}
// 成员函数重载
Str operator+(Str rhs)
{
Str z;
//x imply this->x
z.x = x + rhs.x;
return z;
}
=
,()
, →
,转型运算符&&
,||
,,
。这类运算符往往需要维护特定的求值顺序,而
指可以左右算子互换位置的运算符(比如 +
,==
等)。对称性运算符需要被设计为非成员重载,是因为:
class Str
{
public:
Str(int x) :val(x) {}
Str operator+ (const Str& rhs) {...}
private:
int val;
};
int main(int argc, char const *argv[])
{
Str myStr1(2);
// 可以进行正常运算,4 对应 rhs, 可以通过 Str 的构造函数转化为 Str 类型
Str myRetStr =myStr1 + 4;
// 无法正常进行运算,4 不是 Str (this指向的) 类型
Str myRetStr = 4 + myStr1;
return 0;
}
this
的匹配影响,上述例子中的 4
则可以在函数匹配的过程中进行隐式转换。friend
,否则无法访问私有数据 移位运算符必须重载为非成员函数。这是因为 C++ 中移位运算符有一个更重要的应用:输入输出流运算符。输入输出流运算符一般的格式为:
StreamObjReference << Object;
这种情况下,成员函数版本无法支持该功能。
// 简单的输出流重载实现
// 需要声明为友元函数
std::ostream& operator << (std::ostream& ostr, const Str &output)
{
ostr << output.val;
return ostr;
}
// 简单的实现
Str& Str::operator= (const Str& rhs)
{
if(this != &rhs)
{
val = rhs.val;
}
return *this;
}
该运算符重载用于模拟下标操作。典型的应用是 std::vector
。需要考虑的问题有:
主要的问题在第二个子问题上:
ObjA[0] = ObjB
这种形式的运算进行处理。因此,下标运算的返回结果一定是左值,也就是对象的引用。const
时,这种写入的操作应该被侦测并禁止。因此,我们需要一个专门的 const 实现版本来处理该情况。
// 简单的实现,基于 String 的下标运算重载
class StrVec {
public:
std::string& operator[](std::size_t n)
{ return elements[n]; }
// const 版本
const std::string& operator[](std::size_t n) const
{ return elements[n]; }
private:
std::string* elements; // pointer to the first element in the array
};
以加法为例, 前缀版本:
operator++()
后缀版本:
int
类型参数:operator++(int)
Str& Str::operator++()
{
++val;
return *this;
}
Str Str::operator++(int)
{
Str ret = *this;
++*this;
return ret;
}
两者都是模拟指针的行为。
const
版本
int& Str::operator*()
{
return *ptr;
}
箭头运算符的重载是一个通过递归的方式访问最终数据成员的过程:
当使用箭头运算符的重载时,只要返回值不是指针类型,则编译器会尝试在当前对象中继续寻找 operator →()
的定义,并执行该定义继续进行调用;直到某个调用返回指针时,该过程才会停止。这么做是因为箭头操作符右边的 operand(对象中数据成员的名称) 并不是严格意义上的对象;只有在其与对应的对象组合使用时,才能代表正式的对象(比如 obj.x
)。因此整个该过程实际上是通过一个层层 “开盒” 的行为来找到最终数据成员的过程。
实现上,箭头运算符的重载有返回类型,但没有参数
// 返回指针(Base case)
Str* Str::operator->()
{
return this;
}
// 返回对象(Normal case)
// 编译器会到 Str2 中继续寻找 -> 的定义
Str2 Str::operator->()
{
return Str2;
}
函数调用运算符允许将对象变为可调用对象,即 obj()
这类型的函数。其最大的特点是接收参数数量不定:
struct Str
{
Str(int* p):ptr(p){}
int operator() (int x, int y, int z)
{
return *ptr + x + y + z;
}
int* ptr;
};
int main(int argc, char const *argv[])
{
int x = 100;
Str StrPtr(&x);
// 打印 100 + 1 + 2 + 3 的结果
std::cout << StrPtr(1,2,3);
return 0;
}
const
const
const
operator type() const {}
需要注意的是,除了自定义的类型转换运算符,程序中很可能还存在其他的重载,这些重载也可能具有(隐式)类型转换的功能,比如:
// 同时定义了 class 到 int 的转换,以及 class 与 int 的加法
// int() 可以转换
operator int() const { return val; }
friend Str operator + (const Str& lhs, const Str& rhs);
// 存在两个候选者
// 4 可以通过 int() 转换为 Str, 也可以通过 + 转换为 Str,因此导致歧义
Str obj(100);
std::cout << (obj + 4);
对此,我们可以使用 explicit
关键字禁止其中一个重载进行隐式转换:
explicit operator int() const { return val; }
隐式转换还可能带来一些未知的结果。比如下面的例子:
// 使用 cin 输出,逻辑上是不合理的
// 此处,cin 的隐式转换为 istream -> bool -> int
// 根据 cin 是否有效,最终的表达式为 0 << 3 或是 1 << 3
// 也就是非法的输出操作通过隐式转换转变为了合法的移位操作
std::cin << 3;
这种情况依然可以使用 explict
处理
explict
存在例外。当 explict
修饰的表达式作为条件语句的判断条件时,编译器会将其自动转换为 bool
类型。该行为不受 explicit
关键字约束。
C++ 20 中:
==
的重载,编译器会自动推导 !=
的重载(3-way comparsion 同理)==
会尝试对 operand 进行自动换位,来匹配用户换位的结果,比如
bool operator ==(Str obj, int val) { return obj.x == val; }
// 可以调用
obj == 3;
// C++ 20 之前错误,无法找到匹配的函数
// C++ 20 编译通过
// 编译器会自动尝试 bool operator ==(int val, Str obj)
3 == obj;
class
默认的继承方式是 private
,struct
默认的继承方式是 public
。:public Base
)这一部分可以通过基类的指针和引用指向派生类的对象:
Derive d;
Base& ref = d;
Base* ptr = &d;
当基类(或派生类)中某个函数被定义为虚函数时,意味着该函数可以通过基类的指针或者引用对其进行调用。其作用是为了实现运行期的动态多态:也就是说,根据调用者的动态类型不同,对应的虚函数可能存在不同的实现方式。这些不同的实现方式通过重写(override)实现。一些注意事项:
vritual
关键字定义虚函数之所以能在运行期与特定类型的对象进行绑定,原因是其维护了一个被称为虚表(vitrual table)的数组。该数组会在派生类对象实例化时生成,包含两部分重要的信息:
虚函数的动态绑定会根据这个表中的信息完成。在派生类中,只要定义了虚函数的重写,那么虚表中对应的同名函数的绑定,更新就会到当前的派生类上。
虚函数通过虚表达实现 C++ 中动态多态的功能:在不改变函数签名的情况下,根据实例化的对象来选择对应的函数。
纯虚函数是作为类的接口而存在的。与虚函数不同,纯虚函数没有实现。几个特点:
纯虚函数通过以下方式定义:
virtual returnType Func(parameter_list) = 0;