C++ вылетает в 'for' цикле с отрицательным выражением
Описание проблемы для 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 ответ(ов)
Прежде всего: почему ваша программа падает? Давайте пройдемся по ней так, как это сделал бы отладчик.
Обратите внимание: я предполагаю, что тело вашего цикла не пустое, но обращается к строке. Если это не так, причина сбоя заключается в некорректном поведении из-за переполнения целого числа. В этом случае смотрите ответ Ричарда Хансена.
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 )
// ^ ^ ^- ваше фактическое условие
// ^ ^- проверка, достаточно ли длина строки
// ^- все еще предпочтение беззнаковых типов!
На самом деле, в первой версии вашего кода цикл выполняется очень долго, потому что вы сравниваете i с беззнаковым целым числом, содержащим очень большое значение. Размер строки (в результате) совпадает с типом size_t, который является беззнаковым целым. Когда вы вычитаете 3 из этого значения, происходит переполнение, и оно становится очень большим.
Во второй версии кода вы присваиваете это беззнаковое значение знаковому переменной, и в результате получаете корректное значение.
На самом деле именно условие или значение не вызывает сбой, скорее всего, это связано с тем, что вы индексируете строку вне границ, что является случаем неопределенного поведения.
Предполагая, что вы упустили важный код в цикле 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, поэтому ваша программа завершится без возникновения неопределенного поведения.
Тип 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, так как это знаковый целочисленный тип, и цикл не будет выполняться.
Что касается краха, то данный "бесконечный" цикл не является прямой причиной сбоя. Если вы поделитесь кодом внутри цикла, можно будет получить дополнительное объяснение.
Проблема, с которой вы столкнулись, заключается в том, что 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) {
// Ваш код здесь
}
Таким образом, вы гарантированно получите корректное поведение, избегая ошибки, связанной с беззнаковыми значениями.
Как удалить элемент из std::vector<> по индексу?
`unsigned int` против `size_t`: когда и что использовать?
Какова разница между "new", "malloc" и "calloc" в C++?
Что означает && в конце сигнатуры функции (после закрывающей скобки)?
Инициализация std::string из char* без копирования