🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。

🔥 本文定位 :第 2 讲内容较多,因此拆成上下两篇。上篇从"类型为什么存在"出发,梳理字符、整数、浮点和布尔类型,说明
sizeof、size_t、signed/unsigned、类型范围、变量初始化、作用域、存储期与同名遮蔽。
💡 学习目标 :能读懂常见内置类型声明;正确使用sizeof与%zu;理解类型宽度为何不能写死;区分char、signed char与unsigned char;会通过<limits.h>和<float.h>查询能力;能区分变量的作用域、存储期和常见物理存放位置。
📌 阅读说明 :本文以标准 C 的可移植语义为主,示例可在 GCC、Clang 和 Visual Studio 中练习。教材中的 VS2022 x64 输出只是一个具体实现的结果,不应被当成所有平台的固定结论。下篇将继续讲运算符、类型转换、printf与scanf。
文章目录
- 先建立全景图:类型到底解决什么问题
- C 语言内置类型地图
- 字符类型:三个名字不是完全同义
- 整数类型:short、int、long 与 long long
- 浮点与布尔:近似值和真假值
- sizeof:查询对象表示所占的字节数
- signed 与 unsigned:相同比特的不同解释
- 不要猜范围:limits.h 与 float.h
- 变量的创建、初始化与未初始化风险
- 局部变量、全局变量与作用域
- 存储期、链接与"栈区/静态区"
- 同名变量的遮蔽规则
- 一份可运行的类型观察程序
- 常见误区、高频问题与练习
- 总结
一、先建立全景图:类型到底解决什么问题
生活中我们会区分年龄、身高、温度、字符和真假。计算机内存里没有天然的"年龄"或"字符",只有一组组比特。C 语言中的类型,就是程序和编译器对这些比特所作的约定。
例如,同样一组二进制位可以被解释成:
- 一个有符号整数;
- 一个无符号整数;
- 一个浮点数编码;
- 若干字符;
- 某种更复杂对象的一部分。
类型至少影响四件事:
- 一个对象需要多少存储空间;
- 某一组比特怎样被解释;
- 它能参与哪些运算;
- 运算时是否需要类型转换。
因此,"类型"不是给变量贴的装饰标签,而是编译器理解程序的基础。
c
int age = 18;
double temperature = 36.5;
char grade = 'A';
这里的 18、36.5 和 'A' 最终都会进入内存,但编译器对三个对象生成的读写和计算方式并不相同。

