C语言详解(6)宏定义和条件编译

宏定义和条件编译

 

 关于C语言详解系列blog的目录:http://blog.csdn.net/snake_lp/article/details/78630717

 

一、概述

宏定义是C语言的预处理功能。宏定义就是简单的替换,不作为计算,不也作为表达式。在C语言中作为预处理指令包括:宏定义、文件包含、条件编译。

条件编译其实就是将if…else…的设计思想引入到预处理功能中,给编译器使用的。条件编译时通过增加条件判断的限制,来通知编译器选择性的编译满足条件的代码段,从而减少程序对内存的消耗,同时也可以提高程序的效率。使用条件编译,可以实现:同一套代码,根据不同的编译条件进行编译,产生不同的目标代码文件,以实现大公司中平台和产品线开发的一种形式。

合理的使用宏定义和条件编译,可以大大的优化程序、提高程序的质量、增加程序的可移植性。

 

二、宏定义

普通的宏定义其实就是我们理解的宏常量。宏定义又称为宏替换,简称“宏”。其定义格式如下:

#define 标识符 字符串

其中这个标识符,就是我们的宏常量,也就字符常量,在宏定义中称之为宏名。预处理的工作就是替换,就是将宏名替换成相对于的字符串。

对于宏的关键就是替换,所以宏定义的一切都是以换为前提的,做任何的事情之前都要先替换。这个工作在预编译完成。

例如:

#define NAME“snake”
#define MAX_NUM10

在程序中就是把NAME全部替换为“snake”,把MAX_NUM替换为10.主要只是替换,不增加任何的代码。

 

对于宏定义一般需要遵循以下几点要求,以增加程序的可读性。

⑴        宏名一般使用全大写字母;

⑵        使用宏可以提高程序的通用性和可移植性;

⑶        宏定义末尾不增加分号;

⑷        宏定义的作用域是宏定义之后一直到文件结束;

⑸        所以一般全局的宏都会在文件开头定义,或者在.h文件中定义,通过文件包含就可以实现多个文件的使用;

⑹        对于函数内部的宏定义,可以使用#undef命令来限定宏定义的作用域;

⑺        宏定义允许嵌套;

⑻        字符串中不能包含宏,即使存在宏,也会将宏名单做字符串处理;

⑼        宏定义不分配内存空间,变量的定义需要分配内存空间。

 

三、宏定义函数

宏函数,其实还是宏,只是这一类宏带有参数,我们可以看做是宏函数,从本质上说,他还是宏,在预处理的时候,还是简单的替换。

宏函数的替换,除了一般的字符串替换之外,还要做参数的替换。其格式如下:

#define 标识符(参数列表) 表达式

例如:

#define ADD(a,b) a+b


在使用的过程中,其实就可以看做是一个函数的。宏定义的参数列表就可以看做是宏函数的参数。宏函数和普通的函数有以下不同:

⑴        宏函数的调用不需要要用堆空间。因为宏函数不是真正的函数,其也是直接替换,所以本质上不是函数的调用,故不需要消耗程序调用的堆空间;

⑵        函数的调用是在程序运行期间进行的,存在内存的分配。宏函数是预编译时进行的,不需要分配内存;

⑶        宏函数中的函数没有类型限定;

⑷        宏函数的本质是替换,展开会使源程序变长,函数的调用不存在此问题;

⑸        宏展开不占用运行时间,但是会消耗编译时间;

⑹        函数存在递归调用,但是宏函数不允许存在这样的做法;

⑺        宏函数在预编译的时候被处理,编译器是不知道宏函数的存在;

⑻        宏函数用“实参”完全替代“形参”,不需要进行任何的运算。

 

对宏函数的定义需要特别注意:

⑴        需要注意运算符的优先级。比如:

#define ADD(a,b) a+b

按照宏的展开,运行的结果就不是我们所期望的。所以建议在定义宏的时候,需要考虑到运算的优先级,增加()来限制优先级。

将刚刚的宏改成:

#define ADD(a,b) a+b

这样就能保证运行结果的正确性。

⑵        另外可以利用接续符“\”来实现宏函数更好的可读性

#define ADD(a,b)\
{              \
           ((a) + (b))   \
}              

这样的书写形式,会使程序员看起来更加清晰。

 

对于正确书写宏定义,需要注意以下几点:

⑴        宏名和参数的括号之间不能存在空格;

⑵        宏定义只是在编译期间做替换,不做计算,也不作为表达式;

⑶        函数的调用是程序运行期间,需要占用系统资源和内存空间;宏替换在预处理阶段,不占用系统的任何资源;

