-1

以下是我从目前关于该主题的知识中得出的一系列结论,问题本质上是这是否正确,如果不正确,对这些结论的适当修正是什么。

作为一名经验丰富的 .net 开发人员,我完全认同所有对象实例几乎都以粒子的形式存在于云中,并且对象成员关系只是将这些粒子相互关联。当粒子或粒子云无法通过某些引用链接回框架中的根对象时,它或其成员将被丢弃。这本质上是引用计数和某种网络分析的结果,以保持对对象是否仍有通往根的路径的理解。

在这种结构中,对象的可识别实例可以被许多其他对象引用(“拥有”),并且所有权网络可以根据需要变得复杂和循环/自引用。

在过渡到 C++ 时,为了内存管理的利益,这种自由必须受到限制。所有对象都必须具有清晰的所有权树,对象的生命周期由其父对象维护。

生命周期也可能受到范围的限制,如花括号代码部分 {} 中的临时值。Lifetime 也可以,但绝对可以避免,通过使用new关键字和小心使用delete

在较新的 C++ 标准化中,shared_ptr 之类的东西似乎被设计为允许更接近 .net 托管内存模型的东西。我不知道这些是否也提供与丢弃自引用但未连接的对象云的托管内存相同的好处。

一个例子是 std::list。据我所知,推荐的策略是列表中的对象实例必须在没有 shared_ptr 构造的好处的情况下完全由列表拥有,并且其生命周期由列表自身的生命周期决定。这导致需要复制构造函数、临时调用的析构函数,或者使用要求列表具有单一具体类型的 emplace 方法。替代解决方案包括在别处存储指向对象的指针和管理对象的生命周期,尽管这充满了明显的危险。这一切似乎都很尴尬。

在 .net 中,列表可以引用一个对象,而不必是它的父母或监护人,因为对象的生命周期是由其他方式管理的。

我知道堆内存和堆栈内存之间的区别会对性能产生影响,但是我对这个主题并不了解。

我对这两个系统的看法基本上正确吗?如果不是,可以提供哪些更正?如果它本质上是正确的,那么存在哪些描述我需要采用的 C++ 心智模型来创建大型、强大和高性能应用程序的文献?这包括代码的最佳实践以及稳健地管理大型应用程序的更抽象概念。

4

2 回答 2

2

在(非垃圾收集)C++ 中,这个想法是内存管理是通过析构函数完成的,而不是一些垃圾收集器。这样做的主要优点是:

(1) 非常精确。该语言对何时以及以何种顺序调用析构函数以及释放内存做出了非常有力的保证。

(2) 它简单、透明且完全可移植,而且开销真正最小。与其依赖于一些不透明的垃圾收集机制,它可能会随心所欲地做事,而是确切地知道内存管理将如何在您编译的每个系统上工作。如果你不确定什么时候/如果东西被释放,你可以把调试输出放在析构函数中并检查它。确实,现代垃圾收集器非常好,而且很少引起问题,但是如果您真的遇到过问题并且需要调试一个,您就会知道这可能非常耗时且痛苦。

我知道堆内存和堆栈内存之间存在对性能有影响的区别,但是我对这个主题并不了解。

堆和堆栈正是它们在 C 中的样子。堆栈的基本思想是,它是一块内存,用于存储程序的函数调用堆栈。堆栈被布置为一系列“堆栈帧”。当一个函数被调用时,一个指针被推送,它告诉我们稍后返回时应该返回到哪里。函数调用的所有参数都连续放置在堆栈上。还有一个计数器/指针指示此堆栈帧有多大。当一个函数被调用时,一个新的栈帧继续,当函数返回时我们弹出它。函数的局部变量也分配在堆栈上。堆栈具有固定大小,如果超过它(通常通过无限递归),则会因堆栈溢出错误而崩溃。堆栈上的所有内容都必须具有在编译时已知的固定大小。这是由“sizeof”运算符确定的。如果你想拥有一个动态数组,你必须使用“堆”,它允许在大小仅在运行时知道时分配内存,并且可以以任意顺序破坏事物。

堆栈的优点是内存分配基本上是即时的(堆栈帧指针刚刚移动)并且以非常舒适的方式进行释放。(当我们从函数返回并且本地变量超出范围时,我们只需从堆栈中弹出以释放它们的内存。)C++ 对堆栈分配的对象和析构函数做出了非常强的保证。保证将始终以与创建相反的顺序在函数返回时调用析构函数,即使它抛出异常而不是正常返回也是如此。本质上,除非您的程序异常终止(std::terminate(),或throw 42),或者除非发生一些不寻常的事情(在一些特殊情况下,你的析构函数本身会抛出异常,但基本上你不应该这样做,因为它会自找麻烦),自动对象的析构函数将在非常特殊的点被调用时间,以非常特殊的顺序。如果它们具有带有构造函数/析构函数的成员变量,那么它们也会以特定顺序被调用。如果涉及继承......如果您不熟悉,您可以阅读它。

