C:编译、预处理、库文件

编译

编译详细步骤:
源文件 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件

  1. 预处理
    • 对文件中以#开头的命令进行处理,例如#include<stdio.h>stdio.h文件内容插入到当前文件中
    • 预处理后生成以.i为扩展名的xxx.i文件
    • 命令:gcc -E hello.c -o hello.i
      • -E:对源文件执行预处理
      • -o:指定生成的文件名
  2. 编译
    • 对预处理后得到的xxx.i文件进行编译,生成汇编语言文件xxx.s
    • 命令:gcc -S hello.i -o hello.s
      • -S:对文件进行编译操作
  3. 汇编
    • xxx.s文件翻译为二进制语言指令,然后将指令打包成为目标文件xxx.o
      • 注:xxx.o是二进制文件
    • 命令:gcc -c hello.s -o hello.o
      • -c:只进行汇编操作
  4. 链接
    • 链接是将源代码编译后生成的单独可执行文件(.o文件)合并起来,生成最终的可执行程序的过程
    • 在链接过程中,被合并的文件包括源代码文件编译后的目标文件和库文件。合并后的文件为可执行文件
    • 命令:gcc hello.o -o hello
/*直接编译*/
gcc hello.c  //执行得到a.out
./a.out		//运行a.out

/*指定文件名*/
gcc hello.c -o hello
./hello

/*分步编译*/
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s  -o hello.o
gcc hello.o -o hello
./hello

预处理

预处理指令

gcc -E xxx.c -o xxx.i

#开头的多是预处理指令,例如#include <stdio,h>,功能为将stdio.h头文件内容替换到当前语句位置上

  • #define预处理指令用来定义宏,可以用来给数字起别名
  • 在编程时可以直接用宏名称代替数字,编译器会直接将宏名称替换为对应数字
  • 给宏定义时不可以使用赋值运算符,不能自增自减
  • 宏名称通常由大写英文字母构成,名称中不能包含空格
  • 宏定义格式:#define PI (3.14)
#include <stdio.h>
#define	PI	(3.14)
int main(){
	int r = 10;
    printf("%lg\n",2*PI*r);
    return 0;
}

使用gcc -E circle.c -o circle.i预处理生成xxx.i文件中可以看到PI被替换为了3.14

有参宏-宏函数

  • 宏可以给表达式起名

  • 宏表达式里包含未知数字,宏的参数可以用来表示表达式的未知数字

    • #define SQUARE(x) ((x) * (x))
  • 如果有多个参数就需要在小括号里写多个参数名称,相邻参数名称间用逗号分开

    • #define sub(x,y) ((x)-(y))
  • 有参宏只检查参数个数,不检查参数的数据类型

    • 例如:#define SQUARE(X) X*X

    • 使用宏参数时不要忘记加(),否则会导致一些问题

      #include<stdio.h>
      #define SQUARE(X) X*X	//错误示例
      #define MAX(x,y) ((x>y)?(x):(y))
      int main(int argc, char* argv[]) {
          printf("%d\n", SQUARE(2+3));
          //SQUARE(2+3)相当于:2+3*2+3,输出结果为2+6+3=11
      	printf("%d\n", (MAX(10,20)));
          //MAX(10,20))相当于:((10>20)?(10):(20)),输出结果为20
      	return 0;
      }
      
  • 可以在编译命令里使用-D选项指定宏所代表的数字

    • gcc -DSIZE=10 xxx.c -o xxx
/*如果代码中已经定义了宏,编译时使用gcc -D命令赋值会报错*/
cwork$gcc -DA=2 if.c -o if
if.c:2:0: warning: "A" redefined
 #define A 1
 ^
<command-line>:0:0: note: this is the location of the previous definition

