【C进阶】——预处理详解(二)

简介: 【C进阶】——预处理详解(二)

6. 命名约定

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

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

把宏名全部大写

函数名不要全部大写

当然,也是有例外的,我们其实之前就遇到过:

之前文章里我们学过的用来求偏移量的offsetof ,它的命名虽然是全小写的,但是它并不是库函数,而是一个宏。

daad14445333417c97e708d7b7e59e49.png

7. #undef

#undef是什么东西呢?

我们已经知道#define是用来定义标识符和宏了,那#undef呢?

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

#undef NAME

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

举个例子:

2e42ab3675a844738fe49592d9c49879.png

移除前我们可以正常使用,#undef移除后我们就不能再使用这个符号了。

8. 命令行定义

什么是命令行定义呢?

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

举个例子(命令行定义在vs上不好演示,,这里还是在Linux环境下给大家演示):

我们写这样一段代码:

47a4f0c405304cd78b29613cb201be23.png

我们创建了一个数组,大小是SZ,但是我们并没有定义SZ这个变量。

那这样可以运行嘛?

2bd3de4214bc461181fadbc9836da9e4.png

肯定是不行的,这里报错说SZ没有定义。

那有没有什么方法可以解决呢?

当然,这时就可以使用命令行定义:

64ba53a0d94d4faea1210ff9463b4c14.png

大家看,我们在编译时,通过一个命令给SZ指定一个大小,然后运行,就通过了,并且成功打印出了数组元素。

那命令行定义有什么用处呢?


当我们根据同一个源文件需要编译出一个程序的不同版本时,我们就可以通过命令行定义来实现。

假定某个程序中声明了一个某个长度的数组,如果一个机器内存有限,我们需要一个很小的数组,但是另外一个机器内存比较大,我们需要这个数组能够大一些。

那这时我们就可以通过命令行定义在每次编译时指定数组大小为我们需要的长度,以此来满足我们的需求。

9. 条件编译

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

那什么时候会用到条件编译呢?

比如说:

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

举个例子:

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

这段代码中printf("%d\n", arr[i]);是我们为了观察数组元素是否赋值成功而增添的语句,那对于这句代码我们就可以使用条件编译,需要观察的时候就让它进行编译,不需要的时候就可以不让他编译。

那怎么实现呢?我们看到里面有这样一句代码:

#ifdef __DEBUG__
    printf("%d\n", arr[i]);
#endif 

这就是一个条件编译语句,#ifdef __DEBUG__的作用就是如果__DEBUG__这个符号定义了,就会编译它后面控制的语句,如果没符号没定义,就不会编译。

我们来验证一下:

a482b7e60d6e4505820c3119dd562f9c.png

如果我们现在把#define __DEBUG__注释掉:

8ca958469c42494f98ec754e0b84427e.png

接下来我们就来学习一下常见的条件编译指令:

9.1 单分支条件编译

#if 常量表达式
  //...
#endif
//常量表达式由预处理器求值。

如果常量表达式为真,后面被控制的语句就会参与编译。

注意条件编译能控制的语句到#endif之前,它们之间可以有很多条语句。

举个例子:

int main()
{
#if 1
  printf("hehe\n");
#endif
  return 0;
}

1为真,所以会参加编译:

88caca77da8644fcbd7f38f177e19aba.png

这样呢:

int main()
{
#if 1>8
  printf("hehe\n");
#endif
  return 0;
}

1>8结果为假,那就不会编译

2b6d7de691894ccf8cd0ef24923b195b.png

9.2 多个分支的条件编译

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

其实这个和我们之前学的if else很像的,我相信不需要给大家解释很多,区别就是这个是用来控制语句是否编译的

需要注意的就是别忘了最后要加上#endif

9.3 判断是否已定义

#if defined(symbol)
#ifdef symbol 前两个是等价的,作用一样
#if !defined(symbol)
#ifndef symbol  后两个也是等价的

其实我们一开始给大家举的那个例子就是#ifdef symbol 嘛,它和#if defined(symbol)其实是一样的作用,什么作用呢?

如果后面的symbol符号是已定义的,那么它们后面跟的语句就会参与编译,反之则不会。

举个例子:

#define A 1
#define B 2
int main()
{
#ifdef A
  printf("haha\n");
#endif
#if defined(B)
  printf("hehe\n");
#endif
  return 0;
}

符号AB都是已经被定义过的,所以两个printf语句都会参与编译,最终运行代码可以打印。

如果我们注释掉或移除定义,当然两个printf语句就不会参与编译了。

#define A 1
#define B 2
int main()
{
#undef A
#undef B  //移除定义
#ifdef A
  printf("haha\n");
#endif
#if defined(B)
  printf("hehe\n");
#endif
  return 0;
}

277f1912d5c6485494e53a54f0526694.png

#if !defined(symbol), #ifndef symbol 作用也是等价的,它们又是什么作用呢?

其实从字面意思就能看出来,它们的作用是如果symbol符号没定义,后面跟的语句才会编译:

int main()
{
#ifndef A
  printf("haha\n");
#endif
#if !defined(B)
  printf("hehe\n");
#endif
  return 0;
}

这次AB都没有定义:

4708033f034041be8b463b3e650163aa.png

但是后面的语句参与编译了。

当然如果定义了,它们就不会参与编译了。

当然记得它们后面也都要加上#endif

9.4 嵌套指令

