使用在C ++中动态分配的数组有什么问题?

时间:2014-06-10 08:19:36

标签: c++ dynamic-allocation

与以下代码类似:

int size = myGetSize();
std::string* foo;
foo = new std::string[size];
//...
// using the table
//...
delete[] foo;

我听说这种用法(不是这段代码,而是整个动态分配)在某些情况下可能不安全,并且只能用于RAII。为什么呢?

9 个答案:

答案 0 :(得分:47)

我发现您的代码存在三个主要问题:

  1. 使用赤裸裸的指针。

  2. 使用裸new

  3. 使用动态数组。

  4. 由于其自身原因,每个都是不受欢迎的。我将尝试依次解释每一个。

    (1)违反我喜欢称之为子表达式正确性,以及(2)违反语句正确性。这里的想法是没有语句,甚至任何子表达式本身都不应该是一个错误。我接受术语"错误"松散地表示"可能是一个错误"。

    编写优秀代码的想法是,如果它出错了,那不是你的错。你的基本心态应该是一个偏执的懦夫。不写代码是实现这一目标的一种方法,但由于这很少符合要求,下一个最好的事情是确保无论你做什么,它都不是你的错。您可以系统地证明它不是您的错的唯一方法是,如果您的代码中没有单个部分是错误的根本原因。现在让我们再看一下代码:

    • new std::string[25]是一个错误,因为它会创建一个泄漏的动态分配对象。如果其他人,在其他地方,并且在每种情况下都记得清理,那么此代码只能有条件地成为非错误。

      首先,这需要将此表达式的值存储在某处。这种情况在你的情况下发生,但在更复杂的表达中,可能很难证明它会在所有情况下发生(未指明的评估顺序,我正在看着你)。

    • foo = new std::string[125];是一个错误,因为foo再次泄漏资源,除非星星对齐,并且有人记得,在每种情况下,在适当的时候,清理。

    到目前为止,编写此代码的正确方法是:

    std::unique_ptr<std::string[]> foo(std::make_unique<std::string[]>(25));
    

    请注意,此语句中的每个子表达式都不是程序错误的根本原因。这不是你的错。

    最后,对于(3),动态数组在C ++中是错误的,基本上不应该使用。有几个与动态阵列有关的标准缺陷(并不认为值得修复)。简单的说法是,在不知道数据大小的情况下,不能使用数组。您可能会说可以使用标记或逻辑删除值来动态标记数组的结尾,但这会使程序的正确性 - 依赖,而不是类型 - 依赖,因而不是静态可检查的(&#34;不安全&#34;的定义)。你无法静静断言它不是你的错。

    所以你最终还是要为数组大小维护一个单独的存储空间。猜猜看,你的实现无论如何都必须复制这些知识,因此当你说delete[]时它可以调用析构函数,这样就浪费了重复。相反,正确的方法不是使用动态数组,而是使用逐元素的对象构造来分离内存分配(并通过分配器使其可以自定义)。将所有这些(分配器,存储,元素计数)包装到一个方便的类中是C ++方式。

    因此,您的代码的最终版本是:

    std::vector<std::string> foo(25);
    

答案 1 :(得分:9)

您建议的代码不是例外安全的,替代方案:

std::vector<std::string> foo( 125 );
//  no delete necessary

是。当然,vector稍后会知道尺寸,并且可以 在调试模式下进行边界检查;它可以通过(通过引用 或者甚至通过价值)到一个能够使用的函数 它,没有任何额外的论点。数组新跟随 数组的C约定和C中的数组严重破坏。

据我所见,从不一个数组是新的情况 是合适的。

答案 2 :(得分:8)

  

我听说这种用法(不是这段代码,而是整个动态分配)在某些情况下可能不安全,并且只能用于RAII。为什么呢?

举个例子(与你的相似):

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    delete [] local_buffer;
    return x;
}

这是微不足道的。

即使你正确地编写了上面的代码,有人可能会在一年之后来,并在你的函数中添加一个条件,或十或二十:

