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

Sil*_*pur 30 c++ dynamic-allocation

如下代码:

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

我听说在某些情况下这样的使用(不是这个代码,但是整个动态分配)可能是不安全的,并且只能用于RAII.为什么?

Ker*_* SB 47

我看到你的代码有三个主要问题:

  1. 使用裸体,拥有指针.

  2. 使用裸体new.

  3. 使用动态数组.

由于其自身原因,每个都是不合需要 我将尝试依次解释每一个.

(1)违反我喜欢称为子表达式的正确性,(2)违反陈述正确性.这里的想法是,没有语句,甚至任何子表达式本身都不应该是错误.我松散地用"错误"这个词来表示"可能是一个错误".

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

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

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

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

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

std::unique_ptr<std::string[]> foo(std::make_unique<std::string[]>(25));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

std::vector<std::string> foo(25);
Run Code Online (Sandbox Code Playgroud)

  • 注意:有一个提议的`std :: dynarray`类(被搁置或被拒绝).有些人认为`std :: vector`存储了一个额外的容量成员,并且具有在许多情况下不需要的调整大小功能,并且应该存在修剪版本(没有调整大小). (3认同)
  • 请注意,`std :: make_unique`还不是C++标准的一部分(从C++ 11开始). (2认同)
  • @Alf你能指出一个有效的数组新用途吗?(我认为这就是他所说的"动态数组".)我已经写了大约25年的C++,包括沿着字符串和向量实现预标准容器,我从来没有找到过. (2认同)

Jam*_*nze 9

您提出的代码不是异常安全的,替代方案:

std::vector<std::string> foo( 125 );
//  no delete necessary
Run Code Online (Sandbox Code Playgroud)

是.当然,vector稍后会知道大小,并且可以在调试模式下进行边界检查; 它可以(通过引用或甚至通过值)传递给函数,然后函数可以使用它,而无需任何其他参数.Array new遵循数组的C约定,C中的数组严重破坏.

据我所知,从来没有一个新的数组是合适的.


utn*_*tim 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;
}
Run Code Online (Sandbox Code Playgroud)

这是微不足道的.

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

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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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