在 C + + 中使用堆栈、静态和堆

我已经找过了,但是这三个概念我还没有很好地理解。什么时候我必须使用动态分配(在堆中) ,它的真正优势是什么?静态和堆栈的问题是什么?我可以不在堆中分配变量而编写整个应用程序吗?

我听说其他语言包含一个“垃圾收集器”,所以您不必担心内存问题。垃圾收集器是做什么的?

使用这个垃圾收集器不能自己操作内存,您能做什么呢?

曾经有人对我说:

int * asafe=new int;

我有一个“指向指针的指针”。它是什么意思? 它是不同的:

asafe=new int;

162774 次浏览

主要的区别在于速度和大小。

Stack

分配速度快得多。它是在 O (1)中完成的,因为它是在设置堆栈帧时分配的,所以基本上是空闲的。缺点是,如果您用完了堆栈空间,就会陷入很大的麻烦。您可以调整堆栈大小,但是,IIRC,您有大约2MB 可以使用。另外,一旦退出该函数,堆栈上的所有内容都将被清除。因此,以后再引用它可能会有问题。(指向堆栈已分配对象的指针会导致 bug。)

分配的速度慢得多。但是,你有 GB 可以玩,并指向。

垃圾收集者

垃圾收集器是在后台运行并释放内存的一些代码。在堆上分配内存时,很容易忘记释放内存,这就是所谓的内存泄漏。随着时间的推移,应用程序消耗的内存不断增长,直到崩溃。使用垃圾收集器定期释放不再需要的内存有助于消除这类错误。当然,这是要付出代价的,因为垃圾收集器会降低处理速度。

堆栈内存分配(函数变量、本地变量)在堆栈太“深”并且可用于堆栈分配的内存溢出时可能会出现问题。堆用于需要从多个线程或整个程序生命周期访问的对象。您可以在不使用堆的情况下编写整个程序。

您可以在没有垃圾收集器的情况下非常容易地泄漏内存,但是您也可以指定对象和内存何时释放。当 Java 运行 GC 时,我遇到了一些问题,我有一个实时进程,因为 GC 是一个独占线程(没有其他线程可以运行)。因此,如果性能很关键,并且可以保证不存在泄漏对象,那么不使用 GC 是非常有帮助的。否则,当您的应用程序消耗内存并且您必须追踪泄漏源时,这只会让您痛恨生活。

下面的内容当然并不十分精确。当你读它的时候,你要持保留态度:)

你提到的三件事是 自动、静态和动态存储期限,它与对象的寿命长短以及它们何时开始生命有关。


自动存储时间

对于 短命很小数据使用自动存储持续时间,在某些块中只需要 本地:

if(some condition) {
int a[3]; // array a has automatic storage duration
fill_it(a);
print_it(a);
}

一旦我们退出块,生命周期就结束了,一旦定义了对象,生命周期就开始了。它们是最简单的一种存储持续时间,比特定的动态存储持续时间快得多。


静态存储持续时间

对于自由变量使用静态存储持续时间,如果其作用域允许这种使用(名称空间作用域) ,那么任何代码都可以随时访问自由变量,对于需要在其作用域的出口(本地作用域)延长其生存期的局部变量,对于需要由其类的所有对象(类作用域)共享的成员变量,则使用静态存储持续时间。它们的寿命取决于它们所处的范围。他们可以有 命名空间范围局部范围局部范围类别范围。他们的真实情况是,一旦他们的生命开始,生命结束于 节目结束。这里有两个例子:

// static storage duration. in global namespace scope
string globalA;
int main() {
foo();
foo();
}


void foo() {
// static storage duration. in local scope
static string localA;
localA += "ab"
cout << localA;
}

程序打印 ababab,因为 localA在其块退出时不会被销毁。可以说,具有本地作用域的对象从生存期 当控制达到他们的定义开始。对于 localA,它发生在函数体输入时。对于命名空间范围内的对象,生存期从 程序启动开始。对于类范围的静态对象也是如此:

