前言


C 语言诞生于 1972 年,至今仍是操作系统内核、嵌入式固件、数据库引擎和大量基础库的实现语言。它的特点可以概括成三句话:贴近硬件、类型系统简单但规则严格、把内存管理完全交给程序员。学习 C 语言的第一道坎不是语法,而是数据类型——每个类型占几个字节、能表示多大范围、什么时候会发生转换、哪些行为由标准规定、哪些由编译器自行决定,这些问题如果一开始就含糊,后面写出的程序会充满难以定位的怪现象。


初学者最常见的误解是「int 就是 4 个字节」「char 就是有符号的」「有符号整数溢出会变成负数」。这三条都不成立:int 的宽度由实现决定,标准只保证它至少 16 位;char 是否有符号是实现定义行为;有符号整数溢出是未定义行为,不是「回绕」。这些区别不是学究式的细节,它们直接决定了代码能不能移植、会不会在换平台后悄悄出错。


本文面向刚接触 C 的读者,从程序的基本构成讲起,然后系统梳理基本数据类型、sizeof 与取值范围、类型转换规则、字面量与格式化输出。文中所有代码以 C11 为基准,涉及实现定义行为的地方会明确标注,不会把「某个平台上如此」当成标准结论。


一、C 程序的基本构成


一个最小的 C 程序长这样:


/* 适用于 C11 */
#include <stdio.h>

int main(void) {
printf("hello\n");
return 0;
}

#include 是预处理指令,把 stdio.h 的内容原样插入当前文件;main 是程序入口,标准规定它返回 int,返回 0 表示正常结束;printf 声明在 stdio.h 中,参数个数可变。


从源文件到可执行文件通常经历四步:预处理(展开宏、插入头文件)、编译(生成汇编)、汇编(生成目标文件)、链接(把目标文件和库合并成可执行文件)。理解这四步有实际意义:头文件里的声明只影响编译,函数的实际代码在链接阶段才被找出来;「undefined reference」是链接错误,「implicit declaration」是编译阶段的警告。


二、基本数据类型


C 的类型系统可以分成四组:




类别类型说明



整数char、signed char、unsigned char至少 8 位,char 的有符号性由实现决定

整数short、short int至少 16 位

整数int至少 16 位,通常是 32 位

整数long、long int至少 32 位

整数long long、long long int至少 64 位,C99 起

浮点float、double、long double分别至少 6、10、10 位有效十进制数字

布尔_BoolC99 起,取值 0 或 1;包含 stdbool.h 后可用 bool、true、false

空void表示「无类型」,用于函数返回值、参数列表和 void*



标准只规定最小宽度,不规定确切字节数。int 在常见的 32 位和 64 位桌面平台上是 4 字节,但在一些 16 位嵌入式平台上可能是 2 字节。long 在 Windows 的 LLP64 模型下是 4 字节,在 Linux 的 LP64 模型下是 8 字节——这是跨平台代码里最容易出问题的一处。


需要固定宽度时,用 <stdint.h> 提供的类型:




类型含义



int8_t、int16_t、int32_t、int64_t恰好 8/16/32/64 位的有符号整数(若平台支持)

uint8_t、uint16_t、uint32_t、uint64_t对应无符号版本

intptr_t、uintptr_t能容纳指针值的整数类型

intmax_t、uintmax_t平台支持的最大宽度整数

size_tsizeof 的结果类型,无符号,定义在 <stddef.h>



int8_t 这类「恰好宽度」的类型不是所有平台都提供——标准允许实现不定义它们。真正需要可移植时,应当先检查这些 typedef 是否存在,或者退而使用「至少 N 位」的 int_leastN_t。


三、sizeof 与取值范围


sizeof 是编译期运算符,返回类型为 size_t。它作用于类型时写作 sizeof(int),作用于表达式时写作 sizeof x 或 sizeof(x)。有一个容易忽略的规则:sizeof(char) 恒为 1,这是 C 标准对「字节」的定义——一个字节不一定是 8 位,CHAR_BIT 才是每个字节的位数,标准只保证它至少为 8。


取值范围由 <limits.h> 和 <float.h> 给出:




宏含义



CHAR_BIT一个字节的位数

SCHAR_MIN、SCHAR_MAXsigned char 的范围

UCHAR_MAXunsigned char 的最大值

SHRT_MIN、SHRT_MAX、USHRT_MAXshort 相关

INT_MIN、INT_MAX、UINT_MAXint 相关

LONG_MIN、LONG_MAX、ULONG_MAXlong 相关

