C语言 | 预处理知识详解 #预处理指令有哪些?他们如何使用?宏和函数有哪些区别?...#

news2024/12/26 15:19:57

在这里插入图片描述
在这里插入图片描述

文章目录

  • 前言
  • 预定义符号介绍
  • 预处理指令#define
    • #define替换规则
  • 预处理指令 #undef
  • 宏和函数的对比
    • 宏和函数的对比图
    • 命名约定
  • 命令行定义
  • 条件编译
  • 预处理指令 #include
    • 嵌套文件包含
  • 其他预处理指令
  • 写在最后

前言


上篇文章介绍了一个程序运行的 编译与链接 ,其中编译阶段有个预处理,他会对一些预处理指令进行处理,本章就对这些预处理相关的指令,操作符等等进行探讨。


预定义符号介绍

这里介绍一些可能会常用到的符号:

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

这些预定义符号都是语言内置的,都已经为其设定了特有的值,下面来看看个别的值是啥呢:

#include <stdio.h>

int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);

	// 下面的_STDC_在vs上是未定义的,编译就会报错
	//printf("%d\n", __STDC__); 

	return 0;
}

在这里插入图片描述
有了这些预定义符号,我们可以随时随地的知道此时的时间和文件所在位置啦。

预处理指令#define

在C或C++语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。

  • #define#include 一样,也是以“#”开头的。凡是以“#”开头的均为预处理指令,#define也不例外。

  • #define又称宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define 的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。可以给变量赋值,但绝不能给常量赋值。

  • 宏定义 #define 一般都写在函数外面,与 #include 写在一起。当然,写在函数里面也没有语法错误,但通常不那么写。#define 的作用域为自 #define 那一行起到源程序结束。如果要终止其作用域可以使用 #undef 命令,下面会介绍。


还需详细了解 #define, 可以点此链接观摩大佬解析。


语法:

#define name stuff

举些个栗子:

#define MAX 100
#define FOREVER for(;;)
#define reg register
  • 可以看到宏的命名习惯都是大写,这样更能区别。

  • 标识符的定义与常量是以空格隔开的。

  • 第一个定义了一个标识符 MAX ,它是常量 100,当我们在用这个标识符时,在预处理阶段,MAX 将会被替换成100

  • 第二个是用更形象的符号来替换一种实现, for( ; ; ) 相当于死循环,这里用 FOREVER 来形象的表示它。

  • 第三个是为 register这个关键字,创建一个简短的名字。

那么我们在define 定义标识符的时候,要不要在最后加上 ;

比如:

#define MAX 100;

建议不要加上,因为当我们写C语言程序时,都会习惯在后面加上分号,如果是一个变量等于这个MAXint max = MAX;),这时预处理阶段,会把这个 MAX 替换,变成 int max = 100;; ,此时有两个分号,这就出现了语法问题。


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

下面是宏的申明方式:

#define name( parament-list ) stuff

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

注意:

  1. 参数列表的左括号必须与name紧邻。
  2. 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

如:

#define SQUARE(x)  x * x

这个宏接收一个参数 x ,如果在上述声明后,有以下写法:

SQUARE(5);
SQUARE(2 + 3);

第一种预处理阶段被替换后表达式变为:5 * 5;(计算结果为25
第二种预处理阶段被替换后表达式变为:2 + 3 * 2 + 3;(计算结果为11

可以看出,第二种并不是我们想要的结果,所以这种定义宏的方式有问题

那如果这样子使用呢:

SQUARE((2 + 3));

替换后表达式变为:(2 + 3) * (2 + 3)这样子是可以的,但是这样治标不治本,而且写的代码还不好看,所以我们直接在定义宏处加上括号,这样就更好了。

更新之后:

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

那么这样子是否还会有问题呢?实际上还是有的。

如果是一下定义的宏:

#define SQUARE(x)  (x) + (x)

有了上面的声明后,进行一下操作:

int a = 5 * SQUARE(5);

替换后表达式为:

int a = 5 * 5 + 5;

计算结果为:30,这也与我们想要的值不符。

所以我们在定义时要给整体也加上一个括号,这样才不会出错

正确规范定义:

#define SQUARE(x)  ((x) + (x))

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


#define替换规则

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

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

注意:
4. 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归。
5. 当预处理器搜索 #define 定义的符号的时候,字符串常量的内容并不被搜索。


预处理指令 #undef

这条指令用于移除一个宏定义。

在一个程序块中用完宏定义后,为防止后面标识符冲突需要取消其宏定义。

例如:

#define MAX 100
int a = 100;
#undef MAX

这里第三行就取消了MAX的红定义,在下面还可以继续定义以MAX为标识符的宏。


宏和函数的对比

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个:

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

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

原因有两点:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多(也就是函数调用和函数返回的栈帧的创建和销毁可能比实际的代码功能运行时间还要长)。所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用>来比较的类型。宏是类型无关的。

当然宏跟函数比较,也有其缺点:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的(不可以调试可能程序怎么出错的都不知道)。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

根据宏的优点举个栗子(宏的参数可以出现类型,但是函数做不到):

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);  //类型作为参数

//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

宏和函数的对比图

在这里插入图片描述


命名约定

一般来讲函数和宏的语法很相似。所以语法本身没法帮我们区分二者。

那我们平时的一个习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写

命令行定义

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

例如:

  • 当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些)。

