预处理
C语言的编译步骤
- 预处理
- 编译
- 汇编
- 链接
什么是预处理
预处理就是在源文件(.c文件)编译之前,所进行的一部分预备操作,这部分操是由预处理器(预处理程序)
自动完成。当源文件在编译时,编译器会自动调用预处理程序来完成预处理执行的操作,预处理执行解析完成才能进入下一步的编译过程。
查看预处理结果:
gcc 源文件 -E -o 程序名
预处理功能
宏定义
不带参数的定义
-
语法:
#define 宏名称 宏值(替换文本)
-
**预处理机制:**此时的预处理只做数据替换,不做类型检查
-
**注意:**宏定义不会占用内存空间,因为在编译前已经将宏名称替换成了宏值
-
**宏展开:**在预处理阶段将宏名称替换成宏值的过程称之为“宏展开”。
-
案例:
#include <stdio.h> #define PI 3.1415926 int main(int argc,char *argv[]) { float l,s,r,v; printf("请输入圆的半径:\n"); scanf("%f",&r); // 计算周长 l = 2.0 * PI * r; // 计算面积 s = PI * r * r; printf("l=%10.4f\ns=%10.4f\n",l,s); return 0; }
运行结果:
带参数的定义
-
语法:
#define 宏名(参数列表) 替换表达式
-
面试题:
#define MULTI(a,b) (a)*(b) #define MULTI(a,b) a * b
实现:
#include <stdio.h> // 带参数的宏定义,宏名一般小写 #define MULTI_1(a,b) (a) * (b) #define MULTI_2(a,b) a * b int main(int argc,char *argv[]) { int result1 = MULTI_1(7+2,3); // (7+2) * (3) = 27 printf("%d\n",result1); int result2 = MULTI_2(7+2,3); // 7 + 2 * 3 = 13 printf("%d\n",result2); return 0; }
运行结果:
宏定义的作用域
-
#define
命令出现在程序中函数的外面,宏名的有效范围为定义命令之后到本源文件结束。 -
可以用
#undef
命令终止宏定义的作用域。 -
案例:
#include <stdio.h> #define PI 3.14 // PI的有效范围:10~18行 #define DAY 29 void func1() { float r = 4; float s = PI * r * r; // 预处理后:float s = 3.14 * r * r int day = DAY; // 预处理后:int day = 29; } #undef PI// 终止了 PI的范围 #define PI 3.1415926 void func2() { float r = 4; float s = PI * r * r; // 预处理后:float s = 3.1415926 * r * r int day = DAY; // 预处理后:int day = 29; } int main(int argc, char *argv[]) { return 0; }
在宏定义中引用已定义的宏名
-
案例:
#include <stdio.h> #define R 3.0 // 半径 #define PI 3.14 #define L 2 * PI * R // 周长 在宏定义的时候,引入已定义的宏名 #define S PI * R * R // 面积 #define P_WIDTH = 800 #define P_HEIGHT = 480 #define SIZE = P_WIDTH * P_HEIGHT int main(int argc,char *argv[]) { printf("L=%f\nS=%f\n",L,S);// 预处理后:2 * 3.14 * 3.0, 3.14 * 3.0 * 3.0 return 0; }
运行结果:
条件编译
概念
定义:根据设定的条件选择待编译的语句代码。
预处理机制:将满足条件的语句进行保留,将不满足条件的语句进行删除,交给下一步编译。
语法:
-
语法1:
根据是否找到标记,来决定是否参与编译(标记存在为真,不存在为假)
#ifdef 标记 // 标记 一般使用宏定义 ... 语句代码1 #else ... 语句代码2 #endif
举例:
#define DEBUG 1 #ifdef DEBUG printf("调试模式!\n"); // 保留 #else printf("产品模式!\n"); // 删除 #endif
说明:
printf("调试模式!\n");
和printf("调试模式!\n");
只能保留一个。undef
取消已定义的宏(使其变为未定义状态)。#define DEBUG 1 // 定义宏 #undef DEBUG // 取消定义的宏 #ifdef DEBUG printf("调试模式!\n"); // 删除 #else printf("产品模式!\n"); // 保留 #endif
-
语法2:
根据是否找到标记,来决定是否参与编译(标记不存在为真,存在为假)
#ifndef 标记 ... 语句代码1 #else ... 语句代码2 #endif
举例:
#define DEBUG 1 #ifndef DEBUG printf("调试模式!\n"); // 删除 #else printf("产品模式!\n"); // 保留 #endif
-
语法3:
根据表达式的结果,来决定是否参与编译(表达式成立为真,不成立为假)
// 单分支 #if 表达式 ... 语句代码1 #endif // 双分支 #if 表达式 ... 语句代码1 #else ... 语句代码2 #endif // 多分支 #if 表达式1 ... 语句代码1 #elif 表达式n ... 语句代码n #else ... 语句代码n+1 #endif
案例
案例1
#include <stdio.h>
// 定义一个条件编译的标记
#define LETTER 1 // 默认是大写
int main(int argc, char *argv[])
{
// 测试用的字母字符串
char str[26] = "C Language";
char c;
int i = 0;
// 遍历获取每一个字符
while ((c = str[i]) != '\0')
{
#if LETTER
if (c >= 'a' && c <= 'z')
{
c -= 32; // 大写
}
#else
if (c >= 'A' && c <= 'Z')
{
c += 32; // 小写
}
#endif
printf("%c",c);
i++;
}
printf("\n");
return 0;
}
案例2
需求:跨平台适配代码
#ifdef _WIN32 // Windows系统宏(VC编译器定义)
#include <windows.h>
#else // Linux/Unix系统
#include <unistd.h>
#endif
#include <stdio.h>
// 定义一个条件编译的标记
#define LETTER 1 // 默认是大写
int main(int argc, char *argv[])
{
#ifdef _WIN32 // Windows系统宏(VC编译器定义)
printf("当前是windows平台!\n");
#else // Linux/Unix系统
printf("当前是Linux平台!\n");
#endif
return 0;
}
文件包含
概念
所谓“文件包含”处理是指一个源文件可以将另一个源文件的全部内容包含进来。通常用于共享代码、声明或宏定义。一个常规的C语言程序会包含多个源文件(*.c
),当某些公共资源需要在各个源文件中使用时,为了避免多次编写相同的代码,我们一般会进行代码的抽取(*.h
),然后在各个源文件中直接包含即可。
注意:*.h
中的函数声明必须要在*.c
中有对应的函数定义,否则没有意义。(函数一旦声明,就一定要定义)
基本语法
-
标准库包含(使用尖括号)(会到/usr/include目录下查找)
#include <stdio.h> // 包含标准输入输出库 会到/usr/include目录下查找 #include <stdlib.h> // 包含标准库函数
-
自定义文件包含(使用双引号)(会先在当前目录下查找,找不到再到/usr/include目录下查找)
#include "myheader.h" // 包含当前目录下的自定义头文件 #include "utils/tool.h" // 包含子目录下的头文件
预处理机制
将文件中的内容替换文件包含指令
使用场景
-
**头文件包含:**通常将函数声明、宏定义、结构体定义等放在
.h
头文件中,通过#include
引入到需要使用的.c
文件中。头文件中存放的内容,就是各个源文件的彼此可见的公共资源,包括:
- 全局变量的声明
- 普通函数的声明
- 静态函数的声明(static修饰的函数,建议写在.c文件中)
- 宏定义
- 结构体、共用体、枚举常量列表的定义
- 其他头文件包含
-
**代码复用:**可以将一些通用代码片段(如工具函数)放在单独的文件中,通过包含实现复用。
示例代码:
myhead.h
extern int global; // 全局变量的声明 extern void func1(); // 普通函数的声明 static void func2() // 静态函数的声明,写在.h中,引用此文件的.c文件直接调用,写在.c文件,只能这个.c文件访问 { ... } #define max(a,b) ((a) > (b) ? (a) : (b)) // 宏定义 struct node // 结构体定义 { .. }; union attr // 共用体定义 { .. }; enum SEX // 枚举常量列表定义 { .. }; #include <stdio.h> // 引入系统头文件 #include "myhead2.h" // 引入自定义头文件
注意事项
-
避免循环包含(如
a.h
包含b.h
,同时b.h
又包含a.h
) -
为防止头文件被重复包含,通常会使用条件编译保护(推荐):
// 在myheader.h中 #ifndef MYHEADER_H // _MYHEADER_H, __MYHEADER_H #define MYHEADER_H ... 头文件内容 #endif
或者使用
#pragma once
(非标准但被大多数编译器支持):#pragma once ... 头文件内容
多文件开发
-
myheader.h
#ifndef MYHEADER_H #define MYHEADER_H #include <stdio.h> /** * 求数组的元素累加和 */ extern int sum(const int*, int); #endif
-
myheader.c
#include "myheader.h" /** * 求数组的元素累加和 */ int sum(const int* arr, int len) { const int *p = arr; int sum = 0; for (; p < arr + len; p++) { sum += *p; } return sum; }
-
app.c
#include "myheader.h" int main(int argc, char **argv[]) { int arr[] = {11,22,33,44,55}; int res = sum(arr, sizeof(arr)/sizeof(arr[0])); printf("数组累加和的结果是%d\n", res); return 0; }
-
多文件编译命令:
gcc app.c myhead.c -o app
其他指令(了解)
-
#line
用于修改当前的行号和文件名(主要用于编译器调试,很少手动使用)。#line 100 "test.c" // 后续代码从行号100开始,文件名标识为test.c printf("当前行号:%d\n", __LINE__); // 输出100
-
#error
在编译阶段当条件满足时抛出错误信息,并终止编译。#if VERSION < 1 #error "VERSION必须大于等于1" // 若VERSION<1,编译时会报错并提示此信息 #endif
-
#pragma
用于向编译器传递特定指令(不同编译器支持的#pragma
功能不同),例如:#pragma once
:确保头文件只被包含一次(类似#ifndef
的效果)。简单但兼容性稍差#pragma pack(n)
:设置结构体成员的对齐方式为 n 字节。#pragma warning(disable: 1234)
:禁用特定警告编号的编译警告。
库文件
什么是库文件
库文件本质上是经过编译后生成的可被计算机执行的二进制代码。但注意库文件不能独立运行,库文件需要加载到内存中才能执行。库文件大量存在于Windows,Linux,MacOS等软件平台上。
库文件的分类
- 静态库
- windows:xxx.lib
- linux:libxxxx.a
- 动态库(共享库)
- windows:xxx.dll
- linux:libxxxx.so.major.minor (libmy.so.1.1)
注意:不同的软件平台因编译器、链接器不同,所生成的库文件是不兼容的。
静态库与动态库的区别
- 静态库链接时,将库中所有内容包含到最终的可执行程序中(程序和库合一)。
- 动态库链接时,将库中的符号信息包含到最终可执行文件中,在程序运行时,才将动态库中符号的具体实现加载到内存中(程序和库分离)。
静态库与动态库的优缺点
- 静态库
- 优点:生成的可执行程序不再依赖静态文件
- 缺点:可执行程序体积较大
- 动态库
- 优点:生成的可执行程序体积小;动态库可被多个应用程序共享
- 缺点:可执行程序运行依然依赖动态库文件
静态库与动态库对比
维度 | 静态库 | 动态库 |
---|---|---|
文件体积 | 较大(库代码被复制) | 较小(共享库文件) |
部署难度 | 简单(单文件) | 需确保库存在于目标系统 |
更新维护 | 需重新编译程序 | 替换库文件即可 |
启动速度 | 稍快(无运行时链接开销) | 稍慢(需加载库) |
兼容性风险 | 无 | 需处理版本冲突(如DLL Hell) |
库文件创建
Linux系统下库文件的命名规范:libxxx.a
(静态库) libxxxx.so
(动态库)
静态库文件的生成
- 将需要生成库文件对应的源文件(
*.c
)通过编译(不链接)生成*.o
目标文件 - 用
ar
命令将生成的*.o
打包生成libxxx.a
库的生成
库的使用
动态库文件的生成
- 将需要生成库文件对应的源文件(
*.c
)通过编译(不链接)生成*.o
目标文件 - 将目标文件链接为
*.so
文件
库的生成
库的使用
注意:如果在代码编译过程或者运行中链接了库文件,系统会到/lib
和/usr/lib
目录下查找库文件,所以建议直接将库文件放在/lib
或者/usr/lib
,否则系统可能无法找到库文件,造成编译或者运行错误。
扩展内容
-
查看应用程序(例如:app)依赖的动态库:
-
动态库使用方式:
-
编译时链接动态库,运行时系统自动加载动态库
-
程序运行时,手动加载动态库
-
实现:
-
涉及内容
- 头文件:
#include <dlfcn.h>
- 接口函数:
dlopen、dlclose、dlsym
- 依赖库:
-ldl
- 句柄handler:资源的标识
- 头文件:
-
示例代码:
#include <stdio.h> #include <dlfcn.h> int main(int argc,char *argv[]) { // 1. 加载动态库 "/lib/libdlfun.so" // - RTLD_LAZY: 延迟绑定(使用时才解析符号,提高加载速度) // - 返回 handler 是动态库的句柄,失败时返回 NULL void* handler = dlopen("/lib/libdlfun.so", RTLD_LAZY); if (handler == NULL) { // 打印错误信息(dlerror() 返回最后一次 dl 相关错误的字符串) fprintf(stderr, "dlopen 失败: %s\n", dlerror()); return -1; } // 2. 从动态库中查找符号 "sum"(函数名) // - dlsym 返回 void*,需强制转换为函数指针类型 int sum(int *arr, int size); // - 这里假设 "sum" 是一个接受两个int*,int参数、返回 int 的函数 int (*paddr)(int*, int) = (int (*)(int*, int))dlsym(handler, "sum"); if (paddr == NULL) { fprintf(stderr, "dlsym 失败: %s\n", dlerror()); dlclose(handler); // 关闭动态库(释放资源) return -1; } // 3. 调用动态库中的函数 "sum",计算{11,12,13,14,15}的累加和 int arr[5] = {11,12,13,14,15}; printf("sum=%d\n", paddr(arr, sizeof(arr)/sizeof(arr[0]))); // 4. 关闭动态库(释放内存和资源) dlclose(handler); return 0; }
-
编译命令
gcc demo06.c -ldl
-
-