LLONG_MIN、LLONG_MAX、ULLONG_MAXlong long 相关,C99 起



无符号类型的范围是 0 到 2 的 N 次方减 1,其中 N 是位数。有符号类型在补码表示下是负的 2 的 N-1 次方到 2 的 N-1 次方减 1。C23 之前,标准允许有符号整数使用补码、反码或原码三种表示之一,C23 起补码成为唯一规定;无论如何,用 INT_MIN、INT_MAX 这些宏而不是手写数字,永远是最稳妥的做法。


下面这段程序把当前平台的关键参数打印出来,读者可以在自己的环境里核对:


/* 适用于 C11 */
#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(short) = %zu\n", sizeof(short));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(long long) = %zu\n", sizeof(long long));
printf("sizeof(void*) = %zu\n", sizeof(void *));
printf("sizeof(size_t)= %zu\n", sizeof(size_t));

printf("INT_MIN = %d\n", INT_MIN);
printf("INT_MAX = %d\n", INT_MAX);
printf("UINT_MAX = %u\n", UINT_MAX);
return 0;
}

%zu 是 C99 为 size_t 引入的格式说明,不要用 %d 或 %u 打印 sizeof 的结果——在 64 位平台上 size_t 通常是 8 字节,用 %d 属于格式与实参类型不匹配,是未定义行为。


四、类型转换与整型提升


C 的算术运算有一套明确的转换规则,顺序大致是:



  1. 整型提升:char、short(以及对应的无符号版本,如果 int 能表示其全部值)在参与运算前先转换成 int。

  2. 寻常算术转换:两个操作数类型不同时,转换成「等级更高」的那个;等级相同则无符号优先。

  3. 赋值转换:赋值时把右值转换成左值的类型,可能截断。


这套规则里最坑的是「有符号与无符号混用」。看这个例子:


/* 适用于 C11 */
#include <stdio.h>

int main(void) {
int a = -1;
unsigned int b = 1u;
if (a < b) {
printf("less\n");
} else {
printf("not less\n");
}
return 0;
}

按数学直觉,-1 小于 1,应该输出 less。但按 C 的规则,a 会被转换成 unsigned int,-1 变成 UINT_MAX,结果输出 not less。这类问题在比较 size_t 与 int 时尤其常见,比如 for (int i = 0; i < v.size(); ++i) 在 C++ 里会触发警告,在 C 里则是 int 与 size_t 比较,负数会被悄悄转成大正数。


整型提升还有一个副作用:两个 unsigned char 相加,结果类型是 int 而不是 unsigned char,所以不会在 8 位处回绕,只有赋值回 unsigned char 时才会截断。


五、字面量与格式化输出


字面量的类型由写法决定:




写法类型说明



42int十进制

42uunsigned int后缀 u

42Llong后缀 l 或 L

42ULLunsigned long long后缀组合

0x2Aint十六进制

052int八进制,前导 0

0b101010int二进制,C23 起为标准;更早的编译器可能作为扩展支持

3.14double浮点字面量默认是 double

3.14ffloat后缀 f

'A'int字符常量在 C 中的类型是 int,不是 char

"abc"char[4]字符串字面量,含结尾的 \0



最后一条值得强调:C 里的字符常量 'A' 类型是 int,这是历史遗留,与 C++ 不同(C++ 里是 char)。所以 sizeof('A') 在 C 里通常等于 sizeof(int),不要拿它去推算 char 的宽度。


输出 64 位整数时,格式说明必须与类型严格匹配。<inttypes.h> 提供了拼接用的宏:


/* 适用于 C11 */
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
int32_t small = INT32_C(-2147483647);
uint64_t big = UINT64_C(18446744073709551615);

printf("small = %" PRId32 "\n", small);
printf("big = %" PRIu64 "\n", big);
printf("hex = %" PRIx64 "\n", big);
return 0;
}

PRId32 展开成适合 int32_t 的格式串(例如 "d" 或 "ld"),PRIu64 对应无符号 64 位。这样写的好处是把「类型与格式是否匹配」交给实现去保证,代码在任何平台上都正确。


六、完整示例:安全地做无符号运算


/* 适用于 C11 */
#include <stdint.h>
#include <stdio.h>

/* 计算两个无符号数的平均值,避免相加溢出 */
uint32_t average(uint32_t a, uint32_t b) {
return (a & b) + ((a ^ b) >> 1);
}

