为什么内置数组不安全?

时间:2015-10-24 15:18:38

标签: c++ arrays

C++ Primer, 5th edition(ISBN 0-321-71411-3 / 978-0-321-71411-4)的书 Stanley B. Lippman 提到:

  

[std::]array是一种更安全,更易于使用的内置数组替代方案。

内置数组有什么问题?

7 个答案:

答案 0 :(得分:29)

  1. 内置数组是一个连续的字节块,通常在堆栈上。你真的没有办法保存关于数组,它的边界或状态的有用信息。 std::array保留此信息。

  2. 内置数组在从/向函数传递时会被衰减为指针。这可能会导致:

    • 传递内置数组时,传递原始指针。指针不保留有关数组大小的任何信息。您将不得不传递数组的大小,从而uglify代码。 std::array可以作为参考,复制或移动传递。

    • 无法返回内置数组,如果在该函数范围内声明了数组,最终将返回指向局部变量的指针。 std::array可以安全返回,因为它是一个对象,它的生命周期是自动管理的。

  3. 你无法在内置数组上做有用的事情,比如分配,移动或复制它们。您将结束为每个内置数组编写自定义函数(可能使用模板)。可以分配std::array

  4. 通过访问数组边界之外的元素,您将触发未定义的行为。如果检查失败,std::array::at将执行边界检查并抛出常规C ++异常。

  5. 更好的可读性:内置数组涉及指针算术。 std::array实现了有用的功能,例如frontbackbeginend以避免这种情况。

  6. 假设我想对内置数组进行排序,代码可能如下:

    int arr[7] = {/*...*/};
    std::sort(arr, arr+7);
    

    这不是有史以来最强大的代码。通过将7更改为其他数字,代码会中断。

    使用std::array

    std::array<int,7> arr{/*...*/};
    std::sort(arr.begin(), arr.end());
    

    代码更加强大和灵活。

    为了清楚起见,内置数组有时会更容易。例如,许多Windows以及UNIX API函数/系统调用都需要一些(小)缓冲区来填充数据。我不会使用std::array的开销而不是我可能正在使用的简单char[MAX_PATH]

答案 1 :(得分:25)

很难判断作者的意思,但我猜他们指的是关于原生数组的以下事实:

  • 他们是原始的
    没有.at成员函数可以用于带边界检查的元素访问,尽管我反击你通常不想要。要么您正在访问您知道存在的元素,要么您正在迭代(您可以使用std::array和本机数组进行同样的处理);如果你不知道元素是否存在,那么边界检查访问器已经是一种很难确定的方法,因为它使用了错误的代码流工具,并且会带来很大的性能损失。

  • 他们可能会混淆
    新手倾向于忘记数组名称衰减,将数组传递给函数&#34;按值&#34;然后对随后的指针执行sizeof;这通常不是'#34;不安全&#34;,但它会造成错误。

  • 无法分配
    同样,本质上不安全,但它会导致愚蠢的人们用多级指针和大量动态分配编写愚蠢的代码,然后失去记忆并犯下各种UB罪行。

假设作者推荐std::array,那将是因为它&#34;修复&#34;所有上述内容,默认情况下通常会有更好的代码。

但本机阵列本质上是不安全的&#34;不安全&#34;通过比较?不,我不会这么说。

答案 2 :(得分:6)

std::array如何比内置数组更安全,更易于使用?

