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

时间:2009-08-06 16:12:15

标签: 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;
}

该程序会打印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;

并且这个也没有产生错误。

18 个答案:

答案 0 :(得分:308)

欢迎来到每位C / C ++程序员最好的朋友:未定义的行为

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

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

如果你真的不走运,甚至可能出现才能正常工作。

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

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

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

答案 1 :(得分:28)

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

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

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

它并没有真正帮助您找到或解决问题,但至少segfault会让您知道某些是错误的。

答案 2 :(得分:12)

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

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

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

答案 3 :(得分: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
}

Edit2:

不要跑那个。

EDIT3:

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

当您使用数组索引时,您实际上正在使用伪装的指针(称为“引用”),该指针会自动解除引用。这就是为什么而不是*(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
}

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

答案 4 :(得分:5)

提示

如果您希望快速约束大小数组具有范围错误检查,请尝试使用boost::array,(<tr1/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; } 它将是下一个C ++规范中的标准容器)。它比std :: vector快得多。它在堆或类实例内部保留内存,就像int array []一样 这是一个简单的示例代码:

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

该程序将打印:

{{1}}

答案 5 :(得分:3)

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

您正在堆栈上分配数组。通过array[3]索引数组相当于* (array + 3),其中array是指向&amp; array [0]的指针。这将导致未定义的行为。

在C中捕捉有时的一种方法是使用静态检查器,例如夹板。如果您运行:

splint +bounds array.c

上,

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

    array[1] = 1;

    return 0;
}

然后你会收到警告:

  

array.c :(在函数main中)   array.c:5:9:可能是越界   商店:       阵列[1]       无法解决约束:       要求0> = 1        需要满足前提条件:       需要maxSet(array @ array.c:5:9)&gt; = 1内存写入可能   写到超出的地址   分配缓冲区。

答案 6 :(得分:3)

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

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

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

valgrind 报告错误。

答案 7 :(得分:3)

你当然会覆盖你的堆栈,但程序很简单,不会注意到这种效果。

答案 8 :(得分:2)

对您有利的未定义行为。无论你看起来什么记忆显然都没有重要的东西。请注意,C和C ++不对数组进行边界检查,因此不会在编译或运行时捕获这样的东西。

答案 9 :(得分:1)

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

编辑:

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

答案 10 :(得分:0)

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

这有助于理解:

int * somepointer;

somepointer [0] = somepointer [5];

答案 11 :(得分:0)

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

答案 12 :(得分:0)

当您在C中编写'array [index]'时,它会将其转换为机器指令。

翻译类似于:

  1. '获取数组的地址'
  2. '获取对象类型的大小数组由'
  3. 组成
  4. '按索引'
  5. 乘以类型的大小
  6. '将结果添加到数组'
  7. 的地址
  8. '读取结果地址'
  9. 的内容

    结果解决了可能或可能不是数组的一部分。为了换取机器指令的超快速度,您将丢失计算机检查的安全网。如果你一丝不苟而小心,那不是问题。如果你草率或犯了错误就会被烧焦。有时它可能会生成导致异常的无效指令,有时不会。

答案 13 :(得分:0)

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

然后在循环条件检查中,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类型,则此解决方案不会发出声明。

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

libstdc ++是gcc的一部分,它有一个特殊的debug mode用于错误检查。它由编译器标志-D_GLIBCXX_DEBUG启用。除此之外,它确实以性能为代价检查std::vector。以下是online demo最新版本的gcc。

所以实际上你可以用libstdc ++调试模式进行边界检查,但是你应该只在测试时才这样做,因为与普通的libstdc ++模式相比,它的性能要显着。

答案 16 :(得分: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 已被删除。您的代码将值存储到不存在的数组[3]和数组[4]中,并且能够正确检索它们,但实际使用的存储将来自 foo

所以你可以&#34;离开&#34;在原始示例中超出阵列的范围,但代价是在其他地方造成损害 - 可能被证明非常难以诊断的损坏。

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

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

答案 17 :(得分:0)

行为可能取决于您的系统。通常,您会有越界的余量,有时值为 0 或垃圾值。有关详细信息,您可以查看操作系统中使用的内存分配机制。最重要的是,如果您使用像 c/c++ 这样的编程语言,当您使用某些容器(如数组)时,它不会检查边界。因此,您将遇到“未定义事件”,因为您不知道操作系统在表面之下做了什么。但是就像编程语言Java一样,它会检查边界。如果你跨出界限,你会得到一个例外。