1

При использовании в программе массива, строки или вектора в процессе выполнения возникают ошибки вида:

  • array subscript out of range
  • string subscript out of range
  • vector subscript out of range
  • Segmentation fault (core dumped)
  • Access violation reading location
  • Access violation writing location
  • нарушение прав доступа при чтении по адресу ...
  • нарушение прав доступа при записи по адресу ...
αλεχολυτ
  • 28,987
  • 13
  • 60
  • 119

1 Answers1

4

Подобные ошибки означают, что во время работы программы была предпринята попытка обращения к памяти, не подготовленной должным образом для этого. Программа с такими ошибками содержит неопределённое поведение. Самыми говорящими из рассмотренных в вопросе ошибок являются те, где сказано "subscript out of range". Дословно это переводится как "Индексация вне диапазона". Понятность их обеспечивается в первую очередь тем, что программа собрана в отладочном (Debug) режиме и соответствующий код индексации operator[] того или иного контейнера (std::array, std::string, std::vector) непосредственно содержит проверку значения индекса, передаваемого в оператор индексации. Например, так выглядит код в msvc 2019 для std::array:

    _NODISCARD _CONSTEXPR17 reference operator[](_In_range_(0, _Size - 1) size_type _Pos) noexcept /* strengthened */ {
#if _CONTAINER_DEBUG_LEVEL > 0
        _STL_VERIFY(_Pos < _Size, "array subscript out of range");
#endif // _CONTAINER_DEBUG_LEVEL > 0
    return _Elems[_Pos];
}

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

Решение проблемы прямо вытекает из текста отладочных сообщений - индекс, по которому идёт обращение к элементу контейнера, должен быть в допустимом диапазоне. Т.е. если контейнер имеет размер N, допустимыми индексами будут являться 0...N-1. Исключением из этого правила является std::string, где допускается так же использовать индекс N, но с оговоркой, что писать туда можно только нулевой символ \0, и он же вернётся при чтении.

Довольно часто проблема выхода за допустимые границы диапазона случается в циклах, когда условие завершение содержит нестрогое сравнение индекса с размером контейнера: т.е. i <= size вместо i < size. В подобных случаях итерации по элементам, следует по возможности пользоваться диапазонной версией цикла for, которая не допускает выхода за пределы контейнера, если размер контейнера остаётся постоянным в процессе итерирования.

std::vector<int> v = {1, 2, 3};
for(int i = 0; i <= v.size(); ++i) // ошибка. <= вместо <
   v[i] = 42;                      // проблема на последней итерации

for(auto& e : v) // диапазонный for e = 42; // e всегда принадлежит вектору

Иногда цикл кажется правильным, но вместо v.size() по незнанию указывают v.max_size(), который говорит вообще о потенциально возможном размере контейнера для данной архитектуры, а не о текущем размере. Понятно, что в этом случае можно выйти далеко за пределы разрешённых величин.

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

for(int i = 0; i < v.size(); ++i) {
   v[f(i)] = ...   // f(i) может возвращать другой диапазон
   v[i - 1] = ...  // использование "предыдущего" индекса. Ошибка для i == 0.
}

Другой возможный случай - когда возникает путаница между ассоциативными контейнерами (где operator[] приводит к созданию/добавлению элемента в контейнер, если его ещё не было) и последовательными контейнерами. Например:

std::map<int, int> m;
m[0] = 42; // ok, добавляется элемент с ключом 0 и ему присваивается значение 42

std::vector<int> v; v[0] = 42; // ошибка, вектор пустой, элемента с индексом 0 не существует

Добавить элемент в вектор можно либо при инициализации std::vector<int> v = {42};, либо позднее, например, через v.push_back(42).

Иногда ошибка может возникать, когда вместо std::vector::resize был вызван std::vector::reserve. Т.е. память выделена и доступ идёт только к элементам внутри выделенной памяти, но логически размер вектора не был изменён (элементы вектора фактически не проинициализированы). При Release сборке в этом случае может вовсе не возникнуть ошибки доступа (access violation), но такая программа не будет считаться валидной.

Чтобы обеспечить дополнительную проверку диапазона используемых индексов как в отладочном, так и в релизном режимах сборки вместо operator[] можно воспользоваться функцией at(), которая выбросит исключение std::out_of_range при использовании индекса вне разрешённого диапазона. В некоторых случаях сложного вычисления индекса это может быть оправдано, но внесёт дополнительный оверхед.

αλεχολυτ
  • 28,987
  • 13
  • 60
  • 119