飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 编程基础概念

    • 冯·诺依曼体系结构
    • 数据在计算机中的表示
    • 编程语言的层次
    • C语言的起源与发展
    • C99标准的主要改进
    • 开发环境搭建
    • 第一个C程序
    • 编译与运行流程
    • 可移植性风险的三级体系
  • 第2章 数据类型与运算

    • 字符集与标识符
    • 关键字
    • 注释
    • char 类型
    • short 与 int
    • long 与 long long
    • 有符号与无符号
    • 取值范围与 limits.h
    • float 与 double
    • long double
    • _Bool 类型
    • 变量声明与定义
    • 常量
    • 转义序列
    • 算术运算符
    • 赋值运算符
    • 自增自减运算符
    • 关系与判等运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 逗号运算符
    • 运算符优先级
    • 隐式类型转换
    • 显式类型转换
  • 第3章 控制流

    • 表达式语句与空语句
    • 复合语句
    • if 语句
    • switch 语句
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
    • goto 语句
    • return 语句
  • 第4章 函数与模块化编程

    • 函数定义
    • 函数声明与原型
    • main 函数
    • 函数调用机制
    • 传值调用
    • 数组参数
    • 作用域
    • 存储期
    • 链接属性
    • static 与 extern
    • 递归
    • 头文件与源文件
    • 头文件保护
    • include 规则
  • 第5章 数组与字符串

    • 一维数组声明与初始化
    • 数组的存储模型
    • 数组访问与越界
    • 数组操作
    • 二维数组
    • 变长数组 VLA
    • 字符串基础
    • 字符串输入输出
    • 字符串处理函数
    • 字符串与数字转换
  • 第6章 指针

    • 指针的概念
    • 指针的声明与使用
    • 指针运算
    • const 与指针
    • 数组名与指针
    • 指针遍历数组
    • 指针与多维数组
    • 指针作为函数参数
    • 函数返回指针
    • 函数指针
    • 二级指针
    • 复杂声明解析
  • 第7章 结构体、联合体与枚举

    • 结构体定义与声明
    • 结构体初始化
    • 结构体成员访问
    • 结构体嵌套
    • 结构体指针
    • 结构体与函数
    • 联合体
    • 联合体与类型双关
    • 枚举类型
    • 位域
    • 内存对齐与填充
  • 第8章 动态内存管理

    • malloc 与 free
    • calloc 与 realloc
    • 内存泄漏
    • 悬垂指针
    • 内存分配策略
    • 自定义内存池
    • Valgrind 与内存检测
    • 内存碎片
    • 内存对齐分配
    • 常见内存错误
  • 第9章 文件输入输出

    • 文件打开与关闭
    • 文本读写
    • 格式化输入输出
    • 二进制读写
    • 文件定位
    • 错误处理
    • 标准流
    • 临时文件
    • 文件操作示例
  • 第10章 预处理器

    • 预处理器基础
    • 宏定义
    • 带参数的宏
    • 条件编译
    • 头文件包含
    • 预定义宏
    • 宏的高级技巧
    • 预处理器陷阱
    • 编译器特定扩展
  • 第11章 标准库概览

    • 标准库概述
    • assert.h
    • ctype.h
    • errno.h
    • float.h
    • limits.h
    • locale.h
    • math.h
    • setjmp.h
    • signal.h
    • stdarg.h
    • stddef.h
    • stdlib.h
  • 第12章 进阶主题

    • 内联函数
    • 变长数组 VLA
    • 复数类型
    • 布尔类型
    • stdint 与 inttypes
    • 灵活数组成员
    • 匿名结构体与联合体
    • 静态断言
    • 线程支持
    • 原子操作

可移植性风险的三级体系

C 标准将"标准没有严格规定的行为"分为三个层级:实现定义行为、未指定行为、未定义行为。理解这三者的区别,是写出跨平台、稳定可靠 C 代码的基础。很多难以复现的 Bug,根源在于代码依赖了某种特定平台的行为。

实现定义行为(Implementation-Defined Behavior)

实现定义行为是指:标准允许多种合法选择,但编译器必须在其文档中明确说明采用了哪种选择。同一编译器的同一版本,行为始终一致。

