正确的头文件礼仪

正确的头文件礼仪

介绍

任何使用 c++ 或 c++ 编程的人都知道,组成 api 的常量、宏、类型、结构(或类)和函数声明被放入 头文件 通常具有 .h (或有时为 c++ 的 .hpp)文件扩展名。

然而,许多解释都忽略了头文件中的代码应该如何组织,包括包含其他头文件的顺序。这对于帮助最大限度地提高编译速度和整体可维护性很重要。

c++20 添加了模块,但那是另一个故事了。 鉴于存在大量 c 和 c++20 之前的代码,头文件将继续存在一段时间。

包括警卫

基本的头文件如下:

// foo.h#ifndef foo_h#define foo_h// ... declarations ...#endif /* foo_h */

也就是说,所有声明都应该位于 include guard 中: #ifndef x, #define x, #endif /* x */ 序列,其中 x 是代码库中的唯一名称并派生从文件名。

包含防护的要点是,如果多次包含特定头文件,则编译器不会收到多个声明错误,因为预处理器将忽略防护中已定义的所有内容。

包含保护名称的命名法并不重要:只需选择一种不太可能与系统或第三方标头中使用的名称发生冲突的方法 – 并且保持一致。

#endif 之后的注释当然不是必需的,但为了可读性,最好总是重复 #ifndef(或 #ifdef 或 #if)中使用的条件。

然后在所有使用该标头的 .c(或 .cpp)文件中,只需 #include 它:

// foo.c#include "foo.h"// ... definitions ...

不幸的是,这通常是许多头文件解释停止的地方。有效地创建和使用头文件远不止这些。

自给自足的标头

在继续之前,我想定义头文件自给自足

意味着什么自给自足的标头 是指如果将其自身包含到 .c(或 .cpp)文件中,则该文件将在编译时不会出现错误(具体来说,不会出现“未声明”错误)。

例如,一个简单的程序,例如:

#include "foo.h"int main() {}

只有当 foo.h 是自给自足的时候,编译才会没有错误。

在标头中包含其他标头

通常,头文件需要包含其他头文件,因为声明使用了其他头文件中的其他声明。

在头文件中:

首先包含其他本地标头(如果有),然后是系统标头(如果有)。

例如:

// color.h#ifndef cdecl_color_h#define cdecl_color_h#include "config.h"  // correct: #include local headers ...#include "strbuf.h"#include "util.h"#include    // ... before system headers.// ...#endif /* cdecl_color_h */

本地标头(用“”括起来的)(如果有的话)放在前面,然后是系统标头(用 括起来的)(如果有的话)。

为什么?因为这有助于确保每个头文件都是自给自足的。 例如,如果您将系统标头放在前面:

#include    // wrong: #include of system headers ...#include "strbuf.h"  // ... before local headers.// ...

那么 strbuf.h 中的声明就可以“意外”使用 stdio.h 中的声明(例如 file),而无需 strbuf.h 本身包括 stdio.h。

这将无限期地继续工作,但如果在某个时候您不再需要 color.h 中的 stdio.h 并因此删除 #include ,那么您将在 strbuf.h 中收到“未声明”错误。 直到此时,您永远不会注意到 strbuf.h 不是自给自足的。

一旦您注意到,它很容易修复,但最好首先通过始终在系统标头之前包含本地标头来避免该问题。

前向声明而不是包含

在 c 头文件中:

如果您仅通过指针使用在另一个标头中声明的结构或联合类型,请前向声明该类型而不是包含其他标头。

例如,如果您的标头 print.h 使用标准标头 pwd.h 中声明的 passwd 结构,但仅通过指针(并且您不需要 pwd.h 中的任何其他内容),则前向声明 passwd 而不是包含 pwd。小时:

// print.hstruct passwd;  // instead of: #include void print_passwd( struct passwd *pw );

为什么? 当您只需要一个声明时,它节省了预处理器必须打开 pwd.h 的时间以及编译器必须解析整个文件的时间。 对于大型 c 或 c++ 代码库,时间会增加。

c++ 的等效指南类似,但包括类和引用:

如果您仅通过指针或引用使用在另一个标头中声明的结构、联合或类类型,请前向声明该类型,而不是包含其他标头。

包括一切必要的东西

在头文件中:

必须包含它需要自给自足的所有其他标头(或前向声明)。

永远不要强迫您的标头的用户必须在您的标头之前包含一些其他标头,以便编译时不会出现错误。

