本篇博客就算是C语言的最后一篇博客了,本篇介绍的是C语言中的预处理指令,希望通过本章的学习能让大家掌握预处理指令。
目录
一、预定义符号
- __FILE__ //进行编译的源文件
- __LINE__ //文件当前的行号
- __DATE__ //文件被编译的日期
- __TIME__ //文件被编译的时间
- __func__ //当前函数的函数名
- __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义 (VS2019不遵循,gcc支持)
这些预定义符号都是语言内置的。我们可以直接拿来使用。
接下来我们来看看这些个如何使用。举个例子:
我们可以使用以上这些预处理指令来获取一些我们想要得到的信息。在后期的使用中(工程量大的情况下)我们可以使用这些预处理指令将这些信息写入到文件中(如fprintf函数)。
二、#define
2.1 #define 定义标识符
语法:
#define name stuff
举例:
#define MAX 1000
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换这一种实现
#define CASE break; case //在写case语句的时候自动把break补上。
//如果定义的stuff过长,可以分成几行写,除了最后一行,每行的后面加上一个反斜杠(续行符)
#define DEBUG_PRINT printf("func:%s file:%s lien:%d \
date:%s time:%s i=%d \n", \
__func__, __FILE__, __LINE__, \
__DATE__, __TIME__, i);
注:define是在预处理阶段进行替换的,所以我们在使用define的时候,不能在后面加上分号,如果带了分号,我们在写完一条代码的时候就很可能导致程序的错误。
2.2 #define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实习通常称之为宏(macro)或定义宏(define macro).
宏定义的语法:
#define name(parament-list) stuff
parament-lise是参数列表,我们可以将我们设定的参数放入其中。
stuff则是宏具体实现的内容,其中是 parament-list 可能出现在stuff中。
定义举例:
#define MAX(x,y) (x>y?x:y)
我们在预编译中,预处理阶段就会替换为以下这种形式。
我们可以看替换后的文件text.i,这时替换会将参数替换到文本。
!!注意!!
参数列表的左括号必须与name紧邻(左括号间不要有空格!!!)
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
2.3 #define 定义替换规则
在程序中的扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,他们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何有#define定义的符号。如果是,就重复上述处理过程。
!!注意!!
1.宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
2.4 #和##
#
#的作用是将参数插入到字符串中。
将参数名直接转化为对应的字符串——宏的参数没有发生替换
比如,我们现在要实现使用一个功能将a、b打印到屏幕上,并且打印的时候出现他们对应的名字,我们先看看这个要求是什么样的。
我们想让这个n不是n,而是我们传过去的参数名。
很明显,函数是无法做到的,因为函数形参是实参的一份临时拷贝,我们无法做到让形参的参数名一直与实参的参数名相同。
这时我们可以使用宏,而宏是可以做到的。
我们使用一对双引号加上#和宏中的参数名,就可以做到将参数直接插入到字符串中,这样,这个功能就可以实现了。
那知晓了#的作用,我们就可以使用#来做一些特殊的事情,比如打印一个未知类型的参数。
只要使用者将参数的类型的传入,我们就可以以不同的形式将参数打印出来。
例如:
我们传入一个浮点型的参数,我们就可以直接将他打印到屏幕上:
##
##它的作用是给定两个符号,将这两个符号连接起来
比如
!!注意!!
这样连接必须产生一个合法的标识符。否则其结果就是未定义的。
这两个特殊的操作符的使用场景比较奇怪,也比较少见,这里我们知道它们的使用方式既可。
2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现永久性的效果。
比如我们在使用#define定义宏的时候,写了以下此类代码:
这种则是带有副作用的宏参数,本来我们只是想将a+1的值赋给b,但是代码执行完之后却将a的值加上了1。
这里我们来看看一个代码,来看看副作用给这段代码带来了什么:
大家可以算一下最后a、b、c的最终结果是什么。
答案:
我们在定义宏的时候,尽量避免对宏的参数进行此类修改,否则此类带有副作用的代码可能会对程序产生BUG。
2.6 宏和函数对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
#define MAX(a,b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算所花费的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。
2.更为重要的是函数的参数必须声明为特定类型。
所以函数只能在类型合适的表达式中使用。反之这个宏可以使用于整形、长整型、浮点型等可以用>来比较的类型。
此外,宏是不方便调试的,并且宏不能递归,因为他不能做到自己调用自己。
2.7 命名约定
一般来说函数和宏的使用语法很相似。所以语法本身没法帮助我们区分两者。
我们通常会将宏和函数在命名在有所不同,这也是我们使用宏和函数时的一种习惯。
1.把宏名全部大写
2.函数名不要全部大写
2.8 #undef
当我们不再需要一段宏定义时,或如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
这时我们可以使用#undef来移除宏的定义。
#unfef NAME
三、条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)放弃编译是很方便的。因为我们可以使用条件编译指令来控制语句是否需要执行。
#if //判断条件是否符合
语句;
#endif //结束此次条件编译
常见的条件编译语句:
1.
#if 常量表达式
//……
#endif
//常量表达式由预处理器求值。
2.多分支的条件编译
#if 常量表达式
//……
#elif 常量表达式
//……
#else 常量表达式
//……
#endif 常量表达式
//……
3.判断是否被定义
#if defined(symbol) //定义了则参加条件编译
#ifdef symbol
或
#if !defined(symbol) //如果没有定义这个宏
#ifnedf sybmol //如果没有定义这个宏
4.嵌套指令——嵌套的条件编译指令
#if 常量表达式
#if 常量表达式
//……
#endif
#endif
四、文件包含
4.1 文件的包含方式:
1.本地包含头文件
#include "filename"
查找策略:先在源文件所在的目录下查找,如果该文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
2.库文件包含
#include <stdio.h>
查找策略:查找文件直接去标准路径下去查找,如果找不到就提示编译错误。
所以,我们在包含库文件时也可以使用" "的形式的包含。
但是这样的代价是查找效率相对较慢,增加了无效的查找时间;第二点,这样也不容易区分是库文件还是本地文件了。
4.2 避免头文件的多次包含
在代码量大的程序中,极有可能出现一个头文件被多次引入,这时我们可以使用条件编译指令来避免头文件被多次引入。
我们可以采取两种方式:
每个头文件的开头写:
#ifndef __TEST_H__ #define __TEST_H__ //头文件内容 #endif
或
#pragma once
这两种方式都可以做到避免头文件的重复引入。
本篇博客到这就结束了,C语言部分到这也就告一段落了,后续会更新数据结构相关的内容,也希望大家持续关注。谢谢大家。
如果看到这感觉还不错的话,可以留个赞鼓励一下呗,你们留下的赞是我写博客的巨大动力~