Доступ к массиву за пределами границ не дает ошибки, почему?

Я назначаю значения в С++-программе из следующих значений:

#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;

и это тоже не вызывает ошибок.

Ответы

Ответ 1

Добро пожаловать в каждого лучшего друга программиста на C/С++: Undefined Поведение.

Существует много, которое не указано стандартом языка по разным причинам. Это один из них.

В общем, всякий раз, когда вы сталкиваетесь с поведением undefined, все может случиться. Приложение может потерпеть крах, оно может замерзнуть, оно может изгнать ваш привод CD-ROM или заставить демонов выйти из вашего носа. Он может отформатировать жесткий диск или по электронной почте всех ваших порно к вашей бабушке.

Возможно, даже если вам действительно не повезло, они работают правильно.

Язык просто говорит, что должно произойти, если вы обращаетесь к элементам в пределах массива. Остается undefined, что произойдет, если вы выйдете за пределы. Возможно, это работает сегодня, на вашем компиляторе, но это не законный C или С++, и нет гарантии, что он все равно будет работать при следующем запуске программы. Или что он не перезаписал существенные данные даже сейчас, и вы просто не столкнулись с проблемами, которые он будет вызывать - пока.

Что касается того, почему нет проверки границ, есть несколько аспектов ответа:

  • Массив - это остаток от массивов C. C примерно так же примитивен, как вы можете получить. Просто последовательность элементов со смежными адресами. Проверка границ не проводится, потому что она просто разоблачает необработанную память. Внедрение надежного механизма проверки границ было бы почти невозможно в C.
  • В С++ возможна проверка границ типов классов. Но массив по-прежнему остается простым старым C-совместимым. Это не класс. Кроме того, С++ также построен на другом правиле, которое делает проверку границ неидеальным. Руководящим принципом С++ является "вы не платите за то, что не используете". Если ваш код верен, вам не нужна проверка границ, и вам не следует платить за накладные расходы на проверку границ выполнения.
  • Итак, С++ предлагает шаблон класса std::vector, который позволяет обоим. operator[] рассчитан на эффективность. Стандарт языка не требует, чтобы он выполнял проверку границ (хотя это также не запрещает). Вектор также имеет функцию члена at(), которая гарантированно выполняет проверку границ. Поэтому в С++ вы получаете лучшее из обоих миров, если используете вектор. Вы получаете производительность, подобную массиву, без проверки границ, и вы получаете возможность использовать доступ ограниченного доступа, когда захотите.

Ответ 2

Используя g++, вы можете добавить параметр командной строки: -fstack-protector-all.

В вашем примере это привело к следующему:

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

Это не поможет вам найти или решить проблему, но, по крайней мере, segfault сообщит вам, что что-то не так.

Ответ 3

g++ не проверяет границы массива, и вы можете перезаписать что-то с 3,4, но ничего действительно важного, если вы попытаетесь с более высокими номерами, вы получите сбой.

Вы просто перезаписываете части стека, которые не используются, вы можете продолжить, пока не достигнете конца выделенного пространства для стека, и он в конечном итоге потерпит крах

EDIT: У вас нет способа справиться с этим, может быть, статический анализатор кода может выявить эти сбои, но это слишком просто, вы можете иметь схожие (но более сложные) отказы, не обнаруженные даже для статических анализаторов

Ответ 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
}

Компилятор не будет жаловаться, потому что в программировании вам часто приходится общаться с другими программами, особенно с операционной системой. Это делается с помощью указателей совсем немного.

Ответ 5

Подсказка

Если вы хотите иметь массивы с быстрым ограничением с проверкой ошибок диапазона, попробуйте использовать boost:: array (также std:: tr1:: array из <tr1/array> он будет стандартным контейнером в следующей спецификации на С++). Это намного быстрее, чем 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

Ответ 6

Вы, безусловно, перезаписываете свой стек, но программа достаточно проста, что последствия этого остаются незамеченными.

Ответ 7

C или С++ не будут проверять границы доступа к массиву.

Вы выделяете массив в стеке. Индексирование массива с помощью array[3] эквивалентно * (array + 3), где array - указатель на & array [0]. Это приведет к поведению undefined.

Один из способов поймать этот иногда на C - это использовать статическую проверку, например splint. Если вы запустите:

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) >= 1 Запись в память может напишите на адрес за пределами выделенный буфер.

Ответ 8

