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* без копирования