C语言预处理详解(预编译)-程序员宅基地

技术标签: c++  c语言  开发语言  

目录

1.预定义符号

2.#define

        2.1#define定义的标识符

        2.2#define定义的宏

        2.3#define替换规则

        2.4#和##

        2.5带副作用的宏参数

        2.6宏和函数的对比

3.#undef

4.命令行定义

5.条件编译

6.头文件的包含

        6.1头文件的包含方式

        6.2嵌套文件包含

7.其他预处理指令

1.预定义符号

        主要有:__FILE__,__LINE__,__TIME__,__DATE__,__STDC__等。这些预定义符号都是语言内置的。

举个例子:

#include<stdio.h>
int main()
{
	int i = 0;
	FILE*pf=fopen("a.txt", "w");//打开文件
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	for (i = 0; i < 10; i++)//写文件
	{
		fprintf(pf,"file:%s line:%d data:%s time:%s %d\n",__FILE__,__LINE__,__DATE__,__TIME__, i);
	}
	fclose(pf);//关闭文件
	pf = NULL;
	return 0;
}

__STDC__比较特殊,打印它,如果是1,则说明这个编译器遵循ANSIC,否则不遵循。 

2.#define

        2.1#define定义的标识符

           #define可以定义标识符:语法:define name stuff

比如:

#define MAX 100
#define reg register
#define do_foever for(;;)
//如果定义的stuff太长的话,可以写成几行除了最后一行外,每一行后面都要加反斜杠(续航符)
#define DOUBLE_PRINT printf("file :%s\tline :%d\t\
							data :%s\ttime :%s\n \
							",__FILE__,__LINE__,__DATA__,__TIME__);
int main()
{
	int a = MAX;
	return 0;
}

     如果定义的stuff太长的话,可以写成几行除了最后一行外,每一行后面都要加反斜杠(续航符)

     有一种奇怪的写法:#define CASE break;case
 

#define CASE break;case
int main()
{
	int n = 0;
	switch (n) 
	{
	case 0:
	CASE 1:
	CASE 2:
	CASE 3:
	}
	return 0;
}

        这样就不用再每个case语句后面写break。

        注意在#define 定义的标识符后面最好不要加分号,不然会出现一些问题。如:

#include<stdio.h>
#define MAX 1000;
int main()
{
	printf("%d ", MAX);
	return 0;
}

        2.2#define定义的宏

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

#define name(parament) stuff

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

注意:

参数列表的左边必须与name紧紧相邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

如:#define SQUARE(  x )  x*x

上面的这个宏正确定义如果你使用它,并且 给它传参如下:

SQUARE(5);

它就会被替换为5*5;

但是这个宏是由一定的缺陷的,如果在调用的时候给它传参:

SQUARE(1+5); 

那么这个时候这个宏计算的结果就不是6*6,经过替换变为:1+5*1+5 ;那么这个时候这个宏是有一定问题的所以我们在定义宏的时候一定要在宏的定义的适当位置加上括号。上面这个宏可以修改为:

  

#define SQUARE(x)  (x)*(x)

下面还有一个宏定义:

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

定义中我们使用了括号为了避免前面的问题,但是这个宏可能出现新的错误。

#define DOUBLE(x) (x) + (x)
int main()
{
	int a = 10;
	printf("%d ", 10*DOUBLE(a));
	return 0;
}

这里的计算结果是什么呢?200吗,看上去打印的是200,实际上是110,事实上我们发现替换以后 :

printf("%d ",10*10+10);

乘法的运算级高于加法所以最终结果是110,这个问题的解决方法是在宏定义的表达式的两边加上括号就可以解决,如下:

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

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

        2.3#define替换规则

        在程序中扩展#define定义的宏和符号时,应涉及几个步骤:

        1.在调用宏时首先要对参数进行检查,看看是否包含由#define定义的的符号,如果是,首先要替换它们。

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

        3.最后,再次对结果文本进行扫描 ,看看是否有#define定义的符号,如果有就重复上述过程。

注意:

1.宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归。 

2.当预处理器搜索#define定义的符号时,字符串常量中的内容并不被搜索。

        2.4#和##

        如何将参数插入到字符串中?这时候就需要用到#

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

#include<stdio.h>
int main()
{
	char* p = "hello""world";
	printf("hello""world\n");
	return 0;
}

        这里输出的是不是:hello world?答案是确定的是。

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

        那么我们就可以写出这样的代码,同时打印变量的名字和值,如下:

#define PRINT(VALUE) printf("the value of " #VALUE " is %d \n",VALUE)
int main()
{
	int a = 10;
	PRINT(a);
	//printf("the value of" "a" "is %d \n", a);
	return 0;
}
#define PRINT(FORMAT,VALUE) printf("the value of"#VALUE  " is " FORMAT " \n",VALUE)
int main()
{
	int a = 10;
	PRINT("%d",a+3);
	return 0;
}

 ##是用来连接字符串的。例如:

#define ADD_T(NUM,VALUE)  NUM##VALUE
int main()
{
    int a = 100;
    int b = 10;
    printf("%d ",ADD_T(10,100));
    return 0;
}

这个代码的意思是将100和10连接起来然后打印。 

        2.5带副作用的宏参数

        有些参数是出了自己本来的作用还会带有其他的作用,例如:

#define MAX(a,b) a>b?a:b;
int main()
{
	int a = 5;
	int b = 7;
	int max = MAX(a++, b++);

	return 0;
}

上面这个代码中的变量a和变量b最终会变成多少呢,max是多少呢?如图

 

