自增自减运算符
自增 ++ 和自减 -- 运算符将变量的值加 1 或减 1。它们有前缀和后缀两种形式,区别在于"先增减再使用"还是"先使用再增减"。这个看似简单的运算符是 C 语言中最容易出错的地方之一——在复杂表达式中混用自增运算符,可能产生未定义行为。
前缀与后缀
前缀形式 ++i、--i:先增减,再返回新值
int a = 5;
int b = ++a; /* a 先变为 6,然后 b = 6 */
printf("a = %d, b = %d\n", a, b); /* a = 6, b = 6 */
后缀形式 i++、i--:先返回原值,再增减
int a = 5;
int b = a++; /* b = 5(先返回原值),然后 a 变为 6 */
printf("a = %d, b = %d\n", a, b); /* a = 6, b = 5 */
两种形式都修改了变量,但返回的值不同:
int x = 10;
printf("%d\n", ++x); /* 输出 11,x 变为 11 */
printf("%d\n", x++); /* 输出 11,x 变为 12 */
printf("%d\n", x); /* 输出 12 */
常见用法
循环计数器:
for (int i = 0; i < 10; i++) { } /* 后缀即可 */
for (int i = 10; i > 0; --i) { } /* 前缀后缀效果相同 */
在独立的语句中,前缀和后缀效果相同,但前缀可能稍快(避免创建临时副本)。现代编译器优化后通常没有区别。
数组遍历:
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;
while (p < arr + 5) {
printf("%d ", *p++); /* 先输出 *p,然后 p 递增 */
}
/* 输出:10 20 30 40 50 */
清零计数器:
int count = 10;
while (count-- > 0) { /* 先比较 count > 0,然后 count 减 1 */
printf("%d ", count); /* 输出 9 8 7 6 5 4 3 2 1 0 */
}
注意 count-- > 0 的执行顺序:先取 count 的原值与 0 比较,然后 count 减 1。所以循环体中 count 已经减过了。
危险:复杂表达式中的自增
C99 规定:两个序列点之间,同一对象最多被修改一次,且读取的值只能是用于计算新值的。违反这一规则就是未定义行为。
int i = 5;
/* 未定义行为:同一表达式中 i 被修改两次 */
int a = i++ + i++; /* 结果可能是 10、11 或 12,取决于编译器 */
/* 未定义行为:i 被修改,且读取的值不用于计算新值 */
int b = i++ + i; /* 未定义行为! */
/* 未定义行为:函数参数求值顺序未指定 */
printf("%d %d\n", i++, i++); /* 未指定行为:哪个 i++ 先执行? */
这些代码可能"恰好工作",但换编译器、换优化级别、甚至换编译器版本,结果都可能不同。永远不要写这种代码。
安全做法:将自增拆分为独立语句:
int i = 5;
int a = i + i; /* 10 */
i++; /* i = 6 */
i++; /* i = 7 */
/* 或 */
int temp1 = i++; /* temp1 = 5, i = 6 */
int temp2 = i++; /* temp2 = 6, i = 7 */
int result = temp1 + temp2; /* 11 */
指针的自增
指针的自增按所指向类型的大小为步长:
int arr[] = {10, 20, 30};
int *p = arr;
printf("%d\n", *p); /* 10 */
p++; /* p 增加 sizeof(int) 字节,指向 arr[1] */
printf("%d\n", *p); /* 20 */
char str[] = "ABC";
char *cp = str;
cp++; /* p 增加 1 字节,指向 'B' */
printf("%c\n", *cp); /* B */
常见错误
对非左值使用自增:
int a = 5, b = 3;
(a + b)++; /* 错误:a + b 不是可修改的左值 */
5++; /* 错误:字面量不能自增 */
在条件中误用:
if (i++ == 5) /* 先比较 i == 5,然后 i 加 1 */
/* ... */
if (++i == 5) /* i 先加 1,再与 5 比较 */
/* ... */
两种写法逻辑不同,选择取决于需求。
与赋值混用:
int i = 0;
int arr[5] = {10, 20, 30, 40, 50};
/* 危险 */
int x = arr[i++]; /* 安全:先取 arr[0],i 变为 1 */
int y = arr[++i]; /* 安全:i 先变为 2,取 arr[2] */
/* 危险 */
int z = arr[i++] + arr[i++]; /* 未指定行为! */
过度追求简洁:
/* 晦涩 */
*ptr++ = *src++;
/* 清晰 */
*ptr = *src;
ptr++;
src++;
*ptr++ = *src++ 是 C 语言中常见的模式(复制字符串等),有经验的程序员能读懂,但对初学者来说,拆分为多行更清晰。
最佳实践
- 在独立语句中使用自增(不与其他运算混用)
- 需要表达式的值时,明确选择前缀或后缀
- 永远不在同一表达式中对同一变量使用多次自增/自减
- 不在函数参数中使用自增(求值顺序未指定)
- 优先保证可读性,而非一行代码的"巧妙"