0

C++ вылетает в 'for' цикле с отрицательным выражением

12

Описание проблемы для StackOverflow:


Я столкнулся с проблемой в моем коде на C++, которая вызывает сбой с ошибкой выполнения. Вот код, который вызывает сбой:

#include <string>

using namespace std;

int main() {
    string s = "aa";
    for (int i = 0; i < s.length() - 3; i++) {

    }
}

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

#include <string>

using namespace std;

int main() {
    string s = "aa";
    int len = s.length() - 3;
    for (int i = 0; i < len; i++) {

    }
}

В первом случае цикл выполняется до значения s.length() - 3, что в приведенном примере будет равно 0 - 3 = -3, что, по идее, должно привести к некорректному поведению. Во втором случае я присваиваю результат s.length() - 3 переменной len, и цикл корректно завершает выполнение, так как len становится равным -3.

Не могу понять, почему поведение этих двух фрагментов кода столь различно. Может ли кто объяснить, в чем причина этой ошибки?


5 ответ(ов)

0

Прежде всего: почему ваша программа падает? Давайте пройдемся по ней так, как это сделал бы отладчик.

Обратите внимание: я предполагаю, что тело вашего цикла не пустое, но обращается к строке. Если это не так, причина сбоя заключается в некорректном поведении из-за переполнения целого числа. В этом случае смотрите ответ Ричарда Хансена.

