C/C++的自增/自减操作符

我不喜欢用++--运算符。主要是他们的各种执行顺序让我觉得很懵,而且早期C/C++标准对它们行为定义不严格,所以很容易触发Undefined Behavior。

不过,最近接触到了C++的atomic类,不可避免地用到了自增/自减操作符,所以借此机会补一下知识盲区。

编译器的警告

首先,现在的编译器可以检测出自增/自减导致的UB。下面这三行代码全都是UB:

1
2
3
int n = (i++) + (++i);
int b = i + i++;
i = i++;

GCC会抛出一系列警告:

1
2
3
hello.c:6:15: warning: operation on ‘i’ may be undefined [-Wsequence-point]
6 | int n = (i++) + (++i);
| ~~^~~

所以任何讨论上述代码运行结果是什么的问题,都是无意义的。

为什么是UB?

首先我知道,在一条语句里(严格来说是两个顺序点之间)不能对一个变量赋值两次,所以上面i = i++这种写法是错误的。

但是,上面的第二行代码也是UB:

1
int b = i + i++;

乍一看,只有i++修改了一次i的值。你可能会觉得,程序会先执行b=i+i,最后自增i。但是标准里规定,如果在两个顺序点之间对一个变量又读又写,则该变量的旧值要全部用于计算变量的新值。在上面里,i的新值是i++决定的,和第一个i无关,所以该语句非法。

自增/自减的重载

在使用atomic变量或者迭代器的时候,自增/自减操作符会很常用。

++的重载是使用类似于下面的定义:

1
2
3
4
5
6
7
8
9
10
11
class Foo {
/* 前置版本自增操作符重载实现 (++obj) */
Foo operator++ () {
// ....
}

/* 后置版本自增操作符重载实现 (obj++) */
Foo operator++ (int) {
// ....
}
}

自增是单操作数的运算符,但是后自增却多了一个int参数。这算是一种折中的策略。如果没有这个int,那么前自增和后自增的函数头完全一样,就没办法区分了。对于这个“假参数”,编译器会自动传一个0进去,所以我们不会看到obj++1这样的代码。

造火箭

关于自增自减操作符,我看到过很多让我抓狂但偏偏又没错的写法。此处列举几个我见到过的例子。

*p++

在Linux内核里我经常看到类似*p++这样的写法。这样的代码要如何解释呢?

规则:自增/自减运算符的优先级高于解引用

因此p先与++结合,但后自增返回的是旧值,所以解引用解的是p的旧值。

map.erase(it++)

c++ - What happens if you call erase() on a map element while iterating from begin to end? - Stack Overflow

c++ - Does std::map::erase(it++) maintain a valid iterator pointing to the next element in the map? - Stack Overflow

参考文章

为什么在 C 语言中,i=1;i=(++i)+(++i)+(++i)+(++i); 得到 i 的结果是 15 而不是 14 ? - 知乎

c++ - Undefined behavior and sequence points - Stack Overflow

自增(++)自减(–)操作符 - C++操作符重载 - shadow_xwl的CSDN博客

c++ - Why use int as an argument for post-increment operator overload? - Stack Overflow

C++ Operator Precedence - cppreference.com


C/C++的自增/自减操作符
https://yalandhong.github.io/2022/11/27/cpp/pre-post-increment/
作者
Yaland Hong
发布于
2022年11月27日
许可协议