C语言零基础彻底掌握预处理下篇

2022-11-13 14:11:05 语言 预处理 下篇

1、条件编译

1.1 条件编译如何使用

C语言提供的条件编译的功能可以让我们按照不同的条件去编译不同的程序部分,从而产生不同目标代码文件。

第一种形式:

#ifdef 标识符

程序段1

#else

程序段2

#endif

它的功能是,如果标识符已经被 #define 定义了,则只会对程序段1进行编译,不会对程序段2进行编译,如果没有被定义则反之,如果我们不需要程序段2,也可以省去 #else 和他对应的程序段。

第二种形式:

#ifndef 标识符

程序段1

#else

程序段2

#endif

第二种形式与第一种形式的区别是将 ifdef 改为 ifndef,它的功能是,如果标识符没有被 #dfine 定义,则对程序段1进行编译,不会对程序段2进行编译,如果被定义了则反之,如果我们不需要程序段2,也可以省去 #else 和他对应的程序段。

第三种形式:

#if 常量表达式

程序段1

#else

程序段2

#endif

第三种形式的功能是:如果常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译,因此可以使程序在不同条件下,完成不同的功能。

至于里面还可以添加 #elif 命令,意义与 else if 相同,形成一个 if else 阶梯状语句,可进行多种编译选择。

注意:如果定义空宏则会报错,因为 #if 后面必须要更常量表达式!

1.2 用 #if 模拟 #ifdef

此代码的意思是,如果 PRINT 宏被定义了,则执行第一个打印函数,否则执行第二个打印函数,同时我们也可以模拟 #ifndef,只需前面加个逻辑非就可以 ' ! ',例如:#if (!defined(PRINT))

就这样完了吗?其实并没有,在更复杂的项目中,往往会出现两个或多个宏需要同时定义才能满足需求,我举一个很简单的例子,如果我定义了 C 宏和 CPP 宏,我才可以编译所对应的代码:

如上代码就需要两个宏都被定义才能编译下面的程序段,相信学习过逻辑与的小伙伴应该很容易理解吧,那么我们如果需要两个都未定义才能编译下面的程序段呢?如何写?

两个都未定义才编译: #if (!defined(C) && !defined(CPP))前面分别加逻辑非就可以 ' ! '

或者:#if (!(defined(C) || defined(CPP)))本代码中逻辑或只要有一个被定义,就为真,然后执行逻辑非,这样也能保证两个都未定义才进行编译!

至于最后用不用大括号给括起来,我的建议是括起来,这样我们阅读代码会更直观!

既然出现了逻辑与,是不是也可以出现逻辑或呢?当然上面已经有例子了,但是这里我就不一一演示了,感兴趣的可以下来自己去尝试一下。

条件编译支持嵌套:

这里其实和我们平常用的 if 嵌套式是似的,也很容易理解,这里我们就不细说,有一点要注意的就是,条件编译每个 #if 都需要有对应的 #endif 来结束

1.3 为何要有条件编译

我们先对我们上面2小节的内容做一个总结:条件编译本质上是让编译器对代码进行裁剪!

本质认识:条件编译,其实就是编译器根据实际情况,对代码进行裁剪,而这里 “实际情况” ,取决于代码平台,代码本身的业务逻辑。

  • 可以只保留当前最需要的代码逻辑,其他去掉,可以减少生成代码的大小
  • 可以写出跨平台的代码,让一个具体业务,在不同平台编译的时候,可以有同样的表现

条件编译都用在哪些地方呢?

张三有个公司,公司有个项目,项目对应的软件又有专业版,免费版,精简版等等...

难道每个版本都对应着不同的代码吗?不是的,这样维护起来太麻烦了,其实所谓不同的版本,本质就是功能上的有和无,所以在技术层面上,为了更好的维护,当然可以使用条件编译,需要哪个版本,就是用条件编译裁剪就行。

著名的 linux 内核,功能上,其实也是用条件编译进行功能裁剪的,用来满足不同平台的软件。

2、文件包含

2.1 #include 究竟干了什么

我相信 #include 对于每个编程小伙伴来说都不陌生,很多人写 C 语言第一件事就是写上 #include <stdio.h> 可能老师会告诉你们这是包含标准输入输出头文件,至于如何包含的,可能不会跟你讲。那今天我们就来通过预处理来看一看到底是如何包含的:

我们来写上一小段代码:

前面说过,预处理会将头文件展开,去注释,宏替换,条件编译等等

在 Linux 环境下我们可以执行命令:GCc -E test.c -o test.i保留预处理之后的文件并命名为 test.i

为了更好的对比,我们执行 vim 命令模式下的 vs 指令:vs/sur/include/tdio.h 也就是打开标准输入输出的头文件:

看到预处理的结果之后,发现文件大小比我们实际代码要大得多!