为什么会产生这样的结果呢,因为我们传入的变量a和变量b都是带有副作用的因此会产生不可预测的结果。为了避免这样的结果产生我们使用宏的时候应该尽量不要使用带副作用的参数。  

        2.6宏和函数的对比

        宏通常被用来执行简单的命令,比如在两个数中找较大的数。那为什么不用函数来完成呢?

原因有两个:

1是宏的执行效率比函数高,用于调用函数和从函数返回的代码可能比这个执行小型工作的时间要长。

2.宏的参数没有类型识别。函数的参数必须声明为具体类型,所以函数只能在类型合适的表达式中使用。反之如果是宏的话可以使用多种不同的类型。

当然宏也有许多的劣势:

1.比如,如果宏体很长,每次调用宏就会向函数中插入很长的代码,可能会大幅度增加 程序的长度。

2.宏是没办法调试的。

3.因为宏的参数没有类型检查,所以宏也是不够严谨的。

4.宏可能带来运算符优先级的问题,导致程序容易出错。

宏也可以做到函数做不到的事情,比如宏的参数可以出现类型,但是函数做不到。

宏和函数的对比:

宏和函数的对比
属性 函数
代码长度 如果宏体过长会大大增加代码的长度 函数代码只在定义的时候出现一次,每次使用函数都是调用同一份代码
执行速度 存在函数调用和函数返回的时间开销所以会慢一些
操作符优先级 操作符的优先级可能会使宏产生错误的结果,所以建议宏在书写的时候多加括号 函数传参只会在函数调用的时候求值一次,它的结果会传给函数的形参,表达式的更容易预测
带有副作用的参数 参数可能被替换到宏体中的多个位置,因此如果参数带副作用,会产生不可预料的结果 函数的参数如果带副作用只会在传参的时候产生影响,容易预测
参数类型 宏的参数可以是变量,常量,或者类型,只有对参数的操作是合法的,那么宏可以使用任何类型 函数的参数与类型有关,如果参数的类型不同就需要不同的函数,即使他们的任务是相同的
调试 宏是不方便调试的 函数可以逐语句调试
递归 宏是不可以递归的 函数是可以递归的

命名约定:因为宏和函数不易区分,所以我们一般书写宏名时是全部大写的,书写函数名不会全部大写,这样就可以区分函数和宏。

3.#undef

 它是用来移除一个宏定义的。例如:

#define MAX 100
int main()
{
	int a = MAX;
#undef MAX
	int b = MAX;
	return 0;
}

 这时候定义变量b,并给b赋值MAX就会报错:

4.命令行定义

 许多c编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:有一个代码需要在不同的平台和机器上运行,这段代码中有一个数组,需要这个数组的大小根据不同的平台适配,有时候我们需要这个数组足够大,有时候我们又需要这个数组很小,如下:

#include<stdio.h>
int main()
{
	int arr[SZ];
	int i = 0;
	for (i = 0; i < SZ; i++)
	{
		arr[i] = i;
	}
	for (i = 0; i < SZ; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

这时候我们就需要用到命令行定义了。gcc -D  SZ=10 programe.c

5.条件编译

在编译一个程序的时候我们想要将一条语句(或一组语句)编译 或者放弃编译是很方便的,因为我们有条件编译。比如:调试性代码,删除可惜,保留又碍事这时候我们就能用到条件编译了。

#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__
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

常见的条件编译指令:

//单分支条件编译,常量表达式由预处理器求值

#if 常量表达式

#endif

//多分支条件编译

#if

#elif

#else

#endif 

举个栗子: 

int main()
{
#if 3==2
	printf("hehe\n");
#endif
#if 2 != 1
	printf("哈哈\n");
#elif 2==3
	printf("hello world\n");
#else
	printf("9999\n");
#endif
	return 0;
}

//判断是否被定义

#if defined(symbol) 

#endif symbol

//嵌套定义

#if defined(keep)

        #ifdef OPTION1

        //指向指令

        #endif

        #ifdef OPTION2

        //指向命令

        #endif

elif defined(OS)

        #ifdef OPTION2

        //执行命令

        #endif

#endif

例如:

#define TYPE 100
int main()
{
#if !defined(TYPE)//判断是否被定义
	printf("hehe ");
#endif TYPE
#ifdef TYPE//判断是否被定义
	printf("hello world\n");
#endif
	return 0;
}

6.头文件的包含

        6.1头文件的包含方式

        头文件的包含方式有两种: 

#include<>和#include" "

两种包含方式的不同之处在于,它们的查找策略不同,#include<>会去系统库所在的文件目录下查找,#include" "会现在所在的源文件的目录下查找,如果该头文件未找到,编译器就会像查找库函数一样再标准位置查找头文件(再去系统库所在的文件下查找)。

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

        6.2嵌套文件包含

        如果出现这样的场景

comm.h是公共模块。test1.h和test1.c使用了公共模块,test2.h和test2.c使用了公共模块,test.h 和test.c使用了test1模块和test2模块,这样最终程序会出现两分comm.h。这样就造成了文件内容的重复。如何解决这样的问题?

        条件编译就可以解决。

        在每个头文件的开头写

        #pragma once 

        或者:#ifndef __TEST_H__

        #define   __TEST_H__

        #endif // __TEST_H__

7.其他预处理指令

#error
#pragma
#line

        大家有兴趣可以自己了解一下,这些预处理指令不是很常见。 所以没有在这里介绍。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_68641696/article/details/130475664

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan