当前位置: 首页 > article >正文

C++学习笔记----10、模块、头文件及各种主题(四)---- 头文件

        在C++20的模块引入之前,头文件作为一项技术用于提供给子系统或代码块的接口。头文件最通用的场景是声明要在别的地方定义的函数。声明告诉编译器有一个带有确定名字的实体(函数,变量等)存在。对于函数来说,声明指定了函数如何被调用,声明了参数的数量与类型以及函数的返回类型。定义告诉编译器带有确定名字的实体存在,并且定义了实体本身。对于函数来讲,定义包含了函数的实体代码。所有的定义都是声明,但是并不是所有的声明都是定义。声明,以及类定义,也是声明,通常位于头文件中,典型的是以.h为扩展名。定义,包括非内联类成员的定义,通常位于源文件中,典型的是以.cpp为扩展名。我们一般情况下都会使用模块,但是本节简要讨论使用头文件的令人头疼的方面,比如避免重复定义与循环依赖,因为会在历史遗留代码库中碰到。

1、单一定义规则(ODR)

        单个翻译单元可以只有一个变量、函数、类类型、枚举类型、概念或者模板的定义。对于有些类型,允许多重声明,但是不允许多重定义。更进一步,在整个程序中只能有一个非内联函数与非内联变量的定义。

        对于头文件,比较容易破坏单一定义规则,造成重复定义。下一节我们讨论如何通过头文件避免这样的重复定义。

        在模块之间,破坏单一定义原则是比较难的,因为每一个模块与其它模块进行了更好的隔离。这样做的一个主要原因是在模块中的实体不会从其它模块导出,其他模块拥有模块连接,因此从其它模块的代码无法访问。也就是说,多个模块可以定义自身本地的非导出的实体,可以拥有同样的名字而不会带来问题。另一方面,在非模块源文件中,本地实体缺省有外部连接。当然了,在模块本身之内,仍然需要确保不要破坏单一定义规则。

2、重复定义

        假定A.h包含了Logger.h,定义了Logger类,B.h也包含了Logger.h。如果有一个源文件叫做App.cpp,它包含了A.h与B.h,就会出现Logger类的重复定义,因为Logger.h头文件通过A.h与B.h都包含了。

        这个重复定义的问题可以通过叫做包含哨兵的技术来避免,也叫做头文件哨兵。下面的代码段展示了Logger.h头文件,使用了包含哨兵。在每个头文件的开始,#ifndef指令检查是否定义了特定的键。如果该键已经定义了,编译器就会略过直到匹配的#endif,通常会放置在文件的结尾。如果该键还没有定义,文件处理去定义该键,这样后续包含同样的文件就会被略过。

#ifndef LOGGER_H
    #define LOGGER_H
    class Logger { /* ... */ };
#endif // LOGGER_H

        替代解决方案,目前几乎所有的编译器都支持#pragma once指令,它替换了包含哨兵。在头文件的起始位置放置#pragma once确保它只会被包含一次,因此避免了多次包含头文件导致的重复定义。下面是例子:

#pragma once

class Logger { /* ... */ };

        注意:在一个单独的翻译单元中头文件被多次包含时,包含哨兵与#pragma once指令只是防止了单一定义规则被破坏,而不是多个翻译单元之间。

3、循环依赖

        另一个避免头文件问题的工具是声明传递。如果需要指向一个类,但是不能包含它的头文件(例如,因为它严重依赖你写的类),可以告诉编译器这样的类存在,只是没有通过#include技术提供正式的定义。当然了,在代码中还不能实际使用这个类,因为编译器对这个类一无所知,除非等所有的连接完成之后命名类存在。然而,你仍然可以在代码中使用指针与引用来传递声明类。也可以通过值来声明返回这样的传递声明类的函数,或者拥有传递声明类作为传值函数的参数。当然了,定义了函数的代码与调用该函数的代码都需要包含正确的头文件,它恰当地定义了传递声明类。

        例如,假定Logger类使用了另一个类叫做Preferences,它跟踪用户设置。Preferences类可能反过来使用Logger类,这样就有了一个循环依赖,无法通过包含哨兵解决。在这种情况下需要使用传递声明。在下面的代码中,Looger.h头文件对于Preferences类使用了传递声明,接下来指向Preferences类的就不会再包含它的头文件:

#pragma once
#include <string_view>
class Preferences;    // forward declaration

class Logger
{
public:
    void setPreferences(const Preferences& preferences);
    void logError(std::string_view error);
};

        推荐在头文件中尽可能地使用传递声明而不是包含其它头文件。这样可以降低编译与重编译时间,因为它打破了头文件之间的依赖。当然了,实现文件需要包含正确的头文件,其类型是你要传递声明的;否则的话,编译不会成功。

3、查询头文件是否存在

        查询特定头文件是否存在,使用__has_include(“filename”)或__has_include(<filename>)预处理常量表达式。如果头文件存在结果为1,不存在结果为0.例如,在c++17中<optional>头文件被批准前,有些编译器已经在<experimental/optional>中有了初始版本。可以使用__has_include()来检查下面两个头文件中的哪一个在你的系统中可用:

#if __has_include(<optional>)
    #include <optional>
#elif __has_include(<experimental/optional>)
    #include <experimental/optional>
#endif

4、模块导入声明

头文件不应包含任何模块导入声明。标准严格要求模块import声明必须在文件的开头,在任何其它声明之前,必须不能来自于头文件包含或预处理宏扩展。这使得易于构建系统来发现模块依赖,然后用于决定模块需要构建的顺序。


http://www.kler.cn/a/385007.html

相关文章:

  • WorkFlow源码剖析——Communicator之TCPServer(中)
  • 使用 API 和离线库查询 IP 地址方法详解
  • (蓝桥杯C/C++)——基础算法(下)
  • Spark 的介绍与搭建:从理论到实践
  • C语言 | Leetcode C语言题解之第542题01矩阵
  • 状态模式(State Pattern)详解
  • 论文阅读《Structure-from-Motion Revisited》
  • Excel 无法打开文件
  • 【计网】实现reactor反应堆模型 --- 框架搭建
  • 【论文复现】基于深度学习的手势识别算法
  • 【AI写作宝-注册安全分析报告-无验证方式导致安全隐患】
  • 单细胞 RNA 测序分析的当前最佳实践:教程-文献精读80
  • Elasticsearch可视化工具Elasticvue插件用法
  • JavaWeb项目-----博客系统
  • 如何修改WordPress经典编辑器的默认高度?
  • Prompt 工程
  • 漫漫数学之旅038
  • 贪心算法习题其四【力扣】【算法学习day.21】
  • 推荐一款PowerPoint转Flash工具:iSpring Suite
  • git clone,用https还是ssh
  • Go语言的常用内置函数
  • Webserver(4.9)本地套接字的通信
  • Mysql常用语法一篇文章速成
  • TCP/IP协议介绍
  • RAG(检索增强生成)的实现流程;RAG怎么实现检索增强的
  • 大型语言模型(LLM)的小型化研究进展