• 程序环境和预处理


    在今天的文章中,我将要讲解程序环境和预处理。





    1.程序的翻译环境和执行环境

    ANSI C的任何一种实现中,存在着两种不同的环境。

    第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

    第二种就是执行环境,它用于实际执行代码

    在翻译环境中,一共有三个步骤,编译、汇编、链接,而编译又分为预编译(预处理)、编译两个过程。所以在翻译环境中,可以看成一共有预编译(预处理)、编译、汇编、链接的四个步骤。

    如test.c文件要输出结果,那么它要经历以下一些步骤
    请添加图片描述
    翻译是在编译器下实行,链接是链接器下实行。



    2.详解编译+链接

    2.1 翻译环境

    请添加图片描述

    1.组成一个程序的每个源文件都会通过编译过程分别转换成目标代码(object code)。

    2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

    3.链接器同时也会引入标准(函数库中任何被该程序所用到的函数),而且它可以搜索程序员个人的程序库,将需要的函数也链接到程序中。

    目标文件即为项目文件夹里的后缀为.c的文件夹(在运行成功后生成)

    2.2 翻译环境下的操作

    预编译 —— 编译 —— 汇编 —— 链接

    预编译中
    1.如果包含头文件,那么头文件里面的代码会被拷贝进来
    2.注释会被删除
    3.#define符号的替换,即#define定义的变量替换为值

    编译中
    把C语言代码转换成了汇编代码(过程包括:语法分析、词法分析、语义分析、符号汇总)

    汇编中
    把汇编指令转换为二进制指令,形成符号表
    符号表的例子如下:
    请添加图片描述

    链接中
    1.合并段表(合并每个目标文件)
    2.符号表的合并和重定位
    在定义和声明时,函数都会各自生成一个符号表,在链接时,两个符号表就会合并和重定义为函数定义的符号表(因为声明生成的符号表,里面的地址是随机分配的)。

    2.3 运行环境

    1.程序必须载入内存中。在有操作系统的环境下,一般这个由操作系统来完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。(独立的环境,如:单片机)

    2.程序的执行便开始。接着调用main函数。

    3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)即函数栈帧,存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。

    4.终止程序。正常终止main函数,也有可能是意外终止。

    2.4 集成开发环境(IDE)

    像vs2010这样的编辑器就是集成开发环境,它包含了编辑器、编译器(cl.exe)、链接器(link.exe)、调试器,所以有编辑、编译、链接、调试等功能。



    3.预处理详解

    3.1 预定义符号

    _ _ FILE _ _ //进行编译的源文件
    _ _ LINE _ _ //文件当前的行号
    _ _ DATE _ _ //文件被编译的日期
    _ _ TIME _ _ //文件被编译的时间
    _ _ STDC _ _ //如果编辑器遵循ANSI C,其值为1,否则为定义

    下面,我来举一个例子。

    #include
    int main()
    {
    	printf("%s\n",__FILE__);
    	printf("%d\n",__LINE__);
    	printf("%s\n",__DATE__);
    	printf("%s\n",__TIME__);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,分别打印出来源文件的路径和名称、文件当前行号、文件编译的日期、文件编译的时间。

    在vs2010中,STDC是未定义的,所以我在Linux系统下运行。

    首先,打开test.c文件
    请添加图片描述
    输入测试代码
    请添加图片描述
    编译和运行
    请添加图片描述

    观察可以发现,运行结果是1,所以gcc编辑器遵循ANSI C。

    3.2 #define

    3.2.1 #define定义标识符

    语法:#define name stuff

    下面我来举一个例子

    #define Max 100
    #define STR "GD_small_bit"
    #include
    int main()
    {
    	printf("%d\n",Max);
    	printf("%s\n",STR);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果如下:
    请添加图片描述

    3.2.2 #define定义标识符总结
    #define MAX 100        
    #define reg register             //为register这个关键字,创建一个简短的名字
    #define do_forever for(;;)       //用更形象的符号来替换一种实现
    #define CASE break;case         //在写case语句的时候自动把break写上
    //如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
    #define DEBUG_PRINT printf("file: %s\t line:%d\t\
    							data: %s\t time:%s\n",\
    							__FILE__,__LINE__,\
    							__DATE__,__TIME__)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对于下面这一串代码,我需要画图讲解一下。

    #define    CASE    break;case  
    
    • 1

    请添加图片描述

    3.2.3 #define定义标识符的时候,要不要在最后加上分号?

    如果加上分号的话,会发生什么呢,我举个例子来讲解一下。

    #define Max 100;
    int main()
    {
    	int m = 10;      //错误示范
    	if(m > 5)
    		m = Max;
    	else
    		m = -1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在上面的代码中,我已经在#define语句定义Max标识符后面加上了分号,我运行一下该串代码看看
    请添加图片描述
    毫无疑问,程序报错,这是为什么呢?我将#define定义的标识符替换为值就知道了。

    #define Max 100;
    int main()
    {
    	int m = 10;        //错误示范
    	if(m > 5)
    		m = 100;;
    	else
    		m = -1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将#define定义的标识符替换为值以后,就可以发现其中的一条语句出现了两个分号。

    综上,在#define定义标识符时,在语句的最后面是不能加上分号的,因为在#define定义的值去替换标识符时,分号也会被替换进去,导致了程序错误。

    3.2.4 #define定义宏

    #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或者定义宏(define macro)。

    下面是宏的申明方式:
    #define name(parament_list)stuff
    其中的parament_list是一个由逗号隔开的符号表(参数列表),它们可能出现在stuff中。

    下面,我举一个例子。

    #include
    #define Max(x,y) (x > y? x : y)
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int c = Max(a,b);
    	printf("%d\n",c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在上面的代码中,int c = Max(a,b);语句将会被替换为int c = (a > b? a : b);

    注意:
    1.参数列表的左括号必须与name紧邻。
    2.如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

    如上面的Max(x,y)中的Max名字和(x,y)参数列表紧邻在一起。

    在#define定义宏时,一定要带足括号,不然可能会带来错误。

    第一个例子

    如下面的代码中,我定义一个宏,可以计算数的平方

    #define  SQUARE(x)  x*x
    
    • 1

    下面,我来检测这个宏的功能。

    #include
    #define  SQUARE(x)  x*x
    int main()
    {
    	printf("%d\n",SQUARE(5));    //计算5的平方
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,该程序成功的计算出了5的平方。

    接下来,我来计算(5+1)的平方

    #include
    #define  SQUARE(x)  x*x
    int main()
    {
    	printf("%d\n",SQUARE(5+1));       //计算(5+1)的平方
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    按道理来说,计算(5+1)的平方,结果应该是36。接下来,我运行一下。

    运行结果如下:
    请添加图片描述
    运行结果却是11,这是怎么回事呢?

    我将宏定义的标识符替换为值,可以得到这条式子5+1*5+1,结果就是11。

    如果我将宏定义改为#define SQUARE(x) (x)*(x),在stuff的每个x加上括号,那么在宏定义的标识符替代为值时,可以得到这条式子(5+1)*(5+1),那么结果就是36。

    #include
    #define  SQUARE(x)  (x)*(x)
    int main()
    {
    	printf("%d\n",SQUARE(5+1));       //计算(5+1)的平方
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,我们的猜测是正确的。

    第二个例子
    我定义一个宏,可以计算一个数的两倍

    #define DOUBLE(x) (x)+(x)
    
    • 1

    下面,我来利用该宏来计算三个数

    #include
    #define DOUBLE(x) (x)+(x)
    int main()
    {
    	printf("%d\n",DOUBLE(6));        //计算6+6的结果
    	printf("%d\n",DOUBLE(6+1));      //计算7+7的结果
    	printf("%d\n",10 * DOUBLE(6));   //计算10*(6+6)
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面的代码中,三个数的计算结果应该是12、14、120。接下来,我运行一下。

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,第一个结果和第二个结果都是正确的,而第三个结果却出现了错误,这又是怎么回事呢?

    在第三个数字中,我依然把宏定义的标识符替代为值,那么,可以得到这条式子10*(6)+(6),结果就是66。

    如果我对宏定义修改为#define DOUBLE(x) ((x) + (x)),即在整个stuff外面加上括号。那么,在宏定义的标识符替代为值的时候,可以得到这条式子10 * ( (6) +(6) ),那么,结果就是120。

    #include
    #define DOUBLE(x) ((x)+(x))
    int main()
    {
    	printf("%d\n",DOUBLE(6));        //计算6+6的结果
    	printf("%d\n",DOUBLE(6+1));      //计算7+7的结果
    	printf("%d\n",10 * DOUBLE(6));   //计算10*(6+6)
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,我们的猜测是正确的。

    3.2.5 #define替换规则

    在程序中扩展#define定义符号和宏时,需要涉及几个步骤

    1.在调用宏时,首先会对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
    2.替换文本随后被插入程序中原本文本的位置。对于宏,参数名会被它们的值所替代。
    3.最后,再次对结果文本进行扫描,看看是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
    注意:
    1.宏参数和#define定义中可以出现其他#define定义的符号。但是,对于宏,不能出现递归。
    2.当预处理器搜索#define定义的符号的时候,字符串常量的内容不被搜索。

    例如:

    #include
    #define M 10
    int main()
    {
    	printf("hello M\n");      //字符串常量中的M不会被替换
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面的代码中,字符串常量里存在着#define定义的符号,但是不会被搜索。

    运行结果如下:
    请添加图片描述
    下面我来写一个代码,模拟#define替换流程

    #include
    #define M 10
    #define Max(x,y) ((x)>(y)?(x):(y))
    int main()
    {
    	int m = Max(2+3,M);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意,下面的过程是在预编译时进行的。

    第一步,被#define修饰的变量全部替换为对应的值
    请添加图片描述
    如上图,int m = Max(2+3,M)里面的M被替换为了10。

    第二步,对于宏,替换参数名
    请添加图片描述
    第三步,替换文本插入程序中原来文本的位置
    请添加图片描述
    第四步,再次进行检查。

    3.2.6 #和##
    #号的作用

    如何把参数插入字符串中?
    只有当字符串作为宏参数的时候才可以把参数放在两个字符串的中间。

    在C语言中,两个字符串括起来会合成一个。如下:

    #include
    int main()
    {
    	printf("hello world\n");
    	printf("hello""world");      //两个字符串括起来合成一个
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行结果如下:
    请添加图片描述
    C语言,两个字符串括起来会合成一个,这个功能可以作为实现当字符串为宏参数时,将参数放入两个字符串中间,与字符串合成一个的基础。

    下面,我来举一个宏参数插入字符串的例子。

    未使用宏定义

    #include
    int main()
    {
    	int a = 10;
    	int b = 20;
    	float c = 30;
    	printf("the value of a is %d\n",a);
    	printf("the value of b is %d\n",b);
    	printf("the value of c is %lf\n",c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果如下:
    请添加图片描述
    使用宏定义

    对于下面的宏定义中,一共有两个宏参数,val和format,因为format是接受类似"%d"、"%lf"等格式说明符,本身就是字符串,所以可以直接放在两个字符串的之间,会被直接合成一个字符串。

    宏参数val本身接受的是变量,不是字符串,那么放在两个字符串的中间,无法合成一个字符串,所以在下面的代码中,直接放在了字符串符号的里面(注意是第一个val)。

    #include
    #define PRINT(val,format) printf("the value of val is " format "\n",val)
    int main()
    {
    	int a = 10;
    	int b = 20;
    	float c = 30;
    	PRINT(a,"%d");
    	PRINT(b,"%d");
    	PRINT(c,"%lf");
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    运行结果如下:
    请添加图片描述
    如果在下面中,我依然要使宏参数val放在两个字符串的中间,并且保证宏参数左右字符串也能合成一个,我应该怎么做呢?那么,就要引入#号了。

    具体代码实现如下:

    #include
    #define PRINT(val,format) printf("the value of " #val " is " format "\n",val)
    int main()
    {
    	int a = 10;
    	int b = 20;
    	float c = 30;
    	PRINT(a,"%d");
    	PRINT(b,"%d");
    	PRINT(c,"%lf");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如上面的代码中,我将原本是一整个的字符串(即"the value of val is" )拆成了字符串("the value of “)和(” is "),中间还夹个val,前面已经说过val不是字符串,所以在val的前面加个#号。

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,程序的printf函数打印出来了相应的结果,证明在#号的作用下,val与左右两边的字符串合成了一个字符串。

    而结果又出现了不同,在val放在字符串符号里面时,运行结果中,val分别没有被a、b、c替代。

    val单独放在字符串外面,能够被a、b、c替代,而在#号的作用下,#val变成了"val",所以结果中有出现a、b、c。

    总结:使用#号,可以使一个宏参数变成对应的字符串。

    ##的作用

    ##可以把位于它两边的符号合成一个符号。
    它允许宏定义从分离的文本判断创建标识符。

    下面,我来举一个例子

    #include
    #define CAT(A,B) A##B
    int main()
    {
    	int class107 = 100;
    	printf("%d\n",CAT(class,107));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果如下:
    请添加图片描述

    在宏定义#define CAT(A,B) A##B,##号的作用是将A和B标识符合成一个,而在上面的代码中,传过来class和107,合成了class107,为变量名,值是100,所以运行结果是100。

    注意:这样的连接必须产生一个合法的标识符,否则其结果是未定义的。

    3.2.7 带副作用的宏参数

    当宏参数在宏定义时出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
    如x+1; //不带副作用
    如x++; //带有副作用
    如getchar(),在缓冲区每读取一个字符,缓冲区就少一个字符,带有副作用
    如fgetc(),从文件读取一个字符,文件指针就向后走一步,带有副作用

    猜测下面代码的运行结果

    #include
    #define Max(x,y) ((x)>(y)?(x):(y))
    int main()
    {
    	int a = 3;
    	int b = 4;
    	int m = Max(++a,++b);
    	printf("m = %d  a = %d  b = %d\n",m,a,b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    因为a是3,b是4,然后是前置加加,各种自增变成了a是4,b是5,b大于a,所以m等于b,m是5,所以结果是5、4、5。

    这样看来结果好像是正确的,但是真的是这样吗?让我们运行一下该程序。

    运行结果如下:
    请添加图片描述
    结果好像跟我们想的不一样,这是怎么回事?不急,让我来解释解释。

    将宏参数替换掉,并且将替换文本插入程序原来文本的位置,得到结果如下
    int m = ((++a)>(++b)?(++a):(++b))
    ++a的结果是4
    ++b的结果是5
    4小于5,即(++a) < (++b),所以后面的++a不执行,并且因为a小于b,所以m = ++b。
    4小于5,即(++a) < (++b),执行后面的++b,b又再自增一次
    b的值变成了6,所以m的值也变成了6
    综上,m = 6,a = 4,b = 6

    在上面的代码中,就是宏参数带有副作用而导致的不好预测的结果。

    3.2.8 宏和函数对比

    宏通常被应用于执行简单的运算

    比如在两个数中求出较大的一个
    #define Max(a,b) ((a)>(b)?(a):(b))

    那为什么不用函数来完成这个任务呢?

    原因:1.用于调用函数和从函数返回的代码比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
    2.更为重要的是函数的参数必须声明为特点的类型,所以函数只能在类型合适的表达式上使用,反之这个宏可以适用于整形、长整形、浮点形等可以用于大于号来比较的类型。
    宏是类型无关的。

    我来调试下面该串代码,转到反汇编,观察函数的调用和宏的使用。

    #include
    #define Max(x,y) ((x)>(y)?(x):(y))
    int MAX(int x,int y)
    {
    	return ((x)>(y)?(x):(y));
    }
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int m1 = Max(a,b);
    	int m2 = MAX(a,b);
    	printf("m1 = %d\n",m1);
    	printf("m2 = %d\n",m2);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    宏的汇编代码
    请添加图片描述
    函数的汇编代码
    请添加图片描述
    观察可以发现,函数的汇编代码比宏的汇编代码多太多了,证明了宏的规模和速度与函数相比,确实更胜一筹。

    宏的缺点,宏和函数相比,也存在劣势的地方。

    1.每次使用的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
    2.宏是没法调试的。
    3.宏由于与类型无关,也就不够严谨。
    4.宏可能会带来运算符优先级的问题,导致程序出错。

    解释:

    1.宏在使用的过程中,原来文本会被替换文本更换,如果反复使用,会增加代码长度。(如一个宏有50行,替换三次就会增加150行代码,函数只是调用,而不是替换到文本的位置)
    2.如前面的Max宏在未编译时是没被替换的,而MAX宏在运行前的编译就被替换,所以调试的代码和看到的代码可能不相同,难以进行调试。
    3.宏与类型无关,不够严谨
    4.如不带足括号带来的问题

    宏有时候可以做到函数做不到的事情,比如,宏的参数可以出现类型,但是函数做不到。

    #include
    #define SIZE(num,format) ((num)*(sizeof(format)))
    int main()
    {
    	int arr[] = {1,2,3,4,5,6,7,8,9,10};
    	printf("%d\n",SIZE(10,int));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如上面的代码中,宏参数format就出现了类型。

    函数只在传参的时候,计算一次,而宏定义不是。如:有函数test和宏TEST,往test函数传参test(3+5),那么经过计算后,直接传参test(8),而给宏传参TEST(3+5),那么3+5的值在传参的过程中不会被计算。

    总结:

    属性#define定义宏函数
    代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
    执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
    操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
    带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
    参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。
    调试宏是不方便调试的函数是可以逐语句调试的
    递归宏是不能递归的函数是可以递归的
    3.2.9 命名约定

    一般来讲,函数与宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的一个习惯是;把宏名全部大写,函数名不要全部大写。
    但是有些例外:
    offsetof——宏
    getchar()——宏(有些编辑器是宏实现)



    3.3 #undef

    这条指令用于移除一个宏定义。

    #undef NAME
    //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
    
    • 1
    • 2

    现在,我来举一个例子

    #include
    #define M 100
    int main()
    {
    	printf("%d\n", M);     
    #undef M
    	printf("%d\n", M);     //错误代码
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    报错如下:
    请添加图片描述
    观察报错图片可以得知,在未使用#undef前,M标识符可以被替代,当使用#undef时,系统就找不到M标识符代表的值了,所以,#undef可以移除一个宏定义。



    3.4 命令行定义(Linux环境下演示)

    许多C的编辑器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如,当我们根据同一个源文件要编译程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组够大些)

    下面,我在Linux系统下体验一下该功能

    我先在Linux系统下输入代码
    请添加图片描述
    注意:这里的sz是没有定义和赋初值的

    如下图,采用命令行gcc test.c -D sz=10,将sz赋值为10,运行结果就是打印0~9的数字
    请添加图片描述
    如下图,采用命令行gcc test.c -D sz=100,将sz赋值为100,运行结果就是打印0~99的数字
    请添加图片描述



    3.5 条件编译

    3.5.1 条件编译用于调试代码

    在编译一个程序的时候,如果我们要将一条语句(一组语句),编译或者放弃是方便的,因为我们有条件编译指令。

    比如说:
    调试性的代码(即调试寻找BUG时添加上去的),删除可惜,保留又碍事,所以我们可以选择性的编译。

    下面的代码中,for循环往数组赋值,用一个打印函数检验是否赋值成功。

    #include
    int main()
    {
    	int i = 0;
    	int arr[10] = {0};
    	for(i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    		printf("%d\n",arr[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    那么,对于该条printf函数,我采用条件编译,来实现代码的选择编译。

    #include
    int main()
    {
    	int i = 0;
    	int arr[10] = {0};
    	for(i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    #ifdef __DEBUG__
    		printf("%d\n",arr[i]);
    #endif
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如上面的代码中,我使用了条件编译,大致意思如下,如果宏定义了__DEBUG__的标志符后,就会执行条件编译语句中间的语句。

    在上面的代码中,我并没有宏定义__DEBUG__的标识符,所以我运行一下,看看打印函数是否生效。

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,在未定义__DEBUG__的标识符下,的确没有运行条件编译中间的语句。

    那么,现在我来宏定义一下__DEBUG__语句,观察是否会运行条件编译中间的语句。

    #include
    #define __DEBUG__
    int main()
    {
    	int i = 0;
    	int arr[10] = {0};
    	for(i = 0; i < 10; i++)
    	{
    		arr[i] = i;
    #ifdef __DEBUG__
    		printf("%d\n",arr[i]);
    #endif
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    运行结果如下:
    请添加图片描述
    由运行结果可以得知,在宏定义__DEBUG__的标识符下,会运行条件编译中间的语句。

    以上,就是条件编译的使用内容了。

    3.5.2 常见的条件编译指令
    3.5.2.1 判断是否成立

    #if 常量表达式(常量表达式由预处理器求值)
    //……
    #endif

    如下面的例子:

    #include
    int main()
    {
    #if 2>3
    	printf("hehe\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    因为#if后面的常量表达式不成立,所以条件编译里面的语句不会实行,运行后没有结果。

    3.5.2.2 多个分支的条件编译

    #if 常量表达式(常量表达式由预处理器求值)
    //……
    elif 常量表达式(常量表达式由预处理器求值)
    //……
    #else
    //……
    #endif

    下面,我来举一个例子

    #include
    int main()
    {
    #if 2>3
    	printf("hehe\n");
    #elif 3>4
    	printf("haha\n");
    #else
    	printf("heihei\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果为heihei。

    细心的小伙伴可以发现,条件编译的语句与if、else if、else语句很相似,那么条件编译语句是否也跟if-else语句一样,只进入一条语句呢。

    下面,我来验证一下

    #include
    int main()
    {
    #if 1>0                 //条件成立
    	printf("hehe\n");
    #elif 2<3               //条件成立
    	printf("haha\n");
    #else
    	printf("heihei\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如上面的代码中,有两条判断语句成立,那么是否会执行两条语句呢?

    让我们来运行一下。
    请添加图片描述
    由运行结果可以得知,如果条件编译语句后面的常量表达式多条成立,依然只进入第一条常量表达式成立的条件编译语句。

    3.5.2.3 判断是否被定义

    #if defined(变量名)
    //……
    #endif
    #if !defined(变量名)
    //……
    #endif

    下面,我来举一个例子

    #include
    #define MAX 0
    int main()
    {
    #if defined(MAX)
    	printf("hehe\n");
    #endif
    #if !defined(MAX)
    	printf("haha\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    因为在代码的前面已经宏定义了MAX的标识符,所以运行结果为hehe。

    注意,在这种情况下,判断的是MAX标识符有无被宏定义,不管该标识符定义的值在程序中是真还是假。如:上面的MAX标识符的值为0,为假,但是它有宏定义MAX标识符,所以#if defined(MAX)判断成立。

    前面的代码等价于

    #include
    int main()
    {
    #ifdef MAX
    	printf("hehe\n");
    #endif
    #ifndef MAX
    	printf("haha\n");
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    条件编译与if-else的区别,if-else在代码编译的时候还留着代码,只不过根据实际情况进行判断,但条件编译在预处理阶段就把不要的代码全部删除掉了,只留下想要的代码。

    3.5.2.4 嵌套指令
    #include
    int main()
    {
    #if defined(OS_UNIX)
    	#if defined(MAX)
    		printf("hehe\n");
    	#else
    		printf("haha\n");
        #endif
    #elif !defined(OS_UNIX)
    	#if defined(MIN)
    		printf("heihei");
    	#else
    		printf("enen");
        #endif
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    类比if—else if—else语句的嵌套去理解。

    运行结果是enen。



    3.6 文件包含

    我们已经知道,#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样。这种替换方式很简单:预处理器先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次,那就实际被编译10次。

    3.6.1 头文件被包含的方式
    3.6.1.1 本地文件包含

    语法:

    #include "filename"
    
    • 1

    查找策略:先在源文件所在目录下查找,如果该文件未找到,编辑器就像查找库函数头文件一样在标准位置查找头文件。即先去项目工程的文件夹寻找,如果找不到,就会去编译器提供的库函数的头文件寻找。如果找不到就提示编译错误。

    3.6.1.2 库文件包含

    语法:

    #include
    
    • 1

    查找库文件直接在标准路径下寻找,如果找不到就提示编译错误。

    库文件也可以使用" "的形式包含,但是这样做查找的效率就低些,因为库文件去源文件所在的文件夹寻找,无疑是找不到的,并且,这样操作下,就不容易区分是库文件还是本地文件了。

    3.6.2 嵌套文件包含

    请添加图片描述
    common.h和common.c是公共模块。
    test1.c和test1.h使用了公共模块。
    test2.c和test2.h使用了公共模块。
    test.h和test.c使用了test1模块和test2模块
    这样最终程序就出现两份common.h的内容,这样就造成了文件内容的重复。

    解决方法:条件编译

    方法一:
    每个头文件格式如下
    #ifndef TEST_H
    #define TEST_H
    //头文件的内容
    #endif

    如下,我在头文件里面声明求最大值函数Max

    #ifndef __TEST_H__
    #define __TEST_H__
    int MAX(int x,int y);  //头文件代码
    #endif
    
    • 1
    • 2
    • 3
    • 4

    方法二:
    在头文件的最上方加上#pragma once

    如:

    #pragma once
    int MAX(int x,int y);    //头文件代码
    
    • 1
    • 2


    3.7 面试题

    1.头文件中的 ifndef/define/endif 是干什么用的?

    避免头文件被重复引用的操作。

    2.#define和#include “filename.h” 有什么区别?

    查找的策略不同,一般对库函数的头文件用<>,对自己定义的头文件用"",自己定义的头文件是先在自己工程目录底下查找,如果没有,再去库目录底下查找,而对于库函数的头文件直接去库目录底下查找



    3.8 offsetof的宏实现

    下面,我宏实现offsetof,计算结构体中某变量相对于首地址的偏移。

    #include
    #define OFFSETOF(s_type,m_name) (int)&(((s_type*)0)->m_name)   //将0地址处转为s_type类型,找到该类型下的m_name成员名,再取出地址,转为int类型
    struct s
    {
    	char a;
    	char b;
    	int c;
    };
    int main()
    {
    	printf("%d\n",OFFSETOF(struct s,a));
    	printf("%d\n",OFFSETOF(struct s,b));
    	printf("%d\n",OFFSETOF(struct s,c));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果如下:
    请添加图片描述



    3.9 宏实现交换整数的二进制位的奇数位和偶数位

    写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。

    #include
    #define SWAP_BIT(n) n = ((n & 0xaaaaaaaa)>>1) + ((n & 0x55555555)<<1)
    int main()
    {
    	int n = 10;
    	SWAP_BIT(n);
    	printf("%d\n",n);
    	SWAP_BIT(n);
    	printf("%d\n",n);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行结果如下:
    请添加图片描述
    第一次交换得到5的结果,第二次交换又得到10。

    0xaaaaaaaa
    10101010 10101010 10101010 10101010

    0x55555555
    01010101 01010101 01010101 01010101

    利用这两个数字特点,再进行按位与&,求出奇数位和偶数位的数字,偶数位右移一位,奇数位左移一位,实行该宏。

    今天的讲解到达这里,关注点一点,下期更精彩。

  • 相关阅读:
    部署LVS+Keepalived高可用群集(抢占模式,非抢占模式,延迟模式)
    安卓主动发数据到uniapp界面
    7天酒店斩获五洲钻石奖“年度投资价值酒店连锁品牌” 打造酒店投资极致性价比
    TDengine3.0 新架构设计思路
    sql处理重复的列,更好理清分组和分区
    代码质量与安全 | “吃狗粮”能够影响到代码质量?来了解一下!
    Nginx学习笔记05——Nginx虚拟主机配置
    Windows电脑如何手动设置IP地址和DNS?
    Python进阶:反射
    asp.net教务管理信息系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio计算机毕业设计
  • 原文地址:https://blog.csdn.net/GD_small_bit/article/details/127974994