【C语言数据类型和变量·上】从内置类型到变量模型:一次讲透 sizeof、signed/unsigned 与作用域

🔥 星光编译者 · 个人主页

📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》

🌄 向云端飞扬,编译属于自己的代码星河。


☕ 写在开篇

  你好,这里是 星光编译者

  这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。

  比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。

  如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。


🔥 本文定位 :第 2 讲内容较多,因此拆成上下两篇。上篇从"类型为什么存在"出发,梳理字符、整数、浮点和布尔类型,说明 sizeofsize_tsigned/unsigned、类型范围、变量初始化、作用域、存储期与同名遮蔽。
💡 学习目标 :能读懂常见内置类型声明;正确使用 sizeof%zu;理解类型宽度为何不能写死;区分 charsigned charunsigned char;会通过 <limits.h><float.h> 查询能力;能区分变量的作用域、存储期和常见物理存放位置。
📌 阅读说明 :本文以标准 C 的可移植语义为主,示例可在 GCC、Clang 和 Visual Studio 中练习。教材中的 VS2022 x64 输出只是一个具体实现的结果,不应被当成所有平台的固定结论。下篇将继续讲运算符、类型转换、printfscanf


文章目录

  1. 先建立全景图:类型到底解决什么问题
  2. C 语言内置类型地图
  3. 字符类型:三个名字不是完全同义
  4. 整数类型:short、int、long 与 long long
  5. 浮点与布尔:近似值和真假值
  6. sizeof:查询对象表示所占的字节数
  7. signed 与 unsigned:相同比特的不同解释
  8. 不要猜范围:limits.h 与 float.h
  9. 变量的创建、初始化与未初始化风险
  10. 局部变量、全局变量与作用域
  11. 存储期、链接与"栈区/静态区"
  12. 同名变量的遮蔽规则
  13. 一份可运行的类型观察程序
  14. 常见误区、高频问题与练习
  15. 总结

一、先建立全景图:类型到底解决什么问题

生活中我们会区分年龄、身高、温度、字符和真假。计算机内存里没有天然的"年龄"或"字符",只有一组组比特。C 语言中的类型,就是程序和编译器对这些比特所作的约定。

例如,同样一组二进制位可以被解释成:

  • 一个有符号整数;
  • 一个无符号整数;
  • 一个浮点数编码;
  • 若干字符;
  • 某种更复杂对象的一部分。

类型至少影响四件事:

  1. 一个对象需要多少存储空间;
  2. 某一组比特怎样被解释;
  3. 它能参与哪些运算;
  4. 运算时是否需要类型转换。

因此,"类型"不是给变量贴的装饰标签,而是编译器理解程序的基础。

c 复制代码
int age = 18;
double temperature = 36.5;
char grade = 'A';

这里的 1836.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> 后,还可以使用更易读的 booltruefalse

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.14f3.14F 的类型是 float
  • 3.14L3.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_DIGDBL_DIG 描述十进制有效数字能力;FLT_MAXDBL_MAX 描述最大有限值附近的范围。不要只看 sizeof 就推断所有精度细节。

8.3 类型选择的实用顺序

可以按下面的问题依次判断:

  1. 数据语义是什么:计数、测量值、字符还是状态?
  2. 是否允许负数?
  3. 最小值与最大值可能是多少?
  4. 是否需要精确整数,还是允许浮点近似?
  5. 是否必须匹配库函数、系统调用或文件协议的接口类型?
  6. 是否需要精确位宽类型,如 <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 应该重点观察什么

  1. sizeof(int) 在当前实现上是多少;
  2. value + 3.5 的结果类型为何是 double
  3. bool ready = 42 为什么打印为 1;
  4. file_countcall_count 为什么跨调用保留;
  5. local_value 为什么每次调用都重新初始化为 10。

十四、常见误区、高频问题与练习

14.1 常见误区

误区一:C 语言规定 int 永远是 4 字节

错误。4 字节非常常见,但标准没有把 int 在所有实现上的大小固定成同一个值。应使用 sizeof<limits.h> 查询当前实现。

误区二:一个字节永远等于 8 位

现代平台通常如此,但 C 语言用 CHAR_BIT 描述每字节位数;sizeof(char) 永远为 1。

误区三:char 就是 signed char

错误。普通 charsigned charunsigned 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_tuint64_t 等类型;并非每个实现都必须提供所有精确宽度类型。

14.3 建议练习

练习一:打印类型大小

输出 charshortintlonglong longfloatdoublelong double_Bool 的大小,使用 %zu

练习二:查询类型范围

使用 <limits.h> 打印常见有符号与无符号整数范围,比较教材中的 VS2022 输出与当前编译器。

练习三:观察浮点精度

打印 FLT_DIGDBL_DIG,再分别以较高精度输出 0.1f0.1

练习四:验证 sizeof 不求值

对普通变量执行 sizeof(x++),观察 x;然后调查 VLA 为什么是特殊边界。