当然条件编译也支持像ifelse语句那样进行嵌套:

#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

条件编译指令的嵌套和ifelse分支的控制也是基本一样的,就不再给大家一一举例了。

10. 文件包含

我们已经知道, #include 指令可以使被包它含的那个文件被编译。

就像它实际出现于 #include 指令所在的地方一样。


其实在上一篇文章里我们就一起验证过,当我们的程序包含了一个头文件,比如#include <stdio.h>,那么在预处理之后头文件stdio.h中的内容就真的会被替换到代码中。

这种替换的方式很简单:

预处理器先删除这条指令,并用包含文件的内容替换。

这样一个文件被包含10次,那就实际被编译10次。

10.1 头文件被包含的方式

10.1.1 本地文件包含

#include "filename"

本地的头文件包含,我们应该使用双引号“ ”

对于双引号“ ”包含的头文件,查找策略:

先在源文件所在目录下查找,如果未找到该头文件,编译器就像查找库函数头文件一样在标准位置(标准库里)查该找头文件。

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

93f97b8b0fc0451ab1f91837d5a5fd8b.png

10.1.2 库文件包含

#include <filename.h>

对于C标准库里的头文件,我们使用尖括号<>来包含。

对于尖括号<>包含的头文件,查找策略:

查找尖括号<>包含的头文件直接去标准路径下去查找,如果找不到就提示编译错误。

因此:

对于本地的头文件,我们不能使用尖括号<>包含,只能用双引号“ ”包含

a33937fc48e942bd96077d75f97e74b4.png

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

答案是肯定的,可以。

但是这样做查找的效率就低了,当然这样也不容易区分是库文件还是本地文件了

10.2 解决头文件被重复包含的问题

有时候,在不经意间我们可能会对一个头文件进行多次包含,而我们自己可能并没有发觉。

比如这样的场景:30e16ea233c8422187cb4eff696d8ea5.png

comm.h和comm.c是公共模块。

test1.h和test1.c使用了公共模块。

test2.h和test2.c使用了公共模块。

test.h和test.c使用了test1模块和test2模块。

这样最终程序中就会出现两份comm.h的内容。

这样就造成了头文件的重复包含。

而我们知道:

一个文件被包含一次,就会被编译一次;包含10次,就编译10次,所以一个头文件如果被重复包含,就会导致编译时间增加,或者其它的一些错误

那如何解决这个问题呢?

两个方法!

10.2.1 条件编译

每个头文件中加上:

#ifndef __TEST_H__ 这个符号是我们自己命名的
#define __TEST_H__
  //头文件的内容
#endif

什么意思呢?解释一下:


加上这样一个条件编译之后,我们知道#ifndef __TEST_H__的作用是如果这个符号没定义,后面的代码才会参与编译。

所以,第一次包含这个头文件的时候,符号还没定义,后面的代码(#endif之前的)会参加编译,而下一句代码#define __TEST_H__就会定义这个符号。

这样如果我们以后再次包含了这个头文件,此时这个符号已经定义了,那么头文件的内容就不会在参与编译了。


这样就可以避免一个头文件被重复包含。


不过,这是一种比较古老的方法,现在,我们可以用一种更简便的方法。

10.2.2 #pragma once

这个方法是:

在头文件中加上这句代码:#pragma once,就可以避免头文件被重复包含。

其实现在vs上,我们新创建一个头文件,它里面自动就会加上这句代码:

4833930c28c84b2b8d5cc13245afc832.png

63fb1d087e264a008fcdb9a034e38c26.png

所以,以后我们自己定义的头文件,最好都加上#pragma once,就可以很好的避免头文件被重复包含。

好的,那这篇文章的内容就到这里,希望能帮助到大家,如果有些的不好的地方,也欢迎各位大佬指正,我们一起进步!!!

a62a447869ef4c94a8e755ee3ed2dd6e.png

目录
相关文章
|
2月前
|
编译器 C++
C++语言预处理器学习应用案例
【4月更文挑战第8天】C++预处理器包括条件编译、宏定义和文件包含等功能。例如,条件编译用于根据平台选择不同代码实现,宏定义可简化常量和变量名,文件包含则用于整合多个源文件。示例中展示了如何使用`#ifdef`等指令进行条件编译,当`DEBUG`宏定义时,`PRINT_LOG`会打印调试信息,否则不执行。
27 1
|
19天前
|
编译器 C语言
C primer plus 学习笔记 第16章 C预处理器和C库
C primer plus 学习笔记 第16章 C预处理器和C库
|
12月前
|
编译器 C++
C进阶:预处理(下)
C进阶:预处理(下)
56 0
|
2月前
|
C语言
【C语言进阶篇】你真的了解预处理吗? 预处理详细解析
【C语言进阶篇】你真的了解预处理吗? 预处理详细解析
44 0
|
11月前
预处理的学习
预处理的学习
37 0
|
12月前
|
自然语言处理 编译器
C进阶:预处理(上)
C进阶:预处理
48 0
|
Linux C语言 C++
【C进阶】——预处理详解(一)
【C进阶】——预处理详解(一)
53 0
|
编译器 C语言 C++
C语言编程—预处理器
预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
68 0
|
编译器 C++
c++入门篇之C++ 预处理器
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。 所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
|
编译器
【学习笔记之我要C】预处理
【学习笔记之我要C】预处理
64 0