C/C++学习总结积累(超详细)——C提高:预处理(include、define、可变宏参)、编译、汇编、链接

18 篇文章 1 订阅
18 篇文章 0 订阅

目录

1.预处理的基本概念

1.文件包含处理:#include<> 和 #include“ ” 区别

2.(#define)宏,什么是宏?宏有什么应用?

0.宏的特殊符号

 1.一些预定义宏

 2.宏常量

 3.宏语句 

 4.宏函数

     可以使用 #define 来定义一个带有参数的宏

可变参数宏(__VA_ARGS__、##__VA_ARGS__)

C-可变参数列表(va_list)

     ​ 

 5.条件编译 (宏控)

2.编译阶段

3.汇编阶段 

4.链接阶段

因此这里有了库的概念

1.库文件什么时候可以链接 进来?

2.静态链接?动态链接?静态库?动态库?共享库? 

静态链接、动态链接区别?

 动态库(共享库)、静态库区别?

为什么有了静态库?还需要动态库?      

下面用gcc编译静态库、动态库实例,以及如何使用

 

 


编译器驱动程序读取源程序文件hello.c,把它翻译成一个可执行文件,这个翻译过程分为四个阶段::预处理、编译、汇编、链接

这里知识举个例子、编译最终生成的时叫目标文件,目标文件有三种,下文会有介绍

  • 了解程序是如何编译有什么好处呢?
  1. 优化程序性能(如何提升时空效率)
  2. 理解链接时出现的错误
  3. 避免安全漏洞(如缓冲区溢出)

1.预处理的基本概念

  • 预处理是在程序源代码被编译之前,由预处理器(Preprocessor)对程序源代码进行的处理。这个过程并不对程序的源代码语法进行解析,但它会把源代码分割或处理成为特定的符号为下一步的编译做准备工作
  • 预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
  • 所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
  • 根据#开头的命令,修改原始的c程序。
  • C++ 还支持很多预处理指令,比如 #include、#define、#if、#else、#line 等 

1.文件包含处理:#include<> 和 #include“ ” 区别

文件包含处理”是指一个源文件可以将另外一个文件的全部内容包含进来。C语言提供了#include命令用来实现“文件包含”的操作。

比如 #inlude<stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中

  • “” 表示系统先在file1.c所在的当前目录找file1.h,如果找不到,再按系统指定的目录检索。
  • < > 表示系统直接按系统指定的目录检索。
    注意:
    1. #include <>常用于包含库函数的头文件;
    2. #include ""常用于包含自定义的头文件;
    3. 理论上#include可以包含任意格式的文件(.c .h等) ,但一般用于头文件的包含;

2.(#define)宏,什么是宏?宏有什么应用?

  • 宏的作用  
       是用来替代一段代码,这是为了达到使代码看起来比较明了,也是为了方便修改和控制代码。
       比如你的程序里面很多地方用到一段相同的代码,而你现在想改掉代码中的一部分,如果你刚开始是用宏来表示这一    段代码,那么你只需要改动宏里面的就行了,而不需要在整个程序里到处改
  •   预处理时是怎样处理这些宏的?

         只进行简单替换,不会进行任何逻辑检测,即简单代码复制而已,既然是单纯的替换

         记住:就是替代,这种替代是生硬的,所以进行宏定义时需认真仔细

          所以为了保证宏定义结果的安全性,一般每个变量都要加上括号

         比如:

            

  • 说明:
  1. 宏名一般用大写,以便于与变量区别;
  2. 宏定义可以是常数、表达式等;
  3. 宏定义不作语法检查,只有在编译被宏展开后的源程序才会报错;
  4. 宏定义不是C语言,不在行末加分号;
  5. 宏名有效范围为从定义到本源文件结束;
  6. 可以用#undef命令终止宏定义的作用域;
  7. 在宏定义中,可以引用已定义的宏名;
  • 应用 

0.宏的特殊符号

  #与 ## 是俩个特殊符号,它们的含义如下

  1. # 表示将一个宏参数变成一个字符串
  2. ## 表把俩个字符串粘在一起
#define PRINT(VALUE) \
   printf(#VALUE);
#define STRCAT(str1,str2) \
      str1##str2

 

 1.一些预定义宏

__LINE__这会在程序编译时包含当前行号。
__FUNCTION__当前函数名
__FILE__这会在程序编译时包含当前文件名。
__DATE__这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
__TIME__

这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 

        

等等。。。 

 

 2.宏常量

#define 预处理指令用于创建符号常量该符号常量通常称为

#define PI 3.14

当这一行代码出现在一个文件中时,在该文件中后续出现的所有宏都将会在程序编译之前被替换为 3.14

  • 与const 常量有什么区别? 

      const定义的常量,在程序运行时在常量表中,系统为它分配内存(静态存储区的只读数据区),而宏常量是预处理时只是替换,没有分配

      const定义的常量,在编译时进行严格的类型检验,而宏常量不会 

 3.宏语句 

     

 4.宏函数

优点:
预编译的时候展开,不需要每次运行时载入,这种情况效率比函数高
缺点:

预编译后产生的文件比函数调用要大,
宏表达式中不能出现递归定义,这点区别于函数,因为宏只做简单的文本替换,且只替换一次,如果出现递归定义,就会无法被完全替换,导致后续编译时原宏名被当作函数;

  •      可以使用 #define 来定义一个带有参数的宏

          

a,b,t,就是参数。\是换行

  • 可变参数宏__VA_ARGS__、##__VA_ARGS__

#define debug(...) printf(__VA_ARGS__)
缺省号(...)代表一个可以变化的参数表。使用保留名 __VA_ARGS__ 把参数传递给宏。当宏的调用展开时,实际的参数就传递给 printf()了

例如:调用Debug("Y = %d\n", y); 而处理器会把宏的调用替换成:printf("Y = %d\n", y);

 因为debug()是一个可变参数宏,你能在每一次调用中传递不同数目的参数:

 ##__VA_ARGS__ 作用与__VA_ARGS__类似

 ##__VA_ARGS__ 宏前面加上##的作用在于,当可变参数的个数为0时,这里的##起到把前面多余的","去掉

 但我用vs没测出差距(貌似跟编译器有关),示例如下:

  • 应用

 如果这样,就可以自定义打印信息输出,这样更方便打印调试信息

              

  • C-可变参数列表

      函数 test() 最后一个参数写成省略号,即三个点号(...),省略号之前的那个参数是 int,代表了要传递的可变参数的总数。为了使用这个功能,您需要使用 stdarg.h 头文件,该文件提供了实现可变参数功能的函数和宏

      

VA_LIST的用法: 
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针; 
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量; 
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数); 
(4)最后用VA_END宏结束可变参数的获取

使用VA_LIST应该注意的问题: 
(1)不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。 
(3)由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型

 

 5.条件编译 (宏控)

  • 使用ifndef防止头文件被重复包含和编译
  • 使用 ifdefine  else  endif 选择性编译

 

2.编译阶段

编译器将文本文件hello.i(预处理形成的)翻译成文本文件hello.s,是一个汇编语言程序。汇编语言是:低级机器语言指令

3.汇编阶段 

接下来,汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o(二进制文件)

4.链接阶段

为什么会有链接、当我们需大量重复使用某段代码时,我们可以将其编译成库文件,然后在需要使用它时才去加载它,这样不用每次去找源代码编译

例如:我们的代码中用到的printf,等一些标准库函数,都存在单独的预编译好了的目标文件中,而我们要用到它,就必须以某种方式合并到我们的hello.o程序中。链接器就负责处理这些合并。结果就得到一个目标文件。可以被加载到内存中,由系统来执行。

  • 链接要完成什么任务?

必须完成两个任务:1.符号解析  2.重定位 

  • 因此这里有了的概念

 本质上来说是一种可执行代码的二进制形式,可以被操作系统载入内存执行

 通俗的说就是把这些常用函数的目标文件打包在一起,提供相应函数的接口,便于程序员去使用(只需包含对应头文件即可)

库文件也是目标文件,都是事先编译好的。

那么,根据在不同时刻去使用(即什么时候链接这个库)库文件,库文件有两种:静态库(.a、.lib)、动态库(.so、.dll) 

linux:  .a  .so

windows: .lib  .dll

  • 关于目标文件(就是最终编译生成的文件)
  • 可重定位目标文件
  • 可执行目标文件
  • 共享目标文件(特殊可重定位目标文件,可动态加载进内存并链接) 

注意: 以上说的是编译一个目标文件的最后一步:链接。这个目标文件可以是可执行目标文件、也可以是库文件。

这个链接注意要和后面讲的静态链接、动态链接不是一个东西。

静态、动态链接:是一个库文件什么时候被链接进来。

1.库文件什么时候可以链接 进来?

  • 此链接不是编译库时候的链接,是编译出来的库什么时候链接进来,即就是与其他代码合并进来
  • 可以执行于编译时、也可以执行于加载时、甚至执行于运行时

2.静态链接?动态链接?静态库?动态库?共享库? 

  • 静态链接、动态链接区别?

       说白了就是这个库文件什么时候被链接进来

执行于编译时是静态链接,也就是静态库(只能是静态库)文件是和其他代码一起合并编译成一个可执行目标文件,这个过程是静态链接

执行于加载时、运行时是动态链接,也就是这个动态库库(只能是动态库),不和其他代码合并编译(创建可执行文件时,链接器只是复制了一些重定位和符号表信息,并没有把代码和数据合并进来),他是可执行目标文件在加载才调用这个库里的代码。

执行于运行时也是动态链接,应用程序还能再他运行时要求动态链接器加载和链接某个共享库,而无需编译时将那些库连接进来。linux提供有相应的接口

 

动态库是一个目标模块,可以加载到任意内存地址,并和一个在内存中的程序链接起来,这个过程称之为动态链接

以下两个图应该清楚表现了他俩的区别了

  •  动态库(共享库)、静态库区别?

1.首先他们产生过程不同

静态库:把一些接口文件编译生成.o文件,然后压缩打包即可

动态库:编译生成位置无关.o文件,然后单独编译指定-shared 编译成得到动态库,方便你想啥时候使用它就啥时候使用他。

  • 为什么有了静态库?还需要动态库?      

      其实,从上面所讲的区别也能知道,静态库太占空间了,因为每个需要用到此库的进程都会复制一份代码为自己所用,这是最直观的感受。动态库的出现就是为了解决缺陷

      其次静态库需要定期维护和更新,那么一个程序员就得实时跟踪更新情况,然后显示地与库重新连接

      动态库也可称之为共享库:因为所有引用该库的可执行目标文件都共享这个库的代码和数据,都用这一份代码和数据,而不是像静态库的内容那样被复制和嵌入到引用他们的可执行文件中。

  • 下面用gcc编译静态库、动态库实例,以及如何使用

  注意:gcc编译参数说明:见另一篇博客:https://blog.csdn.net/Wmll1234567/article/details/109852877

  •  静态库制作和使用,只是简单示例
  • 工程目录

     myself_test  

           include

           lib

           src

           main.c

  • 加载时动态库制作和使用 

  •  运行时动态库使用(直接使用上面已经编译好的 libMytest_dynamic.so
  •   头文件
#include <dlfcn.h>
  • 打开动态链接库
函数原型
void *dlopen (const char *filename, int flag);

filename:共享库名
flag:分为这两种 
RTLD_NOW:在dlopen返回前,解析出全部没有定义的符号,解析不出来返回NULL。
RT_GLOBAL:动态库定义的符号可被其后打开的其他库解析。
RT_LOCAL:和上面相反,不能被其他库解析。默认。
RTLD_LAZY:暂缓决定,等有需要时再解出符号


返回值: 
打开错误返回NULL 
成功,返回库引用 
dlopen用于打开指定名字(filename)的动态链接库(最好文件绝对路径),并返回操作句柄。
  • 取函数执行地址
void *dlsym(void *handle, char *symbol); 
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。
  • 关闭动态库
函数原型
int dlclose (void *handle); 
returns 0 on success, and nonzero on error.
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
  • 动态库错误函数 
const char *dlerror(void);
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

   以下是展示过程:

  • 注意:

     编译参数-rdynamic 的意义。这个意味着符号解析,全局可用,意味着无论是库里定义的还是程main程序这边定义的全局变量。都能互相访问。

但值得注意的是,如果两边定义了同名的强符号变量,按理来说会报错,但经过实验并不会。。。那么假如两边同名但功能上两者不相干,那就出问题了。。。。所以这个参数慎用 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值