Хранение символа EOF (конец файла) в типе символа

11

Я прочитал в книге Денниса Ритчи « Язык программирования C», которая intдолжна использоваться для переменной, содержащей EOF - чтобы сделать ее достаточно большой, чтобы она могла содержать значение EOF, - нет char. Но следующий код работает нормально:

#include<stdio.h> 

main()  { 
  char c; 
  c=getchar(); 
  while(c!=EOF)  { 
    putchar(c); 
    c=getchar(); 
  } 
} 

Когда больше нет ввода, getcharвозвращает EOF. И в приведенной выше программе переменная cс типом char может успешно его хранить.

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

user1369975
источник
5
Этот код, скорее всего, потерпит неудачу, если вы прочитаете символ со значением 0xff. Сохранение результата getchar()в Ап intрешает эту проблему. Ваш вопрос по сути такой же, как вопрос 12.1 в FAQ по comp.lang.c , который является отличным ресурсом. (Кроме того, main()должно быть int main(void), и это не мешало бы добавить return 0;до закрытия }.)
Кит Томпсон
1
@delnan: связанная статья не совсем верна о том, как Unix относится к control-D. Он не закрывает входной поток; это просто заставляет любой fread (), который блокирует консоль, немедленно возвращаться с любыми пока непрочитанными данными. Многие программы интерпретируют нулевой байт возврата из fread () как указывающий на EOF, но файл фактически останется открытым и сможет предоставить больше входных данных.
суперкат

Ответы:

11

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

getchar()возвращает значение intсо значением, которое соответствует диапазону unsigned charили равно EOF(которое должно быть отрицательным, обычно это -1). Обратите внимание, что EOFсам по себе это не символ, а сигнал о том, что больше нет доступных символов.

При сохранении результата из getchar()in cесть две возможности. Любой тип charможет представлять значение, и в этом случае это значение c. Или тип char не может представлять значение. В этом случае не определено, что произойдет. Процессоры Intel просто отсекают старшие биты, которые не вписываются в новый тип (эффективно уменьшая значение по модулю 256 для char), но вы не должны на это полагаться.

Следующий шаг - сравнить cс EOF. Как EOFи int, cбудет также преобразован intв, сохраняя значение, хранящееся в c. Если cможно сохранить значение EOF, тогда сравнение будет успешным, но если cне удалось сохранить значение, то сравнение не удастся, поскольку при преобразовании EOFв тип произошла безвозвратная потеря информации char.

Кажется, ваш компилятор решил сделать charтип подписанным, а значение EOFдостаточно маленьким, чтобы уместиться char. Если бы они charбыли без знака (или если бы вы использовали unsigned char), ваш тест не прошел бы, потому что unsigned charне может содержать значение EOF.


Также обратите внимание, что есть вторая проблема с вашим кодом. Поскольку EOFэто не сам символ, но вы принудительно вводите его в charтип, существует весьма вероятный символ, который будет неправильно истолкован как существующий, EOFи для половины возможных символов он не определен, если они будут обработаны правильно.

Барт ван Инген Шенау
источник
Принуждение к вводу charзначений за пределами диапазона CHAR_MIN. CHAR_MAXБудет необходимо либо для получения значения, определенного реализацией, либо для битового шаблона, который реализация определяет как представление прерывания, либо для выдачи сигнала, определенного реализацией. В большинстве случаев реализация должна была бы пройти большую дополнительную работу, чтобы сделать что-то кроме сокращения с двумя дополнениями. Если люди в Комитете по стандартам поддержали идею о том, что компиляторам следует поощрять к реализации поведения, согласующегося с поведением большинства других компиляторов, при отсутствии оснований поступать иначе ...
суперкат
... Я хотел бы, чтобы такое принуждение было надежным (не говоря уже о том, что код не должен документировать свои намерения, но это (signed char)xследует считать более ясным и таким же безопасным, как и ((unsigned char)x ^ CHAR_MAX+1))-(CHAR_MAX+1).) Как такового, я не вижу никакой вероятности компиляторы, реализующие любое другое поведение, соответствующее сегодняшнему стандарту; единственная опасность состоит в том, что Стандарт может быть изменен, чтобы нарушить поведение в предполагаемом интересе «оптимизации».
суперкат
@supercat: стандарт написан так, что ни один компилятор не должен создавать код, поведение которого не поддерживается процессором, на который он нацелен. Большая часть неопределенного поведения присутствует, потому что (на момент написания стандарта) не все процессоры вели себя согласованно. С ростом зрелости компиляторов авторы компиляторов начали использовать неопределенное поведение для более агрессивной оптимизации.
Барт ван Инген Шенау
Исторически намерение Стандарта было в основном таким, как вы описали, хотя Стандарт описывает некоторые поведения достаточно подробно, чтобы требовать, чтобы компиляторы для некоторых распространенных платформ генерировали больше кода, чем требовалось бы в более слабой спецификации. Приведение типов в int i=129; signed char c=i;является одним из таких поведения. Относительно немногие процессоры имеют инструкцию, которая делала бы cравной, iкогда она находится в диапазоне от -127 до +127, и давала бы любое непротиворечивое отображение других значений iв значения в диапазоне от -128 до +127, которые отличались от сокращения на два дополнения, или. ..
суперкат
... будет постоянно повышать сигнал в таких случаях. Поскольку Стандарт требует, чтобы реализации либо давали согласованное отображение, либо постоянно повышали сигнал, единственными платформами, на которых Стандарт оставлял бы место для чего-то другого, кроме сокращения на два дополнения, были бы такие вещи, как DSP с аппаратно-арифметическим оборудованием. Что касается исторической основы Undefined Behavior, я бы сказал, что проблема не только в аппаратных платформах. Даже на платформе, где переполнение будет вести себя очень согласованно, может быть полезно, чтобы компилятор перехватил его ...
supercat