访问数组越界没有错误,为什么?
我正在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; }
该程序打印3
和4
。 这应该是不可能的。 我正在使用g ++ 4.3.3
这里是编译和运行命令
$ g++ -W -Wall errorRange.cpp -o errorRange $ ./errorRange 3 4
只有在分配array[3000]=3000
时才会给我一个分段错误。
如果gcc不检查数组边界,我怎么能确定我的程序是否正确,因为它可能会导致一些严重的问题呢?
我用上面的代码replace了
vector<int> vint(2); vint[0] = 0; vint[1] = 1; vint[2] = 2; vint[5] = 5; cout << vint[2] << endl; cout << vint[5] << endl;
而这一个也没有产生任何错误。
欢迎来到每一位C / C ++程序员最好的朋友: 未定义的行为 。
由于各种原因,语言标准没有规定很多。 这是其中之一。
一般来说,无论何时遇到未定义的行为, 都可能发生任何事情。 应用程序可能会崩溃,可能会冻结,它可能会popup您的CD-ROM驱动器或让恶魔从你的鼻子里出来。 它可以格式化您的硬盘或将您的色情邮件发送给您的祖母。
甚至,如果你真的不走运, 似乎工作正常。
该语言简单地说,如果访问数组边界内的元素,会发生什么情况。 这是不明确的,如果你超出界限会发生什么。 今天在你的编译器上似乎可以工作,但它不是合法的C或C ++,并且不能保证它在你下一次运行程序的时候仍然有效。 或者,即使现在它也没有覆盖基本的数据,而且你还没有遇到这样的问题,而且还会造成这种问题。
至于为什么没有边界检查,有几个方面的答案:
- 一个数组是C的剩余.C数组大约是原始的,你可以得到。 只是一系列具有连续地址的元素。 没有边界检查,因为它只是暴露原始内存。 在C中实现强大的边界检查机制几乎是不可能的
- 在C ++中,可以在类types上进行边界检查。 但是数组仍然是普通的旧C兼容的。 这不是一个阶级。 此外,C ++也build立在另一个使边界检查不理想的规则上。 C ++的指导原则是“你不支付你不使用的东西”。 如果你的代码是正确的,那么你不需要边界检查,而且也不应该被迫支付运行时边界检查的开销。
- 所以C ++提供了
std::vector
类模板,它允许这两个模板。operator[]
被devise为高效的。 语言标准并不要求它执行边界检查(尽pipe它也不禁止)。 一个向量也有at()
成员函数, 保证执行边界检查。 所以在C ++中,如果你使用一个向量,你可以得到两全其美的好处。 您可以在没有边界检查的情况下获得类似数组的性能, 并且您可以在需要时使用边界检查访问。
使用g ++,你可以添加命令行选项: -fstack-protector-all
。
就你的例子而言,结果如下:
> g++ -ot -fstack-protector-all t.cc > ./t 3 4 /bin/bash: line 1: 15450 Segmentation fault ./t
这并不能真正帮助你find或解决问题,但至lesssegfault会让你知道有什么地方是错的。
g ++不检查数组的边界,你可能会用3,4覆盖的东西,但没有什么是非常重要的,如果你尝试更高的数字,你会得到一个崩溃。
你只是覆盖没有使用的堆栈的一部分,你可以继续下去,直到你到达堆栈的分配空间的末尾,并最终崩溃
编辑:你没有办法处理,也许静态代码分析仪可以揭示这些失败,但是这太简单了,即使对于静态分析仪,您可能也没有发现类似(但更复杂)的失败
就我所知,这是不确定的行为。 用这个运行一个更大的程序,它会在某个地方崩溃。 边界检查不是原始数组的一部分(甚至是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:
好的,这里是关于数组及其与指针关系的一个简短课程:
当你使用数组索引的时候,你实际上使用了一个伪装的指针(叫做“引用”),这个指针会被自动解引用。 这就是为什么而不是*(数组[1]),数组[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 }
编译器不会抱怨,因为在编程中,你经常不得不与其他程序,尤其是操作系统进行通信。 这是用指针完成的。
暗示
如果你想要使用范围错误检查的快速约束大小的数组,请尝试使用boost :: array (也是<tr1/array>
std :: tr1 :: array ,它将成为下一个C ++规范中的标准容器)。 它比std :: vector快得多。 它保留堆或内部类实例的内存,就像int数组[]一样。
这是简单的示例代码:
#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
你当然会覆盖你的堆栈,但是这个程序很简单,这个效果不会被人注意到。
C或C ++将不会检查数组访问的边界。
您正在堆栈上分配数组。 通过array[3]
索引数组相当于* (array + 3)
,其中数组是指向数组[0]的指针。 这将导致未定义的行为。
在C中有时候需要用到一个静态检查器,比如夹板 。 如果你运行:
splint +bounds array.c
上,
int main(void) { int array[1]; array[1] = 1; return 0; }
那么你会得到警告:
array.c:(在main函数中)array.c:5:9:可能超出限制store:array [1]无法解决约束:需要0> = 1来满足前提条件:需要maxSet(array @ array .c:5:9)> = 1内存写入可能会写入超出分配缓冲区的地址。
未定义的行为对你有利。 无论你打破了什么记忆显然没有什么重要的东西。 请注意,C和C ++不会对数组进行边界检查,所以像这样的东西不会在编译或运行时被捕获。
通过Valgrind运行,你可能会看到一个错误。
正如Falaina所指出的那样,valgrind没有发现很多堆栈损坏的情况。 我刚刚尝试valgrind下的示例,它确实报告零错误。 然而,Valgrind可以帮助你发现许多其他types的内存问题,除非你修改你的bulid来包含–stack-check选项,否则这种方法并不是特别有用。 如果您构build并运行样本
g++ --stack-check -W -Wall errorRange.cpp -o errorRange valgrind ./errorRange
valgrind 会报告一个错误。
当你用int array[2]
初始化数组时,2个整数的空间被分配; 但标识符array
只是指向该空间的开始。 当你访问array[3]
和array[4]
,如果数组足够长,编译器只需递增该地址以指向这些值的位置; 尝试访问像array[42]
没有初始化它的东西,你会最终得到发生的任何值已经在内存中的位置。
编辑:
有关指针/数组的更多信息: http : //home.netcom.com/~tjensen/ptr/pointers.htm
当你声明int数组[2]; 您预留了2个每个4字节的存储空间(32位程序)。 如果在代码中键入数组[4],它仍然对应于一个有效的调用,但是只有在运行时才会抛出未处理的exception。 C ++使用手动内存pipe理。 这实际上是一个用于黑客程序的安全缺陷
这可以帮助理解:
int * somepointer;
somepointer [0] = somepointer [5];
据我所知,局部variables是分配在堆栈上的,所以在你自己的栈上超出界限只能覆盖其他局部variables,除非你太多,超出你的栈大小。 由于您的函数中没有声明其他variables,因此不会产生任何副作用。 尝试在第一个variables/数组之后声明另一个variables/数组,看看会发生什么。
当你在C中写入'array [index]'时,它将它转换为机器指令。
翻译是这样的:
- '获取数组的地址'
- '获取对象数组的types的大小是由'
- “通过索引乘以types的大小”
- '将结果添加到数组的地址'
- “看看结果地址是什么”
结果解决了可能或不可能成为数组一部分的东西。 为了换取机器指令的速度,你将失去计算机的安全networking检查你的东西。 如果你一丝不苟,小心不是问题。 如果你马虎或犯了一个错误,你会被烧毁。 有时可能会生成导致exception的无效指令,有时不会。
我经常看到的一个很好的方法是我在数组的末尾注入了一个NULLtypes的元素(或者是一个创build的元素,比如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
types,则此解决scheme不会生成字词。
正如现在提到的使用std :: vector :: at的问题将解决问题,并在访问之前进行绑定检查。
如果你需要一个常量大小的数组作为你的第一个代码使用C ++ 11中的新容器std :: array; 作为向量有std :: array :: at函数。 实际上,函数存在于所有标准容器中,它们有一个含义,即operator []被定义为:( deque,map,unordered_map),std :: bitset除外,它被称为std :: bitset: :testing。
作为gcc一部分的libstdc ++具有特殊的错误检查debugging模式 。 它由编译器标志-D_GLIBCXX_DEBUG
启用。 除了其他的东西,它会以性能为代价来检查std::vector
。 这里是最近版本的gcc 在线演示 。
所以实际上,你可以使用libstdc ++debugging模式进行边界检查,但是只有在testing时才应该这样做,因为与普通的libstdc ++模式相比,它的性能是显着的。
如果你稍微改变你的程序:
#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; }
(首都的变化 – 如果你要试试这个,请把它们写成小写)
你会看到variablesfoo已经被删除了。 你的代码将存储值到不存在的数组[3]和数组[4],并能够正确地检索它们,但实际使用的存储将从foo 。
所以你可以在原来的例子中超越arrays的范围,但是在其他地方造成损害的代价 – 可能被certificate是非常难以诊断的损害。
至于为什么没有自动边界检查 – 一个正确的书面程序不需要它。 一旦完成,没有理由进行运行时间限制检查,这样做只会减慢程序。 最好在devise和编码时弄清楚这一切。
C ++基于C语言,它被devise成尽可能接近汇编语言。