C语言最终讲:预处理详解

news2024/11/27 8:31:36

C语言最终讲:预处理详解

  • 1.预定义符号
  • 2.#define定义常量
  • 3.#define定义宏
  • 4.带有副作用的宏参数
  • 5.宏替换的规则
  • 6.宏和函数的对比
    • 6.1宏的优势
      • 6.1.1\符号
    • 6.2宏的劣势
  • 7.#和##
    • 7.1#运算符
    • 7.2##运算符
  • 8.命名约定
  • 9.#undef
  • 10.命令行定义
  • 11.条件编译
  • 12.头文件的包含
    • 12.1本地文件包含
    • 12.2库文件包含
    • 12.3嵌套文件包含
  • 13.其他预处理指令

结语:这一讲是C语言基础知识的最后一讲了,后续将会学习数据结构相关的知识,坚持不易,希望各位都能坚持在自己所干的事情上,我们共勉

这一讲讲的是预处理相关的内容,上一讲虽然我们已经了解了一些关于#define相关的知识,这一讲我们讲详细阐述它的作用和缺陷

1.预定义符号

C语言中有着一些预定义符号,可以直接使用,是在预处理阶段进行处理的:

__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

它们的使用方法为:

int main()
{
	printf("%s\n", __FILE__);//打印当前文件所在的位置
	printf("%d\n", __LINE__);//打印行号
	printf("%s\n", __DATE__);//打印日期
	printf("%s\n", __TIME__);//打印时间
	//printf("%d\n", __STDC__);//err,表明VS并不完全遵循ANSI C标准

	return 0;
}

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

2.#define定义常量

我们知道,define定义的常量是直接替换的,所以有着下面的几处用法:

#define TEST 20 //方法1,直接定义常量
#define I int//方法2:可以为关键字替换成一个更简便的名字
#define CASE break;case//方法3:这是一种很“奇葩”的用法,建议还是别用了

int main()
{
	//使用起来也很方便,方法1:
	int a = TEST;//直接使用即可

	//方法2:
	I b = 20;
	printf("%d\n", b);

	//方法3:
	int input = 1;
	scanf("%d", &input);
	switch (input)
	{
	case 1:
	CASE 2 ://这样后面就不用加break了
	CASE 3 :
		break;
	}
	return 0;
}

对于加;问题,解答如下:

#define ROW 20;//假设#define定义时加上了;

int main()
{
	int a = ROW;//这是a就会被替换成int a = 20;;这是后面就会有两个;,特殊情况下会出现错误
	//总结:加上;可能会出现错误,不加;肯定不会出现错误,所以还是不要加上;

	return 0;
}

3.#define定义宏

定义方式:

#define name( parament-list ) stuff
name为宏名称
parament-list是由逗号隔开的符号表,可能出现在stuff中
stuff可以是一个计算方式,也可以是一个指令等等
//需要注意的是,符号表的左括号必须要和name紧密相连,否则括号里的内容也会被当成是stuff中的一部分

使用实例:

#define MUL(x) x*x//定义了一个宏,实现x的平方

int main()
{
	int a = 5;
	int b = MUL(a);

	printf("b = %d\n", b);
	//结果为25
	return 0;
}

看起来使用起来很好用,但是我们也要注意符号的优先级问题,如下:

#define ADD(x) x+x

int main()
{
	int a = 5;
	int b = ADD(a) * ADD(a);
	printf("%d\n", b);
	//我们可能会误以为结果为10 * 10,这不就是100吗,但是并非如此
	//b会被替换成int b = 5+ 5*5 +5,根据符号优先级问题,结果为35
	return 0;
}

知道错误就要改正,所以改正方法如下:

#define ADD(x) ((x)+(x)) //在x上加一个括号,因为x为表达式,如果传入5+5,也要被括起来
               //总体上再加上一个括号,是为了将它们算成一个整体

int main()
{
	int a = 5;
	int b = ADD(a) * ADD(a);
	printf("%d\n", b);
	//这样才能计算出一个整体的值,此时结果为100
	return 0;
}

