“#define & #if & #include & #pragma……”你真的熟悉嘛?(预处理详解)

前言

本篇文章比较长,但是涉及了初学者在预处理方面能遇到的基本所有问题,包含了各种热门问题,还有一些冷门小知识,帮助大家在之后的编码过程中,避免一些预处理方面的问题,也能更游刃有余。本篇主讲预处理的应用方面,至于预处理的原理,请移步至程序的编译与链接


目录

前言

1.预定义符号

 2.#define

2.1#define替换规则

2.2#define宏定义种类及注意事项

2.2.1定义标识符

2.2.2数值宏常量

2.2.3字符串宏常量

2.2.4定义表达式

2.2.5宏替换多行代码

2.2.5#define宏定义整体实现

2.3#和##

2.3.1#的作用

 2.3.2##的作用

总结:

2.4带副作用的宏参数

2.5宏和函数的比较

2.6命名约定

3. #undef

3.1宏只能在main上面定义吗?

3.2#undef

4. 条件编译(#if):代码裁剪的工具

4.1为什么要有条件编译

4.2 #if

4.3多个分支的条件编译

4.4判断是否被定义

4.5嵌套指令

5.#include文件包含

5.1头文件包含的方式

5.1.1本地文件包含:

5.1.2库文件包含

5.2嵌套文件包含

6.一些“冷门”的骚知识

6.1 #error

 6.2 #line

6.3 #pragma

6.3.1#pragma warning()

6.3.2 #pragma message()

6.3.2 #pragma pack()

6.3.3 #pragma once

6.3.4其它


1.预定义符号

__FILE__                 //进行编译的源文件

__LINE__                 //文件内当前的行号

__DATE__                //文件被编译的日期

__TIME__                 //文件被编译的时间

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

这些符号都是语言内置的,一般用于日志。

代码举例:

int main()
{
	printf("FILE:%s\n", __FILE__);
	printf("LINE:%d\n", __LINE__);
	printf("DATE:%s\n", __DATE__);
	printf("TIME:%s\n", __TIME__);
	return 0;
}

运行结果:

 


 2.#define

2.1#define替换规则

在程序中扩展#define定义符号和宏时,需要注意以下几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数#define定义中可以出现其他#define定义的符号。
  2. 对于宏,不能出现递归。
  3. 当预处理器搜索#define定义的符号的时候,字符串常量(即字符串内部的#define)不会被搜索。

2.2#define宏定义种类及注意事项

2.2.1定义标识符

举例:

        #define reg register

作用: 创建一个简短的名字。

建议:尽量不用,不利于代码阅读

2.2.2数值宏常量

举例:

#define MAX 1000

#define PI 3.1415926

作用:某些会频繁用到的数字,最好用宏进行替换,如果以后需要对值进行修改,直接修改宏。

否则改代码的成本就会很高,可维护性较差。

  注意:

如下:

#define MAX 1000;

#define MAX 1000
建议不要在最后加上 ";"
由于#define是直接替换所有部分,所以 " ; " 也会进行替换,重复的" ; "就可能出现如下场景:
if(condition)
 max = MAX;
else
 max = 0;

就会出现语法错误。

2.2.3字符串宏常量

举例:

#define MY_CSDN "https://blog.csdn.net/yue152152"

使用:

#define MY_CSDN "https://blog.csdn.net/yue152152"
int main()
{
    const char* path = MY_CSDN;
    printf("%s\n", path);
    return 0;
}

注意:宏定义代表字符串的时候,一定要带上双引号。

小技巧:

1. 若替换的宏过长,可以使用“ \ ”进行续行。

2. 若要在字符串中输出“ \ ”,可使用“ \\ ”进行替换,避免发生转义输出乱码。

2.2.4定义表达式

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

下面是宏的申明方式:

#define name( parament-list )stuff

其中 parament-list 是一个由逗号分开的符号表,这些符号可能出现在stuff中。

注意:

参数列表的左括号必须与name紧邻,否则()会被解释为stuff的一部分。

使用举例与注意:

#define ADD(a, b) a + b
#define SQUARE(a, b) a * b
int main()
{
	printf("%d %d", ADD(5, 7) * 2, SQUARE(3,5 +1));--

}

我们预期的打印结果应该是:

(5 + 7) * 2  =  24;

3 * (5 + 1) = 18;

但实际却是:

 实际则是直接替换,过程是:

5 + 7 * 2 = 19;

3 * 5 + 2 = 16;

为避免如上问题的发生,我们应该每个宏定义整个宏定义表达式上都加括号就可以了:

#define ADD(a, b) ((a) + (b))
#define SQUARE(a, b) ((a) * (b))
int main()
{
	printf("%d %d", ADD(5, 7) * 2, SQUARE(3,5 +1));--

}

2.2.5宏替换多行代码

首先看下面一段代码:

该宏最大的特征是,替换的不是单个变量/符号/表达式,而是多行代码。

#include <stdio.h>
#define INIT_VALUE(a,b)\
	a = 0;\
	b = 0; 
int main() 
{
	int flag = 0;
	scanf("%d", &flag); 
	int a = 100; int b = 200;
	printf("before: %d, %d\n", a, b);
	if(flag) 
		INIT_VALUE(a,b); 
	else
		printf("error!\n");
	printf("after: %d, %d\n", a, b);
	return 0; 
}

 上述代码,编译直接报错,原因是if没有匹配到else,那么我们看一下预处理之后的结果:

 if倘若没有带{},那么if后面只能跟一条语句,当else匹配if时,直接报错了。

修正方法1:

我们给它加上花括号,我也推荐大家平时用这种方法写分支语句:

修正方法2:

如果我们不给if带{}呢?(好的解决方法是,即使不带{}也可以正常运行,写出更健壮的宏,是优秀的程序员该做的)

我们来看看下面的代码:

#define INIT_VALUE(a,b) \
do{\
	a = 0;\
	b = 0;\
}while(0) 
int main()
{ 
	int flag = 0;
	scanf("%d", &flag); 
	int a = 100; int b = 200; 
	printf("before: %d, %d\n", a, b); 
	if(flag)
		INIT_VALUE(a,b);
	else
		printf("error!\n"); 
	printf("after: %d, %d\n", a, b); 
	return 0; 
}

总结: 一个do-while-zero结构我们实现了“站着挣钱”,当我们需要宏进行多条语句替换的时候,我推荐使用这种结构,当然,如若可以。宏方面的东西,推荐少用。

2.2.5#define宏定义整体实现

#define MAX 1000             //数值宏常量
#define reg register         //创建一个简短的名字(尽量不用,不利于代码阅读)
#define MY_CSDN "https://blog.csdn.net/yue152152"//字符串宏常量
//如果定义的stuff过长,可以分几行写,出最后一行外,每行的后面加一个"\"(续航符)
#define LOG_PRINT \
do{\
	printf("%s\n",MY_CSDN);\
	printf("FILE:%s\n", __FILE__);\
    printf("LINE:%d\n", __LINE__);\
    printf("DATE:%s\n", __DATE__);\
    printf("TIME:%s\n", __TIME__);\
}while(0) 
int main()
{
	reg int a = 10;
	printf("%d %d\n", MAX, a);
	LOG_PRINT;
	return 0;
}

运行结果:

2.3#和##

2.3.1#的作用

首先我们来看看这样的代码:

int main()
{
	char* str = "te" "st";
	printf("%s\n", str);
	printf("te" "st\n");
	printf("%s\n", "te" "st");
	return 0;
}

我们发现字符串是有自动连接的特点的。

1. 于是我们接下来理解一下这段代码:

#define STR(s) #s
int main()
{
	printf("PI:"STR(3.1415926)"\n");
	return 0;
}

这里STR()就是将括号中的内容转换成一个字符串

替换过程:

1.STR(3.1415926)被换成#3.1415926

2.#3.1415926被换成"3.1415926"

3.printf("PI:"STR(3.1415926)"\n")就变成了printf("PI:" "3.1415926" "\n");

4.最后连接就成了printf("PI:3.1415926\n");

2.不知道大家有没有做过这样的问题:把一个整形转化成一个字符串数组

之前我们可能会去使用一些循环算法之类的,那么此时我们试试能不能用#的特性实现一步实现:

#define STR(s) #s
int main()
{
	char str[64] = { 0 };
	strcpy(str, STR(123456789));
	printf("%s\n", str);
}

这里的strcpy如果大家不了解可见字符串函数和内存函数的使用

当然,这里也是有局限的,括号里只能是数值常量。

因为宏替换是发生在预处理过程中,也就是在代码编译之前,所以如果里面放的是变量,也只能是转化成这个变量名的字符串。(站着挣钱难呐🙃)

3.我们再看最后一个例子:

#define PRINT(FORMAT, VALUE) printf("the value is: " #FORMAT "\n",VALUE)
int main()
{
	for (int i = 1; i < 3; i++)
	{
		PRINT(%d, i);
	}
	for (double i = 1.1; i < 1.3; i += 0.1)
	{
		PRINT(%.1lf, i);
	}
}

这里我们是想定义一个宏,它既能输出"the value is:1”这样的,有整形替换的字符串,也能输出"the value is:1.5”这样的,有浮点型进行替换的字符串。

那么我们就要让%d或者%f这样的符号出现在 “” 中,但是如果写为 "FORMAT" ,则一定会输出the value is: FORMAT ,所以我们使用 #FORMAT 来代替 "FORMAT",最后再将value替换。

整段宏替换就是:

PRINT(%d, i)

printf("the value is: %d\n",i)

注:这样的#也只能在宏定义中使用​​​​​​​

小总结:总的来说,这个#也并非那么好用,今后除非能实现很大优化,还是尽可能的不用吧,毕竟这样的代码可读性也差。

 2.3.2##的作用

##可以把位于它两边的符号合成一个符号。

允许宏定义从分离的文本片段创建标识符。

#define ADD_TO_SUM(num, value) sum##num += value
int main()
{
	int sum5 = 0;
	ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
}
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

总结:

总的来说,#就是文本变为字符串,##就是将两段文本合成一段文本

2.4带副作用的宏参数

当宏参数在宏定义的时候出现一次以上的时候,如果参数带有副作用,那使用这个宏的时候可能出现不可预测的后果。

这里的副作用是改变自己的操作,例如:

x+1;//不带副作用

x++;//带副作用

MAX 宏可以证明具有副作用的参数所引起的问题。
#include <stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);//z = ( (x++) > (y++) ? (x++) : (y++));
	printf("%d %d %d\n", x, y, z);
}