bsd 派生的操作系统历来倾向于违反此准则。 这样做的理由是,这是帮助最大化编译速度的另一种方法。 它通过强迫成为人类包括守卫来做到这一点。

例如:

#include #include         // needs #include      // needs  too

pwd.h 和 unistd.h 各自执行#include ,而是依赖自己执行包含操作。

这有什么帮助? 它消除了预处理器必须打开 sys/types.h、读取文件、遇到包含防护并忽略其余内容(如果之前已见过该防护)的步骤(如 unistd.h 的情况) .

因此,虽然它确实有帮助,但代价是它迫使用户必须记住手动包含文件,这可能会导致不必要的包含,从而减慢编译速度。 例如,如果在某个时候您删除了 pwd.h 和 unistd.h 的包含内容,则可能会导致不再需要 sys/types.h,但您可能会忘记删除它。

与计算机科学中的许多其他事物一样,这是一种权衡。 bsd 派生的操作系统已经放弃了这种做法,并使标头自给自足。

子目录

大型代码库通常将代码划分到子目录中,每个子目录包含一组相关文件。 对于 #include “…”,预处理器仅在当前目录中查找不在其子目录中查找

要在子目录中使用标头,有两种选择:

使用引号之间的子目录名称;或:告诉编译器也查看子目录。

第一个示例是:

#include "subsystem/out_q.h"

执行第二个操作是特定于编译器的,但对于基于 unix 的编译器(例如 gcc 和 clang),您通常会添加 -isubsystem 形式的命令行选项,将子系统添加到编译器包含路径.

这两种方法都可以,但如果您采用第二种方法,头文件名必须在整个代码库中是唯一的。 如果不同子目录中的两个标头具有相同的名称,则包含其中一个标头将仅包含编译器包含路径中较早的标头。

仅大小写差异

另一件事要做的是:

不要有名称不同的文件大小写不同。

例如,有out_q.h out_q.h。为什么不呢?

很容易写错。在不区分大小写但保留大小写的文件系统(例如 apfs 和 hfs+)上,此类文件被视为相同文件。

对于第二个问题,这可能意味着即使您包含 out_q.h,如果 out_q.h 在包含路径中排在第一位,您最终也可能会包含 out_q.h。

切勿使用../

你必须永远不要做的一件事是:

切勿在包含路径中使用 ../,例如:

#include "../subsystem/out_q.h"

为什么不呢?

代码库的构建过程可能使用符号链接,并且..可能最终相对于解析路径,而不是原始路径,因此您结束的目录up 包括 from 可能不是您想象的那样。 这可能会导致难以诊断的错误。

如果您的代码库架构良好,代码不应该具有循环依赖关系 – 并且包含路径将被适当设置以防止这种情况。

对于第二个,如果您尝试包含subsystem/out_q.h 并得到“没有这样的文件”,则意味着您不应该包含您的文件中的该文件正在努力,因为这会产生循环依赖。 使用 ../ 只是为了让你的代码编译破坏了这个有意的限制。

循环依赖通常很糟糕,因为它们可能会导致静态初始化顺序惨败。

在 .c 或 .cpp 文件中包含标头

对于 .c(或 .cpp)文件,在头文件中包含标头的所有准则也适用,但需要进行一项调整以包含本地标头:

对于给定的 .c(或 .cpp)文件,例如 foo.c,首先包含其相应的标头 foo.h。

为什么?这确保了 foo.h 是自给自足的。

结论

正确的头文件规范有助于最大限度地提高编译速度和整体可维护性。总结一下:

自给自足的标头 是一个如果将其自身包含到 .c(或 .cpp)文件中,则该文件将编译而不会出现错误(具体来说,没有“未声明”)错误)。

在头文件中,使用包含防护

在头文件中,首先包含其他本地标头(如果有),然后包含系统标头(如果有)。

如果您使用仅通过指针(或 c++ 中的引用)在另一个标头中声明的结构或联合(或 c++ 中的类)类型,请前向声明该类型,而不是包含其他标头。

对于标头,您必须包含它需要自给自足的所有其他标头(或前向声明)。

不要有名称不同的文件大小写不同。

切勿在包含路径中使用 ../。

对于给定的 .c(或 .cpp)文件,例如 foo.c,首先包含其相应的标头 foo.h。

负责任地包含。

