C语言预处理和函数

来源:互联网 发布:淘宝店推广引流的技巧 编辑:程序博客网 时间:2024/06/01 19:30

预处理

预处理是C语言中的一个重要特性,值得我们深入讨论,

源代码到可执行程序

源代码经过编译。生成.o文件,再经过链接,成为elf可执行程序,但是实际上,源码经过编译之后,不是直接的.o文件,而是汇编文件.S,汇编文件经过汇编,才得到目标文件.o,所以说,C语言程序需要先被编译成汇编文件,再把汇编文件编译为目标文件,但是更进一步,在源码被编译成汇编文件之前,需要先进行预处理,也就是我们这个章节的重点,源代码文件经过预处理,成为预处理过的源码文件,再经过编译成为汇编文件,预处理后的源码文件仍然是.c文件。

预处理的意义

编译器本身的主要目的是将C的源代码转化为.S的汇编代码,预处理专门做一些正式编译之前的事情,让编译器专注于自身的编译功能。

常见的预处理功能

头文件包含#include

#include有两种方式,#include<>和#include”“,其中<>专门用于包含系统提供的头文件,例如C标准库文件,不是程序员自己写的,而”“用于包含程序员自己写的头文件。

如果使用<>编译器只会到系统指定目录或者编译器配置(使用-I指定)的位置寻找头文件,如果找不到会提示头文件不存在。

使用”“包含的头文件,编译器会默认现在当前的目录先寻找,如果当前目录中没找到,再到系统指定目录或者编译器指定的目录去寻找,如果再找不到,才会提示头文件不存在。

一般默认使用原则,如果系统指定自带目录,使用<>,如果是自己写的并且在当前目录下,就是用”“,如果是自己写的并且存储在一个专门存储头文件的目录下,就在编译器中使用-I来制定。

头文件包含实质上,是在包含语句那一行将包含的头文件展开,替换该包含语句,这些过程都在预处理阶段执行。

注释

注释是代码阅读者看的,不是给编译器看的,所以对编译器来说注释是无用的,所以最好在编译阶段就不要出现注释,在预处理阶段会擦除所有的注释,以免在编译阶段影响编译器编译。

条件编译

有时候我们希望程序有多种配置,利用这些配置来更改程序,我们编写配置代码,然后使用胚子开关,来使程序编译出不同的效果,#ifdef和#if是有区别的,#ifdef判断条件是否成立主要是看目标宏在该语句之前是否成立,#if (条件表达式)判断标准是小括号中的表达式结果是否是true。

在gcc中,编译时可以传递一些参数来对编译做一些控制,例如-c,-o参数等,同理,我们可以使用-E参数实现只进行预处理,不进行编译。

宏定义

宏定义的处理在预处理时是一项非常重要的工作,所以我们专门拿出一节来对宏定义的处理作出解释。

宏定义的使用规则和解析

宏定义在预处理阶段由预处理器进行替换,这个替换是纯文本的原封不动的替换,甚至精确到符号,空格也会原封不动的替换或者保留,宏定义的替换会递归进行,一直到最后一个宏定义符号也被替换完成。

带参数的宏定义

宏定义可以带参数,使用方法和带参的函数非常类似,定义这种宏时,每一个宏结构中的参数都必须加括号,整个宏结构都应该加上括号,防止在宏替换的时候,由于简单的文本替换导致意外的结果。

宏定义是在预处理期间处理的,函数是在编译期间处理的,宏定义最终是在调用宏的地方把宏结构原地展开,函数是无法展开的,在函数调用的时候跳入被调用函数中执行,执行完毕后回到原来的函数中,所以宏定义的优势在于没有调用和传参的开销,当要执行的程序很短时,可以尝试用宏定义替代,以提高效率。

宏定义不会检查参数的类型,返回值也不会带类型,但是函数有明确的参数和返回值类型,调用函数时编译器会做参数类型检查,类型不匹配则会报错,所以在使用宏的时候必须要注意保持实际参数和宏定义的时候规定的类型一致,否则会报错。

如果程序代码较多或者逻辑 比较复杂,适合使用函数,对于那些代码很少的代码,适合使用宏,但是宏不检查类型,使用的时候一定要注意。

内联函数

内联函数在函数定义前加inline关键字,内联函数本质上是一个函数,可以使用编译器进行静态类型检查,同时,内联函数也有带参宏定义的优点,不需要调用,可以直接在原地展开,省去了函数调用的开销,可以把内联函数认为是带了参数类型检查的宏。

