【C++多重类循环依赖问题】基于class前向声明与类定义和实现分离的解决方案与分析

server/2024/12/28 3:46:49/

前言

  • 在前几节中,我们讲述了如何借助CMakeCMakeLists去创建自定义C++库,如何链接到.so并包含hpp去运行自定义库
    • # 【动态库.so | 头文件.hpp】基于CMake与CMakeList编写C++自定义库
  • C++类编程中,我们常常会遇到多个类相互包含的工程文件,如果没有正确处理好这些文件之间的关系,就会导致程序报错。
  • 今天这一期分享我们就来分享多个类互相包含的问题,并尝试分析其解决方案。

1 前向声明

1-1 前向声明介绍
  • 前向声明是指在代码中声明一个类、函数、结构体等的名字,但不提供其完整的定义。前向声明告诉编译器某个实体的存在,以便可以在之后的代码中引用该实体。这对于避免循环依赖、减少编译时间以及解决一些设计上的问题非常有用。
  • 前向声明的作用
    • 解决循环依赖问题:当两个或多个类相互引用时,使用前向声明可以避免头文件的互相包含。
    • 减少编译时间:前向声明通常比包含完整的头文件要高效,可以减少不必要的编译工作。
    • 改善设计结构:前向声明有助于减少类之间的强依赖关系,从而使代码更加模块化。
  • 语法:
class MyClass;  // 这是对 MyClass 的前向声明
int add(int, int);  // 前向声明,声明一个返回 int 类型的 add 函数
1-2 类的循环依赖
  • 我们来看一个例子:我们有如下文件结构
├── CMakeLists.txt
├── include
│   ├── ClassA.hpp
│   └── ClassB.hpp
└── src└── main.cpp
  • 我们让classAclassB相互包含
has
uses
ClassA
+ClassB* classB
+ClassA()
+~ClassA()
ClassB
+ClassA* classA
+ClassB(ClassA* classA_)
  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__
#include "ClassA.hpp"class ClassB {
public:ClassB(ClassA* classA_);
private:   ClassA* classA;  // 引用了 ClassA};#endif
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp
#include "./ClassB.hpp"class ClassA {
public:ClassA();~ClassA();
private:ClassB* classB;  // 引用了 ClassB};#endif
  • 如何编译:

    • # 【动态库.so | 头文件.hpp】基于CMake与CMakeList编写C++自定义库
  • 这时候我们编译代码,就会出现如下报错请添加图片描述

  • 这样做会导致编译器陷入一个死循环,因为它需要ClassA的完整定义来处理ClassB,而同时它又需要ClassB的定义来处理ClassA。这时,我们就可以通过前向声明来避免循环依赖。

1-3 (使用前向声明)
  • 我们分别在代码中添加前向声明
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp
#include "./ClassB.hpp"class ClassB;
class ClassA {
public:ClassA();~ClassA();
private:ClassB* classB;  // 引用了 ClassB};#endif
  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__
#include "ClassA.hpp"class ClassA;class ClassB {
public:ClassB(ClassA* classA_);
private:   ClassA* classA;  // 引用了 ClassA};#endif
  • 再次编译,代码没有报错。
  • 请添加图片描述

2 类定义和实现分离