所以,对于宏的定义,我们不能吝惜括号

4.带有副作用的宏参数

什么是带有副作用呢,其实就是表达式求值会改变原来的参数的值,如下:

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

为了验证此写法的危害性,我们举例来说明:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	//这里会被替换成((x++)>(y++) ? (x++):(y++))
	//先看问号左边,后置++,先使用后++,5>8为假,所以看的是y++的值,还是先使用后++,但是要注意的是
	//由于++,此时y的值已经变成了9,所以z被赋值为9,y再++,值位10
	//而x只++了一次,因为第二次只对y进行了++,所以x的值为6

	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
	//所以输出的结果应该为6 10 9
	return 0;
}

5.宏替换的规则

1.在调用宏时,首先看参数中是否包含由#define定义的符号,如果有,它们首先被替换:

#define R 2
#define RR R+2
//其中RR中包含了一个常量的定义R,所以R先被替换成2,再计算RR

2.替换文本随后会被插入到原来文本所在的位置
3.最后还会对文件进行扫描,看是否仍然存在#define定义的符号,如果有,继续进行上面的两个步骤

对于宏,需要注意的是,它并不能出现递归

6.宏和函数的对比

上面我们了解了宏,那么既然宏这么好用,那我们是不是就能够将宏代替为函数使用呢,肯定是不能的,下面我们来分析宏相较于函数的利与弊

6.1宏的优势

1.对于简单的任务,宏有着天然的优势:

#define MAX(a, b) ((a)>(b)?(a):(b))
1.首先,宏的参数是类型无关的,也就是说,宏参数能够接受任意类型的值,在特定条件下是很好用的
2.其次,宏在使用过程中也是一个简单的替换,函数不同,函数在使用前后会产生空间的创建和销毁,使用效率较慢

2.相较于函数,宏有着函数不能够实现的情况:

//如果我们要使用函数进行空间的开辟:
int* Mal(int a, int size)
{
	//我们首先会将返回值和参数确定,也就是说,它们的类型都是固定的,用于整形开辟的函数就不能用于浮点型类型的开辟
	return (int*)malloc(a * sizeof(int));
}

//但是当我们使用宏时:
#define MAL(num, type)  \
        (type*)malloc(num * sizeof(type))
int main()
{
	//使用函数开辟空间
	int* pa = Mal(10, sizeof(int));//假设我们要开辟10个整形的空间

	//使用宏开辟空间
	int* pb = MAL(10, int);
	//1.我们可以将int直接作为参数传入
	//2.此时宏的使用包含的情况更多,更好用
	return 0;
}

6.1.1\符号

上面我们使用了一个\符号,我们看看这是怎么个事:

#define MAL(num, type)  \
        (type*)malloc(num * sizeof(type))
//这个宏定义中使用了\符号,它是一个换行符,当宏里的内容过多时,就可以在后边加上一个\符号进行换行
//但是要注意的是,\后边什么也不能有,包括空格
//可以理解为:一个\抵消了一个\n,所以后边不能跟任何东西

6.2宏的劣势

1.每次使用宏时,一份宏代码就会被插入到程序中,如果宏很长的话,会大幅增加程序的长度
2.宏在预处理阶段就被替换,不能够被调试
3.宏由于类型无关,所以也容易出现问题
4.宏可能造成运算符优先级的问题

对于宏和函数的一个对比,我们可以看图:
图不是很好,大家对付着看
在这里插入图片描述

7.#和##

7.1#运算符

#所实现的操作被称为“字符串化”,作用是将宏的一个参数转换成字符串变量:

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

int main()
{
	int a = 10;

	PRINT(a);
	//此时会打印出:The value of a is 10
	//预处理阶段,代码会被替换成:printf("the value of ""a" " is %d", a);
	//也就是说,#n就表示"n",对于printf,多个""会被合并成一个
	return 0;
}

7.2##运算符

该运算符被称为记号粘合,可以将两边的符号合并成一个符号

使用方法如下:

