【C语言】 —— 预处理详解(下)
- 前言
- 七、# 和 \##
- 7.1 # 运算符
- 7.2 ## 运算符
- 八、命名约定
- 九、# u n d e f undef undef
- 十、命令行定义
- 十一、条件编译
- 11.1、单分支的条件编译
- 11.2、多分支的条件编译
- 11.3、判断是否被定义
- 11.4、嵌套指令
- 十二、头文件的包含
- 12.1 头文件被包含的方式
- (1) 本地文件被包含的方式
- (2)库文件包含
- 12.2 嵌套文件包含
- 十三、 其他预处理指令
前言
在上期【C语言】 —— 预处理详解(下)的学习中,我们详细介绍了预处理中宏的相关知识,相信大家都收获不少。别急还有,本期让我们继续学习预处理方面的其他知识吧。
七、# 和 ##
7.1 # 运算符
# 运算符
将宏的一个参数转换成字符串字面量。它进允许出现在带参数的宏的替换列表中# 运算符
所执行的操作可理解为“字符串化”
什么意思呢?
我们先来做一个铺垫:
int mian()
{printf("hello" "world\n");printf("helloworld\n");return 0;
}
上述两句代码有什么区别呢?我们一起来看看:
可以看到,两个字符串和一个字符串的效果是一样的。C语言会把两个字符串天然连成一个字符串
,中间加空格也没用。
现在有这么一个场景:
int main()
{int a = 1;printf("The value of a is %d\n", a);int b = 20;printf("The value of b is %d\n", b);float f = 8.5f;printf("The value of f is %f\n", f);return 0;
}
我们发现三句代码的逻辑都是非常相像
的,但又有些许不同
。
那我们想既然他们这么相像,能不能把他们封装成一个函数
,以方便使用呢?
但是函数是做不到的这个功能的
那怎么办呢?
我们可以尝试用宏来解决呢
#define Print(n, format) printf("The value of n is " format "\n", n)int main()
{int a = 1;Print(a, "%d");//printf("The value of a is %d\n", a);int b = 20;Print(b, "%d");//printf("The value of b is %d\n", b);float f = 8.5f;Print(f, "%f");//printf("The value of f is %f\n", f);return 0;
}
运行结果:
我们发现 n n n 一直没变,那应该怎么修改呢?
这时就应该用到我们的 # 运算符了:# 将宏的一个参数转换成字符串字面量,即 n n n 变成 “ n ” “n” “n”
这时,我们再运用拼接大法就成了
#define Print(n, format) printf("The value of " #n " is " format "\n", n)
不懂?看下面的解释就懂啦
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 GENERIC_MAX(type) \type type##_max(type x, type y)\{\return x > y ? x : y;\}
#define GENERIC_MAX(type) \type type##_max(type x, type y)\{\return x > y ? x : y;\}GENERIC_MAX(int); //相当于定义了一个函数int_max
GENERIC_MAX(float); //相当于定义了一个函数float_maxint main()
{int r1 = int_max(3, 5);printf("%d\n", r1);float r2 = float_max(2.3f, 7.6f);printf("%f\n", r2);return 0;
}
运行结果:
我们也可以在 g c c gcc gcc 环境下观察预处理后的 .i
文件,有个更直观地了解
当然,这样生成的函数也是不方便调试的
那这里 ##
起到什么作用呢?
加了 ##
,编译器才会认为他们是一个符号
让我们来看看不加 ##
的效果:
八、命名约定
一般来讲,函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者
我们平时的一个习惯是:
- 把宏名全部大写
- 函数名不要全部大写
当然,这些命名规则并不是绝对的
比如 o f f s e t offset offset 这个宏就写成了全小写
注: o f f s e t offset offset 是用来计算结构体成员相对于结构体起始位置的偏移量的
九、# u n d e f undef undef
# u n d e f undef undef 指令用来移出一个宏定义
上述代码,在 169 行使用 # u n d e f undef undef 移除了宏 MAX。在移除之前的 168 行调用时没问题的,但移除之后的 170 行调用就会报错
十、命令行定义
许多 C 的编译器(不包括VS)提供了一种能力,允许在命令行中定义符号。用于启动编译过程
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性就有用处。(假定某个程序中声明了一个某长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要的数组能够大些)
命令行定义式在预处理阶段处理的,在预处理阶段时,上述代码中 s z sz sz 的值已经确定了
十一、条件编译
在编译一个程序的时候如果将一条(一组语句)编译或者放弃是很方便
的。因为我们可以用条件编译指令
条件编译指令就是这段代码我想让你编译就编译,不想让你编译你就不要编译了
。我们可以给他设定一个条件,条件为真,这段代码就参与编译,条件为假,这段代码就不要编译了。
比如说:
一些调试性的代码,删除可惜,保留又碍事,所以我们可以选择性编译
常用的条件编译指令:
11.1、单分支的条件编译
#if 常量表达式//···
#endif
11.2、多分支的条件编译
#if 常量表达式//···
#elif 常量表达式//···
#else//···
#endif
哪条语句为真,就执行哪条语句
#define M 1
int main()
{
#if M == 0printf("hello\n");
#elif M == 1printf("world\n");
#elif M == 2printf("csdn\n");
#endifprintf("886\n");return 0;
}
11.3、判断是否被定义
#if defined(symbol)
#ifdef symbol//上面两个的反面
if !defined(symbol)
#ifndef symbol
11.4、嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif#endif
十二、头文件的包含
12.1 头文件被包含的方式
(1) 本地文件被包含的方式
# include "filename"
查找策略:先在源文件所在的工程目录下查找,如果头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件
如果再找不到就编译错误
L i n u x Linux Linux 环境的标准头文件路径(头文件放在哪):
/usr/include
VS 环境的标准头文件路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//这是VS2013的默认路径
(2)库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “ ”
的形式包含
答案是肯定的,但是这样做查找的效率比较低,当然这样也不容易区分是库文件还是本地文件
12.2 嵌套文件包含
学习了前面的(编译和链接),我们知道头文件的包含在预处理阶段就是直接将该文件的代码拷贝到包含头文件的地方
如果一个头文件被包含了 10 次,那就实际被编译了 10 次,如果重复包含,对编译的压力就比较大
t e s t . c test.c test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
t e s t . h test.h test.h
void test();
struct Stu
{
int id;
char name[20];
};
但在一个工程中,一个文件难免被包含多次,那么如何解决这个问题呢?
答案:条件编译
#ifndef __TEST_H__
#define __TEST_H__//头文件的内容
#endif
怎么理解呢?
- 当第一次包含头文件时,要不要编译呢?先进行判断
- __TEST_H__这个符号并没有被定义,要进行编译
- 紧接着定义__TEST_H__符号
- 之后再次包含该头文件,发现__TEST_H__已被定义,不再对之后包含的该头文件进行编译
不过上面这种写法比较麻烦,还有另外一种写法:
#pragma once
效果与上面的方式是一样的
这样就可以避免头文件的重复引入
十三、 其他预处理指令
#error
#pragma
#line
···
#pragma pack()//在结构体部分介绍
有兴趣的小伙伴可以阅读 《C语言深度解剖》
好啦,本期关于预处理的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!