【C语言进阶】宏

简介: 【C语言进阶】宏

前言:


 在上一篇博客中,我们学习了程序环境和预处理相关的知识。我们在预处理阶段就是在对宏进行替换,今天我就来给大家详细讲解于宏相关的知识!


一.宏:


#define:


 #define 的用处非常多,就比如我们常用的定义标识符常量、定义宏等等,而在这个过程中,也有一些细节值得我们去注意。


①. #define 定义标识符:

我们常常会使用 #define 去定义一些标识符来方便我们的代码书写:


语法:
 #define name stuff


定义了之后,只有我们看到 name 这个东西,就给他一比一替换成 stuff这个东西就行即可


举个栗子:


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


注意:

 在 #define 的最后,最好不要加上分号 “ ; ” 加上可能会出现两个;号,可能会导致语法错误:


②.#define 定义宏:

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


宏的申明方式为:


#define name( parament-list ) stuff

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


注意:

注意:

参数列表的左括号必须与name紧邻。

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


易错点:


初学者在学习宏时很容易出现以下问题:

举个栗子:


#define SQUARE( x ) x * x
int main
{
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
}


乍一看,你可能觉得这段代码将打印36这个值。

事实上,它将打印11.

为什么?


我们前面说过了,标识符的替换是一比一替换!

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:

printf (“%d\n”,a + 1 * a + 1 ),所以结果是11。


所以我们可以对这个宏进行修改:


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


由此可见,我们在进行宏定义的时候,一定要舍得加括号!避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。


③. #define 替换规则:

#define 在进行符号替换时,遵循以下规则:


调用宏时首先对参数进行检查,检查是否包含任何由 #define 定义的符号。如果有,它们将首先被替换。

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

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

注意:

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

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


④#与##的使用:

#的使用:

 上面我们说到字符串常量的内容不会被#define定义的情况下替换掉。那么,我们如何将参数插入字符串中?


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


char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);


这里输出的是不是

hello bit ?

答案是确定的:是。

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


方法一:

我们可以将带有" "(双引号)的字符串替换掉原字符串中


举个栗子:


#define PRINT(FORMAT, VALUE)\
 printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10)


这里的FORMAT替换为"%d",正好完成拼接在这里我们就可以正常打印出结果了!这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。

方法二:

对于没有" "(双引号)的标识符怎么替换字符串中的标识符呢?

其实我们只要在要替换的标识符前加一个#即可,此时就把一个宏参数变成对应的字符串


举个栗子:


int i = 10;
#define PRINT(FORMAT, VALUE)\
 printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
...
PRINT("%d", i+3);


代码中的 #VALUE 会预处理器处理为:

"VALUE"

最终的输出的结果应该是


the value of i+3 is 13


##的使用:

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。


举个栗子:


#define ADD_TO_SUM(num, value) \
 sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10


注:

这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。


二.宏与函数对比:


宏通常被应用于执行简单的运算。

比如在两个数中找出较大的一个。


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


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

原因有二:


用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。

所以宏比函数在程序的规模和速度方面更胜一筹。

更为重要的是函数的参数必须声明为特定的类型。

所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。

宏是类型无关的。

当然宏和函数相比宏也有劣势的地方,


原因如下:


每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

宏是没法调试的。

宏由于类型无关,也就不够严谨。

宏可能会带来运算符优先级的问题,导致程容易出现错(记得加括号)

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


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


三. 宏与函数的命名约定:


一般来讲,我们使用的宏定义函数,与普通函数的使用语法很相似,这就导致语言本身无法帮我们区分二者。所以我们通常对二者的命名进行约定:


把宏名全部大写

函数名不要全部大写


四.#undef:


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


#undef NAME

如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除


五.条件编译:


 通过使用条件编译指令,我们在编译一段代码时如果要将一条(或一组)语句编译或舍弃是很方便的。例如一些调试性的代码,删除可惜,保留碍事,于是我们就可以通过使用条件编译指令来实现选择性的进行编译。


举个栗子:


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



当我们发现赋值成功的时候,只要把#define __DEBUG__这句代码删掉就行。


常见的条件编译指令:


①. 单分支条件编译指令:


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


②. 多分枝条件编译指令:


2.多个分支的条件编译


#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif


③. 判断是否被定义:


3.判断是否被定义


#if defined(symbol)
等价于:#ifdef symbol
#if !defined(symbol)
等价于:#ifndef symbol


④.嵌套指令:


#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.头文件被包含的方式:


本地文件包含


#include "filename"


查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在找

准位置查找头文件。

如果找不到就提示编译错误。


库文件包含


#include <filename.h>


查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?


答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。


2.嵌套文件包含


如果出现这样的场景:


9067d3bde07aaee0e85bdd8501dce253_61fc653bbc3d484bb1d3155b878aaa63.png


我们会发现:最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。当程序越复杂,程序就和同时打开很多一模一样的文件。导致预处理之后的代码很冗长。


如何解决这个问题?

答案:条件编译。

每个头文件的开头写:


#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__


或者:


#pragma once


总结:


 c语言的相关知识到这里就结束了!之后我会出一个总结的思维导图!更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

目录
相关文章
|
2天前
|
C语言
【C语言】库宏offsetof(结构体成员偏移量计算宏)
【C语言】库宏offsetof(结构体成员偏移量计算宏)
19 0
|
2天前
|
存储 编译器 Linux
c语言进阶(2)
c语言进阶(2)
28 0
|
2天前
|
存储 编译器 Linux
【C语言】自定义类型:结构体深入解析(二)结构体内存对齐&&宏offsetof计算偏移量&&结构体传参
【C语言】自定义类型:结构体深入解析(二)结构体内存对齐&&宏offsetof计算偏移量&&结构体传参
|
2天前
|
存储 C语言
C语言进阶---------作业复习
C语言进阶---------作业复习
|
2天前
|
存储 Linux C语言
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-2
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
2天前
|
自然语言处理 Linux 编译器
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)-1
C语言进阶第十一节 --------程序环境和预处理(包含宏的解释)
|
2天前
|
存储 编译器 C语言
C语言进阶第十课 --------文件的操作-1
C语言进阶第十课 --------文件的操作
|
2天前
|
存储 程序员 C语言
C语言进阶第九课 --------动态内存管理-2
C语言进阶第九课 --------动态内存管理
|
2天前
|
编译器 C语言
C语言进阶第九课 --------动态内存管理-1
C语言进阶第九课 --------动态内存管理
|
2天前
|
C语言
C语言进阶第八课 --------通讯录的实现
C语言进阶第八课 --------通讯录的实现