//当我们要求两个数的最大值时:
//使用函数完成较为繁琐,因为不同类型需要分别进行处理:
int int_max(int x, int y)
{
	return x > y ? x : y;
}

float float_max(float x, float y)
{
	return x > y ? x:y;
}

//我们可以使用宏来实现:
#define MAX(type)                      \
		type type##_max(type x, type y)\
		{                              \
			return x > y ? x : y;      \
		}

MAX(int);//这时我们可以依靠宏来创建一个函数,函数名为int_max

int main()
{
	//使用方法也比较简单:
	int ret1 = int_max(2, 3);//我们直接使用创建好的函数即可
	printf("%d\n", ret1);
	return 0;
}

8.命名约定

在命名宏和函数时,有着一个不成文的约定:

1.把宏名全部大写
2.函数名不要全部大写

9.#undef

用于移除一个宏定义,使用方法如下:

#define A 20

int main()
{
	int a = A;
	printf("%d\n", a);//20

#undef A
	int b = A;//err
	return 0;
}

10.命令行定义

在不同的场景下,我们定义的数组长度需求可能不同,所以许多C的编译器提供了一种命令行定义的能力,允许在命令行中,也就是使用前对一些变量进行赋值

例子:

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
这里表示给ARRAY_SIZE赋值为10,然后再执行程序

11.条件编译

对于一些为了调试而使用的代码,删除了可惜,保留了又费事,所以我们可以通过条件编译来选择性编译

常见的条件编译指令:

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

总结:

1.#if#endif是一体的,它们两个必须同时使用
2.#if#elif就相当于if else
3.#if defined(symbol)表示如果定义了symbol,也可以写成:#ifdef symbol
4.#if !defined(symbol)表示如果没有定义symbol,也可以写成:#ifndef symbol

它的原理为:

//使用原理为:
//当条件为真时,保留代码
//条件为假时,删除代码
int main()
{
#if 1
	printf("haha\n");//条件为真,保留代码
#elif 0
	printf("hehe\n");//条件为假,删除代码
#endif
	return 0;
}
//所以上面的代码在预处理之后就变成了:
int main()
{
	printf("haha\n");//条件为真,保留代码
	return 0;
}

12.头文件的包含

头文件有两种包含方式:本地文件包含和库文件包含,它们的包含方式是什么呢,我们来看:

12.1本地文件包含

#include "filename"
使用“”来包含

查找策略:

1.现在源文件目录下查找,也就是这个路径:

在这里插入图片描述

2.如果没有找到,在库函数头文件中进行查找
3.如果找不到,提示编译错误

Linux环境下标准头文件的路径:

/usr/include

VS环境下(现在VS环境下头文件的路径应该会有差异,这里只当成一个代表):

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

12.2库文件包含

直接去标准路径下查找,如果没有找到就提示编译错误

那么我们就要问了,是不是库文件的包含也可以使用""呢,原理上来说可以,但是缺点也很明显:
1.效率低
2.这样就不能够分辨出哪个是包含的是标准库文件,哪个是包含本地文件

12.3嵌套文件包含

当我们使用#include包含多个头文件时,会发生什么呢:

在这里插入图片描述
在预处理阶段,竟然会有5个头文件被写入了.c文件中,因为头文件的包含其实就是将头文件的内容写入.c文件中,情况如下:

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

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{

	return 0;
}

为了避免这种情况,所以我们用到了:条件编译,方法如下:

//头文件的改进如下:
//方法1:条件编译
#ifndef __TEST_H__
#define __TEST_H__//也就是说,如果没有__TEST_H__的话,定义一个__TEST_H__,这样后续就有了__TEST_H__,就不会再包含这个头文件了
struct Stu
{
	int id;
	char name[20];
};
#endif

//方法2:
#pragma once
//这是一个预处理指令,用于告诉编译器该头文件之应该被包含一次,通常推荐放在头文件开头

13.其他预处理指令

#error
#pragma
#line
...
不做介绍,⾃⼰去了解

可以参考《C语言深度解剖》一书进行学习

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1808519.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

超详细 | 使用Nexus搭建私服 (带代码演示)