初学阶段最重要的不是把所有类型名称一次背完,而是形成主线:先确定数据的语义与范围,再选择类型;先看操作数类型,再判断表达式怎样计算。
二、C 语言内置类型地图
本讲主要讨论语言直接提供的内置类型。数组、指针、函数、结构体、联合体和枚举等,会在后续章节中继续展开。
2.1 字符类型
c
char
signed char
unsigned char
这三个都是字符类型,但它们并不完全等价。尤其是普通 char 到底表现为有符号还是无符号,由编译器实现决定。
2.2 标准有符号整数类型
c
signed char
short int
int
long int
long long int
其中很多关键字可以省略:
c
short /* 等价于 short int */
signed /* 等价于 signed int */
long long /* 等价于 long long int */
实际代码中通常使用更简洁、也更符合习惯的写法。
2.3 标准无符号整数类型
c
unsigned char
unsigned short int
unsigned int
unsigned long int
unsigned long long int
同样可以省略部分 int:
c
unsigned short
unsigned
unsigned long
unsigned long long
2.4 浮点类型
c
float
double
long double
它们用于表示带小数部分、范围很大或很小的数。浮点数通常是近似表示,不能默认每个十进制小数都能被二进制浮点格式精确保存。
2.5 布尔类型
C99 引入 _Bool。包含 <stdbool.h> 后,还可以使用更易读的 bool、true 和 false:
c
#include <stdbool.h>
bool ready = true;
if (ready) {
/* 条件为真 */
}
在经典 C 代码中,整数 0 表示假,非零值在条件判断中表示真。把任意标量值转换为 _Bool 时,零得到 0,非零得到 1。
三、字符类型:三个名字不是完全同义
3.1 char 的基本职责
char 是 C 语言中最小的可寻址对象类型。标准规定:
c
sizeof(char) == 1
这里的 1 是一个 C 字节,不直接等于"十进制 8 个比特"的语言承诺。一个字节包含多少比特可通过 <limits.h> 中的 CHAR_BIT 查看;现代通用平台通常是 8。
c
#include <limits.h>
#include <stdio.h>
int main(void)
{
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("sizeof(char) = %zu\n", sizeof(char));
return 0;
}
3.2 plain char 的符号性由实现决定
下面三个类型要分开看:
c
char a;
signed char b;
unsigned char c;
普通 char 会采用有符号或无符号行为中的一种,但它仍是独立类型。不要依赖某台机器上 char 恰好有符号,就把这条结论写进跨平台协议。
如果数据明确是小整数,需要负数,可以考虑 signed char;如果要检查对象的原始字节表示,unsigned char 更合适;如果表达的就是字符文本,通常使用普通 char。
3.3 字符常量会参与整数运算
c
char digit = '7';
int value = digit - '0';
字符编码中 '0' 到 '9' 连续,因此这里得到整数 7。表达式计算时,小整数类型通常还会先发生整数提升,这也是为什么"变量是 char"不代表整条表达式都按窄字符宽度完成。
四、整数类型:short、int、long 与 long long
4.1 类型等级不等于固定字节表
很多入门表格会写:
text
short 2 字节
int 4 字节
long 4 或 8 字节
long long 8 字节
这是常见平台的结果,不是标准对所有实现的逐项固定。标准更关注最低表示能力和相对等级。不同数据模型中,long 尤其可能不同:
- Windows 64 位环境常见
int为 32 位、long仍为 32 位; - 许多 64 位 Unix-like 环境常见
int为 32 位、long为 64 位。
所以,二进制协议、文件格式和网络报文如果要求精确位宽,不应仅凭"我电脑上的 long 是多少"来设计。
4.2 类型至少满足怎样的相对关系
从表示能力上,可以形成下面的直觉:
text
signed char → short → int → long → long long
后一级的范围不能比前一级更弱,但相邻类型允许具有相同大小。sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long) 是可依赖的相对关系之一,不能据此推断每一步都严格增加。
4.3 为什么默认整数通常选择 int
int 往往对应目标平台处理整数最自然的宽度。对于普通计数、循环变量和函数返回状态,它通常是合适起点。
选择更小类型不必然更快;在表达式中,小整数常被提升为 int。选择类型应优先考虑语义、范围、接口和内存布局,而不是凭名字猜性能。
五、浮点与布尔:近似值和真假值
5.1 float、double 与 long double
三种标准浮点类型通常提供逐级不降低的范围和精度:
c
float f = 3.14f;
double d = 3.14;
long double ld = 3.14L;
字面量后缀很重要:
3.14的类型是double;3.14f或3.14F的类型是float;3.14L或3.14l的类型是long double。
浮点数的"范围"和"精度"是两件事。能表示很大的数量级,不代表所有整数或所有小数都能精确表示。
c
#include <stdio.h>
int main(void)
{
double x = 0.1;
printf("%.17f\n", x);
return 0;
}
输出可能显示出 0.1 的近似误差。这不是 printf 算错,而是有限二进制位对十进制小数的近似表示。
5.2 _Bool 会规范化为 0 或 1
c
#include <stdbool.h>
#include <stdio.h>
int main(void)
{
bool a = 0;
bool b = -20;
bool c = 3.5;
printf("%d %d %d\n", a, b, c);
return 0;
}
这里会观察到 0 1 1。输出时 _Bool 会经过整数提升,所以可用 %d 打印提升后的 int 值。
六、sizeof:查询对象表示所占的字节数
sizeof 是运算符,也是关键字。它不是普通函数。
6.1 两种常见写法
操作数是类型名时,括号不能省略:
c
sizeof(int)
sizeof(double)
操作数是表达式时,语法上可以省略括号:
c
sizeof value
sizeof(value)
sizeof(value + 3.5)
实际代码中保留括号通常更便于阅读,但要知道它不是函数调用括号。

