C++两个类的头文件相互包含

2023-11-18

C++中头文件相互包含的几点问题

一、类嵌套的疑问

C++头文件重复包含实在是一个令人头痛的问题,前一段时间在做一个简单的数据结构演示程序的时候,不只一次的遇到这种问题。假设我们有两个类A和B,分别定义在各自的有文件A.h和B.h中,但是在A中要用到B,B中也要用到A,但是这样的写法当然是错误的:class B;

class A{      public:          B b;};

class B{      public:          A a;};因为在A对象中要开辟一块属于B的空间,而B中又有A的空间,是一个逻辑错误,无法实现的。在这里我们只需要把其中的一个A类中的B类型成员改成指针形式就可以避免这个无限延伸的怪圈了。为什么要更改A而不是B?因为就算你在B中做了类似的动作,也仍然会编译错误,表面上这仅仅上一个先后顺序的问题。      为什么会这样呢?因为C++编译器自上而下编译源文件的时候,对每一个数据的定义,总是需要知道定义的数据的类型的大小。在预先声明语句class B;之后,编译器已经知道B是一个类,但是其中的数据却是未知的,因此B类型的大小也不知道。这样就造成了编译失败,VC++6.0下会得到如下编译错误:      error C2079: 'b' uses undefined class 'B'将A中的b更改为B指针类型之后,由于在特定的平台上,指针所占的空间是一定的(在Win32平台上是4字节),这样可以通过编译。

二、不同头文件中的类的嵌套

      在实际编程中,不同的类一般是放在不同的相互独立的头文件中的,这样两个类在相互引用时又会有不一样的问题。重复编译是问题出现的根本原因。为了保证头文件仅被编译一次,在C++中常用的办法是使用条件编译命令。在头文件中我们常常会看到以下语句段(以VC++6.0自动生成的头文件为例):

#if !defined(AFX_STACK_H__1F725F28_AF9E_4BEB_8560_67813900AE6B__INCLUDED_)#define AFX_STACK_H__1F725F28_AF9E_4BEB_8560_67813900AE6B__INCLUDED_      //很多语句……#endif

其中首句#if !defined也经常做#ifndef,作用相同。意思是如果没有定义过这个宏,那么就定义它,然后执行直到#endif的所有语句。如果下次在与要这段代码,由于已经定义了那个宏,因此重复的代码不会被再次执行。这实在是一个巧妙而高效的办法。在高版本的VC++上,还可以使用这个命令来代替以上的所有:      #pragma once它的意思是,本文件内的代码只被使用一次。

      但是不要以为使用了这种机制就全部搞定了,比如在以下的代码中:

//文件A.h中的代码#pragma once

#include "B.h"

class A{      public:          B* b;};

//文件B.h中的代码#pragma once

#include "A.h"

class B{      public:          A* a;};

这里两者都使用了指针成员,因此嵌套本身不会有什么问题,在主函数前面使用#include "A.h"之后,主要编译错误如下:      error C2501: 'A' : missing storage-class or type specifiers仍然是类型不能找到的错误。其实这里仍然需要前置声明。分别添加前置声明之后,可以成功编译了。代码形式如下:

//文件A.h中的代码#pragma once

#include "B.h"

class B;

class A{      public:          B* b;};

//文件B.h中的代码#pragma once

#include "A.h"

class B;

class B{      public:          A* a;};

这样至少可以说明,头文件包含代替不了前置声明。有的时候只能依靠前置声明来解决问题。我们还要思考一下,有了前置声明的时候头文件包含还是必要的吗?我们尝试去掉A.h和B.h中的#include行,发现没有出现新的错误。那么究竟什么时候需要前置声明,什么时候需要头文件包含呢?

三、两点原则(非常重要)

      头文件包含其实是一想很烦琐的工作,不但我们看着累,编译器编译的时候也很累,再加上头文件中常常出现的宏定义。感觉各种宏定义的展开是非常耗时间的,远不如自定义函数来得速度。我仅就不同头文件、源文件间的句则结构问题提出两点原则,仅供参考:

第一个原则应该是,如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。

第二个原则应该是,尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并便宜成功,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分(H文件)。

如下在C++ primer(第五版)中的StrBlob中,chapter 12动态数组

如果把StrBlob中的成员函数begin()和end()写成

StrBlobPtr  begin(){ return StrBlobPtr(*this);}StrBlobPtr end(){auto ret = StrBlobPtr(*this, data->size());return ret;}

但是将这两个成员函数的实现写在类中,由于会用到StrBlobPtr的成员函数,所以要引用“StrBlobPtr.h”的头文件,所以会出现相互引用。

StrBlob.h 如下:

#ifndef STRBLOB_H
#define STRBLOB_H

#include <vector>
#include <memory>
//#include "StrBlobPtr.h"  //当两个头文件相互包含的时候,编译是通不过的。

using namespace std;
class StrBlobPtr;

class StrBlob{
	friend ostream& operator<<(ostream &out, const StrBlob &blob);
	friend class StrBlobPtr;
public:
	typedef vector<string>::size_type size_type;
	StrBlob();
	StrBlob(initializer_list<string> il);
	size_type size(){ return data->size(); }
	void push_back(const string &str){ data->push_back(str); }
	void pop_back();
	const string &front()const ;
	const string &back()const ;
	ostream& operator<< (const ostream &out);
	StrBlobPtr begin();//因为,使用StrBlobStr,所以要包含StrBlobPtr头文件 。
	StrBlobPtr end();
private:
	shared_ptr<vector<string>> data;
	void check(size_type i, const string &msg)const;
};
ostream& operator<< (ostream &out,const StrBlob &blob);
#endif

我们通过
class StrBlobPtr;

来声明一下,而不引用 “StrBlobPtr.h”头文件,然后将成员函数begin()和end()在类的定义中声明,而不在其内部实现,放到StrBlobPtr.cpp中去实现。这样就不会出现相互引用了。
StrBlobPtr begin();//因为,使用StrBlobStr,所以要包含StrBlobPtr头文件 。
	StrBlobPtr end();

这两个函数,要使用StrBlobPtr来定义,我们可以将这两个函数放在StrBlob.cpp可实现就可以了。

StrBlob.cpp如下

#include "StrBlob.h"
#include "StrBlobPtr.h"
#include <iostream>
#include <string>

using namespace std;

StrBlob::StrBlob() :data(make_shared<vector<string>>()){}
StrBlob::StrBlob(initializer_list<string> il) : data(make_shared<vector<string>>(il)){}
void StrBlob::check(size_type i, const string &msg) const
{
	if (i >= data->size())
		throw out_of_range(msg);
}
void StrBlob::pop_back()
{
	check(0, "pop_back on empty StrBlob");
	data->pop_back();
}
const string &StrBlob::back() const
{
	check(0, "back on empty StrBlob");
	return data->back();
}

const string &StrBlob::front() const
{
	check(0, "front on empty StrBlob");
	return data->front();
}
ostream &operator<<(ostream &os, const StrBlob &strBlob)
{
	for (auto it = strBlob.data->begin(); it != strBlob.data->end(); it++){
		os << " " << *it;
	}
	return os;
}
StrBlobPtr StrBlob::begin()
{
	return StrBlobPtr(*this);
}

StrBlobPtr StrBlob::end()
{
	auto ret = StrBlobPtr(*this, data->size());
	return ret;
}
所以可以看出,引入头文件的两个原则还是很需要注意的啦:

第一个原则应该是,如果可以不包含头文件,那就不要包含了。这时候前置声明可以解决问题。如果使用的仅仅是一个类的指针,没有使用这个类的具体对象(非指针),也没有访问到类的具体成员,那么前置声明就可以了。因为指针这一数据类型的大小是特定的,编译器可以获知。