如有下面的代码(在linux中):

#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;
}

可以看到,上面的 ARRAY_SIZE是未定义的,但是我们可以通过以下指令对其赋值:

linux 环境演示
指令: gcc -D ARRAY_SIZE=10 programe.c

这样也可以灵活的控制数组大小啦。


条件编译

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

比如说:

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

#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\n", arr[i]);    //为了观察数组是否赋值成功。 
	     #endif     //__DEBUG__
     }
 
 return 0;
}

#ifdef 是如果定义了就干嘛干嘛,#endif 是截断 #ifdef的作用继续往下延伸。

可以看到,前面定义了_DEBUG_,所以后面的 printf("%d\n", arr[i]); 这条语句将会被编译执行。

  • 常见的条件编译指令:
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

预处理指令 #include

#include 可以说是再熟悉不过了,我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。

这种替换的方式很简单:

  1. 预处理器先删除这条指令,并用包含文件的内容替换。
  2. 这样一个源文件被包含10次,那就实际被编译10次。

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

  • 一种是本地文件包含:
#include "filename"

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


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

/usr/include

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

注意: 不同编译器可能放在不同地方,要按照自己的安装路径去找。

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

  • 一种是库文件包含
#include <filename.h>

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

嵌套文件包含

如果出现这样的场景:

在这里插入图片描述

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__

或者:

#pragma once

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

注:

推荐 《高质量C/C++编程指南》 中附录的考试试卷(很重要)。

笔试题:

1. 头文件中的 ifndef/define/endif是干什么用的?
2. #include <filename.h> 和 #include "filename.h"有什么区别?

其他预处理指令

#error
#pragma
#line
...

这里就不一一做介绍,可以自己去了解。 #pragma pack() 在 结构体一章 介绍了,可以去看噢。

写在最后

C语言阶段的知识学到这里,差不多就结束了呢,一路过来还是学到了非常多的知识,这也让我更加认清了自己的路还长着呢,接下来我会继续更新 基本数据结构 阶段的相关知识。

感谢阅读本小白的博客,错误的地方请严厉指出噢!

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

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

相关文章

python+django在线教学网上授课系统vue

随着科技的进步&#xff0c;互联网已经开始慢慢渗透到我们的生活和学习中&#xff0c;并且在各个领域占据着越来越重要的部分&#xff0c;很多传统的行业都将面临着巨大的挑战&#xff0c;包括学习也不例外。现在学习竞争越来越激烈&#xff0c;人才的需求量越来越大&#xff0…

Java高级-集合-Collection部分

本篇讲解java集合 集合 集合框架的概述 集合、数组都是对多个数据进行存储操作的结构&#xff0c;简称Java容器。 说明&#xff1a;此时的存储&#xff0c;主要指的是内存层面的存储&#xff0c;不涉及到持久化的存储&#xff08;.txt,.jpg,.avi&#xff0c;数据库中&#xf…

Java面试——MyBatis相关知识

目录 1.什么是MyBatis 2.MyBatis优缺点 3.MyBatis工作原理 4.MyBatis缓存模式 5.MyBatis代码相关问题 6.MyBatis和hibernate区别 1.什么是MyBatis MyBatis是一个半ORM持久层框架&#xff08;对象关系映射&#xff09;&#xff0c;基于JDBC进行封装&#xff0c;使得开发者…

【Python实战案例】Python3网络爬虫:“可惜你不看火影,也不明白这个视频的分量......”m3u8视频下载,那些事儿~

前言 哈喽&#xff01;上午好嘞&#xff0c;各位小可爱们&#xff01;有没有等着急了呀~ 由于最近一直在学习新的内容&#xff0c;所以耽搁了一下下&#xff0c;抱歉.jpg 双手合十。 所有文章完整的素材源码都在&#x1f447;&#x1f447; 粉丝白嫖源码福利&#xff0c;请移…

蓝海创意云获苏州电信2022年度“云业务优秀合作方”表彰

2月8日&#xff0c;中国电信苏州分公司召开产业数字化生态合作峰会&#xff0c;围绕“力量源于团结 奋斗创造奇迹”主题&#xff0c;凝聚合作伙伴合力&#xff0c;构建共生共赢的产业生态&#xff0c;蓝海创意云作为合作企业代表应邀出席峰会。会上&#xff0c;蓝海创意云荣获峰…

在阿里干了8年测试的表哥放假回来了,聊完之后大彻大悟

表哥是阿里某个项目组的测试开发&#xff0c;今年过年提前半个月放假回来了&#xff0c;一见面就给我们几个弟弟妹妹一人拿了部iPhone13pm。这一出手属实是阔绰&#xff0c;想想他的工作单位&#xff0c;也许对于他来说三四万也就是半个月工资而已。想想我那个小公司&#xff0…

第七节 平台设备驱动