class A {
static string classScopeA;
};


string A::classScopeA;


A a, b; &a.classScopeA == &b.classScopeA == &A::classScopeA;

如您所见,classScopeA不绑定到其类的特定对象,而是绑定到类本身。上面三个名字的地址是相同的,而且都表示同一个对象。关于何时以及如何初始化静态对象有一些特殊的规则,但是我们现在不要关心这个问题。这就是 静态初始化顺序失败的意思。


动态存储持续时间

最后的存储持续时间是动态的。如果您想让对象生活在另一个岛上,并且希望将指针放在引用它们的周围,那么可以使用它。如果对象是 很大,并且希望创建只有在 运行时间中才知道大小的数组,则也可以使用它们。由于这种灵活性,具有动态存储持续时间的对象很复杂,管理起来也很慢。当发生适当的 新的操作符调用时,具有该动态持续时间的对象开始生存期:

int main() {
// the object that s points to has dynamic storage
// duration
string *s = new string;
// pass a pointer pointing to the object around.
// the object itself isn't touched
foo(s);
delete s;
}


void foo(string *s) {
cout << s->size();
}

只有当您为它们调用 删除时,它的生命周期才会结束。如果你忘了这一点,那些对象永远不会结束生命。定义用户声明的构造函数的类对象不会调用它们的析构函数。具有动态存储持续时间的对象需要手动处理其生存期和相关的内存资源。图书馆的存在是为了方便人们使用它们。显式垃圾收集特定的物体可以通过使用智能指针建立:

int main() {
shared_ptr<string> s(new string);
foo(s);
}


void foo(shared_ptr<string> s) {
cout << s->size();
}

您不必关心调用 delete: 如果引用该对象的最后一个指针超出范围,那么共享 ptr 会为您执行这个操作。共享 ptr 本身具有自动存储持续时间。因此,它的生存期是自动管理的,允许它检查是否应该删除析构函数中指向的动态对象。有关 share _ ptr 引用,请参见升级文档: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm

静态和堆栈的问题是什么?

“静态”分配的问题在于分配是在编译时进行的: 您不能使用它来分配一些可变数量的数据,这些数据的数量直到运行时才知道。

在“堆栈”上进行分配的问题在于,一旦执行分配的子例程返回,分配就会被销毁。

我可以写一个完整的应用程序没有分配变量在堆?

也许不是一个普通的大型应用程序(但是所谓的“嵌入式”程序可以不使用堆编写,使用 C + + 的一个子集)。

垃圾收集工是做什么的?

它会一直监视您的数据(“标记和扫描”) ,以检测您的应用程序何时不再引用它。这对应用程序来说很方便,因为应用程序不需要释放数据... ... 但是垃圾收集器可能计算量很大。

垃圾收集器不是 C + + 编程的常见特性。

使用这个垃圾收集器不能自己操作内存,您能做什么呢?

学习确定性内存释放的 C + + 机制:

  • ‘ static’: 永远不会释放
  • ‘ stack’: 一旦变量“超出作用域”
  • “堆”: 当指针被删除时(由应用程序显式删除,或在某个或其他子例程中隐式删除)

如果程序事先不知道要分配多少内存(因此不能使用堆栈变量) ,该怎么办。比如说链表,链表可以在不预先知道其大小的情况下增长。因此,当您不知道将有多少元素插入到链表中时,在堆上进行分配对链表来说是有意义的。

一个类似的问题被问到,但是它没有问关于静力学的问题。

