Инициализация int влияет на возвращаемое значение функции

Извините за неопределенность названия этого вопроса, но я не уверен, как именно это задать.

Следующий код, выполняемый на микропроцессоре Arduino (с ++ скомпилирован для микропроцессора ATMega328), работает нормально. Возвращаемые значения отображаются в комментариях в коде:

// Return the index of the first semicolon in a string
int detectSemicolon(const char* str) {

    int i = 0;

    Serial.print("i = ");
    Serial.println(i); // prints "i = 0"

    while (i <= strlen(str)) {
        if (str[i] == ';') {
            Serial.print("Found at i = ");
            Serial.println(i); // prints "Found at i = 2"
            return i;
        }
        i++;
    }

    Serial.println("Error"); // Does not execute
    return -999;
}

void main() {
    Serial.begin(250000);
    Serial.println(detectSemicolon("TE;ST")); // Prints "2"
}

Это выдает «2» в качестве позиции первой точки с запятой, как и ожидалось.

Однако, если я изменю первую строкуdetectSemicolon функция кint i; т.е. без явной инициализации у меня возникают проблемы. В частности, выводом будет «i = 0» (хорошо), «найдено при i = 2» (хорошо), «-999» (плохо!).

Таким образом, функция возвращает -999, несмотря на то, что выполнила инструкцию print непосредственно передreturn 2; и несмотря на то, что оператор print никогда не выполнялся непосредственно передreturn -999; линия.

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

РЕДАКТИРОВАТЬ: Спасибо всем, кто принял участие, и особенно underscore_d за их отличный ответ. Кажется, что неопределенное поведение действительно приводит к тому, что компилятор просто пропускает все, что связано сi, Вот некоторые из сборок с serial.prints в Detection Semicolon закомментированы:

void setup() {
    Serial.begin(250000);
    Serial.println(detectSemicolon("TE;ST")); // Prints "2"
  d0:   4a e0           ldi r20, 0x0A   ; 10
  d2:   50 e0           ldi r21, 0x00   ; 0
  d4:   69 e1           ldi r22, 0x19   ; 25
  d6:   7c ef           ldi r23, 0xFC   ; 252
  d8:   82 e2           ldi r24, 0x22   ; 34
  da:   91 e0           ldi r25, 0x01   ; 1
  dc:   0c 94 3d 03     jmp 0x67a   ; 0x67a <_ZN5Print7printlnEii>

Похоже, что компилятор фактически полностью игнорирует цикл while и приходит к выводу, что вывод всегда будет "-999", и поэтому он даже не беспокоится о вызове функции, вместо этого он жестко кодирует 0xFC19. Я еще раз посмотрю с включенным serial.prints, чтобы функция все еще вызывалась, но я думаю, что это сильный указатель.

РЕДАКТИРОВАТЬ 2:

Для тех, кто действительно заботится, вот ссылка на дизассемблированный код, точно такой же, как показано выше (в случае UB):

https://justpaste.it/vwu8

Если вы посмотрите внимательно, кажется, компилятор назначает регистр 28 в качестве местоположенияi и "инициализировать" его в ноль в строкеd8, Этот регистр обрабатывается так, как если бы он содержалi в циклах while, в операторах if и т. д., поэтому кажется, что код работает, и операторы print выводятся, как и ожидалось (например, строка 122, где «i» увеличивается).

Однако, когда дело доходит до возврата этой псевдопеременной, это слишком далеко для нашего проверенного и испытанного компилятора; он рисует линию и выводит нас в другой оператор возврата (строка 120 переходит на строку 132, загружая «-999» в регистры 24 и 25, прежде чем вернуться кmain()).

Или, по крайней мере, это насколько я могу получить с моим ограниченным пониманием сборки. Мораль этой истории - странные вещи, которые происходят, когда поведение вашего кода не определено.

 CharlieB01 июл. 2016 г., 20:10
#KeepingItRealWithMicroprocessors
 underscore_d01 июл. 2016 г., 19:16
@RealtimeRik Пожалуйста, дайте нам ссылку или, по крайней мере, суммируйте общие ситуации, в которых вы видите несколькоreturn используемые очки и что вы будете делать вместо этого. «Хорошая практика» подразумевает, что это общепринятое эмпирическое правило, но я не могу вспомнить, прочитав какую-либо конкретную рекомендацию в этом ключе.
 CharlieB01 июл. 2016 г., 20:08
На самом деле, окончательное чтение получит NULL char, при условии, что строка правильно завершена. Это похмелье от неупрощенной версии этого кода, которая также обнаруживает пустые значения, но я удалил это, чтобы получить MWE этой ошибки
 Angew01 июл. 2016 г., 19:07
@RealtimeRik Я не верю, что это общепринятая точка зрения. Я нахожу код раннего возвратамного более читабельный, чем код, искусственно растянутый до одной точки возврата.
 underscore_d01 июл. 2016 г., 20:09
@CharlieB А, конечно. я используюstd::string так много, что мне никогда не придется думать о нулевом терминаторе! Я удалю свой комментарий, поскольку вы четко знаете, как работает индексация в реальных массивах :-)
 underscore_d04 июл. 2016 г., 13:06
