超出范围访问数组不会出错,为什么?


176

我在C ++程序中分配值,如下所示:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

程序将打印34。这应该是不可能的。我正在使用g ++ 4.3.3

这是编译并运行命令

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

只有在分配时array[3000]=3000,它才会给我带来细分错误。

如果gcc不检查数组边界,我如何确定我的程序是否正确,因为以后可能会导致一些严重的问题?

我将上面的代码替换为

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

而且这个也不会产生错误。



15
该代码当然是错误的,但是会产生未定义的行为。未定义表示它可能完成或可能无法完成。无法保证会发生崩溃。
dmckee ---前主持人小猫,

4
您可以通过不使用原始数组来确保程序正确无误。除了嵌入式/ OS编程外,C ++程序员应改用容器类。请阅读此内容以了解用户容器的原因。parashift.com/c++-faq-lite/containers.html
jkeys

8
请记住,向量不必使用[]进行范围检查。使用.at()的作用与[]相同,但是会进行范围检查。
David Thornley,2009年

3
访问越界元素时,A vector 不会自动调整大小!只是UB!
帕维尔·米纳夫

Answers:


361

欢迎使用每个C / C ++程序员最好的朋友:Undefined Behavior

由于多种原因,语言标准中没有指定很多内容。这就是其中之一。

通常,每当遇到未定义的行为时,任何事情都可能发生。该应用程序可能会崩溃,可能冻结,可能弹出您的CD-ROM驱动器或使恶魔从您的鼻子中冒出来。它可能会格式化硬盘或将所有色情内容通过电子邮件发送给祖母。

即使您真的很倒霉,它似乎也可以正常工作。

该语言仅说明如果访问数组范围的元素会发生什么。如果您超出范围,将会发生什么,这是不确定的。它现在似乎可以在您的编译器上运行,但是它不是合法的C或C ++,并且不能保证它在下次运行该程序时仍然可以运行。或者说,它并没有被覆盖的基本数据即使是现在,你只是还没有遇到的问题,它将原因-但。

至于为什么没有边界检查,答案有几个方面:

  • 数组是C的剩余物。C数组与原始数组差不多。只是具有连续地址的一系列元素。没有边界检查,因为它只是公开原始内存。在C语言中几乎不可能实现强大的边界检查机制。
  • 在C ++中,可以在类类型上进行边界检查。但是,数组仍然是老式的C兼容数组。这不是一堂课。此外,C ++还建立在另一个使边界检查不理想的规则上。C ++的指导原则是“您不为不使用的商品付费”。如果您的代码是正确的,则不需要边界检查,也不必强迫您支付运行时边界检查的开销。
  • 因此,C ++提供了std::vector类模板,两者都允许。operator[]旨在提高效率。语言标准不要求执行边界检查(尽管也不禁止)。向量还具有at()成员函数,该成员函数可以确保执行边界检查。因此,在C ++中,如果使用向量,则可以兼得两全。您无需边界检查即可获得类似数组的性能,并且可以在需要时使用边界检查访问。

5
@Jaif:我们使用这个数组东西已经很久了,但是为什么仍然没有测试来检查这种简单的错误呢?
seg.server.fault,2009年

7
C ++设计原则是它不应该慢于等效的C代码,并且C不执行数组绑定检查。C设计原则基本上是针对系统编程的速度。数组绑定检查需要时间,因此无法完成。对于C ++中的大多数用法,无论如何都应该使用容器而不是数组,并且可以分别通过.at()或[]访问元素来选择绑定检查或不选择绑定检查。
KTC

4
@seg这样的支票要花些钱。如果您编写正确的代码,则您不想为此付出代价。话虽如此,我已经完全转换为std :: vector的at()方法,该方法已经过检查。使用它在我认为是“正确”的代码中暴露了很多错误。

10
我相信,当遇到某些类型的不确定行为时,旧版本的GCC实际上会启动Emacs并在其中模拟河内塔。就像我说的,任何事情都有可能发生。;)
杰夫

4
一切都已经说过了,因此仅需进行少量补充。与发布版本相比,在这些情况下,调试版本可能会非常宽容。由于调试二进制文件中包含调试信息,因此重要内容被覆盖的可能性较小。这有时就是为什么调试版本在发行版本崩溃时工作正常的原因。
丰富

31

使用g ++,您可以添加命令行选项: -fstack-protector-all

在您的示例中,结果如下:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