宏运算符

  • 编写宏使用的特殊符号叫做宏运算符
  • #是一个宏运算符,可以将宏的参数转换成字符串
    • 示例:#define PRINTDBL(x) printf(#x"= %f",x)
      • PRINTDBL(x)(PI * 3) 将被替换为printf("PI * 3" "= %f",PI * 3)
      • 因为编译器会自动合并字符串,所以上述代码等效于:printf("PI * 3= %f",PI * 3)
  • ##可以把代表标识符的参数和其他内容连接成一个新的标识符
    • #define STR(N) qwe##N
    • STR(123)将被替换为qwe123

内置宏

编译器默认已经定义好的宏,可以直接使用(前后都是双下划线)

占位符 含义
__FILE__ %s 所在文件名
__LINE__ %d 所在行号
__FUNCTION__ %s 所在函数名
__func__ %s 所在函数名
__DATE__ %s 编译该文件的日期
__TIME__ %s 编译该文件的时间

条件编译

条件编译可以在编译的时候从几组语句里选择一组编译而忽略其他组,条件成立就编译,条件不成立就不编译。

含义
#if 如果条件成立,则所包含代码参与编译,否则不编译
#ifdef 如果定义了某个宏,则所包含的代码参与编译,否则不编译
#ifndef 如果未定义宏,则所包含代码参与编译,否则不编译
#elif 相当于else if
#else 否则
#endif 必须与#if、#ifdef、#ifndef配对使用
#include<stdio.h>

int main(){
#if A==1
    printf("this is Windows");
#else
    printf("this is Linux");
#endif
    
#if B==1
    printf("this is ARM");
#elif B==2
    printf("this is X86");
#else
    printf("error");
#endif
    return 0;
}
/*	
cwork$gcc if.c -o if
cwork$./if
this is Linux
error
cwork$gcc -DA=2 -DB=2 if.c -o if
cwork$./if
this is Linux
this is X86
*/

多文件编程

  • 把程序编写在多个不同文件里叫做多文件编程
  • 多文件编程时一个文件里面可以包含多个函数,每个函数只属于一个文件
  • 将所有函数分散在不同的源文件中(通常主函数main.c单独占用一个文件)
  • 为每个源文件编写配对的.h头文件(main函数不需要头文件)
  • 源文件中使用#include预处理指令包含必要的头文件(只要源文件使用了某个头文件声明的函数就算必要头文件)

头文件卫士

gcc命令后列出所有源文件的路径就可以编译采用多文件方式编写的程序

头文件对应宏名称的命名习惯

  • a.h —— _A_H
  • common.h —— _COMMON_H
  • print.h —— _PRINT_H
#include<stdio.h>
#include "a.h"

/*防止头文件包含,相当于#program once*/
#ifndef __A_H	/*如果没有定义宏__A_H,就编译下面的代码*/
#define __A_H	/*如果之前已经编译过这段代码就会存在__A_H宏,然后跳过这段代码*/
int a = 100;
#endif

Makefile

  • 把多文件程序的编译步骤记录在Makefile文件里,然后使用make工具按照Makefile文件里记录的步骤完成编译
  • Makefile文件里每个编译命令前都不可以使用空格符,而使用tab
  • Makefile是一个文本文件
#目标:依赖文件
#	(TAB)由依赖文件生成目标的命令
main:main.o	swap.o
	gcc	main.o	swap	-o	main
main.o:main.c
	gcc	-c	main.c	-o	main.o
swap.o:swap.c
	gcc	-c	swap.c	-o	main.o
  • 小技巧:由.c文件得到对应.o文件
    • %o:%c
    • (tab)gcc -c $< -0 $@
  • %.o$@都是指目标文件.o
  • %.c$<都是源文件.c
main:main.o	swap.o
	gcc	main.o	swap.o	-o	main
%.o:%.c
	gcc	-c	$<	-o	$@

库文件

  • 单一模型

    • 将程序中所有功能全部实现于一个单一的源文件内部。编译时间长,不易于维护和升级,不易于协作开发
  • 分离模型

    • 将程序中不同功能模块划分到不同的源文件中。缩短编译时间,易于维护和升级,易于协作开发
  • 因对多个目标文件的管理比较麻烦,所以将多个目标文件统一整合成一个库文件。

    • 集零为整,方便使用、易于复用
  • 可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数和类

  • 库文件一般指计算机的一类文件,分两种,一种是静态库,另一种是动态库

