恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
C++单目操作符详解:从优先级到自增自减与重载实战
首页
资讯中心
/
C++单目操作符详解:从优先级到自增自减与重载实战
C++单目操作符详解:从优先级到自增自减与重载实战
发布时间:2026/10/12 8:44:17
单目操作符在C入门资料里往往被薄薄一页带过可实际上它才是新手真正开始写表达式时最容易阴沟翻船的地方。你可能会觉得不就是-、!、、--这些东西吗还真不是。同样是*在声明里是指针的一部分在表达式里可能是解引用也可能是乘法同样是可能是取地址也可能是按位与。搞不清楚这些“单目”和“双目”的区分看代码就像看加密文字写代码更是踩坑踩到怀疑人生。这篇文章就把C入门阶段最关键的单目操作符一次性讲透从语法规则到实际建议把那些编译器不告诉你的细节全翻出来。1. 被低估的单目操作符入门阶段就该搞清楚的运算符家族1.1 从“几目”说起单目、双目、三目的定义在C里操作符按“操作数个数”分类这个“目”就是操作数的意思。单目操作符只需要一个操作数比如-5里的负号作用在5上双目操作符需要两个操作数比如a - b里的减号三目操作符只有一个就是?:它需要条件、真值、假值三个操作数。这个分类看起来简单但它决定了表达式的解析方式。C里同一个符号往往是“多面手”比如-既是一元负号也是二元减法*既是解引用也是乘法既是取地址也是按位与。编译器判断这个符号到底该当“单目”还是“双目”靠的不是符号本身而是符号在表达式中的位置和前后文。如果符号左侧已经有了一个完整的表达式它就倾向于是双目如果左侧没有操作数它就只能当单目。很多新手一上来就背优先级表背到后面还是一团浆糊。我的建议是先把“单目操作符有哪些”记熟因为单目操作符的优先级整体高于所有双目操作符把它单独拎出来优先级瞬间就清爽了一大半。1.2 先认全C里的单目操作符C里的单目操作符比很多人想象的多得多下面这张表把入门阶段会碰到的都列出来了操作符名称示例简要说明一元正号x不改变值但会做整型提升-一元负号-x取相反数!逻辑非!x把真变假、假变真结果是bool~按位取反~x对整数的每一位取反前置/后缀自增i/i给变量加1--前置/后缀自减--i/i--给变量减1*解引用*p访问指针指向的对象取地址a获得变量的地址sizeof大小运算符sizeof(int)返回类型或对象的字节大小new/new[]动态分配new int/new int[10]从堆上分配内存delete/delete[]动态释放delete p/delete[] arr释放new分配的内存static_cast静态转换static_castdouble(n)编译期类型转换之一const_cast常量转换const_castchar*(p)去掉或添加 constreinterpret_cast重解释转换reinterpret_castuintptr_t(p)不安全的位级转换dynamic_cast动态转换dynamic_castD*(p)运行时多态转换除了这些还有alignof、typeid这类比较冷门的单目运算符。入门阶段先认个脸熟真到用的时候再查不迟。重点是每一种操作符背后都有一套独立的运算规则它们之间没有“长得像就一回事”的关系。2. 正负号与算术运算符最容易混淆的“一张脸”2.1 一元负号 vs 二元减法编译器怎么区分先看这个表达式a - -3。阅读它的人会疑惑怎么两个减号挤在一起其实第一个-是二元减法第二个-是一元负号。编译器在解析时先看到了a这个操作数遇到-时它知道这是双目减法再往后解析-的右边又出现一个-但这个位置左边没有操作数所以它把后者当成一元负号。再看3 - - 2你可能会收到编译错误或者解析出诡异的结果因为- -连在一起可能被词法分析器识别成--自减符号。所以写代码时连续负号中间必须加空格3 - -2或者加括号3 - (-2)。这是C入门阶段极其经典的一个坑别看它小很多人的第一份奇怪报错就是从这里来的。一元正号x用得少但它有一项隐蔽作用对char、short、枚举等类型施加整型提升。比如char c A;时c的结果是int类型而不是char。在模板元编程里有人会用来触发类型推导这算是单目操作符的冷门知识不推荐新手马上用但要知道不是纯装饰。2.2 优先级与结合性负号为什么比乘法高单目正负号优先级高于乘法、除法和加减法并且是右结合。所以-a * b会解析成(-a) * b而不是-(a * b)。如果我要表示“负的 a 的平方”自然写-a * -a如果想写“负的乘积”就得写-(a * b)。右结合是什么意思看这个- - a。由于一元负号右结合实际含义是-(-a)。假如没有右结合- -a无法确定先算哪个-。C里所有前缀单目操作符都是右结合的*p中后缀优先级更高所以是*(p)*p中前缀和*优先级相同且右结合所以是(*p)。这里有一个容易搞混的地方-a到底是(-a)还是-(a)答案是后者。后缀的优先级远高于一元负号所以-a等价于-(a)。这类细节在选择题里考得很频繁但实际写代码时我的建议很简单复杂表达式别秀技一律拆开加括号不存在谁更厉害。2.3 实战中常见误区最典型的就是把^当乘方用。-a^2在C里不是“负的 a 的平方”而是“负 a 按位异或 2”。因为^是按位异或操作符优先级还比负号低。数学里的乘方符号C里没有要用函数pow(a, 2)或直接a * a。其次是%取模时别忘了负号优先级。-a % b等价于(-a) % b结果的符号在C11之后规定为“由被除数决定”但如果想表达-(a % b)必须加括号。很多人在这上面栽过跟头不是理论不会而是表达式写得太长后括号就丢了。还有一个经验不要在输出语句里写过于复杂的单目表达式。比如std::cout -a std::endl;读代码的人要停下来想很久才知道是-(a)。不如先int b -a; a;再输出b。工程代码的核心不是“我能写得多短”而是“别人在读的时候不会误解”。3. 自增自减前缀和后缀的恩怨情仇3.1 前缀i与后缀i的区别到底在哪绝大多数人挂在嘴边的是“前缀先加再用后缀先用再加”这句话方向没错但遗漏了最关键的一点返回值。i返回的是i自增之后的那个对象本身它是一个左值所以i 10是合法的虽然写上会被人嫌弃。i返回的是自增之前的一个旧值拷贝它是个右值所以i 10不合法。换句话说后缀自增本质上做的是“复制旧值、自增对象、返回临时值”的三件事。这个差异在性能上差别巨大。对内置类型int来说编译器能够优化掉临时对象i和i生成的机器码几乎一样。但对自定义类型比如迭代器i必须额外构造成员副本这个副本可能还牵涉资源的拷贝代价就出来了。所以有一条公认的代码规范不依赖旧值的时候一律写i而不是i。3.2 在表达式里乱用出现未定义行为这是C入门最吓人的一个知识点。i i i;这段代码看起来只是在“考逻辑”实际上它是未定义行为结果完全不可预测。不同编译器可能给出不同结果同一编译器在不同优化级别下也可能给出不同结果甚至在你没注意的时候“正常”跑出个诡异数字。为什么未定义因为C标准规定同一个表达式内对同一个标量对象的多次“副作用”自增、自减、赋值等之间必须有明确的顺序如果它们的执行顺序未定整个程序的行为就不受语言标准保护。像arr[i] i;这种也是典型反例左边和右边都修改了i谁先谁后说不清。我在实际写代码和帮人review时只要看到一行里对同一个变量出现两次及以上或--就会直接标红。这不是保守而是这种行为没有任何跨平台、跨编译器的一致性。要增加i就单独写一行i要使用那个位置再自增就写进arr[i] x这种“只有一次修改”的惯用法。别去挑战编译器编译器在未定义行为上不会给你面子。3.3 什么时候用前缀什么时候用后缀前缀自增自减的适用场景循环计数、迭代器遍历、临时变量运算等一切“只需要最终值”的场合。for (int i 0; i n; i)就是标准写法。后缀的适用场景只有一类你需要“旧位置的值”同时让变量往后走一步。比如把数组元素拷贝到另一个数组int i 0; int j 0; while (j n) { dest[i] src[j]; }这段代码里i返回旧值正好是dest要写的位置写完再把数组下标往后挪一位。再比如遍历链表时while (p p-val ! target) { p p-next; }如果有需要先取当前节点再后移可以写Node* cur p;但可读性不如拆开。我的经验是后缀自增不是不能用是要用得克制并且最好被读懂。你写*p没问题读的人也知道这是“指针后移取旧位置的元素”这是一种惯用法。但如果后面的逻辑不是这种经典模式就老实拆成两行。4. 剩下的一批单目操作符逻辑非、按位非、地址与解引用4.1!和~两个“取反”别搞混!是逻辑非它只关注操作数的“真/假”结果永远是bool。!0是true!非0是false。~是按位取反它把操作数每个二进制位从0变1、1变0结果是一个整数操作数必须是整数或能隐式转成整数的类型。看一个具体例子int a 5; // 二进制 000...0101 int b ~a; // 二进制 111...1010补码表示下值为 -6 bool c !a; // 5 是非零值所以 c 为 false如果只看到表面值~5是-6!5是false完全不同。很多bug都出在“我想判断某个东西不等于 -1”时写成了if (~a)。对于int a 0~a -1非零也算真但如果要表达a ! -1正确写法就是a ! -1千万别拿~a当布尔判断用。还要注意!的优先级比关系运算符、算术运算符都低。比如!i j解析为(!i) j而不是!(i j)。这种表达式建议加括号否则读者要猜。4.2*和解引用与取地址指针学习的基石指针是C入门的大山*和就是刨这座山的锄头int a 42; int* p a; // a 取 a 的地址赋给指针变量 p *p 99; // *p 解引用通过 p 找到 a把 a 改成 99取地址作用于左值时返回该左值的地址是一个指针值*解引用作用于指针值返回其指向对象的左值。两者互为逆运算*(a) a。这里有一个结合性细节极其容易出错*p因为后缀优先级高于前缀*实际是*(p)它先移动指针再解引用旧的指针位置。而(*p)是解引用得到元素后对元素本身自增。两者的含义差之毫厘、谬以千里。看这段int arr[] {10, 20, 30}; int* p arr; std::cout *p; // 输出 10p 指向 arr[1] std::cout *p; // 输出 20如果要既输出元素又让该元素自增就应该这么写std::cout (*p); // 输出 10arr[0] 变成 11不要把这两个表达式混在一起记。我在做代码审阅时常看到有人写*p却以为是“把 p 所指对象自增”结果指针到处乱跳却浑然不觉。另外一个新手高频坑声明多个指针时写在同一个声明里。int* p1, p2; // 只有 p1 是指针p2 是普通 int要两个指针必须写成int *p1, *p2; // 星号贴近变量名更不容易错虽然这是声明语法不是运算符但根源还是对“*是用来修饰变量、表示该变量是指针”的理解不够。反正我的习惯是每个变量单独一行谁是指针一目了然。4.3sizeof、new、delete隐藏的单目操作符sizeof看起来像函数其实它是一个单目操作符。它返回操作数的字节大小类型是size_t。它最反直觉的特性是不会真正求值操作数。所以sizeof(i)并不会让i增加因为编译器只关心类型大小不计算这个表达式值。很多人拿着这个知识点去“考验”别人但实际工程里不推荐写这类代码因为太容易误导。sizeof作用于数组和指针结果完全不同int arr[10]; int* p arr; std::cout sizeof(arr); // 40整个数组字节数 std::cout sizeof(p); // 8 或 4指针本身大小如果你把数组作为函数参数传进去形参会退化成指针函数内sizeof得到的永远是指针大小这是经典误区。想求数组长度要么把数组长度作为额外参数传入要么用模板推导template typename T, size_t N size_t arraySize(T ()[N]) { return N; }new和delete也是单目操作符。new int从堆上分配一块足以存放int的内存并返回指向这块内存的指针new int[10]分配数组delete p释放单个对象delete[] arr释放数组。配对规则必须严格遵守用new[]分配的就必须用delete[]释放用new分配的就必须用delete释放混用会产生未定义行为堆内存损坏都是轻的。现代C入门已经很少直接推荐裸指针new/delete了更建议用std::vector、std::string和智能指针。但作为知识体系new/delete的语法必须能看懂否则市面上大量老代码都要卡壳。5. 单目操作符的重载C进阶的独特玩法5.1 哪些单目操作符可以重载哪些不行C允许用户自定义类型重载大部分操作符单目操作符中一般可重载的有、-、!、~、*、、、--以及new、delete、类型转换操作符。但sizeof、typeid、alignof不能重载三目?:不能重载作用域::、成员访问.、.*也不能重载。为什么标准要限制这些因为sizeof需要在编译期知道类型大小如果它被重载成函数编译器就没法静态计算了.成员访问如果需要重载语义会变得极其诡异。语言把这些基础能力牢牢握在自己手里才能保证核心语义不被破坏。重载单目操作符的格式一般是成员函数或友元函数。比如重载一元负号class Money { public: Money operator-() const { Money tmp *this; tmp.amount -tmp.amount; return tmp; } private: double amount; };通过成员函数重载时操作数就是这个对象本身所以只有一个隐式参数。5.2 重载前置和后置自增的签名陷阱前置和后置的签名不同后置要额外加一个int哑元来区分。标准写法如下class Counter { public: Counter operator() { value_; return *this; } Counter operator(int) { Counter tmp(*this); value_; return tmp; } private: int value_ 0; };前置返回引用后置返回旧值的副本。这个设计严格模仿了内建行为前置可以连续递增后置需要旧值。很多人重载出错是把后置也返回了引用或者把前置返回了值。前一个错误会导致a可以“跨对象操作”后一个错误会导致临时对象被修改语义完全拧巴。重载的另一个细节是后置先保存副本再调用前置这样代码复用干净也避免了重复实现自增逻辑。同时要注意一次性做两件事修改对象和返回旧值这两个动作在这个自定义类里也是“手工组合”所以更容易写错。建议单元测试里专门写几组表达式验证别想当然。5.3 别用重载破坏直觉操作符重载不是让你创造新语言。它应该让类型用起来更自然。比如重载是为了输出重载[]是为了像数组一样访问重载!是为了表示“非”。如果我看到有人把重载成大于或者把!重载成“价格翻倍”会直接打回工单重写。有些重载连老手都劝退。最典型的就是重载。operator在自定义类里可以对类的实例返回任意值导致你无法用常规obj拿到真正的地址代码里要绕路用std::addressof。这在模板库中也许有极少数用途但一个普通项目里出现这种重载几乎等于自杀。我的建议是入门阶段掌握“哪些能重载、签名长什么样、语义必须符合直觉”就够了不要急着在自己的类里重载一堆操作符。优先用普通函数命名比如negate()、increment()让意图直白。等你真正遇到需要表达式内嵌的容器、迭代器、数值类对象时再按标准模式重载。6. 记忆口诀与自查清单把单目操作符用稳6.1 优先级记忆技巧C的优先级表很长但单目操作符其实能帮你切开一个突破口。我的记忆层级是这样的后缀操作符、--、()、[]、-前缀单目、--、!、~、、-、*、、sizeof、new、delete等乘除、取模加减移位关系比较按位与/异或/或逻辑与/或三目赋值逗号单目操作符整体高于所有双目操作符这是不容易记错的一条。唯一要小心的是后缀单目比前缀单目还要高。所以在看*p时先找后缀再找前缀*就能确定是*(p)看*p时前后缀同时出现因为和*都是前缀且优先级相同、右结合所以是(*p)。遇到特别复杂的表达式脑子里过一遍“后缀 前缀 乘除 加减”然后立刻不想了直接加括号。你的目标不是证明你自己优先级背得多好而是让代码在任何编译器、任何读代码的人眼里都没有歧义。6.2 代码自测清单你可以先用下面的表达式测试自己对单目操作符的理解再对照答案int i 0; int j i i; // 存在未定义行为不要这么写 int arr[] {1, 2, 3, 4}; int* p arr; std::cout *p ; // 输出 1p 指向 arr[1] std::cout *p ; // 先 p 指向 arr[2]然后解引用输出 3 std::cout (*p) \n; // 输出 3arr[2] 变成 4 int a 5; bool b !a; // false int c ~a; // -6 int n sizeof(i); // i 仍然是 0n 为 4第一句是故意写出来告诫你的千万别在真实代码里出现后面几个则是经典考点。如果每个结果你都能脱口而出单目操作符的核心语义基本就过关了。6.3 常见问题速查表问题/报错可能原因解决建议3--2编译报错两个负号被识别成--写成3 - -2或3 - (-2)i i i;结果不稳定同一变量多次修改且无顺序拆成多行一行只改一次*p结果和自己想的不一样后缀优先级高于*确认意图想自增元素用(*p)sizeof(数组形参)得到指针大小数组参数退化为指针用模板推导或在外部传数组长度new[]配delete释放方式不匹配一一对应用delete[]释放数组if (~a)代替if (a ! -1)~a是整数不是布尔值用明确比较条件重载报错前置/后置签名不对前置返回引用后置加int哑元返回旧值副本a 10编译失败前置自增返回了 const 值/右值标准实现应返回非 const 左值这张表我建议你收藏到项目笔记里写代码时遇到相关报错回来翻一下能省不少时间。最后再分享一个从带新人过程中总结的小技巧单目操作符的学习不要靠“背优先级”要靠“读表达式”。每看到一个包含单目操作符的语句先在旁边用注释写出它的完整等价形式再把等价形式运行一下。坚持两周你对*p、*p、-a这类表达式的敏感度会大幅提升写代码时也能自然避开未定义行为和可读性灾难。C入门阶段能把单目操作符这一关稳稳过去后面的运算符重载和模板学习都会顺得多。