我搜索过,但是对这三个概念并没有很好的理解。什么时候需要使用动态分配(在堆中)?它的真正优势是什么?静态和栈的问题是什么?我能否编写一个完整的应用程序而不在堆中分配变量?
我听说其他语言包含“垃圾回收器”,因此您不必担心内存。垃圾回收器是做什么的?
自己操作内存能做什么事情,使用垃圾回收器不能做到?
曾经有人对我说过:
int * asafe=new int;
我有一个“指向指针”的指针。这是什么意思?它与以下内容不同:
asafe=new int;
?
我搜索过,但是对这三个概念并没有很好的理解。什么时候需要使用动态分配(在堆中)?它的真正优势是什么?静态和栈的问题是什么?我能否编写一个完整的应用程序而不在堆中分配变量?
我听说其他语言包含“垃圾回收器”,因此您不必担心内存。垃圾回收器是做什么的?
自己操作内存能做什么事情,使用垃圾回收器不能做到?
曾经有人对我说过:
int * asafe=new int;
asafe=new int;
?
有一个类似的问题被问到了,但它没有涉及静态内存。
静态变量基本上是全局变量,即使您无法全局访问它。通常,它的地址在可执行文件本身中。整个程序只有一个副本。无论您进入多少次函数调用(或类)(以及多少线程!),该变量都指向同一内存位置。
堆是一堆可以动态使用的内存。如果您想要4kb的对象,则动态分配器将在堆的空闲空间列表中查找4kb块,并将其分配给您。通常,动态内存分配器(malloc、new等)从内存末尾开始向后工作。
解释堆栈如何增长和收缩有点超出了本回答的范围,但可以说您始终只能从末尾添加和删除。堆栈通常从高处开始,并向低地址增长。当堆栈在中间遇到动态分配器时,您就会耗尽内存(但请参阅物理内存与虚拟内存和碎片化)。多个线程将需要多个堆栈(进程通常为堆栈保留最小大小)。
静态/全局变量对于你知道你总是需要的内存非常有用,而且你知道你永远不想释放它们。 (顺便说一句,嵌入式环境可以被认为只有静态内存…堆栈和堆是由第三种内存类型共享的已知地址空间的一部分:程序代码。当程序需要像链接列表这样的东西时,程序通常会从其静态内存中进行动态分配。但无论如何,静态内存本身(缓冲区)本身并没有“分配”,而是其他对象从缓冲区所占用的内存中分配出来的。在非嵌入式环境中也可以做到这一点,控制台游戏通常会避免使用内置的动态内存机制,而是使用预设大小的缓冲区来紧密控制分配过程,以此来控制分配过程。)
堆栈变量对于你知道只要函数在作用域内(在堆栈某个地方),你希望变量保留非常有用。堆栈对于你在代码所在位置需要的变量很好,但是在该代码之外不需要。当你访问资源(如文件)并希望资源在离开该代码时自动消失时,它们也非常好用。
堆分配(动态分配的内存)对于你想比上述更灵活非常有用。经常情况下,一个函数被调用以响应事件(用户单击“创建框”按钮)。适当的响应可能需要分配一个新对象(新的Box对象),该对象应在函数退出后仍然存在,因此不能在堆栈上。但是你不知道程序开始时需要多少个框,所以它不能是静态的。
最近我听说垃圾回收器有多么好,因此也许一个异议的声音会很有帮助。
当性能不是特别重要时,垃圾回收是一种非常棒的机制。我听说GC变得越来越好并且更加复杂,但事实是,你可能被迫接受性能损失(取决于使用情况)。而且如果你懒惰,它仍然可能无法正常工作。在最好的情况下,垃圾回收器意识到当没有更多引用时,你的内存就会消失(参见引用计数)。但是,如果你有一个指向自身的对象(可能通过引用另一个对象来实现),那么单独的引用计数将无法指示可以删除内存。在这种情况下,GC需要查看整个引用关系图,并确定是否存在只被自身引用的孤岛。一时之间,我猜测这将是O(n^2)操作,但不管它是什么,如果你关心性能,它可能会变得很糟糕。(编辑:马丁·B 指出对于相当高效的算法,它是O(n),但如果你关心性能并且可以在没有垃圾回收的情况下进行常量时间的释放,则仍然是O(n)太多。)
个人而言,当我听到有人说C++没有垃圾回收时,我的脑海中会认为这是C++的一个特性,但我可能是少数人。对于在C和C++中编程的人来说,最难学习的可能就是指针以及如何正确地处理它们的动态内存分配。其他一些语言,比如Python,如果没有垃圾回收,将会非常糟糕,因此我认为这取决于你想从语言中得到什么。如果你想要可靠的性能,那么没有垃圾回收的C++是我能想到的除Fortran之外唯一的选择。如果你想要易用性和训练轮(可以帮助你避免崩溃,而无需学习“正确”的内存管理),那么选择一个具有垃圾回收功能的语言。即使你知道如何很好地管理内存,它也会为你节省时间,让你可以优化其他代码。现在真的没有太多的性能惩罚了,但如果你真的需要可靠的性能(以及在底层发生了什么事情、何时发生的能力),那么我会坚持使用C++。我听说过的每个主要游戏引擎都是用C++(如果不是用C或汇编语言)。Python等脚本语言适合编写脚本,但不适合作为主要游戏引擎。
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
不绑定于其类的特定对象,而是绑定于类本身。上述三个名称的地址相同,并且都表示同一对象。有关何时以及如何初始化静态对象的特殊规则,但现在不必担心。这就是术语“静态初始化顺序混乱”的含义。
最后一个存储期是动态的。如果您想让对象在另一个岛上存活,并想要放置引用它们的指针,则使用该存储期。如果您的对象很大,并且想要创建仅在运行时已知大小的数组,则也可以使用该存储期。由于此灵活性,具有动态存储期的对象管理起来复杂且缓慢。具有该动态存储期的对象在适当的 new 运算符调用发生时开始生命周期。
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();
}
只有在调用delete后,它们的生命周期才会结束。如果您忘记了,这些对象的生命周期将永远不会结束。定义用户声明构造函数的类对象不会调用其析构函数。具有动态存储期的对象需要手动处理其生命周期和相关内存资源。已经存在的库可简化使用它们的过程。使用智能指针可以为特定对象建立显式垃圾回收:
int main() {
shared_ptr<string> s(new string);
foo(s);
}
void foo(shared_ptr<string> s) {
cout << s->size();
}
这里简短地概括了:
静态变量(类)
生命周期 = 程序运行时间 (1)
可见性 = 由访问修饰符(private/protected/public)决定
静态变量(全局范围)
生命周期 = 程序运行时间 (1)
可见性 = 它所实例化的编译单元 (2)
堆变量
生命周期 = 由你定义(从new到delete)
可见性 = 由你定义(无论你将指针分配给什么)
栈变量
可见性 = 从声明到退出作用域
生命周期 = 从声明到声明作用域退出
(1) 更准确地说:从编译单元(即 C/C++ 文件)初始化到去初始化。编译单元的初始化顺序不被标准定义。
(2) 注意:如果在头文件中实例化静态变量,每个编译单元都会有自己的副本。
当堆栈太“深”并且溢出可用于堆栈分配的内存时,堆栈内存分配(函数变量、局部变量)可能会出现问题。堆是为需要从多个线程或整个程序生命周期中访问的对象而设计的。您可以编写一个完整的程序,而不使用堆。
如果没有垃圾收集器,您很容易泄漏内存,但是您也可以控制何时释放对象和内存。当Java运行GC并且我有一个实时进程时,我遇到了问题,因为GC是一个独占线程(没有其他东西可以运行)。因此,如果性能至关重要并且您可以保证没有泄漏的对象,则不使用GC非常有帮助。否则,当应用程序消耗内存并且您必须跟踪泄漏源时,它只会让您讨厌生活。
栈是编译器分配的内存,每当我们编译程序时,默认情况下编译器会从操作系统中分配一些内存(我们可以在IDE的编译器设置中更改设置),而操作系统是提供内存的来源,它取决于系统上可用的内存和其他许多因素。至于栈内存,当我们声明一个变量时,它们被复制(作为形式参数)并推送到栈上,它们默认遵循一些命名约定,如Visual Studios中的CDECL。 例如:中缀表示法: c=a+b; 栈的推送是从右到左进行的,将b推入栈中,然后是运算符、a和这些变量的结果即c。 前缀表示法: =+cab 在这里,所有变量都首先被推入栈中(从右到左),然后进行操作。 编译器分配的这种内存是固定的。因此,假设我们的应用程序分配了1MB的内存,假设变量使用了700kb的内存(除非它们是动态分配的本地变量,否则所有本地变量都会被推送到栈上),那么剩余的324kb内存将被分配给堆。 这个栈的生命周期较短,当函数的范围结束时,这些栈就会被清除。