如何解决由 C++ 包装器中 C 对象之间的交互引起的内存相关错误?

How to resolve memory related errors that arise from interaction between C objects in a C++ wrapper?

问题

我正在围绕一个面向对象的 C 库编写一个薄的 C++ 包装器。这个想法是自动化内存管理,但到目前为止还不是很自动化。基本上当我使用我的包装器 类 时,我会遇到各种内存访问和不适当的释放问题。

C 库的最小示例

假设 C 库由 AB 类 组成,每个库都有几个 'methods' 与之关联:

#include <memory>
#include "cstring"
#include "iostream"

extern "C" {
typedef struct {
    unsigned char *string;
} A;

A *c_newA(const char *string) { 
    A *a = (A *) malloc(sizeof(A)); // yes I know, don't use malloc in C++. This is a demo to simulate the C library that uses it. 
    auto *s = (char *) malloc(strlen(string) + 1);
    strcpy(s, string);
    a->string = (unsigned char *) s;
    return a;
}

void c_freeA(A *a) {
    free(a->string);
    free(a);
}

void c_printA(A *a) {
    std::cout << a->string << std::endl;
}


typedef struct {
    A *firstA;
    A *secondA;
} B;

B *c_newB(const char *first, const char *second) {
    B *b = (B *) malloc(sizeof(B));
    b->firstA = c_newA(first);
    b->secondA = c_newA(second);
    return b;
}

void c_freeB(B *b) {
    c_freeA(b->firstA);
    c_freeA(b->secondA);
    free(b);
}

void c_printB(B *b) {
    std::cout << b->firstA->string << ", " << b->secondA->string << std::endl;
}

A *c_getFirstA(B *b) {
    return b->firstA;
}

A *c_getSecondA(B *b) {
    return b->secondA;
}

}

测试'C lib'

void testA() {
    A *a = c_newA("An A");
    c_printA(a);
    c_freeA(a);
    // outputs: "An A"
    // valgrind is happy =]
}
void testB() {
    B *b = c_newB("first A", "second A");
    c_printB(b);
    c_freeB(b);
    // outputs: "first A, second A"
    // valgrind is happy =]
}

AB

的包装器 类
class AWrapper {

    struct deleter {
        void operator()(A *a) {
            c_freeA(a);
        }
    };

    std::unique_ptr<A, deleter> aptr_;
public:

    explicit AWrapper(A *a)
            : aptr_(a) {
    }

    static AWrapper fromString(const std::string &string) { // preferred way of instantiating
        A *a = c_newA(string.c_str());
        return AWrapper(a);
    }

    void printA() {
        c_printA(aptr_.get());
    }
};


class BWrapper {

    struct deleter {
        void operator()(B *b) {
            c_freeB(b);
        }
    };

    std::unique_ptr<B, deleter> bptr_;
public:
    explicit BWrapper(B *b)
            : bptr_(std::unique_ptr<B, deleter>(b)) {
    }

    static BWrapper fromString(const std::string &first, const std::string &second) {
        B *b = c_newB(first.c_str(), second.c_str());
        return BWrapper(b);
    }

    void printB() {
        c_printB(bptr_.get());
    }

    AWrapper getFirstA(){
        return AWrapper(c_getFirstA(bptr_.get()));
    }

    AWrapper getSecondA(){
        return AWrapper(c_getSecondA(bptr_.get()));
    }

};

包装器测试


void testAWrapper() {
    AWrapper a = AWrapper::fromString("An A");
    a.printA();
    // outputs "An A"
    // valgrind is happy =]
}

void testBWrapper() {
    BWrapper b = BWrapper::fromString("first A", "second A");
    b.printB();
    // outputs "first A"
    // valgrind is happy =]
}

问题演示

太好了,所以我继续开发完整的包装器(很多 类)并意识到当 类 这样的(即聚合关系)都在范围内时,C++ 将自动调用两个 类 的 descructors 分开,但是由于底层库的结构(即对 free 的调用),我们遇到了内存问题:

void testUsingAWrapperAndBWrapperTogether() {
    BWrapper b = BWrapper::fromString("first A", "second A");
    AWrapper a1 = b.getFirstA();
    // valgrind no happy =[

}