静态内存、堆和堆栈内存的概述:

  • 静态变量基本上是一个全局变量,即使您不能全局访问它。通常在可执行文件本身中有一个它的地址。整个程序只有一个副本。无论进入函数调用(或类)多少次(以及进入多少个线程!)变量指的是同一个内存位置。

  • 堆是一组可以动态使用的内存。如果您希望一个对象的空间为4kb,那么动态分配器将查看堆中的空闲空间列表,选择一个4kb 的块,然后将其提供给您。通常,动态内存分配器(malloc、 new 等)从内存末尾开始向后工作。

  • 解释堆栈如何增长和收缩有点超出了这个答案的范围,但是只要说明始终从末尾添加和删除就足够了。堆栈通常从高处开始,然后向下生长到较低的地址。当堆栈在中间某处遇到动态分配器时(但请参考物理内存、虚拟内存和碎片) ,就会耗尽内存。多个线程将需要多个堆栈(进程通常为堆栈保留最小大小)。

当你想使用每一个的时候:

  • 静态值/全局值对于内存非常有用,您知道您总是需要这些内存,并且您知道您永远不想释放这些内存。(顺便说一下,嵌入式环境可能被认为只有静态内存... 堆栈和堆是第三种内存类型(程序代码)共享的已知地址空间的一部分。当程序需要链表之类的东西时,它们通常会从静态内存中进行动态分配。但无论如何,静态内存本身(缓冲区)本身并不是“分配”的,而是从缓冲区为此目的持有的内存中分配其他对象。您也可以在非嵌入的情况下这样做,控制台游戏将经常避开内置的动态内存机制,而是通过对所有分配使用预设大小的缓冲区来严格控制分配过程。)

  • 当您知道只要函数在作用域中(在堆栈的某个地方) ,就希望变量保持不变时,堆栈变量非常有用。堆栈对于您需要的变量来说是很好的,这些变量可以用于它们所在的代码,但是在代码之外是不需要的。它们也非常适合于访问资源(比如文件)时使用,并希望在您离开该代码时资源自动消失。

  • 当您希望比上述方法更灵活时,堆分配(动态分配的内存)非常有用。通常,调用函数来响应事件(用户单击“ create box”按钮)。正确的响应可能需要分配一个新的对象(一个新的 Box 对象) ,这个对象应该在函数退出之后很长时间仍然存在,所以它不能在堆栈上。但是您不知道在程序开始时需要多少个框,所以它不可能是静态的。

垃圾收集

我最近听说了很多关于垃圾收集者有多棒的事情,所以也许有一点反对的声音会有所帮助。

当性能不是一个大问题时,垃圾收集是一种很好的机制。我听说 GC 正在变得越来越好,越来越复杂,但事实是,您可能被迫接受性能损失(取决于用例)。如果你懒惰,它仍然可能无法正常工作。在最好的情况下,当垃圾收集器意识到没有更多的引用时,它会意识到您的内存消失了(参见 参考计数)。但是,如果您有一个引用自己的对象(可能通过引用另一个引用回来的对象) ,那么仅仅引用计数并不表示可以删除内存。在这种情况下,GC 需要查看整个引用汤,并确定是否有任何岛屿只被它们自己引用。不经意间,我猜测这是一个 O (n ^ 2)运算,但是不管它是什么,如果您关心性能的话,它可能会变得很糟糕。(编辑: 马丁 B 指出,它是 O (n)的合理有效的算法。如果您关心的是性能,并且可以在不进行垃圾回收的情况下在常量时间内释放,那么这仍然是 O (n)过多。)

就我个人而言,当我听到人们说 C + + 没有垃圾收集功能时,我的脑海里就会把它标记为 C + + 的一个特性,但我可能是少数。对于用 C 和 C + + 编程的人来说,最难学习的可能是指针以及如何正确处理动态内存分配。其他一些语言,比如 Python,如果没有 GC 就会很糟糕,所以我认为这可以归结为你想从一门语言中得到什么。如果你想要可靠的性能,那么没有垃圾收集的 C + + 是我能想到的唯一的事情。如果您想要易用性和辅助轮(不需要学习“适当的”内存管理即可避免崩溃) ,请选择使用 GC。即使您知道如何很好地管理内存,它也会节省您可以用来优化其他代码的时间。实际上性能方面的损失已经不大,但是如果您真的需要可靠的性能(以及确切知道正在发生什么、什么时候发生、在什么情况下发生的能力) ,那么我还是坚持使用 C + + 。我听说过的所有主流游戏引擎都是用 C + + (如果不是 C 或汇编语言的话) ,这是有原因的。Python 等编写脚本是可以的,但主要的游戏引擎就不行了。