6.2 sizeof 返回 size_t
sizeof 的结果类型是 size_t,它是实现选择的一种无符号整数类型,定义在多个标准头文件中。
打印 size_t 应使用 %zu:
c
#include <stdio.h>
int main(void)
{
int value = 10;
printf("sizeof value = %zu\n", sizeof value);
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(value + 3.5) = %zu\n", sizeof(value + 3.5));
return 0;
}
不要因为某个平台上 size_t 恰好等同于 unsigned long,就固定写 %lu。格式与实参类型不匹配会破坏可移植性,严重时属于未定义行为。
6.3 sizeof 表达式通常不求值
c
#include <stdio.h>
int main(void)
{
int x = 10;
printf("%zu\n", sizeof(x++));
printf("x = %d\n", x);
return 0;
}
对于这里的普通表达式,x++ 不会执行,因此 x 仍为 10。sizeof 只需要根据表达式的类型给出大小。
但"sizeof 永远在编译期完成、永远不求值"说得过头了。若实现支持 C99 变长数组(VLA),相关大小可能在运行期确定,操作数也可能被求值。
c
void inspect(int n)
{
int values[n];
printf("%zu\n", sizeof values);
}
6.4 sizeof 数组与 sizeof 指针不同
c
int values[10];
printf("%zu\n", sizeof values);
printf("%zu\n", sizeof values[0]);
printf("%zu\n", sizeof values / sizeof values[0]);
在数组仍是数组对象的作用域中,可以用总大小除以单个元素大小得到元素个数。
但是数组作为函数参数声明时会调整为指针类型:
c
void show(int values[10])
{
/* 这里的 values 是指针参数,不再是原数组对象。 */
printf("%zu\n", sizeof values);
}
此时 sizeof values 得到的是指针大小,不能再用来计算调用者数组的元素数。
七、signed 与 unsigned:相同比特的不同解释
7.1 int 默认带符号
c
int a;
signed int b;
signed c;
这三种声明中的类型等价。与之对应:
c
unsigned int d;
unsigned e;
后两者表示无符号整数类型。
7.2 无符号类型为什么能表示更大的非负值
在相同存储宽度下,无符号类型不需要表示负数,因此所有值都用于 0 和正整数。若类型有 N 个值位,典型无符号范围为:
text
0 ~ 2^N - 1
有符号类型还要覆盖负数,因此最大正值会更小。

7.3 无符号运算按模回绕
无符号整数运算按照 2^N 取模,回绕是语言定义的一部分:
c
#include <limits.h>
#include <stdio.h>
int main(void)
{
unsigned int x = UINT_MAX;
x = x + 1;
printf("%u\n", x); /* 0 */
return 0;
}
这并不意味着所有溢出都安全。有符号整数溢出不是同一种规则;对其结果作猜测可能导致错误优化和未定义行为。
7.4 signed 与 unsigned 混合比较的陷阱
c
#include <stdio.h>
int main(void)
{
int a = -1;
unsigned int b = 1;
printf("%s\n", a < b ? "true" : "false");
return 0;
}
很多实现上结果会让初学者意外:比较前 a 可能被转换为很大的无符号值,于是条件为假。
处理原则不是"见到警告就强转",而是先检查建模:
- 数据是否真的不可能为负;
- 两端能否统一为语义一致的类型;
- 转换前是否已验证范围;
- 循环边界是否会在
0附近下溢。
经典危险写法:
c
for (size_t i = count - 1; i >= 0; --i) {
/* i 是无符号,i >= 0 恒真。 */
}
一种倒序写法是:
c
for (size_t i = count; i-- > 0; ) {
/* 使用 i */
}
八、不要猜范围:limits.h 与 float.h
8.1 整数极限看 limits.h
常用宏包括:
| 类型 | 最小值宏 | 最大值宏 |
|---|---|---|
signed char |
SCHAR_MIN |
SCHAR_MAX |
short |
SHRT_MIN |
SHRT_MAX |
int |
INT_MIN |
INT_MAX |
long |
LONG_MIN |
LONG_MAX |
long long |
LLONG_MIN |
LLONG_MAX |
unsigned char |
0 | UCHAR_MAX |
unsigned short |
0 | USHRT_MAX |
unsigned int |
0 | UINT_MAX |
unsigned long |
0 | ULONG_MAX |
unsigned long long |
0 | ULLONG_MAX |
c
#include <limits.h>
#include <stdio.h>
int main(void)
{
printf("int: %d .. %d\n", INT_MIN, INT_MAX);
printf("unsigned int max: %u\n", UINT_MAX);
printf("long long: %lld .. %lld\n", LLONG_MIN, LLONG_MAX);
return 0;
}
8.2 浮点能力看 float.h
c
#include <float.h>
#include <stdio.h>
int main(void)
{
printf("FLT_DIG = %d\n", FLT_DIG);
printf("DBL_DIG = %d\n", DBL_DIG);
printf("FLT_MAX = %e\n", FLT_MAX);
printf("DBL_MAX = %e\n", DBL_MAX);
return 0;
}
FLT_DIG、DBL_DIG 描述十进制有效数字能力;FLT_MAX、DBL_MAX 描述最大有限值附近的范围。不要只看 sizeof 就推断所有精度细节。