为什么需要搭建私有仓库&#xff1f; 在企业开发的过程中&#xff0c;不是所有公司都能直接访问外网。在这种情况下&#xff0c;就需要在局域网内找一台有外网访问权限的服务器&#xff0c;搭建Nexus私服仓库&#xff0c;开发人员连接到这台私服上&#xff0c;通过搭建的Nexus…

【学习笔记】finalshell上传文件夹、上传文件失败或速度为0

出现标题所述的情况&#xff0c;大概率是finalshell上传文件的过程中的权限不够。 可参照&#xff1a;Finalshell上传文件失败或者进度总为百分之零解决方法 如果不成功&#xff0c;建议关闭客户端重试。 同时建议在设置finalshell的ssh连接时根据不同用户设置多个连接&#xf…

RAG vs Fine-Tuning 微调哪种大模型(LLM)技术更好?

数据科学和机器学习的研究人员和从业者都在不断探索创新策略来增强语言模型的能力。在众多方法中&#xff0c;出现了两种突出的技术&#xff0c;即检索增强生成 (RAG)和微调。本文旨在探讨模型性能的重要性以及 RAG 和微调策略的比较分析。 模型性能在 NLP 中的重要性 增强用…

LinkedList详解(含数据结构动画演示)

目录 LinkedList详解1、LinkedList的继承体系2、LinkedList的构造函数3、LinkedList的add(E e)方法4、LinkedList的Node节点5、双向链表的概念和Node节点的详细解释6、LinkedList的add(E e)方法梳理7、LinkedList的getXXX方法8、LinkedList的removeXXX方法①、removeFirst()方法…

vue中插槽的本质

定义slotCompoent.vue 组件 <template><slot></slot><slot nameslot1></slot><slot name"slot2" msg"hello"></slot> </template>使用组件&#xff1a; <slotComponent><p>默认的</p>…

电脑上的瑞士军刀

一、简介 1、一款专为 Windows 操作系统设计的桌面管理工具&#xff0c;它具备保存和恢复桌面图标位置的功能&#xff0c;使用户能够在各种情况下&#xff0c;如分辨率变动、系统更新或其他原因导致的图标位置混乱后&#xff0c;快速恢复到熟悉的工作环境。它还拥有诸多实用功能…

1.nginx介绍

介绍 是一个高性能的http和反向代理服务器。 特点 占用内存少&#xff0c;并发能力强。 nginx专为性能优化而开发&#xff0c;性能是其最重要的考量&#xff0c;实现上非常注重效率&#xff0c;能经受高负载的考验&#xff0c;有报告表明能支持高达50,000个并发连接数。 基…

李廉洋:6.10黄金原油非农之后,今日行情走势分析策略。

黄金消息面分析&#xff1a;即美联储不会在短期内以降息的方式出手纾困。该报告确实将首次降息的可能性推迟到了12月&#xff0c;但股市的反应不像多数交易商预期的那样。股市非但没有崩盘&#xff0c;反而随着交易员逢低买入而大幅反弹。很明显&#xff0c;市场完全专注于盈利…

基于STM32F030设计的多点温度采集系统(BC26+OneNet)

一、项目背景 随着物联网技术的迅猛发展&#xff0c;越来越多的智能设备应运而生&#xff0c;而温度采集系统是其中重要的一类。在现代工业和家庭生活中&#xff0c;温度对于生产、居住和储存等过程的控制有着非常重要的作用。因此&#xff0c;准确地采集环境温度数据并进行处…

网络安全领域六大顶级会议介绍:含会议介绍、会议地址及会议时间和截稿日期

引言&#xff1a; 从事网络安全工作&#xff0c;以下六个顶会必须要知道&#xff0c;很多安全的前沿技术都会在如下会议中产生与公开&#xff0c;如下会议发表论文大部分可以公开下载。这些会议不仅是学术研究人员展示最新研究成果的平台&#xff0c;也是行业专家进行面对面交流…

vscode copilot git commit 生成效果太差,用其他模型替换