@CharlieB Отлично! "Мораль этой истории - странные вещи, которые происходят, когда поведение вашего кода не определено."- важный момент, хорошо продемонстрированный :-)
 Realtime Rik01 июл. 2016 г., 19:17
Это не может быть общепринятым, но каждая компания, в которой я работал в течение последних 20 лет, обеспечивает это, как и стандарт Misra. Лично я считаю, что в большинстве случаев это хорошая практика.
 Realtime Rik01 июл. 2016 г., 18:54
Рекомендуется иметь только одну точку возврата из функции. Я бы предложил рефакторинг кода. Также, как отмечали другие, никогда не используйте неинициализированную переменную. Вам не обязательно инициализировать его там, где он объявлен, если вы уверены, что он будет инициализирован в другом месте, прежде чем его использовать.
 Realtime Rik01 июл. 2016 г., 19:04
Err, не goto. У них есть использование, но они немногочисленны. Единственная точка возврата делает код намного более тестируемым. Я постараюсь найти ссылку для вас.
 CharlieB02 июл. 2016 г., 13:53
@underscore_d это сделало его более странным, у меня были некоторые проблемы с расшифровкой, что именно происходило в полной версии. Если вам интересно я выложу полную разобранную версию в понедельник
 underscore_d02 июл. 2016 г., 14:05
@CharlieB Круто, дай мне знать, как только ты отредактируешь!
 Realtime Rik01 июл. 2016 г., 19:37
Извините, был занят. Похоже, это что-то вроде открытого обсуждения с людьми по обе стороны забора. Я склонен кодировать стандарт MISRA из-за требований к работе и всегда предпочитаю одну точку выхода, где это возможно. Я не собираюсь спорить об этом, хотя. Вкладки против пробелов кто-нибудь?
 underscore_d01 июл. 2016 г., 20:10
@CharlieB Определенно! Я достану тот Mega2560 из шкафа в конце концов, и когда я это сделаю ... это будет великолепно. Кроме того, в последние годы у меня были действительно забавные набеги на Z80 и 68000, нужно подумать о другом оправдании, чтобы вернуться назад. : D
 underscore_d02 июл. 2016 г., 12:51
@CharlieB забыл сказать, спасибо за показ разборки. Всегда хорошо иметь краткие доказательства совершенно нелогичных вещей, которые могут возникнуть в результате UB. Вы узнали, включая лиprint делает вещи еще страннее?
 CharlieB04 июл. 2016 г., 12:48
@underscore_d Полная версия загружена для вашего удовольствия.
 underscore_d01 июл. 2016 г., 19:31
@RealtimeRik Хорошо, я сделаю это сам:stackoverflow.com/questions/36707/... programmers.stackexchange.com/questions/118703/... Достаточно сказать, что эта «хорошая практика» вызывает много споров и что разумный ответ, вероятно, где-то посередине, в зависимости от контекста. Для моих соответствующих контекстов я сохраню свои множественные точки возврата, чтобы мне не нужно было объявлять временные переменные для возврата (относящиеся к RVO), ужасный отступ стрелки для проверок ошибок, хаки сwhile/break, так далее
 underscore_d01 июл. 2016 г., 19:01
@RealtimeRik Я не слышал, чтобы многие люди жаловались на множественныеreturn точки. Можете ли вы указать мне на статью доверенного эксперта, который советует это? Я имею в виду, что это, похоже, самый чистый способ выйти сюда. Что бы вы порекомендовали вместо этого?goto?

Ответы на вопрос(2)

она имеет случайное значение, каким бы оно ни было в адресе памяти, поэтомуwhile (i <=strlen(str)) будет вести себя непредсказуемо. Вы должны всегда инициализировать.

(Конфигурации отладки Visual Studio автоматически инициализируют переменные.)

 Angew01 июл. 2016 г., 19:09
@underscore_d Нет, вы не ошиблись. Это поведение по умолчанию для отладочных сборок VS.
 underscore_d01 июл. 2016 г., 18:55
У него нет случайного значения. Это вызывает неопределенное поведение. Эти два принципиально различны и не обязаны производить аналогичные результаты. Люди, которым нужны случайные числа, используют классы генератора случайных чисел stdlib
 underscore_d01 июл. 2016 г., 19:44