8.3 类型选择的实用顺序
可以按下面的问题依次判断:
- 数据语义是什么:计数、测量值、字符还是状态?
- 是否允许负数?
- 最小值与最大值可能是多少?
- 是否需要精确整数,还是允许浮点近似?
- 是否必须匹配库函数、系统调用或文件协议的接口类型?
- 是否需要精确位宽类型,如
<stdint.h>的int32_t?
对普通应用逻辑,先用自然、可读的类型;对二进制边界,使用接口明确定义的类型并做范围检查。
九、变量的创建、初始化与未初始化风险
9.1 声明变量
基本语法是:
text
类型说明符 标识符;
例如:
c
int age;
char grade;
double weight;
声明让编译器知道标识符的类型。对象在什么时候创建、初始值是什么,还取决于声明位置和存储期。
9.2 初始化与赋值不是一回事
c
int age = 18; /* 初始化:对象创建时获得初值 */
age = 20; /* 赋值:对象已经存在,再修改其值 */
虽然都出现 =,但语义阶段不同。初始化发生在对象定义时,赋值发生在后续表达式中。
9.3 自动存储期局部对象不会自动清零
c
#include <stdio.h>
int main(void)
{
int score;
printf("%d\n", score); /* 错误:读取不确定值 */
return 0;
}
不要把调试环境中偶然看到的 0 当成保证。读取未初始化自动对象可能产生未定义行为。
更稳妥的做法是:
c
int score = 0;
或者确保在任何读取路径之前都完成赋值:
c
int score;
if (read_score(&score)) {
printf("%d\n", score);
}
9.4 静态存储期对象默认零初始化
文件作用域对象和使用 static 声明的对象具有静态存储期。若没有显式初始化,会先被零初始化:
c
int global_count; /* 初值为 0 */
void visit(void)
{
static int times; /* 初值为 0,跨调用保留 */
++times;
}
"默认清零"不能泛化到普通块内局部变量。
十、局部变量、全局变量与作用域
教材里常用"大括号外"和"大括号内"帮助入门,但更精确的关键词是作用域:一个标识符在源代码的哪些位置可以被直接使用。
10.1 文件作用域
c
int global_value = 2023;
int main(void)
{
return global_value == 2023 ? 0 : 1;
}
在所有函数之外声明的标识符通常具有文件作用域:从声明点开始到当前翻译单元结束可见。
这不自动等于"整个工程任意文件直接可用"。跨源文件使用还涉及声明、定义与链接。
10.2 块作用域
c
int main(void)
{
int outer = 1;
{
int inner = 2;
outer += inner;
}
/* inner 在这里已经不可见 */
return 0;
}
inner 的名字只在内层复合语句中可见。离开代码块后,不能再通过这个标识符访问它。
10.3 for 初始化声明也形成局部范围
c
for (int i = 0; i < 10; ++i) {
printf("%d\n", i);
}
/* i 在这里不可见 */
把循环变量限制在循环内部,通常比在函数顶部统一声明更容易维护。
十一、存储期、链接与"栈区/静态区"