第二个原则应该是,尽量在CPP文件中包含头文件,而非在头文件中。假设类A的一个成员是是一个指向类B的指针,在类A的头文件中使用了类B的前置声明并便宜成功,那么在A的实现中我们需要访问B的具体成员,因此需要包含头文件,那么我们应该在类A的实现部分(CPP文件)包含类B的头文件而非声明部分(H文件)。








本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++两个类的头文件相互包含 的相关文章

  • 通过 SocketCAN 进行 boost::asio

    我正在考虑利用升压阿西奥 http www boost org doc libs 1 49 0 doc html boost asio html从a读取数据套接字CAN http en wikipedia org wiki SocketCA
  • 使用 mono/nunit-console/4 在 Mac OS X 控制台上运行测试

    我安装了 Max OS X 10 11 1 上面装有 Xamarin 我编写了简单的测试类 只是为了测试在 Mac OS X 和 Ubuntu 上运行 Nunit 测试 该类实际上有一个返回字符串的方法 using System names
  • 我如何理解这个 C 类型声明?

    double bar int double double double double 在查看讲座幻灯片时 我发现了留给学生的练习 用简单的英语来说 什么是类型bar在这个 C 声明中 Please帮助我解决这个问题 我什至不知道从哪里开始
  • 更改 Qt OpenGL 窗口示例以使用 OpenGL 3.3

    我正在尝试更改 Qt OpenGL 示例以使用更现代的 opengl 版本 330 似乎合适 所以我做了 在 main cpp 上设置版本和配置文件 设置着色器版本 更改着色器以使用统一 它现在构建没有任何错误 但我只看到一个空白窗口 我错
  • EF Core 通过完全替换断开集合导航属性的更新

    使用 EF Core 5 0 我有一个 SPA 页面 可以加载Group实体及其集合Employee来自 API 的实体 var groupToUpdate await context Groups Include g gt g Emplo
  • 类特定的新删除运算符是否必须声明为静态

    标准中是否要求类特定的 new new delete 和 delete 是静态的 我可以让它们成为非静态成员运算符吗 为什么需要它们是静态的 它们被隐式声明为静态 即使您没有键入 static
  • 信号处理程序有单独的堆栈吗?

    信号处理程序是否有单独的堆栈 就像每个线程都有单独的堆栈一样 这是在 Linux C 环境中 来自 Linux 手册页signal 7 http kernel org doc man pages online pages man7 sign
  • GCC 和 ld 找不到导出的符号...但它们在那里

    我有一个 C 库和一个 C 应用程序 尝试使用从该库导出的函数和类 该库构建良好 应用程序可以编译 但无法链接 我得到的错误遵循以下形式 app source file cpp text 0x2fdb 对 lib namespace Get
  • 时间:2019-03-17 标签:c#ThreadSafeDeepCopy

    我一直在阅读很多其他问题以及大量谷歌搜索 但我一直无法找到明确的解决方案 根据我读过的一些最佳实践 类的静态方法应该创建线程安全的 并且实例成员应该将线程安全留给消费者 我想为该类实现深度复制方法 该类本身还有其他引用类型成员 有没有什么方
  • fprintf() 线程安全吗?

    我正在为野人就餐问题的某些变量编写一个 C 解决方案 现在 我创建线程 每个线程都将 FILE 获取到同一个调试文件 在线程内我正在使用 fprintf 进行一些打印 打印的语句不受任何类型的互斥锁等保护 我没有在调试文件中观察到任何交错行
  • 如何获取 QTableView 的标题列表?

    我有一个QTableView我的对话框中的对象 我需要访问该表的水平标题并将它们放入QStringList object 尽管进行了大量搜索 但我在 Qt 文档中找不到如何获取此标头列表 编辑 我发现的最接近的地方是this https w
  • 在 JSQMessagesViewController 中显示 LocationMediaItem

    我刚刚尝试实施LocationMediaItem in my Xamarin iOS应用程序使用JSQMessagesViewController 一切都很顺利 唯一的问题是UICollectionView应该显示位置的单元格永远停留在加载
  • 为什么 set_symmetry_difference 无法与比较器一起使用?

    Example program include
  • 如何从文本文件读取整数到数组

    这就是我想做的 我对此有些不满 但我希望你能容忍我 这对我来说是一个非常新的概念 1 在我的程序中 我希望创建一个包含 50 个整数的数组来保存来自文件的数据 我的程序必须获取用户的文档文件夹的路径 2 文件的名称为 grades txt
  • 如何在服务器端按钮点击时关闭当前标签页?

    我尝试在确认后关闭当前选项卡 因此我将以下代码放在确认按钮的末尾 但选项卡没有关闭 string jScript ClientScript RegisterClientScriptBlock this GetType keyClientBl
  • 将二进制数据从 C# 上传到 PHP

    我想将文件从 Windows C 应用程序上传到运行 PHP 的 Web 服务器 我知道 WebClient UploadFile 方法 但我希望能够分块上传文件 以便我可以监控进度并能够暂停 恢复 因此 我正在读取文件的一部分并使用 We
  • 如何在标准 WPF ListView 中启用 UI 虚拟化

    我正在使用 NET 4 5 VS2012 并且我有一个 ListView 看起来像这样
  • 在 EnvDTE 中调试时捕获 VS 局部变量

    是否可以使用 EnvDTE 进行 vsix Visual Studio 扩展来捕获本地和调试窗口使用的调试数据 或者可以通过其他方法吗 我想创建一个自定义的本地窗口 我们可以修改它以根据需要显示一些较重的内容 而无需为高级用户牺牲原始的本地
  • .NET Core 中的跨平台文件名处理

    如何处理文件名System IO以跨平台方式运行类以使其在 Windows 和 Linux 上运行 例如 我编写的代码在 Windows 上完美运行 但它不会在 Ubuntu Linux 上创建文件 var tempFilename Dat
  • C++ Streambuf 方法可以抛出异常吗?

    我正在尝试找到一种方法来获取读取或写入流的字符数 即使存在错误并且读 写结束时间较短 该方法也是可靠的 我正在做这样的事情 return stream rdbuf gt sputn buffer buffer size 但如果streamb

