C++内存管理

8

我在大学里学到了,你总是要释放未使用的对象,但是不知道如何实际操作。例如正确地构建您的代码等等。 在C ++中处理指针是否有任何通用规则?

目前我不允许使用boost。我必须坚持使用纯C ++,因为我正在使用的框架禁止使用任何泛型。


我必须坚持使用纯C++,因为我正在使用的框架禁止使用任何泛型。这个决定肯定有其合理性吧?我很想听听原因,因为我一时想不到完全禁止使用模板的任何理由。 - Konrad Rudolph
@KonradRudolph:官方答案是:它们在不同操作系统上不可移植,特别是在编译器和链接编辑器的支持方式上。 - Alexander Stolz
@AlexanderStolz:这是错误的。我的意思是,C++作为一个整体在不同的操作系统上是不兼容的(大多数平台都有清晰和明确定义的C ABI,但C++则不然,特别是当你涉及到虚继承表示等细节时)。模板在这方面并没有改变任何东西;实例化的模板只是具有奇怪名称的类。编译器的支持曾经是一个紧迫的问题,但现在没有任何借口不使用g++ 4.x或VC++ 2005/2008,它们都足够好用于任何正常的使用。 - DrPizza
太遗憾了,这个已经过时了。我很想听一下为什么你不能使用(具体地说)泛型的解释。 - jmucchiello
从框架文档中: 不要使用模板。 它们在不同操作系统上不可移植,特别是在编译器和链接器支持它们的方式方面。 这就是我目前能说的所有内容。 - Alexander Stolz
2009年,只要操作系统具备基本的编译器能力,模板在不同的操作系统上都得到了完美支持。任何撰写框架的人都应该能够提供一些实际证据来支持自己的主张。 - underscore_d
8个回答

14

我曾经使用过嵌入式Symbian OS,它有一个出色的系统,完全基于开发者约定来实现。

  1. 只有一个对象会拥有指针,默认情况下是创建者。
  2. 所有权可以被传递。为了表示所有权的传递,在方法签名中将对象作为指针传递(例如void Foo(Bar *zonk);)。
  3. 所有者将决定何时删除该对象。
  4. 要将对象传递给一个方法仅供使用,对象在方法签名中作为引用传递(例如void Foo(Bat &zonk);)。
  5. 非所有者类可以存储它们获得的对象的引用(永远不是指针),只有当它们可以确定所有者在使用期间不会销毁它时才能存储。

基本上,如果一个类仅仅使用某些东西,它就使用引用。如果一个类拥有某些东西,它就使用指针。

这个系统工作得非常好,使用起来非常愉快。内存问题非常少见。


我会使用std::auto_ptr<>来表明所有权正在转移(而不是原始指针)。假设您已经在智能指针中存储了数据,如果期望独占所有权,则std::auto_ptr<>是完美的选择。 - Martin York
OP说他不能使用泛型,这可能排除了任何智能指针类型。 - Sam Stokes
如果引用指向的对象被错误删除,应用程序核心转储会出现吗?如果它是一个指针,我们可以检查是否为空(前提是删除器将其设置为null)。 - balki
@balki 可能会,也可能不会;可能会让烤面包机从天而降:这是未定义行为。但 Sander 表示,只有在保证引用不会成为悬空引用时才使用它们,因此这是代码质量问题。更重要的是,指针也不能解决这个问题:另一个实体删除对象时,观察者的指针副本不会被设置为 nullptr,因此它不能像引用一样测试有效性(即:nullptr 是地址的属性,而不是该地址处数据的属性)... 除非每个观察者指针都通过向所有者注册以供以后更新或执行某些复杂操作的方式进行引用。 - underscore_d

5

规则:

  1. 尽可能使用智能指针。Boost有一些好的智能指针
  2. 如果无法使用智能指针,请在删除指针后将其置空
  3. 永远不要在禁止使用规则1的地方工作。

如果有人不允许使用规则1,请记住,如果你拿别人的代码,更改变量名称并删除版权声明,除了学校项目会用相当复杂的工具检查这种诡计之外,没有人会注意到。另请参见此问题


-1 代表空值。我不会在这里讨论它,但这不是一个好的实践方法。如果代码编写得相当好,就不应该有必要这样做,因为在确定没有其他引用/指针指向您分配的内存块之前,不应该进行删除操作。 - Rudy Velthuis

3

我在这里想再添加一条规则:

  • 如果自动对象可以胜任,就不要使用new/delete创建一个对象。

