[深入浅出C语言]浅析编译与预处理(篇二)

74 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情

前言

        本文就来分享一波作者对编译和预处理的学习心得与见解。本篇属于第二篇,主要介绍预处理的一些内容,后续还有,可以期待一下。

        笔者水平有限,难免存在纰漏,欢迎指正交流。

 预处理

预定义符号

        先来看看一些符号

__FILE__  //进行编译的源文件

__LINE__  //文件当前的行号

__DATE__  //文件被编译的日期

__TIME__  //文件被编译的时间

__STDC__  //如果编译器遵循ANSI C,其值为1,否则未定义

        这些预定义符号都是语言内置的。

        例如:

printf("file:%s line:%d \ndate:%s time:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);

image.png

         那这么些符号有啥用呢?可以用来记录日志。

逻辑行

        在预处理前,编译器定位到每一个续行符即反斜杠 \ ,删除它并且将几个物理行合成一个逻辑行。如:

printf("That's \ 
right.");            //两个物理行

printf("That's right");  //转换成一个逻辑行

        编译器用一个空格字符代替每一条注释,所以实际上注释压根就没进入到编译过程,更不用说最后的可执行程序了,因为注释是给人看的,机器并不会去看。

        预处理器指令从#开始,执行长度为一个逻辑行,所以预处理指令若要使用多个物理行的话需要用续行符合成一个逻辑行。

#define定义宏

        每行#define由三部分组成,用宏代表值的称为类对象宏,代表函数的称为类函数宏

不带参数的宏

        对于类对象宏,可以代表常量,也可以表示任何字符串,甚至整个C表达式,并且不带参数。

image.png

        在预处理阶段,编译器找到宏并替换,称为宏展开

         需要注意的是,预处理器不做表达式的计算求值,仅仅进行文本替换操作,将宏用替换体代替,而表达式的计算则由编译器在编译过程进行。

        比如下面代码,P在预处理阶段被替换成 2*2而不是4,等到了编译过程P才为4。

#define P TWO*TWO
#define TWO 2
int main()
{
    printf("%d",P);
    return 0;
}

        #define定义标识符常量其实就是定义类对象宏的一类。 

提问:

        在define定义标识符的时候,要不要在最后加上;? 

比如:

#define MAX 1000;
#define MAX 1000

        建议不要加上; ,直接是什么就替换什么,加上的话容易导致问题。
比如下面的场景:

if(condition)
    max = MAX;
else
    max = 0;

        这里如果加上;就会出现语法错误。

带参数的宏

         对于类函数宏,其形式与函数类似,在使用时宏参数进入替换体中执行对应语句,其实就是带参数的宏。

image.png

        下面是类函数宏的声明方式:

#define name( parament-list ) stuff

        其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:

        参数列表的左括号必须与name紧邻。

        如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

比如:

#define SQUARE(x) x * x
#define SQUARE (x) x * x

        第一个式子就是正确的类函数宏,第二个式子就是类对象宏了,它没有宏参数,单纯的用(x) x * x来替换SQUARE。

        #define SQUARE(x) x * x 这个宏接收一个参数 x ,如果带入5的话,预处理器就会用5 * 5来替换宏SQUARE(x)。

 但是这个宏存在一个问题。

        看看下面的代码:

#define SQUARE(X) X*X

int main()
{
    int x = 4;
    printf("%d",SQUARE(2));
    printf("%d",SQUARE(2+1));
    printf("%d",100 / SQUARE(2));
    printf("%d",SQUARE(++x));
    return 0;
}

         结果会是什么?

         预处理中替换宏是完全替换,SQUARE(2+1)相当于2+1*2+1,预处理中不对表达式求值,那我想得到3*3怎么办?括号很重要! 只要把宏定义处改成SQUARE(X) (X)*(X)就行了。

        100 / SQUARE(2)相当于100 / 2 * 2,还是括号的问题,把宏定义处改为SQUARE(X) ((X)*(X))即可得到25。而对于最后一个,SQUARE(++x)相当于++x*++x其实挺奇怪的,因为C标准中对此表达式的求值称为未定义行为,在不同编译器上的结果可能不同。

        这里还有一个宏定义:

#define DOUBLE(x) (x) + (x)

        定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

        这将打印什么值呢?

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

        看上去,好像打印100,但事实上打印的是55.
我们发现替换之后变成:

printf ("%d\n",10 * (5) + (5));

        这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了:

#define DOUBLE( x) ( ( x ) + ( x ) )

        提示:

        所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

#define 替换规则

在预处理过程中宏展开时,需要涉及几个步骤:

        1. 在调用类函数宏时,首先对参数进行检查,看看是否包含任何类对象宏的符号。如果是,它们首先被替换。

        2. 替换文本随后被插入到程序中原来文本的位置。对于类函数宏,参数名被他们的值所替换。

        3. 最后,再次对文件进行扫描,看看它是否包含任何宏。如果是,就重复上述处理过程。

注意:

        1. 宏参数和类对象宏中可以出现其他类对象宏的符号。但是对于类函数宏,不能出现递归。

        2. 当预处理器搜索宏的时候,字符串常量的内容并不被搜索。

         当预处理器搜索#define定义的符号时,字符串常量的内容不被搜索,比如:

#define M 100
int main()
{
    printf("M is %d",M);
    return 0;
}

        打印出来的内容是M is 100 ,printf里面的M并未被替换。

        

        宏的替换体看作记号型字符串,每一个记号就是替换体中单独的“词”,字符型字符串中每一个字符就是其中单独的“词”。解释为字符型字符串,把空格视为替换体的一部分,解释为记号型字符串,把空格视为各记号的分隔符。

用宏参数创建字符串:关于#和##

        #可以把记号转换成字符串,如果x是一个宏参数,那么#x就是转换成字符串“x”,用于组合字符串。

#define PX(x) printf("the value of "#x"is %d",x)

int main()
{
    int a = 5;
    PX(a);//相当于printf("the value of ""a""is %d",a),再根据字符串串联功能,
          //就得到了printf("the value of a is %d",a)
}

        ##把两个记号组合成一个记号,用于把记号组合为一个新的标识符,如

#define XNAME(n) x ## n

int main()
{
    int x2 = 5;
    printf("%d",XNAME(2));//printf("%d",x2)
    return 0;
}

某些宏参数带有副作用

        当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的额外的持久性效果(一般指的是变量的值发生改变)。
比如:

x+1;//不带副作用
x++;//带有副作用

        下面代码中m的结果是什么?

#define MAX(x, y) ((x)>(y)?(x):(y))

int main()
{
    int a = 5;
    int b = 8;
    int m = MAX(a++, b++);
    printf("a = %d, b = %d, m = %d ", a, b, m);
}

        注意都是后置自增!预处理时,替换成int m = ((a++)>(b++)?(a++):(b++)); ,编译时先判断5>8为假,然后a自增为6,b自增为9,所以m = b++,先赋值后自增,则m最终结果为9,b的值为10,而a的值为6。

练习:实现offsetof宏

        写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明。实际上就是要我们实现offsetof宏。

offsetof(struct name, member name) 

        第一个参数是结构体类型名,第二个参数是结构体成员名,用于计算该成员在对应的结构体中的偏移量大小。

思路分析:

        这个宏要求的是一个结构体类型中某成员的偏移量,实际上很容易想到偏移量就等于该成员地址与结构体地址之差,但是要注意的是,这个宏的第一个参数给的是结构体类型名,也就是说给的不是变量,还没开辟内存,何来地址之差。

        其实我们可以给该结构体类型假设一个地址,是不是就可以计算地址之差了,也就是把一个非负整数值强转成对应结构体类型的指针,该数值就作为假设的地址。那这个数值能随便假设吗?最好用0,为什么?因为任何数减去0都为它本身,所以地址之差和直接取成员的地址数值上就相同了,省事儿。

#define OFFSETOF(type, memberName) (size_t)&(((type*)0)->memberName)

以上就是本文全部内容了,感谢观看,你的支持就是对我最大的鼓励~

src=http___c-ssl.duitang.com_uploads_item_201708_07_20170807082850_kGsQF.thumb.400_0.gif&refer=http___c-ssl.duitang.gif