它并不能真正帮助您找到或解决问题,但是至少段错误会让您知道问题出在哪里


10
我刚刚找到了一个更好的选择:-fmudflap
Hi-Angel

12

g ++不会检查数组边界,您可能会用3,4覆盖某些内容,但没有什么真正重要的,如果尝试使用更高的数字,则会崩溃。

您只是覆盖堆栈中未使用的部分,您可以继续操作直到到达为堆栈分配的空间的尽头,最终崩溃

编辑:您无法解决这个问题,也许静态代码分析器可以揭示那些故障,但这太简单了,即使对于静态分析器,您也可能有未检测到的类似(但更复杂)的故障


6
如果从array [3]和array [4]的地址那里得到的信息没有什么真正重要的呢?
namezero 2013年

7

据我所知,这是未定义的行为。运行一个更大的程序,它将在运行过程中崩溃。边界检查不是原始数组(甚至std :: vector)的一部分。

使用带有std::vector::iterators的std :: vector 代替,因此您不必担心。

编辑:

只是为了好玩,运行以下命令,看看崩溃多久了:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

编辑2:

不要那样

编辑3:

好的,这是关于数组及其与指针的关系的快速课程:

使用数组索引时,实际上是在变相使用指针(称为“引用”),该指针会自动取消引用。这就是为什么array [1]代替*(array [1])而是自动返回该值的原因。

当您有一个指向数组的指针时,如下所示:

int array[5];
int *ptr = array;

然后,第二个声明中的“数组”实际上将衰减为指向第一个数组的指针。这是等效的行为:

int *ptr = &array[0];

当您尝试访问超出分配范围的内容时,您实际上只是在使用指向其他内存的指针(C ++不会抱怨)。以我上面的示例程序为例,这等效于:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

编译器不会抱怨,因为在编程时,您通常必须与其他程序(尤其是操作系统)进行通信。这是通过指针完成的。


3
我认为您在最后一个示例中忘记了增加“ ptr”。您不小心产生了一些定义明确的代码。
杰夫·莱克2009年

1
哈哈,明白为什么不应该使用原始数组吗?
jkeys

“这就是为什么array [1]代替*(array [1])而是自动返回该值的原因。” 您确定*(array [1])可以正常工作吗?我认为应该是*(array + 1)。ps:大声笑,就像向过去发送信息一样。但是,无论如何:
muyustan

5

暗示

如果你想有快速约束大小的数组与范围错误检查,请尝试使用升压::数组(也性病:: TR1 ::阵列<tr1/array>它将会成为新一代C ++规范标准集装箱)。它比std :: vector快得多。它像int array []一样在堆或类实例内部保留内存。
这是简单的示例代码:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

该程序将打印:

array.at(0) = 1
Something goes wrong: array<>: index out of range


3

C或C ++将不会检查数组访问的范围。

您正在堆栈上分配数组。索引数组via array[3]等效于* (array + 3),其中array是指向&array [0]的指针。这将导致不确定的行为。

有时在C语言中捕获此错误的一种方法是使用静态检查器,例如splint。如果您运行:

splint +bounds array.c

上,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

那么您将得到警告:

array.c:(在函数主函数中)array.c:5:9:可能越界存储:array [1]无法解析约束:要求0> = 1,需要满足先决条件:要求maxSet(array @ array .c:5:9)> = 1存储器写操作可能会写入分配的缓冲区之外的地址。


更正:它已经由操作系统或其他程序分配。他正在覆盖其他记忆。
jkeys

1
说“ C / C ++将不检查范围”并不是完全正确的-没有任何东西可以排除特定的兼容实现,无论是默认设置还是带有一些编译标志。只是他们都没有打扰。
帕维尔·米纳夫

3

通过Valgrind运行此程序,您可能会看到错误。

正如Falaina指出的那样,valgrind不会检测到许多堆栈损坏实例。我只是在valgrind下尝试了该示例,并且确实报告了零错误。但是,Valgrind可以帮助发现许多其他类型的内存问题,在这种情况下它并不是特别有用,除非您修改bulid以包括--stack-check选项。如果您以以下方式构建和运行示例

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind 报告错误。


2
实际上,Valgrind在确定堆栈上不正确的数组访问方面非常差劲。(理所当然的,最好的办法是将整个堆栈标记为有效的写入位置)
Falaina

@Falaina-很好,但是Valgrind至少可以检测到一些堆栈错误。
托德·斯托特