int f()
{
    char *local_buffer = new char[125];
    get_network_data(local_buffer);
    int x = make_computation(local_buffer);
    if(x == 25)
    {
        delete[] local_buffer;   
        return 2;
    }
    if(x < 0)
    {
        delete[] local_buffer; // oops: duplicated code
        return -x;
    }
    if(x || 4)
    {
        return x/4; // oops: developer forgot to add the delete line
    }
    delete[] local_buffer; // triplicated code
    return x;
}

现在,确保代码没有内存泄漏更复杂:你有多个代码路径,每个代码路径都必须重复删除语句(我故意引入了内存泄漏,给你一个例子)。 / p>

仍然是一个简单的情况,只有一个资源(local_buffer),它(天真地)假设代码在分配和释放之间不会抛出任何异常。当你的函数分配~10个本地资源,可以抛出,并且有多个返回路径时,问题会导致无法维护的代码。

更重要的是,上面的进展(简单,简单的案例扩展到具有多个退出路径的更复杂的功能,扩展到多个资源等)是大多数项目开发中代码的自然进展。不使用RAII,为开发人员创建了一种自然的方式,在项目的整个生命周期中以一种降低质量的方式更新代码(这称为残酷,并且是一件非常糟糕的事情)。

TLDR:在C ++中使用原始指针进行内存管理是一种不好的做法(尽管实现了一个观察者角色,一个带有原始指针的实现,很好)。使用原始poiners的资源管理违反了SRPDRY原则。

答案 3 :(得分:2)

它有两个主要缺点 -

  1. new并不保证您分配的内存是使用0null进行初始化的。除非您初始化它们,否则它们将具有未定义的值。

  2. 其次,内存是动态分配的,这意味着它托管在heap而不是stack中。 heapstack之间的区别在于,当变量超出范围但是heap未自动清除且C ++不包含内置垃圾收集器时,堆栈将被清除,这意味着如果错过delete调用的任何方式,最终会导致内存泄漏。

答案 4 :(得分:2)

原始指针很难正确处理,例如WRT。复制对象。

使用经过良好测试的抽象(例如std::vector)会更简单,更安全。

简而言之,不要不必要地重新发明轮子 - 其他人已经创造了一些你在质量或价格上不太可能匹配的精湛轮子

答案 5 :(得分:1)

如果在不再需要时释放分配的内存,则会导致内存泄漏。没有说明泄漏内存会发生什么,但是当程序终止时,现代操作系统会收集它。内存泄漏可能非常危险,因为系统可能会耗尽内存。

答案 6 :(得分:1)

最后可以跳过delete。从最严格的意义上说,所显示的代码并非“错误”,但C ++在保留范围后立即为变量提供自动内存管理;在您的示例中,不需要使用指针。

答案 7 :(得分:0)

在try块中进行分配并且catch块应该释放所有已分配的内存到目前为止以及异常块外的正常退出,并且catch块不应该通过正常执行块来避免双重删除

答案 8 :(得分:-1)

JPL Coding standards。动态内存分配导致不可预测的执行。我已经看到了完美编码系统中动态内存分配的问题 - 随着时间的推移,就像硬盘一样存在内存碎片。从堆中分配内存块将花费更长时间,直到无法分配所请求的大小。在哪个时间点,您开始获取返回的NULL指针并且整个程序崩溃,因为很少有人测试内存不足的情况。重要的是要注意,通过本书,您可能有足够的可用内存,但其碎片是阻止分配的原因。这在.NET CLI中得到解决,使用“handle”instead of pointers,运行时可以使用标记和清除垃圾收集器进行垃圾收集,移动内存。在扫描期间,它压缩内存以防止碎片并更新句柄。而指针(内存地址)无法更新。但这是一个问题,因为垃圾收集不再具有确定性。尽管如此,.NET已经添加了一些机制来使其更具确定性。但是,如果您遵循JPL的建议(第2.5节),您不需要花哨的垃圾收集。您可以在初始化时动态分配所需的所有内容,然后重用已分配的内存,永不释放它,然后就没有碎片风险,您仍然可以使用确定性垃圾回收。

相关问题