2

在智能指针(能够获取动态区域中的资源的所有权并在使用后释放它们)出现之前,我想知道当作为参数传递给采用资源指针的函数时,如何对动态创建的对象进行簿记。

通过簿记,我的意思是如果有一个“新的”,那么在稍后的某个时候应该有一个“删除”跟随它。否则,程序将遭受内存泄漏。

这是一个示例,其中 B 是一个类,而 void a_function(B*) 是第三方库函数:

void main() {

  B* b = new B(); // line1

  a_function(b);  // line2

  ???             // line3
}

我在第 3 行做什么?我是否假设第三方功能已经处理了内存的取消分配?如果它没有并且我假设它有,那么我的程序就会出现内存泄漏。但是,如果它取消分配 b 占用的内存并且我也在 main() 中这样做以便安全起见,那么 b 实际上最终会被释放两次!我的程序会因为双释放错误而崩溃!

4

10 回答 10

5

启用“智能指针”的两个核心语言特性,以及更普遍的范围绑定资源管理(SBRM,有时也被拟声称为 RAII,“资源获取即初始化”)是:

  • 析构函数(自动goto

  • 无约束变量(每个对象都可以作为变量出现)

这两者都是 C++ 的基本核心特性,并且一直是该语言的一部分。因此,智能指针在 C++ 中一直是可实现的。

[顺便说一句,这两个特性意味着在 Cgoto必须以系统的、通用的方式处理资源分配和多次退出,而在 C++ 中它们基本上是被禁止的。C++goto融入核心语言。]

与任何语言一样,人们需要很长时间才能学习、理解和采用“正确”的习语。尤其是考虑到 C++ 与 C 的历史联系,许多曾经和正在从事 C++ 项目的程序员都来自 C 背景,并且可能发现坚持熟悉的模式更自在,尽管这些模式仍然受到 C++ 的支持。不可取(“只需替换malloc所有人new,我们就可以发货了”)。

于 2012-09-08T18:28:16.397 回答
2

好的,不要讨论为什么这无关紧要,无论如何你都应该使用智能指针......

在所有其他条件相同的情况下(没有自定义分配器或任何类似的东西),规则是分配内存的人应该解除分配内存。第三方函数,例如您的示例中的函数,绝对不应该释放它没有创建的内存,主要是因为 1)这通常是不好的做法(可怕的代码气味),更重要的是 2)它不知道如何内存被分配以开始。想象一下:

int main()
{
    void * memory = malloc(sizeof(int));
    some_awesome_function(memory);
}

// meanwhile, in a third-party library...

void some_awesome_function(void * data)
{
    delete data;
}

如果malloc/free并且new/delete正在使用不同的分配器进行操作,会发生什么?您正在查看某种潜在错误,因为用于的分配器delete不知道如何处理由malloc分配器分配的内存。你永远不会 free记得那是new'd,你永远不会delete记得那是malloc'd。曾经。

至于第一点,您必须询问如果第三方库释放内存并且您尝试(或未尝试)手动释放会发生什么,这正是不应该这样做的原因: 因为你根本没有办法知道。因此,公认的做法是,负责分配的代码部分也负责解除分配。如果每个人都遵守这条规则,那么每个人都可以跟踪他们的记忆,没有人会猜测。

于 2012-09-08T18:30:22.880 回答
0

智能指针是一种简化策略实施的方法。使用了相同的策略(将删除的责任归于一个所有者或一组所有者)。您只需记录政策,不要忘记采取相应行动。智能指针既是记录所选策略的一种方式,也是同时实施它的一种方式。在您的情况下,您查看了 a_function 文档并看到了它的要求。或者,如果没有记录,则进行或多或少有根据的猜测。

于 2012-09-08T19:07:26.310 回答
0

我在第 3 行做什么?

您查阅a_function. 通常的规则是函数对所有权或生命周期没有任何作用,除非它们说它们这样做。通过引用 C API,可以清楚地确定对此类文档的需求,其中智能指针不可用。

所以,如果它没有说它删除了它的参数,那么它就没有。如果它没有说它在返回时间之后保留其参数的副本,直到某个其他指定时间,那么它没有。

如果它说了什么,你就采取相应的行动,如果它什么也没说,那么你delete b(或者最好你改写B b; a_function(&b);- 观察通过不破坏对象,函数不需要关心你如何创建对象,你可以自由决定)。

希望它明确地说出它所说的任何内容,但是如果您不走运,它会通过某种约定来说明,即 API 中的某些类型的函数拥有其参数所引用的对象的所有权。例如,如果它被调用set_global_B_instance,那么您可能会偷偷怀疑它会保留该指针,并在设置后立即删除它是不明智的。