char 是有符号还是无符号

char c = 200;
printf("%d\n", c);      /* x86 GCC:-56(char 有符号) */
                        /* ARM GCC:200(char 无符号) */

C99 §6.2.5/15 规定 char 的类型等价于 signed char 或 unsigned char,由实现决定。可以通过 <limits.h> 中的 CHAR_MIN 判断:

#include <limits.h>

if (CHAR_MIN < 0)
    printf("char is signed\n");
else
    printf("char is unsigned\n");

如果需要明确语义,直接写 signed char 或 unsigned char。

sizeof(int) 的值

printf("sizeof(int) = %zu\n", sizeof(int));   /* 通常是 4,但标准只保证 ≥ 2 */

C99 §6.5.3.4 规定 sizeof 的结果是实现定义的。16 位系统上 sizeof(int) 可能是 2,64 位系统上某些编译器可能设为 8。

右移有符号负整数

int x = -8;
printf("%d\n", x >> 1);   /* 可能是 -4(算术右移,补 1) */
                          /* 也可能是 2147483644(逻辑右移,补 0) */

应对策略:查阅编译器文档;使用 <limits.h> / <stdint.h> 获得明确的类型语义;避免依赖特定实现的行为。

未指定行为(Unspecified Behavior)

未指定行为是指:标准允许多种合法选择,但编译器不必文档化其选择,同一程序的不同编译或不同位置可能产生不同结果。

函数参数的求值顺序

int f(int a, int b) { return a + b; }
int i = 1;
f(i++, i++);            /* 未指定行为:先算第一个 i++ 还是第二个? */
                        /* 可能是 f(1, 2) 或 f(2, 1) */

C99 §6.5.2.2/10 规定函数参数的求值顺序未指定。含有副作用的表达式(i++)不应作为参数相互依赖。

正确写法:

int a = i++;
int b = i++;
f(a, b);                /* 明确控制求值顺序 */

sizeof 的求值

int i = 0;
sizeof(i++);            /* i++ 可能执行也可能不执行 */
                        /* 结果肯定是 sizeof(int),但 i 是否递增未指定 */

C99 §6.5.3.4/2 规定 sizeof 的操作数不一定被求值。

应对策略:不依赖特定求值顺序;将含副作用的表达式拆分为独立语句;一个表达式中只修改一个变量一次。

未定义行为(Undefined Behavior)

未定义行为是最危险的层级。标准对此没有任何要求,编译器可以产生任何结果——包括"看起来正确"的结果。C99 Annex J.2 列出了约 190 多条未定义行为,以下是最常见的:

有符号整数溢出

int max = INT_MAX;
max = max + 1;          /* 未定义行为!有符号整数溢出 */

编译器可能假设有符号整数永远不会溢出,并基于此进行激进优化。例如,它可能将 x + 1 > x 优化为恒真,因为"溢出不可能发生"。

数组越界访问

int arr[5];
arr[10] = 1;            /* 未定义行为 */

可能崩溃,可能静默破坏其他数据,可能在某些编译器上"恰好工作"。

使用已释放的内存

int *p = malloc(sizeof(int));
free(p);
*p = 1;                 /* 未定义行为:悬垂指针 */

修改字符串字面量

char *s = "hello";
s[0] = 'H';             /* 未定义行为:字符串字面量通常存储在只读段 */

除以零

int x = 1 / 0;          /* 未定义行为 */

未初始化变量的使用

int x;
printf("%d\n", x);      /* 未定义行为:自动变量未初始化 */

应对策略:开启编译器全部警告(-Wall -Wextra);使用静态分析工具(Clang Static Analyzer、Coverity);使用运行时检测工具(Valgrind、AddressSanitizer);遵循"防御式编程"原则——假设所有输入都可能恶意。

三级体系速查表

层级标准态度可预测性示例
实现定义必须文档化同一实现一致char 是否有符号、sizeof(int)
未指定不必文档化可能变化参数求值顺序
未定义无任何要求完全不可预测溢出、越界、空指针解引用

写出可移植 C 代码的核心原则:只依赖标准明确保证的行为,对实现定义行为显式处理,彻底避免未定义和未指定行为。

上一页
编译与运行流程