以上就是正确的头文件礼仪的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1458648.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
C++ 匿名函数和函数对象的优势对比
上一篇 2025年12月18日 10:47:02
C++ 匿名函数在编程中的灵活运用
下一篇 2025年12月18日 10:47:28

相关推荐

  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • c#文件怎么打开

    打开 C# 文件有三种方法:Visual Studio:启动 Visual Studio,通过“文件”菜单打开 C# 文件。文本编辑器:使用文本编辑器打开 C# 文件,将其视为普通文本。.NET Core 命令行工具:使用 csc.exe 命令行工具编译 C# 文件,生成可执行文件。 如何打开 C#…

    2026年5月10日
    000
  • c++如何实现UDP通信_c++基于UDP的网络通信示例

    UDP通信基于套接字实现,适用于实时性要求高的场景。1. 流程包括创建套接字、绑定地址(接收方)、发送(sendto)与接收(recvfrom)数据、关闭套接字;2. 服务端监听指定端口,接收客户端消息并回传;3. 客户端发送消息至服务端并接收响应;4. 跨平台需处理Winsock初始化与库链接,编…

    2026年5月10日
    100
  • 函数指针在 C++ 多态中的作用:揭示多态背后的真相

    函数指针在 C++ 多态中的作用:揭示多态背后的真相 简介 多态是面向对象编程的一项强大功能,它允许对象在运行时以不同的方式表现。C++ 中的多态实现依赖于函数指针。本文将深入探讨函数指针在多态中的作用,并通过一个实战案例展示如何利用它们。 函数指针 立即学习“C++免费学习笔记(深入)”; 函数指…

    2026年5月10日
    000
  • C++框架与Java框架在易用性方面的比较

    c++++ 框架的易用性低于 java 框架,具体原因如下:c++ 框架学习曲线陡峭,需要深入理解 c++ 语言。易出错且调试困难。而 java 框架具有以下易用性优势:学习曲线低,尤其适合 java 初学者。提供丰富的库和工具,简化开发。运行时异常处理,简化异常处理。 C++ 框架与 Java 框…

    2026年5月10日
    000
  • c++中头文件和源文件的区别_c++头文件与源文件作用对比

    头文件声明接口,源文件实现逻辑。头文件含类、函数声明及宏定义,通过#include被多文件共享,用include守卫防重;源文件实现具体功能,编译为目标文件后由链接器合并。声明与实现分离提升模块化与编译效率,模板和内联函数因需编译时可见故常置于头文件,命名空间避免符号冲突,整体结构使项目更清晰易维护…

    2026年5月10日
    000
  • C++ 函数重载在事件驱动的编程中的应用

    在事件驱动的编程中,函数重载可创建具有不同参数签名的相似功能,为单一函数名提供多样化功能。它包含以下优点:代码可读性:使用单一函数名表示相关任务。可维护性:避免重复编写类似逻辑。可重用性:跨项目和应用程序 reutilizar。 C++ 函数重载在事件驱动的编程中的应用 在事件驱动的编程中,函数重载…

    2026年5月10日
    000
  • C++ 函数性能优化对系统稳定性的影响

    标题:C++ 函数性能优化对系统稳定性的影响 简介 函数性能优化是 C++ 程序员提高程序效率的关键技术。本文将探讨函数性能优化对系统稳定性的影响,并提供实战案例来证明这一点。 性能优化对稳定性的作用 立即学习“C++免费学习笔记(深入)”; 函数性能优化不仅可以提升程序速度,还可以提高系统的稳定性…

    2026年5月10日
    000
  • WebAssembly中导入JavaScript函数:无胶水代码集成指南

    本文深入探讨了在WebAssembly模块中直接导入和使用JavaScript函数的机制,特别是当使用Emscripten的STANDALONE_WASM和SIDE_MODULE编译模式时。文章详细分析了TypeError: import object field ‘GOT.mem&#8…

    2026年5月10日
    000
  • C++如何编译和链接_C++从源码到可执行文件的过程解析

    c++kquote>预处理展开宏和头文件,编译生成汇编代码,汇编转为机器码,链接合并目标文件与库生成可执行程序。 当你写完一段C++代码,比如一个简单的hello world程序,最终能运行起来,背后其实经历了一系列步骤:预处理、编译、汇编和链接。这个过程将人类可读的源码转换成机器可以执行的程…

    2026年5月10日
    000
  • c++中sizeof运算符的用法和常见陷阱 _c++ sizeof使用技巧及陷阱解析

    sizeof运算符在编译时计算类型或对象的字节大小,返回size_t类型,常用于获取数据大小、数组元素个数及内存操作;但存在数组传参退化为指针导致失效、对指针无法获知动态内存大小、表达式不求值、结构体因对齐产生填充等常见陷阱;需结合模板、显式传参、对齐控制等方式规避问题,提升代码可移植性和安全性。 …

    2026年5月10日
    000
  • C#如何进行网络编程?Socket与TCP/IP通信编程实例详解

    C#通过Socket类实现TCP通信,首先服务器绑定IP和端口并监听,客户端发起连接,双方通过Send/Receive收发数据,最后关闭连接。 C# 进行网络编程主要依赖于 System.Net 和 System.Net.Sockets 命名空间,其中最核心的是使用 Socket 类实现基于 TCP…

    2026年5月10日
    000
  • C++ 函数递归详解:递归查找列表中的元素

    递归查找列表元素的步骤如下:递归基础条件:如果列表为空,则元素不存在。递归过程:使用递归调用查找列表的剩余部分,并调整返回的索引。检查列表的第一个元素:如果第一个元素与所查找的元素相等,则元素位于索引 0 处。找不到:如果递归和第一个元素检查都没有找到,则元素不存在。 C++ 函数递归详解:递归查找…

    2026年5月10日
    000
  • C++怎么使用C++17的并行算法库_C++ std::execution与多核性能优化

    c++kquote>C++17通过std::execution策略引入并行算法支持,需编译器(如GCC 8+)和线程库(如TBB)配合;提供seq、par、par_unseq三种策略控制执行模式;可用于sort、for_each等算法提升大数据性能,但需避免数据竞争,推荐使用reduce等安全…

    2026年5月10日
    000
  • c++ lambda表达式怎么写 c++匿名函数用法详解

    答案是lambda表达式可简洁定义匿名函数,用于STL算法等场景。其语法包含捕获列表、参数列表、mutable、返回类型和函数体,如[=](int x) { return x > 0; }可值捕获外部变量并用于判断正数。 在C++中,lambda表达式是一种创建匿名函数的简洁方式,常用于需要传…

    2026年5月10日
    200
  • C++框架的Unlicense许可类型简介

    unlicense 许可证类型为免费且宽松,允许用户在不附加任何限制的情况下使用、修改和分发软件。它旨在最大限度地减少限制和允许最大的自由度,具有以下好处:简洁易懂高度开放无保证 C++ 框架的 Unlicense 许可证类型简介 了解 Unlicense Unlicense 是一个自由和宽松的软件…

    2026年5月10日
    000
  • 利用日志记录增强 C++ 函数的调试能力

    如何利用日志记录增强 c++++ 函数的调试能力?使用 glog 库进行日志记录: 安装 glog,并在代码中使用 glog 头文件和 initgooglelogging() 初始化日志记录。添加日志记录语句: 使用 log() 宏在要记录的代码块中添加日志记录语句,以记录函数开始、结束或其他重要事…

    2026年5月10日
    000
  • C++ 函数模板如何使用并在实际场景中应用?

    函数模板允许您定义可以处理不同类型参数的函数的通用版本。语法为:template,其中 t 是类型参数。要使用函数模板,请指定所需的参数类型,例如:max(10, 20)。函数模板在排序等实际应用中很有用,例如:template void sort(t arr[], int size)。它们具有通用…

    2026年5月10日
    000
  • C++ 并发编程中内存访问问题及解决方法?

    在 c++++ 并发编程中,共享内存访问问题包括数据竞争、死锁和饥饿。解决方案有:原子操作:确保对共享数据的访问是原子性的。互斥锁:一次只允许一个线程访问临界区。条件变量:线程等待某个条件满足。读写锁:允许多个线程并发读取,但只能允许一个线程写入。 C++ 并发编程中的内存访问问题及解决方案 在多线…

    2026年5月10日
    000
  • c++如何实现函数的重载_c++函数重载实现方法

    函数重载通过参数列表差异实现,如类型、数量或顺序不同,编译器根据实参选择对应函数,返回类型不同不能单独用于重载。 在C++中,函数重载允许在同一作用域内定义多个同名函数,只要它们的参数列表不同(参数个数、类型或顺序不同),编译器会根据调用时传入的实参来选择匹配的函数。函数重载不能仅通过返回类型的不同…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信