@Angew В этой старой статье указывается, что переменные не инициализируются по умолчанию в режиме отладки, но тот факт, что они могут находиться в разных областях памяти, может привести к разному поведению кода, который читает переменные с помощью UB, если он фактически читает их физические ( не-) значение:codeproject.com/Articles/548/Surviving-the-Release-Version Я знаю, насколько он комедийно стар и что начиная с версии 2005 года области памяти для таких переменных заполнены детерминированными образцами. Я по-прежнему скептически отношусь к тому, что режим отладки «официально» инициирует такие переменные и таким образом определяет их поведение.
 CharlieB01 июл. 2016 г., 18:35
Приветствия, Мэтт, я понимаю, что мне нужно инициализировать переменные, и это действительно была опечатка. Однако я пытаюсь понять, что здесь происходит, из интереса. Конечно, сравнение всегда должно возвращать либо true, либо false, и цикл while ведет себя соответствующим образом, независимо от мусора вi?
 Angew01 июл. 2016 г., 18:36
@CharlieB Нет. Компилятор полностью имеет право сказать: «Я вижу, чтоi никогда не инициализируется, поэтому я могу предположить, что оно имеет любое значение, которое я считаю наиболее удобным. "Например, не изменять регистр при передачеi к функции, и принимая все условия, включающиеi ложные (Примечание: я не знаю, действительно ли это делает компилятор, но это объясняет поведение, которое вы видите).
 underscore_d01 июл. 2016 г., 19:11
@Angew Должности, которые я нашел, предполагают, что он инициализирует основную память, таким образом потенциально влияя на то, что происходит с любыми операциями чтения UB, которые оказываются успешными ... но, конечно, он не «переопределяет» поведение только в режиме отладки? Извините, я просто нахожу это невероятным! Вы знаете, где я могу прочитать об этом?
 underscore_d01 июл. 2016 г., 18:59
Кроме того, «(конфигурации отладки Visual Studio автоматически инициализируют переменные.)» - что, действуя так, как будто пользователь инициализировал их по умолчанию? Это изменило бы поведение кода таким образом, что приведет к ложному чувству безопасности при отладке, прежде чем бросать их волкам, когда они переключаются в режим релиза и включают оптимизатор, тем самым предоставляя UB двойной мандат. Конечно, я недоразумение?
Решение Вопроса

static продолжительность хранения, декларирование, но не определениеint не вызывает инициализацию по умолчанию. Оставляет переменную неинициализированной. Что делаетне имею в видуi просто содержит случайное значение. Держитнет (известное, действительное) значение, и поэтому вам еще не разрешено его читать.

Вот соответствующая цитата из C ++ 11 Standard, черезAngew в комментариях. Это не было новым ограничением и не изменилось с тех пор:

C ++ 11 4.1 / 1, говорящий о преобразовании lvalue-в-значение (в основном чтение значения переменной): «Если объект, на который ссылается glvalue, ... неинициализирован, программа, которая требует этого преобразования, имеет неопределенное поведение. "

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

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

Печать переменной не считается «проверкой», как вы думаете, так что это не имеет значения. Невозможно «проверить» неинициализированную переменную и таким образом сделать прививку от UB. Поведение чтения переменной определяется только в том случае, если программа уже записала в нее определенное значение.

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

Почему вы хотите использовать его без инициализации? Это просто «академический»?

 Angew02 июл. 2016 г., 12:10
@ Clifford Действительно, еслиi не инициализирован, компилятор может сказать:i к функции? Я просто оставлю этот регистр нетронутым. тестированиеi в условном? Вместо этого я просто оставлю регистр флага ".
 Angew02 июл. 2016 г., 12:08
@Клиффорднет, в этом-то и дело.Любое поведение позволено. Оптимизатору разрешают предположить, что код, демонстрирующий UB, никогда не достигается, например. Пожалуйста прочтите этоБлог LLVMособенновторая часть.
 CharlieB01 июл. 2016 г., 18:44
Я предполагаю, что мой основной вопрос: «Как я могу вызвать оператор печати, который возвращает значение, а затем сразу же возвращает то же значение, но получает другой ответ?» Кажется, что этот код пропускается от строки к строке, даже после того, как он разрешен UB, вызванныйi.
 oldrinb01 июл. 2016 г., 18:49
Я знаю, что это не очень удовлетворительный ответ, но это все, что мы можем сказать с уверенностью. Предоставление дизассемблированного кода позволило бы нам попытаться выяснить, что именно сделал компилятор. поскольку Arduino IDE использует внутренний порт g ++ (avr-g ++), вы должны узнать, как передать флаг -S для генерации результирующей сборки ARM для вашего SSCCE
 CharlieB01 июл. 2016 г., 18:45
