С++: векторные границы

Я прихожу из Java и изучаю С++ в данный момент. Я пользуюсь принципами и практикой использования С++ для Stroustrup Progamming. Сейчас я работаю с векторами. На стр. 117 он говорит, что доступ к несуществующему элементу вектора вызовет ошибку времени выполнения (то же самое в Java, индекс за пределами границ). Я использую компилятор MinGW и когда компилирую и запускаю этот код:

#include <iostream>
#include <cstdio>
#include <vector>

int main() 
{ 
    std::vector<int> v(6);
    v[8] = 10;
    std::cout << v[8];
    return 0;
}

Это дает мне результат 10. Еще более интересно то, что если я не изменю несуществующий векторный элемент (я просто печатаю его, ожидая ошибки во время выполнения или, по крайней мере, значения по умолчанию), он печатает некоторые большие целые числа. Итак... неправильно ли Stroustrup, или у GCC есть некоторые странные способы компиляции С++?

Ответ 1

Книга немного расплывчата. Это не столько "ошибка времени выполнения", сколько поведение undefined, которое проявляется во время выполнения. Это означает, что все может случиться. Но ошибка строго связана с вами, а не с выполнением программы, и на самом деле невозможно и неразумно даже говорить о выполнении программы с поведением undefined.

В С++ нет ничего, что защищало бы вас от ошибок программирования, в отличие от Java.


Как говорит @sftrabbit, std::vector имеет альтернативный интерфейс .at(), который всегда дает правильную программу (хотя она может генерировать исключения), и, следовательно, о котором можно рассуждать.


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

int read_int()
{
    std::cout << "Please enter a number: ";
    int n;
    return (std::cin >> n) ? n : 18;
}

Теперь рассмотрим следующие три программы:

Опасный: Правильность этой программы зависит от пользовательского ввода! Это не обязательно неверно, но это небезопасно (до такой степени, что я бы назвал его сломанным).

int main()
{
    int n = read_int();
    int k = read_int();
    std::vector<int> v(n);
    return v[k];
}

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

int main() try
{
    int n = read_int();
    int k = read_int();
    std::vector<int> v(n);
    return v.at(k);
}
catch (...)
{
    return 0;
}

Разумный: Вышеприведенная версия с .at() неудобна. Лучше проверить и предоставить обратную связь. Поскольку мы выполняем динамическую проверку, неконтролируемый векторный доступ на самом деле гарантированно хорош.

int main()
{
    int n = read_int();

    if (n <= 0) { std::cout << "Bad container size!\n"; return 0; }

    int k = read_int();

    if (k < 0 || k >= n)  { std::cout << "Bad index!\n"; return 0; }

    std::vector<int> v(n);
    return v[k];
}

(Мы игнорируем возможность того, что построение вектора может вызвать исключение из его собственных.)

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

Если бы я хотел быть преувеличенным, я бы сравнил этот подход с Python, который позволяет писать невероятно короткие и правильные программы без какой-либо пользовательской обработки ошибок. Оборотная сторона заключается в том, что любая попытка использовать такую ​​программу, которая немного отличается от того, что предлагает программист, оставляет вам неспецифическую, трудночитаемую исключение и трассировку стека, а также небольшие рекомендации относительно того, что вы должны были сделать лучше. Вы не обязаны писать какую-либо обработку ошибок, и часто обработка ошибок не записывается. (Я не могу отличить С++ от Java, потому что, хотя Java вообще безопасен, мне еще предстоит увидеть короткую Java-программу.) </rantmode>

Ответ 2

C и С++ не всегда выполняют проверки границ. Это может привести к ошибке выполнения. И если вам нужно переусердствовать с вашим номером достаточно, скажем, 10000 или около того, это почти наверняка вызовет проблему.

Вы также можете использовать vector.at(10), что обязательно должно дать вам исключение. видеть: http://www.cplusplus.com/reference/vector/vector/at/ по сравнению с: http://www.cplusplus.com/reference/vector/vector/operator%5B%5D/

Ответ 3

Я надеялся, что векторный "оператор []" будет проверять границу как "at()", потому что я не так осторожен.: -)

Один из способов наследовал бы векторный класс и оператор переопределения [] для вызова в(), чтобы можно было использовать более читаемые "[]" и не нужно было заменять все "[]" на "at()". Вы также можете определить унаследованный вектор (например: safer_vector) как обычный вектор. Код будет таким (в С++ 11, llvm3.5 Xcode 5).

#include <vector>

using namespace std;

template <class _Tp, class _Allocator = allocator<_Tp> >
class safer_vector:public vector<_Tp, _Allocator>{
private:
    typedef __vector_base<_Tp, _Allocator>           __base;
public:
    typedef _Tp                                      value_type;
    typedef _Allocator                               allocator_type;
    typedef typename __base::reference               reference;
    typedef typename __base::const_reference         const_reference;
    typedef typename __base::size_type               size_type;
public:

    reference operator[](size_type __n){
        return this->at(__n);
    };

    safer_vector(_Tp val):vector<_Tp, _Allocator>(val){;};
    safer_vector(_Tp val, const_reference __x):vector<_Tp, _Allocator>(val,__x){;};
    safer_vector(initializer_list<value_type> __il):vector<_Tp, _Allocator>(__il){;}
    template <class _Iterator>
    safer_vector(_Iterator __first, _Iterator __last):vector<_Tp,_Allocator>(__first, __last){;};
    // If C++11 Constructor inheritence is supported
    // using vector<_Tp, _Allocator>::vector;
};
#define safer_vector vector

Ответ 4

Это ценный комментарий @Evgeny Sergeev, который я продвигаю к ответу:

Для GCC вы можете -D_GLIBCXX_DEBUG заменить стандартные контейнеры безопасными реализациями. Совсем недавно это, похоже, также работает с std:: array. Подробнее здесь: gcc.gnu.org/onlinedocs/libstdc++/manual/debug_mode.html

Я бы добавил, что также можно объединить отдельные "безопасные" версии векторных и других классов полезности, используя префикс gnu_debug:: namespace, а не std::.

Другими словами, не изобретайте колесо, проверки массива доступны, по крайней мере, с помощью GCC.