12--宏

编译 编译 器: GCC

 

1 预处理

GCC 在第一个阶段会调用预处理器 cpp 来对 C 源程序进行预处理,所谓的预处理就是解释源程序当中的所有的预处理指令,那些诸如#include、#define、#if 等以井号’#’开头的语句就是预处理指令,预处理指令实际上并不是 C 语言本身的组成部分,而是为了更好地组织程序所使用的一些“预先处理的”工作,这些工作用一种叫做与处理指令的语句来描述,然后用预处理器来解释,这些工作包括我们熟悉的诸如文件包含、宏定义、条件编译等等。

gcc 02\ 联合体的实际例子.c -o Even.i -E

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

2 编译

经过预处理之后生成的.i 文件依然是一个文本文件,不能被处理器直接解释,我们需要进一步的翻译。接下来的编译阶段是四个阶段中最为复杂的阶段,它包括词法和语法的分析(检查代码是否出现语法错误),最终生成对应硬件平台的汇编语言(不同的处理器有不同的汇编格式),具体生成什么平台,的汇编文件取决于所采用的编译器,如果用的是 GCC,那么将会生成 x86 格式的汇编文件,如果用的是针对 ARM 平台的交叉编译器,那么将会生成 ARM 格式的汇编文件。

gcc 02\ 联合体的实际例子.c -o Even.s -S

加上一个编译选项 -S 就可以使得 gcc 在进行完第一和第二阶段之后停下来,生成一个默认后缀名为.s 的文本文件。

3 汇编

接下来的步骤相对而言比较简单,编译器 gcc 将会调用汇编器 as 将汇编源程序翻译成为可重定位文件(二进制的可‘执行文件’)。汇编指令跟处理器直接运行的二进制指令流之间基本是一一对应的关系,该阶段只需要将.s 文件里面的汇编翻译成指令即可。

只要在编译的时候加上一个编译选项-c,则会生成一个扩展名为.o 的文件,这个文件是一个 ELF 格式的可重定位(relocatable)文件。

所谓的可重定位,指的是该文件虽然已经包含可以让处理器直接运行的指令流,但是程序中的所有的全局符号(全局变量或函数的入口地址等..)尚未定位(未确定其地址),所谓的全局符号,就是指函数和全局变量,函数和全局变量默认情况下是可以被外部文件引用的,由于定义和调用可以出现在不同的文件当中,因此他们在编译的过程中需要确定其入口地址,比如 a.c 文件里面定义了一个函数 func( ),b.c 文件里面调用了该函数,那么在完成第三阶段汇编之后,b.o 文件里面的函数 func( )的地址将是 0,显然这是不能运行的,必须要找到 a.c 文件里面函数 func( )的确切的入口地址,然后将 b.c 中的“全局符号”func重新定位为这个地址,程序才能正确运行。因此,接下来需要进行第四个阶段:链接。

4 链接

如前面所述,经过汇编之后的可重定位文件不能直接运行,因为还有两个很重要的工作没完成,首先是重定位(确定全局符号的确切地址),其次是合并相同权限的段(每一个可冲定位的文件中都有自己的数据段、代码段等...)。关于重定位的问题,上面已经给出了简单的描述。更一般的地,我们编译一个程序通常都需要链接系统的标准 C 库、gcc 内置库等基本库文件。因为 Linux 下任何一个程序编译都需要用到这些基本库的全局符号。

vincent@ubuntu:~$ gcc hello.o -o hello -lc -lgcc

标准 C 库和 gcc 内置库是如此的基本,因此-lc 和-lgcc 是默认的,可以省略。

预处理 

在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:

  1. 头文件:#include
  2. 定义宏:#define
  3. 取消宏:#undef
  4. 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
  5. 显示错误:#error
  6. 修改当前文件名和行号:#line
  7. 向编译器传送特定指令:#progma
  • 基本语法
    • 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠 \ 连接成一个逻辑行
    • 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
    • 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
      gcc example.c -o example.i -E

宏的概念 

宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。

语法:

#define 宏   对应的值(表达式)
#define PI   3.14

此处,PI 就是宏(宏一般习惯用全大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:

#define     PI    3.141592 
int main(int argc, char const *argv[])
{

    PI

    PI


    printf("PI:%f" , PI ) PI; PI

    PI

    
    
    return 0;
}
  • 宏的作用:
    • 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
    • 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
    • 提高程序的运行效率:程序的执行不再需要函数切换开销(宏函数),而是就地展开。

实际例子:

#define ARR_LEN 10
#define BUF_LEN 32

int main(int argc, char const *argv[])
{

    int arr [ ARR_LEN ] = {0} ;

    for (int i = 0; i < ARR_LEN ; i++)
    {
        

    }

    char * ptr = malloc(BUF_LEN);

    memcpy(ptr , msg , BUF_LEN );
    bzero(ptr , BUF_LEN );
    strcpy(ptr , msg , BUF_LEN );


    return 0;
}

无参宏

无参宏意味着使用宏的时候,无需指定任何参数,比如:

#define PI          3.14
#define SCREEN_SIZE 800*480*4 
int main()
{
    // 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
    printf("圆周率: %f\n", PI); 
    mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}

注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:

// 自定义宏:
#define PI          3.14
#define SCREEN_SIZE 800*480*4 

// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ    0x1    /* Page can be read.  */
#define PROT_WRITE    0x2    /* Page can be written.  */
#define MAP_SHARED    0x01    /* Share changes.  */

 宏的最基本特征是进行直接文本替换。

带参宏

带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如:

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

 以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y));
    printf("最小值:%d\n", MIN(x, y));
    // 以上代码等价于:
    // printf("最大值:%d\n", x>y ? x : y);
    // printf("最小值:%d\n", x<y ? x : y);
}
  • 带参宏的特点:
    1. 直接文本替换,不做任何语法判断,更不做任何中间运算。
    2. 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
    3. 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。

带参宏的副作用

由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:

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

int main()
{
    int x = 100, y = 200;
    printf("最大值:%d\n", MAX(x, y==200?888:999));

}

直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:

printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);

可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:

  • 将宏定义中所有能用括号括起来的部分,都括起来(单独强调每一个变量的整体性避免替换后出现优先级的问题),比如:
    #define MAX(a, b) ((a)>(b) ? (a) : (b))

 

  • 24
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值