Или "Как я могу вернуть -999 без выполнения предыдущего оператора?"
 underscore_d01 июл. 2016 г., 22:32
@ Clifford Я все о нюансах! Не могли бы вы уточнить эти различия? ОП теперь, похоже, показал, что любое решение, принятое на основе ценностиi оптимизируется, когдаi имеет неопределенное значение. И это часто сообщается. Так что, определенно, здесь есть UB.
 Clifford02 июл. 2016 г., 00:22
@underscore_d: я просто говорю, чтозначение не определено, а неповедение, Если код читает и обрабатывает такое значение, он может вести себя не так, как задумано, но это не то же самое, чтонеопределенное поведение который является атрибутом компилятора, а не компилируемого кода.
 Clifford01 июл. 2016 г., 22:02
Толькозначение унитарной переменной не определено или, возможно, более точнонедетерминирована, Простое чтение такой переменной не вызываетнеопределенное поведение в том смысле, что «все может произойти», хотя воздействие на это значение может привести кнедетерминирована поведение - например, отмена ссылки на неверный указатель приведет к неопределенному поведению.
 underscore_d01 июл. 2016 г., 18:39
@CharlieB Смотрите мои изменения. UBточно объясняет, что здесь происходит.
 Clifford02 июл. 2016 г., 12:02
@Angew: Я ценю то, что вы говорите, но «все может случиться», как следствие, несколько преувеличивает эффект - скорее «любая ценность может привести» возможно?
 underscore_d02 июл. 2016 г., 00:36
@ Clifford Ну да, UB - это атрибут написанного кода, а не скомпилированная программа. Как только программа скомпилирована, она ведет себя особым образом, а не капризно изменяя свою логику (но, конечно, наблюдаемый результат может меняться между запусками). Но когда вы говорите «значение не определено, а не поведение», это звучит как неправильное представление многих людей, как изначально ОП и другие ответы о том, что происходит с неинициализированными переменными: «оно имеет случайное значение, и ваша программа действует [как обычно» ] на это случайное значение '. Мы видели, что это не так. Целые разделы, закодированные для использования переменной, были исключены
 CharlieB01 июл. 2016 г., 18:37
Обнаружив опечатку, которую я сейчас исправил, этот вопрос - скорее пост-вскрытие, чтобы помочь мне получить лучшее представление о том, что происходит внутри компилятора. Я понимаю, что int занимает определенное количество места. Без инициализации он будет содержать случайный мусор, но он все равно должен представлять некоторое целое число. Это не объясняет странное поведение, которое я видел, хотя
 Clifford02 июл. 2016 г., 10:01
Я ценю, что это несколько педантичный момент, но вы попросили меня уточнить, и, кажется, сделали больше, чем предполагалось. Это было предназначено как разъяснение, а не критика. Это просто лингвистическая семантическая разница между «поведением» и «ценностью», а также «неопределенным» и «недетерминированным».
 underscore_d01 июл. 2016 г., 18:48
@CharlieB Опять же, я утверждаю, что «в нас нет никакого смысла рассуждать о том, почему происходят определенные произвольные типы UB». Однако, если вы хотите понять этот точный пример на вашем точном компиляторе с таким точным направлением ветра ...oldrinb находится на отметке. Я также рекомендую подумать об идее, упомянутой только сейчасДжеймс и интересный комментарий, сделанныйAngew на другой ответ, так как, хотя сновачто-нибудь может случиться, реальность частонемного более предсказуемо.
 Angew02 июл. 2016 г., 10:55
@Clifford Но это не языковая семантика. «Неопределенное поведение» - это определенный термин в стандарте C ++, явно вызванный многочисленными правилами. Одним из них является C ++ 11 4.1 / 1, в котором говорится о преобразовании lvalue в rvalue (в основном, при чтении значения переменной): «Если объект, на который ссылается glvalue, ... неинициализирован, программа, которая требует этого преобразования имеет неопределенное поведение. " Таким образом, чтение неинициализированной переменной дает программе Undefined Behavior, period.
 oldrinb01 июл. 2016 г., 18:45
@CharlieB этоне имеет «разрешен» UB - UB означает, что выполнение кода не имеет поведенческих гарантий. если вы хотите более точный ответ о том, что именно произошло, опубликуйте разборку скомпилированного кода :)
 James Adkison01 июл. 2016 г., 18:48
@CharlieB Просто потому, что ты видишь0 печать не исключает возможность того, что некоторые ненужные данные передаютсяprintln просто интерпретируется как0 для отображения.

Ваш ответ на вопрос