C语言 | 预处理

程序的编译过程

第一步:预处理阶段
demo.c --> demo.i(展开#中内容)
第二步:编译阶段
demo.i --> demo.S (汇编文件)
第三步:汇编阶段
demo.S --> demo.o (没有带地址的二进制文件)
第四步:链接阶段
demo.o --> demo.elf (带地址的二进制文件)

前三阶段只需要声明即可,仍未链接成段

链接阶段则需查证源码

  1. 补齐所有用到的代码(包括_start)
  2. 分段
  3. 补上可执行地址

关于ELF文件

.o文件就是对象文件,是可重定向文件的一种,通常以ELF格式保存,里面包含了对各个函数的入口标记,描述,当程序要执行时还需要链接(link).链接就是把多个.o文件链成一个可执行文件。
在win平台下,用来链接的重定向文件也可为PE格式的.obj文件
当多种编程语言希望混合编译时,就可以通过分别编译成.o文件,再链接(link)成可执行文件。

预处理指令

机器指令:让处理器执行某个操作
编译指令: 让编译器执行某个编译动作

C文件 ——>机器指令 + 编译指令(预处理阶段执行指令)

预处理阶段:
C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译。

为了区分预处理指令和一般的C语句,所有预处理指令都以符号" #"开头,并且结尾不用分号。例如:

#define
MAX_ CNT 100

预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件

C语言提供的预处理指令主要有:
文件包含: #include
宏的定义: #define
条件编译: #ifdef, #if_ _#elf, #ifndef

文件包含指令

在程序开发过程,用户开发程序时,经常需要调用(引用)其他已经存在的程序,这时需要包含这些进到你的文件,一般使用include(本质就是copy),一般格式如下:

#indlude “带路径的文件”

include后面的路径使用”还是使用<>?

<文件名>:

表示到系统默认指定的- -些录,比如/usr/indude 下面去查找指定文件名的文件,前提是这些文件必须在默认目录
优点:是可以省掉路径

“带路径文件":

表示先到指定路径下查找这个文件,如果没有这个文件,还要到系统默认路径查找这个文件
优点:是包括了<>的功能
带路径的文件:可以是绝对路径,也可以是相对路径
比如:如果hello.c跟代码是同一级目录,可以如下写:
#indlude “/var/GZE2020/hello.c”
#indlude “./hello.c”
#incude “hello.c” // 同上一条也是表示当前目录

注意

当某个.c文件中包含的功能函数过多,而我们的需求功能只占少数时:

  1. include 虽然可以直接包含.c文件,但是在预处理阶段会将整个.c文件的内容拷贝到调用文件,导致效率低下
  2. include并不会说明库文件在哪,而是由编译器在链接阶段根据环境变量自行遍历默认库文件, 一般是用于包括.h文件,我们要将文件中想被别人调用的代码封装声明到头文件中去,此时想要调用的代码被编译成库,编译时编译器自动查找,则不会产生1中问题

调用一段代码的三种方法

  1. #include “lib.c”
  2. 编译成 .o文件 之后在编译链接阶段主动提供
  3. 编译成库(做成 .so文件 并放入默认lib目录)后编译器自动查找

在未知该库函数内功能函数传参及返回值时,直接声明函数不方便;因此调用前采取声明一个 .h文件(该文件专门用于给调用者加载声明、告诉调用者有哪些函数、传参及返回类型)

提供或调用代码的过程
写好包含功能函数的**.c文件后,将其编译成.o文件给别人直接链接;或做成库.so文件**放到固定目录中给他人调用,同时通过 .h文件 提供 .c文件中各个函数的原型声明

lib.c

int add_func(int a, int b)
{
	return a + b; 
}

int sub_func(int a, int b)
{
	return a - b; 
}

lib.h


int add_func(int , int ); 
int sub_func(int , int ); 

test.c(别人写的需要调用你写的lib.c中功能函数的代码)

//#include "lib.c"

//int add_func(int , int ); 
//int sub_func(int , int ); 

#include "lib.h"


int main(int argc, char *argv[])
{
	int c; 
	int a = 1; 
	int b = 2; 

	c = add_func(a, b); 
	c = sub_func(a, b);   
	
	return 0; 
}

宏定义指令

在C语言中,主要是通过预处理指令“#define"来定义宏,一般格式如下:
#define 宏名 宏体
实际为编译器允许的通过简单形式表达的复杂内容,并在预处理阶段时宏名作为一个符号已经将其中内容转换展开
实际使用过程中,又分为两种情况,分别为不带参数及带参数

不带参数
#define 宏名 字符串,比如
#define DEBUG 10
右边的字符串也可以省略,比如
#define DEBUG
表示宏是空的,在预处理时会按照空的来处理,所以也是可以的,不会报错。

带参数
#define 宏名(参数列表) 字符串
带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。

示例

#include <stdio.h>	

#define MAX  100
#define ADD(x,y)  (x+2*y)      


#define  strcpy__(dst, src)      strcpy(dst, #src)
//#是字符串化的意思,将跟在#后面的宏参数转成一个字符串
strcpy__(buff,abc)  相当于 strcpy__(buff,“abc”)


#define FUN(arg)     my##arg//##是连接符FUN(ABC)
等价于  myABC


#define SH_MINOR_LED_ON		0 
#define SH_MINOR_LED_OFF	1 

#define a1 	1 
#define a2      2 


int main()
{
	printf("add: %d\n", 3*ADD(1,2));

	//printf("%s\n", CONCAT(1,2));

	printf("%s, %d, %s\n", __FILE__, __LINE__, __FUNC__);


	return 0; 
}

当宏的内容比较复杂时,可用 \ 换行,如下

# define atomic_compare_and_exchange_val_rel(mem, new, old)      \
  __atomic_val_bysize (__arch_compare_and_exchange_val, int,    \
                       mem, new, old, __ATOMIC_RELEASE)

ANSI标准预定义的宏名
__FILE__是内置宏, 代表源文件的文件名,使用%s打印
__LINE__是内置宏,代表该行代码的所在行号,使用%d打印
__DATE__宏指令,含有形式为月/日/年的串,表示源文件被翻译到代码时的日期,使用%s打印
__TIME__源代码翻译到目标代码的时间作为串包含在__TIME__中。 串形式为时:分:秒
__STDC__如果是ANSI标准c常量为1,如果为其他非ANSI标准则取决于平台

#include <stdio.h>

int main()
{
	printf("filename:%s\n",__FILE__);
	printf("filenumber:%d\n",__LINE__);
	printf("maketime: %s, %s\n", __DATE__, __TIME__);

	return 0;
}

宏函数与函数的区别

从整个使用过程可以发现,带参数的宏定义,在源程序中出现的形式与函数很像。但是两者是有本质区别的:

  1. 宏定义不涉及存储空间的分配、参数类型匹配、参数传递、返回值问题
  2. 函数调用在程序运行时执行,而宏替换只在编译预处理阶段进行。所以带参数的宏比函数具有更高的执行效率(相对于处理器执行效率高,但是编译效率会拉低)
  3. 宏函数适合使用率高但逻辑较为简单代码块。

那么,宏函数具体怎么使用呢?
假设有这样一条功能函数比较两个数或者表达式大小,首先我们把它写成宏定义:

#define MAX( a, b) ( (a) > (b) (a) : (b) )

其次,把它用函数来实现:

int max( int a, int b)
{
	return (a > b a : b)
}

如果这段代码要频繁使用,让我们选择用函数宏或者用函数来实现。很显然我们不会选择用函数来完成这个任务,原因有两个:

  1. 函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;
    2.函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。

反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。

还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
看下面的例子:

#define MALLOC(n, type) \

((type * ) malloc ( (n)* sizeof (type)))

利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。我们可以观察一下这个宏确切的工作过程:

int *ptr;

ptr = MALLOC ( 5, int );

//将这宏展开以后的结果:

ptr = (int *) malloc ( (5) * sizeof(int) );

这个例子是宏定义的经典应用之一,完成了函数不能完成的功能。

条件编译指令

条件编译是指在预处理阶段,识别哪些代码需要编译到程序里面去,哪些代码不需要编译,这个主要是通过条件编译指令#if、 #ifdef、 #ifndef 等来实现的

运用场景
  1. 当要使用一个文件处于不同平台如win、x86、arm(编译器有区别)时,实现相同功能的代码可能不同,此时就需要告诉编译器编译相应的分支代码
  2. 嵌套调用一段代码时,#include "某.h文件"本已经展开了一段代码,若二次调用该代码时,则会重复展开.h文件中的代码,此时需要在 .h文件 中判断功能代码是否已展开

运用2示例

.h文件
#ifndef __MYFUNC_H__    
#define __MYFUNC_H__

int add_func(int, int); 

#endif 
.c文件
#include "func.h"
#include "func.h"//模拟嵌套调用

int main(int argc, char *argv[])
{
	int a = 1,b = 2, c; 
	c = add_func(1, b); 

	printf("c: %d\n", c);

	return 0; 
}

#if结构一般格式:

#if 条件1
…code1…
#elif 条件2
…code2…
#else
…code3…
#endif

实现以下逻辑:

  1. 如果条件1成立,那么编译器就会把 #if 与 #elif 之间的 code1 代码编译进去(注意:是编译进去,不是执行,和平时用的 if-else 是不一样的)
  2. 如果条件1不成立、条件2成立,那么编译器就会把 #elif 与 #else 之间的code2代码编译进去
  3. 如果条件1、2都不成立,那么编译器就会把 #else 与 #endif 之间的code3编译进去
  4. 注意,条件编译结束后,要在最后面加一个 #endif ,不然后果很严重(自己思考一下后果)

#if 和 #elif 后面的条件一般是判断宏定义而不是判断变量,因为条件编译是在编译之前做的判断,宏定义也是编译之前定义的,而变量是在运行时才产生的、才有使用的意义

#ifdef结构一般格式:

#ifdef MAX
…code…
#endif
如果前面已经定义过MAX这个宏,就将code编译进去。

#ifdef MAX
…code1…
#else
…code2…
#endif

#ifndef结构一般格式:

#ifndef MAX
…code…
#endif
如果前面没有定义过MAX这个宏,就将code编译进去。

常见的预处理指令还有哪些?有什么用?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值