13. 重载一元运算符:负号、逻辑非、取地址

一元运算符,说白了就是只对一个对象操作的符号。比如 -a!b&c。你可能会想:“这些不是内置类型早就有的吗?为什么还要自己重载?”

嗯,原因很简单——当你自己写了一个类,比如 Vector3DMoneySmartPtr,你希望它们用起来像内置类型一样自然。我刚开始学 C++ 时也觉得重载这些符号有点多余,直到我在一个数学库项目里,发现如果不重载负号,就得写 v.negate() 而不是 -v——那代码读起来,真的别扭。

13.1 重载负号运算符 operator-

负号运算符是一元运算符,它不改变原对象,而是返回一个新对象。比如 -a,a 本身不变,返回的是 a 的相反数。

我个人的习惯是:一元负号通常实现为成员函数,返回一个新对象。为什么?因为它不修改自身,返回的是副本的取反结果。

class Vector2D {
public:
    Vector2D(double x, double y) : x_(x), y_(y) {}

    // 一元负号重载
    Vector2D operator-() const {
        return Vector2D(-x_, -y_);
    }

    void print() const {
        std::cout << "(" << x_ << ", " << y_ << ")" << std::endl;
    }

private:
    double x_, y_;
};

// 使用
Vector2D v(3.0, 4.0);
Vector2D neg = -v;   // neg 是 (-3.0, -4.0)
v.print();           // v 仍然是 (3.0, 4.0)

小提示:注意函数签名里的 const。一元负号不会修改对象本身,所以加上 const 是合理的。我见过有人忘了加,结果 -v 不能用在 const 对象上,编译报错,排查了半天。

13.2 重载逻辑非运算符 operator!

逻辑非 ! 通常用于检查对象是否为“空”或“无效”状态。比如智能指针的 !ptr 检查是否为空,或者一个 String 类的 !str 检查是否为空字符串。

我曾经在一个网络库中重载了 operator!,用来判断一个 Socket 对象是否处于有效连接状态。代码写起来非常直观:

class Socket {
public:
    Socket() : is_valid_(false) {}
    explicit Socket(int fd) : fd_(fd), is_valid_(true) {}

    bool operator!() const {
        return !is_valid_;
    }

private:
    int fd_ = -1;
    bool is_valid_ = false;
};

// 使用
Socket sock(3);
if (!sock) {
    std::cerr << "Socket 无效!" << std::endl;
}

注意:重载 operator! 时,返回值类型通常是 bool。但 C++ 允许你返回任何类型,只是那样会让人困惑。你想想看,如果 !obj 返回一个整数,别人读代码时得多费劲?

13.3 重载取地址运算符 operator&

取地址运算符 & 比较特殊。一般情况下你不需要重载它,因为编译器默认会返回对象的地址。但有些场景下,比如你写了一个智能指针类,或者一个代理类,你可能想控制“取地址”的行为。

我记得有一次,我在做一个内存池分配器,对象是从预分配的内存块中构造的。如果用户对池中的对象取地址,默认的 & 会返回对象在池中的偏移地址,而不是用户期望的“逻辑地址”。这时候重载 operator& 就派上用场了。

class PoolObject {
public:
    PoolObject* operator&() {
        // 返回逻辑地址,而不是物理地址
        return logical_address_;
    }

    const PoolObject* operator&() const {
        return logical_address_;
    }

private:
    PoolObject* logical_address_ = nullptr;
};

核心原则:除非你非常清楚自己在做什么,否则不要重载 operator&。标准库里的很多算法和容器都依赖默认的取地址行为。你一旦改了,可能会引发意想不到的 bug。

13.4 一元运算符的实现方式对比

运算符 典型语义 推荐实现方式 返回值类型
operator- 取相反数 成员函数,const 返回新对象(值)
operator! 逻辑取反 成员函数,const bool
operator& 取地址 成员函数,const 指针类型

13.5 避坑指南

我踩过不少一元运算符的坑,这里挑几个典型的说说:

  • 不要混淆一元负号和二元减法。一元负号只有一个参数(隐式的 this),二元减法有两个参数。如果你不小心把参数个数搞错了,编译器会报错,但错误信息可能很隐晦。
  • 重载 operator! 时,记得考虑短路求值。内置的 ! 是短路求值的,但重载后就不再是了。不过对于一元运算符来说,这通常不是问题,因为只有一个操作数。
  • 重载 operator& 会影响 std::addressof标准库提供了 std::addressof 来获取对象的真实地址,即使 operator& 被重载了。所以如果你重载了 &,别人可以用 std::addressof 绕过你的设计。

我的建议:除非你写的是底层基础设施(比如内存分配器、智能指针),否则不要碰 operator&。对于 operator-operator!,大胆用,它们能让你的类用起来像内置类型一样自然。

13.6 知识结构图

下面这张图帮你理清一元运算符重载的核心逻辑:

一元运算符重载核心逻辑 一元运算符重载 operator- operator! operator& 返回新对象 不修改原对象 成员函数 + const 返回 bool 检查空/无效状态 成员函数 + const 返回指针 谨慎使用 影响 std::addressof

说白了,一元运算符重载的核心就三件事:语义正确、不修改原对象、返回合理类型。你只要把握住这三点,写出来的代码既安全又易读。

公众号:蓝海资料掘金营,微信deep3321