【C】预编译指令

预编译处理

预处理命令是C标准规定加入C语言源程序中的,目的是改进程序设计环境,提高编程效率。但这些命令不是C语言本身的组成部分,所以编译器无法识别,不能对它们直接进行变编译。因而要编译程序在对c源程序进行编译之前,先由预编译程序对这些编译预处理命令进行处理,这一过程称为“预编译处理”。
在C语言中,凡是以“#”开头的行,都称为预编译指令,主要有:

#include  //调用头文件
#define  //宏定义
#if
#else
#endif
#elif
#ifndef  //如果没有定义指定头文件,那么执行子代码
#undef  //取消宏定义
#line
#program  //设定编译器的状态或者是指示编译器完成一些特定的动作
#error

根据需要,指令可以出现在程序的任何位置,其作用域一直持续到源文件结束。


#define 宏定义

我们已经见过#define指令的一些简单用法,就是为数值命名一个符号
正式描述一下定义方式: #define 标识符 替换数值(stuff)
有了这条指令后,预处理器会将源程序中出现的所有标识符,替换成数值。

#define PAI 3.14  //之后的代码中都可以用PAI来替换3.14这个浮点数
                  //一般大写别名

数值😗:可以是常数,表达式,字符串等任意字符,甚至C语言语句也可以。如果定义中的stuff非常长,它可以分成几行,除了最后一行之外,每行的末尾都要加一个反斜杠,如下面的例子所示:

#define maye printf("file is in the %d line", 
							__LINE__);

宏定义的作用范围是从定义开始到取消定义结束。取消定义我们使用** #undef** 这个指令。

#define MAYE "钟声"
printf("%s", MAYE);
#undef MAYE
printf("%s", MAYE); //从程序运行角度考虑,第二句输出是不会被执行的,因为针对MAYE的宏定义已经被取消了

#define 不是C语言规范语句,所以不需要在末尾加上分号";"


typedef

typedef属于C语言语句,所以需要在结尾加上分号";"


#define带参宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为带参宏。下面是宏的声明方式:

#define name(参数列表) stuff

其中,parameter-list (参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff 的一部分。(这个参数不需要声明数据类型)

当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。当参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。

下面举一个例子:

int main(void)
{
#define SQUARE(x) (x * x)
	printf("%d\n", SQUARE(3, 9));
	system("pause");
	return 0;
}

正常由于我的宏定义函数中只含有一个未知参数,所以对应输出个应该是根据这个输入参数的表达式的值,但是在实际输出过程中我们向这个宏定义函数中输入了两个变量,vs系列编译器不会报错,但是仅会将第一个参数传入函数并进行输出,所以最后输出的结果应该为9。【切记,宏定义函数实现的作用仅仅是将对应的带参宏和表达式进行替换】

所以这里就会出现下面要提到的问题:


#define带参宏的警告

警告:带参宏仅仅做替换,意味着使用时会出现一个问题。
比如:

printf("%d", SQUARE(3+1));

按照我们上述宏定义的带参宏,他应该输出的是4*4=16,但是实际上他输出的竟然是7。
我们来看一下,如果严格按照我们上述的单纯替换的原理的话,那么实际计算机在运行的时候进行的替换是:

printf("%d", 3+1*3+1);  //按照四则运算的先后顺序,确实是7
//如果我们将宏定义进行一下更改,那么就可以实现我们期望的结果
#define SQUARE(x) (x)*(x)
//这样我们再次进行输出,实现的效果为:
printf("%d", (3+1)*(3+1));  //这样就可以实现输出16了!

综上,我们在进行带参宏替换的时候,一定要考虑到数学运算的逻辑问题!上述例子仅仅是涉及到了加法,当面对乘除法时,以我们的SQUARE宏为例,就没有优先级可言,乘法的操作可能会被拆开!


#和##

# 用在宏定义中,表示把参数字符串化,即把一个参数转换成字符串。(这个参数是不需要考虑其初始数据类型的)

#define TOSTRING(x) #x
puts(TOSTRING(1));  //1
puts(TPSTRING("num"));  //"num"

我们可以注意到第一行输出虽然我们要转化的是一个整数,但是这个数字1也被我们转化为了字符串类型;当我们输入一个字符串的时候,我们会发现的他的输出是带有双引号的,因为他本身就是字符串类型。

## 是连接两个参数成为一个字符串

#define CON(x) cc##x
int CON(num);
ccnum = 100;
printf("%d\n", ccnum);

注意: 这里的连接符一个需要用到你输入的参数,切记你自己输入的参数可以是字符串或者数字,但是你想要链接的必须是字符串,不能存在数字。

#define CON(X) 1##x  //这种定义方式就是错误的

带参宏与函数

带参宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找其中较大的一个:

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

为什么不用函数来完成这个任务呢?
有两个原因:

  • 首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹。
  • 更为重要的是,函数的参数必须是一种特定的类型,所以他只能在类型合适的表达式上使用。而上面的带参宏可用于int、long int、float、double、char等。就是说宏是与类型无关的。

#define条件编译

#define A 0
#if A
	printf("好好学习");
#elif
	printf("考好成绩");
#else
	printf("不要分心");
#endif

宏定义中的条件定义语法规则和C语言中,但是要注意的是:因为预编译代码是要求提前变异的,如果我们在宏定义中使用了C语言定义的变量,那么是不会遵循语法规则的,相应变量被我们当作0。

int a = 0;
#if a
	printf("考好成绩");
#else
	printf("不要分心");
#endif  //该程序输出"不要分心"因为a在预编译中被当做0

预定义符号

C标准定义了一些y预处理符号

__FILE__  //进行预编译的文件名  %s
__LINE__  //__LINE__所在的行号  %d
__DATA__  //文件被编译的日期  %s
__TIME__  //文件被编译的时间  %s
__FUNCTION__  //所在函数的函数名  %s
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATA__);
printf("%s\n", __TIME__);
printf("%s\n", __FUNCTION__);