Valgrind 输出

我尝试过的事情

无法克隆

我尝试的第一件事是获取 A 的副本,而不是让他们尝试释放相同的 A。这虽然是个好主意,但由于我使用的库的性质,在我的情况下是不可能的。实际上有一个捕获机制,因此当您使用之前看到的字符串创建新的 A 时,它会返回相同的 ASee this question for my attempts at cloning A.

自定义析构函数

我获取了 C 库析构函数的代码(此处为 freeAfreeB)并将它们复制到我的源代码中。然后我尝试修改它们,使 A 不会被 B 释放。这已经部分奏效了。内存问题的一些实例已经解决,但是因为这个想法没有解决手头的问题(只是暂时掩盖了主要问题),新问题不断出现,其中一些是模糊的并且难以调试。

问题

所以我们终于遇到了问题:如何修改这个 C++ 包装器来解决由于底层 C 对象之间的交互而产生的内存问题?我可以更好地利用智能指针吗?我应该完全放弃 C 包装器并按原样使用库指针吗?或者有没有更好的方法我没有想到?

提前致谢。

编辑:对评论的回复

自从问了上一个问题(上面链接)后,我重构了我的代码,以便在与它包装的库相同的库中开发和构建包装器。所以对象不再是不透明的。

指针是从对库的函数调用生成的,它使用 callocmalloc 进行分配。

在实际代码中 Araptor2raptor_uri* (typdef librdf_uri*) 并分配了 librdf_new_uri while B is raptor_term* (aka librdf_node*) and allocated with librdf_new_node_* functionslibrdf_node 有一个 librdf_uri 字段。

编辑 2

我还可以指向代码行,如果字符串相同,则返回相同的 A。参见 line 137 here

您正在释放 A 两次

BWrapper b = BWrapper::fromString("first A", "second A");

当 b 超出范围时,会调用 c_freeB,这也会调用 c_freeA

AWrapper a1 = b.getFirstA();

用另一个 unique_ptr 包装 A,然后当 a1 超出范围时,它将在同一个 A 上调用 c_freeA

请注意,在使用 AWrapper 构造函数时,BWrapper 中的 getFirstA 将 A 的所有权授予另一个 unique_ptr。

解决此问题的方法:

  1. 不要让 B 管理 A 内存,但由于您使用的是不可能的库。
  2. 让BWrapper管理A,不让AWrapper管理A,使用AWrapper时要确保BWrapper存在。即在AWrapper中使用裸指针,而不是智能指针。
  3. 在 AWrapper(A *) 构造函数中复制 A,为此您可能需要使用库中的函数。

编辑:

  1. shared_ptr 在这种情况下不起作用,因为 c_freeB 无论如何都会调用 c_freeA。

编辑 2:

在这种特定情况下,考虑到您提到的 raptor 库,您可以尝试以下操作:

explicit AWrapper(A *a)
            : aptr_(raptor_uri_copy(a)) {
}

假设A是raptor_uriraptor_uri_copy(raptor_uri *) 将增加引用计数和 return 相同的传递指针。然后,即使 raptor_free_uri 在同一个 raptor_uri * 上被调用两次,它也只会在计数器变为零时调用 free。

问题是 AWrappergetFirstAgetSecondA return 个实例,这是一个拥有类型。这意味着在构建 AWrapper 时,您将放弃 A * 的所有权,但 getFirstAgetFirstB 不会这样做。构造 returned 对象的指针由 BWrapper.

管理

最简单的解决方案是您应该 return 一个 A * 而不是包装器 class。这样您就不会传递内部 A 成员的所有权。我还建议让构造函数在包装器 classes 中采用私有指针,并具有类似于 fromStringfromPointer 静态方法,该方法获取传递给它的指针的所有权。这样你就不会意外地从原始指针创建包装器 classes 的实例。

如果你想避免使用原始指针或者想在来自 getFirstAgetSecondA 的 returned 对象上使用方法,你可以编写一个简单的引用包装器,它有一个作为成员的原始指针。

class AReference
{
private:
    A *a_ref_;
public:
    explicit AReference(A *a_ref) : a_ref_(a_ref) {}

    // other methods here, such as print or get

};