Доступ к массиву вне границ не дает ошибок, почему? - PullRequest
150 голосов
/ 06 августа 2009

Я присваиваю значения в программе на 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;

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

Ответы [ 17 ]

311 голосов
/ 06 августа 2009

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

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

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

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

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

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

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

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

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

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

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

12 голосов
/ 06 августа 2009

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

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

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

7 голосов
/ 06 августа 2009

Насколько я знаю, это неопределенное поведение. Запустите большую программу с этим, и она потерпит крах где-то по пути. Проверка границ не является частью необработанных массивов (или даже 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
}

Редактировать2:

Не запускайте это.

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 голосов
/ 06 августа 2009

Подсказка

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

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

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

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

valgrind сообщит об ошибке.

3 голосов
/ 06 августа 2009

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

Вы размещаете массив в стеке. Индексирование массива с помощью array[3] эквивалентно *(array + 3), где array является указателем на & array [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: вероятно, выходит за пределы хранить: Массив [1] Невозможно разрешить ограничение: требует 0> = 1 необходимо выполнить предварительное условие: требует maxSet (array @ array.c: 5: 9)> = 1 Запись в память может написать по адресу за пределами выделенный буфер.

3 голосов
/ 06 августа 2009

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

2 голосов
/ 06 августа 2009

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

1 голос
/ 06 августа 2009

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

Edit:

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...