随机推荐

  • [QT]day3

    1 一个闹钟 widget cpp include widget h include ui widget h include
  • FPGA的基本设计流程

    FPGA开发主要包括系统设计 设计输入 功能仿真 综合优化 综合后仿真 实现与布局布线 时序方针与验证 板级方针与验证 芯片编程与调试等9个部分 如下图所示 1 电路设计 在系统设计之前 首先要进行的是方案论证 系统设计和FPGA芯片选择等
  • PCL—低层次视觉—点云分割(RanSaC)

    点云分割 点云分割可谓点云处理的精髓 也是三维图像相对二维图像最大优势的体现 不过多插一句 自Niloy J Mitra教授的Global contrast based salient region detection出现 最优分割到底鹿死
  • 教程来啦!5分钟快速学习使用Tbarcode Office创建条码!

    Tbarcode Office是一款具有强大功能的条形码插件 无论在 Microsoft Word 还是在 Excel 中设置条码都非常的轻松 TBarCode Office 无缝集成在Microsoft Word和Microsoft Ex
  • Mybatis-Plus 实现用户ID自增出现的问题

    问题描述 项目基于 SpringBoot MybatisPlus 3 5 2 使用数据库自增ID时 出现重复键的问题 自增ID介绍 1 局部式配置 如下述代码所示 通过 TableId 字段来指定自增字段 Value 为数据库字段名 可以大
  • Kotlin与Java的异同(一)

    本文章只为了方便查阅 文章目录 Kotlin简介 Kotlin与Java的异同 1 函数 2 变量 变量类型 可变变量 不可变变量 3 类和属性 有参数的构造方法 setter 和 getter 4 枚举和 when 枚举 when 5 w
  • vim打开文件并跳转

    vim filename n 打开文件并跳转到指定行 vim filename pattern 打开文件并跳转到指定匹配字符
  • openpose&3d-pose-baseline搭建

    1 openpose如果遇到gstreamer的问题 把gstreamer 1 0 apt remove掉以后 重新装gstreamer 0 1 并且打开opencv cmakelist里的相关选项 然后重新make opencv 就解决了
  • 剑指offer-解决面试题的思路

    大家好 我是Nefelibat 在博客上记录算法笔记 是因为想push自己每天坚持刷几道算法题 同时也希望能把自己总结到的经验分享给大家 希望大家阅读愉快 目录 在写代码之前明确自己做的事情是什么 该怎么做 画图让抽象问题形象化 题目 二叉
  • Gin之获取path参数

    文章目录 Gin之获取path参数 1 path参数介绍 2 获取一个 path参数 Param方法 3 获取二个 path参数 Param方法 4 获取 path参数 Param方法 5 获取一个 path参数 ShouldBindUri
  • python语法(高阶)-设计模式(单例模式)

    参考内容 黑马程序员
  • ObjectC基础之块(Block)学习

    用Java语言来说 OC中的Block有着类的感觉 但OC的类与block又有着不同之处 OC的类有 m和 h文件 即 interface 与 implementation 而Block却没有类 但是它有着属性 举个Block例子 Bock
  • 洛谷P1028 [NOIP2001 普及组] 数的计算 —— 简单DP+双指针优化

    This way 题意 给出自然数 n n n 要求按如下方式构造数列 只有一个数字 n n n 的数列是一个合法的数列 在一个合法的数列的末尾加入一个自然数 但是这个自然数不能超过该数列最后一项的一半 可以得到一个新的合法
  • 【区块链与密码学】第6-7讲:SM9数字签名算法

    本课堂内容全部选编自PlatON首席密码学家 武汉大学国家网络安全学院教授 博士生导师何德彪教授的 区块链与密码学 授课讲义 教材及互联网 版权归属其原作者所有 如有侵权请立即与我们联系 我们将及时处理 6 7 SM9数字签名算法 为了降低
  • C#如何写入二进制文件

    我们在程序运行过程中有时需要将一些数据以二进制的形式记录到文本中 相比普通的文本记录方式 二进制的记录需要先将原始数据转换为byte 格式 再通过二进制文件流进行记录 下面小编就来介绍一下C 中二进制文件的详细记录过程 工具 原料 Micr
  • ElasticSearch添加mapping

    1 创建索引 创建索引 param indexName public static void createIndex String indexName 插入前删除 以免报错 boolean flag client admin indices
  • Qml中信号的发送与接收信号槽函数的使用

    在Qt C 中 发送信号的函数和对应的槽函数在参数顺序 类型方面必须保持一致 此外 要让一个信号发射后进入相应的槽函数 必须用connect进行连接 这样 信号和槽才能连接起来 在QML中 发送信号和接受信号的槽 用起来比Qt C 还更方便
  • 【转载】DC的逻辑综合与优化

    转载 IC learner 博客园 作者 IC learner 对进行时序路径 工作环境 设计规则等进行约束完成之后 DC就可以进行综合 优化时序了 DC的优化步骤将在下面进行讲解 然而 当普通模式下不能进行优化的 就需要我们进行编写脚本来
  • JavaWeb学习笔记-02-Tomcat&Servlet&Thymeleaf

    1 Tomcat下载及使用 1 1 下载 官方网站 https tomcat apache org 1 2 使用 下载后解压 bin 可执行文件目录 conf 配置文件目录 lib 存放库目录 logs 日志文件目录 websapp 项目部
  • C++两个类的头文件相互包含

    C 中头文件相互包含的几点问题 一 类嵌套的疑问 C 头文件重复包含实在是一个令人头痛的问题 前一段时间在做一个简单的数据结构演示程序的时候 不只一次的遇到这种问题 假设我们有两个类A和B 分别定义在各自的有文件A h和B h中 但是在A中