静态库

  • 静态库的本质就是将多个目标文件打包成一个文件
  • 链接静态库就是将库中被调用的代码赋值到调用模块中
  • 静态库的拓展名是.a,例如:libmath.a

静态库的构建

顺序如下:

  • 编辑库的实现代码和结构声明

    • 计算模块:calc.hcalc.c

      /*calc.h*/
      #ifndef __CALC_H_
      #define __CALC_H_
      int add(int,int);
      int sub(int,int);
      #endif
      
      /*calc.c*/
      //计算模块实现
      #include"calc.h"
      int add(int a,int b){
          return a+b;
      }
      int sub(int a,int b){
          return a-b;
      }
      
    • 显示模块:show.hshow.c

      /*show.h*/
      #ifndef __SHOW_H_
      #define __SHOW_H_
      void show(int,char,int,int);
      #endif
      
      /*show.c*/
      #include<stdio.h>
      #include"show.h"
      void show(int l,char op,int r,int res){
          printf("%d %c %d = %d\n",l,op,r,res);
      }
      
    • 接口文件:math.h

      /*math.h*/
      #ifndef __MATH_H__
      #define __MATH_H__
      #include"calc.h"
      #include"show.h"
      #endif
      
      /*math.c*/
      #include<stdio.h>
      #include"math.h"
      int main(void){
          int a = 3,b = 8;
          show(a,'+',b,add(a,b));
          show(a,'-',b,sub(a,b));
          return 0;
      }
      
  • 编译成目标文件

    gcc -c calc.c
    gcc -c show.c
    
  • 打包成静态库

    • ar [选项] <静态库文件> <目标文件列表>
      • -r 将目标插入到静态库中,已存在则更新
      • -q 将目标文件追加到静态库尾
      • -d 从静态库中删除目标文件
      • -t 列表显示静态库中的目标文件
      • -x 将静态库展开为目标文件
    • ar -r libmath.a calc.o show.o
      static$ar -r libmath.a calc.o show.o
      ar: 正在创建 libmath.a
      static$ls
      calc.c  calc.h  cacl.o  libmath.a  main.c  main.h  show.c  show.h  show.o
      

静态库的使用

编译并链接静态库

  • 直接链接静态库:gcc main.c libmath.a
static$gcc main.c
/tmp/ccV0GpkT.o:在函数‘main’中:
main.c:(.text+0x21):对‘add’未定义的引用
main.c:(.text+0x35):对‘show’未定义的引用
main.c:(.text+0x44):对‘sub’未定义的引用
main.c:(.text+0x58):对‘show’未定义的引用
collect2: error: ld returned 1 exit status
static$gcc main.c libmath.a 
static$ls
a.out   cacl.h  libmath.a  main.h  show.h
cacl.c  cacl.o  main.c     show.c  show.o
static$./a.out 
3 + 4 = 7
3 - 4 = -1
  • -l指定库名,用-L指定库路径(当前路径可以用“.”表示)

    • gcc main.c -lmath -L.
    #-L可以使用绝对路径也可以用相对路径
    static$gcc main.c -lmath -L/home/tarena/cwork/static
    static$
    static$gcc main.c -lmath -L.
    static$
    
  • -l指定库名,用LIBRARY_PATH环境变量指定库路径

    • export LIBRARY_PATH=$LIBRTARY_PATH:.
    • gcc main.c -lmath
    #不通过L指定路径话,需要配置环境变量
    #如需环境变量永久生效,需要在~/.bashrc文件末尾添加LIBRARY_PATH=.
    #~/.bashrc修改后需要使用source命令生效
    static$ gcc main.c -lmath
    /usr/bin/ld: 找不到 -lmath
    collect2: error: ld returned 1 exit status
    static$ export LIBRARY_PATH=$LIBRARY_PATH:.
    static$ gcc main.c -lmath
    static$ 
    