很容易陷入内置数组,特别是对于那些不是C ++专家和程序员的程序员,他们有时会犯错误。这会导致许多错误和安全漏洞。

  • 使用std::array a1,您可以访问边界检查a.at(i)或没有边界检查a[i]的元素。使用内置数组,始终您有责任努力避免越界访问。否则,代码可能会粉碎一些长时间未被注意的内存,并且变得非常难以调试。 即使只是在数组边界外读取也可以利用安全漏洞,例如泄漏私有加密密钥的Heartbleed漏洞。
  • C ++教程可能会假装T的数组和T指针是同一个东西,然后告诉你各种异常,它们不是同一个东西。例如。结构中嵌入了一个T数组,而结构中的指针T是指向内存的指针,您最好分配它。或者考虑一组数组(例如栅格图像)。是否将指针自动递增到下一个像素或下一行?或者考虑一个对象数组,其中一个对象指针强制它的基类指针。 这一切都很复杂,编译器也没有发现错误。
  • 使用std::array a1,您可以获取其大小a1.size(),将其内容与另一个std :: array a1 == a2进行比较,并使用其他标准容器方法,如a1.swap(a2)。使用内置数组,这些操作需要更多的编程工作,并且更容易搞乱。例如。如果int b1[] = {10, 20, 30};获得其大小而没有硬编码3,则必须执行sizeof(b1) / sizeof(b1[0])。要比较它的内容,你必须遍历这些元素。
  • 您可以通过引用f(&a1)或值f(a1)将std :: array传递给函数[即通过复制]。传递内置数组只能通过引用进行处理,并使用指向第一个元素的指针将其混淆。那不是一回事。编译器不传递数组大小。
  • 您可以按值return a1从函数返回std :: array。返回一个内置数组return b1会返回一个悬空指针,该指针已被破坏。
  • 你可以用通常的方式a1 = a2复制一个std :: array,即使它包含带有构造函数的对象。如果你尝试使用内置数组b1 = b2,它只会复制数组指针(或者无法编译,具体取决于b2的声明方式)。您可以使用memcpy(b1, b2, sizeof(b1) / sizeof(b1[0]))来解决这个问题,但如果数组具有不同的大小或者它们包含带有构造函数的元素,则会破坏它。
  • 您可以轻松更改使用std :: array的代码,以使用另一个容器,如std :: vector或std :: map。

请参阅C ++常见问题解答Why should I use container classes rather than simple arrays?以了解详情,例如:包含具有析构函数(如std::string)或继承的C ++对象的内置数组的危险。

不要害怕性能

每次获取或存储数组元素时,

边界检查访问a1.at(i)需要更多指令。在一些内部循环代码中,这些代码会阻塞大型数组(例如,您在每个视频帧上调用的图像处理例程),这个成本可能会加起来很重要。在极少数情况下,使用未经检查的访问a[i] 是有意义的,请仔细确保循环代码处理边界。

在大多数代码中,您要么将图像处理代码卸载到GPU,要么边界检查成本只占总运行时间的一小部分,或者整体运行时间不是问题。同时,阵列访问错误的风险很高,从调试它的时间开始。

答案 3 :(得分:5)

