C语言预处理详解

这篇具有很好参考价值的文章主要介绍了C语言预处理详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

C语言预处理详解,c语言,开发语言,后端

C语言预处理详解,c语言,开发语言,后端

前言 \color{maroon}{前言} 前言

上一篇博客中我们讲了C语言的编译与链接,在编译过程中有三个小阶段:预处理、编译、汇编。本篇博客将详细讲述预处理部分的有关知识点

1.预定义符号

在C语言中,C语言本身设置了⼀些预定义符号,可以直接使⽤,预定义符号的处理也是在预处理期间进行的。

在这里介绍几个常用的预定义符号:

__FILE__:当前编译的源文件名。

__DATE__:源文件被编译的日期。

__TIME__:源文件被编译的时间。

__LINE__:当前源代码的行号。

__STDC__:如果编译器遵循ANSI C标准,其值为1,否则为未定义标识符。

__cplusplus:如果编译C程序并且在C++环境中,其值为1,否则也为未定义标识符。

V S 2022 编译器就不遵循 A N S I   C 标准 : \color{red}{VS2022编译器就不遵循ANSI\ C标准:} VS2022编译器就不遵循ANSI C标准:
C语言预处理详解,c语言,开发语言,后端


预定义符号使用举例:

#include <stdio.h>

int main()
{
	printf("文件名:%s\n", __FILE__);
	printf("日期:%s\n", __DATE__);
	printf("时间:%s\n", __TIME__);
	printf("代码行号:%d\n", __LINE__);
	return 0;
}

打印结果:

C语言预处理详解,c语言,开发语言,后端

  • 这里文件名是将文件的整个路径都写下来。

预定义符号的使用__DATE____TIME__ 可以记录日志时使用 \color{red}{可以记录日志时使用} 可以记录日志时使用,在向文件中输入数据时写入日期和时间:

#include <stdio.h>

int main()
{
	FILE* fp = fopen("Diary.txt", "w"); // 向名为Diary的文本文件中写入
	fprintf(fp, "日志日期:%s\n", __DATE__);
	fprintf(fp, "日志时间:%s\n", __TIME__);
	fclose(fp);
	return 0;
}
  • 输入后的Diary.txt文件:

C语言预处理详解,c语言,开发语言,后端


2.#define 定义常量

基本语法

#define name stuff

一些常见定义:

#define MAX 1000
#define reg register //为 register这个关键字,创建⼀个简短的名字 
#define do_forever for( ; ; ) //用更形象的符号来替换⼀种实现 
#define CASE break;case //在写case语句的时候自动把 break写上。 
// 如果定义的 stuff过长,可以分成几行写,除了最后⼀行外,每行的后面都加⼀个反斜杠(续行符)。
// 因为按下回车键等于输入一个 \tab 字符 ,输入一个 反斜杠\ 相当于把回车抵消
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 							date:%s\ttime:%s\n",\
 							__FILE__,__LINE__, \
 							__DATE__,__TIME__) 

注意: \color{red}{注意:} 注意:在define定义标识符的时候,不要在最后加上分号;,有可能会导致出错

例如:

#define MAX 100

if(condition)
	max = MAX;
else
	max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

3.#define 定义宏

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

宏的声明方式

#define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在 stuff 中。

注意: \color{red}{注意:} 注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的
⼀部分。

强调: \color{red}{强调:} 强调:宏的参数及宏的表达式都要要加上括号,不然可能发生错误
例如:

#define SQUARE( x ) x * x

int main()
{
	printf("%d",SQARE(3 + 1));
	//宏替换之后就变成:printf("%d", 3 + 1 * 3 + 1);
	//最后得到的结果是7,不是我们预期的16
	//但如果宏改为 #define SQUARE( x ) ( x ) * ( x )
	//结果就是16
	return 0;
}

不仅参数要加上括号,宏表达式也要加上括号: \color{red}{不仅参数要加上括号,宏表达式也要加上括号:} 不仅参数要加上括号,宏表达式也要加上括号:
例如:

#define DOUBLE( x ) ( x ) + ( x )

int main()
{
	printf("%d",DOUBLE(3 + 1) * 2);
	//宏替换之后变成:printf("%d", (3 + 1) + (3 + 1) * 2);
	//最后得到的结果是12,不是我们预期的16
	//但是如果我们把宏表达式加上括号:#define DOUBLE( x ) (( x ) + ( x ))
	//宏替换之后就是 ((3 + 1) + (3 + 1)) * 2
	//结果就是预期的16
	return 0;
}

