Я не могу рассказать вам о специфике вашей платформы, поскольку я не знаю ее, но есть общий ответ на поведение, которое вы видите.
Когда компилируется некоторая функция, которая имеет возврат,компилятор будет использовать соглашение о том, как вернуть эти данные.Это может быть машинный регистр или определенная ячейка памяти, например, через стек или что-то еще (хотя обычно используются машинные регистры).Скомпилированный код может также использовать это местоположение (зарегистрироваться или иным образом), выполняя работу функции.
Если функция ничего не возвращает, компилятор не будет генерировать код, который явно заполняет это местоположение возвращаемым значением.Однако, как я сказал выше, он может использовать это местоположение во время функции.Когда вы пишете код, который читает возвращаемое значение (ch2 = toUpper(ch);)
, компилятор напишет код, который использует свое соглашение о том, как получить этот возврат из обычного местоположения.Что касается кода вызывающей стороны, он будет просто читать это значение из местоположения, даже если там ничего не было написано явно.Следовательно, вы получаете значение.
Теперь посмотрите на пример @ Ray, компилятор использовал регистр EAX, чтобы сохранить результаты операции верхнего регистра.Так получилось, это, вероятно, место, в которое возвращаются значения.На вызывающей стороне ch2 загружается со значением, которое находится в EAX - отсюда и фантомное возвращение.Это относится только к ряду процессоров x86, так как на других архитектурах компилятор может использовать совершенно другую схему при принятии решения о том, как следует организовать соглашение
Однако хорошие компиляторы будут пытаться оптимизировать в соответствии с наборомместные условия, знание кода, правил и эвристики.Поэтому важно отметить, что это просто удача, что это работает.Компилятор может оптимизировать и не делать этого или чего-либо еще - вы не должны отвечать на поведение.