如果它没有说任何东西,但是您的代码最终出现了错误,并且您最终发现a_function调用delete了它的参数,那么您会找到记录的任何人,然后将其a_function在鼻子上,然后在他们的文档上提交错误。

通常那个人就是你自己,在这种情况下,试着吸取教训——记录对象所有权。

除了帮助避免编码错误外,智能指针还为在存在所有权问题的情况下接受或返回指针的函数提供了某种程度的自文档。在没有自我记录的情况下,您有实际的文档。例如,如果一个函数返回auto_ptr而不是原始指针,则表明您delete需要在指针上调用。你可以让它auto_ptr为你做这件事,或者你可以将它分配给其他一些智能指针,或者你可以release()自己管理指针。您调用的函数无关紧要,也不需要记录任何内容。如果一个函数返回一个原始指针,那么它必须告诉你指针所指对象的生命周期,因为你无法猜测。

于 2012-09-08T19:12:43.593 回答
0

如果没有智能指针,程序员通常采用分配的实体负责解除分配的规则。

在您的示例中,您的第三方函数删除传递给它的指针通常被认为是不良行为(尽管是有效代码),并且您应该在第 3 行将其删除。

这是程序员之间的社会契约,编译器通常不会强制执行。

于 2012-09-08T21:13:30.430 回答
0

我看到很多人指出智能指针从 C++ 开始就已经存在。但事实是,即使在今天,并非所有代码都在使用它们。一种常见的方法是手动进行引用计数:

void main() {
  B* b = createB(); //refcount = 1 
  a_function(b);
  releaseB(b); //--refcount
}

void a_function(B* b) {
  acquireB(b); //refcount++ when we store the reference somewhere
  ...
}
于 2012-09-08T18:42:57.207 回答
0

答案在第三方功能的文档中a_function()。可能的情况可能是:

  • 该函数只使用对象中的数据,并且在函数调用结束后不会保留对它的引用(例如:)printf。您可以在函数调用结束后安全地删除该对象。
  • 该函数(在某些内部库对象中)将保留对该对象的引用,直到稍后调用(比如说 to b_function())。您负责删除该对象,但必须保持它处于活动状态,直到您调用b_function(例如:)strtok
  • 该函数获取对象的所有权,并且不保证对象在被调用后存在(例如:)free()。在这种情况下,文档通常会指定如何创建对象 ( malloc, new, my_library_malloc)。

这些只是可能的许多不同行为的一些示例,但只要函数记录得足够好,您就应该能够做正确的事情。

于 2012-09-08T19:18:28.287 回答
0

只需查看 C API 以获取提示。C API 提供显式创建和销毁函数是很常见的。这些通常遵循库中的一些正式命名约定。

使用您的示例,如果未将参数明确标记为销毁函数,则删除/释放参数将是一个糟糕的设计a_function(在这种情况下,您不应在调用该函数后使用该参数。在大多数情况下,这是一个糟糕的设计假设销毁不属于你的对象是安全的。当然,对于智能指针,所有权、生命周期和清理机制通常由智能指针处理。

所以是的,人们使用newand delete,尽管我在templates 之前没有编写 C++——在程序中看到明确的newand会更常见delete。智能指针不是转移对象和传达所有权的好方法templates——除了例外,它是在 1990 年左右引入的(在 C++ 可用 7 年后)。自然,编译器需要一些时间来支持所有这些特性,人们需要一些时间来实现容器并改进这些实现。请注意,在模板之前这是可能的,但实现/克隆任意类型的容器并不总是可行的,因为该语言在模板之前并不支持泛型。当然,具有具体类型的具体类可以很容易地完成智能指针的机制,在那些日子里类型是不变的……但是当泛型不可用时,这确实会导致代码重复的形式。

但即使在今天,替换或销毁智能指针参数的内容对象也是一种不寻常的设计,除非明确标记。这种可能性也降低了,因为将智能指针作为参数而不是它所持有的对象传递也是不寻常的。因此,从那时起,与内存相关的错误数量有所减少,但仍应遵守一些谨慎和良好的所有权约定。

于 2012-09-08T19:28:24.830 回答
0

简单的答案:阅读文档。这在 C 接口中很常见,因为资源管理是接口的重要组成部分,如果函数声明对对象的所有权,它将被记录在案。

于 2012-09-08T19:30:43.440 回答
0

你摧毁你创造的东西,图书馆摧毁它创造的东西。

如果您与库共享数据(例如,用于文件数据的 char*),库的文档将指定它是否保留对您的数据的引用(在这种情况下,在库使用完之前不要删除您的副本)或复制您的数据(在这种情况下,图书馆的工作是在完成后删除数据)。

于 2012-09-08T18:35:44.873 回答