[C语言] 14--编译器、宏定义和头文件

1.编译器

概念:编译器是一个用来帮助我们把原码.c翻译成计算机能够直接识别的二进制编码。使用不同的编译器可以翻译出来不同机器的二进制编码。
gcc编译器:
gcc hello.c -o hello
gcc -->C语言编译器
hello.c -->需要编译的原码
-o -->指定输出文件名
hello --> 可执行文件的名字

编译过程:
在这里插入图片描述

1.1 预处理

gcc hello -o hello.i -E
加上一个编译选项 -E就可以使得gcc在进行完第一阶段的预处理之后停下来生成一个默认后缀名为.i的文本文件

预处理是指编译代码之前先进行预先的处理工作,这些工作包含哪些内容:
头文件被包含进来(复制): #include
宏定义会被替换:#define
取消宏定义: #undef
条件编译: #if #ifdef #ifndef #else #elif #endif
修改行号以及文件名: #line 998 “Hello.c”

清除注释
预处理大部分的工作是在处理以#开头的一些语句,从严格意义来讲这些语句不属于C语言的范畴,它们在编译的第一阶段被所谓的预处理器来处理。

1.2 编译

gcc hello -o hello.s -S
加上一个编译选项-S就可以使得gcc在进行完第一和第二阶段之后停下来,生成一个默认后缀名为.s的文本文件。打开此文件看一看,你就会发现这是一个符合x86汇编语言的源程序文件。
经过预处理之后生成的.i文件依然是一个文本文件,不能被处理器直接解释,我们需要进一步的翻译。接下来的编译阶段是四个阶段中最为复杂的阶段,它包括词法和语法的分析,最终生成对应硬件平台的汇编语言(不同的处理器有不同的汇编格式)。汇编文件取决于所采用的编译器,如果用的是gcc,那么将会生成x86格式的汇编文件,如果用的是针对ARM平台的交叉编译器,那么将会生成ARM格式的汇编文件。

1.3 汇编

gcc hello.s -o hell.o -C
-c则是让编译器在对汇编文件进行编译后停下来,这里会生成一个待链接的可执行文件
则会生成一个扩展名为.o的文件,这个文件是一个ELF格式的可重定位(relocatable)文件,所谓的可重定位,指的是该文件虽然已经包含可以让处理器直接运行的指令流,但是程序中的所有全局符号尚未定位,所谓的全局符号,就是指函数和全局变量,函数和全局变量默认情况下是可以被外部文件引用的,由于定义和调用可以出现在不同的文件当中,因此他们在编译的过程中需要确定其入口地址,比如a.c文件里面定义了一个函数func(), b.c文件里面调用了该函数,那么在完成第三阶段汇编之后,b.o文件里面的函数func()的地址将是0,显然这是不能运行的,必须找到a.c文件里面函数func()的确切的入口地址,然后将b.c中的“全局符号”func重新定位为这个地址,程序才能正确的运行。因此,接下来需要进行第四个阶段:链接。
可以尝试使用命令readelf来查看可重定位文件
$readelf demo.o -a
在这里插入图片描述

1.4 链接

gcc hello.o -o hello -lc -lgcc
-lc —> -l链接 c标准C库
-lgcc —> -l链接 gcc链接GCC的库
有两个很重要的工作没有完成,首先是重定位,其次是合并相同权限的段
一个可执行镜像文件可以由多个可重定位文件连接而成,比如a.o, b.o, c.o这三个可重定位文件链接生成一个叫x的可执行文件,这些文件不管是可重定位的,还是可执行的,它们都是ELF格式的,ELF格式是符合一定规范的文件格式,里面包含很多段(section),比如我们上面所述的hello.c变异生成的hello.c有如下的格式
在这里插入图片描述

2.宏的概念

概念:宏(macro)其实就是一个特定的字符串,用来直接替换
比如:#define PI 3.14
上面定义了一个宏名为PI,在下面代码的引用过程中PI将会被直接替换为实际的值

int main(int argc,char const *argv[])
{
    printf("%f\n",PI);
    printf("%f\n",3.14);
    return 0;
}

宏的作用:
使得程序的可读取有所提高,使用一个又有意义的单词来表示一个无意义数字(某个值);
方便对代码进行迭代更新, 如果代码中有多处使用到该值, 则只需要修改一处即可(定义);
提高程序的执行效率,可以使用宏来实现一个比较简单的操作。用来替代函数的调用。

2.1 无参宏

意味着我们在使用该宏的时候不需要携带额外的参数
// 定义一个宏用来表示 人数
#define PEOPLE 10
系统中有预定义一些宏:
比如一下宏, 如果需要使用则需要包含它所在的头文件
/usr/include/linux/input-event-codes.h

#define ABS_X 0x00
#define ABS_Y 0x01
#define ABS_Z 0x02
#define ABS_RX 0x03
#define ABS_RY 0x04
#define ABS_RZ 0x05

2.2 带参宏

意味着我们在使用这些宏的时候,需要额外传递参数

