C语言入门篇--初识C语言及数据类型
前言
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 位有效十进制数字 |
| 布尔 | _Bool | C99 起,取值 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_t | sizeof 的结果类型,无符号,定义在 <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_MAX | signed char 的范围 |
UCHAR_MAX | unsigned char 的最大值 |
SHRT_MIN、SHRT_MAX、USHRT_MAX | short 相关 |
INT_MIN、INT_MAX、UINT_MAX | int 相关 |
LONG_MIN、LONG_MAX、ULONG_MAX | long 相关 |
LLONG_MIN、LLONG_MAX、ULLONG_MAX | long 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 的算术运算有一套明确的转换规则,顺序大致是:
- 整型提升:
char、short(以及对应的无符号版本,如果int能表示其全部值)在参与运算前先转换成int。
- 寻常算术转换:两个操作数类型不同时,转换成「等级更高」的那个;等级相同则无符号优先。
- 赋值转换:赋值时把右值转换成左值的类型,可能截断。
这套规则里最坑的是「有符号与无符号混用」。看这个例子:
/* 适用于 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 时才会截断。
五、字面量与格式化输出
字面量的类型由写法决定:
| 写法 | 类型 | 说明 |
|---|
42 | int | 十进制 |
42u | unsigned int | 后缀 u |
42L | long | 后缀 l 或 L |
42ULL | unsigned long long | 后缀组合 |
0x2A | int | 十六进制 |
052 | int | 八进制,前导 0 |
0b101010 | int | 二进制,C23 起为标准;更早的编译器可能作为扩展支持 |
3.14 | double | 浮点字面量默认是 double |
3.14f | float | 后缀 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>,不要凭「我在某台机器上见过」下结论;凡是涉及转换的地方,显式写强制转换或调整类型,让意图在代码里看得见。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)