而且valgrind不会看到代码有什么问题,因为编译器足够聪明,可以优化数组,只输出文字3和4。这种优化发生在gcc检查数组边界之前,这就是为什么gcc会发出越界警告的原因没有显示。
Goswin von Brederlow

2

未定义的行为对您有利。无论您要破坏什么内存,显然都没有任何重要意义。请注意,C和C ++不会对数组进行边界检查,因此不会在编译或运行时捕获此类问题。


5
不,未定义的行为在完全崩溃时会“对您有利”。当它看起来可行时,那就是最糟糕的情况。
jalf

@JohnBode:那么最好按照Jalf的评论纠正措辞
Destructor

1

当您使用初始化数组时int array[2],会分配2个整数的空间。但标识符array只是指向该空间的开头。当您访问array[3]and时array[4],如果数组足够长,则编译器将简单地将该地址递增以指向这些值所在的位置。尝试访问类似的内容array[42]而不先对其进行初始化,最终您将获得该位置已经存在于内存中的任何值。

编辑:

有关指针/数组的更多信息:http : //home.netcom.com/~tjensen/ptr/pointers.htm


0

当您声明int array [2]时;您保留2个每个4字节的内存空间(32位程序)。如果您在代码中键入array [4],它仍然对应于一个有效的调用,但是只有在运行时,它才会引发未处理的异常。C ++使用手动内存管理。这实际上是用于黑客程序的安全漏洞

这可以帮助理解:

int *指针

somepointer [0] = somepointer [5];


0

据我了解,局部变量是在堆栈上分配的,因此超出自己堆栈的范围只能覆盖一些其他局部变量,除非您花费太多并超过了堆栈大小。由于您没有在函数中声明其他变量-它不会引起任何副作用。尝试在第一个变量/数组之后立即声明另一个变量/数组,看看会发生什么。


0

当您在C语言中编写“ array [index]”时,它会将其翻译为机器指令。

翻译是这样的:

  1. '获取数组的地址'
  2. '获取对象的大小是由数组组成的类型'
  3. “类型的大小乘以索引”
  4. '将结果添加到数组的地址'
  5. “阅读结果地址上的内容”

结果处理的东西可能是也可能不是数组的一部分。以机器指令的快速交换,您将失去计算机为您进行检查的安全网。如果您细致周到,那不是问题。如果您马虎或犯了个错误,您会被烫伤。有时它可能会生成导致异常的无效指令,有时则不会。


0

我经常看到的一种好方法,实际上我曾经使用过,uint THIS_IS_INFINITY = 82862863263;是在数组末尾注入一些NULL类型的元素(或创建的元素,如)。

然后在循环条件检查中,TYPE *pagesWords是某种指针数组:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

如果数组中填充了struct类型,则此解决方案无法解决。


0

正如现在在问题中提到的那样,使用std :: vector :: at将解决问题并在访问之前进行绑定检查。

如果在第一个代码中需要一个位于堆栈上的恒定大小的数组,请使用C ++ 11新容器std :: array; 作为向量,有std :: array :: at函数。实际上,该函数存在于所有具有其含义的标准容器中,即,在其中定义了operator [] :( deque,map,unordered_map)的情况除外,其中std :: bitset除外,其中将其称为std :: bitset: :测试。


0

libstdc ++是gcc的一部分,具有用于错误检查的特殊调试模式。它由编译器标志启用-D_GLIBCXX_DEBUG。除其他事项外,它确实std::vector以性能为代价进行边界检查。这是最新版本的gcc的在线演示

因此,实际上您可以使用libstdc ++调试模式进行边界检查,但是您仅应在测试时进行边界检查,因为与正常的libstdc ++模式相比,它具有显着的性能。


0

如果您稍稍更改程序:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(大写字母的更改-如果您要尝试使用小写字母,请将其小写。)

您将看到变量foo已被删除。您的代码会将值存储到不存在的array [3]和array [4]中,并能够正确检索它们,但实际使用的存储空间将来自foo

所以,你可以“蒙混过关”用超过原来的例子中,数组的边界,但在造成损害的其他地方的成本-造成的损害可能被证明是非常难以诊断。

至于为什么没有自动边界检查-正确编写的程序不需要它。一旦完成,就没有理由进行运行时边界检查,这样做只会减慢程序速度。最好在设计和编码过程中弄清所有这些。

C ++基于C,C被设计为尽可能接近汇编语言。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.