访问数组越界没有错误,为什么?

seg*_*ult 157 c++ arrays

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

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

这是编译和运行命令

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4
Run Code Online (Sandbox Code Playgroud)

只有在分配时才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;
Run Code Online (Sandbox Code Playgroud)

而且这个也没有产生任何错误.

jal*_*alf 327

欢迎来到每个C/C++程序员最好的朋友:Undefined Behavior.

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

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

它甚至可以,如果你真的不走运,似乎才能正常工作.

该语言简单地说明了如果访问数组边界的元素会发生什么.如果你走出界限会发生什么事情.它现在似乎可以在您的编译器上运行,但它不是合法的C或C++,并且无法保证它在下次运行程序时仍然可用.或者说,它不会被覆盖的基本数据即使是现在,你只是还没有遇到的问题,它会导致-但.

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

  • 数组是来自C的剩余数据.C数组与您可以得到的一样简单.只是一系列具有连续地址的元素.没有边界检查,因为它只是暴露原始内存.在C中实现强大的边界检查机制几乎是不可能的.
  • 在C++中,可以在类类型上进行边界检查.但阵列仍然是普通的C兼容阵列.这不是一个阶级.此外,C++也建立在另一个规则上,这使得边界检查非理想.C++指导原则是"你不为你不使用的东西买单".如果你的代码是正确的,你不需要边界检查,你不应该被迫支付运行时边界检查的开销.
  • 所以C++提供了std::vector类模板,允许两者.operator[]旨在提高效率.语言标准不要求它执行边界检查(尽管它也不禁止它).向量还具有保证执行边界检查的at()成员函数.因此,在C++中,如果使用向量,则可以获得两全其美的效果.您可以在没有边界检查的情况下获得类似数组的性能,并且您可以在需要时使用边界检查访问.

  • 我相信旧版本的GCC实际上是在它遇到某些类型的未定义行为时启动了Emacs和一个河内塔的模拟.就像我说的,*任何事情都可能发生.;) (8认同)
  • C++设计原则是它不应该比等效的C代码慢,并且C不进行数组绑定检查.C设计原则基本上是速度,因为它的目标是系统编程.数组绑定检查需要时间,因此没有完成.对于C++中的大多数用途,您应该使用容器而不是数组,并且您可以通过分别通过.at()或[]访问元素来选择绑定检查或无绑定检查. (6认同)
  • @Jaif:我们一直在使用这个数组的东西,但为什么还没有测试来检查这么简单的错误? (4认同)
  • @seg这样的支票花费了一些东西.如果你写了正确的代码,你不想支付这个价格.话虽如此,我已经成为一个完整的转换为std :: vector的at()方法,并进行了检查.使用它在我认为"正确"的代码中出现了很多错误. (4认同)
  • 已经说过了所有的事情,所以这只是一个小小的附录.与发布版本相比,在这些情况下,调试版本可能非常宽容.由于调试二进制文件中包含调试信息,因此覆盖重要内容的可能性较小.这有时候为什么调试版本在发布版本崩溃时似乎工作正常. (4认同)
  • @Hooked:实际上,C++非常清楚什么是错误.越界访问是明显的错误.C++标准只是没有指定编写错误代码的*后果*. (3认同)
  • 因为 C++ 让程序员决定什么是错误。我不知道其他语言如何进行边界检查,但如果是在运行时,就会产生相关的开销。C++ 常用于需要速度的应用程序、游戏和操作系统开发。 (2认同)
  • @camino 编译器供应商完全可以自由地为 UB 的特定情况定义特定的行为。许多 *do* 在调试版本中检查了“[]”,这是*完全兼容*的。标准不必关心“调试”和“发布”配置的存在 (2认同)

Ric*_*den 28

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

在您的示例中,它产生以下结果:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t
Run Code Online (Sandbox Code Playgroud)

它并没有真正帮助你找到或者解决问题,但至少段错误会让你知道什么是错的.

  • 我发现了更好的选择:[ - fmudflap](https://gcc.gnu.org/wiki/Mudflap_Pointer_Debugging) (8认同)
  • @Hi-Angel:现代等效项是“-fsanitize=address”,它在编译时(如果优化)和运行时捕获此错误。 (3认同)
  • @NateEldredge +1,现在我什至使用`-fsanitize=undefined,address`。但值得注意的是,[当清理程序未检测到越界访问时,std 库存在罕见的极端情况](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=91878)。因此,我建议另外使用“-D_GLIBCXX_DEBUG”选项,这会增加更多检查。 (2认同)
  • 谢谢你嗨天使。当“-fmudflap”和“-fsanitize=address”对我不起作用时,“-fsanitize=undefined,address”不仅发现了一个没有返回值的函数,它还发现了正在发生的数组赋值的界限。 (2认同)

Ark*_*nez 12

g ++没有检查数组边界,你可能用3,4覆盖了一些东西,但没有什么真正重要的,如果你尝试使用更高的数字,你就会崩溃.

您只是覆盖了未使用的堆栈部分,您可以继续直到到达堆栈的已分配空间的末尾并最终崩溃

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

  • 如果从数组[3]和数组[4]的地址得到,那么"没有什么真的重要"? (5认同)

jke*_*eys 7

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

使用std :: vector std::vector::iterator代替,所以你不必担心它.

编辑:

只是为了好玩,运行它,看看你崩溃多久:

int main()
{
   int array[1];

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

   return 0; //will be lucky to ever reach this
}
Run Code Online (Sandbox Code Playgroud)

EDIT2:

不要跑那个.

EDIT3:

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

当您使用数组索引时,您实际上正在使用伪装的指针(称为"引用"),该指针会自动解除引用.这就是为什么而不是*(array [1]),array [1]会自动返回该值的值.

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

int array[5];
int *ptr = array;
Run Code Online (Sandbox Code Playgroud)

然后第二个声明中的"数组"实际上已经衰减为指向第一个数组的指针.这与此相同:

int *ptr = &array[0];
Run Code Online (Sandbox Code Playgroud)

当你尝试访问超出你分配的内容时,你实际上只是使用指向其他内存的指针(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
}
Run Code Online (Sandbox Code Playgroud)

编译器不会抱怨,因为在编程中,您经常需要与其他程序进行通信,尤其是操作系统.这是用指针完成的.

  • 我想你忘了在你上一个例子中增加"ptr".你不小心产生了一些明确定义的代码. (3认同)

Tod*_*out 5

运行这个通过Valgrind命令,您可能会看到错误。

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

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange
Run Code Online (Sandbox Code Playgroud)

valgrind报错。

  • 实际上,Valgrind 在确定堆栈上不正确的数组访问方面相当糟糕。(理所当然,它能做的最好的事情就是将整个堆栈标记为有效的写入位置) (3认同)

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

该程序将打印:

array.at(0) = 1
Something goes wrong: array<>: index out of range
Run Code Online (Sandbox Code Playgroud)