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

C++面试题:C++怎么避免头文件循环引用?

头文件循环引用是C++编程中常见的问题,通常发生在两个或多个头文件相互包含对方的情况下。这种情况下,编译器可能会陷入无限递归,导致编译错误或不正确的代码生成。

1、问题描述

首先看一个典型的循环引用场景:

// a.h
#ifndef A_H
#define A_H
#include "b.h"

class A {
    B* b_ptr;  // 需要完整的B类定义
public:
    void doSomething();
};
#endif

// b.h
#ifndef B_H
#define B_H
#include "a.h"

class B {
    A* a_ptr;  // 需要完整的A类定义
public:
    void doSomething();
};
#endif

这会导致编译错误,因为两个头文件互相包含。

2、解决方案

2.1 前向声明

最常用也是最简单的方法:

// a.h
#ifndef A_H
#define A_H

class B;  // 前向声明

class A {
    B* b_ptr;  // 只需要不完整类型声明
public:
    void doSomething();
};
#endif

// b.h
#ifndef B_H
#define B_H

class A;  // 前向声明

class B {
    A* a_ptr;  // 只需要不完整类型声明
public:
    void doSomething();
};
#endif

// a.cpp
#include "a.h"
#include "b.h"  // 在实现文件中包含完整定义

void A::doSomething() {
    b_ptr->doSomething();
}

// b.cpp
#include "b.h"
#include "a.h"  // 在实现文件中包含完整定义

void B::doSomething() {
    a_ptr->doSomething();
}

2.2 接口分离原则

循环引用的根本原因是设计上的问题。通过重构代码,减少类之间的直接依赖,可以从根本上解决问题。例如,可以考虑将共同的功能提取到一个独立的模块中,或者使用接口或抽象类来解耦类之间的关系

假设 AB 之间有很强的依赖关系,可以通过引入一个中间类 C 来解耦:

引入类C

// C.h
#ifndef C_H
#define C_H

class C {
public:
    virtual void doSomething() = 0;
    virtual ~C() = default;
};

#endif // C_H

类A

// A.h
#ifndef A_H
#define A_H

#include "C.h"  // 只依赖于 C

class A:public C
{
public:
    C* m_Pc;;
public:
    void setProcessor(C* p) { m_Pc = p; }
    void doWork() { m_Pc->doSomething(); }

    void doSomething() override
    {
        std::cout << "A do something" << std::endl;
    }
};

#endif // A_H

类B

// B.h
#ifndef B_H
#define B_H

#include "C.h"  // 只依赖于 C

class B : public C
{
public:
    C* m_Pc;;
public:
    void setProcessor(C* p) { m_Pc = p; }
    void doWork() { m_Pc->doSomething(); }

public:
    void doSomething() override
    {
        std::cout << "B Do Something" << std::endl;
    }
};
#endif // B_H

main函数使用

#include <iostream>
#include "a.h"
#include "b.h"
#include "c.h"
int main()
{
        {
                C* pC = new B();
                A a;
                a.setProcessor(pC);
                a.doWork();
        }
        {
                C* pC = new A();
                B b;
                b.setProcessor(pC);
                b.doWork();
        }
        return 0;
}

运行main函数,a.dowork输出是B的内容,b.dowork是A的内容。

2.3 PIMPL模式

PIMPL模式不能直接解决循环依赖问题,但是这种做法很常见,所以这里简单介绍下

PIMPL(Pointer to IMPLementation,指向实现的指针)模式是一种用于隐藏类的实现细节的设计模式。它通过将类的私有成员和实现细节移到一个独立的实现类中,并在头文件中只保留一个指向该实现类的指针,PIMPL 模式的核心思想是将类的接口与其实现分离。

使用 PIMPL 模式重构代码

类A

// A.h
#ifndef A_H
#define A_H

class A {
public:
    A();
    ~A();
    void doSomething();

private:
    class Impl;  // 前向声明实现类
    std::unique_ptr<Impl> pImpl;  // 指向实现类的智能指针
};

#endif // A_H

// A.cpp
#include "A.h"
#include "B.h"  // 只在 .cpp 文件中包含 B 的头文件