⑷        宏函数的函数,没有任何的类型限定,也不存在类型转换,所以在使用的时候需要注意输入参数的含义;

⑸        宏函数的展开会使程序变长,函数的调用不会;

⑹        宏定义的展开需要消耗编译的时间;但是函数调用会消耗运行的时间和空间。

 

四、内置宏

在C中存在部分内置的宏定义

内置宏名

含义

_FILE_

被编译函数的名字

_LINE_

当前行号

_DATE_

编译时的日期

_TIME_

编译时的时间

_STDC_

编译器是否遵循标准C的规范

程序员在程序设计的时候可以利用这些内部的宏来输出相关的信息,以便对错误信息进行快速准确的定位。

另外程序员还可以查看各个包含的头文件来查看头文件包含的宏定义,在应用程序中是可以直接使用的。合理的利用内部的宏定义可以减少代码量。

 

五、高级使用方法

⑴        宏定义中的“#”运算符

#运算符的作用是在预编译期间将宏参数转换为字符串

例如:

#define STRING(s)  #s

STRING(snake_lp)   就等价于 “snake_lp”

 

下面代码(代码摘自国嵌的数据结构培训资料中):

#include
 
#define CALL(f,p) (printf("Call function %s\n", #f), f(p))
  
int square(intn)
{
    return n * n;
}
 
int f(int x)
{
    return x;
}
 
int main(void)
{
   
    printf("1. %d\n", CALL(square,4));
    printf("2. %d\n", CALL(f, 10));
 
    return 0;
}

 

⑵        宏定义中的“##”运算符

##运算符用于在预编译期间粘连两个字符串

例如:

#define STRING(s)  name_##s
STRING(number1)  就等价于 name_number1


此运算符真正的妙用是用来定义结构体,请欣赏下面代码(代码摘自国嵌的数据结构培训资料中):

#include 
 
#define STRUCT(type) typedef struct_tag_##type type;\
struct _tag_##type
 
STRUCT(Student)
{
   char* name;
   int id;
};
 
int main()
{
   
   Student s1;
   Student s2;
   
   s1.name = "s1";
   s1.id = 0;
   
   s2.name = "s2";
   s2.id = 1;
   
   printf("%s\n", s1.name);
   printf("%d\n", s1.id);
   printf("%s\n", s2.name);
   printf("%d\n", s2.id);
 
   return 0;
}

在体会以上两段代码之后,你会发现巧妙使用这两种运算符的好处。但是在不是特别了解此运算符的情况下,看这两段代码会摸不着头脑的。但是在一般的公司基本上很少这样使用代码的,说实话,对C语言了解不深的人员,根本是不了解这两种运算符的。所以这两点,我建议只是作为了解,不需要过多的深究。

 

六、条件编译

在C语言中,如果需要对程序中的一些代码段进行选择性的编译,就需要用到条件编译的命令,条件编译的格式有以下几种:

⑴        #if…#else…格式

#if 判断条件
           代码段1
#else
           代码段2
#endif

或者

#if 判断条件1
           代码段1
#elif 判断条件2
           代码段2
#else
           代码段3
#endif

功能:和if…else…表达式是一样的。适用的场景是存在真假的判断条件,此条件一般情况下是一个表达式。

 

⑵        #ifdef…#else…或者#ifndef…#else…格式

#ifdef 标识符
           代码段1
#else
           代码段2
#endif

或者

#ifndef 标识符
           代码段1
#else
           代码段2
#endif

功能:判断条件主要是查看标识符是否被定义(#define定义)。

 

在现实的工程项目中会使用大量的条件编译。比如说通过条件编译来使用各个不同的硬件平台;通过条件编译来实现平台和产品线管理;通过条件编译来区分正式版本和调试版本等等。


条件编译的本质是选择性的编译,其意义在于:

⑴        增加代码的兼容性,一套代码兼容多个硬件平台或者软件平台;

⑵        区分产品的调试版本和正式发布版本;

⑶        不同的产品线共用代码,使用条件编译来产生适用不同产品的目标文件;

⑷        同时也为程序员提供了一种屏蔽代码块的方式  #if 0….#endif。

 

理论上来说,条件编译是在预编译的时候生效的,但是我们不要就认为编译好了之后,条件编译就是不起作用了。其实对于第一种形式的条件编译在程序运行中也是有效的。即如果在运行中通过某些触发条件来修改条件编译判断条件的运算结果,也是可以完成实际执行代码段的切换。其实这样的做法在很多的产品中运用,即通过某些设置开关来开启和关闭一些功能。

你可能感兴趣的:(C语言)