int main(void) {
uint32_t x = UINT32_C(4000000000);
uint32_t y = UINT32_C(4000000000);

/* 直接相加会回绕:无符号运算按 2 的 32 次方取模,结果有定义但不对 */
uint32_t naive = (x + y) / 2;
uint32_t safe = average(x, y);

printf("naive = %" PRIu32 "\n", naive);
printf("safe = %" PRIu32 "\n", safe);
return 0;
}

无符号整数的溢出是有定义的:按 2 的 N 次方取模回绕。所以 x + y 不会崩溃,只是结果不再是数学上的 8000000000。average 用位运算把「相加」拆成「不进位部分」和「进位部分」,避开了中间结果溢出。注意这段代码只对无符号类型成立——如果换成 int32_t,x + y 就是有符号溢出,属于未定义行为,不能靠「回绕」来推理。


常见坑点


1. 把 main 写成 void


❌ void main() { ... } ✅ int main(void) { ... return 0; }


标准只承认 int main(void) 和 int main(int argc, char* argv[]) 两种形式。void main 在某些编译器上能通过,是扩展行为,不可移植,而且无法向调用方返回退出状态。


2. 认为 int 一定是 4 字节


❌ unsigned char buf[sizeof(int) * 4]; 然后假设能放下 16 字节 ✅ 用 sizeof(int) 直接参与计算,或用 <stdint.h> 的固定宽度类型


标准只保证 int 至少 16 位。把字节数写死,换到别的平台就会出错,而且这种错误往往在数据边界处才暴露。


3. 假定 char 有符号


❌ char c = 200; 然后期待 c > 0 ✅ 明确需要无符号时用 unsigned char,需要有符号时用 signed char


char 是否有符号由实现决定(implementation-defined)。在默认有符号 char 的平台上,200 超出范围,转换结果由实现定义;在无符号 char 的平台上则是 200。


4. 用 %d 打印 sizeof 的结果


❌ printf("%d\n", sizeof(int)); ✅ printf("%zu\n", sizeof(int));


sizeof 返回 size_t,在 64 位平台上通常是 8 字节无符号类型。用 %d 读取会导致实参与格式不匹配,属于未定义行为,输出可能是垃圾值。


5. 把有符号溢出当成回绕


❌ int x = INT_MAX; x = x + 1; 以为会得到 INT_MIN ✅ 需要回绕语义就用无符号类型;有符号运算前先检查边界


有符号整数溢出是未定义行为(undefined behavior, UB),编译器可以假设它不会发生,从而做出任意优化。无符号溢出才是有定义的模运算。


6. 用 gets 读字符串


❌ char buf[32]; gets(buf); ✅ char buf[32]; if (fgets(buf, sizeof buf, stdin) == NULL) { /* 处理错误 */ }


gets 无法知道缓冲区大小,C11 已把它从标准中移除。fgets 会保留换行符,必要时自行去掉。


7. 用 == 比较浮点数


❌ if (0.1 + 0.2 == 0.3) { ... } ✅ if (fabs((0.1 + 0.2) - 0.3) < 1e-9) { ... },容差按量级选取


二进制浮点无法精确表示大多数十进制小数,相加后的舍入结果与字面量不一定相等。比较浮点应该用容差,容差大小取决于数值量级和精度要求。


8. 用 %ld 打印 int64_t


❌ printf("%ld\n", (int64_t)value); ✅ printf("%" PRId64 "\n", value);


int64_t 的底层类型在 Linux 上可能是 long,在 Windows 上可能是 long long,%ld 只在其中一种平台上正确。用 <inttypes.h> 的宏可以避免这个问题。


总结




主题要点



类型宽度标准只规定最小值,sizeof 才是当前平台的真实答案

取值范围用 <limits.h> 的宏,不要手写数字

char 符号性实现定义,跨平台代码显式写 signed char 或 unsigned char

整型提升小类型运算前先提升为 int,结果类型可能出人意料

有符号/无符号混用有符号会被转成无符号,负数变成大正数

溢出无符号回绕有定义,有符号溢出是 UB

格式化输出格式说明必须与实参类型匹配,64 位用 PRI* 宏

固定宽度<stdint.h> 的 int32_t 等,但并非所有平台都提供



C 的数据类型不多,规则却不少,而且很多规则在写错时不会立刻报错,只会在换平台或换编译器时爆发。学习阶段的正确姿势是:凡是涉及宽度、范围、符号的问题,一律查 <limits.h> 和 <stdint.h>,不要凭「我在某台机器上见过」下结论;凡是涉及转换的地方,显式写强制转换或调整类型,让意图在代码里看得见。




Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