想象一下以下课程:
class MyString {
public:
const char* str;
std::size_t str_len;
MyString(const char* str, std::size_t str_len)
: str { str }
, str_len { str_len }
{}
}
我对为MyString
实现析构函数感到有些困惑。我的第一个想法是它看起来像这样:
~MyString() {
delete [] str;
}
但是如果我不能确定它已被分配,我怎么能删除str?例如,我可以像这样创建MyString
的实例:
const char* c_string = "Hello, World!";
MyString my_string(c_string, 13);
在这种情况下,我不应该删除str
,因为它没有在堆上声明,但如果我创建了MyString
这样的实例:
char* char_array = new char[13]{'H','e','l','l','o',',',' ','W','o','r','l','d','!'};
MyString my_string(char_array, 13);
不删除str
会导致内存泄漏(我假设),因为它会在堆上声明。但是,如果我创建MyString
的实例,例如 this:
char* char_array = new char[13]{'H','e','l','l','o',',',' ','W','o','r','l','d','!'};
MyString my_string(char_array + 3, 10);
我不应该删除str
,因为虽然它在堆上,但尚未分配;它只是指向已经分配的其他东西的一部分。
那么我怎么能确定我没有删除我不应该删除的内容或者没有删除需要删除的内容?如果MyString使用char*
而不是const char*
s,答案会有所不同吗?如果我使用MyString my_string = new MyString...
怎么办?
编辑:为了澄清,我实际上并没有写一个字符串类。我使用char数组作为字节数组。我假设std :: string不起作用,因为字节可能是0。
答案 0 :(得分:10)
有几种不同的模式适用:
始终分配模式。在这种方法中,类不接受传入资源的所有权,而是在它分配的缓冲区中创建一个副本,因此知道如何在其析构函数中释放。原始参数由调用类的代码拥有,并且调用者应该在需要时清理自己的数据,因为类实例具有独立的副本。示例:std::string
。
调用者指定的删除模式。在这种方法中,类确实拥有所有权,并且为了容纳各种allocator / deallocator对,它接受一个参数,该参数是一个知道如何释放数据的函数或函数对象。类析构函数将调用此删除函数/函数对象,执行该特定缓冲区所需的正确释放(或根本不执行)。示例:std::shared_ptr
。
嵌套所有权模式。这里,类只保留指向原始数据块的指针或引用。调用者仍然拥有所有权并且有责任释放数据,但是除了它创建的类实例存在之外,还需要保持该块有效。这是运行时最低的开销,但也是最难跟踪的开销。示例:C ++ 11 lambda中的by-reference变量捕获。
无论您使用哪种类型设计,请务必记录下来,以免您的班级用户感到疑惑。
答案 1 :(得分:1)
但如果我不能确定它已被分配,我怎么能删除
str
?
只有在以下情况下才能删除str
您证明您将获得传递给构造函数的指针的所有权。
您记录了您将在传递给构造函数的内存上调用delete
。
仅通过调用new
分配内存来构造类的实例。
我还建议更改班级,使用char*
代替const char*
。
class MyString {
public:
char* str;
std::size_t str_len;
MyString(char* str, std::size_t str_len)
: str { str }
, str_len { str_len }
{}
}
这可以防止意外使用,例如:
MyString my_string("Hello, World!", 13);
然后,您必须确保遵循The Rule of Three。
答案 2 :(得分:1)
为了阐明界面,您可以使用适当的智能指针,例如:
MyString(std::unique_ptr<const char[]> str, std::size_t str_len)
或者如果您没有获得所有权,请使用适当的字符串视图,例如:
MyString(std::experimental::observer_ptr<const char> str, std::size_t str_len)
然后你不再怀疑班上的记忆政策。