我们常常在代码中使用一些字符常量,比如 '\\n''\\t',以及字符串的结束符 '\\0'。但我们很少留意这些字符常量在实际使用中占用的内存大小。比如下面的示例:

#include <stdio.h>
#define NULL_CHAR '\\0'

int main(void)
{
	char null_char_literal = '\\0';
	char null_char_macro_literal = NULL_CHAR;
	printf("Size of int: %d\\n", sizeof(int));
	printf("Size of char: %d\\n", sizeof(char));
	printf("Size of '\\\\0': %d\\n", sizeof '\\0');
	printf("Size of null_char_literal: %d\\n", sizeof null_char_literal);
	printf("Size of null_char_macro_literal: %d\\n", sizeof null_char_macro_literal);

	return 0;
}

在这里用 null_char_literalnull_char_macro_literal 代表直接写在代码中的 '\\0' 和使用宏定义的 '\\0'。然后先后输出:

  • int 类型的内存分配大小
  • char 类型的内存分配大小
  • '\\0' 字符常量的内存分配大小
  • null_char_literal 变量的内存占用
  • null_char_macro_literal 变量的内存占用

我们将上述代码按照 C11 标准使用 gcc-11.4.0 在 x86_64 的 Windows Cygwin 环境编译后运行:

Size of int: 4
Size of char: 1
Size of '\\0': 4
Size of null_char_literal: 1
Size of null_char_macro_literal: 1

请留意第三行和第四五行的区别:

  1. '\\0' 字符常量占用了 4 字节内存
  2. 值为 '\\0'char 类型变量占用了 1 字节内存

而我们通常理解的 '\\0' 就应该是单字节字符,理应只占用 1 字节内存,为什么和一开始的 int 类型占用相同了呢 (皆为 4 字节)?为了进一步确认 '\\0' 字符常量占用了 4 字节内存,而 char 只占用了 1 字节,我们用 IDA Freeware 反汇编二进制文件后,看到如下代码 (只保留核心部分):

var_2= byte ptr -2
var_1= byte ptr -1

; ...
mov     [rbp+var_1], 0
mov     [rbp+var_2], 0
mov     edx, 4
lea     rax, Format     ; "Size of int: %d\\n"
mov     rcx, rax        ; Format
call    printf
mov     edx, 1
lea     rax, aSizeOfCharD ; "Size of char: %d\\n"
mov     rcx, rax        ; Format
call    printf
mov     edx, 4
lea     rax, aSizeOf0D  ; "Size of '\\\\0': %d\\n"
mov     rcx, rax        ; Format
call    printf
mov     edx, 1
lea     rax, aSizeOfNullChar ; "Size of null_char_literal: %d\\n"
mov     rcx, rax        ; Format
call    printf
mov     edx, 1
lea     rax, aSizeOfNullChar_0 ; "Size of null_char_macro_literal: %d\\n"
mov     rcx, rax        ; Format
call    printf

根据 var_1var_2 两个变量的内存地址,我们可以确定 char 类型变量确实只分配了 1 字节内存。而 GCC 显然直接将 sizeof 在编译时进行了替换,用结果填充了相应的汇编语句,即 '\\0' 字符常量占用了 4 字节,和 int 类型完全一致。

于是我们猜测,是不是字符常量在编译器处理时,就是当做整型 (int) 处理的呢?

根据 C 语言标准 (PDF) 6.4.4.4 Character constants 部分内容 (摘要)

An integer character constant has type int. The value of an integer character constant containing a single character that maps to a single-byte execution character is the numerical value of the representation of the mapped character interpreted as an integer. The value of an integer character constant containing more than one character (e.g., 'ab'), or containing a character or escape sequence that does not map to a single-byte execution character, is implementation-defined. If an integer character constant contains a single character or escape sequence, its value is the one that results when an object with type char whose value is that of the single character or escape sequence is converted to type int.

我们使用 '\\0' 这个单字符常量时,它的类型是整型,即 int 而不是 char。所以我们直接获取其大小时,确实应该是当前机器的整型数值的大小,即 4 字节。但是因为定义上述 null_char_literalnull_char_macro_literal 变量时显式指明了类型为 char,所以它会按照 char 类型变量的大小进行存储,也就是 1 字节。这就是为什么汇编中相对栈顶地址的偏移是 1 而不是 4 了。

同样的表述,我们还可以从 CPP Reference 的 C 语言 Character constant 部分找到佐证,参见其中关于 c-char 第一中类型的描述:

1) single-byte integer character constant, e.g. 'a' or '\\n' or '\\13'. Such constant has type int and a value equal to the representation of c-char in the execution character set as a value of type char mapped to int. If c-char is not representable as a single byte in the execution character set, the value is implementation-defined.

参考资料

  1. sizeof('\\0') null terminator as literal is four bytes but how come in string of characters it takes only one byte?
  2. Character constant