ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

详解C++编程中一元运算符的重载

详解C++编程中一元运算符的重载

前言


一元运算符是只接受一个操作数的运算符:取负-a、取正+a、逻辑非!a、按位取反~a、自增++a/a++、自减、解引用*p、取地址&a、成员访问p->m。重载它们,就是为自定义类型定义这些符号的行为,让Integer a; -a;这样的写法有意义。


重载一元运算符的语法比二元简单,但有几个必须拿准的细节:成员函数不带参数,非成员函数带一个参数;前缀++返回引用,后缀++返回旧值且要多一个int形参;operator-不应修改自身,应该声明为const并返回新对象。把这些签名写对,代码才能既符合直觉又能参与泛型算法。


常见误解是「重载了+就自动有-」——不会,每个运算符都要单独定义。另一个误解是「后缀++的int参数要传值」——它只是个占位符,编译器用来区分前缀和后缀,调用时不需要也不能显式传它。还有人把所有一元运算符都实现成修改自身,导致-a把a变成相反数,违反最小惊讶原则。


本文按「有哪些一元运算符、成员还是非成员、前缀与后缀、完整示例」四部分讲解,最后给出重载时的取舍原则。代码以 C++17 为准,explicit operator bool等 C++11 特性会注明。


一、一元运算符清单




运算符表达式语义常见返回类型



一元++a取正,通常返回副本T

一元--a取负T

!!a逻辑非bool

~~a按位取反T

**p解引用T&

&&a取地址T*(极少重载)

前缀++++a先自增,返回自身T&

后缀++a++返回旧值,再自增T

前缀----a先自减T&

后缀--a--返回旧值T

->p->m成员访问指针或代理

转换函数static_cast<bool>(a)类型转换bool(常加explicit)



operator&重载后,&obj不再返回对象地址,而是调用你写的函数。这会破坏依赖「取地址」语义的泛型代码,标准库用std::addressof绕过它。除非在写特殊代理类,否则不要重载operator&。


二、成员还是非成员


一元运算符可以写成成员函数,也可以写成非成员函数:


// 适用于 C++17
struct Num {
int v;
Num operator-() const; // 成员:无参数
};

Num operator-(const Num& x); // 非成员:一个参数

成员版本的左操作数隐含为*this,所以参数列表为空;非成员版本显式接收一个参数。两者不能同时定义,否则调用时产生二义性。


选择建议:



  • 需要访问私有成员、或运算符与类型强相关时,用成员;

  • 需要允许左操作数隐式转换时,用非成员(成员版本的左操作数必须是该类类型);

  • 一元运算符的左操作数就是对象本身,隐式转换的需求较少,所以多数写成成员。


赋值类运算符(=、+=)必须是成员;->、()、[]也必须是成员。一元++/--没有这个限制,但习惯上写成成员。


三、前缀与后缀


前缀和后缀的签名不同:


// 适用于 C++17
T& operator++(); // 前缀,返回引用
T operator++(int); // 后缀,int 是占位符,返回旧值

后缀的int参数没有名字,也不会被使用。编译器看到a++时,会自动传一个 0 进去,用来和前缀区分;你写a.operator++(0)也能调用后缀,但正常代码不会这么写。


返回类型的选择有明确理由:



  • 前缀返回T&,因为++a的结果就是a本身,链式写法++(++a)应该修改同一个对象;

  • 后缀返回T(按值),因为要保留自增前的值,不能返回引用——局部变量在函数返回后就销毁了,返回引用会悬垂。


后缀的典型实现是先拷贝旧值,再调用前缀,最后返回旧值:


// 适用于 C++17
T T::operator++(int) {
T old(*this);
++(*this); // 复用前缀逻辑
return old;
}

拷贝构造在这里不可避免:要返回自增前的状态,就必须保留一份副本。对重对象,后缀自增的成本比前缀高,这也是泛型代码里常推荐++it而不是it++的原因之一。


四、完整示例


下面的Integer重载了一元+、-、!、~和前后缀自增自减;RawPtr演示operator*与operator->。


// 适用于 C++17
#include <iostream>
#include <string>

class Integer {
public:
explicit Integer(int value = 0) : value_(value) {}

Integer operator-() const { return Integer(-value_); }
Integer operator+() const { return *this; }
bool operator!() const { return value_ == 0; }
Integer operator~() const { return Integer(~value_); }

Integer& operator++() {
++value_;
return *this;
}
Integer operator++(int) {
Integer old(*this);
++(*this);
return old;
}
Integer& operator--() {
--value_;
return *this;
}
Integer operator--(int) {
Integer old(*this);
--(*this);
return old;
}

int value() const { return value_; }

private:
int value_;
};

