C++中的各种循环依赖和解决办法
智能指针的循环引用问题
问题描述
智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄露。下例是一个典型的循环引用的场景。
struct A
{
std::shared_ptr<B> bptr;
~A() { cout << "A is deleted ! " <<endl; }
};
struct B
{
std::shared_ptr<A> aptr;
~B() { cout << "B is deleted! " << endl; }
};
void TestPtr()
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
测试结果是两个指针A和B都不会被删除,存在内存泄露。循环引用导致ap和 bp 的引用计数为2,在离开作用域之后,ap和 bp 的引用计数减为1,并不会减为0,导致两个指针都不会被析构,产生了内存泄露。
1、weak_ptr解决循环引用问题
上述循环引用问题通过weak_ptr就可以解决,只要将A或B的任意一个成员变量改为weak_ptr即可。
struct A
{
std:: shared_ptr<B> bptr;
~A(){ cout << "A is deleted! " << endl; }
} ;
struct B
{
std::weak_ptr<A> aptr; //改为weak_ptr
~B() { cout <<"B is deleted!" <<endl; }
};
void TestPtr()
{
{
std::shared_ptr<A> ap (new A);
std::shared_ptr<B> bp (new B);
ap->bptr = bp;
bp->aptr = ap;
} //objects should be destroyed .
}
在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是 weak_ptr,它并不会增加引用计数,所以ap 的引用计数仍然会是1,在离开作用域之后,ap 的引用计数会减为0A指针会被析构,析构后其内部的 bptr的引用计数会减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也将被析构,不会发生内存泄露。
类的循环依赖问题
问题描述
class B;
class A {
public:
B b;
};
class B {
public:
A a;
};
若两个类之间存在循环依赖则在编译时会报错,原因是两个类中存在相互的调用,无法为两个类分配具体的空间。
即使已经进行了前向声明,但是A、B类完全依赖彼此,各自编译都需要彼此的完整定义,此时编译器是无法办到的。
1、使用前向声明
A.h
class B;
class A {
public:
B *b;
int fun(B* b);
};
B.h
class B {
public:
A *a;
int fun(A*a);
};
由于前向声明而没有定义的类是不完整的,所以class B只能用于定义指针、引用、或者用于函数形参的指针和引用,不能用来定义对象,或访问类的成员。
这是因为需要确定class A空间占用的大小,而类型B还没有定义不能确定大小,但B是指针类型大小已知,因此Class A中可以使用B的指针定义成员变量。
2、抽象出父类,A,B作为派生类
用指针替代变量声明的方法虽然能够编译通过,但是存在一个问题,就是无法进行单元测试。测A需要B是完整的,B又依赖A,那A到底要怎么测试。为了能够进行单元测试,可以考虑将方法和数据抽出作为父类。
class Base {
public:
int fun1();
int fun2();
};
class A {
public:
int funA() { return fun1(); };
class B {
public:
int funB() { return fun2(); };
};
3、借助第三类抽离耦合部分函数
// *.h
struct Helper {
static int doFunA();
static int doFunB();
};
class A {
public:
int funA();
int data_;
};
class B {
public:
int funB();
};
// *.cpp
int Helper::doFunA() {
A a;
return a.data_;
}
int Helper::doFunB(){
B b;
return b.funB();
}
int A::funA() {
return Helper::doFunB();
}
int B::funB(){
return Helper::doFunA();
}
4、抽象基类,分离接口和实现
#include <iostream>
using namespace std;
class IB
{
public:
virtual int getVal() = 0;
};
class B : public IB
{
public:
int val = 3;
int getVal() override {
return val;
};
int getValofA();
};
class A
{
public:
IB* b;
int val = 2;
int getValOfB()
{
return b->getVal();
}
};
int B::getValofA() {
A a;
return a.val;
}
int main()
{
IB* b = new B();
A a;
B bb;
a.b = b;
cout << a.getValOfB() << endl;
cout << bb.getValofA() << endl;
return 0;
}
5、函数绑定
以上方法其实类之间还是存在依赖关系,函数的调度对于调用者来说,其实只关心需要传入的参数、能够返回的结果,至于是哪个对象,内部如何实现的,调用者是无感的。因此可以利用函数绑定来解除依赖关系。
#include <iostream>
#include <functional>
using namespace std;
class B { // 下层类型,可以不知道A类型的存在
public:
void method1(int n, function<void(int)> func) { func(n); }
int method2(int n1, int n2, function<int(int, int)> func) { return func(n1, n2); }
void method3(const string& name, function<void(const string&)> func) { func(name); };
int method4(const int& n1, const int& n2, function<int(const int&, const int&)> func) { return func(n1, n2); };
int method5(const int& n1, const int& n2, function<int(const int&, const int&)> func) { return func(n1, n2); };
};
class A { // 上层类型
public:
void set_value(int n) { i_a = n; }
int get_value() { return i_a; }
static int add(int a, int b) { return a + b; }
private:
int i_a;
};
void show(const string& name) {
cout << name << endl;
}
struct funopt {
int operator()(const int& a, const int& b){
return a * b;
}
};
int main() {
A a;
B b;
a.set_value(1);
cout << a.get_value() << endl; // 输出1
//绑定类成员函数
auto bindfun1 = bind(&A::set_value, &a, placeholders::_1); // 绑定显式对象成员函数的指针
b.method1(10, bindfun1);
cout << a.get_value() << endl; // 输出10
//绑定静态函数
auto bindfun2 = bind(&A::add, placeholders::_1, placeholders::_2);
int ret = b.method2(10, 15, bindfun2);
cout << ret << endl; // 输出25
//绑定普通函数
auto bindfun3 = bind(show, placeholders::_1);
b.method3("JIE", bindfun3); //输出JIE
//绑定lambda表达式
auto bindfun4 = bind([](const int& a, const int& b) {return a > b ? a : b; }, placeholders::_1, placeholders::_2);
ret = b.method4(5, 3, bindfun4);
cout << ret << endl; // 输出5
//绑定仿函数
auto bindfun5 = bind(funopt(), placeholders::_1, placeholders::_2); // 绑定匿名对象成员函数的引用
ret = b.method4(5, 3, bindfun5);
cout << ret << endl; // 输出15
return 0;
}
动态库循环依赖
问题描述
假设存在两个不相互依赖动态库A和动态库B,如何实现跨DLL调用函数。
接口和回调函数
动态库B(接口定义)
假设动态库B提供了一个接口,用于处理某种事件。这个接口包含一个纯虚函数handleEvent,用于处理事件。
// 动态库B的头文件(example.h)
class IEventHandler {
public:
virtual ~IEventHandler() {}
virtual void handleEvent() = 0; // 纯虚函数
};
动态库A(实现回调)
在动态库A中,我们实现了一个类MyEventHandler,该类实现了IEventHandler接口,并提供了事件处理的具体实现。
// 动态库A的头文件(myeventhandler.h)
#include "example.h"
class MyEventHandler : public IEventHandler {
public:
void handleEvent() override {
// 处理事件的代码逻辑
}
};
动态库B(使用回调)
在动态库B中,我们可以通过传递回调函数的指针或引用,将MyEventHandler实例传递给需要处理事件的函数或对象。
// 动态库B的代码(example.cpp)
#include "example.h"
#include "myeventhandler.h" // 引入动态库A的头文件
void someFunction(IEventHandler* eventHandler) {
// 使用传递进来的事件处理器处理事件
eventHandler->handleEvent(); // 调用回调函数处理事件
}
主程序(加载和使用)
在主程序中,我们需要加载动态库B和动态库A,并创建MyEventHandler的实例,将其传递给动态库B中的函数。
#include <iostream>
#include "example.h" // 引入动态库B的头文件
#include "myeventhandler.h" // 引入动态库A的头文件
int main() {
// 加载动态库B和A(这里仅为示意,具体加载方式取决于你的环境)
// ...
MyEventHandler myEventHandler; // 创建事件处理器实例
someFunction(&myEventHandler); // 将事件处理器传递给动态库B中的函数
return 0;
}
在这个例子中,通过使用纯虚函数和接口,动态库B定义了一个通用的回调接口,而动态库A提供了该接口的具体实现。主程序通过加载两个动态库,并将实现类的实例传递给接口,实现了动态库之间的回调机制。这种方式使得动态库B和A之间的耦合度降低,提高了代码的可扩展性和可维护性。
动态库A通过实现动态库B中定义的接口类(如IEventHandler),提供了事件处理的具体实现。这意味着动态库A提供了对动态库B接口的具体实现,并依赖于动态库B的接口来与动态库B进行交互。通过使用接口和回调机制,动态库A可以独立于动态库B进行开发和测试,而不需要直接依赖动态库B。同时,动态库B也可以独立于动态库A进行开发和测试,
参考文献
http://t.csdnimg.cn/iWYPw
http://t.csdnimg.cn/2P0oP
http://t.csdnimg.cn/GaQLp