11.1 作用域与存储期不要混为一谈
- 作用域回答:名字在哪里可见?
- 存储期回答:对象从什么时候存在到什么时候结束?
它们描述不同维度。
c
void counter(void)
{
static int count = 0;
++count;
printf("%d\n", count);
}
count 的标识符具有块作用域,只在函数体对应范围内可见;但对象具有静态存储期,会在多次函数调用之间保留值。
11.2 自动存储期
普通块内局部变量通常具有自动存储期:每次控制流进入声明所在块时创建,离开块时生命周期结束。
c
void work(void)
{
int value = 42;
/* value 的生命周期属于这次块执行 */
}
常见实现会把这类对象放入调用栈,但优化器也可能把它只保存在寄存器中,甚至完全消除。语言语义首先谈生命周期,不要求你把每个局部变量机械画进"栈区"。
11.3 静态存储期
文件作用域对象和显式 static 局部对象具有静态存储期。对象在程序执行期间持续存在。
它们通常落在可执行文件的数据相关区域,但具体段划分、合并与优化属于实现、工具链和操作系统层面。
11.4 链接决定跨翻译单元身份
文件作用域下的 static 会给予内部链接,使名字只在当前翻译单元中表示对应实体:
c
static int file_private_count;
而跨文件共享通常采用"一个源文件定义、头文件提供 extern 声明"的方式:
c
/* counter.h */
extern int global_count;
c
/* counter.c */
int global_count = 0;
不要在头文件里随意放一个普通变量定义,否则被多个 .c 文件包含时可能形成重复定义。
十二、同名变量的遮蔽规则
12.1 内层声明会遮蔽外层同名标识符
c
#include <stdio.h>
int n = 1000;
int main(void)
{
int n = 10;
printf("%d\n", n); /* 10 */
return 0;
}
名字查找从当前最内层可见作用域开始,因此这里使用局部 n。全局对象仍然存在,只是外层同名标识符在该块中被遮蔽。