  • 在C++中,类的定义实现通常是分开的,这种做法是为了提高代码的可维护性、可扩展性,并且减少编译依赖和编译时间。常见的做法是将类的声明放在头文件 (.hpp.h),而将类的实现放在源文件 (.cpp) 中。
2-1 类定义与实现分离的意义
  1. 提高可维护性
    • 头文件包含了类的接口(即成员函数的声明、成员变量的声明),源文件包含了类的实现(即成员函数的具体实现)。这种分离方式使得程序的结构更加清晰,易于管理和维护。
  2. 减少编译依赖
    • 头文件中的接口通常不需要修改,而源文件中的实现可能会频繁更改。通过分离定义和实现,当实现部分更改时,编译器只需要重新编译源文件,而不需要重新编译所有依赖该类的文件。这样可以显著减少编译时间。
  3. 隐藏实现细节
    • 通过将实现放在源文件中,类的实现细节对外部代码是不可见的,外部代码只能看到接口。这是面向对象编程中的封装概念,有助于保护类的内部实现不被滥用。
  4. 提高代码可读性
    • 头文件通常只包含接口(声明),对于类的使用者来说,只需要关注接口即可,不必关心具体的实现。这使得代码更简洁,易于理解。
2-2 扩展实现
  • 我们同样使用上面的代码,尝试不分离类定义和实现分离,看看他们在重复包含这里会出现什么问题
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp
#include<iostream>
#include "./ClassB.hpp"class ClassB;
class ClassA {
public:ClassA():classB(new ClassB(this)){}~ClassA(){if (classB)delete classB;}
private:ClassB* classB;  // 引用了 ClassB};#endif
  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__
#include "ClassA.hpp"class ClassA;class ClassB {
public:ClassB(ClassA* classA_):classA(classA_){}private:   ClassA* classA;  // 引用了 ClassA};#endif
  • 我们编译代码,发现代码没有报错请添加图片描述

2-3 问题抛出
  • 我们进一步扩展,我们为两个类分别编写两个函数,并让B调用A的函数
"has"
"uses"
ClassA
+ClassB* classB
+ClassA()
+~ClassA()
+funcA()
ClassB
+ClassA* classA
+ClassB(ClassA* classA_)
+funcB()
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp
#include<iostream>
#include "./ClassB.hpp"class ClassB;
class ClassA {
public:ClassA():classB(new ClassB(this)){}~ClassA(){if (classB)delete classB;}void funcA(){std::cout<<"funcA()"<<std::endl;}
private:ClassB* classB;  // 引用了 ClassB};#endif
  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__
#include "ClassA.hpp"class ClassA;class ClassB {
public:ClassB(ClassA* classA_):classA(classA_){}void funcB(){classA->funcA();std::cout<<"funcB()"<<std::endl;}
private:   ClassA* classA;  // 引用了 ClassA};#endif
  • 我们编译代码,发现代码报错请添加图片描述

  • 从错误信息和代码结构来看,问题的根本原因是 ClassB 尝试使用 ClassA 的成员函数 funcA() 时,ClassA 只是被前向声明(即 class ClassA;),并没有完整的定义。

  • ClassB.hpp 中, classA->funcA(); 这一行需要 ClassA 类的完整定义,因为 funcAClassA 的成员函数,编译器需要知道 ClassA 类的结构以及它的成员函数定义才能正确解析这行代码。由于 ClassA 仅仅是前向声明了,编译器不知道 ClassA 的具体实现,因此会报错。