预处理后的结果:

z = ( ( x ++ ) > ( y ++ ) ? ( x ++ ) : ( y ++ ));
运行结果:
x = 6   y = 10  z = 9
 

2.5宏和函数的比较

属性#define定义宏函数
代码长度

每次预使用这个宏时,宏代码都会被插到代码中,除了非常小的宏外,程序长度会大幅增长。从预编译产生的文件到最后的exe文件,这之间所有的文件都会变大。

函数代码只出现在一个地方,每次使用函数都调用那个地方的同一份代码
执行速度更快存在函数在调用和返回过程中产生的额外开销,所以相对慢一些
操作符优先级由于是插入表达式的上下文,除非严格带括号,否则可能产生不可预料的后果运算后返回值,表达式的结果更容易预测
带有副作用的参数参数可能会被替换到宏体的多个位置,带有副作用的参数可能会被进行多次副作用操作只在传参时求一次,结果更容易控制
参数类型只要对参数的操作合法,可使用任何类型哦那个的参数,由于是替换,所以甚至可以是类型函数的参数是与类型有关的,如果参数类型不同,则需要不同的函数,即使执行的任务相同,但同时严格也对应着函数会更安全
调试宏是不方便调试的函数可以逐语句调试
递归宏是不能递归的函数可以

2.6命名约定