动态库

  • 动态库和静态库不同,链接动态库不需要将被调用的函数代码复制到包含调用代码的可执行文件中,相反链接器会在调用语句处嵌入一段指令,在该程序执行到这段指令时,会加载该动态库寻找被调用函数的入口地址并执行
  • 如果动态库中的代码同时为多个进程调用,动态库在内存的实例仅需一份,为所有使用该库的进程所共享,因此动态库也称为共享库
  • 动态库的扩展名是.o,例如:lib.so

动态库的构建

  • 编辑库的实现代码和结构声明

    • 计算模块:calc.h calc.c
    • 显示模块:show.h show.c
    • 接口文件:math.h
  • 编译成目标文件

    shared$ gcc -c -fpic calc.c
    shared$ gcc -c -fpic show.c
    shared$
    
  • 打包成动态库

    • gcc -shared calc.o show.o -o libmath.so

      shared$ gcc -shared calc.o show.o -o libmath.so
      shared$ ls libmath.so
      libmath.so
      
  • 编译链接也可以合并一步完成

    • gcc -shared -fpic calc.c show.c -o libmath.so
      • PIC(Position Independent Code,位置无关代码)
      • 一种计算机编程的概念,它指的是一种可以在程序执行期间在内存中的任何位置运行的代码。这种代码与程序中的特定位置无关,因此可以在程序的不同部分之间自由移动,而不会影响程序的执行。
      • -fPIC:大模式,生成代码比较大,运行速度比较慢,所有平台都支持
      • -fpic:小模式,生成代码比较小,运行速度比较快,仅部分平台支持
      shared$ ls
      calc.c  calc.h  main.c  main.h  show.c  show.h
      shared$ gcc -shared -fpic calc.c show.c -o libmath.so
      shared$ ls
      calc.c  calc.h  libmath.so  main.c  main.h  show.c  show.h
      

动态库的使用

编译并链接动态库

  • 直接链接动态库:gcc main.c libmath.so

  • -l指定库名,用-L指定库路径:gcc main.c -lmath -L

  • -l指定库名,用LIBRARY_PATH环境变量指定库路径

    • export LIBRARY_PATH=$LIBRTATY_PATH:.
    • gcc main.c -lmath
  • 运行时需要保证LD_LIBRARY_PATH环境变量中包含共享库所在的路径用于告知链接器在运行时链接动态库

    • export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
    shared$ ./a.out 
    ./a.out: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
    #执行a.out提示无法打开共享库文件libmath.so
    #使用ldd查看提示libmath.so => not found
    shared$ ldd a.out 
    	linux-vdso.so.1 =>  (0x00007ffec17fa000)
    	libmath.so => not found
    	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9c3e4c5000)
    	/lib64/ld-linux-x86-64.so.2 (0x00007f9c3e88f000)
    
    #库路径未设置:当程序试图加载共享库时,它会查找LD_LIBRARY_PATH环境变量中指定的目录。需要确保 libmath.so 所在的目录被包含在这个环境变量中。例如,如果 libmath.so 在 /home/user/libs/ 目录下,您可以这样设置:
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/libs/
    
  • 在可执行程序的链接阶段,并不将所有调用函数的二进制代码复制到可执行程序中,而只是将该函数在共享库中的地址嵌入到调用模块中,因此运行时需要依赖共享库

动态库的动态加载

使用动态加载共享库时需要在gcc命令后添加-ldl参数

gcc load.c -ldl

在程序执行过程中,开发人员可以动态加载共享库(使用时再加载)

在程序中动态加载动态库需要调用一组特殊的函数,它们被声明于一个专门的头文件中,并在一个独立的库中予以实现

使用这组函数需要包含此头文件,并链接该库