内置数组的唯一好处是更简洁的声明语法。但是std::array的功能性好处使其脱离水中。 我还要补充一点,这并不重要。如果你必须支持较旧的编译器,那么你当然没有选择,因为std::array仅适用于C ++ 11。否则,你可以使用你喜欢的任何一个,但除非你只是简单地使用数组,你应该更喜欢std :: array只是为了让事情与其他STL容器保持一致(例如,如果你以后决定使大小动态,那该怎么办? ,并使用std :: vector,然后你会很高兴你使用std::array因为所有你必须改变的可能是数组声明本身,其余的将是相同的,特别是如果你使用自动和C ++ 11的其他类型推断功能。

  

std::array是一个模板类,它封装了一个存储在对象本身内的静态大小的数组,这意味着,如果在堆栈上实例化该类,则数组本身将位于堆栈中。它的大小必须在编译时知道(它作为模板参数传递),并且它不能增长或缩小。

数组用于存储一系列对象 查看教程:http://www.cplusplus.com/doc/tutorial/arrays/

std :: vector做的相同,但它比内置数组更好(例如:一般来说,vector的效率差别不大于内置数据 在通过operator []访问元素时在数组中:http://www.cplusplus.com/reference/stl/vector/

  

内置数组是错误的主要来源 - 尤其是当它们用于构建多维数组时。   对于新手来说,他们也是混乱的主要来源。尽可能使用vector,list,valarray,string等。   STL容器与内置数组没有相同的问题

因此,C ++中没有理由坚持使用内置数组。内置数组采用C ++,主要是为了向后兼容C。

如果OP真的想要一个数组,C ++ 11为内置数组std :: array提供了一个包装器。使用std :: array非常类似于使用内置数组对其运行时性能没有影响,具有更多功能。

  

与标准库中的其他容器不同,交换两个数组容器是一种线性操作,涉及单独交换范围中的所有元素,这通常是一种效率相当低的操作。另一方面,这允许两个容器中的元素的迭代器保持其原始容器关联。   数组容器的另一个独特特性是它们可以被视为元组对象:头部重载get函数以访问数组的元素,就像它是一个元组一样,以及专门的tuple_size和tuple_element类型。

无论如何,内置数组都是通过引用传递的方式。这样做的原因是当你将一个数组作为参数传递给一个函数时,会传递指向它的第一个元素的指针。

当你说void f(T[] array)编译器会把它变成void f(T* array)时 说到字符串。 C风格的字符串(即空终止字符序列)都是通过引用传递的方式,因为它们也是'char'数组。

默认情况下,STL字符串不会通过引用传递。它们就像正常变量一样。 没有用于通过引用传递参数的预定义规则。即使数组总是自动通过引用传递。

vector<vector<double>> G1=connectivity( current_combination,M,q2+1,P );
vector<vector<double>> G2=connectivity( circshift_1_dexia(current_combination),M,q1+1,P );

这也可能是复制向量,因为连接按值返回向量。在某些情况下,编译器会对此进行优化。但要确保避免这种情况,您可以将向量作为非const引用传递给连接而不是返回它们。 maxweight的返回值是一个由值返回的三维向量(可以复制它)。 向量仅在最后插入或擦除时有效,如果要推送大量值,最好调用reserve()。如果您不需要随机访问,您可以使用列表重新编写它;使用list会丢失下标运算符,但仍然可以进行线性传递,并将迭代器保存到元素而不是下标。

对于某些编译器,使用预增量而不是后增量可能更快。除非你真的需要使用后增量,否则首选++ i到i ++。它们不一样。

无论如何,如果你没有编译优化,矢量会非常慢。通过优化,它接近内置数组。内置数组也可能非常慢,没有优化,但没有矢量那么糟糕。

答案 4 :(得分:2)

std::array具有at成员函数,这是安全的。它还有beginendsize,您可以使用它来使代码更安全。

原始数组没有。 (特别是,当原始数组在作为参数传递时被衰减为指针-eg时,您将丢失任何大小信息,这些信息保存在std::array类型中,因为它是一个大小为参数的模板)

优秀的C ++ 11编译器将像原始数组一样有效地处理std::array(或对它们的引用)。

答案 5 :(得分:2)

内置数组本质上不安全 - 如果使用正确。但是错误地使用内置数组比使用std::array等替代方法更容易,这些替代方法通常会提供更好的调试功能,以帮助您检测错误使用时间。

答案 6 :(得分:2)

内置数组非常精细。即使是有经验的程序员,也有很多方面表现得出乎意料。

std::array<T, N>确实是T[N]的包装器,但许多已经提到的方面基本上是免费的,这是非常理想的,也是你想要的。

这些是我还没读过的:

  1. 大小: N应该是一个常量表达式,对于两者都不能是变量。 但是,内置数组,还有VLA(可变长度数组)也允许这样做。

    官方说来,只有C99支持它们。尽管如此,许多编译器仍允许在以前版本的C和C ++中使用extensions。因此,你可能有

    int n; std::cin >> n;
    int array[n]; // ill-formed but often accepted
    

    compiles fine。如果您使用std::array,则无法使用,因为N是必需的并且已检查是实际的常量表达式!

  2. 对阵列的期望:一个常见的缺陷是,当阵列不再是一个阵列时,阵列的大小随阵列本身一起携带误导C语法,如:

    void foo(int array[])
    {
        // iterate over the elements
        for (int i = 0; i < sizeof(array); ++i)
             array[i] = 0;
    }
    

    但这是错误的,因为array已经衰减到指针,指针没有关于指向区域大小的信息。如果array具有少于sizeof(int*)个元素(通常为8个),那么误解会触发未定义的行为,除了在逻辑上是错误的。

    1. 疯狂使用:更进一步,有一些奇怪的阵列有:

      • Whether you have array[i] or i[array],没有区别。对于array,情况并非如此,因为调用重载运算符实际上是函数调用,参数的顺序很重要。

      • 零大小的数组:N大于零但仍然允许它作为扩展名,并且像以前一样,除非需要更多的迂腐,否则通常不会发出警告。更多信息herearray有不同的semantics

          

        零长度数组(N == 0)有一种特殊情况。在那里面   case,array.begin() == array.end(),这是一个独特的值。该   在零大小的数组上调用front()back()的效果是   未定义。