  • 那你可能会问,我不是已经#include "ClassA.hpp"了吗,但是在ClassA.hpp右包含了ClassB,也就是再一次陷入了循环的问题。

2-4 分离类定义和实现
  • 我们将类的实现和定义分开,变成下属文件结构:
├── CMakeLists.txt
├── include
│   ├── ClassA.hpp
│   └── ClassB.hpp
└── src├── classA.cpp├── classB.cpp└── main.cpp
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp#include<iostream>class ClassB;class ClassA {
public:ClassA();~ClassA();void funcA();
private:ClassB* classB;  // 引用了 ClassB};#endif
  • classA.cpp
#include "../include/ClassA.hpp"
#include "../include/ClassB.hpp"ClassA::ClassA():classB(new ClassB(this)){}
ClassA::~ClassA(){if (classB)delete classB;
}
void ClassA::funcA()
{std::cout<<"funcA()"<<std::endl;
}
  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__class ClassA;class ClassB {
public:ClassB(ClassA* classA_);void funcB();
private:   ClassA* classA;  // 引用了 ClassA};#endif
  • classB.cpp
#include "../include/ClassB.hpp"
#include "../include/ClassA.hpp"ClassB::ClassB(ClassA* classA_):classA(classA_){}
void ClassB::funcB()
{classA->funcA();std::cout<<"funcB()"<<std::endl;
}
  • CMakeLists.txt
cmake_minimum_required(VERSION 3.10)# 设置项目名称
project(MyProject)# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)# 设置可执行文件输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)# 包含头文件目录
include_directories(${CURSES_INCLUDE_DIR})# 添加源文件
set(SOURCESsrc/main.cppsrc/classA.cppsrc/classB.cpp
)# 生成可执行文件
add_executable(MyProject ${SOURCES})
  • 如此以来我们进行编译请添加图片描述

2-5 问题分析
  • ClassA.hpp:只声明了 ClassA 类和它的成员函数,且没有包含 ClassB.hpp。在这里,ClassB 仅通过前向声明告知编译器 ClassB 的存在,编译器并不需要知道 ClassB 的具体实现细节。这样,ClassA 的定义可以独立存在。
  • ClassB.hpp:同理,ClassB.hpp 只声明了 ClassB 类和它的成员函数,并且通过前向声明告知编译器 ClassA 的存在。此时,ClassB 仅依赖 ClassA 的指针或引用,不需要了解 ClassA 的完整定义。
  • classA.cppclassB.cpp:这两个源文件包含了完整的类定义,==因此在源文件中,编译器可以访问到类的完整实现,包括成员函数和构造函数等。==在这里,ClassAClassB 都被完全实现,且它们的互相依赖在源文件中不会导致任何问题。
  • 通过这种分离方式,避免了循环依赖问题:
    • 头文件中,只声明类,而不实现类的细节。
    • 在源文件中,类的实现和类之间的互相调用可以正常工作。

2-6 总结
  • 完整流程总结
    1. 头文件只包含类声明和前向声明,避免了互相依赖的循环。
    2. 源文件包含了具体的实现部分,其中包括完整的类定义和方法实现。
    3. 在编译过程中,编译器在处理源文件时,会看到类的完整实现,因此能够正确地解析类的成员函数和指针引用。

3 进阶三个类互相包含问题

  • 你可以先根据问题描述自己敲一遍看看搞清楚没有~~~
3-1 问题描述
  • 我们来看一个测试一下刚刚学到的东西,假设我们现在有三个类,每个类中都有一个函数:
    • classAfuncA(),
    • classBfuncB()
    • classCfuncC()
  1. 其中classA包含 ClassBClassC,且ClassBClassC均持有classA的一个指针。
  2. funcB()funcC()将访问classA中的变量value并输出
  3. funcA()将调用funcB()funcC()
  • 我们可以绘制UML类土如下:
has
has
uses
uses
ClassA
+ClassC* classC
+ClassB* classB
+value
+funcA()
ClassB
-ClassA* classA
+funcB()
ClassC
-ClassA* classA
+funcC()
  • 我们分别创建三个头文件:classA.hpp,classB.hpp,classC.hpp
  • 以及三个实现文件
3-2 代码实现
  • 我们定义如下文件架构:
├── CMakeLists.txt
├── include
│   ├── ClassA.hpp
│   ├── ClassB.hpp
│   └── ClassC.hpp
└── src├── classA.cpp├── classB.cpp├── classC.cpp└── main.cpp
  • ClassA.hpp
#ifndef __CLASSA_HPP__
#define __CLASSA_HPP__
// ClassA.hpp#include<iostream>class ClassB;
class ClassC;class ClassA {
public:int value;
public:ClassA();~ClassA();void funcA();
private:ClassB* classB;  ClassC* classC; };#endif
  • classA.cpp
#include "../include/ClassA.hpp"
#include "../include/ClassB.hpp"
#include "../include/ClassC.hpp"ClassA::ClassA():classB(new ClassB(this)),classC(new ClassC(this)),value(42){}
ClassA::~ClassA(){if (classB)delete classB;if(classC)delete classC;
}   
void ClassA::funcA()
{std::cout<<"funcA()"<<std::endl;classC->funcC();classB->funcB();
}

  • ClassB.hpp
#ifndef __CLASSB_HPP__
#define __CLASSB_HPP__class ClassA;class ClassB {
public:ClassB(ClassA* classA_);void funcB();
private:   ClassA* classA;  // 引用了 ClassA};#endif
  • classB.cpp
#include "../include/ClassB.hpp"#include "../include/ClassA.hpp"ClassB::ClassB(ClassA* classA_):classA(classA_){}void ClassB::funcB(){std::cout<<"funcB():"<<classA->value<<std::endl;}

  • ClassC.hpp
#ifndef __CLASSC_HPP__
#define __CLASSC_HPP__class ClassA;class ClassC {
public:ClassC(ClassA* classA_);void funcC();
private:   ClassA* classA; };#endif
  • class.cpp
#include "../include/ClassC.hpp"
#include "../include/ClassA.hpp"ClassC::ClassC(ClassA* classA_):classA(classA_){}
void ClassC::funcC()
{std::cout<<"funcC():"<<classA->value<<std::endl;
}
  • main.cpp
#include "../include/ClassA.hpp"int main()
{ClassA* classA=new ClassA();classA->funcA();if(classA)delete classA;return 0;
}
  • CMakeLists.txt
cmake_minimum_required(VERSION 3.10)# 设置项目名称
project(MyProject)# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)# 设置可执行文件输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)# 包含头文件目录
include_directories(${CURSES_INCLUDE_DIR})# 添加源文件
set(SOURCESsrc/main.cppsrc/classA.cppsrc/classB.cppsrc/classC.cpp
)# 生成可执行文件
add_executable(MyProject ${SOURCES})
  • 编译执行请添加图片描述

4 总结

