c++基础-头文件相互引用与循环依赖问题

c++基础-头文件相互引用与循环依赖问题,第1张

c++基础-头文件相互引用与循环依赖问题

本文主要分析C++头文件的相互引用,与类的相互依赖问题

1. C++头文件的相互引用

如果C++头文件相互引用,编译无法通过:

// A.cpp
#include "A.h"

int main()
{
    return 0;
}

// A.h
#include "B.h"

// B.h
#include "A.h"

尝试编译,报错

                 from A.h:1,
                 from B.h:1,
                 from A.h:1,
                 from B.h:1,
                 from A.h:1,
                 ...
                 ...
                 from B.h:1,
                 from A.h:1,
                 from B.h:1,
                 from A.cpp:1:
B.h:1:15: error: #include nested too deeply
 #include "A.h"

这是由于预处理阶段,A.h与B.h,相互嵌套,导致头文件展开无限循环。

使用#ifndef或#pragma once

为了避免同一个头文件被包含(include)多次,C/C++中有两种宏实现方式:一种是#ifndef方式;另一种是#pragma once方式。

在能够支持这两种方式的编译器上,二者并没有太大的区别。但两者仍然有一些细微的区别
方式一:

#ifndef  __SOMEFILE_H__

#define   __SOMEFILE_H__

 ... ... // 声明、定义语句

#endif

#ifndef的方式可以保证同一文件不会被包含多次,也能保证内容完全相同的两个文件不会被同时包含。需要注意不同文件中的__SOMEFILE_H__宏命名不能相同。

方式二:

#pragma once

 ... ... // 声明、定义语句

#pragma once由编译器保证同一个文件不会被包含多次,不能处理两个文件内容相同的情况,如果相同的文件在两个位置就会被处理两次。GCC3.4版本以前不支持#pragma once。

2. 类的循环依赖

#ifndef和#pragma once解决了头文件循环引用的问题,但是如果存在类的相互依赖,编译会出现新的问题:

// A.cpp
#include "A.h"

int main()
{
    return 0;
}

// A.h
#include "B.h"
class A
{
    B b;
}

// B.h
#include "A.h"
class B
{
    A a;
}

此时编译报错:

In file included from A.h:2:0,
                 from A.cpp:1:
B.h:5:5: error: ‘A’ does not name a type
     A a;
     ^

错误原因:
在A.h:2,处理语句#include “B.h”,进行头文件展开
在B.h:5进行的是在class B中声明一个A类型的成员变量,而此时class A还没有被声明
因此编译报错:‘A’ does not name a type

解决循环依赖的问题有两种方式:

1.使用前向声明(forward declaration)
2.设计层面避免循环引用

a.使用前向声明

C++的类可以进行前向声明,此例子中在B.h文件中对class A进行前向声明,就可以编译通过,如下:

// #include "A.h"
class A;
class B
{
    A* a;
};

由于前向声明而没有定义的类是不完整的,所以class A只能用于定义指针、引用、或者用于函数形参的指针和引用,不能用来定义对象,或访问类的成员。
这是因为确定class B空间占用的大小,而A还没有定义不能确定大小,A是确定的指针大小,
因此Class B中可以使用A
定义成员变量。

前向声明的作用:

1.不需要include头文件,大量引入的头文件会导致编译变慢
2.可以解决两个类相互循环调用的情况

b.重新设计程序结构,避免循环依赖

良好的程序设计可以避免循环依赖,参考接口隔离原则和依赖倒置原则:

接口隔离原则:
客户端不应该依赖它不需要的接口
类间的依赖关系应该建立在最小的接口上
通俗来讲提供给每个模块单一的接口

依赖倒置原则:
上层模块不应该依赖于底层模块,它们都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象

在A依赖B,B依赖A的情况下,考虑为什么A类要内含B类的成员呢,那么可以抽象出一个接口,一个抽象类class IB,B是它的实现,class A和B都依赖于class IB,循环依赖便消除了。

编码示例:

// C++中,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类
class IB
{
public:
    virtual int getVal() = 0;
}

class B : public IB
{
public:
    A a;
    int val = 2;
    int getVal(){
        return val;
    }
};

class A
{
public:
    IB *b;
    int getValOfB()
    {
        return b->getVal();
    }
};


int main()
{
    IB *b = new B();
    A a;
    a.b = b;

    cout << a.getValOfB() << endl;   
    return 0;
}

此时class A和class B依赖于IB,class B依赖于class A,循环依赖被打破,并且class A仍能以接口形式得到class B的数据。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/3971087.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-10-21
下一篇 2022-10-21

发表评论

登录后才能评论

评论列表(0条)

保存