结论:#include 本质是把头文件相关内容,拷贝到源文件中。

2.2 防止头文件重复包含的条件编译是如何做到的

既然我们会包含头文件,那有没有可能存在头文件重复被包含的可能性呢?导致我们头文件被重复拷贝?

这里可能会有很多老师也教过,同学们啊,我们写头文件的时候一定要写如下代码啊,这是防止头文件重复包含的啊:

#ifndef _TEST_H_
#define _TEST_H_
#include <stdio.h>
#define MAX 999
int g_val = 10;
extern void Print();
...
#endif

如上代码很多小伙伴都知道在#ifndef _TEST_H_ 和 #endif 之间写的头文件包含,宏定义,全局变量,函数声明,都不会被重复拷贝,为什么呢?他是如何做到的?我们实验证明 (如下两张图最右边是预处理之后的结果) :

如下代码是没有带上条件编译防止头文件重复包含,但在源文件已经重复包含的例子:

我们加上#ifndef _TEST_H_ 和 #endif在来看重复包含的效果:

已经没有重复拷贝的情况了,看来确实有防止头文件重复包含的效果!

那么这条语句是如何做到的呢?

我们前面学过 #ifndef 如果没有定义这个宏,则执行后续语句,当第一次我们头文件展开的时候,确实没有定义_TEST_H_ 这个宏,所以会执行后续的语句,但是在第一次展开的时候我们立马定义了_TEST_H_ 宏,所以我们重复包含头文件第二次展开的时候,这个宏已经被定义了,所以也就不会去执行#ifndef后续语句了!

结论:所有头文件都得带上条件编译,防止头文件重复包含!当然也可以直接 #pragma once

重复包含的一定会报错吗?显然是不会的,但是会引起多次拷贝,会影响编译效率。

3、选学内容

3.1 #error 预处理

#error 预处理指令的作用是:编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译:

3.2 #line 预处理

#line 的作用时改变当前行数和文件名称,他们是在编译程序中预先定义的标识符。这里我就不给你们看运行结果了,感兴趣的可以复制代码下去自行了解下哦:

int main()
{
	printf("%s, %d\n", __FILE__, __LINE__); //C预定义符号,代表当前文件名和代码行号
#line 60 "hehe.h" //定制化完成
	printf("%s, %d\n", __FILE__, __LINE__);
	return 0;
}

本质其实是可以定制化你的文件名称和代码行号,很少使用!

3.3 #pragma 预处理

3.3.1 #pragma message

message 参数他能在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。

#define TEST
int main()
{
#ifdef TEST
#pragma message("TEST Macor activated!")
#endif
    return 0;
}

当我们定义了 TEST 这个宏后,应用程序在编译时就会在编译输出窗口里显示TEST macor activated! 因此我们就不会因为不记得自己定义的一些宏而着急了!

3.3.2 #pragma once

这个还是比较常用的,只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,但是考虑到兼容性的问题,并没有太多的使用。

3.3.3 #pragma warning

#pragma warning(disable : 4507 34; once : 4385; error : 164)
//等价于:
#pragma warning(disable : 4507 34) //不显示 4507 和 34 号警告信息
#pragma warning(once : 4385)       //4385 号警告信息仅报告一次
#pragma warning(error : 164)       //把 164 号警告信息作为一个错误

当使用 windows vs 环境的小伙伴们,在使用库函数的时候比如 scanf 会说这个函数不安全,推荐你使用 scanf_s,那我们要保证代码可以移植性如何办呢?通过查看报错发现是 4996 报错,那我们则可以:

#pragma warning(disable : 4996) //这样就解决问题了!

3.3.4 #pragma pack

设置结构体内存对齐,我们还没更新到结构体,加上用的并不算多,所以感兴趣的可以先去自行研究哦。

3.4 # 和 ##

假设说我们今天定义了一个打印宏:

#define PRINT(x) printf("hello x is %d.\n", ((x)*(x)))

调用宏 PRINT(8); 则会输出:hello x is 64.

如果你希望字符串中包含宏参数,那我们就可以使用 "#",它可以把语言符号转换成字符串:

#define PRINT(x) printf("hello "#x" is %d.\n", ((x)*(x)))

这样调用PRINT(8); 则会输出:hello 8is 64.

## 使用起来也很简单,就是将两个相连的符号,连接成为一个符号:

#define XNAME(n) x##n

如果这样使用宏: XNAME(8)则会被展开成为:x8

在 "#" 或 "##" 预处理操作符相关的计算次序,如果未被指定则会产生问题,为了避免该问题,在单一的宏定义中只能使用其中一种操作符。除非是必须使用,否则尽量不适用这两个预处理操作符!

到此这篇关于C语言零基础彻底掌握预处理下篇的文章就介绍到这了,更多相关C语言预处理内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

相关文章