预处理,库文件

预处理

C语言的编译步骤

  1. 预处理
  2. 编译
  3. 汇编
  4. 链接

什么是预处理

预处理就是在源文件(.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
    
其他指令(了解)
  1. #line 用于修改当前的行号和文件名(主要用于编译器调试,很少手动使用)。

     #line 100 "test.c"  // 后续代码从行号100开始,文件名标识为test.c
     printf("当前行号:%d\n", __LINE__);  // 输出100
    
  2. #error 在编译阶段当条件满足时抛出错误信息,并终止编译。

     #if VERSION < 1
     #error "VERSION必须大于等于1"  // 若VERSION<1,编译时会报错并提示此信息
     #endif
    
  3. #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)

注意:不同的软件平台因编译器、链接器不同,所生成的库文件是不兼容的。

静态库与动态库的区别

  1. 静态库链接时,将库中所有内容包含到最终的可执行程序中(程序和库合一)。
  2. 动态库链接时,将库中的符号信息包含到最终可执行文件中,在程序运行时,才将动态库中符号的具体实现加载到内存中(程序和库分离)。

静态库与动态库的优缺点

  1. 静态库
    • 优点:生成的可执行程序不再依赖静态文件
    • 缺点:可执行程序体积较大
  2. 动态库
    • 优点:生成的可执行程序体积小;动态库可被多个应用程序共享
    • 缺点:可执行程序运行依然依赖动态库文件

静态库与动态库对比

维度静态库动态库
文件体积较大(库代码被复制)较小(共享库文件)
部署难度简单(单文件)需确保库存在于目标系统
更新维护需重新编译程序替换库文件即可
启动速度稍快(无运行时链接开销)稍慢(需加载库)
兼容性风险需处理版本冲突(如DLL Hell)

库文件创建

Linux系统下库文件的命名规范:libxxx.a(静态库) libxxxx.so(动态库)

静态库文件的生成

  1. 将需要生成库文件对应的源文件(*.c)通过编译(不链接)生成*.o目标文件
  2. ar命令将生成的*.o打包生成libxxx.a
库的生成

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

库的使用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

动态库文件的生成

  1. 将需要生成库文件对应的源文件(*.c)通过编译(不链接)生成*.o目标文件
  2. 将目标文件链接为*.so文件
库的生成

在这里插入图片描述

库的使用

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:如果在代码编译过程或者运行中链接了库文件,系统会到/lib/usr/lib目录下查找库文件,所以建议直接将库文件放在/lib或者/usr/lib,否则系统可能无法找到库文件,造成编译或者运行错误。

扩展内容

  1. 查看应用程序(例如:app)依赖的动态库:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. 动态库使用方式:

    • 编译时链接动态库,运行时系统自动加载动态库

    • 程序运行时,手动加载动态库

    • 实现:

      • 涉及内容

        • 头文件:#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
        

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值