#include<dlfcn.h>
void * dlopen(char const* filename,int flag);
  • 功能:将共享库载入内存并获得其访问句柄
    参数:
    • filename 动态库路径,若只给文件名不带目录,则根据LD_LIBRARY_PATH环境变量的值搜索动态库
    • flag 加载方式,可以取以下值:
      • RTKD_LAZY - 延迟加载,使用动态库中的符号时才真的加载进内存
      • RTLD_NOW - 立即加载
  • 返回值:成功返回动态库的访问句柄,失败返回NULL
  • 句柄:句柄唯一地表示了系统内核所维护的共享库对象,将作为后续函数调用的参数
void* dlsym(void* handle,char const* symbol)
  • 功能:从已被加载的动态库中获取特定名称的符号地址
    • 参数:
      • handle 动态库访问句柄
      • symbol 符号名
  • 返回值:成功返回给定符号的地址,失败返回NULL
  • 该函数所返回的指针为void*类型,需要类型与实际目标类型相同的指针才能使用
int(*p_add)(int,int) = (int(*)(int,int)dlsym(handle,"add"));
if(!p_add){
    fprintf(stderr,"获取地址失败!\n");
    exit(EXIT_FALURE);
}
int sum = p_add(30,20);
int dlclose(void* handle);
  • 功能:从内存中卸载动态库
  • 参数:handle动态库句柄
  • 返回值:成功返回0,失败返回非0
  • 所卸载的共享库未必会真的从内存中立即消失,因为其他程序可能还需要使用该库
  • 只有所有使用该库的程序都显示或隐式地卸载了该库,该库所占用的内存空间才会真正得到释放
  • 无论所卸载的共享库是否真正被释放,传递给dlclose函数的句柄都会在该函数成功返回后立即失效
char* dlerrror(void);

功能:获取在加载、使用和卸载共享库过程中所发生的错误

返回值:有错误则返回指向错误信息字符串的指针,否则返回NULL

void* handle = dlopen("libmath.so",RTLD_NOW);
if(!handle){
    fprintf(stderr,"dlopen:%s\n",dlerror());
    exit(EXIT_FALURE);
}

示例:

//动态库的动态加载
#include<stdio.h>
#include<dlfcn.h>//dlopen() dlsym() dlclose() dlerror()
int main(void){
    //加载动态库到内存
    void* handle = dlopen("./shared/libmath.so",RTLD_NOW);
    if(handle == NULL){
        fprintf(stderr,"dlopen:%s\n",dlerror());
        return -1;
    }
    //获取库中函数地址
    int (*add)(int,int) = dlsym(handle,"add");
    if(add == NULL){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
    }
    int (*sub)(int,int) = dlsym(handle,"sub");
    if(sub == NULL){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
    }
    void (*show)(int,char,int,int) = dlsym(handle,"show");
    if(show == NULL){
        fprintf(stderr,"dlsym:%s\n",dlerror());
        return -1;
    }
    //使用
    int a = 3,b = 8;
    show(a,'+',b,add(a,b));
    show(a,'-',b,sub(a,b));
    //卸载动态库
    dlclose(handle);
    return 0;
}

辅助工具

  • 查看符号表:nm
    • 列出目标文件、可执行程序、静态库、或共享库中的符号
    • 例如:nm libmath.a
  • 查看依赖:ldd
    • 查看可执行文件或者共享库所依赖的共享库
    • 例如:ldd a.out

静态库和动态库对比

  • 静态库
    • 优点
      • 执行速度快;
      • 可执行程序不依赖库的存在
    • 缺点
      • 文件体积相对大;
      • 更新困难,维护成本告
  • 动态库
    • 优点
      • 可执行文件体积小,节省空间;
      • 易于链接,便于更新维护
    • 缺点
      • 文件执行速度较慢;
      • 可执行程序依赖库文件的存在
静态库 动态库
优点 执行速度快;可执行程序不依赖库的存在 可执行文件体积小,节省空间;易于链接,便于更新维护
缺点 文件体积相对大;更新困难,维护成本告 文件执行速度较慢;可执行程序依赖库文件的存在
posted @ 2024-12-06 14:05  -O-n-e-  阅读(143)  评论(0)    收藏  举报