#define   MAX(a,b)  a>b? a:b 
int main(int argc, char const *argv[])
{
     printf("%d\n" , MAX(198,288) );
     //printf("%d\n" , 198>288? 198:288 );  --> 预处理后
     return 0;
}

以上MAX 的宏在使用的时候 ,把198 以及 288 分别作为 a ,和b 的值, 然后进行替换。
注意:
宏只是直接文本替换,没有任何的判断以及语法检查的操作甚至运算;
宏在编译的第一个阶段, 被处理完成,运行的过程中不占用时间(宏已经不存在);
宏在预处理的时候直接展开。

2.3 带参宏的副作用

由于宏只是一个简单的文本替换,中间不涉及任何的计算以及语法检查(类型),因此在使用复杂宏的时候需要小心注意

#define   MAX(a,b)  a>b? a:b 
int x = 100 ;
int y = 200 ;
printf("%d\n" , MAX( x, y==200 ? 988:1024 ) );
// printf("%d\n" , x>y==200 ? 988:1024? x:y==200 ? 988:1024 );

从直观上来看不管 y==200 ? 988:1024 结果如何都应该比100 大 ,但是由于宏是直接的文本替换,导致替换之后三目运算符的运算结果出现了偏差。
由于带参宏的这个偏差出现的原因主要是优先级的问题,因此可添加括号来解决:

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

printf("%d\n" , (x)>(y==200 ? 988:1024)? (x):(y==200 ? 988:1024) );

注意:
宏只能写在同一行中(逻辑行),如果迫不得已需要使用多行来写一个宏则可以使用转义字符\把换行符转义掉
在这里插入图片描述

如上 41 42 43 为3个物理行, 但是通过转义字符让这三个物理行被看作同一个逻辑行

3.条件编译

3.1 无值宏

在定义宏的时候不需要给定某一个值,对于无值来说只是用来做一个简单的判断(是否有定义)
#define DE_BUG

3.2 条件编译

根据某一个条件来决定某一代码块是否需要编译。
语法:
形式1:
通过无值的宏来判断 , 则只能判断是否有定义

#ifdef    // 判断某一个宏是否有定义

        // 代码块

#endif     // 判断语句的结束

#ifdef  DE_BUG  // 如果定义了DE_BUG 宏则一下代码块会被编译,反之则不会被编译
    printf("new:%s--%d--%s--%d\n" , 
    new->Name ,new->age ,new->skill , new->udel );
#endif
#ifdef  DE_BUG  // 判断是否定义了
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#endif

#ifndef  DE_BUG  // 判断是否没定义 
      printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#endif

形式2 :
通过有值的宏来进行判断, 则可以通过值来判断(非零则真)

#define MACRO  0  // 非零则真
#define MACRO  "Hello" // 错误的, 不允许出现字符串
#define MACRO  'A'  // 允许

#if MACRO  // 只要判断MACRO为非零值则表示条件为真
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#endif
#if MACRO
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#else
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#endif


#if MACRO1
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#elif MACRO2
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#else
    printf("__%s__%s__%d__\n", __FUNCTION__ , __FILE__ , __LINE__ );
#endif

注意:
在使用有值宏进行条件编译的时候, 宏的值只允许出现整型/字符
多路分支可以根据自己的需求继续延续下去
使用条件编译必须有结束的语句 #endif 与开头进行对应

3.3 条件编译的实际应用场景

除了打开代码进行修改宏的值或者重新定义或删除宏的定义,还可以通过编译命令来定义宏

$ gcc ifdef.c  -DDE_BUG
-D     --> 定义宏  define
DE_BUG  --> 需要定义宏的名字为 DE_BUG

4.头文件

4.1 头文件的作用

一般来说我们C语言程序需要用到的很多的.c 文件,当某一些公共的资源需要在各个源文件中使用的时候,就可以把它写在头文件中,被其它的.c文件包含,可以避免编写同样的代码。
头文件内部放:

  1. 普通函数声明
  2. 宏定义
  3. 结构体、共用体模板定义 (声明)
  4. 枚举常量列表
  5. static 函数和 inline内函数定义
  6. 其他头文件
    在这里插入图片描述

4.2 头文件的格式

/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __LED_H
#define __LED_H
/* Includes ------------------------------------------------------------------*/
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
/* Exported macro ------------------------------------------------------------*/
/* Exported functions ------------------------------------------------------- */
#endif /* __LED_H */

由于项目中各个文件比较多,建议使用不同的文件夹来存放不同类型的文件:
bin : 二进制的可执行文件
inc : 用户自己写的头文件
src : 源文件
在这里插入图片描述

如何编译:

$ gcc src/*.c -o bin/Tiezhu  -I./inc

gcc          编译器
src/*.c      需要编译的源文件路径 + 文件名
-o           指定声明文件的名字  后边必须接 目标文件的路径+名字
bin/Tiezhu   目标文件的路径 + 名字
-I./inc       -I 指定头文件路径  后面必须接头文件所在的路径
  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值