正确的宏定义形式

#define DOUBLE( x ) (( x ) + ( x )) //不该省的括号不要省

所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的
操作符或邻近操作符之间不可预料的相互作⽤。

4.带有副作用的宏参数

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果(参数带自++或- -改变参数的原值)。

例如:

x+1;  //  不带副作用
x++;  //  带有副作用 

副作用实例:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);

输出的结果是什么?
因为宏替换之后变成:

z = MAX( (x++) > (y++) ? (x++) : (y++) );

所以输出结果是:

x=6 y=10 z=9

我们只是想求出x和y中的最大值z,但是因为参数是带++的,导致x和y在运算过程中值被改变,结果z也不是预期的8,这不是我们想达到的效果,所以出现了副作用, 这种带 + + , − − 的参数就叫做带副作用的参数。 \color{red}{这种带++,- -的参数就叫做带副作用的参数。} 这种带++的参数就叫做带副作用的参数。

5.宏替换的规则

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

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。

  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。

  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

这里是指宏里面有#define 定义的其他常量的情况:

#define MAX 100
#define ADD(x) ((x) + MAX)
// 因为#define 定义的本质是在编译的预处理阶段将文本中的代码替换
// 所以这种包含#define定义的常量的宏,不能一遍就替换完成
// 因为在替换之前,编译器不知道宏里有MAX
...

int z = ADD(3);
//第一遍替换:int z = ((3) + MAX);
//第二遍替换:int z = ((3) + 100); 

注意: \color{red}{注意:} 注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。(因为本质是替换)
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

6.宏和函数的对比

宏通常被应⽤于执⾏简单的运算。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

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

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

原因有⼆:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多(函数的调用需要建立堆栈)。所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹。
  2. 更为重要的是函数的参数必须声明为特定的类型(只能传与形参类型一致的参数 例如 i n t   M a x ( i n t   a , i n t   b ) \color{blue}{例如int\ Max(int\ a,int\ b) } 例如int Max(int a,int b),这个函数只能传两个整型,也就是只能比较两个整型的大小,比较局限)。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。

宏也有一些比较明显的缺点: \color{crimson}{宏也有一些比较明显的缺点:} 宏也有一些比较明显的缺点:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。(如果一个宏定义的代码有100行,而程序中调用了100次,替换后就多了近10000行代码大大增加了代码的长度
  2. 宏是没法调试的。(调试时不会进入宏内部,而且宏替换的代码我们也看不到,只会在编译阶段的 test.i 文件中看到)
  3. 宏由于类型⽆关,也就不够严谨。(用整型和浮点型比较等)
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。(上文提到的忘加括号的情况)

宏有时候可以做函数做不到的事情: \color{crimson}{宏有时候可以做函数做不到的事情:} 宏有时候可以做函数做不到的事情:⽐如,宏的参数可以出现类型,但是函数做不到。

#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
 ...
 //使⽤ 
 int* tmp = MALLOC(10, int);//类型作为参数 
 
 //预处理器替换之后: 
 int* tmp = (int*)malloc(10 * sizeof(int));

宏和函数的一一对比:
C语言预处理详解,c语言,开发语言,后端


7.#和##运算符

7.1 #运算符

1.在了解#运算符之前,首先我们要知道一个知识点:两个字符串连在一起时会自动转化成一个字符串。

看下面的代码:

#include <stdio.h>

int main()
{
	printf("%s","Hello"" World");
	// 这里两个""靠在一起会转化成printf("%s","Hello World");
	return 0;
}

它的输出结果:
C语言预处理详解,c语言,开发语言,后端


2.然后我们再了解一下#运算符:#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。#运算符所执⾏的操作可以理解为”字符串化“。(即 #a 变为 “a”)

看下面这个例子理解一下:
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 就可以写:

#define PRINT(n) printf("the value of "#n" is %d", n);
...

 int a = 10;
 PRINT(a);

当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为:

printf("the value of ""a"" is %d", a);
// 就相当于printf("the value of a is %d", a);

运行代码就能在屏幕上打印:

C语言预处理详解,c语言,开发语言,后端


7.2 ##运算符

##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合。
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。

例如:

#define link(a,b) (a##b)

int main()
{
	int data_max = 10;
	printf("%d", link(data,_max));
	// 宏替换以后就是printf("%d",(data##_max));
	// ##再转换就是:printf("%d",data_max);
	// 结果就是10
	return 0;
}

输出结果:

C语言预处理详解,c语言,开发语言,后端

在实际开发过程中##使⽤的很少,所以很难取出⾮常贴切的例⼦。

8.命名约定

⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
那我们平时的⼀个习惯是:
把宏名全部⼤写
如:#define ADD(a,b) ((a) + (b))
函数名不要全部⼤写
如:int Add(int a, int b) { return a + b }

9.#undef

这条指令⽤于移除⼀个宏定义。

#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10.命令行定义

许多C的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)