  • 本节讲述了如何使用前向声明和头文件和源文件分离解决类的重复依赖问题
  • 如有错误!欢迎指出!
  • 感谢大家的支持!在这里插入图片描述

http://www.ppmy.cn/server/153797.html

相关文章

Java前端基础—HTML

Java前端基础—HTML 目录 Java前端基础—HTML1.简介2.基础语法2.1HTML页面固定结构2.2标题标签2.3段落标签2.4换行标签2.5水平线标签2.6文本标签2.7图片标签2.8音频标签2.9视频标签2.10链接标签2.11列表标签2.12表格标签2.13表单标签2.14语义标签 1.简介 1.网页组成&#xff1…

Elasticsearch检索方案之一:使用from+size实现分页

前面两篇文章介绍了elasticsearch以及Kibana的安装&#xff0c;检索引擎以及可视化工具都已经安装完成&#xff0c;接下来介绍下如何使用golang的sdk实现简单的分页查询。 1、下载Elastic官方golang sdk 在讲解elasticsearch检索之前&#xff0c;需要先把golang的环境安装好&…

微服务分布式(二、注册中心Consul)

首先我们需要安装consul,到官网下载Consul Install | Consul | HashiCorp Developer 在解压的consul目录下 执行启动命令 consul agent -dev 启动服务 -dev表示开发模式 -server表示服务模式 启动后在浏览器访问8500端口http://localhost:8500/可以看到服务管理界面 项目…

YOLOv9-0.1部分代码阅读笔记-torch_utils.py

torch_utils.py utils\torch_utils.py 目录 torch_utils.py 1.所需的库和模块 2.def smart_inference_mode(torch_1_9check_version(torch.__version__, 1.9.0)): 3.def smartCrossEntropyLoss(label_smoothing0.0): 4.def smart_DDP(model): 5.def reshape_classif…

Charles安装证书过程(手机)

背景&#xff1a;使用模拟器抓包时&#xff0c;发现https请求无法抓取&#xff0c;需要安装相应证书。我自己是因为模拟器升级了安卓7&#xff0c;发现之前安装的证书无效了&#xff0c;需要重新安装。 参考博客&#xff1a;夜神模拟器12Charles进行Https抓包_模拟器抓包ssl-C…

十二月第22讲:巧用mask属性创建一个纯CSS图标库

&#xff08;Scalable Vector Graphics&#xff0c;可缩放矢量图形&#xff09;是一种基于 XML 的图像格式&#xff0c;用于定义二维图形。与传统的位图图像&#xff08;如 PNG 和 JPG&#xff09;不同&#xff0c;SVG 图像是矢量图形&#xff0c;可以在任何尺寸下保持清晰度&a…

C#关键字volatile

文章目录 一、 基本概念二、可见性问题没有 volatile 关键字的情况使用 volatile 关键字后的可见性保证 三、防止指令重排序指令重排序的概念volatile 防止指令重排序的原理 四、使用场景示例生产者 - 消费者模式示例 五、注意事项性能影响不能替代锁机制 一、 基本概念 在 C# …

2024年中最新!鸿蒙4.2成功开启无线调试

前言 鸿蒙4.2支持“开发人员选项”&#xff0c;但根本没有“无线调试”的按钮可以选&#xff0c;只有USB调试和ADB。 无法使用Shizuku&#xff0c;也无法安装VMOS。 是否能开启“无线调试” 用不了&#xff0c;但是可以用电脑连接手机&#xff0c;打开ADB调试&#xff0c;然…