内联函数适用于函数体很短或者逻辑很简单,同时又需要使用编译器进行类型检查,减少调用开销的时候使用。

函数

函数的本质

整个程序分成多个源文件,一个文件由多个函数构成,每个函数由多个语句构成,这样的好处在于便于程序的编写,分工组织并分化问题。函数的出现方便了程序员的编码。

函数要遵循一定的格式,必须符合函数的基本要求,包含基本的函数元素,一个函数只做一件事情,不能太长,也不宜过短。函数参数不能过多,过多的参数会降低函数的执行效率,如果确实需要很多参数参与运算,考虑使用结构体进行封装。函数内部尽量不要使用全局变量,使用参数和外部沟通。

函数会被编译成可执行的代码段,而变量被编译后成为数据,程序运行需要代码和数据结合才能完成,代码需要加工数据,两者彼此配合,才能完成任务。程序运行的主要任务是得到目标数据,函数就是为了完成这些任务而生的,所以说,实质上,函数是一个数据处理器。

函数的使用

函数有三个要素,定义,声明,使用:

  1. 定义就是函数体,由函数名,参数列表,返回值,函数体构成了一个函数,函数定义表明了函数在内存中的地址,有了地址,我们才能通过函数名来调用
  2. 声明就是函数原型,主要是告诉编译器函数的原型
  3. 函数调用就是使用函数,

函数原型的主要作用是,让编译器帮助我们进行参数的静态类型检查。

程序编译是以单个源文件为单位的,所以一定要在调用之前声明,编译器从源文件的第一行进行编译,遇到一个函数声明时,就会收集到函数声明表中,当遇到一个函数调用,就会去函数声明表中去查询对应的函数原型,如果没有或者没有完全匹配,则会报错或者警告。

递归函数

递归函数就是函数中调用了自己的函数,实际上递归函数是在栈内存上递归执行的, 每递归执行一次,就需要耗费一些栈内存,所以递归一定要设置递归终止条件,以防止耗尽栈内存,出现栈溢出异常。

递归函数必须把握好递归终止条件,主要用于斐波那契数列或者阶乘计算等场景。

函数库

函数库其实就是事先写好的函数的集合,函数本来就具有可复用性,可以很容易的被反复使用。

早期的函数共享都是以源代码的形式共享的,后来有商业公司将其发展成库的形式提供,库主要有两种,静态链接库和动态链接库。

静态链接库

静态链接库将函数源代码只编译不链接,形成.o的目标文件,然后用ar工具将这些目标文件归档为.a压缩文件,形成静态链接库文件,将.a静态链接库文件和.h头文件提供出去,使用者通过头文件得到函数原型,然后调用这些库函数即可,链接器会将被调用的函数链接到使用者的程序中。

动态链接库

动态链接库是静态库的改进型,效率较静态库高一点,静态库链接自己的程序时就把库函数的代码链接到程序中了,但是这样会占用程序的大小,导致编译出来的程序体积太大,使用动态链接库的程序在编译的时候并没有把动态库链接进去,而是在程序运行的时候再去加载到内存,可以实现多程序共用一个动态链接库,可以节省很大的空间。

函数库的使用

GCC编译链接程序的时候默认使用的是动态链接库,如果需要静态链接需要显示的指定-static强制使用动态链接库,库函数使用一定要包含对应的头文件,并且正确使用其函数原型,某些非系统的库函数在链接时需要显示使用-lxxx指定链接。动态链接库需要使用-L指定动态链接库的地址。

制作函数库

静态链接库

静态链接库的制作过程:

  • 使用gcc -c文件只编译不链接,生成目标文件
  • 使用ar -rc指令,将这些目标文件归档为.a文件
  • 库文件名不能随便乱起,一般是lib+库名称
  • 将库文件和头文件发布出去,即可供别的程序调用

使用静态链接库时,使用-laston -L链接静态链接库到我们的程序中。

动态链接库

动态链接库的制作过程:

  • 动态链接以so作为后缀名
  • 使用gcc -c -fPIC将源文件边编译成位置无关码,以便运行时被操作系统动态加载,得到目标文件
  • 使用gcc -o -shared将目标文件编译成动态链接库
  • 动态链接库的文件名必须要以so结尾
  • 将库文件和头文件发布出去,即可供别的程序调用
原创粉丝点击