起因是碰到这样一个问题:

uint8_t a = 0x1;
请问 ~a 是多少?
A. 0xfe
B. 0xffff fffe

乍一看不就是选 A 吗?但答案却是 B,验证了一下确实是这样:

#include <stdint.h>
#include <stdio.h>

int main() {
  uint8_t a = 0x1;
  printf("%x, %zu\n", ~a, sizeof(~a));  // fffffffe, 4

  return 0;
}

仔细一想,模糊的记忆被唤起,是有在一些文章中看到过这个知识点:Integer promotions。

查阅资料看到:

Integer promotion is the implicit conversion of a value of any integer type with rank less or equal to rank of int or of a bit-field of type _Bool(until C23)bool(since C23), int, signed int, unsigned int, to the value of type int or unsigned int.

简单来说就是小于 int 类型的被提升为 int 类型的过程。那么什么情况下会出现?

Note: integer promotions are applied only

  • as part of usual arithmetic conversions
  • as part of default argument promotions
  • to the operand of the unary arithmetic operators + and -
  • to the operand of the unary bitwise operator ~
  • to both operands of the shift operators « and »

前两条就不用说了吧,太常见了:两个不同类型的值加减乘除,肯定会有一个类型提升;实参和形参类型对不齐也会类型提升。

文章开头的问题考的就是后面几个知识点,正负转换、取反和位移操作也会 Integer promotions!

可以通过汇编看一下这个过程:

movb    $1, -1(%rbp)
movzbl  -1(%rbp), %eax    // 挪到 eax 并在前面补 0
notl    %eax              // 取反
movl    %eax, %esi        // 结果挪到 esi
leaq    .LC0(%rip), %rdi
movl    $0, %eax
call    printf@PLT
movl    $0, %eax
leave

可以看到 0x1 被挪到 32-bit 的 eax 寄存器里,对整个 eax 寄存器取反,接着把整个 32-bit 结果挪到 esi 寄存器等着调用 printf 函数。

至于为什么要类型提升到 int 再操作,我猜和机器指令有关了,或许直接对 32-bit 的 eax 操作,比对 8-bit 的 al 更便宜。或许这就是 C++ 的心智负担。

所以为了避免踩到坑,尽量避免使用小于 int 的类型,如果碰到就做个类型转换。

Ref

Implicit conversions