template <typename T>
class RawPtr {
public:
explicit RawPtr(T* p = nullptr) : p_(p) {}
~RawPtr() { delete p_; }

RawPtr(const RawPtr&) = delete;
RawPtr& operator=(const RawPtr&) = delete;

T& operator*() const { return *p_; }
T* operator->() const { return p_; }

private:
T* p_;
};

int main() {
Integer a(5);
const Integer b = -a;
std::cout << a.value() << ' ' << b.value() << '\n'; // 5 -5

std::cout << std::boolalpha << !a << ' ' << !Integer(0) << '\n'; // false true

Integer c(1);
Integer d = ++c;
std::cout << c.value() << ' ' << d.value() << '\n'; // 2 2

Integer e = c++;
std::cout << c.value() << ' ' << e.value() << '\n'; // 3 2

Integer f = +a;
std::cout << f.value() << '\n'; // 5

RawPtr<std::string> p(new std::string("hi"));
std::cout << p->size() << ' ' << *p << '\n'; // 2 hi
return 0;
}

按标准语义,输出依次是5 -5、false true、2 2、3 2、5、2 hi。


~的结果依赖有符号整数的表示。C++20 起标准规定符号整数采用补码,~5等于 -6;在 C++17 及以前,表示方式由实现定义,理论上可能是反码或原码,~5的值不一定相同。示例没有打印~的结果,就是为了避开这个差异。如果业务依赖按位取反的具体数值,应改用无符号类型。


RawPtr的operator*返回T&而不是T,这样*p = "new"才能修改指向的对象;返回按值就只能读。operator->返回裸指针,编译器会对返回值继续应用->,直到找到成员。若返回的是对象而不是指针,该对象还必须自己重载operator->。


五、重载的原则



  1. 保持语义直觉:-a不应修改a,++a应该修改,!a应返回布尔。让运算符的行为偏离内置类型,会让读代码的人困惑。

  2. 不要重载&&、||、,:它们的内置版本有短路或顺序求值保证,重载后变成普通函数调用,这些保证消失,容易埋坑。

  3. 谨慎重载operator&:它会改变取地址的含义,破坏泛型代码。

  4. operator bool加explicit(C++11):避免对象被隐式转换成整数,参与意外的算术或重载决议。

  5. 成对提供:有++就给--,有+就给-,保持接口完整;但不要为了「对称」实现没有意义的运算符。


常见坑点


1. 后缀++返回引用


❌T& operator++(int) { T old(*this); ++*this; return old; }—— 返回局部变量的引用,悬垂。


✅ 返回T(按值),让调用方拿到自增前的副本。


2. 后缀忘记int参数


❌ 同时定义T& operator++()和T operator++()—— 签名相同,编译报错。


✅ 后缀写成T operator++(int),参数只用于区分。


3. 一元-修改自身


❌T& operator-() { v = -v; return *this; }——-a把a改了,不符合语义。


✅T operator-() const { return T(-v); },返回新对象。


4.operator++返回void


❌void operator++()——++(++a)无法编译,泛型代码也可能要求返回引用。


✅ 前缀返回T&。


5. 非成员版本参数个数写错


❌T operator-(const T& a, const T& b);当成一元 —— 这是二元减法。


✅ 一元非成员只接收一个参数。


6. 后缀实现里返回*this


❌T operator++(int) { ++*this; return *this; }—— 返回的是自增后的值。


✅ 先保存旧值,再自增,返回旧值。


7. 重载operator&破坏取地址


❌ 为了「拦截取地址」而重载&,导致模板和标准库拿到错误地址。


✅ 不要重载;确需真实地址时用std::addressof。


8. 隐式operator bool


❌operator bool() const;不加explicit—— 对象能隐式转成整数,参与意外运算。


✅explicit operator bool() const;(C++11),在if条件里仍可正常使用。


总结




运算符签名(成员)返回是否修改自身



-aT operator-() constT否

+aT operator+() constT否

!abool operator!() constbool否

~aT operator~() constT否

++aT& operator++()T&是

a++T operator++(int)T(旧值)是

*pT& operator*() constT&否

p->mT* operator->() const指针/代理否



一元运算符重载的关键不在语法,而在「语义是否与内置类型一致」。取负返回新值、逻辑非返回布尔、前缀返回引用、后缀返回旧值——这四条对齐了,代码就能被直觉理解;签名写对只是让编译器接受。最后记住:运算符重载是给类型加「自然的语法」,不是炫技,语义不直观时,写一个具名函数往往更好。




返回列表