基本上,这是一种非常强大和严格的机制,您可以使用它来控制各种资源——不仅仅是内存,还有诸如套接字、到打印机的连接、指向必须“关闭”的 C 库实例的指针等任何东西独占的共享资源,或者在不再需要时必须以某种方式清理的资源。

不是所有的东西都可以入栈。例如,如果你有一个动态大小的数组,它不能放在堆栈上,它必须放在堆上,这与 C 中通常教授的方式相同。(因为只有编译器知道大小的东西才能进入堆栈。)当你把东西放在堆上时,(在 C++ 中使用 operator new )它必须与 delete 调用配对,否则内存不会被释放并且析构函数不会被调用。

在 C++ 中,管理它的首选方法是利用堆栈的力量。您无需自己手动调用 delete,而是使用其析构函数调用 delete 的堆栈分配对象。最简单的例子是“std::unique_ptr”(又名 boost::scoped_ptr)。你提到的 shared_ptr 也做了类似的事情。所有的 C++ 标准容器,如 vector、list、map 等也都这样做。

IMO 这是模式基本上是 C++ 的福音。在编写良好的 C++ 程序中,不仅所有内存,而且所有资源都以这种方式管理,由析构函数释放。这是非常重要的RAII习语:“Resource Acquisition is Initialization”。你可以在这里阅读更多关于它的信息:http ://www.c2.com/cgi/wiki?ResourceAcquisitionIsInitialization

在许多程序中,所有对象都只有一个所有者,对象所有权图是一棵树。在这种情况下,一切都可以堆栈分配/堆栈分配的成员变量,然后所有内存都打开并由堆栈管理。在某些情况下,对象需要共享所有权——所有权图是一个 DAG(有向无环图)。然后您可以将 shared_ptr 用于共享对象。它们将在堆上分配,但它会自动工作并且内存将被管理,而无需您做更多的事情。在最复杂的情​​况下,所有权图中存在循环引用、循环。如果您在这种情况下仅使用 shared_ptr ,则不会释放循环。这就是垃圾收集器应该为你“做艰苦的工作”的情况。在现代 C++ 中,首选的方法是在大多数地方仍然使用 shared_ptr,但在任何循环中至少有一个链接,您使用 weak_ptr 代替,这样循环“被破坏”并且可以自动释放。这很少是必要的,当它是,它真的没有那么多工作。它只是许多其他语言的自动垃圾收集的另一种工程风格。

于 2015-08-16T14:10:26.550 回答
1

您的描述有效,但看起来有些奇怪,因为它本质上试图定义对象生命周期概念,因为对象的概念对于ISO C++ 和 .net 都是相同的。

事实上……它不是。因此,您的分析适用于处理基于堆的对象的 C++子部分,而 C++ 则更多。

在 .net 中,诸如int或之类的东西不是正确的对象(它们被称为值,并且仅在boxeddouble时才被视为对象)。

.net(和 C#)在value classes (struct-s) 和reference classes之间做了一个巧妙的区分。

考虑

b=a; a.val+=2;

的价值是b.val多少?

值/引用的概念,在 C++ 中与类的概念无关。唯一的纠缠是在涉及运行时多态性时。

std::list<int>以及std::list<persons>工作相同。int-s 和 person-s 仅由列表拥有,因为它们在概念上是列表的成员

person a("john"), b("dave");
a = b;
b.name="robert";

实际上会让 a.name 保持“dave”。

std::list<shared_ptr<person> >

是另一个故事:列表拥有 ptr,而 ptr 又拥有该人。列表策略(独占所有者)适用于其策略(共享)适用于人员的指针。

要匹配与 java 或 c# 类似的语义,您应该使用类似的语义技巧

class person_ { ... };
typedef std::shared_ptr<person_> person;

std::list<person> ...

现在这就像使用引用类(除了使用 -> 而不是 .),aperson->name = "..."并且该人的所有引用都将看到新名称。

还要注意shared_ptr只是引用计数指针。循环引用不会被丢弃。没有指针网络可供后台进程尝试遵循以发现不可达性。C++ 没有任何垃圾收集器。

于 2015-08-16T13:28:06.247 回答