由于函数和宏在调用时语法很相似,所以为了区分二者,我们平时的一个习惯是:

宏名全部大写

函数名不要全部大写


3. #undef

3.1宏只能在main上面定义吗?

看这样一段代码:

#include <stdio.h> 
void print()
{
#define M 10
	printf("%d \n", M);
}
int main()
{
#define N 100
	print();
	printf("%d, %d\n", M, N); 
	return 0; 
}

结论:宏,在哪里都可以,习惯定义到最上面。

再看这样一段代码:

#include <stdio.h> 
void print()
{
	printf("%d \n", M);
}
int main()
{
#define M 10
	print();
	printf("%d\n", M); 
	return 0; 
}

可以打印出两个10吗?

答案是不能,运行的时候会直接报错,会直接显示函数内的M未定义。原因也很简单,宏的原则是直接进行替换,且只会替换宏下面的文本内容,而且是在编译之前就进行了替换,所以,这里虽然先定义宏,后调用函数,但函数的文本在上,所以不会进行替换。

3.2#undef

作用:用于移除一个宏定义

#undef NAME

//如果现存的标识符需要被重新定义,那么他的旧名字需要先被移除

我们测试一下:

#include <stdio.h>
#define M 10 
int main() 
{
#define N 100 
	printf("%d, %d\n", M, N); 
	printf("%d, %d\n", M, N); 
	printf("%d, %d\n", M, N);
#undef M //取消M
#undef N //取消N 
	printf("%d, %d\n", M, N); 
	printf("%d, %d\n", M, N);
	printf("%d, %d\n", M, N); 
	return 0; 
}

