【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

目录
相关文章
|
4月前
|
人工智能 自然语言处理 安全
阿里云万小智AI建站:基础版、标准版、企业版主要功能及价格对比和选择参考
阿里云万小智 AI 建站是一款基于 AI 驱动的自助建站产品,无需代码基础,通过可视化拖拽与 AI 对话即可快速构建高性能、多语言、安全合规的网站。系统深度集成阿里云 ECS、RDS、OSS、CDN、SLB 与 Web 应用防火墙,保障高可用性、数据安全与全球访问速度。其提供多个版本,精准匹配从个人工作室到中大型企业的差异化需求。
764 167
|
4月前
|
存储 弹性计算 人工智能
阿里云免费云服务器领取教程及阿里云免费云产品全解析:从资源配置到实用指南
在云计算普及的当下,阿里云作为国内领先的云服务提供商,长期推出免费云产品试用体系,覆盖从基础设施到上层应用的全场景需求,为个人开发者、学生及初创企业降低了上云门槛。然而,免费资源背后往往存在配置限制、合规要求等细节问题,用户需结合自身场景理性选择。本文基于阿里云官方规则与真实使用反馈,系统梳理免费云产品的资源矩阵、使用体验、常见风险及适配场景,为不同需求的用户提供全面参考。
|
4月前
|
运维 数据库 数据安全/隐私保护
DAS Agent、MCP Server 与 Dify 集成,实现跨账号数据库智能运维!
针对多账号管理难题,通过DAS Agent、MCP Server 与 Dify 集成,实现跨阿里云账号数据库实例的统一智能运维。主账号可纳管其他账号的DAS Agent,集中生成运维日报并推送至钉钉,提升集团级数据库管理效率。
|
9月前
|
安全 C语言
C语言中的字符、字符串及内存操作函数详细讲解
通过这些函数的正确使用,可以有效管理字符串和内存操作,它们是C语言编程中不可或缺的工具。
419 15
|
人工智能 Java 程序员
一文彻底搞清楚C语言的函数
本文介绍C语言函数:函数是程序模块化的工具,由函数头和函数体组成,涵盖定义、调用、参数传递及声明等内容。值传递确保实参不受影响,函数声明增强代码可读性。君志所向,一往无前!
584 1
一文彻底搞清楚C语言的函数
|
小程序 开发者
微信小程序跳转微信小程序,使用微信开发者工具的测试记录
本文是博主学习微信小程序的记录,希望对大家有所帮助。
921 0
微信小程序跳转微信小程序,使用微信开发者工具的测试记录
|
存储 编译器 C语言
【C语言】C语言的变量和声明系统性讲解
在C语言中,声明和定义是两个关键概念,分别用于告知编译器变量或函数的存在(声明)和实际创建及分配内存(定义)。声明可以多次出现,而定义只能有一次。声明通常位于头文件中,定义则在源文件中。通过合理组织头文件和源文件,可以提高代码的模块化和可维护性。示例包括全局变量、局部变量、函数、结构体、联合体、数组、字符串、枚举和指针的声明与定义。
687 12
|
存储 网络协议 编译器
【C语言】深入解析C语言结构体:定义、声明与高级应用实践
通过根据需求合理选择结构体定义和声明的放置位置,并灵活结合动态内存分配、内存优化和数据结构设计,可以显著提高代码的可维护性和运行效率。在实际开发中,建议遵循以下原则: - **模块化设计**:尽可能封装实现细节,减少模块间的耦合。 - **内存管理**:明确动态分配与释放的责任,防止资源泄漏。 - **优化顺序**:合理排列结构体成员以减少内存占用。
1177 14
|
存储 算法 C语言
【C语言】字符常量详解
字符常量是C语言中处理字符数据的重要工具。通过单引号括起一个字符,我们可以方便地使用字符常量进行字符判断、字符运算和字符串处理等操作。理解字符常量的表示方法、使用场景和ASCII码对应关系,对于编写高效的C语言程序至关重要。
1407 11
|
存储 算法 C语言
C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项
本文深入探讨了C语言中常见的字符串处理技巧,包括字符串的定义、初始化、输入输出、长度计算、比较、查找与替换、拼接、截取、转换、遍历及注意事项,并通过案例分析展示了实际应用,旨在帮助读者提高编程效率和代码质量。
850 4
下一篇
开通oss服务