C语言-预处理器

目录

宏定义

宏定义用法

1.宏常量

2.宏语句

3.宏函数

4.其它

宏定义相关作用符

1.换行符 "\"

2.字符串化符 "#"

3.片段连接符"##"

宏函数的巧用

1.类型传递

2.传递数组

注意事项

1.运算符优先级问题

2.宏参数重复调用

3.分号吞噬问题

4.递归调用问题

5.宏参数预处理

#include指令

插入头文件的内容

预处理器如何找到头文件

嵌套的 #include 命令

#undef指令

防止宏定义冲突

增强代码可读性

自定义接口

用于调试头文件中

内联函数

什么是内联函数

内联函数的编程风格

慎用内联

总结


宏定义

编译一个C语言程序的第一步骤就是预处理阶段,这一阶段就是宏发挥作用的阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,主要任务包括删除注释、插入被#include进来的文件内容、定义和替换由#define 定义的符号以及确定代码部分内容是否根据条件编译(#if )来进行编译。”文本性质”的操作,就是指一段文本替换成另外一段文本,而不考虑其中任何的语义内容。宏仅仅是在C预处理阶段的一种文本替换工具,编译完之后对二进制代码不可见

宏定义用法

1.宏常量

  我们最常使用到的#define的用法就是用#define来定义一个符号常量,而要修改时,只需修改#define这条语句就行了,不必每处代码都修改
例:

#include"stdio.h"
#define PI 3.14
#define STR "圆周率约等于"
int main()
{
	printf("%s %f",STR,PI); //预处理时会被替换为 printf("%s %f","圆周率约等于",3.14);
	return 0;
}

运行结果:
在这里插入图片描述

2.宏语句

  我们还可以用宏定义一条或多条语句
例:

#include"stdio.h"
#define Print printf("hello world!")
int main()
{
	Print;  //预处理时会被替换为 printf("hello world!");
	return 0;
}

操作结果:
在这里插入图片描述

3.宏函数

  我还可以用宏来定义函数,因为宏定义也可以带参数
例:

#include"stdio.h"
#define Print(str) printf("%s",str)
int main()
{
	Print("这是一个只有一条语句的宏函数!");
    //预处理时会被替换为 printf("%s","这是一个只有一条语句的宏函数!")
	return 0;
}

在这里插入图片描述

4.其它

1.#undef 是用来撤销宏定义的,用法如下:

#define PI 3.141592654
 
…
 
// code
#undef PI
//下面开始 PI 就失效了

2.使用ifndef防止头文件被重复包含和编译

  这是宏定义的一种,它可以根据是否已经定义了一个变量来进行分支选择,一般用于调试等等.实际上确切的说这应该是预处理功能中三种(宏定义,文件包含和条件编译)中的一种----条件编译。 C语言在对程序进行编译时,会先根据预处理命令进行“预处理”。C语言编译系统包括预处理,编译和链接等部分。

#ifndef x //先测试x是否被宏定义过
#define x //如果没有宏定义下面就宏定义x并编译下面的语句
...
...
...
#endif //如果已经定义了则编译#endif后面的语句

条件指示符#ifndef检查预编译常量在前面是否已经被宏定义。如果在前面没有被宏定义,则条件指示符的值为真,于是从#ifndef到#endif之间的所有语句都被包含进来进行编译处理。相反,如果#ifndef指示符的值为假,则它与#endif指示符之间的行将被忽略。条件指示符#ifndef 的最主要目的是防止头文件的重复包含和编译。
  千万不要忽略了头件的中的#ifndef,这是一个很关键的东西。比如你有两个C文件,这两个C文件都include了同一个头文件。而编译时,这两个C文件要一同编译成一个可运行文件,于是问题来了,大量的声明冲突。

所以还是把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:

  #ifndef <标识>

  #define <标识>

  ......

  #endif

<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h


  #ifndef _STDIO_H_

  #define _STDIO_H_

  ......

    #endif