class A::Impl {
public:
    B* m_B;  // 实现类中持有 B 的指针
    void doSomething() {
        if (m_B) {
            m_B->doSomething();
        }
    }
};

A::A() : pImpl(std::make_unique<Impl>()) {
    pImpl->m_B = nullptr;
}

A::~A() = default;

void A::doSomething() {
    pImpl->doSomething();
}

类B

// B.h
#ifndef B_H
#define B_H
class B {
public:
    B(); 
    ~B();
    void doSomething();

private:
    class Impl;  // 前向声明实现类
    std::unique_ptr<Impl> pImpl;  // 指向实现类的智能指针
};

#endif // B_H

// B.cpp
#include "B.h"
#include "A.h"  // 只在 .cpp 文件中包含 A 的头文件

class B::Impl {
public:
    A* m_A;  // 实现类中持有 A 的指针
    void doSomething() {
        if (m_A) {
            m_A->doSomething();
        }
    }
};

B::B() : pImpl(std::make_unique<Impl>()) {
    pImpl->m_A = nullptr;
}

B::~B() = default;

void B::doSomething() {
    pImpl->doSomething();
}

代码解析

前向声明:在 A.h 和 B.h 中,我们只前向声明了各自的实现类 Impl,而没有包含对方的头文件。这样,头文件之间不再存在直接的依赖关系,从而避免了循环引用。

实现类在 .cpp 文件中定义:A::Impl 和 B::Impl 的定义被移到了 .cpp 文件中。这意味着只有在编译时,A.cpp 和 B.cpp 才会引入对方的头文件,而不是在头文件中直接包含。

智能指针:我们使用 std::unique_ptr 来管理 Impl 对象的生命周期,确保资源的自动释放,避免内存泄漏。

总结

优先使用前向声明

当只需要指针或引用时,前向声明是最简单的解决方案

减少编译依赖,加快编译速度

合理拆分头文件

将相关的声明放在同一个头文件中

避免在头文件中包含不必要的其他头文件

使用接口抽象

通过抽象接口解耦具体实现

遵循依赖倒置原则

实现逻辑放在cpp文件

头文件只包含声明

具体实现放在cpp文件中

使用PIMPL模式

对于复杂的类,考虑使用PIMPL模式

可以完全隐藏实现细节,提供更好的ABI兼容性

相关文章:

  • 游戏引擎学习第146天
  • 学习小程序开发--Day1
  • 学习网络安全需要哪些基础?
  • C++单例进化论
  • P8686 [蓝桥杯 2019 省 A] 修改数组--并查集 or Set--lower_bound()的解法!!!
  • 设计模式 一、软件设计原则
  • Spring源码探析(二):BootstrapContext初始化深度解析(默认配置文件加密实现原理)
  • [算法笔记]cin和getline的并用、如何区分两个数据对、C++中std::tuple类
  • uniapp版本加密货币行情应用
  • Unity DOTS从入门到精通之EntityCommandBufferSystem
  • C#模拟鼠标点击,模拟鼠标双击,模拟鼠标恒定速度移动,可以看到轨迹
  • 【vitepress】如何搭建并部署自己的博客网站
  • sqlserver中的锁模式 | SQL SERVER如何开启MVCC(使用row-versioning)【启用行版本控制减少锁争用】
  • 基于单片机的智慧农业大棚系统(论文+源码)
  • mysql(community版)压缩包安装教程
  • 【计算机网络】确认家庭网络是千兆/百兆带宽并排查问题
  • 解决电脑问题(5)——鼠标问题
  • Android SharedPreferences 工具类封装:高效、简洁、易用
  • MySql数据库增删改查常用语句命令-MySQL步骤详解教程
  • Docker 的基本概念和优势,以及在应用程序开发中的实际应用
  • 高培勇:中国资本市场的发展应将预期因素全面纳入分析和监测体系
  • 昆明一学校门外小吃摊占满人行道,城管:会在重点时段加强巡查处置
  • 央行:当前我国债券市场定价效率、机构债券投资交易和风险管理能力仍有待提升
  • 105岁八路军老战士、抗美援朝老战士谭克煜逝世
  • 昆明阳宗海风景名胜区19口井违规抽取地热水,整改后用自来水代替温泉
  • 《2025城市青年旅行消费报告》发布,解码青年出行特征