#include <stdio.h>

int main()
{
	int array [ARRAY_SIZE];
	int i = 0;
	
	for(i = 0; i< ARRAY_SIZE; i ++)
	{
		array[i] = i;
	}
	
	for(i = 0; i< ARRAY_SIZE; i ++)
	{
		printf("%d " ,array[i]);
	}
	
	printf("\n" );
	return 0;
}

编译指令:

//linux 环境演⽰ 
gcc -D ARRAY_SIZE=10 programe.c

11.条件编译

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

⽐如说:
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

#include <stdio.h>
#define __DEBUG__ 
//当我们不需要打印观察数组内容时可以将这个定义注释掉
//下面的条件编译就不会执行,很方便

int main()
{
	int i = 0;
	int arr[10] = {0};
	
	for(i=0; i<10; i++)
	{
		arr[i] = i;
		#ifdef __DEBUG__ //如果定义了__DEBUG__则执行下面的语句
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。  
		#endif //__DEBUG__
	}
	
	return 0;
}

常⻅的条件编译指令:

1.
#if 常量表达式
	//...
#endif
//常量表达式由预处理器求值。 
如:
#define __DEBUG__ 1
#if __DEBUG__
	//..
#endif

2.多个分支的条件编译
#if 常量表达式
	//...
#elif 常量表达式
	//...
#else
	//...
#endif

3.判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

4.嵌套指令
#if defined(OS_UNIX)
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif

12.头文件的包含

12.1 头文件被包含的方式

1.本地文件包含

#include "filename.h"

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

Linux环境的标准头⽂件的路径:

/usr/include

VS环境的标准头⽂件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径 

注意按照⾃⼰的安装路径去找。


2.库文件包含

#include <filename.h>

查找策略: 查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。

  • 这样是不是可以说,对于库⽂件也可以使⽤ “ ” 的形式包含?
    • 答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。 (将include的头文件的内容拷贝替换到#include位置)

这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

下面举一个例子:

test.c

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
 return 0;
}

test.h

void test();

struct Stu
{
	int id;
	char name[20];
};

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。

  • 如果 t e s t . h 文件⽐较大,这样多次重复包含后代码量会剧增。 \color{red}{如果test.h文件⽐较大,这样多次重复包含后代码量会剧增。} 如果test.h文件较大,这样多次重复包含后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。

如何解决头⽂件被重复引⼊的问题?答案:条件编译

每个头⽂件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容 
//例如 #include <stdio.h>
#endif //__TEST_H__
// 这段代码的意思是如果没定义 __TEST_H__ 则 定义__TEST_H__ 和下面的头文件内容
// 如果定义了__TEST_H__就不会再重复定义

或者写:

#pragma once

就可以避免头⽂件的重复引⼊。

13.其他预处理指令

#error
#pragma
#line
...
不做介绍,自行了解
#pragma pack()在博客结构体部分已介绍。

例题 \color{maroon}{例题} 例题
下面哪个不是宏和函数的区别?( )
A.函数可以递归,宏不能递归
B.函数参数有类型检查,宏参数无类型检查
C.函数的执行速度更快,宏的执行速度慢
D.由于宏是通过替换完成的,所以操作符的优先级会影响宏的求值,应该尽量使用括号明确优先级

答案:C

解析:宏不存在执行速度,它是查找替换,选C。A中宏是查找替换,无法设定递归跳出条件,自然无法递归。B中宏是查找替换,都没有执行,类型更是无从谈起。D中直接说了宏的本质。所以只要知道了宏是查找替换,其他问题也就不是问题了。文章来源地址https://www.toymoban.com/news/detail-852240.html

