协慌网

登录 贡献 社区

为什么 C ++ 程序员应该尽量减少 “新” 的使用?

在使用 std :: list 偶然发现 Stack Overflow 问题内存泄漏与 std :: string其中一条评论说:

停止使用new东西。我看不出你在任何地方使用新的任何理由。您可以使用 C ++ 中的值创建对象,这是使用该语言的巨大优势之一。您不必在堆上分配所有内容。不要像 Java 程序员那样思考。

我不太确定他的意思是什么。为什么要尽可能经常地用 C ++ 中的值创建对象,它在内部有什么区别?我误解了答案吗?

答案

有两种广泛使用的内存分配技术:自动分配和动态分配。通常,每个都有一个相应的内存区域:堆栈和堆。

堆栈总是以顺序方式分配内存。它可以这样做,因为它要求您以相反的顺序释放内存(First-In,Last-Out:FILO)。这是许多编程语言中局部变量的内存分配技术。它非常非常快,因为它需要最少的簿记,下一个要分配的地址是隐含的。

在 C ++ 中,这称为自动存储,因为存储在范围结束时自动声明。一旦完成当前代码块(使用{}分隔)的执行,就会自动收集该块中所有变量的内存。这也是调用析构函数来清理资源的时刻。

堆允许更灵活的内存分配模式。簿记更复杂,分配更慢。因为没有隐式释放点,所以必须使用deletedelete[]手动释放内存(在 C 中为free )。但是,缺少隐式释放点是堆灵活性的关键。

使用动态分配的原因

即使使用堆较慢并且可能导致内存泄漏或内存碎片,动态分配也有很好的用例,因为它的限制较少。

使用动态分配的两个主要原因:

  • 您不知道在编译时需要多少内存。例如,在将文本文件读入字符串时,通常不知道文件的大小,因此在运行程序之前无法确定要分配的内存量。

  • 您想要分配在离开当前块后将保留的内存。例如,您可能希望编写一个返回文件内容的函数string readfile(string path) 。在这种情况下,即使堆栈可以保存整个文件内容,也无法从函数返回并保留分配的内存块。

为什么动态分配通常是不必要的

在 C ++ 中,有一个称为析构函数的简洁结构。此机制允许您通过将资源的生命周期与变量的生命周期对齐来管理资源。这种技术称为RAII ,是 C ++ 的一个显着特点。它将资源 “包装” 到对象中。 std::string是一个很好的例子。这个片段:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

实际上分配了可变数量的内存。 std::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];
 };

然后

int main ()
 {
     Table table;
 }

分配四个std::string实例,四个Line实例,一个Table实例和所有字符串的内容, 所有内容都自动释放

因为堆栈快速而且万无一失

在 C ++ 中,只需要一条指令就可以为给定函数中的每个局部作用域对象分配空间(在堆栈上),并且不可能泄漏任何内存。该评论意图(或应该有意)说出“使用堆栈而不是堆” 之类的东西。

情况很复杂。

首先,C ++ 不是垃圾回收。因此,对于每个新的,必须有相应的删除。如果你没有把这个删除,那么你有内存泄漏。现在,对于这样一个简单的情况:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

这很简单。但是如果 “Do stuff” 抛出异常会发生什么?糟糕:内存泄漏。如果 “做东西” 问题提前return怎么办?糟糕:内存泄漏。

这是最简单的情况 。如果您碰巧将该字符串返回给某人,现在他们必须将其删除。如果他们将其作为参数传递,接收它的人是否需要删除它?什么时候应该删除它?

或者,你可以这样做:

std::string someString(...);
//Do stuff

没有delete 。该对象是在 “堆栈” 上创建的,一旦超出范围,它将被销毁。您甚至可以返回对象,从而将其内容传输到调用函数。您可以将对象传递给函数(通常作为引用或 const 引用: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis) 。依此类推。

全部没有newdelete 。毫无疑问,谁拥有记忆或谁负责删除记忆。如果你这样做:

std::string someString(...);
std::string otherString;
otherString = someString;

据了解, otherStringsomeString 数据的副本。它不是指针; 它是一个单独的对象。它们可能恰好具有相同的内容,但您可以在不影响另一个的情况下更改一个:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

看到这个想法?