#ifndef xxx //如果没有定义xxx
#define xxx //定义xxx
#endif //结束如果
这个用法主要是在头文件中,主要是为了防止类重复的include,所以在类的头文件之前加上前面两个,用类名替代xxx,在最后加上最后一句

宏定义相关作用符

1.换行符 "\"

  我们定义宏语句或者宏函数时不可能总是一条语句呀,那要是有很多条语句时怎么办?都写在一行吗?这样显然代码就不美观,可读性不好,所以有多条语句时,我们就在每行末尾(除了最后一行)加上"\",代表换行的意思
例:

#include"stdio.h"
#define Print   printf("这是第1条语句\n");\
 		    	printf("这是第2条语句\n");\
 		    	printf("这是第3条语句\n")
 		    	
#define Show(str1,str2,str3)\
{\
	printf("%s\n",str1);\
	printf("%s\n",str2);\
	printf("%s\n",str3);\	
}
int main()
{
	Print;  //无参数宏函数
	Show("first","second","third"); //带参数宏函数
	return 0;
}

操作结果:
在这里插入图片描述

2.字符串化符 "#"

  "#"是“字符串化”的意思,将出现在宏定义中的#是把跟在后面的参数转换成一个字符串
例:

#include"stdio.h"
#define Print(str)\
{\
	printf(#str"的值是%d",str);\	
}
int main()
{
	int x=3,y=4;
	Print(x+y); //此处等价于printf("x+y""的值是%d",x+y);
	            //#str等价于"x+y",所以#str不需要再用双引号引起来 
	return 0;
}

操作结果:
在这里插入图片描述

3.片段连接符"##"

  “##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。在普通的宏定义中,预处理器一般把空格解释成分段标志,对于每一段和前面比较,相同的就被替换。但是这样做的结果是,被替换段之间存在一些空格。如果我们不希望出现这些空格,就可以通过添加一些##来替代空格。
例:

#include"stdio.h"
#define Add(n,value)\
{\
	num##n+=value;\
 } 
int main()
{
	int num1=1;
	int num2=10;
	Add(2,10); //等价于num2+=10; 这里把num和2连接成了num2 
	printf(" num1=%d\n num2=%d",num1,num2); 
	return 0;
}

操作结果:
在这里插入图片描述
 

宏函数的巧用

1.类型传递

  我们知道函数虽然可以传递参数,但是却不能把类型作为参数传递,有时我们为了实现函数的复用性,就要使用STL模板,但是我们这个时候还有另外一种选择,就是写成宏函数
例:
一个开辟内存的函数

#define Malloc(type,size) (type*)malloc(sizeof(type)*size)

这个时候,我们只有把类型,容量作为参数传递进行,就可以开辟各种类型的内存了

int *p=Malloc(int,100); //开辟int类型的内存
char *q=Malloc(char,100); //开辟字符类型的内存

2.传递数组

  由数组作为函数参数传递时,会失去其数组特性,也就是无法使用sizeof()函数计算出数组的大小,比如我们写一个排序函数,排序时我们不仅需要知道数组的首地址,还需要知道数组的大小,但是仅仅把数组名作为参数传递时,无法得知其数组大小,这时我们的函数就需要传递第二个参数,也就是数组的大小,于是函数就要写成Sort(int *a,int size).但宏函数就可以解决这个问题
例:
下面用宏函数写一个插入排序算法

#include"stdio.h"
#define InsertSort(list)\
{\
	int s=sizeof(list)/4;\
	int i,j;\
	for(i=2;i<=s;i++)\
	{\
		list[0]=list[i];\
		for(j=i-1;list[j]>list[0];j--)\
				list[j+1]=list[j];\	
		list[j+1]=list[0];\		
	}\ 
}
int main()
{
	int num[]={0,2,5,7,3,1,8,0,8,22,57,56,74,18,99,34,31,55,41,12,9,4};
	InsertSort(num);
	for(int i=1;i<sizeof(num)/4;i++)	
		printf("%d ",num[i]);
	return 0;
} 

操作结果:
在这里插入图片描述
当然还有很多宏定义的巧妙用法,这里就不全部列举了
 

注意事项

1.运算符优先级问题

#define MULTIPLY(x, y) x * y

  这是一个很简单的乘法函数,当计算MULTIPLY(10, 10),结果是100,这个大家都知道,但是当你计算MULTIPLY(5+5, 10)时,你以为结果还是100吗?当然不是,MULTIPLY(5+5, 10)=5+5*10=55,所以结果是55,所以我们写宏函数时要特别注意运算符的优先级,这里稳妥一点的写法应该这样写

#define MULTIPLY(x, y) ((x)*(y))

2.宏参数重复调用

#define MAX(a,b) ((a)>(b)?(a):(b))
int a=0;
int b =1;
int c =MAX(++a,++b);

这里很多人都以为是c=MAX(1,2)=2;而实际上上面代码等价于

int c =((++a)>(++b)?(++a):(++b));

可以看到实际上a b都各自加了两次,所以c=1>2?2:3=3,所以结果是3

3.分号吞噬问题

#include"stdio.h"
#define FUN(n)\
{\
	while(n>0)\
	{\
		if(n==3)\
			break;\	
	}\	
}
int main()
{
	int num=10;
	if(num>0)
		FUN(num);
	else
		num=-num;
	return 0;
}

  看似代码没有问题,但是一编译就报错,编译器显示"error: ‘else’ without a previous ‘if’",原来是因为FUN函数是一个代码块,然后if(num>0) FUN(num); 就等价于if(num>0) {…}; 这不就是在大括号后面打分号了吗?这样else当然就缺少if了
  这个时候我们可以用do{…}while(0)来解决这个问题,写成如下就没问题了,因为while()后面正好需要一个分号

#define FUN(n)\ 
do\
{\
	while(n>0)\
	{\
		if(n==3)\
			break;\	
	}\	
}while(0)

4.递归调用问题

#define NUM (4 + NUM)

  按前面的理解,(4 + NUM)会展开成(4 + (4 + NUM)),然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,NUM只会展开成(4 + NUM),而展开之后NUM的含义就要根据上下文来确定了。

5.宏参数预处理

  宏参数中若包含另外的宏,那么宏参数在被代入到宏体之前会做一次完全的展开,除非宏体中含有#或##。

有如下宏定义:

#define A(y) X_##y
#define B(y) A(y)
#define SIZE 1024
#define S SIZE

A(S)会被展开成X_S。因为宏体中含有##,宏参数直接代入宏体。
B(S)会被展开成X_1024。因为B(S)的宏体是A(S),并没有#或##,所以S在代入前会被完全展开成1024,然后才代入宏体,变成X_1024。


#include指令

  • #include 命令是预处理命令的一种,预处理命令可以将别的源代码内容插入到所指定的位置;可以标识出只有在特定条件下才会被编译的某一段程序代码;
  • 可以定义类似标识符功能的宏,在编译时,预处理器会用别的文本取代该宏。

插入头文件的内容

  • #include 命令告诉预处理器将指定头文件的内容插入到预处理器命令的相应位置。有两种方式可以指定插入头文件:
#include <文件名>
#include "文件名"
  • 如果需要包含标准库头文件或者实现版本所提供的头文件,应该使用第一种格式。如下例所示:
#include <math.h>               // 一些数学函数的原型,以及相关的类型和宏
  • 如果需要包含针对程序所开发的源文件,则应该使用第二种格式。采用 #include 命令所插入的文件,通常文件扩展名是 .h,文件包括函数原型、宏定义和类型定义。
  • 只要使用 #include 命令,这些定义就可被任何源文件使用。如下例所示:
#include "myproject.h"         // 用在当前项目中的函数原型、类型定义和宏
  • 你可以在 #include 命令中使用宏。如果使用宏,该宏的取代结果必须确保生成正确的 #include 命令。
  • 例 1 展示了这样的 #include 命令。
  • 【例1】在 #include 命令中的宏
#ifdef        _DEBUG_
  #define       MY_HEADER       "myProject_dbg.h"
#else
  #define       MY_HEADER       "myProject.h"
#endif
#include        MY_HEADER
  • 当上述程序代码进入预处理时,如果 DEBUG 宏已被定义,那么预处理器会插入 myProject_dbg.h 的内容;如果还没定义,则插入 myProject.h 的内容。

预处理器如何找到头文件

  • 由给定的 C 语言实现版本决定 #include 命令所指定文件的搜索路径。同时,也由实现版本决定文件名是否区分大小写。对于命令中使用尖括号指定的文件(),预处理器通常会在特定的系统路径下搜索,例如,在 Unix 系统中,会搜索路径 /usr/local/include 与 /usr/include。
  • 对于命令中用双引号指定的文件("文件名"),预处理器通常首先在当前目录下寻找,也就是包含该程序其他源文件的目录。如果在当前目录下没有找到,那么预处理器也会搜索系统的 include 路径。文件名中可以包含路径。但如果文件名中包含了路径,则预处理器只会到该目录下寻找。
  • 你也可以通过使用编译器命令行选项,或在环境变量(该变量通常称为 INCLUDE)中加入搜索路径,为 #include 命令指定自己的搜索路径。具体的做法请参考采用的编译器的说明文档。

嵌套的 #include 命令

  • #include 命令可以嵌套使用;也就是说,通过 #include 命令插入的源文件本身也可以包含另一个 #include 命令。预处理器最多允许 15 层的嵌套包含。
  • 因为头文件有时候会包含另一个头文件,很容易发生相同的一个文件被多次包含的情况。例如,假设文件 myProject.h 中包含如下代码:
#include <stdio.h>
  • 如果源文件包含下面的 #include 命令,就会两次包含 stdio.h,一次是直接包含,另一次是间接包含:
#include <stdio.h>
#include "myProject.h"
  • 然而,可以采用条件式编译的命令,方便地避免多次包含相同的文件。例 2 使用了这个技巧。
  • 【例2】避免多次包含
#ifndef INCFILE_H_
#define INCFILE_H_
/* ...实际的头文件incfile.h的内容写在这里... */
#endif  /* INCFILE_H_ */
  • 第一次出现包含 incfile.h 的命令时,INCFILE_H_ 宏是没有定义的。预处理器因此插入 #ifndef 和 #endif 之间的内容,这段内容包含了对 INCFILE_H_ 宏的定义。嵌入 incfile.h 文件之后,#ifndef 条件就会为 false,预处理器会忽略 #ifndef 和 #endif 之间的内容。

#undef指令

C语言中#undef的语法定义是:#undef 标识符,用来将前面定义的宏标识符取消定义。

然而,在实际应用中,#undef到底可以用来做什么?

整理了如下几种#undef的常见用法。

防止宏定义冲突

在一个程序块中用完宏定义后,为防止后面标识符冲突需要取消其宏定义。

例如:

#include <stdio.h>

int main()
{
#define MAX 200
printf("MAX = %d\n", MAX);
#undef MAX

    int MAX = 10;
    printf("MAX = %d\n", MAX);

    return 0;
}

/******** 例程1:main.c ********/

在一个程序段中使用完宏定义后立即将其取消,防止在后面程序段中用到同样的名字而产生冲突。

增强代码可读性

在同一个头文件中定义结构类型相似的对象,根据宏定义不同获取不同的对象,主要用于增强代码的可读性。

例如:在头文件student.h中定义两个学生对象(小明和小红),两个对象互不干涉。

#ifdef MING
#define MING_AGE 20
#define MING_HEIGHT 175
#endif

#ifdef HONG
#define HONG_AGE 19
#define HONG_HEIGHT 165
#endif

/******** 例程2:student.h ********/

在源文件中使用这两个对象:

#include <stdio.h>

#define MING
#include "student.h"
#undef MING
#define HONG
#include "student.h"
#undef HONG

int main()
{
printf("Xiao Ming's age is %d.\n", MING_AGE);
printf("Xiao Hong's age is %d.\n", HONG_AGE);

return 0;
}

/******** 例程3:main.c ********/

在一个头文件里定义的两个对象与分别在两个头文件里定义效果相同,但如果将相似的对象只用一个头文件申明,可以增强源代码的可读性。

自定义接口

将某个库函数包装成自定义接口,而只允许用户调用自定义接口,禁止直接调用库函数。

(此例来源于《C和指针》)

例如,自定义安全的内存分配器接口:

/*
** 定义一个不易发生错误的内存分配器
*/
#include <stdlib.h>

#define malloc                         //防止直接调用malloc!
#define MALLOC(num, type)   (type *)alloc((num) * sizeof(type))
extern void *alloc(size_t size);

/*********** 例程4:alloc.h ***********/

其中“#define malloc”是为了防止用户直接调用库函数malloc,只要包含了这个头文件alloc.h,就不能直接调用库函数malloc,而只能调用自定义函数MALLOC,如果用户要调用库函数malloc编译器会发生错误。

自定义安全的内存分配器的实现:

/*
** 不易发生错误的内存分配器的实现
*/
#include <stdio.h>
#include "alloc.h"
#undef malloc

void *alloc(size_t size)
{
    void *new_mem;
    new_mem = malloc(size);
    if(new_mem == NULL)
    {
        printf("Out of memory!\n");
        exit(1);
    }
    return new_mem;
}

/*********** 例程5:alloc.c ***********/

因为在实现中需要用到库函数malloc,所以需要用这一句“#undef malloc”取消alloc.h中对malloc的宏定义。

这种技巧还是比较有意思的,用于对已经存在的库函数进行封装。而且如果包含了自定义接口文件,就不能直接调用库函数,而只能调用自定义封装的函数。

用于调试头文件中

用于调试头文件中,偶然看到这样一个代码用到了#undef,写于此作为记录:

#ifdef _DEBUG_
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#define new DEBUG_NEW
#endif

/*********** 例程6:debug.h ***********/

 


内联函数

什么是内联函数

       在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。

       为了解决这个问题,特别的引入了inline修饰符,表示为内联函数

       栈空间就是指放置程式的局部数据也就是函数内数据的内存空间,在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足所造成的程式出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

下面我们来看一个例子:

#include <stdio.h>  

//函数定义为inline即:内联函数  
inline char* dbtest(int a) 
{  
	return (i % 2 > 0) ? "奇" : "偶";  
}   
  
int main()  
{  
	int i = 0;  
	for (i=1; i < 100; i++) 
	{  
		printf("i:%d    奇偶性:%s /n", i, dbtest(i));      
	}  
} 

     上面的例子就是标准的内联函数的用法,使用inline修饰带来的好处我们表面看不出来,其实在内部的工作就是在每个for循环的内部任何调用dbtest(i)的地方都换成了(i%2>0)?"奇":"偶"这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。

     其实这种有点类似咱们前面学习的动态库和静态库的问题,使 dbtest 函数中的代码直接被放到main 函数中,执行for 循环时,会不断调用这段代码,而不是不断地开辟一个函数栈。

内联函数的编程风格

1、关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用

如下风格的函数Foo 不能成为内联函数:

inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y)
{
}

而如下风格的函数Foo 则成为内联函数:

void Foo(int x, int y);
inline void Foo(int x, int y) // inline 与函数定义体放在一起
{
}

       所以说,inline 是一种 “用于实现的关键字” ,而不是一种“用于声明的关键字”。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

2、inline的使用是有所限制的

      inline只适合涵数体内代码简单的函数数使用,不能包含复杂的结构控制语句例如while、switch,并且内联函数本身不能是直接递归函数(自己内部还调用自己的函数)。

慎用内联

       内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?

       内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收
获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间

以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline 不应该出现在函数的声明中)。

总结

       因此,将内联函数放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦.而所以声明跟定义要一致,其实是指,如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为,即是说,如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定.所以,最好将内联函数定义放在头文件中. 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兔子递归

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值