Java有一个自动GC,它偶尔会停止世界,但会处理堆上的垃圾。现在C / C ++应用程序没有这些STW冻结,它们的内存使用也不会无限增长。如何实现这种行为?如何处理死物?
new
。
What happens to garbage in C++?
通常不是将其编译为可执行文件吗?
Java有一个自动GC,它偶尔会停止世界,但会处理堆上的垃圾。现在C / C ++应用程序没有这些STW冻结,它们的内存使用也不会无限增长。如何实现这种行为?如何处理死物?
new
。
What happens to garbage in C++?
通常不是将其编译为可执行文件吗?
Answers:
程序员负责确保通过创建的对象new
被删除delete
。如果创建了一个对象,但在最后一个指针或对该对象的引用超出范围之前未将其销毁,则该对象会掉入裂缝并成为Memory Leak。
不幸的是,对于C,C ++和其他不包含GC的语言,随着时间的流逝,它们只会堆积起来。它可能导致应用程序或系统内存不足,并且无法分配新的内存块。此时,用户必须求助于结束应用程序,以便操作系统可以回收使用的内存。
就减轻此问题而言,有几件事使程序员的生活变得更加轻松。这些主要受到范围性质的支持。
int main()
{
int* variableThatIsAPointer = new int;
int variableInt = 0;
delete variableThatIsAPointer;
}
在这里,我们创建了两个变量。它们存在于大括号定义的“ 块范围”中{}
。当执行移出该范围时,这些对象将被自动删除。在这种情况下,variableThatIsAPointer
顾名思义,它是指向内存中对象的指针。当它超出范围时,指针将被删除,但指向的对象将保留。在这里,我们delete
将此对象移出范围以确保没有内存泄漏。但是,我们也可以将此指针传递到其他地方,并希望以后再删除它。
范围的这种性质扩展到了类:
class Foo
{
public:
int bar; // Will be deleted when Foo is deleted
int* otherBar; // Still need to call delete
}
在此,同样的原则适用。我们不必担心bar
何时Foo
删除。但是,对于otherBar
,仅删除指针。如果otherBar
是指向它所指向的任何对象的唯一有效指针,则可能delete
应该在Foo
析构函数中使用它。这是RAII背后的驱动概念
资源分配(获取)由构造函数在对象创建(特别是初始化)期间完成,而资源释放(释放)由析构函数在对象破坏(特别是最终确定)期间完成。因此,可以保证在初始化完成和完成之间开始时保持资源(保持资源是类不变的),并且仅在对象处于活动状态时才保持资源。因此,如果没有对象泄漏,就不会有资源泄漏。
RAII还是智能指针背后的典型驱动力。在C ++标准库,这些是std::shared_ptr
,std::unique_ptr
,和std::weak_ptr
; 尽管我已经看到并使用了遵循相同概念的其他shared_ptr
/ weak_ptr
实现。对于这些对象,引用计数器跟踪给定对象有多少个指针,并且delete
在没有更多引用指向该对象时自动对其进行处理。
除此之外,对于程序员来说,一切都取决于适当的实践和纪律,以确保其代码正确处理对象。
delete
-那就是我要找的东西。太棒了
delete
在应用程序代码中(从C ++ 14开始,与相同new
),而应使用智能指针和RAII删除堆对象。std::unique_ptr
类型和std::make_unique
功能是直接,简单的更换new
,并delete
在应用程序代码的水平。
C ++没有垃圾回收。
C ++应用程序需要处理自己的垃圾。
C ++应用程序程序员需要了解这一点。
当他们忘记时,结果称为“内存泄漏”。
new
和也很好delete
。
malloc
和free
,或new[]
和delete[]
,或任何其他分配器(如Windows的GlobalAlloc
,LocalAlloc
,SHAlloc
,CoTaskMemAlloc
,VirtualAlloc
,HeapAlloc
,...),而内存为您分配(例如,通过fopen
)。
在C,C ++和其他没有垃圾收集器的系统中,该语言及其库为开发人员提供了指示何时可以回收内存的工具。
最基本的设施是自动存储。很多时候,语言本身确保物品被处置:
int global = 0; // automatic storage
int foo(int a, int b) {
static int local = 1; // automatic storage
int c = a + b; // automatic storage
return c;
}
在这种情况下,编译器负责知道何时未使用这些值并回收与它们关联的存储。
在使用动态存储时,在C中,传统上是使用分配malloc
和回收内存的free
。在C ++中,内存通常是由分配new
和回收的delete
。
多年来,C并没有太大变化,但是现代的C ++避开了,new
并且delete
完全并依赖于库工具(它们本身使用new
并delete
适当地使用):
std::unique_ptr
和std::shared_ptr
std::string
,std::vector
,std::map
,...所有的内部动态管理分配的内存透明说到这shared_ptr
,存在一个风险:如果形成一个引用循环并且没有破坏引用,则可能存在内存泄漏。开发人员应避免这种情况,最简单的方法是shared_ptr
完全避免,第二个最简单的方法是避免类型级别的循环。
其结果是内存泄漏是不是在C ++的问题,即使是新用户,只要他们使用避免new
,delete
或std::shared_ptr
。这与C语言不同,在C语言中,严格的纪律是必需的,并且通常不够用。
但是,如果没有提到内存泄漏的双胞胎姐妹:悬空指针,这个答案是不完整的。
悬空指针(或悬空引用)是通过保持指向死对象的指针或引用而造成的危险。例如:
int main() {
std::vector<int> vec;
vec.push_back(1); // vec: [1]
int& a = vec.back();
vec.pop_back(); // vec: [], "a" is now dangling
std::cout << a << "\n";
}
使用悬空指针或引用是Undefined Behavior。通常,幸运的是,这是立即崩溃。不幸的是,这经常会首先导致内存损坏……并且不时出现怪异的行为,因为编译器会发出真正怪异的代码。
就程序的安全性/正确性而言,迄今为止,未定义行为是C和C ++的最大问题。您可能想检查Rust的语言,该语言没有垃圾收集器,也没有未定义的行为。
new
,delete
而shared_ptr
”; 没有new
,shared_ptr
您拥有直接所有权,因此不会泄漏。当然,您可能有悬挂的指针等。但是,恐怕您需要离开C ++才能摆脱这些指针。
C ++有个叫做RAII的东西。从根本上讲,这意味着您在走走时会清理垃圾,而不是将其堆放,然后让清洁工将其整理干净。(想象我在房间里看足球-当我喝啤酒罐并且需要新的罐时,C ++的方法是将空罐在冰箱的路上拿到垃圾箱中,C#的方法是将其装在地板上然后等着女仆来打扫卫生。
现在可以在C ++中泄漏内存,但是这样做需要您保留常规的构造并恢复为C的处理方式-分配一个内存块并在没有任何语言帮助的情况下跟踪该内存块的位置。有些人忘记了该指针,因此无法删除该块。
应该注意的是,在C ++的情况下,常见的误解是“您需要执行手动内存管理”。实际上,您通常不会在代码中进行任何内存管理。
在大多数情况下,当您需要一个对象时,该对象将在程序中具有定义的生存期,并在堆栈上创建。这适用于所有内置原始数据类型,也适用于类和结构的实例:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
函数结束后,将自动删除堆栈对象。在Java中,对象总是在堆上创建,因此必须通过诸如垃圾回收的某种机制将其删除。这不是堆栈对象的问题。
在堆栈上使用空间可用于固定大小的对象。当您需要可变数量的空间(例如数组)时,可以使用另一种方法:将列表封装在固定大小的对象中,该对象将为您管理动态内存。之所以可行,是因为对象可以具有特殊的清除功能,即析构函数。当对象超出范围并且与构造函数相反时,可以保证调用它:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
使用内存的代码中根本没有内存管理。我们唯一需要确定的是我们编写的对象具有合适的析构函数。不管我们如何离开范围listTest
,无论是通过异常还是通过从其返回,~MyList()
都将调用析构函数,并且我们不需要管理任何内存。
(我认为使用二进制NOT运算符~
指示析构函数是一个有趣的设计决策。当用于数字时,它会将位求反;以此类推,在这里,它表明构造函数所做的是求反的。)
基本上所有需要动态内存的C ++对象都使用此封装。它被称为RAII(“资源获取就是初始化”),这是表达对象关心自己内容的简单想法的一种怪异方法。他们获得的是他们要清理的东西。
现在,这两种情况都用于具有明确定义的生存期的内存:生存期与作用域相同。如果我们不希望对象在离开范围时过期,则可以使用第三种机制为我们管理内存:智能指针。当您具有类型在运行时变化但具有公共接口或基类的对象实例时,也会使用智能指针:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
还有另一种智能指针, std::shared_ptr
,用于在多个客户端之间共享对象。它们仅在最后一个客户端超出范围时才删除所包含的对象,因此可以在完全未知将有多少客户端以及将使用该对象多长时间的情况下使用它们。
总而言之,我们看到您实际上并没有执行任何手动内存管理。一切都被封装,然后通过全自动,基于作用域的内存管理来处理。在这还不够的情况下,将使用智能指针封装原始内存。
在C ++代码中的任何地方,在构造函数之外的原始分配以及delete
在析构函数之外的原始调用中,都使用原始指针作为资源所有者是极不明智的做法,因为在发生异常时几乎无法管理原始指针,并且通常很难安全使用它们。
RAII的最大好处之一是它不仅限于内存。实际上,它提供了一种非常自然的方式来管理资源,例如文件和套接字(打开/关闭)以及同步机制(例如互斥锁)(锁定/解锁)。基本上,可以在C ++中以完全相同的方式管理可以获取并必须释放的每个资源,并且这些管理都不留给用户。它全部封装在构造函数中获取并在析构函数中释放的类中。
例如,锁定互斥量的函数通常在C ++中这样编写:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
其他语言使情况变得更加复杂,要么要求您手动执行此操作(例如,在finally
子句中),要么衍生出专门的机制来解决此问题,但不是以一种特别优雅的方式(通常是在他们的晚年,当有足够的人拥有遭受缺点)。这种机制是Java 中的try-with-resources和C#中的using语句,两者都是C ++ RAII的近似值。
因此,总而言之,所有这些都是C ++中RAII的非常肤浅的描述,但是我希望它可以帮助读者理解C ++中的内存甚至资源管理通常不是“手动”的,而是实际上是自动的。
delete
,你就死了”的答案飙升到30分以上并被接受,而这个答案只有5分。有人在这里实际使用C ++吗?
特别是对于C语言,该语言没有为您提供管理动态分配内存的工具。您绝对有责任确保每个*alloc
人在free
某处都有对应的内容。
真正令人讨厌的地方是资源分配中途失败时。您是否会重试,是否从头开始回滚并重新开始,是否因错误而回滚并退出,是否只是完全保释并让OS处理?
例如,这是一个分配非连续2D数组的函数。这里的行为是,如果在过程中途发生分配失败,我们将回滚所有内容并使用NULL指针返回错误指示:
/**
* Allocate space for an array of arrays; returns NULL
* on error.
*/
int **newArr( size_t rows, size_t cols )
{
int **arr = malloc( sizeof *arr * rows );
size_t i;
if ( arr ) // malloc returns NULL on failure
{
for ( i = 0; i < rows; i++ )
{
arr[i] = malloc( sizeof *arr[i] * cols );
if ( !arr[i] )
{
/**
* Whoopsie; we can't allocate any more memory for some reason.
* We can't just return NULL at this point since we'll lose access
* to the previously allocated memory, so we branch to some cleanup
* code to undo the allocations made so far.
*/
goto cleanup;
}
}
}
goto done;
/**
* We encountered a failure midway through memory allocation,
* so we roll back all previous allocations and return NULL.
*/
cleanup:
while ( i ) // this is why we didn't limit the scope of i to the for loop
free( arr[--i] ); // delete previously allocated rows
free( arr ); // delete arr object
arr = NULL;
done:
return arr;
}
此代码是对接丑与goto
S,但是,在没有任何类型的结构化异常处理机制,这是相当多的应对问题,而只是想逃出来完全,只有这样,特别是如果你的资源分配代码嵌套更多超过一个循环。这是极少数goto
实际具有吸引力的选择之一。否则,您将使用一堆标志和额外的if
语句。
通过为每种资源编写专用的分配器/释放器函数,您可以使自己的生活更轻松,例如
Foo *newFoo( void )
{
Foo *foo = malloc( sizeof *foo );
if ( foo )
{
foo->bar = newBar();
if ( !foo->bar ) goto cleanupBar;
foo->bletch = newBletch();
if ( !foo->bletch ) goto cleanupBletch;
...
}
goto done;
cleanupBletch:
deleteBar( foo->bar );
// fall through to clean up the rest
cleanupBar:
free( foo );
foo = NULL;
done:
return foo;
}
void deleteFoo( Foo *f )
{
deleteBar( f->bar );
deleteBletch( f->bletch );
free( f );
}
goto
陈述,这也是一个很好的答案。建议在某些地区进行此操作。这是一种防止C语言中的异常等效的常用方案。看看Linux内核代码,其中充满了goto
语句-而且不会泄漏。
goto
是无关紧要的。如果你改变了它会是更具可读性goto done;
来return arr;
和arr=NULL;done:return arr;
到return NULL;
。尽管在更复杂的情况下可能确实存在多个goto
s,但它们开始在不同的就绪级别上展开(通过在C ++中展开异常堆栈来完成)。
我学会了将内存问题分为许多不同的类别。
一滴水。假设程序在启动时泄漏了100个字节,但是再也不会泄漏了。追逐并消除那些一次性泄漏是一件好事(我确实喜欢通过泄漏检测功能获得一份清晰的报告),但这不是必需的。有时还有更大的问题需要解决。
反复泄漏。在程序寿命期间重复调用的函数会定期泄漏内存,这是一个大问题。这些滴水会折磨该程序,甚至可能折磨OS,直至致死。
相互参考。如果对象A和B通过共享指针相互引用,则必须在这些类的设计中或在实现/使用这些类的代码中打破圆形性的过程中,做一些特殊的事情。(对于垃圾收集语言,这不是问题。)
记住太多了。这是垃圾/内存泄漏的邪恶表亲。RAII将无济于事,垃圾收集也不会。无论哪种语言,这都是一个问题。如果某个活动变量具有将其连接到某个随机内存块的路径,则该随机内存块不会成为垃圾。使程序变得健忘,使其可以运行几天是很棘手的。制作一个可以运行几个月(例如,直到磁盘出现故障)的程序非常非常棘手。
很长一段时间以来,我都没有遇到严重的泄漏问题。在C ++中使用RAII非常有助于解决这些滴漏和泄漏。(但是,使用共享指针时一定要小心。)更重要的是,我遇到了一些应用程序的问题,这些应用程序的内存使用量不断增长,因为与内存的未断开连接已不再使用。