Запустите это через Valgrind, и вы увидите ошибку.

Как отметил Фалаина, valgrind не обнаруживает много случаев повреждения стека. Я просто попробовал образец под valgrind, и он действительно сообщает о нулевых ошибках. Тем не менее, Valgrind может сыграть важную роль в поиске многих других проблем с памятью, это просто не особенно полезно в этом случае, если вы не измените свой bulid, чтобы включить опцию -stack-check. Если вы создаете и запускаете образец как

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

valgrind будет сообщать об ошибке.

Ответ 9

Undefined поведение работает в вашу пользу. Какая бы память, которую вы сбиваете, по-видимому, не держит ничего важного. Обратите внимание, что C и С++ не выполняют проверку границ на массивах, поэтому такие вещи не будут пойманы при компиляции или времени выполнения.

Ответ 10

Когда вы инициализируете массив с помощью int array[2], выделяется пространство для 2 целых чисел; но идентификатор array просто указывает на начало этого пространства. Когда вы затем получаете доступ к array[3] и array[4], тогда компилятор просто увеличивает этот адрес, указывая на то, где эти значения будут, если массив был достаточно длинным; попробуйте получить доступ к чему-то вроде array[42], не инициализируя его в первую очередь, вы получите то, что уже произошло в памяти в этом месте.

Edit:

Дополнительная информация о указателях/массивах: http://home.netcom.com/~tjensen/ptr/pointers.htm

Ответ 11

когда вы объявляете int array [2]; вы резервируете 2 пространства памяти по 4 байта каждый (32-битная программа). если вы введете массив [4] в свой код, он по-прежнему соответствует действительному вызову, но только во время выполнения он выкинет необработанное исключение. С++ использует ручное управление памятью. Это на самом деле недостаток безопасности, который использовался для взлома программ

Это может помочь понять:

int * somepointer;

somepointer [0] = somepointer [5];

Ответ 12

Как я понимаю, локальные переменные выделяются в стеке, поэтому выход за пределы вашего собственного стека может только перезаписать некоторую другую локальную переменную, если вы не перейдете слишком много и не превысите размер вашего стека. Поскольку у вас нет других переменных, объявленных в вашей функции, это не вызывает никаких побочных эффектов. Попробуйте объявить другую переменную/массив сразу после первого и посмотреть, что с ним произойдет.

Ответ 13

Когда вы пишете 'array [index]' в C, он переводит его в машинные инструкции.

Перевод выглядит примерно так:

  • 'получить адрес массива
  • 'получить размер массива объектов, состоящий из
  • 'умножить размер типа по индексу
  • 'добавить результат в адрес массива
  • 'прочитайте, что на результирующем адресе

Результат обращается к тому, что может или не может быть частью массива. В обмен на стремительную скорость машинных инструкций вы теряете систему безопасности компьютера, проверяющую вещи для вас. Если вы внимательны и осторожны, это не проблема. Если вы небрежны или ошибаетесь, вас сжигают. Иногда он может генерировать неверную инструкцию, которая вызывает исключение, иногда нет.

Ответ 14

Хороший подход, который я видел часто, и я был использован на самом деле, заключается в том, чтобы в конце массива ввести некоторый элемент типа 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.

Ответ 15

Как уже упоминалось в вопросе с использованием std::vector:: at, вы решите проблему и сделаете связанную проверку перед доступом.

Если вам нужен массив с постоянным размером, который находится в стеке, так как ваш первый код использует новый контейнер С++ 11 std:: array; в качестве вектора есть std:: array:: at function. Фактически, эта функция существует во всех стандартных контейнерах, в которых она имеет значение, т.е. Где оператор [] определен: (deque, map, unordered_map), за исключением std:: bitset, в котором он называется std:: bitset::. тест

Ответ 16

libstdС++, являющийся частью gcc, имеет специальный режим отладки для проверки ошибок. Он включен флагом компилятора -D_GLIBCXX_DEBUG. Помимо прочего, он ограничивает проверку std::vector ценой производительности. Ниже приведена онлайн-демонстрация с последней версией gcc.

Итак, вы можете выполнить проверку границ с помощью режима отладки libstdС++, но вы должны делать это только при тестировании, потому что оно стоит заметной производительности по сравнению с обычным режимом libstdС++.

Ответ 17

Если вы немного измените свою программу:

#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, который был разработан как можно ближе к языку ассемблера.