12.2 离开内层块后,外层名字重新可见
c
#include <stdio.h>
int main(void)
{
int value = 1;
{
int value = 2;
printf("inner = %d\n", value);
}
printf("outer = %d\n", value);
return 0;
}
输出为:
text
inner = 2
outer = 1
12.3 合法不代表值得大量使用
深层嵌套里反复使用同名变量,会增加阅读和调试成本。尤其当全局状态和局部临时值同名时,代码评审者很难快速判断正在修改哪个对象。
实践建议:
- 循环计数器等惯用短生命周期名字可以简洁;
- 业务状态应使用有语义的名字;
- 避免局部变量与重要全局对象同名;
- 开启编译器的 shadow 相关警告选项。
十三、一份可运行的类型观察程序
下面把 sizeof、类型范围、布尔转换和变量作用域放进同一个小程序:
c
#include <float.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
static int file_count;
static void visit(void)
{
static int call_count;
int local_value = 10;
++call_count;
++file_count;
printf("call=%d file=%d local=%d\n",
call_count, file_count, local_value);
}
int main(void)
{
bool ready = 42;
int value = 3;
printf("CHAR_BIT = %d\n", CHAR_BIT);
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(value + 3.5) = %zu\n", sizeof(value + 3.5));
printf("int range = %d .. %d\n", INT_MIN, INT_MAX);
printf("unsigned max = %u\n", UINT_MAX);
printf("DBL_DIG = %d\n", DBL_DIG);
printf("ready = %d\n", ready);
visit();
visit();
return 0;
}
13.1 推荐编译方式
GCC 或 Clang:
bash
gcc -std=c17 -Wall -Wextra -Wpedantic types_and_variables.c -o types_and_variables
./types_and_variables
Visual Studio 开发者命令提示符:
bat
cl /std:c17 /W4 types_and_variables.c
types_and_variables.exe
具体输出中的字节数和范围可能因实现而异,这正是本实验要观察的内容。
13.2 应该重点观察什么
sizeof(int)在当前实现上是多少;value + 3.5的结果类型为何是double;bool ready = 42为什么打印为 1;file_count与call_count为什么跨调用保留;local_value为什么每次调用都重新初始化为 10。
十四、常见误区、高频问题与练习
14.1 常见误区
误区一:C 语言规定 int 永远是 4 字节
错误。4 字节非常常见,但标准没有把 int 在所有实现上的大小固定成同一个值。应使用 sizeof 和 <limits.h> 查询当前实现。
误区二:一个字节永远等于 8 位
现代平台通常如此,但 C 语言用 CHAR_BIT 描述每字节位数;sizeof(char) 永远为 1。
误区三:char 就是 signed char
错误。普通 char、signed char 和 unsigned char 是三个不同类型,普通 char 的符号行为由实现选择。
误区四:unsigned 只是能表示更大的正数
不完整。它还有模运算语义,会影响溢出、比较、减法和循环边界。
误区五:sizeof 是函数
错误。它是运算符。对类型名必须使用括号,对普通表达式语法上可省略括号。
误区六:sizeof 的结果用 %d 打印即可
错误。结果类型是 size_t,标准输出格式应使用 %zu。
误区七:sizeof 中的表达式绝对不会执行
对普通非 VLA 表达式通常不求值,但涉及变长数组类型时可能运行期确定大小,不能把结论说成绝对。
误区八:局部变量自动初始化为 0
错误。未显式初始化的自动存储期局部对象具有不确定值,读取它可能导致未定义行为。
误区九:局部变量一定在栈上
"栈上"是常见实现现象,不是 C 抽象语义对每个对象的强制物理位置。优化器可将对象放在寄存器或消除。
误区十:全局变量在整个工程里天然可见
错误。文件作用域、声明位置和链接共同决定可见性与身份;跨文件使用需要正确的声明和定义组织。
误区十一:同名时局部变量会覆盖全局变量的值
错误。局部声明只是遮蔽外层名字,两个对象仍然独立。
误区十二:long double 一定是 16 字节并且精度更高
错误。具体大小和表示由实现决定;它的能力不会低于 double,但某些实现可能采用相同表示。
14.2 高频问题
问题 1:类型对编译器有什么用?
类型决定对象表示、大小、允许操作以及表达式中的转换规则,让编译器能正确生成访问和计算代码。
问题 2:sizeof 的单位是什么?
单位是 C 字节。sizeof(char) 定义为 1,每字节位数由 CHAR_BIT 给出。
问题 3:为什么 sizeof 返回 size_t?
对象大小不能为负,并且需要覆盖实现能够表示的最大对象大小,因此标准使用专门的无符号整数类型 size_t。
问题 4:怎样获得 int 的最大值?
包含 <limits.h>,使用 INT_MAX,不要在代码中手写某个平台的固定数字。
问题 5:有符号整数和无符号整数的最大区别是什么?
除了可表示范围不同,无符号算术具有定义明确的模回绕,而有符号溢出没有同样的保证。
问题 6:为什么 signed 和 unsigned 混合比较危险?
通常算术转换可能把负的有符号值转换成很大的无符号值,使直觉上的大小关系反转。
问题 7:初始化与赋值有什么区别?
初始化在对象创建时提供初值;赋值在对象已存在后修改其值。
问题 8:作用域和生命周期有什么区别?
作用域描述标识符在哪里可见;对象生命周期由存储期决定,描述对象何时存在。
问题 9:static 局部变量为什么能跨调用保留?
它的名字仍是块作用域,但对象具有静态存储期,在整个程序执行期间持续存在。
问题 10:什么时候应该使用精确宽度整数?
当协议、文件格式、硬件寄存器或算法明确要求固定位宽时,可检查 <stdint.h> 提供的 int32_t、uint64_t 等类型;并非每个实现都必须提供所有精确宽度类型。
14.3 建议练习
练习一:打印类型大小
输出 char、short、int、long、long long、float、double、long double 和 _Bool 的大小,使用 %zu。
练习二:查询类型范围
使用 <limits.h> 打印常见有符号与无符号整数范围,比较教材中的 VS2022 输出与当前编译器。
练习三:观察浮点精度
打印 FLT_DIG、DBL_DIG,再分别以较高精度输出 0.1f 和 0.1。
练习四:验证 sizeof 不求值
对普通变量执行 sizeof(x++),观察 x;然后调查 VLA 为什么是特殊边界。
练习五:比较数组与指针
在数组定义所在函数中计算元素个数,再把数组传给另一个函数,比较两处 sizeof 结果。
练习六:观察无符号回绕
让 unsigned int 从 UINT_MAX 加一、从 0 减一,解释结果;不要对有符号溢出做同类实验结论。
练习七:复现混合比较
比较 -1 与 1u,开启 -Wall -Wextra 或 /W4,阅读编译器警告并改成语义统一的写法。
练习八:观察 static 局部对象
编写函数,每次调用递增一个普通局部变量和一个 static 局部变量,比较输出。
练习九:制造变量遮蔽
在三层嵌套块中声明同名变量,逐层打印;开启 Clang/GCC 的 -Wshadow 后观察诊断。
练习十:组织跨文件全局状态
使用一个头文件的 extern 声明和一个源文件的唯一变量定义,再由 main.c 访问;故意重复定义并观察链接错误。
14.4 一份自检清单
- 我能说清类型为什么影响对象表示与运算;
- 我知道
char、signed char、unsigned char不完全等价; - 我不会把常见平台的类型大小写成标准固定值;
- 我会使用
sizeof查询大小,并用%zu输出size_t; - 我知道普通
sizeof表达式不求值,但 VLA 是边界; - 我会通过
<limits.h>和<float.h>查询能力; - 我知道无符号算术按模回绕;
- 我会警惕 signed/unsigned 混合比较;
- 我能区分初始化与赋值;
- 我不会读取未初始化的自动对象;
- 我能区分作用域与存储期;
- 我知道
static局部变量的名字范围和对象寿命不同; - 我理解同名变量只是遮蔽,不会合并或覆盖外层对象;
- 我会开启编译器警告并逐条理解。
14.5 参考资料
- 本文课程来源:
第2讲:C语言数据类型和变量.pdf - C 语言参考:
cppreference - C language - 基本类型:
cppreference - Arithmetic types - 对象与对齐:
cppreference - Objects and alignment sizeof:cppreference - sizeof operator- 整数极限:
cppreference - Numeric limits
总结
这一篇从内置类型出发,最终落到变量的可见范围和生存时间。主线可以压缩为:
text
现实中的数据需求
↓
选择字符 / 整数 / 浮点 / 布尔类型
↓
类型规定对象表示、大小与运算规则
↓
sizeof 查询对象表示所占字节数
limits.h / float.h 查询数值能力
↓
使用类型定义并初始化变量
↓
通过作用域理解"名字在哪里可见"
通过存储期理解"对象存在多久"
通过链接理解"跨文件是否同一实体"
最值得记住的是下面十点:
- 内存里只有比特,类型规定这些比特如何解释和参与运算。
- 普通
char的符号性由实现决定,它不等同于signed char。 - C 标准没有把所有整数类型固定成一张统一字节表。
sizeof的单位是 C 字节,结果类型是size_t,打印应使用%zu。- 普通非 VLA 表达式在
sizeof中通常不求值,但不能忽略变长数组边界。 - 类型范围应从
<limits.h>和<float.h>获取,不要手抄某个平台常量。 - 无符号整数按模运算,混合有符号比较可能把负数转换成巨大正数。
- 初始化发生在对象创建时,赋值发生在对象已经存在之后。
- 作用域、存储期和物理存放位置是不同层次的问题。
- 内层同名声明只会遮蔽外层标识符,不会把两个对象合并。
下一篇,我们会让这些类型真正进入表达式和输入输出:为什么 6 / 4 不是 1.5,前置与后置 ++ 差在哪里,强制转换会丢掉什么,以及 printf、scanf 为什么必须让格式与类型严格匹配。