Настоящая причина сводится к фундаментальной разнице в намерениях между C и C ++, с одной стороны, и Java и C # (только для пары примеров), с другой. По историческим причинам большая часть обсуждения здесь говорит о C, а не C ++, но (как вы, вероятно, уже знаете) C ++ является довольно прямым потомком C, поэтому то, что он говорит о C, в равной степени относится и к C ++.
Хотя они в значительной степени забыты (а их существование иногда даже отрицается), самые первые версии UNIX были написаны на ассемблере. Большая часть (если не только) первоначальной цели C была переносом UNIX с языка ассемблера на язык более высокого уровня. Часть намерения состояла в том, чтобы написать как можно больше операционной системы на языке более высокого уровня - или смотреть на это с другой стороны, чтобы минимизировать количество, которое должно было быть написано на языке ассемблера.
Для этого C необходимо было обеспечить почти такой же уровень доступа к оборудованию, как и на языке ассемблера. PDP-11 (для одного примера) сопоставляет регистры ввода / вывода с конкретными адресами. Например, вы прочитали одну ячейку памяти, чтобы проверить, была ли нажата клавиша на системной консоли. В этом месте был установлен один бит, когда были данные, ожидающие чтения. Затем вы читаете байт из другого указанного местоположения, чтобы получить код ASCII нажатой клавиши.
Аналогично, если вы хотите распечатать некоторые данные, вы проверите другое указанное местоположение, а когда устройство вывода будет готово, вы запишите свои данные еще в одном указанном месте.
Для поддержки написания драйверов для таких устройств C позволил вам указать произвольное местоположение, используя некоторый целочисленный тип, преобразовать его в указатель и прочитать или записать это местоположение в памяти.
Конечно, это имеет довольно серьезную проблему: не у каждой машины на Земле есть память, идентичная PDP-11 начала 1970-х годов. Таким образом, когда вы берете это целое число, конвертируете его в указатель, а затем читаете или пишете через этот указатель, никто не может дать разумную гарантию того, что вы собираетесь получить. Просто для наглядного примера, чтение и запись могут отображаться в отдельные регистры аппаратного обеспечения, поэтому вы (в отличие от обычной памяти), если вы что-то пишете, затем пытаетесь прочитать это обратно, то, что вы читаете, может не соответствовать тому, что вы написали.
Я вижу несколько возможностей, которые оставляют:
- Определите интерфейс для всех возможных аппаратных средств - укажите абсолютные адреса всех местоположений, которые вы, возможно, захотите прочитать или записать, чтобы каким-либо образом взаимодействовать с оборудованием.
- Запретите этот уровень доступа и постановьте, что любой, кто хочет делать такие вещи, должен использовать язык ассемблера.
- Позвольте людям делать это, но предоставьте им право читать (например) руководства для оборудования, на которое они нацелены, и писать код, соответствующий используемому оборудованию.
Из них 1 кажется достаточно нелепым, что вряд ли стоит его обсуждать. 2 в основном отбрасывает основные намерения языка. Это оставляет третий вариант, по сути, единственным, который они могут разумно рассмотреть вообще.
Другой вопрос, который встречается довольно часто, это размеры целочисленных типов. C занимает «позицию», которая int
должна быть естественного размера, предложенного архитектурой. Таким образом, если я программирую 32-битный VAX, он, int
вероятно , должен быть 32-битным , но если я программирую 36-битный Univac, int
вероятно , должен быть 36 бит (и так далее). Вероятно, нецелесообразно (и может даже не быть возможным) писать операционную систему для 36-битного компьютера, используя только типы, которые гарантированно будут кратны размеру 8 бит. Может быть, я просто поверхностен, но мне кажется, что если бы я писал ОС для 36-битной машины, я бы, вероятно, хотел бы использовать язык, который поддерживает 36-битный тип.
С языковой точки зрения это ведет к еще более неопределенному поведению. Если я возьму наибольшее значение, которое поместится в 32 бита, что произойдет, когда я добавлю 1? На типичном 32-битном оборудовании оно будет переворачиваться (или, возможно, генерировать какую-то аппаратную неисправность). С другой стороны, если он работает на 36-битном оборудовании, он просто ... добавит один. Если язык будет поддерживать написание операционных систем, вы не можете гарантировать ни одно из этих действий - вы просто должны позволить как разным типам, так и поведению переполнения изменяться от одного к другому.
Java и C # могут игнорировать все это. Они не предназначены для поддержки написания операционных систем. С ними у вас есть пара вариантов. Один из них заключается в том, чтобы обеспечить аппаратную поддержку тем, что им требуется - поскольку они требуют типов 8, 16, 32 и 64 бита, просто создайте оборудование, которое поддерживает эти размеры. Другая очевидная возможность заключается в том, что язык может работать только поверх другого программного обеспечения, которое обеспечивает необходимую среду, независимо от того, что может потребоваться базовое оборудование.
В большинстве случаев это на самом деле не выбор. Скорее, многие реализации делают мало того и другого. Обычно вы запускаете Java на JVM, работающей в операционной системе. Чаще всего ОС написана на C, а JVM на C ++. Если JVM работает на процессоре ARM, вполне вероятно, что процессор включает в себя расширения Jazelle ARM, чтобы адаптировать аппаратное обеспечение более близко к потребностям Java, поэтому в программном обеспечении требуется меньше, а код Java работает быстрее (или меньше). все равно медленно)
Резюме
C и C ++ имеют неопределенное поведение, потому что никто не определил приемлемую альтернативу, которая позволяет им делать то, что они должны делать. C # и Java используют другой подход, но этот подход плохо (если вообще) подходит для целей C и C ++. В частности, ни один из них не обеспечивает разумного способа написания системного программного обеспечения (такого как операционная система) на большинстве произвольно выбранных аппаратных средств. Оба, как правило, зависят от возможностей, предоставляемых существующим системным программным обеспечением (обычно написанным на C или C ++), для выполнения своей работы.