在之前的字符设备程序中驱动程序&#xff0c;我们只要调用open() 函数打开了相应的设备文件&#xff0c;就可以使用read()/write() 函数&#xff0c;通过file_operations 这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观&#xff0c;但是从软件设计的角度看&#…

【Linux】操作系统进程概念

文章目录1. 冯诺依曼体系结构2. 操作系统3. 进程进程的基本概念查看进程和杀死进程父进程和子进程通过系统调用创建子进程1. 冯诺依曼体系结构 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数学家冯诺依曼提出了计算…

适配器模式(Adapter Pattern)

1.什么是适配器模式&#xff1f; 适配器模式&#xff08;Adapter Pattern&#xff09;是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式&#xff0c;它结合了两个独立接口的功能。 这种模式涉及到一个单一的类&#xff0c;该类负责加入独立的或不兼容的接…

浅谈现代GNSS模拟中的软件定义架构

随着技术的迭代更新&#xff0c;GPS/GNSS模拟技术也在不断发展进步。在过去&#xff0c;想要进行GNSS仿真基本上只有一种选择&#xff1a;使用固定式或分配式的硬件进行模拟。而如今&#xff0c;带来颠覆性创新的新型软件定义架构正在迅速取代传统的定制架构&#xff0c;这种独…

7款应用最广泛的 Linux 桌面环境

多样性应该是 Linux 最好的特性之一&#xff0c;用户可以不断尝试各种喜欢和新鲜玩法与花样&#xff0c;并从中找出最适合自己的应用。无论你是 Linux 新人还是老鸟&#xff0c;层出不穷的应用和桌面环境可能都会让我们应接不暇&#xff0c;特别是尝试不同的 Linux 桌面环境&am…

基于微信小程序的国产动漫论坛小程序

文末联系获取源码 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏览器…

05- 线性回归算法 (LinearRegression) (机器学习)

线性回归算法(LinearRegression)就是假定一个数据集合预测值与实际值存在一定的误差, 然后假定所有的这些误差值符合正太分布, 通过方程求这个正太分布的最小均值和方差来还原原数据集合的斜率和截距。当误差值无限接近于0时, 预测值与实际值一致, 就变成了求误差的极小值。 fr…

你的 TypeScript 还只是用来声明 string、number……的吗?

深入TS类型 联合类型 使用|操作符将变量可能出现的数值类型连接起来&#xff0c;就是联合类型 function css(ele: Element, attr: string, value: string | number) {// value参数的类型只能是string或者number// ...... } 交叉类型 使用&操作符将多种类型合并在一起&a…

JavaSE · 常量介绍 · 变量类型转换 · 理解数值提升 · int 和 Stirng 之间的相互转换

书接上回 Java 变量介绍 我们继续学习以下内容. 四、常量字面值常量final 关键字修饰的常量五、理解类型转换int 和 long/double 相互赋值int 和 boolean 相互赋值int 字面值常量给 byte 赋值强制类型转换类型转换小结六、理解数值提升int 和 long 混合运算byte 和 byte 的运算…

你还在用CSS旧时代布局的王者:浮动float 吗?

写在前面 随着flex、grid等布局的兼容性越来越好&#xff0c;float几乎已经在大众的视野消失了&#xff0c;曾经默认的小妖精终究成为了时代的眼泪。 作为前端开发者&#xff0c;你经历过float的时代吗&#xff1f;还在用float吗&#xff1f; 一、什么是浮动&#xff1f; 首…

ccc-台大林轩田机器学习基石-hw0

文章目录1 Probability and Statistics2 Linear Algebra3.Calculus一览图hw0题目链接1 Probability and Statistics 不妨假设C(n,K)C(n,K)&#xff0c;0≤K≤NC(n, K)C(n, K) &#xff0c;0 \le K\le NC(n,K)C(n,K)&#xff0c;0≤K≤N成立&#xff0c;只需证明C(n1,K)(n1)!K!(…

list类的使用和模拟实现

目录 一、list类的介绍 二、list的使用 1.构造、拷贝构造函数和迭代器 2.数据的增删查改 三、list的部分接口实现 1.节点定义 2.list类的组织 四、list的迭代器 1.迭代器的设计思路 2.const迭代器 3.->操作符的重载 4.反向迭代器 一、list类的介绍 list就是C库…

Elasticsearch基本概念和索引原理

一、Elasticsearch是什么&#xff1f; Elasticsearch是一个基于文档的NoSQL数据库&#xff0c;是一个分布式、RESTful风格的搜索和数据分析引擎&#xff0c;同时也是Elastic Stack的核心&#xff0c;集中存储数据。Elasticsearch、Logstash、Kibana经常被用作日志分析系统&…

[CCS 2022] 皇帝没有衣服:用于网络安全的AI/ML

AI/ML for Network Security: The Emperor has no ClothesCCS 22: Proceedings of the 2022 ACM SIGSAC Conference on Computer and Communications Securityhttps://dl.acm.org/doi/abs/10.1145/3548606.3560609摘要最近的一些研究工作提出了基于机器学习&#xff08;ML&…