std::string s = "aa"; // присваиваем переменной s строку из двух символов "aa"
for ( int i = 0; // создаём переменную i типа int с начальным значением 0 
i < s.length() - 3 // вызываем s.length(), вычитаем 3 и сравниваем результат с i. В порядке!
{...} // выполняем тело цикла
i++ // выполняем инкремент в цикле, теперь i равно 1!
i < s.length() - 3 // снова вызываем s.length(), вычитаем 3 и сравниваем результат с i. В порядке!
{...} // выполняем тело цикла
i++ // выполняем инкремент, теперь i равно 2!
i < s.length() - 3 // снова вызываем s.length(), вычитаем 3 и сравниваем результат с i. В порядке!
{...} // выполняем тело цикла
i++ // выполняем инкремент, теперь i равно 3!
.
.

Мы ожидаем, что проверка i < s.length() - 3 сразу же вернёт false, так как длина s равна двум (мы лишь один раз присвоили ей значение в начале и больше не меняли его), следовательно, 2 - 3 будет равно -1, и 0 < -1 — это ложное утверждение. Однако мы видим "OK" здесь.

Это происходит потому, что s.length() не равно 2. Оно равно 2u. Функция std::string::length() возвращает значение типа size_t, которое является беззнаковым целым числом. Теперь возвращаемся к условию цикла. Сначала мы получаем значение s.length(), т.е. 2u, затем вычитаем 3. 3 — это литерал типа int, который компилятор интерпретирует как тип int. Следовательно, компилятор должен вычислить 2u - 3. Операции над примитивными типами можно производить только для значений одного типа, поэтому одно из них должно быть преобразовано в другой тип. Существует множество строгих правил, и в данном случае "беззнаковое" "выигрывает", поэтому 3 преобразуется в 3u. В беззнаковых целых числах 2u - 3u не может быть равно -1u, так как такого числа не существует (поскольку оно имеет знак!). Вместо этого вычисляется всё с использованием операции по модулю 2^(n_bits), где n_bits — это количество бит в данном типе (обычно 8, 16, 32 или 64). Таким образом, вместо -1 мы получаем 4294967295u (при условии 32-битного типа).

Теперь компилятор закончил с s.length() - 3 (безусловно, он гораздо быстрее меня 😉), давайте перейдём к сравнению: i < s.length() - 3. Подставляем значения: 0 < 4294967295u. Снова разные типы, и 0 становится 0u, следовательно, сравнение 0u < 4294967295u очевидно истинно, условие цикла проверяется положительно, и мы можем выполнять тело цикла.

После инкремента единственное, что меняется в вышесказанном, это значение переменной i. Значение i снова будет преобразовано в беззнаковое целое, так как это требуется для сравнения.

Таким образом, у нас получаются такие операции:

(0u < 4294967295u) == true, выполняем тело цикла!
(1u < 4294967295u) == true, выполняем тело цикла!
(2u < 4294967295u) == true, выполняем тело цикла!

И вот проблема: что вы делаете в теле цикла? Предположительно, вы обращаетесь к символу строки с индексом i, не так ли? Хотя вы не имели этого в виду, вы обращаетесь не только к нулевому и первому элементу, но и ко второму! Но второго элемента нет (так как ваша строка содержит всего два символа, нулевой и первый), вы обращаетесь к некорректной памяти, программа делает то, что хочет (некорректное поведение). Обратите внимание, что программа не обязана сразу же крашиться. Она может работать еще полчаса, и такие ошибки трудно поймать. Но всегда опасно обращаться к памяти вне границ, именно здесь и возникают большинство сбоев.

В итоге, вы получаете другое значение из выражения s.length() - 3, чем ожидали, это приводит к положительной проверке условия цикла, которая ведет к многократному выполнению тела цикла, что, в свою очередь, обращается к памяти, к которой нельзя обращаться.

Теперь давайте рассмотрим, как это избежать, т.е. как сообщить компилятору, что вы на самом деле имели в виду в условии цикла.


Длины строк и размеры контейнеров по своей природе беззнаковые, поэтому вам следует использовать беззнаковое целое в циклах.

Поскольку unsigned int довольно длинный тип и нежелательно писать его много раз в циклах, просто используйте size_t. Это тип, который используется каждым контейнером в STL для хранения длины или размера. Вам, возможно, понадобится подключить cstddef, чтобы гарантировать независимость от платформы.

#include <cstddef>
#include <string>

using namespace std;

int main() {

    string s = "aa";

    for ( size_t i = 0; i + 3 < s.length(); i++) {
    //    ^^^^^^         ^^^^
    }
}

Поскольку a < b - 3 математически эквивалентно a + 3 < b, мы можем их поменять местами. Однако a + 3 < b предотвращает ситуацию, когда b - 3 становится огромным значением. Напомню, что s.length() возвращает беззнаковое целое, и беззнаковые целые выполняют операции по модулю 2^(bits), где bits — это количество бит в данном типе (обычно 8, 16, 32 или 64). Поэтому, если s.length() == 2, то s.length() - 3 == -1 == 2^(bits) - 1.


В качестве альтернативы, если вы хотите использовать i < s.length() - 3 по личным предпочтениям, вам необходимо добавить условие:

for ( size_t i = 0; (s.length() > 3) && (i < s.length() - 3); ++i )
//    ^             ^                    ^- ваше фактическое условие
//    ^             ^- проверка, достаточно ли длина строки
//    ^- все еще предпочтение беззнаковых типов!
0

На самом деле, в первой версии вашего кода цикл выполняется очень долго, потому что вы сравниваете i с беззнаковым целым числом, содержащим очень большое значение. Размер строки (в результате) совпадает с типом size_t, который является беззнаковым целым. Когда вы вычитаете 3 из этого значения, происходит переполнение, и оно становится очень большим.

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

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

0

Предполагая, что вы упустили важный код в цикле for

Большинство людей здесь, похоже, не могут воспроизвести сбой — я в их числе — и, похоже, другие ответы основаны на предположении, что вы упустили какой-то важный код в теле цикла for, и что отсутствующий код и вызывает ваш сбой.

Если вы используете i для доступа к памяти (предположительно, к символам в строке) в теле цикла for, и вы не включили этот код в свой вопрос в попытке предоставить минимальный пример, тогда сбой легко объяснить тем, что s.length() - 3 имеет значение SIZE_MAX из-за модульной арифметики для неотрицательных целых типов. SIZE_MAX — это очень большое число, поэтому i будет продолжать увеличиваться, пока не станет использоваться для доступа к адресу, который вызовет сегментационную ошибку.

Тем не менее, теоретически ваш код может вызвать сбой "как есть", даже если тело цикла for пусто. Я не знаю о каких-либо реализации, которые бы вызывали сбой, но, возможно, ваш компилятор и ЦПУ экзотические.

Следующее объяснение не предполагает, что вы опустили код в своем вопросе. Оно исходит из того, что код, который вы представили в своем вопросе, вызывает сбой "как есть"; что это не сокращенный анализ какого-то другого кода, который вызывает сбой.

Почему ваша первая программа вызывает сбой

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

Неопределенное поведение возникает из-за переполнения int. Стандарт C++11 говорит (в [expr] пункт 5, параграф 4):

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

В вашей программе s.length() возвращает size_t со значением 2. Вычитая 3 из этого значения, получается -1, за исключением того, что size_t — это тип без знака. Стандарт C++11 говорит (в [basic.fundamental] пункт 3.9.1 параграф 4):

Беззнаковые целые числа, объявленные как unsigned, должны подчиняться законам арифметики по модулю 2^n, где n — это количество бит в представлении значения данного размера целого числа.

Это означает, что результат s.length() - 3 будет size_t со значением SIZE_MAX. Это очень большое число, больше, чем INT_MAX (максимальное представимое значение для int).

Поскольку s.length() - 3 такое большое, выполнение застревает в цикле, пока i не достигнет INT_MAX. На следующей итерации, когда произойдет попытка увеличить i, результат будет INT_MAX + 1, но это значение уже не входит в диапазон представимых значений для int. Таким образом, поведение будет неопределенным. В вашем случае поведение приводит к сбою.

В моей системе поведение моей реализации, когда i увеличивается за пределы INT_MAX, заключается в том, чтобы обернуть (установить i в INT_MIN) и продолжить. Как только i достигнет -1, обычные арифметические преобразования вызывают равенство i со значением SIZE_MAX, и цикл завершится.

Любая реакция уместна. Это и есть проблема неопределенного поведения — оно может работать так, как вы задумывали, может привести к сбою, может отформатировать ваш жесткий диск или отменить "Firefly". Вы никогда не можете быть уверены.

Как ваша вторая программа избегает сбоя

Как и в первой программе, s.length() - 3 — это тип size_t со значением SIZE_MAX. Однако в этот раз значение присваивается переменной типа int. Стандарт C++11 говорит (в [conv.integral] пункт 4.7 параграф 3):

Если назначенный тип имеет знак, значение остается неизменным, если оно может быть представлено в целевом типе (и в ширине битового поля); в противном случае значение определяется реализацией.

Значение SIZE_MAX слишком велико, чтобы быть представленным типом int, поэтому len получает значение, определяемое реализацией (вероятно, -1, но может и не так). Условие i < len в конечном итоге станет истинным, независимо от присвоенного значения len, поэтому ваша программа завершится без возникновения неопределенного поведения.

0

Тип s.length() – это size_t, который равен 2. Следовательно, выражение s.length() - 3 также будет иметь беззнаковый тип size_t и значение SIZE_MAX, заданное реализацией (это 18446744073709551615, если size_t занимает 64 бита). Это как минимум 32-битный тип (может быть 64-битным на 64-битных платформах), и это высокое число приведет к бесконечному циклу. Чтобы избежать этой проблемы, вы можете просто привести s.length() к типу int:

for (int i = 0; i < (int)s.length() - 3; i++)
{
          //..какой-то код, который вызывает сбой
}

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

Что касается краха, то данный "бесконечный" цикл не является прямой причиной сбоя. Если вы поделитесь кодом внутри цикла, можно будет получить дополнительное объяснение.

0

Проблема, с которой вы столкнулись, заключается в том, что s.length() возвращает значение типа size_t, который является беззнаковым целым числом. Когда вы выполняете операцию s.length() - 3, если длина строки меньше 3, результат станет отрицательным. Однако поскольку size_t — это беззнаковый тип, отрицательное значение будет интерпретировано как большое положительное значение. Это приведет к тому, что цикл будет выполняться бесконечно, что, в конечном итоге, приведет к сбою программы.

Чтобы исправить эту проблему, необходимо явно привести результат s.length() к знаковому целому типу. Вы можете сделать это с помощью static_cast<int>(s.length()). Вот как это может выглядеть:

for (int i = static_cast<int>(s.length()) - 3; i >= 0; --i) {
    // Ваш код здесь
}

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь