文件包含的本质、预处理符号、# vs ##

news/2024/10/30 15:31:51/

何为头文件?

        在C语言中,文件包含是一种常见的编程技术,它允许程序员在一个源文件中使用另一个源文件中的函数或变量。

        文件包含通常使用`#include`预处理指令来实现。`#include`指令告诉预处理器将文件的内容插入到当前文件的指定位置中。

        例如,在一个C源文件中,如果想要使用另一个源文件中的函数,可以使用以下语句:

#include "otherfile.c"

        这个语句会告诉编译器将`otherfile.c`中的代码插入到当前文件的位置,然后再进行编译。当编译器遇到调用`otherfile.c`中的函数时,它能够找到函数的定义,并将它们编译到可执行文件中。

        需要注意的是,文件包含应该遵循一些最佳实践:

  • 为了避免重复包含,应该使用头文件而不是源文件进行文件包含。例如,使用`#include "otherfile.h"`而不是`#include "otherfile.c"`。
    #include "otherfile.h"  //使用
    #include "otherfile.c"  //不使用
  • 应该避免在头文件中放置函数或变量的定义。头文件应该只包含函数和变量的声明。
  • 应该避免在头文件中使用全局变量。全局变量会在包含文件的每个源文件中创建一个独立的实例,这样可能会导致命名冲突和意外行为。

为何所有头文件,都推荐写入下面代码?本质是为什么?

#ifndef XXX
#define XXX//TODO#endif

        这是为了避免头文件重复包含多次,导致编译错误或者不必要的浪费。当一个头文件被多次包含时,如果没有预处理器指令的保护,就会重复定义同一个符号,从而出现编译错误。

        为了避免这种问题,使用了 `#ifndef`、`#define`、`#endif` 三个预处理器指令,将头文件的内容包含在一个条件编译的块中。第一次包含头文件时,`XXX`未被定义,`#ifndef` 判断为真,进入条件编译块,`#define XXX` 定义符号 `XXX`,然后包含头文件的内容。

        当再次包含同一头文件时,`XXX`已被定义,`#ifndef` 判断为假,直接跳过条件编译块,从而避免了重复定义的问题。

#include究竟干了什么?

#include本质是把头文件中相关内容,直接拷贝至源文件中!

那么,在多文件包含中,有没有可能存在头文件被重复包含,乃至被重复拷贝的问题呢?

test.h
#ifndef _TEST_H_
#define _TEST_H_ //注意,这里没有包含<stdio.h>防止信息太多干扰我们
extern void show(); //任意一个函数声明
#endif
test.c
#include "test.h" //故意包含两次
#include "test.h"
int main()
{return 0;
}

经过预编译后的结果

# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "test.c"
# 1 "test.h" 1 //test.h只被包含了1次extern void show();
# 2 "test.c" 2
int main()
{return 0;
}

但是当我们去掉条件编译呢?

# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "test.c"
# 1 "test.h" 1extern void show(); //内容被拷贝第一次
# 2 "test.c" 2
# 1 "test.h" 1extern void show(); //内容被拷贝第二次
# 3 "test.c" 2
int main()
{return 0;
}

结论:

        所有头文件都必须带上条件编译,防止被重复包含! 那么,重复包含一定报错吗?不会! 重复包含,会引起多次拷贝,主要会影响编译效率!同时,也可能引起一些未定义错误,但是特别少。

#error 预处理

`#error`指令是C语言预处理器中的一个预编译指令,用于在预处理阶段产生编译错误。

#include <stdio.h>
#define __welcome
int main()
{
#ifdef __welcome
#error 老铁,非常感谢观看此篇文章哟!!!
#endifreturn 0;
}

        程序使用了条件编译指令`#ifdef`和`#endif`来判断`__welcome`宏是否已经定义。如果已经定义,则使用`#error`指令生成一个编译错误,输出一条提示信息。

#include <stdio.h>
//#define __welcome
int main()
{
#ifndef __welcome
#error 老铁,非常感谢观看此篇文章哟!!!
#endifreturn 0;
}

        程序定义了一个名为`__welcome`的宏,并使用了条件编译指令`#ifndef`和`#endif`来判断`__welcome`宏是否已经定义。如果未定义,则使用`#error`指令生成一个编译错误,输出一条提示信息。

结论:核心作用是可以进行自定义编译报错。

#line 预处理

 `#line`指令是C语言预处理器中的一个预编译指令,用于更改源代码中的行号和文件名,从而影响编译器错误和警告信息中的行号和文件名。

//本质其实是可以定制化你的文件名称和代码行号,很少使用
#include <stdio.h>
int main()
{printf("%s, %d\n", __FILE__, __LINE__); //C预定义符号,代表当前文件名和代码行号
#line 60 "welcome.h" //定制化完成printf("%s, %d\n", __FILE__, __LINE__);return 0;
}

        程序使用了`#line`指令将当前行号设置为60,并将当前文件名设置为`"welcome.h"`,从而定制化了文件名称和代码行号。在后续的`printf`函数中,预处理器会将`__FILE__`和`__LINE__`再次替换成定制化后的文件名和行号,从而输出新的信息。

#pragma 预处理

`#pragma`指令是一种不可依赖的、非标准的预处理指令,它通常被编译器用来提供一些与平台、编译器或者其他特殊需求相关的功能。

一些常见的`#pragma`指令包括:

  1.  `#pragma once`:告诉编译器只包含一次某个头文件,避免重复定义。
  2.  `#pragma GCC optimize`:指示GCC编译器优化代码。
  3.  `#pragma warning`:指示编译器输出警告信息。
  4.  `#pragma pack`:指示编译器对结构体进行字节对齐。
  5.  `#pragma message()`:可以用来进行对代码中特定的符号(比如其他宏定义)进行是否存在进行编译时消息提醒。
#include <stdio.h>
#define READ 
int main()
{
#ifdef READ
#pragma message("谢谢宝子阅读文章!!!")
#endifreturn 0;
}

 # 运算符

`#`运算符是C/C++语言中的一个预处理运算符,用于将宏定义参数转换成字符串常量。

#include<stdio.h>
int main()
{printf("hello world\n");printf("hello""world""\n");const char* msg = "hello""world""\n";//printf("%s\n",msg);printf(msg);return 0;
}

结论:相邻字符串自动连接特性 

#include<stdio.h>
#define STR(s) #s
int main()
{printf("PI = "STR(3.1415926)"\n");return 0;
}

 ## 预算符

`##`预算符是C/C++语言中的一个预处理运算符,用于将两个符号拼接成一个新的符号。

#include<stdio.h>
#define XNAME(n) student##n
int main()
{XNAME(1);XNAME(2);XNAME(3);XNAME(4);XNAME(5);XNAME(6);return 0;
}

 ##的实质:将##相连的两个符号,连接成为一个符号。

小练习实例:计算一个的科学计数法值

#include<stdio.h>#define CONT(x,n) (x##e##n)
int main()
{//计算浮点数科学计数法,相当于1.1 * (10^2)printf("%f\n", 1.1e2);printf("%f\n", CONT(1.1, 2)); return 0;
}

这段代码定义了一个宏`CONT`,用于将两个参数拼接成一个科学计数法格式的浮点数。在`main`函数中,首先使用`1.1e2`的科学计数法直接输出了`110.000000`,然后使用`CONT(1.1, 2)`宏对参数进行拼接,得到了相同的结果。由于`1.1`和`2`经过了拼接,因此最终展开的结果相当于`1.1e2`,即`110.000000`。这说明了`##`预算符可以用于将数字、字符串、变量名等不同类型的记号拼接在一起,从而得到想要的结果。

#include <stdio.h>#define CONCAT(x, y) x##yint main() {int xy = 10;printf("%d\n", CONCAT(x, y));//10return 0;
}

        使用`##`运算符和`CONCAT`宏定义输出变量`xy`的值。首先,在`main`函数中定义了一个名为`xy`的整型变量,并赋值为`10`。然后,通过`CONCAT(x, y)`宏调用,将参数`x`和`y`拼接在一起,得到了记号`xy`。最后,使用`printf`函数输出了`xy`变量的值,结果为`10`。由此可见,`##`运算符可以将字符串、变量名等记号拼接在一起,从而实现更加灵活的程序设计。


http://www.ppmy.cn/news/83611.html

相关文章

029:Mapbox GL绘制铁路黑白交替的线段

第029个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+mapbox中加载数据显示铁路标识的那种黑白交替的线段。 直接复制下面的 vue+mapbox源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共94行)相关API参考:专栏目标示例效果 配置方式 1)…

arthas使用分析

一 简介 Arthas 是一款线上监控诊断产品&#xff0c;通过全局视角实时查看应用 load、内存、gc、线程的状态信息&#xff0c;并能在不修改应用代码的情况下&#xff0c;对业务问题进行诊断&#xff0c;包括查看方法调用的出入参、异常&#xff0c;监测方法执行耗时&#xff0c…

【学习记录】大数据课程-学习十六周总结

4.2.4.外部表操作 在创建表的时候可以指定external关键字创建外部表,外部表对应的文件存储在location指定的hdfs目录下,向该目录添加新文件的同时&#xff0c;该表也会读取到该文件(当然文件格式必须跟表定义的一致)。 外部表因为是指定其他的hdfs路径的数据加载到表当中来&am…

Java15:枚举和注解

一&#xff1a;枚举&#xff1a; 1.枚举类的使用&#xff1a; 》枚举类的理解&#xff1a;类的对象只有有限个&#xff0c;确定的。我们称为枚举类 》当需要定义一组常量时&#xff0c;强烈建议使用枚举类 》如果枚举类中只有一个对象&#xff0c;则可以作为单列模式的方式…

拉货搬家货运APP开发分析和功能列表

作为国家经济发展的重要基础设施&#xff0c;物流行业正在面对转型升级的风口。巨大的市场体量&#xff0c;也迎来了激烈的市场竞争。为了从同质化的服务中脱颖而出&#xff0c;开拓更大的市场&#xff0c;并且解决线下司机的载货痛点&#xff0c;货运APP的开发必不可少。 开发…

workquue

参考 讲解Linux内核工作队列workqueue源码分析 - 知乎 浅谈Linux内核中断下半部——工作队列&#xff08;work queue&#xff09; - 知乎 kernel/workqueue.c 初始化 /** 6004 * workqueue_init_early - early init for workqueue subsystem 6005 * 6006 * This is th…

【阿里云】文本转语音— 阿里云语音合成,文本转语音工具类

目录 一、导入SDK坐标 二、文本转语音 —步骤和工具类 1.登录阿里云账号&#xff0c;创建一个语音合成项目和创建用户并设置权限。 2.获取appKey、accessKeyId、accessKeySecret在相应位置进行填写。 3.相应位置填写自己需要保存的本地路径。意思就是说&#xff0c;自己定…

奥特曼的变身能力如何启发卷积神经网络的图像识别能力?

目录 一、引言 二、奥特曼的变身能力 1. 奥特曼的变身过程 2. 奥特曼的变身能力的启示 三、卷积神经网络的图像识别能力 1. 卷积神经网络的基本原理 2. 卷积神经网络在图像识别中的应用 四、奥特曼的变身能力如何启发卷积神经网络的图像识别能力 1. 奥特曼的变身能力与…