结论易知:前面三条可以正常编过,后面三条会报错未定义。

有了以上两条结论,我们再来看看下面这段代码:
 

#include <stdio.h>
int main()
{
#define X 3
#define Y X*2
#undef X 
#define X 2 
	printf("%d\n", Y); 
	return 0;
}

再来看一下运行结果:

老铁们想一下,为什么结果是4?

基于以上的一些只是,我们看看是不是这样:
1.首先替换Y,替换为X*2.

2.接下来再替换X,这里X为3的只有第五行一行,而我们要替换的位置是倒数第三行的位置,所以被换成了2也就不难理解。


4. 条件编译(#if):代码裁剪的工具

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
#if 0
int main()
{
	printf("#if test");
}
#endif

#if 0和#endif中间的代码将不进行编译,相当于被注释掉了。

以下将展示几种常见的条件编译指令

4.1为什么要有条件编译

本质认识:
条件编译,其实就是编译器根据实际情况,对代码进行裁剪。而这里 实际情况 ,取决于运行平台,代码本身的业务逻辑等。
可以认为有两个好处:
1. 可以只保留当前最需要的代码逻辑,其他去掉。可以减少生成的代码大小.
2. 可以写出跨平台的代码,让一个具体的业务,在不同平台编译的时候,可以有同样的表现
在哪些地方用:
举一个例子吧
我们经常听说过,某某版代码是完全版 / 精简版,某某版代码是商用版 / 校园版,某某软件是基础版 / 扩展版等。
其实这些软件在公司内部都是项目,而项目本质是有多个源文件构成的。所以,所谓的不同版本,本质其实就是功能的有 无,在技术层面上,公司为了好维护,可以维护多种版本,当然,也可以使用条件编译,你想用哪个版本,就使用哪种条件进行裁剪就行。
著名的 Linux 内核,功能上,其实也是使用条件编译进行功能裁剪的,来满足不同平台的软件。

4.2 #if

结构:

#if 常量表达式

//......

#endif

这里的常量表达式由预处理器求值。

举例:

#define _DEBUG_ 1
#if _DEBUG_
int main()
{
	printf("#if test");
}
#endif

若_DEBUG_为0,则整段代码将被注释掉,_DEBUG_为1,将不会被注释。

4.3多个分支的条件编译

#if 常量表达式

//......

#elif 常量表达式

//......

#else

//......

#endif

4.4判断是否被定义

#if defined(symbol)

#elif defined(symbol)

#if !defined(symble)

#ifdef symbol                   //相当于#if defined(symbol)

#ifndef symbol                 //相当于#if !defined(symble)

与条件编译相同,当一套分支结束之后都要加#endif

这里#if defined()中的defined()可以理解为一个表达式,若被定义了返回1,否则返回0,再用 #if 进行判断,那么要同时判断两个宏是否被同时定义,是不是就可以这样写:

#if (defined(C) && defined(CPP))

那么,是否有一个被定义、是否没有都被定义……只要你想,就都能写出来:

#if (defined(C) || defined(CPP))

#if !(defined(C) && defined(CPP))

4.5嵌套指令

#if defined(TEST1)
	#ifdef OPTION1
		test1_option1();
	#elif define(OPTION2)
		test1_option2();
	#else
		test1_op_else();
	#endif

	#if JUDGE1
		test1_judge1();
	#elif JUDGE2
		test1_judge2();
	#else
		test1_jud_else();
	#endif
#elif defined(TEST2)
	test2();
#endif

5.#include文件包含

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

5.1头文件包含的方式

5.1.1本地文件包含:

#include "filename"

查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标 准位置查找头文件。
如果找不到就提示编译错误。

5.1.2库文件包含

#include <filename>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的, 可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
总的来说:本地文件就用"",库文件就用<>.

5.2嵌套文件包含

当我们使用多文件的时候,常出现出现这样的场景:

 comm.h和comm.c是公共模块。

test1.h和test1.c使用了公共模块。

test2.h和test2.c使用了公共模块。

test.h和test.c使用了test1模块和test2模块。

这样最终程序中就会出现两份 comm.h 的内容。这样就造成了文件内容的重复。
如何解决这个问题呢?
答案:条件编译

在每个文件开头写:

#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
#endif   //__TEST_H__  

解释:如果是第一次调用时__TEST_H__是没有被定义的,那么下面的代码就会被保留,首先将__TEST_H__定义,然后包含头文件,此后再调用__TEST_H__都是被定义过的,也就不会再包含下面的头文件。

或者在头文件的开始:

 #pragma once

这样就可以避免头文件的重复引入。

6.一些“冷门”的骚知识

6.1 #error

看这样一段代码:

int main() 
{
#ifndef __cplusplus
#error 老铁,你用的不是C++的编译器哦 
#endif
	return 0;
}

首先说明一下:如果我们使用的是c++编译器,即使我们自己不定义,编译器也会默认定义宏"__cplusplus"

也就是说如果使用的不是c++编译器,那么就会执行<#error 老铁,你用的不是C++的编译器哦>这段代码,下面是我用C语言编译器执行的结果。

我们发现,他直接报错了,报错的内容就是我们#error定义的那段文字。

啊没错,#error它就是这个作用,但凡编译过程出现了它,就会报错,报错的内容就是我们定义的那段内容。

 6.2 #line

还记得我们最一开始讲的预定义符号吗,其中__FILE__和 __LINE__分别是文件名和行号,而这里的#line就是对这两个预定义符号进行修改。

#include <stdio.h>
int main()
{
	printf("%s, %d\n", __FILE__, __LINE__);
#line 60 "hehe.h" 
	printf("%s, %d\n", __FILE__, __LINE__);
	return 0; 
}

第一次打印会打印它的文件名和行号,那么#line这条指令过后呢?​​​​​​​

 不出所料,__FILE__和 __LINE__的值果然被修改为了"hehe.h"和60。

6.3 #pragma

在所有的预处理指令中,#pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。下面,我们列举一些常用的 #pragma使用。

6.3.1#pragma warning()

我相信,用vs编译器同学们,在刚学习c语言的时候一定都使用过”scanf()"这样的输入函数,那想必也一定会遇到这样的报错:

error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS.

那为什么会出现这样的报错呢?

这是因为vs觉得scanf这个库函数不安全,所以想让我们使用其他方案替换。

不知道大家当时是如何解决的,我这里提供三种解决方案:

1.报错内容上"Consider using scanf_s instead",是想让我们使用vs自己的scanf_s去替换scanf,当然替换之后就可以运行了,但是这里的scanf_s()函数只有vs编译器可以用,当使用其他编译器的时候将不再能找到这个库函数,可移植性就变差了,所以我们不建议使用这种方法。

2.当然VS也没把事情做绝,他后面也提供了无视方案:To disable deprecation, use _CRT_SECURE_NO_WARNINGS.,这里是让我们定义_CRT_SECURE_NO_WARNINGS这个宏,让它不再对这个错误进行检查。

#define _CRT_SECURE_NO_WARNINGS

只需要在代码的最一开头写上这样的宏定义就可。

3.前面讲了这么多,终于要”上正轨“了,我们也可以使用

#pragma warning(disable:4996)

这样的预处理,无视第4996号报错。

不仅仅是disable,#pragma warning还有一些其他用法:

once :只显示一次 (警告 /错误等 )消息
default :重置编译器的警告行为到默认状态(1 , 2 , 3 , 4 :四个警告级别)
disable :禁止指定的警告信息
error :将指定的警告信息作为错误报告

6.3.2 #pragma message()

回忆一下前面讲的 #error,一旦出现,直接报错终止。那么,如果我们想让程序在编译通过的条件下,还能让编译器提示一些信息,该怎么做呢?

没错,使用

#pragma message()

接下来,我们运行这样一段代码:

#define M 10 
int main()
{
#ifdef M
#pragma message("M宏已经被定义了") 
#endif
	return 0; 
}

我们再来看看运行结果:

 我们发现这条信息并非显示在了调试控制台,而是和error一样,出现在了编译输出框中,只不过#error是报错级的信息,这里只是提示级的信息。

6.3.2 #pragma pack()

这个预处理符是用来控制结构体默认对齐数的,具体详情见:结构体内存对齐,结构体变量大小计算,位段内存分配及相关问题_Massachusetts_11的博客-CSDN博客_结构体内存对齐结构体内存对齐,结构体变量大小计算,位段内存分配及相关问题https://blog.csdn.net/yue152152/article/details/122848201?spm=1001.2014.3001.5502

6.3.3 #pragma once

见5.2嵌套文件包含。

6.3.4其它

更多关于#pragma的知识见:#pragma其他知识

  • 23
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值