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文件包含,可以避免编写同样的代码。
头文件内部放:
- 普通函数声明
- 宏定义
- 结构体、共用体模板定义 (声明)
- 枚举常量列表
- static 函数和 inline内函数定义
- 其他头文件
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 指定头文件路径 后面必须接头文件所在的路径