1.翻译环境:将源代码转为可执行的机器指令。
1.执行环境:用于实际执行代码。
1.要了解的名词:源文件 (c),目标文件(obj) 编译器 ,链接器 ,链接库,可执行程序。
2.源文件(可多个) ——> 编译器(每个源文件对应一个)——>目标文件——> 链接器(将目标文件捆在一起) ——>可执行程序。
链接库——> 链接器 ——>可执行程序。
(会引入标准C函数库中任何被该程序所用到的函数,它还可以搜索程序员个人的程序库)
3.编译(阶段):1.预处理阶段 , 2. 编译 ,3.汇编 ,4.链接。
1.预处理阶段:gcc -E test.c -o test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件。
2.编译:gcc -S test.i -o test.s 经过语法分析 ,词法分析 ,语义分析 ,符号分析 产生的都放入test.s文件。
3.汇编:gcc -C test.s -o test.o 合并段表 ,符号表的合并和符号表的重定位。完成汇编产生test.o文件。
4.链接:gcc test.o -o test 。零散的目标文件、库整合,解决符号依赖,产出真正能跑的程序。
5.一步到位:gcc test.c -o test
4.程序执行过程:
- 程序载入:依赖环境,有操作系统时由系统完成,独立环境下需手工或通过可执行代码置入只读内存等方式载入内存 。
- 执行启动:程序执行后调用
main
函数 。- 代码执行:用运行时堆栈存局部变量和返回地址,也可用静态内存,其中变量值在程序执行全程保留 。
- 程序终止:分正常(
main
函数正常结束 )和意外终止情况 。
__FILE__ //编译时,会自动替换成当前源文件的名字(带路径) 。比如你写代码的文件叫test.c
,它就变成"test.c"
(具体可能带完整路径,看编译器)。__LINE__ //替换成当前代码所在的行号 。比如代码写在第 10 行,它就变成10
,用来调试、定位代码位置很方便。__DATE__ //替换成文件编译的日期 ,格式是"MMM DD YYYY"
(比如"Jun 29 2025"
)__TIME__ //替换成文件编译的时间 ,格式是"HH:MM:SS"
(比如"14:30:00"
)。__STDC__ //如果编译器遵守 ANSI C 标准(比如常见的 GCC、Clang 基本都满足),它的值就是1
;不遵守的话,可能没定义(或者值不是 1 )。用来判断编译器是否符合标准。
1.解释:这是 C 语言里的预定义符号,是编译器内置的,不用自己定义就能直接用,作用是在编译时获取一些环境信息。
2.例子:
printf("file:%s line:%d\n", __FILE__, __LINE__);
编译时:
__FILE__
会被替换成你当前代码文件的名字(比如 " test.c "
)。
__LINE__
会被替换成这行代码所在的行号(比如代码在第 5 行,就变成5 )。
运行后,输出就会是:file:test.c line:5
(假设文件是 test.c
,代码在第 5 行 ),相当于动态 “打印出代码在哪个文件、哪一行”,调试时能快速定位问题。
用 #define name stuff
格式,把 name
定义为 stuff
,编译前会做「文本替换」,让代码更灵活、简洁。(一般用大写字母)
#define MAX 1000
,用 MAX
代替数值,方便修改、增强可读性。#define reg register
,给关键字起简短别名,写代码更顺手。#define do_forever for(;;)
:用更直观的名字封装逻辑(这里是无限循环)。#define CASE break;case
:写 case
时自动加 break
,减少重复代码(利用预处理自动补充逻辑)。stuff
过长,除最后一行外,每行末尾加 \
(续行符)。————问题:define定义后面要加;吗?
——回答:最好不要,它就像当于替换的,你加;号,如果你要用这个name ,会一不小心再+;号:会导致程序报错。
// 计算平方的宏(存在隐患)
#define SQUARE(x) x * x
// 改进版:添加括号避免优先级问题
#define SQUARE(x) ((x) * (x))
// 安全的数值计算宏
#define DOUBLE(x) ((x) + (x))
属性 | 宏定义 | 函数 |
---|---|---|
代码长度 | 每次使用时插入代码,可能导致程序膨胀 | 代码集中,仅在调用时执行 |
执行速度 | 无函数调用开销,速度更快 | 存在调用和返回开销 |
类型安全 | 类型无关,可能引发隐式错误 | 类型严格检查,更安全 |
副作用处理 | 参数可能被多次求值,副作用难控 | 参数仅求值一次,结果可控 |
调试支持 | 无法单步调试 | 支持完整调试 |
递归能力 | 不支持递归 | 支持递归调用 |
1.#
:将参数转换为字符串。
#define PRINT(value) printf("值为: " #value "\n", value)
PRINT(10); // 输出:值为: 10
2.##
:连接两个符号为一个标识符
#define CONCAT(name, num) name##num
int CONCAT(var, 1) = 100; // 等价于 int var1 = 100;
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
3.副作用:表达式求值时产生的永久性效果,比如 x++
(会改变 x
的值,属于有副作用);而 x + 1
不改变 x
原值,无副作用。
若宏参数在宏定义中多次出现,且参数带副作用(如 x++
这类改变变量值的操作 ),宏展开后会因参数重复执行副作用,导致结果不可预测。
4.例子:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
,当传入带副作用的参数(如 x++
、y++
):
z = MAX(x++, y++);
//宏展开后变为
z = ((x++) > (y++)) ? (x++) : (y++);
由于 x++
、y++
多次执行,最终 x
、y
、z
的结果会因副作用叠加,出现不符合预期的情况(比如示例里最终 x=6
、y=10
、z=9
)。
#include
直接在标准库路径中查找适用于包含系统头文件
#include "filename.h"
先在当前源文件目录查找,再查找标准路径适用于包含自定义头文件
当多个头文件相互包含时,可能导致内容重复编译,解决方案:
#ifndef/#define/#endif
包裹头文件内容#pragma once
指令(现代编译器支持)1.解释:C 语言预处理指令,作用是移除(取消定义 )之前用 #define 定义的宏。比如 #undef NAME 就是移除名为 NAME 的宏定义,常用于宏的重新定义场景,重新定义前需先移除旧定义 。
条件编译允许根据不同条件选择性编译代码,常用于:
1.调试代码的开关控制
2.跨平台代码适配
3.版本差异化处理
#define DEBUG_MODE 1
#if DEBUG_MODE
printf("调试模式已开启\n");
// 调试相关代码
#endif
#if defined(OS_WINDOWS)
// Windows 平台代码
#elif defined(OS_LINUX)
// Linux 平台代码
#else
// 其他平台代码
#endif
// 方式一
#ifdef CONFIG_FEATURE_A
// 启用功能A的代码
#endif
// 方式二
#if defined(CONFIG_FEATURE_B)
// 启用功能B的代码
#endif
// 传统方式
#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__
// 头文件内容...
#endif // __MY_HEADER_H__
// 现代方式(部分编译器支持)
#pragma once
常见的条件编译指令
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2()
#endif
#endif