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

    • 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 程序的编译分为预处理、编译、汇编、链接四个阶段,可以用 GCC 的选项分别查看每个阶段的输出。

1. 预处理(Preprocessing)

预处理器处理所有以 # 开头的指令:展开 #include 头文件、替换 #define 宏、处理条件编译 #if 等。预处理后的文件仍然是文本,但已经没有预处理指令。

gcc -E hello.c -o hello.i     /* 只预处理,输出 hello.i */

预处理后的 .i 文件通常很大,因为头文件内容被全部插入。例如 #include <stdio.h> 可能展开为数百行声明。

2. 编译(Compilation)

编译器将预处理后的 C 代码翻译为汇编语言。这一阶段进行语法分析、类型检查、优化,生成人类可读(但晦涩)的汇编代码。

gcc -S hello.i -o hello.s     /* 编译到汇编 */
/* 或直接从 .c 文件:gcc -S hello.c */

生成的 .s 文件内容类似:

    .file   "hello.c"
    .section    .rodata
.LC0:
    .string "Hello, World!"
    .text
    .globl  main
main:
    pushq   %rbp
    ...

3. 汇编(Assembly)

汇编器将汇编代码翻译为机器码,生成目标文件(Object File)。目标文件包含二进制指令和数据,但还不能直接运行——它可能引用了其他文件中的函数或变量。

gcc -c hello.s -o hello.o     /* 汇编到目标文件 */
/* 或直接从 .c 文件:gcc -c hello.c */

目标文件是二进制格式(ELF 在 Linux,COFF/PE 在 Windows),可以用工具查看符号表:

nm hello.o                    /* 查看目标文件中的符号 */
objdump -d hello.o            /* 反汇编查看机器码 */

4. 链接(Linking)

链接器将一个或多个目标文件与库文件合并,解析所有外部引用,生成最终的可执行文件。例如 printf 函数定义在 C 标准库中,链接器负责找到它的实现并建立连接。

gcc hello.o -o hello          /* 链接生成可执行文件 */
/* 或一步完成所有阶段:gcc hello.c -o hello */

多文件编译示例

实际项目通常由多个源文件组成:

/* math_utils.c */
int add(int a, int b)
{
    return a + b;
}

/* main.c */
#include <stdio.h>

int add(int a, int b);      /* 函数声明(原型) */

int main(void)
{
    printf("%d\n", add(2, 3));
    return 0;
}

编译链接:

gcc -c math_utils.c -o math_utils.o
gcc -c main.c -o main.o
gcc math_utils.o main.o -o program

如果修改了 math_utils.c,只需重新编译它,再重新链接,不必重新编译 main.c——这是大型项目使用 Makefile 或构建系统的根本原因。

目标文件与可执行文件格式

Linux/Unix:ELF(Executable and Linkable Format)

file hello                    /* 查看文件类型 */
ldd hello                   /* 查看依赖的动态库 */

Windows:PE(Portable Executable)

Windows 可执行文件以 .exe 结尾,也是 COFF 格式的变种。MinGW 编译器在 Windows 上生成 PE 格式文件。

macOS:Mach-O

苹果系统使用 Mach-O 格式,但编译命令与 Linux 几乎相同。

常见错误类型

语法错误(Syntax Error)

编译阶段发现,通常是拼写错误、缺少分号、括号不匹配等。编译器会指出错误所在的文件和行号。

int main(void)
{
    printf("Hello\n")      /* 错误:缺少分号 */
    return 0;
}

链接错误(Linker Error)

链接阶段发现,通常是函数声明了但没有定义、多个文件中定义了同名全局变量等。

/* 只声明,没有对应的函数定义 */
int undefined_func(int x);
int main(void) { undefined_func(1); return 0; }

运行时错误(Runtime Error)

程序编译链接通过,但运行时报错或崩溃。如除以零、数组越界、空指针解引用、内存泄漏等。这类错误最隐蔽,需要调试工具定位。

int arr[5];
arr[10] = 1;                /* 运行时错误:数组越界(可能崩溃,也可能静默破坏数据) */

理解编译流程,能让你在出错时快速判断"问题发生在哪个阶段",从而选择正确的排查方向。

上一页
第一个C程序
下一页
可移植性风险的三级体系