C语言基础(十八)---预处理、库文件

内容提要

  • 预处理

  • 库文件

预处理

一、C语言的编译步骤

  1. 预处理

  2. 编译(→ 汇编语言)

  3. 汇编( → 二级制代码)

  4. 链接

二、什么是预处理

  • 预处理就是在源文件(.c文件)编译之前,所进行的一部分预备操作,这部分操作是由预处理程序自动完成的。当源文件在编译时,编译器会自动调用预处理程序来完成预处理操作,预处理执行解析完成才能进入下一步的编译过程

  • 查看预处理结果:

     gcc 源文件 -E -o 程序名[.后缀]     //程序名[.后缀]---可以指定输出文件名,程序名由用户自定义,后缀可选(如i,txt或无后缀)

三、预处理功能

1、宏定义
  • 不带参数的定义

    • 语法:

       #define 宏名称 常量数据
    • 宏定义的预处理机制:此时的预处理只做数据替换,不做类型检查

    • 注意:宏定义不会占用内存空间,(运行时才会占用内存空间,程序在编译之前就已经将符号常量替换),因为在编译前已经将宏名替换成常量数据

    • 宏展开:在预编译时将宏名替换成字符串的过程称之为宏展开(将宏名替换成常量数据的过程)

    • 案例:

       #include 
       ​
       #define PI 3.1415926
       ​
       int main()
       {
           float l, s, r, v;
           
           printf("Please input r of the circle:\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 
       ​
       //定义一个带参数的宏,带参数的宏名为小写
       #define multi_1(a, b) (a) * (b)     
       #define multi_2(a, b) a * b 
       ​
       int main()
       {
           int result1 = multi_1(7 + 2, 3);
           printf("%d\n", result1);     //27
           
           int result2 = multi_2(7 + 2, 3);
           printf("%d\n", result2);     //7 + 2 * 3 = 13
           
           return 0;
       }
2、宏定义的作用域
  • #define命令出现在程序中函数的外面,宏名的有效范围为定义命令之后到本源文件结束

  • 可以用undef命令终止宏定义的作用域

     #include 
     ​
     #define PI 3.114     //3~12
     #define DAY 28     //DAY的作用域:4~文件末尾
     ​
     void func1()
     {
         float r = 4;
         float s = PI * r * r;
         int day = DAY;
     }
     ​
     #undef PI     //终止了PI的范围
     ​
     #define PI 3.1415926
     ​
     void func1()
     {
         float r = 4;
         float s = PI * r * r;
         int day = DAY;
     }
     ​
     int main()
     {
         func1();     //3.14 28
         func2();     //3.1415926 28
     }
3、在宏定义中引用已定义的宏名
 #include 
 ​
 #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()
 {
     printf("L = %f\nS = %f\n", L, S);
 }
4、文件包含
(1)概念

所谓的”文件包含“处理是指一个源文件可以将另一个源文件的内容包含进来。

通常,一个常规的C语言程序会包含多个源码文件(*.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,我们一般会进行代码的抽取(*.h),然后在各个源码文件中直接包含即可

  • 注意:*.h中的函数声明必须要在*.c中有对应的函数定义(函数的实现),否则没有意义。(函数一旦声明,就一定要定义)

(2)头文件(.h)的内容

头文件中所存放的内容就是各个源码文件的彼此可见的公共资源,包括:

  • 全局变量的声明

  • 普通函数的声明

  • 静态函数的定义

  • 宏定义

  • 结构体、共用体的定义

  • 枚举常量列表的定义

  • 其他头文件包含

示例代码
//head.h

extern int global;     //全局变量的声明
extern void func1();     //普通函数的声明
static void func2()     //静态函数的声明,写在.h中,引用.h的.c文件直接使用。写在.c文件中,只能当前文件访问
{
    ...
}
#defien MAX(a, b) ((a) > (b) ? (a) : (b))     //宏定义

struct node     //结构体定义
{
    ...
};

union attr     //共用体定义
{
    ...
};

enum SEX     //枚举常量列表定义
{
    ...
};

#include      //系统头文件
#include "myhead.h"     //自定义头文件

特别说明:

  1. 全局变量、普通函数的定义一般出现在某个源文件(*.c, *.cpp)中,其他源文件想要使用都需要进行声明,因此一般放在头文件中更方便

  2. 静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要的话,放到头文件中的内容替换文件包含指令

  3. 预处理机制:将文件中的内容替换文件包含指令

(3)包含方式
  1. #include :系统会到标准库文件目录(Linux下/usr/include)查找包含的文件,建议对于系统库访问采用这种写法

  2. #include "xxxx.h":在当前工程路径下(Linux下./)查找包含的文件,如果未找到,就去标准库文件目录下查找。建议对自定义库采用这种写法。

(4)案例
//myhead.h

#ifndef _MYHEAD_H
#define _MYHEAD_H

/**
 * 数组的累加和运算
 * @param int* int数组
 * @param int 数组大小
 */
extern int sum(const int*, int);     //函数光声明没有用,sum的实现在myhead.c,所以运行的时候需要一起编译myhead.c文件

#endif
//myhead.c

#include 
#include "myhead.h"

/**
 * 数组的累加和运算(定义)
 */
int sum(const int *arr, int len)
{
    int sum = 0;
    
    //当函数形参声明为常量指针时,必须使用相同类型或兼容类型的常量指针来接收实参
    const int *p = arr;     //如果要操作数组,建议不要直接使用形参,找一个指针变量接收一下
    
    
    for(; p < arr + len; p++)
    {
        sum += *p;
    }
    return sum;
}

//app.c

#include 
#include "myhead.h"

int main()
{
    int arr[] = {11, 12, 13, 14, 15};
    
    int result = sum(arr, sizeof(arr) / sizeof(arr[0]));
    
    printf("数组累加和的结果是:%d\n", result);
    
    return 0;
}
//多文件编译命令(把引用的.c文件全部写上)因为编译只编译.c文件,不编译.h文件
gcc app.c myhead.c -o app
./app
    
-E是显示编译后的结果
5、条件编译
(1)定义
  • 根据设定的条件选择待编辑的语句代码

  • 预处理机制:将满足条件的语句进行保留,将不满足条件的语句进行删除,交给下一步编译

(2)语法
  • 语法一:

    根据是否找到标识,来决定是否参与编译(标识存在为真)

    #ifdef 标识     //判断标识符定义与否,定义为真,未定义为假(找到标识符为真①,找不到为假②)
    ...语句1
    #else
    ...语句2
    #endif
  • 语法二:

    根据是否找到标识,来决定是否参与编译(标识不存在为真)

    #ifndef 标识     //判断标识符定义与否,定义为假,未定义为真(找到标识符为假②,找不到为真①)
    ...语句1
    #else
    ...语句2
    #endif
  • 语法三:

    根据表达式的结果,来决定是否参与编译(表达式成立为真(1), 不成立为假(0))

    #if 表达式     //判断表达式结果,成立为1,不成立为0
    ...语句1
    #else
    ...语句2
    #endif
(3)案例
#include 

#define LETTER 0     //默认是大写

int main()
{
    //测试用的字母字符串
    char str[20] = "C Language";
    
    char c;
    
    int i = 0;
    
    //遍历获取每一个字符
    while((c = str[i]) != '\0')
    {
#if LETTER     //0,执行else后面的语句
        if(c >=  'a' && c <= 'z')
        {
            c -= 32;
        }
#else
        if(c >= 'A' && c <= 'Z')
        {
            c += 32;
        }
#endif
        printf("%c", c);
        i++;
    }
    
    printf("\n");
    return 0;
}
6、避免头文件重复包含的方法

其实就是头文件去重复。

由于头文件包含指令#include的本质是复制张贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现头文件被重复包含的情况,此时就需要我们进行去重,去重需要用到预处理提供的去重的相关指令

语法:

#ifndef _XXXX_H     //一般为 下划线 + 头文件名大写 + 下划线 + H     //qt里面不加下划线
#defien _XXXX_H

..//头文件内容
    
#endif

案例:

//myhead.h

#ifndef _MYHEAD_H
#define _MYHEAD_H

/**
 * 数组的累加和运算
 * @param int* int数组
 * @param int 数组大小
 */
extern int sum(int*, int);     //函数光声明没有用,sum的实现在myhead.c,所以运行的时候需要一起编译myhead.c文件

#endif

库文件

一、什么是库文件

  • 库文件本质上是经过编译后生成的可被计算机执行的二进制代码,但注意库文件不能独立运行,库文件需要加载到内存中才能执行。库文件大量存在于Windows、Linux、MacOs等软件平台上

二、库文件的分类

1、静态库
  • Windows:xxx.lib

  • Linux:libxxxx.a

2、动态库(共享库)
  • Windows:xxx.dll

  • Linux:libxxxx.so.major.minor

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

3、静态库与动态库的区别
  • 静态库链接时,将库中所有内容包含到最终的可执行程序中

  • 动态库链接时,将库中的符号信息包含到最终可执行程序中,在程序运行时,才将动态库中符号的具体实现加载到内存中

4、静态库与动态库的优缺点
  • 静态库

    • 优点:生成的可执行程序不再依赖静态库文件

    • 缺点:可执行程序体积较大

  • 动态库

    • 优点:生成的可执行程序体积小;动态库可被多个应用程序共享

    • 缺点:可执行程序运行依然依赖动态库文件

三、库文件的创建

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

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

  2. ar命令将生成的*.o打包生成libxxxx.a

(1)库的生成:
gcc -c myhead.c -o fun.o     //该命令用于将 myhead.c 源文件编译为可重定位目标文件 fun.o,但不进行链接操作
ar -csr libmyfun.a fun.o     //该命令将目标文件 fun.o 打包为静态库 libmyfun.a。
(2)库的使用
gcc app.c -o app -lmyfun -L./     //
2、动态库文件的生成
  1. 利用源文件(*.c)通过编译(不链接)生成位置无关*.o目标文件

  2. 将目标文件链接为*.so文件

(1)库的生成+使用

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

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

  2. 动态库使用方式

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

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

    • 实现

      • 涉及内容

        • 头文件:#include

        • 接口函数:dlopen、dlclose、dlsym

        • 依赖库:-ldl

        • 句柄handler:资源的标识

      • 示例

        #include 
        #include 
        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

你可能感兴趣的:(c语言,开发语言)