当使用std::list<std::string>时,我偶然发现了堆栈溢出问题std::string的内存泄漏,其中一个注释说:
不要用那么多new。我看不出你为什么在任何地方都用新的。你可以在c++中通过值创建对象,这是使用该语言的一个巨大优势。您不必分配堆上的所有内容。不要像Java程序员那样思考。
我不太确定他这么说是什么意思。
为什么在c++中应该尽可能多地根据值创建对象,这在内部有什么不同?我误解了答案吗?
当使用std::list<std::string>时,我偶然发现了堆栈溢出问题std::string的内存泄漏,其中一个注释说:
不要用那么多new。我看不出你为什么在任何地方都用新的。你可以在c++中通过值创建对象,这是使用该语言的一个巨大优势。您不必分配堆上的所有内容。不要像Java程序员那样思考。
我不太确定他这么说是什么意思。
为什么在c++中应该尽可能多地根据值创建对象,这在内部有什么不同?我误解了答案吗?
当前回答
在很大程度上,这是某人将自己的弱点提升为普遍准则。使用new操作符创建对象本身没有任何问题。有一些争论是,你必须遵循一些规则:如果你创建了一个对象,你需要确保它会被销毁。
最简单的方法是在自动存储中创建对象,这样c++就知道在它超出作用域时销毁它:
{
File foo = File("foo.dat");
// Do things
}
现在,观察一下,当你在结束大括号之后离开那个块时,foo超出了作用域。c++会自动调用它的析构函数。与Java不同,您不需要等待垃圾回收来找到它。
你写过
{
File * foo = new File("foo.dat");
你需要显式地匹配它
delete foo;
}
或者更好的是,将你的File *分配为“智能指针”。如果你不小心,它可能会导致泄漏。
答案本身做了一个错误的假设,如果你不使用new,你就不会在堆上分配;事实上,在c++中你不知道这一点。最多,您知道少量的内存,比如一个指针,肯定分配在堆栈上。但是,考虑一下File的实现是否像这样:
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
然后FileImpl仍然会被分配到堆栈上。
是的,你最好确保你有
~File(){ delete fd ; }
在课堂上也是如此;如果没有它,即使根本没有在堆上分配内存,也会从堆中泄漏内存。
其他回答
我想海报的意思是,你不必把所有的东西都分配到堆上,而不是堆栈上。
基本上,对象是在堆栈上分配的(当然,如果对象大小允许的话),因为堆栈分配的成本较低,而不是基于堆的分配,后者涉及分配器的大量工作,并增加了冗长的内容,因为这样您就必须管理分配在堆上的数据。
有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个对象都有相应的内存区域:堆栈和堆。
堆栈
堆栈总是按顺序分配内存。它可以这样做,因为它要求您以相反的顺序释放内存(先入,后出:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,并且下一个要分配的地址是隐式的。
在c++中,这被称为自动存储,因为存储是在作用域结束时自动声明的。一旦当前代码块(使用{}分隔)的执行完成,该代码块中所有变量的内存将被自动收集。这也是调用析构函数来清理资源的时刻。
Heap
堆支持更灵活的内存分配模式。记账更复杂,分配更慢。因为没有隐式释放点,你必须手动释放内存,使用delete或delete[] (C中的free)。然而,没有隐式释放点是堆灵活性的关键。
使用动态分配的原因
即使使用堆速度较慢,并可能导致内存泄漏或内存碎片,动态分配也有很好的用例,因为它的限制较少。
使用动态分配的两个关键原因:
You don't know how much memory you need at compile time. For instance, when reading a text file into a string, you usually don't know what size the file has, so you can't decide how much memory to allocate until you run the program. You want to allocate memory which will persist after leaving the current block. For instance, you may want to write a function string readfile(string path) that returns the contents of a file. In this case, even if the stack could hold the entire file contents, you could not return from a function and keep the allocated memory block.
为什么动态分配往往是不必要的
在c++中,有一个简洁的构造叫做析构函数。这种机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术被称为RAII,是c++的特点。它将资源“包装”到对象中。Std::string就是一个很好的例子。这个代码片段:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
实际上分配的内存是可变的。string对象使用堆分配内存,并在析构函数中释放内存。在这种情况下,您不需要手动管理任何资源,仍然可以获得动态内存分配的好处。
特别地,它在这段代码中暗示:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
存在不需要的动态内存分配。该程序需要更多的输入(!),并引入了忘记释放内存的风险。这样做没有明显的好处。
为什么你应该尽可能多地使用自动存储
基本上,最后一段总结了一下。尽可能多地使用自动存储会使你的程序:
打字更快; 跑起来更快; 不容易发生内存/资源泄漏。
加分
在引用的问题中,还有其他的关注点。特别是下面的类:
class Line {
public:
Line();
~Line();
std::string* mString;
};
Line::Line() {
mString = new std::string("foo_bar");
}
Line::~Line() {
delete mString;
}
实际上比下面这个更有风险:
class Line {
public:
Line();
std::string mString;
};
Line::Line() {
mString = "foo_bar";
// note: there is a cleaner way to write this.
}
原因是std::string正确地定义了一个复制构造函数。考虑下面的程序:
int main ()
{
Line l1;
Line l2 = l1;
}
使用原始版本,这个程序可能会崩溃,因为它对同一个字符串使用了两次delete。使用修改后的版本,每个Line实例将拥有自己的字符串实例,每个实例都有自己的内存,并且都将在程序结束时释放。
其他的笔记
由于上述原因,RAII的广泛使用被认为是c++中的最佳实践。然而,还有一个不太明显的额外好处。基本上,它比各个部分的和要好。整个机构组成。这尺度。
如果你使用Line类作为构建块:
class Table
{
Line borders[4];
};
Then
int main ()
{
Table table;
}
分配四个std::string实例,四个Line实例,一个Table实例和所有字符串的内容,所有的东西都会自动释放。
在很大程度上,这是某人将自己的弱点提升为普遍准则。使用new操作符创建对象本身没有任何问题。有一些争论是,你必须遵循一些规则:如果你创建了一个对象,你需要确保它会被销毁。
最简单的方法是在自动存储中创建对象,这样c++就知道在它超出作用域时销毁它:
{
File foo = File("foo.dat");
// Do things
}
现在,观察一下,当你在结束大括号之后离开那个块时,foo超出了作用域。c++会自动调用它的析构函数。与Java不同,您不需要等待垃圾回收来找到它。
你写过
{
File * foo = new File("foo.dat");
你需要显式地匹配它
delete foo;
}
或者更好的是,将你的File *分配为“智能指针”。如果你不小心,它可能会导致泄漏。
答案本身做了一个错误的假设,如果你不使用new,你就不会在堆上分配;事实上,在c++中你不知道这一点。最多,您知道少量的内存,比如一个指针,肯定分配在堆栈上。但是,考虑一下File的实现是否像这样:
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
然后FileImpl仍然会被分配到堆栈上。
是的,你最好确保你有
~File(){ delete fd ; }
在课堂上也是如此;如果没有它,即使根本没有在堆上分配内存,也会从堆中泄漏内存。
核心原因是堆上的对象总是比简单值更难使用和管理。编写易于阅读和维护的代码始终是任何认真的程序员的首要任务。
另一种情况是,我们使用的库提供了值语义,使动态分配变得不必要。Std::string就是一个很好的例子。
然而,对于面向对象的代码,使用指针——这意味着使用new来预先创建指针——是必须的。为了简化资源管理的复杂性,我们有几十种工具使其尽可能简单,比如智能指针。基于对象的范式或泛型范式假设值语义,并需要更少或不需要新的,正如其他地方的海报所述。
传统的设计模式,尤其是GoF书中提到的那些,大量使用new,因为它们是典型的面向对象代码。
新就是新目标。
Recall why goto is so reviled: while it is a powerful, low-level tool for flow control, people often used it in unnecessarily complicated ways that made code difficult to follow. Furthermore, the most useful and easiest to read patterns were encoded in structured programming statements (e.g. for or while); the ultimate effect is that the code where goto is the appropriate way to is rather rare, if you are tempted to write goto, you're probably doing things badly (unless you really know what you're doing).
New也类似——它经常被用来使事情变得不必要的复杂和难以阅读,最有用的使用模式可以被编码到各种各样的类中。此外,如果您需要使用任何新的使用模式,而这些模式还没有标准类,您可以编写自己的类来编码它们!
我甚至认为new比goto更糟糕,因为需要对new和delete语句进行配对。
像goto一样,如果您认为需要使用new,那么您可能做得很糟糕——特别是如果您在一个类的实现之外这样做,这个类的目的是封装您需要做的任何动态分配。