#include文件包含

我们已经非常熟悉#include指令了,用来包含头文件,实际上任意文件都可以。它会把我们包含的文件全包复制到包含位置。

标准库文件包含对于编译器已经提供好的库文件,我们可以用过下面这种语法。
#include<filename> stdio、stdlib、string、malloc

  • 对于filename,并不存在任何限制,不过根据约定,标准库文件以.h后缀结尾。编译器通过定义好的“库文件位置“查找头文件。

本地文件包含对于自定义的库文件,我们可以使用下面这种语法。
#include“filename”

  • 处理本地头文件首先是在源文件所在的当前目录进行查找,如果该头文件并未找到,编译器就像查找函数库头文件一样在标准位置查找本地头文件。

你可以在所有的#include语句中使用双引号。但是,使用这种方法,编译器在查找函数库头文件时会浪费少许时间。而且,对函数库头文件使用尖括号可以一眼就看出包含的是标准库文件还是自定义头文件,便于区分。

嵌套文件包含

多重包含在绝大多数情况下出现于大型程序中,它往往需要使用很多头文件,因此要发现这种情况并不容易。要解决这个问题,我们可以使用条件编译。如果所有的头文件都像下面这样编写:

#ifndef _HEAD_H_  //检测如果没有定义指定头文件,那么执行下面代码
#define _HEAD_H_
/*中间写_HEAD_H_头文件中含有的代码*/
#endif

这样,多重包含的危险就被消除了,在第一次包含时头文件被正常替换,并且定义宏_HEAD_H_。如果头文件被再次包含,通过条件编译,它的所有内容被忽略。所以我们在平时写代码的时候,就应该尽可能避免重头文件产生,避免多重包含。


#pragma

设定编译器的状态或者是指示编译器完成一些特定的动作。
#pragma message(“消息文本”)

  • 当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。

#pragma once

  • 在头文件的最开始加入这条指令就能够保证头文件被编译一次。也可以当作避免头文件多重包含的代码

#pragma warning(command:错误代码)

  • 设置警告信息状态 disable:520 //屏蔽520号警告信息
  • once:520 //仅报告一次520警告信息 (针对多次重复相同警告代码操作)
  • error:520 //把520警告作为一个错误 (有的代码会显示风险,但是不会报错可以正常运行,这里我们可以让他报错无法运行)
    在程序调试阶段,我们可以看到代码错误警告对应的错误代码(一般是Cxxx,取后面的数字,然后进行上述调试的操作)
    #pragma comment(comment-type,”commentString”)
  • comment-type是一个预定义的标识符,指定注释的类型,一般用来加载静态库
  • commentString是一个提供为comment-type提供附加信息的字符串。

#pragma pack() 指定对齐长度:用在结构,联合中

#pragma pack(1)
struct num
{
	int a;
	char c[10];
}
printf("%d", sizeof(struct num));  //输出为14,因为指定最小对齐长度为1
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页