本期的主要内容是箭头运算符以及它对于结构体与类的指针可以做什么,最后实现我们自己的运算符重载。
01 为什么要使用运算符重载
从开发的角度而言,运算符重载的存在是为了提高开发效率,增加代码的可复用性,很多时候简化了代码。
C++强大的原因之一是支持泛型编程。泛型编程使得算法可以独立于数据类型存在。自定义的数据类型通过操作符重载具有了和内建类型同样的使用接口,然后C++的模板加持下,你的算法可以利用这种接口一致性,实现泛化。
1.1 如何使用运算符重载
这是一个基本的 Entity 类,有一个输出提示语的 Print 函数。
#include <iostream>
#include <string>
class Entity
{
public:
void Print() const { std::cout << "Hello!" << std::endl; }
};
int main()
{
std::cin.get();
}
如果按照正常的使用方法,我们会这样创建对象。
int main()
{
Entity e;
e.Print();
std::cin.get();
}
可如果是在堆上创建的,或者出于某种原因,程序中有一个指向它的指针,为了调用 Print 函数,该怎么办呢?这个时候就不能使用 . 了,下面的写法很明显是错的。
int main()
{
Entity e;
e.Print();
Entity* ptr = &e;
ptr.Print();
std::cin.get();
}
因为 ptr 只是一个指针,也就是一个数值,不是对象,无法调用方法。
我们可以使用逆向引用 ptr 来实现调用 Print 函数(使用括号是因为符号的优先级的原因)。
int main()
{
Entity e;
e.Print();
Entity* ptr = &e;
(*ptr).Print();
std::cin.get();
这样写当然没有问题,但它看起里有点笨重。
我们可以使用箭头运算符来简化这个操作。
int main()
{
Entity e;
e.Print();
Entity* ptr = &e;
ptr->Print();
std::cin.get();
}
这样的操作就相当于逆向引用了 Entity 指针。
这就是它的全部内容了。
它只是一个快捷方式,成员变量也是同样的道理。
箭头运算符作为一个操作符,我们实际上可以重载它,并在你自定义类中使用它。
我们一起来看一个例子,来说明为什么要这么做以及怎么做。
假设我正在写一个智能指针的类,如下所示。
class ScopePtr
{
private:
Entity* m_Obj;
public:
ScopePtr(Entity* entity)
:m_Obj(entity)
{
}
~ScopePtr()
{
delete m_Obj;
}
};
这是一个基本的 ScopePtr 类,当 Entity 超出范围时,这个类会自动删除它。
我可以这样使用它。
int main()
{
ScopePtr entity = new Entity();
std::cin.get();
}
我希望能够调用 Print 函数,该怎么做呢?
首先我不能用 . 符号,但我可以将 m_Obj 变成 public 的,或者写一个 GetObject 函数,返回一个 Entity 指针,类似下面这样写法。
int main()
{
ScopePtr entity = new Entity();
entity.GetObject()->Print();
std::cin.get();
}
不过这方法看起来太乱了,我希望能够使用堆分配的 Entity 一样使用它,类似于下面这样用法。
int main()
{
ScopePtr* entity = new Entity();
entity->Print();
std::cin.get();
}
这就是我们想要重载箭头运算符的原因。
重载箭头运算符。
总结
良好的重载除了提高写代码的效率,也往往降低了代码维护的成本,更容易减少bug,提高代码质量,增加可读性。
如果非要说带来了什么负面影响,我觉得往往还是人为的原因。没有做好抽象导致滥用运算符重载。如果设计不好接口的话,还是不要强行引入运算符重载,这样可能反而降低代码可读性,增加耦合。