问题 众所周知&#xff0c;copilot git commit 就像在随机生成 git commit 这种较为复杂的内容还是交给大模型做比较合适 方法 刚好&#xff0c;gitlens 最近开发了 AI commit的功能&#xff0c;其提供配置url api可以实现自定义模型 gitlens 只有3种模型可用&#xff1a…

SpringCloud-面试篇(二十四)

&#xff08;1&#xff09;Nacos如何支撑数十万服务注册的压力 小型企业来讲nacos压力没有那么大&#xff0c;但是想阿里&#xff0c;服务的数量可能会达到数万&#xff0c;那麽多的服务。当服务原来越多时&#xff0c;除了服务注册以外&#xff0c;还有服务的定时更新&#x…

【数据分享】《中国投资领域统计年鉴》1950-2022(中国固定资产投资统计年鉴)

​最近老有同学过来询问《中国投资领域统计年鉴》、《中国固定资产投资统计年鉴》这两本年年鉴的关系以及怎么获取这两本本年鉴。今天就在这里给大家分享一下这两本年鉴的具体情况。 《中国投资领域统计年鉴》是一部全面反映中国固定资产投资情况的权威资料。本书收集了全国、…

张霖浩在娱乐“名利场”玩出“修罗场”的贵族范儿

众所周知娱乐圈是个大型“名利场”&#xff01;近日&#xff0c;2025年北京广播电视台春晚发布会现场&#xff0c;众大咖汇聚&#xff0c;妆容、装扮、穿搭&#xff0c;更是争奇斗艳、八仙过海各显神通。同时&#xff0c;也揭露出娱乐圈当下穿搭界”修罗场”的残酷现实。在出彩…

刷代码随想录有感(99):动态规划——使用最小花费爬楼梯

题干&#xff1a; 代码&#xff1a; class Solution { public:int minCostClimbingStairs(vector<int>& cost) {vector<int>dp(cost.size() 1);dp[0] 0;dp[1] 0;for(int i 2; i < cost.size(); i){dp[i] min(dp[i - 1] cost[i - 1], dp[i - 2] cost…

MySQL快速入门(极简)

SQL 介绍及 MySQL 安装 一、实验简介 本课程为实验楼提供的 MySQL 实验教程&#xff0c;所有的步骤都在实验楼在线实验环境中完成&#xff0c;学习中请按照实验步骤依次操作。 本课程为 SQL 基本语法及 MySQL 基本操作的实验&#xff0c;理论内容较少&#xff0c;动手实践多…

C# BindingSource 未完BindingNavigator

数据绑定导航事件数据验证自定义示例示例总结 在 C#中&#xff0c; BindingSource 是一个非常有用的控件&#xff0c;它提供了数据绑定的基础设施。 BindingSource 允许开发者将数据源&#xff08;如数据库、集合、对象等&#xff09;与用户界面控件&#xff08;如文本框、下…

Fedora的远程桌面

要在 Fedora 40 上开启远程桌面功能。 首先&#xff0c;要确保已安装 gnome-remote-desktop 和 vino 包。 这些软件包通常默认安装在 Fedora 的 GNOME 桌面环境中。 可以按照以下步骤操作&#xff1a; 1、判断电脑是否安装了 gnome-remote-desktop 和 vino 包: tomfedora:…

51单片机独立按键控制LED灯,按键按一次亮,再按一次灭

1、功能描述 独立按键控制LED灯&#xff0c;按键按一次亮&#xff0c;再按一次灭 2、实验原理 轻触按键:相当于是一种电子开关&#xff0c;按下时开关接通&#xff0c;松开时开关断开&#xff0c;实现原理是通过轻触按键内部的金属弹片受力弹动米实现接通和断开&#xff1b;…

demo xshell (程序替换 工作目录 内建命令)

1.程序替换 在学习完一些列的进程替换接口之后我们大概就能知道&#xff0c;我们的环境变量以及命令行参数是如何传递给子进程的&#xff0c;这些参数是我们在调用进程替换时就传给了子进程的数据。 那么如果我们自己要实现一个简单的命令行解释器&#xff0c;我们是不是首先…