练习五:比较数组与指针

在数组定义所在函数中计算元素个数,再把数组传给另一个函数,比较两处 sizeof 结果。

练习六:观察无符号回绕

unsigned intUINT_MAX 加一、从 0 减一,解释结果;不要对有符号溢出做同类实验结论。

练习七:复现混合比较

比较 -11u,开启 -Wall -Wextra/W4,阅读编译器警告并改成语义统一的写法。

练习八:观察 static 局部对象

编写函数,每次调用递增一个普通局部变量和一个 static 局部变量,比较输出。

练习九:制造变量遮蔽

在三层嵌套块中声明同名变量,逐层打印;开启 Clang/GCC 的 -Wshadow 后观察诊断。

练习十:组织跨文件全局状态

使用一个头文件的 extern 声明和一个源文件的唯一变量定义,再由 main.c 访问;故意重复定义并观察链接错误。

14.4 一份自检清单

  • 我能说清类型为什么影响对象表示与运算;
  • 我知道 charsigned charunsigned char 不完全等价;
  • 我不会把常见平台的类型大小写成标准固定值;
  • 我会使用 sizeof 查询大小,并用 %zu 输出 size_t
  • 我知道普通 sizeof 表达式不求值,但 VLA 是边界;
  • 我会通过 <limits.h><float.h> 查询能力;
  • 我知道无符号算术按模回绕;
  • 我会警惕 signed/unsigned 混合比较;
  • 我能区分初始化与赋值;
  • 我不会读取未初始化的自动对象;
  • 我能区分作用域与存储期;
  • 我知道 static 局部变量的名字范围和对象寿命不同;
  • 我理解同名变量只是遮蔽,不会合并或覆盖外层对象;
  • 我会开启编译器警告并逐条理解。

14.5 参考资料


总结

这一篇从内置类型出发,最终落到变量的可见范围和生存时间。主线可以压缩为:

text 复制代码
现实中的数据需求
        ↓
选择字符 / 整数 / 浮点 / 布尔类型
        ↓
类型规定对象表示、大小与运算规则
        ↓
sizeof 查询对象表示所占字节数
limits.h / float.h 查询数值能力
        ↓
使用类型定义并初始化变量
        ↓
通过作用域理解"名字在哪里可见"
通过存储期理解"对象存在多久"
通过链接理解"跨文件是否同一实体"

最值得记住的是下面十点:

  1. 内存里只有比特,类型规定这些比特如何解释和参与运算。
  2. 普通 char 的符号性由实现决定,它不等同于 signed char
  3. C 标准没有把所有整数类型固定成一张统一字节表。
  4. sizeof 的单位是 C 字节,结果类型是 size_t,打印应使用 %zu
  5. 普通非 VLA 表达式在 sizeof 中通常不求值,但不能忽略变长数组边界。
  6. 类型范围应从 <limits.h><float.h> 获取,不要手抄某个平台常量。
  7. 无符号整数按模运算,混合有符号比较可能把负数转换成巨大正数。
  8. 初始化发生在对象创建时,赋值发生在对象已经存在之后。
  9. 作用域、存储期和物理存放位置是不同层次的问题。
  10. 内层同名声明只会遮蔽外层标识符,不会把两个对象合并。

下一篇,我们会让这些类型真正进入表达式和输入输出:为什么 6 / 4 不是 1.5,前置与后置 ++ 差在哪里,强制转换会丢掉什么,以及 printfscanf 为什么必须让格式与类型严格匹配。

相关推荐
Navigator_Z3 小时前
LeetCode //C - 1252. Cells with Odd Values in a Matrix
c语言·算法·leetcode
是隼人5 小时前
buuctf-pwn [NewStarCTF 2023 公开赛道]stack migration(64位栈迁移)题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
Logic1018 小时前
C语言/数据结构贪心算法题解:买卖股票的最佳时机——一次交易最大利润(O(n)时间O(1)空间)
c语言·数据结构·贪心算法·数组·时间复杂度·算法题·股票交易
Logic1018 小时前
C语言/数据结构位运算题解:异或XOR找出货币交易中的“独特面值“——只出现一次的数字
c语言·数据结构·数组·位运算·时间复杂度·算法题·异或性质
aaaameliaaa10 小时前
结构体 结构体
c语言·笔记·算法
是隼人11 小时前
buuctf-pwn PWN8题解(学习过程持续更新)
c语言·学习·安全·pwn入门·ctf入门
GG-_-Bond11 小时前
9.1kv存储持久化模拟面试
linux·c语言·数据结构·c++
无敌贵点大王12 小时前
RTThread学习记录10——C语言实现面向对象的编程
c语言·stm32·学习·链表
en.en..13 小时前
TCP与UDP对比
c语言
ThornArmor14 小时前
《向内深潜,向外飞掠》
c语言·开发语言·c++·vim·visual studio