它被精心地说出来,就像“简短的回答”一样:

  • 静态变量(类)
    生存期 = 程序运行时(1)
    可见性 = 由访问修饰符(private/protected/public)

  • 确定
  • 静态变量(全局范围)
    生存期 = 程序运行时(1)
    可见性 = 在(2)

  • 中实例化的编译单元
  • 堆变量
    Life = 由您定义(新的要删除)
    可见性 = 由您定义(无论您将指针指向何处)

  • 堆栈变量堆栈变量
    可见性 = 从声明到作用域退出
    生命周期 = 从声明到声明范围退出


(1)更确切地说: 从初始化到反初始化的编译单元(即 C/C + + 文件)。编译单元的初始化顺序不由标准定义。

(2)注意: 如果你在头部实例化一个静态变量,每个编译单元都有自己的副本。

GC 在某些情况下的优势在另一些情况下是一种烦恼; 对 GC 的依赖鼓励不要过多地考虑它。理论上,等待直到“空闲”时期或直到它绝对必须,当它将窃取带宽和造成响应延迟在您的应用程序。

但你不必不去想它。就像多线程应用程序中的其他所有东西一样,当你可以屈服时,你就可以屈服。举个例子。Net 中,可以请求 GC; 通过这样做,您可以更频繁地更短时间地运行 GC,并分散与此开销相关的延迟,而不是运行较长时间的 GC。

但是,这打败了 GC 的主要吸引力,它似乎“被鼓励不必考虑太多,因为它是自动的。”

如果你第一次接触编程是在 GC 变得流行之前,并且对 malloc/free 和 new/delete 感到舒服,那么你甚至可能会发现 GC 有点烦人和/或者不信任(因为人们可能不信任“优化”,它有一个复杂的历史)许多应用程序容忍随机延迟。但是对于那些无法接受随机延迟的应用程序来说,一个常见的反应是避开 GC 环境,转向纯粹的非托管代码(或者说,上帝保佑,一种长期消亡的艺术,汇编语言)

不久前,我这里有一个暑期学生,一个实习生,聪明的孩子,谁是断奶的 GC; 他是如此坚信 GC 的优越性,甚至当编程在非托管 C/C + + 他拒绝遵循 malloc/free new/delete 模型,因为,引用,“你不应该这样做,在一个现代编程语言。”你知道吗?对于小型的、短时间运行的应用程序,你确实可以逃脱惩罚,但是对于长时间运行的性能应用程序就不行了。

堆栈是由编译器分配的内存,当我们编译程序时,默认情况下,编译器从操作系统分配一些内存(我们可以改变 IDE 中编译器设置的设置) ,而操作系统是给你内存的那个,它依赖于系统中的许多可用内存和许多其他东西,进入堆栈内存是分配的,当我们声明一个变量时,他们复制(参考形式)这些变量被推到堆栈上,它们遵循一些命名约定默认情况下,它的 CDECL 在 Visual 工作室 例如: 中缀表示法: C = a + b; 堆栈推送是从右到左推送,b 到堆栈,操作符,a 到堆栈,这些 i,e,c 到堆栈的结果。 前缀符号: = + 出租车 这里所有的变量都被推到堆栈的第一个(从右到左) ,然后执行操作。 由编译器分配的内存是固定的。因此,让我们假设为应用程序分配了1MB 的内存,假设变量使用了700kb 的内存(除非动态分配,否则所有本地变量都会被推到堆栈中) ,这样剩下的324kb 内存就会被分配给堆。 并且当函数的作用域结束时,这个堆栈的生存时间更短,这些堆栈将被清除。