我们发现,对于新学习C++的程序员或者从Java等语言转过来的程序员,他们似乎会学到new关键字后,无论上下文如何,都会过度使用它来创建任何对象。当在函数内部局部创建一个对象来完成某些有用的任务时,这种使用new的方式可能会对性能造成负面影响,并且很容易在忘记相应的delete时引入愚蠢的内存泄漏。是的,智能指针可以帮助解决后者的问题,但它并不能解决性能问题(假设在幕后使用了new/delete或类似的东西)。有趣的是(也许),我们发现在使用Visual C++时,delete通常比new更昂贵。

这种混淆还来自于调用的函数可能会将指针甚至智能指针作为参数(当引用可能更好/更清晰时)。这使他们认为他们需要“创建”一个指针(很多人似乎认为这就是new的作用)才能传递指针给函数。显然,这需要一些关于如何编写API的规则,以使调用约定尽可能明确,并通过函数原型中提供的清晰注释来加强这些规则。


从Nice的观点来看,“这使得他们认为他们需要“创建”一个指针(许多人似乎认为这就是new所做的事情),以便能够将指针传递给函数。”或许是这样,但我认为这是因为大多数大学都先教授Java。在Java中,你必须“new”一切,而且很难改掉这个习惯,特别是当两种语言看起来如此相似,但实际上完全不同。要真正玩得开心,请尝试同时使用这两种语言! - Chris Huang-Leaver

2
一般情况下(资源管理,其中资源不一定是内存),您需要熟悉RAII模式。这是C++开发人员最重要的信息之一。

2
一般来说,除非必须这样做,否则应避免从堆中分配内存。如果必须这样做,则对于需要在代码的不同部分之间共享且具有长期生存期的对象,请使用引用计数。
有时您需要动态分配对象,但它们只会在一定时间范围内使用。例如,在以前的项目中,我需要创建一个数据库模式的复杂内存表示——基本上是对象的复杂循环图。然而,该图仅在数据库连接的持续时间内需要,之后所有节点都可以一次性释放。在这种情况下,使用的一种好模式是我称之为“本地GC惯用语法”。我不确定它是否有“官方”名称,因为它是我自己的代码和Cocoa(请参见苹果的Cocoa参考中的NSAutoreleasePool)中所看到的东西。
简而言之,您创建一个“收集器”对象,该对象保留对使用 new 分配的临时对象的指针。通常与程序中的某个作用域相关联,可以是静态作用域(例如 - 作为实现 RAII 习惯的堆栈分配对象)或动态作用域(例如 - 与数据库连接的生命周期相关联,如我以前的项目)。当“收集器”对象被释放时,它的析构函数释放它所指向的所有对象。
此外,像 DrPizza 一样,我认为不使用模板的限制太严格了。但是,我曾在古老版本的 Solaris、AIX 和 HP-UX 上进行了大量开发(最近才做过 - 是的,这些平台仍然存在于财富50强),我可以告诉您,如果您真的关心可移植性,应尽可能少地使用模板。不过,对于容器和智能指针来说,使用模板应该没问题(对我有效)。没有模板,我描述的技术更加痛苦。它需要由“收集器”管理的所有对象都派生自一个共同的基类。

虽然本地GC习语听起来不错,但并非必要。手动内存管理并不难做好。 - Rudy Velthuis

1

你好,

我建议阅读Scott Meyers的“Effective C++”相关章节。易于阅读,他涵盖了一些有趣的陷阱来捕捉不谨慎的人。

我也对缺乏模板感到好奇。所以没有STL或Boost。哇。

顺便说一句,让人们达成共识的惯例是一个很好的主意。就像让每个人都同意OOD的惯例一样。顺便说一句,“Effective C++”最新版没有第一版那么出色的关于OOD惯例的章节,这是遗憾的,例如公共虚拟继承始终模拟“isa”关系。

罗布


0
  • 当您必须手动管理内存时,请确保在相同的作用域/函数/类/模块中调用delete,以先到者为准,例如:
  • 让函数的调用者分配由其填充的内存,不要返回新的指针。
  • 始终在与new调用相同的exe/dll中调用delete,否则可能会出现堆损坏问题(不同不兼容的运行时库)。

0

你可以从实现智能指针功能的一些基类中派生出所有内容(使用ref()/unref()方法和计数器)。

@Timbo强调的所有要点在设计该基类时都很重要。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接