如何解决由 C++ 包装器中 C 对象之间的交互引起的内存相关错误?
How to resolve memory related errors that arise from interaction between C objects in a C++ wrapper?
问题
我正在围绕一个面向对象的 C 库编写一个薄的 C++ 包装器。这个想法是自动化内存管理,但到目前为止还不是很自动化。基本上当我使用我的包装器 类 时,我会遇到各种内存访问和不适当的释放问题。
C 库的最小示例
假设 C 库由 A
和 B
类 组成,每个库都有几个 '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 =]
}
A
和 B
的包装器 类
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
时,它会返回相同的 A
。 See this question for my attempts at cloning A
.
自定义析构函数
我获取了 C 库析构函数的代码(此处为 freeA
和 freeB
)并将它们复制到我的源代码中。然后我尝试修改它们,使 A 不会被 B 释放。这已经部分奏效了。内存问题的一些实例已经解决,但是因为这个想法没有解决手头的问题(只是暂时掩盖了主要问题),新问题不断出现,其中一些是模糊的并且难以调试。
问题
所以我们终于遇到了问题:如何修改这个 C++ 包装器来解决由于底层 C 对象之间的交互而产生的内存问题?我可以更好地利用智能指针吗?我应该完全放弃 C 包装器并按原样使用库指针吗?或者有没有更好的方法我没有想到?
提前致谢。
编辑:对评论的回复
自从问了上一个问题(上面链接)后,我重构了我的代码,以便在与它包装的库相同的库中开发和构建包装器。所以对象不再是不透明的。
指针是从对库的函数调用生成的,它使用 calloc
或 malloc
进行分配。
在实际代码中 A
是 raptor2
的 raptor_uri*
(typdef librdf_uri*
) 并分配了 librdf_new_uri while B
is raptor_term*
(aka librdf_node*
) and allocated with librdf_new_node_* functions。 librdf_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。
解决此问题的方法:
- 不要让 B 管理 A 内存,但由于您使用的是不可能的库。
- 让BWrapper管理A,不让AWrapper管理A,使用AWrapper时要确保BWrapper存在。即在AWrapper中使用裸指针,而不是智能指针。
- 在 AWrapper(A *) 构造函数中复制 A,为此您可能需要使用库中的函数。
编辑:
- shared_ptr 在这种情况下不起作用,因为 c_freeB 无论如何都会调用 c_freeA。
编辑 2:
在这种特定情况下,考虑到您提到的 raptor 库,您可以尝试以下操作:
explicit AWrapper(A *a)
: aptr_(raptor_uri_copy(a)) {
}
假设A是raptor_uri
。 raptor_uri_copy(raptor_uri *)
将增加引用计数和 return 相同的传递指针。然后,即使 raptor_free_uri
在同一个 raptor_uri *
上被调用两次,它也只会在计数器变为零时调用 free。
问题是 AWrapper
的 getFirstA
和 getSecondA
return 个实例,这是一个拥有类型。这意味着在构建 AWrapper
时,您将放弃 A *
的所有权,但 getFirstA
和 getFirstB
不会这样做。构造 returned 对象的指针由 BWrapper
.
管理
最简单的解决方案是您应该 return 一个 A *
而不是包装器 class。这样您就不会传递内部 A
成员的所有权。我还建议让构造函数在包装器 classes 中采用私有指针,并具有类似于 fromString
的 fromPointer
静态方法,该方法获取传递给它的指针的所有权。这样你就不会意外地从原始指针创建包装器 classes 的实例。
如果你想避免使用原始指针或者想在来自 getFirstA
和 getSecondA
的 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
};
问题
我正在围绕一个面向对象的 C 库编写一个薄的 C++ 包装器。这个想法是自动化内存管理,但到目前为止还不是很自动化。基本上当我使用我的包装器 类 时,我会遇到各种内存访问和不适当的释放问题。
C 库的最小示例
假设 C 库由 A
和 B
类 组成,每个库都有几个 '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 =]
}
A
和 B
的包装器 类
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
时,它会返回相同的 A
。 See this question for my attempts at cloning A
.
自定义析构函数
我获取了 C 库析构函数的代码(此处为 freeA
和 freeB
)并将它们复制到我的源代码中。然后我尝试修改它们,使 A 不会被 B 释放。这已经部分奏效了。内存问题的一些实例已经解决,但是因为这个想法没有解决手头的问题(只是暂时掩盖了主要问题),新问题不断出现,其中一些是模糊的并且难以调试。
问题
所以我们终于遇到了问题:如何修改这个 C++ 包装器来解决由于底层 C 对象之间的交互而产生的内存问题?我可以更好地利用智能指针吗?我应该完全放弃 C 包装器并按原样使用库指针吗?或者有没有更好的方法我没有想到?
提前致谢。
编辑:对评论的回复
自从问了上一个问题(上面链接)后,我重构了我的代码,以便在与它包装的库相同的库中开发和构建包装器。所以对象不再是不透明的。
指针是从对库的函数调用生成的,它使用 calloc
或 malloc
进行分配。
在实际代码中 A
是 raptor2
的 raptor_uri*
(typdef librdf_uri*
) 并分配了 librdf_new_uri while B
is raptor_term*
(aka librdf_node*
) and allocated with librdf_new_node_* functions。 librdf_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。
解决此问题的方法:
- 不要让 B 管理 A 内存,但由于您使用的是不可能的库。
- 让BWrapper管理A,不让AWrapper管理A,使用AWrapper时要确保BWrapper存在。即在AWrapper中使用裸指针,而不是智能指针。
- 在 AWrapper(A *) 构造函数中复制 A,为此您可能需要使用库中的函数。
编辑:
- shared_ptr 在这种情况下不起作用,因为 c_freeB 无论如何都会调用 c_freeA。
编辑 2:
在这种特定情况下,考虑到您提到的 raptor 库,您可以尝试以下操作:
explicit AWrapper(A *a)
: aptr_(raptor_uri_copy(a)) {
}
假设A是raptor_uri
。 raptor_uri_copy(raptor_uri *)
将增加引用计数和 return 相同的传递指针。然后,即使 raptor_free_uri
在同一个 raptor_uri *
上被调用两次,它也只会在计数器变为零时调用 free。
问题是 AWrapper
的 getFirstA
和 getSecondA
return 个实例,这是一个拥有类型。这意味着在构建 AWrapper
时,您将放弃 A *
的所有权,但 getFirstA
和 getFirstB
不会这样做。构造 returned 对象的指针由 BWrapper
.
最简单的解决方案是您应该 return 一个 A *
而不是包装器 class。这样您就不会传递内部 A
成员的所有权。我还建议让构造函数在包装器 classes 中采用私有指针,并具有类似于 fromString
的 fromPointer
静态方法,该方法获取传递给它的指针的所有权。这样你就不会意外地从原始指针创建包装器 classes 的实例。
如果你想避免使用原始指针或者想在来自 getFirstA
和 getSecondA
的 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
};