Доступ к массиву вне границ не дает ошибок, почему?
Я присваиваю значения в программе на 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 не проверяет границы массивов, как я могу быть уверен, что моя программа верна, так как это может привести к серьезным проблемам позже?
Я заменил вышеуказанный код на
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 ответов
Добро пожаловать к лучшему другу каждого программиста на C/C++: неопределенному поведению.
Существует много того, что не указано в стандарте языка по разным причинам. Это одна из них.
В общем, всякий раз, когда вы сталкиваетесь с неопределенным поведением, может произойти все что угодно. Приложение может зависнуть, оно может зависнуть, оно может вытолкнуть ваш привод CD-ROM или заставить демонов выйти из носа. Он может отформатировать жесткий диск или по электронной почте всех ваших порно к своей бабушке.
Даже если вам действительно не повезло, может показаться, что он работает правильно.
Язык просто говорит, что должно произойти, если вы обращаетесь к элементам в пределах массива. Не определено, что произойдет, если вы выйдете за пределы. Может показаться, что он работает сегодня на вашем компиляторе, но это не является допустимым C или C++, и нет никакой гарантии, что он все равно будет работать при следующем запуске программы. Или что он не переписал важные данные даже сейчас, и вы просто не сталкивались с проблемами, которые это вызовет - пока.
Что касается того, почему нет проверки границ, есть несколько аспектов ответа:
- Массив является остатком от C. Массивы C настолько примитивны, насколько это возможно. Просто последовательность элементов со смежными адресами. Там нет проверки границ, потому что это просто показывает необработанную память. Реализация надежного механизма проверки границ была бы почти невозможна в C.
- В C++ возможна проверка границ для типов классов. Но массив все еще является старым C-совместимым. Это не класс. Кроме того, C++ также построен на другом правиле, которое делает проверку границ неидеальной. Руководящий принцип C++: "Вы не платите за то, что не используете". Если ваш код верен, вам не нужна проверка границ, и вам не нужно платить за накладные расходы, связанные с проверкой границ во время выполнения.
- Так что C++ предлагает
std::vector
Шаблон класса, который позволяет оба.operator[]
разработан, чтобы быть эффективным. Стандарт языка не требует, чтобы он выполнял проверку границ (хотя он также не запрещает это). Вектор также имеетat()
функция-член, которая гарантированно выполняет проверку границ. Таким образом, в C++ вы получаете лучшее из обоих миров, если используете вектор. Вы получаете массивную производительность без проверки границ, и вы получаете возможность использовать доступ с проверкой границ, когда вы этого хотите.
Используя g++, вы можете добавить параметр командной строки: -fstack-protector-all
,
На вашем примере это привело к следующему:
> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault ./t
На самом деле это не поможет вам найти или решить проблему, но, по крайней мере, segfault даст вам понять, что что- то не так.
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:
Хорошо, вот небольшой урок о массивах и их отношениях с указателями:
Когда вы используете индексирование массива, вы действительно используете скрытый указатель (называемый "ссылкой"), который автоматически разыменовывается. Вот почему вместо *(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
}
Компилятор не будет жаловаться, потому что в программировании вам часто приходится общаться с другими программами, особенно с операционной системой. Это делается с помощью указателей совсем немного.
намек
Если вы хотите иметь быстрые массивы размера ограничения с проверкой ошибок диапазона, попробуйте использовать boost:: array, (также std:: tr1:: array from <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
Запустите это через Valgrind, и вы можете увидеть ошибку.
Как указал Фалаина, valgrind не обнаруживает много случаев повреждения стека. Я только что попробовал образец под valgrind, и он действительно сообщает об отсутствии ошибок. Тем не менее, Valgrind может быть полезен при обнаружении многих других типов проблем с памятью, но в этом случае он не особенно полезен, если вы не измените свой bulid, включив в него опцию --stack-check. Если вы создаете и запускаете образец как
g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange
valgrind сообщит об ошибке.
C или C++ не будут проверять границы доступа к массиву.
Вы размещаете массив в стеке. Индексирование массива с помощью array[3]
эквивалентно *(array + 3)
где массив является указателем на & массив [0]. Это приведет к неопределенному поведению.
Один из способов отловить это иногда в C - это использовать статическую проверку, такую как splint. Если вы запускаете:
splint +bounds array.c
на,
int main(void)
{
int array[1];
array[1] = 1;
return 0;
}
тогда вы получите предупреждение:
array.c: (в функции main) array.c: 5: 9: Вероятно, хранилище вне границ: array[1] Невозможно разрешить ограничение: требуется 0 >= 1, необходимое для выполнения предварительного условия: требуется maxSet(array @ array.c:5:9) >= 1 Запись в память может выполнять запись по адресу за пределами выделенного буфера.
Вы, конечно, перезаписываете свой стек, но программа достаточно проста, чтобы последствия этого остались незамеченными.
libstdC++, который является частью gcc, имеет специальный режим отладки для проверки ошибок. Это включено флагом компилятора -D_GLIBCXX_DEBUG
, Помимо прочего, он выполняет проверку границ std::vector
за счет производительности. Вот онлайн демо с последней версией gcc.
Таким образом, на самом деле вы можете выполнять проверку границ в режиме отладки libstdC++, но вы должны делать это только при тестировании, потому что это стоит ощутимой производительности по сравнению с обычным режимом libstdC++.
Неопределенное поведение работает в вашу пользу. Независимо от того, что память вы бьете, очевидно, не имеет ничего важного. Обратите внимание, что C и C++ не выполняют проверку границ для массивов, поэтому подобные вещи не будут обнаружены при компиляции или во время выполнения.
Когда вы пишете 'array[index]' в C, это переводит его в машинные инструкции.
Перевод выглядит примерно так:
- 'получить адрес массива'
- 'получить размер типа объекта, из которого состоит массив'
- "умножить размер типа на индекс"
- "добавить результат к адресу массива"
- "прочитайте, что находится по полученному адресу"
Результат обращается к чему-то, что может или не может быть частью массива. В обмен на невероятную скорость машинных инструкций вы теряете сеть безопасности компьютера, проверяющего все за вас. Если вы дотошны и осторожны, это не проблема. Если вы неряшливы или допустили ошибку, вас обожгут. Иногда он может генерировать недопустимую инструкцию, которая вызывает исключение, иногда нет.
Когда вы инициализируете массив с int array[2]
, выделено место для 2 целых чисел; но идентификатор array
просто указывает на начало этого пространства. Когда вы получите доступ array[3]
а также array[4]
затем компилятор просто увеличивает этот адрес, чтобы указать, где будут эти значения, если массив будет достаточно длинным; попробуйте получить доступ к чему-то вроде array[42]
без предварительной инициализации вы получите то значение, которое уже было в памяти в этом месте.
Редактировать:
Больше информации об указателях / массивах: http://home.netcom.com/~tjensen/ptr/pointers.htm
Хороший подход, который я часто видел и который я использовал на самом деле, заключается в том, чтобы внедрить некоторый элемент типа 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
типы.
Как уже упоминалось в этом вопросе, использование std::vector::at решит проблему и выполнит обязательную проверку перед доступом.
Если вам нужен массив постоянного размера, который находится в стеке в качестве первого кода, используйте новый контейнер C++11 std::array; в качестве вектора есть функция std::array::at. Фактически функция существует во всех стандартных контейнерах, в которых она имеет значение, т. Е. Где определен operator[]:( deque, map, unordered_map), за исключением std::bitset, в котором она называется std::bitset::тестовое задание.
Как я понимаю, локальные переменные размещаются в стеке, поэтому выход за пределы собственного стека может перезаписать только некоторые другие локальные переменные, если вы не слишком перегружены и не превышаете размер стека. Поскольку у вас нет других переменных, объявленных в вашей функции - это не вызывает никаких побочных эффектов. Попробуйте объявить другую переменную / массив сразу после вашей первой и посмотрите, что будет с ней.
Если вы немного измените свою программу:
#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.
Таким образом, вы можете "уйти" с превышением границ массива в вашем первоначальном примере, но ценой нанесения ущерба в другом месте - повреждения, которое может оказаться очень трудно диагностировать.
Что касается того, почему нет автоматической проверки границ - правильно написанная программа не нуждается в этом. Как только это будет сделано, нет причин выполнять проверку границ во время выполнения, и это просто замедлит работу программы. Лучше всего, чтобы все выяснилось при разработке и кодировании.
C++ основан на C, который был разработан как можно ближе к языку ассемблера.
Поведение может зависеть от вашей системы. Как правило, у вас будет запас для выхода за пределы, иногда со значением 0 или значениями мусора. Подробности вы можете узнать о механизме распределения памяти, используемом в вашей ОС. Вдобавок ко всему, если вы используете язык программирования, такой как c / c++, он не будет проверять границы при использовании некоторых контейнеров, таких как массив. Итак, вы встретите «неопределенное событие», потому что вы не знаете, что ОС сделала под поверхностью. Но, как и язык программирования Java, он будет проверять границы. Если вы выйдете за границу, вы получите исключение.
Когда вы объявляете массив int [2]; Вы резервируете 2 пространства памяти по 4 байта в каждом (32-битная программа). если вы введете массив [4] в своем коде, он по-прежнему будет соответствовать допустимому вызову, но только во время выполнения вызовет необработанное исключение. C++ использует ручное управление памятью. Это на самом деле недостаток безопасности, который был использован для взлома программ
это может помочь понять:
int * somepointer;
somepointer [0] = somepointer [5];