到了这里,关于C语言预处理详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 详解C语言—预处理

    目录 一、预处理 1、预定义符号介绍 2、预处理指令 #define #define 定义标识符:  #define 定义宏: #define 替换规则 3、预处理操作符# 4、预处理操作符## 5、带副作用的宏参数 6、宏和函数对比 二、命名约定 三、预处理指令 #undef 四、命令行定义 五、条件编译  1、单分支#if:

    2024年02月08日
    浏览(43)
  • 【C语言:编译、预处理详解】

    我们都知道,一个程序如果想运行起来要经过编译、链接然后才能生成.exe的文件。 编译⼜可以分解为三个过程: 预处理(有些书也叫预编译)、 编译 汇编 预处理阶段 主要处理那些源文件中以#开始的预编译指令。比如:#include,#define,处理的规则如下: 删除所有的注释

    2024年02月03日
    浏览(37)
  • 【C语言进阶】预处理详解

    对预处理的相关知识进行详细的介绍                  ✨  猪巴戒 :个人主页✨                 所属专栏 :《C语言进阶》         🎈 跟着猪巴戒 ,一起学习C语言🎈 目录 引言 预定义符号 #define定义常量 #define定义宏 带有副作用的宏参数 宏替换的规则 宏函数的

    2024年01月23日
    浏览(33)
  • 【C语言基础】:预处理详解(一)

    一、预定义符号 在C语言中设置了许多的预定义符号,这些预定义符号是可以直接使用的,预定义符号也是在预处理阶段进行处理的。 常见的预定义符号 : 【示例】 : 我们在VS上使用 _ _ STDC _ _ 会发现显示未定义,这也就说明VS的编译器是不完全遵循 ANSI C 的,为了展示效果

    2024年04月22日
    浏览(26)
  • 【c语言】详解c语言#预处理期过程 | 宏定义前言

    c语言系列专栏: c语言之路重点知识整合   创作不易,本篇文章如果帮助到了你,还请点赞支持一下♡𖥦)!!  主页专栏有更多知识,如有疑问欢迎大家指正讨论,共同进步! 给大家跳段街舞感谢支持!ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ 代码编译到执

    2024年02月01日
    浏览(34)
  • 【C语言】预处理详解:#define的各种使用方法

    目录 1.#define定义标识符 1.1赋值 1.2   定义 1.3用更形象的符号来替换一种实现 1.4   加续行符换行 1.5#define定义宏 1.6  #define替换的规则 注意事项 2.#和## 3.带有副作用的宏参数 4.函数和宏的对比 #define定义标识符的用法非常简单 name可以由自己来命名,尽量取一些有意义

    2024年02月15日
    浏览(26)
  • 初始C语言最后一章《编译、链接与预处理详解》

    感谢老铁们的陪伴和支持,初始C语言专栏在本章内容也是要结束了,这创作一路下来也是很不容易,如果大家对 Java 后端开发感兴趣,欢迎各位老铁来我的Java专栏!当然了,我也会更新几章C语言实现简单的数据结构!不过由于我是Java 技术栈的,所以如果以后有机会学习C

    2024年04月16日
    浏览(25)
  • C语言中程序的编译(预处理操作)+链接详解(详细介绍程序预编译过程)

    今天我们来学习C语言中程序的编译和链接是如何进行的。 在ANSI C的任何一种实现中,存在两个不同的环境。 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境,它用于实际执行代码。 本文主要是介绍预编译阶段的相关知识。 1.组成一个程

    2023年04月09日
    浏览(24)
  • C语言之预处理命令使用详解----#if、#endif、#undef、#ifdef、#else、#elif

    查了好久才知道的这个原理,记录一下吧! 参考教程 预处理命令 在接触#if、#undef这类预处理指令前,大部分都都接触过#define、#include等预处理命令,通俗来讲预处理命令的作用就是在编译和链接之前,对源文件进行一些文本方面的操作,比如文本替换、文件包含、删除部分

    2024年02月02日
    浏览(29)
  • C语言——程序环境和预处理(再也不用担心会忘记预处理的知识)

    先简单了解一下程序环境,然后详细总结翻译环境里的编译和链接,然后在总结编译预处理。 在 ANSI C 的任何一种实现中,存在两个不同的环境 翻译环境:这个环境中源代码被转换为可执行的机器指令。 执行环境:执行二进制代码。 计算机如何执行二进制指令? 我们写的C语

    2024年02月09日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包