Курилка

История разработки языка C*

Для просмотра страниц с подсветкой синтаксиса разрешите, пожалуйста, исполнение JavaScript.

Источник
Сохранённая копия

См. также:

Деннис М. Ритчи

Bell Labs/Lucent Technologies
Murray Hill, NJ 07974 USA

dmr@bell-labs.com

Аннотация

Язык программирования C был разработан в ранних 1970-х в качестве системного языка для зарождающейся тогда операционной системы Unix. Будучи производным от безтипового языка BCPL, он ввёл систему типов; созданный на слабой машине в качестве инструмента для улучшения скудного программного окружения, он стал на сегодня одним из доминирующих. Эта статья — исследование его эволюции.

Введение

Примечание: * Copyright © 1993 Association for Computing Machinery, Inc. Электронное переиздание (оригинал — прим. пер.) сделана с любезного разрешения автора. Для получения прав на издание обращайтесь в ACM или к автору. Эта статья была представлена на конференции «Second History of Programming Languages», Cambridge, Mass., в апреле 1993 года.

После этого она вошла в состав материалов конференции: History of Programming Languages-II ed. Thomas J. Bergin, Jr. and Richard G. Gibson, Jr. ACM Press (New York) and Addison-Wesley (Reading, Mass), 1996; ISBN 0-201-89502-1.

Д. Ритчи на HOPL-II
Деннис М. Ритчи (слева) и K. N. King на HOPL-II. (Источник)

Эта статья — о разработке языка программирования C, о том что на него повлияло и условиях, в которых он создавался. Для краткости я опускаю описание самого C, его родителя B [Johnson 73] и его прародителя BCPL [Richards 79], а вместо этого сконцентрируюсь на наиболее важных элементах каждого языка и на том, как они развивались.

Язык C появился в 1969–1973 годах параллельно с ранними версиями операционной системы Unix; период наибольшего развития приходится на 1972 год. После этого много изменений было внесено в период с 1977 по 1979 год, когда была продемонстрирована переносимость Unix. В середине этого второго периода стало широко доступным описание языка: The C Programming Language, которое часто называют «Белая книга» или «K&R» [Kernighan 78]. Наконец, в середине 1980-х, язык был официально стандартизирован комитетом ANSI X3J11, который внёс дополнительные изменения. Вплоть до ранних 1980-х, не смотря на то, что компилятор существовал для многих машинных архитектур и операционных систем, язык в основном ассоциировался с Unix; позднее его использование распространилось шире и сегодня он находится среди наиболее часто применяемых в компьютерной индустрии языков.

История: время и место действия

Конец 1960-х — эра бурного развития компьютерных систем в Bell Telephone Laboratories [Ritchie 78] [Ritchie 84]. Компания вышла из проекта Multics [Organick 75], который начинался совместно с MIT, General Electric и Bell Labs; в 1969 году менеджеры и даже научные сотрудники Bell Labs стали считать, что реализация Multics займёт слишком много времени и сил. Но ещё до того, как компьютер GE-645 с Multics был убран из помещения лаборатории, неформальная группа, возглавляемая в основном Кеном Томпсоном, начала альтернативное исследование.

Томпсон хотел создать комфортабельное вычислительное окружение, сконструированное в соответствии с его дизайном, используя любые доступные средства. Его замыслы, что очевидно оглядываясь назад, вбирали в себя многие инновации Multics, включая понятие процесса как основы управления, древовидную файловую систему, интерпретатор команд в качестве пользовательской программы, упрощённое представление текстовых файлов и обобщённый доступ к устройствам. Они включали и другие, такие как унифицированный доступ к памяти и файлам. Кроме того, он и все остальные планировали добавить новаторский (хоть и не оригинальный) элемент Multics — написание большинства кода только на языке высокого уровня. PL/I, на котором была сделана Multics, нам не очень нравился, кроме него мы использовали другие языки, включая BCPL, и нам не хотелось терять преимуществ написания программ на языке более высокого уровня, чем ассемблер, такие как простота и ясность понимания. Тогда мы ещё не сильно заботились о переносимости, интерес к ней возник позже.

Томпсон столкнулся с бедными возможностями оборудования, спартанскими даже по тем временам: DEC PDP-7, на котором он начинал в 1968 году, был компьютером с 8K 18-битных слов памяти и не имел полезного для Кена программного обеспечения. Хотя он и хотел использовать язык высокого уровня, он писал первую систему Unix на ассемблере PDP-7. В начале он программировал даже не на самом PDP-7, а использовал набор макросов для ассемблера GEMAP на GE-635. Постпроцессор пробивал бумажную ленту, которую мог прочитать PDP-7.

Эти ленты переносились для тестирования с GE на PDP-7 до тех пор, пока не были завершены примитивное ядро Unix, редактор, ассемблер, простая оболочка (интерпретатор команд) и несколько утилит (таких как, команды Unix rm, cat, cp). После этого операционная система стала самодостаточной: программы можно было писать и тестировать, не прибегая к бумажной ленте, и разработка продолжилась на самом PDP-7.

Ассемблер PDP-7 Томпсона при всей его простоте обошёл даже ассемблер DEC; он вычислял выражения и испускал соответствующие биты. Не было ни библиотек, ни загрузчика, ни линкера: исходный код программы полностью передавался ассемблеру, а получившийся выходной файл (с фиксированным именем) был исполнимым непосредственно. (Это имя, a.out, немного проясняет этимологию Unix; это — вывод ассемблера. Даже после того, как система обзавелась линкером и способом указать другое имя явно, оно по умолчанию осталось именем исполняемого файла — результата компиляции.)

Спустя немного времени после первого запуска Unix на PDP-7, в 1969 году, Дуг Макилрой создал новый системный язык высокого уровня — реализацию языка TMG Макклура [McClure 65]. TMG — язык для написания компиляторов (более общё, TransMoGrifier-ов) в стиле «сверху-вниз», «рекурсивно-нисходящий», который объединяет контекстно-независимую нотацию синтаксиса с процедурными элементами. Макилрой и Боб Моррис использовали TMG для написания первого компилятора PL/I для Multics.

Создание Макилроем реализации языка TMG привело Томпсона к решению, что Unix — возможно, она тогда ещё и названия не имела — нужен системный язык программирования. После быстро провалившейся попытки с Fortran он создал свой язык, который он назвал B. Язык B можно рассматривать как C без типов; более точно это — BCPL, втиснутый в 8K байт памяти и отфильтрованный мозгом Томпсона. Его название, скорее всего, — сокращение от BCPL, хотя альтернативная теория утверждает, что оно происходит от Bon [Thompson 69], другого языка, созданного Томпсоном в дни работы над Multics. Bon, в свою очередь, был назван либо в честь его жены Бонни, либо (согласно цитате из энциклопедии в руководстве к нему) в честь религии, в которой ритуалы включали в себя проговаривание магических формул.

Источники: языки

BCPL разработал Мартин Ричардс в середине 1960-х во время пребывания в MIT. Язык использовался в начале 1970-х в нескольких интересных проектах, в числе которых — операционная система OS6 в Оксфорде [Stoy 72] и частично в зарождающихся разработках Xerox PARC в Альто [Thacker 79]. Мы познакомились с ним, потому что система MIT CTSS [Corbato 62], с которой работал Ричардс, использовалась при разработке Multics. В Bell Labs Радд Канадей сотоварищи перенесли оригинальный компилятор BCPL и для Multics, и для системы GECOS GE-635 [Canaday 69]; во время последних конвульсий Multics в Bell Labs и сразу после этого он был любимым языком среди людей, которые позже стали заниматься Unix.

BCPL, B и C входят в группу традиционных процедурных языков, таких как Fortran и Algol 60. В частности, они ориентированы на системное программирование, небольшие по размеру и описанию и легко поддаются трансляции простыми компиляторами. Они «близки к машине» в том, что вводимые ими абстракции легко связываются с конкретными типами данных и операциями, предоставляемыми обычными компьютерами, а для ввода-вывода и взаимодействия с системой они используют библиотечные подпрограммы. С меньшим успехом они также используют библиотечные процедуры для создания таких интересных управляющих конструкций, как сопрограммы и замыкания процедур. В тоже время, эти абстракции находятся на достаточно высоком уровне, и при достаточной осторожности может быть достигнута переносимость между различными машинами.

Во многих деталях BCPL, B и C различаются синтаксически, но в основном они похожи. Программы состоят из последовательности глобальных деклараций и объявлений функций (процедур). В BCPL процедуры могут быть вложенными, но не могут ссылаться на нестатические объекты определённые в содержащих их процедурах. B и C избегают такого ограничения, вводя более строгое: вложенных процедур нет вообще. Каждый из языков (за исключением самых древних версий B) поддерживает раздельную компиляцию и предоставляет средства для включения текста из именованных файлов.

Некоторые синтаксические и лексические механизмы BCPL более элегантны и привычны, чем в B и C. Например, объявления процедуры и данных в BCPL более единообразны, и он предоставляет более полный набор циклических конструкций. Хотя программа на BCPL и рассматривается как неразграниченный строками поток символов, умные правила позволяют опустить точку с запятой после выражений, которыми заканчивается строка. B и C не поддерживают такого удобства и требуют точку с запятой после большинства выражений. Несмотря на различия, большинство выражений и операторов BCPL совпадают с соответствующими в B и C.

Некоторые структурные различия между BCPL и B происходят из-за ограничений промежуточной памяти. Например, декларации BCPL могут иметь такую форму:

let P1 be команда
and P2 be команда
and P3 be команда
...

где текст программы представлен командами содержащими всю процедуру. Поддекларации, объединённые при помощи and обрабатываются одновременно, поэтому имя P3 известно внутри процедуры P1. Подобно этому BCPL может упаковывать группу деклараций и выражений в выражение, которое возвращает значение, например

E1 := valof (декларации ; команды ; resultis E2 ) + 1

Компилятор BCPL без труда обработает такие конструкции, сохранив и проанализировав разобранное представление всей программы в памяти до вывода результата. Ограничения по памяти для компилятора B требовали однопроходной техники, когда вывод производится как можно раньше, и модификация синтаксиса, которая делает это возможным, была перенесена в C.

Некоторые менее приятные аспекты BCPL были связаны с его технологическими проблемами и были сознательно отброшены при разработке B. Например, BCPL использует механизм «глобального вектора» для обмена информацией между частями раздельно скомпилированных программ. При этом программист явно связывает имена видимых извне процедур или объектов данных с численным смещением в глобальном векторе; связывание в скомпилированном коде производится при помощи этих численных смещений. Язык B изначально избегал этого неудобства при помощи требования передавать программу компилятору всю полностью. Поздние реализации B и все реализации C используют обычный линкер для разрешения внешних связей в раздельно скомпилированных файлах, а не возлагают бремя назначения смещений на программиста.

Другие детали при переходе с BCPL на B были внесены по соображениям вкуса, а некоторые остались противоречивыми, например, решение использовать одиночный символ = для присвоения вместо :=. Похожим образом B использует /**/ для комментариев, в то время как BCPL использует // для игнорирования текста до конца строки. Здесь очевидно наследие PL/I. (C++ возродил соглашения по комментированию BCPL.) Fortran повлиял на синтаксис деклараций: декларации B начинаются спецификатором наподобие auto или static, за которыми идёт список имён, а C не только следует этому стилю, но и украшает его, помещая в начале декларации ключевые слова их типа.

Здесь рассмотрены не все различия между BCPL, описанном в книге Ричардса [Richards 79], и языком B; мы начинали с более ранней версии BCPL [Richards 67]. Например, конструкции endcase, которая используется для выхода из оператора switchon в BCPL, не было в языке, когда мы изучали его в 1960-х, поэтому применение ключевого слова break для выхода из оператора switch в B и C должно рассматриваться как другая ветвь эволюции, а не преднамеренное изменение.

В противоположность повсеместному изменению синтаксиса, которое происходило во время создания B, основная семантика BCPL — его структура типов и правила вычисления выражений — осталась нетронутой. Оба языка — безтиповые, вернее имеют единственный тип данных — «слово» или «ячейка», набор битов фиксированной длины. Память в этих языках — массив таких ячеек, а смысл содержимого ячейки зависит от операции, которая к ней применяется. Например, оператор + просто складывает свои операнды при помощи машинной инструкции add, и другие арифметические операции также безразличны к смыслу своих операндов. Так как память является линейным массивом, возможно интерпретировать значение в ячейке как индекс в этом массиве, и BCPL предоставляет оператор для этого. В первоначальном варианте он записывался как rv, позже !, а B использует унарную *. Поэтому, если p — ячейка, содержащая индекс (или адрес, или указатель на) другой ячейки, то *p относится к содержимому указываемой ячейки, либо к её значению в выражении, либо как к цели в присвоении.

Так как указатели в BCPL и B являются просто целыми индексами массива памяти, имеют смысл арифметические действия с ними: если p — адрес ячейки, то p+1 будет адресом следующей. Это соглашение является основой семантики массивов в обоих языках. Если в BCPL написать

let V = vec 10

или в B

auto V[10];

то эффект будет одинаковым: под ячейку с именем V будет получена память, затем другая группа из 10 последовательных ячеек будет зарезервирована отдельно, и индекс памяти первой из них будет помещён в V. Согласно общему правилу, в B выражение

*(V+i)

суммирует V и i и ссылается на i-е место после того, на которое ссылается V. Чтобы подсластить такой доступ к массиву, и BCPL, и B добавляют специальную нотацию; в B эквивалентом является

V[i]

а в BCPL

V!i

Этот приём работы с массивами был необычен даже в то время; позже C вобрал это в себя в ещё более необычной форме.

Ни BCPL, ни B, ни C не выделяют в языке символьные данные; они считают строки векторами целых чисел и дополняют общие правила несколькими соглашениями. И в BCPL, и в B строковый литерал означает адрес статической области инициализированный символами строки упакованными в ячейки. В BCPL первый упакованный байт содержит число символов в строке; в B нет счётчика, строки ограничиваются специальным символом, который в B записывается как «*e». Это изменение было сделано частично чтобы избежать ограничений на длину строки из-за хранения счётчика в 8- или 9-битной ячейке, а частично потому, что, по нашему опыту, поддержка счётчика в актуальном состоянии менее удобна, чем ограничитель.

Манипуляции с отдельными символами строки в BCPL обычно производились при помощи распаковки строки в другой массив (по одному символу на ячейку), который затем упаковывался обратно; B предоставлял соответствующий набор действий, но для получения или замены отдельных символов в строке люди чаще использовали сторонние библиотечные функции.

Ещё немного истории

После того, как B стал компилироваться TMG, Томпсон переписал B на нём самом (стадия раскрутки). Во время разработки он постоянно боролся с ограничениями памяти: каждое расширение языка раздувало компилятор, и он с трудом помещался в памяти, но каждая переделка давала преимущество уменьшенного размера в будущем. Например, B ввёл обобщённые операторы присваивания, такие как x=+y для добавления y к x. Эта нотация пришла из Algol-а 68 [Wijngaarden 75] с помощью Макилроя, который включил её в свою версию TMG. (В языке B и раннем C оператор записывался как =+, а не как +=; эта ошибка, исправленная в 1976 году, была вызвана соблазнительно простой для лексического анализатора B обработкой первой формы.)

Томпсон пошёл дальше, введя операторы ++ и --, инкремента и декремента соответственно; их префиксное или постфиксное положение определяет когда, до или после получения значения, происходит изменение операнда. Их не было в самых первых версиях B, они появились в процессе развития. Люди часто предполагают, что они были созданы, чтобы использовать режимы автоинкремента и автодекремента адреса предоставляемые DEC PDP-11, на которых C и Unix впервые стали популярными. Это невозможно по историческим причинам, так как, когда разрабатывался B, PDP-11 ещё не было. Однако PDP-7 имел несколько «автоинкрементных» ячеек памяти с таким свойством, что косвенное обращение к памяти через них увеличивало их значение. Возможно это их свойство и подтолкнуло Томпсона создать такие операторы, но обобщение их в обеих формах, префиксной и постфиксной, было его. На самом деле, автоинкрементные ячейки напрямую не использовались в реализации операторов, а более сильной мотивацией для введения новшества, вероятно, было его наблюдение, что результат трансляции ++x по размеру был меньше, чем x=x+1.

Компилятор B на PDP-7 генерировал не машинные инструкции, а «шитый код» [Bell 72], интерпретируемую схему, в которой вывод компилятора состоит из последовательности адресов фрагментов кода, который производит элементарные операции. Как правило, операторы — в частности, B — выполнялись на простой стековой машине.

В Unix на PDP-7 на языке B были написаны только несколько вещей, кроме самого B, потому что компьютер был слишком маленьким и слишком медленным, чтобы делать на нём что-то большее, чем эксперимент; переписывание операционной системы и утилит полностью на B было слишком дорогостоящим в осуществлении. В какой-то момент Томпсон уменьшил тесноту адресного пространства, введя «виртуальный» компилятор B, который позволял интерпретируемой программе занимать больше, чем 8K байт, путём подкачки кода и данных внутри интерпретатора, но это было слишком медленно, чтобы быть полезным для обычных утилит. Тем не менее, на B были написаны несколько утилит, включая раннюю версию калькулятора с переменной точностью dc, который знаком пользователям Unix [McIlroy 79]. Наиболее амбициозным проектом, которым я занимался, был настоящий кросс-компилятор, который транслировал B для GE-635 в машинные инструкции, а не в шитый код. Это был маленький tour de force (подвиг): полный компилятор B, написанный на своём собственном языке и генерировавший код для 36-битного мэйнфрейма; компилятор работал на 18-битной машине с 4K слов пользовательского адресного пространства. Этот проект был возможным только благодаря простоте языка B и его системе времени исполнения.

Несмотря на то, что мы иногда подумывали о реализации одного из основных языков того времени, таких как Fortran, PL/I или Algol 68, такой проект казался безнадёжно большим для наших ресурсов: разрабатывались гораздо более простые и маленькие инструменты. Все эти языки повлияли на нашу работу, но гораздо интереснее было делать что-то своё.

В 1970 году проект Unix показал себя достаточно многообещающим, и мы смогли обзавестись новым DEC PDP-11. Одним из первых DEC доставила процессор, три месяца спустя прибыл его диск. Для того, чтобы на нём работали программы на B с использованием шитого кода, потребовалось написать только фрагменты кода для операторов и простой ассемблер, который я закодировал на B; вскоре, ещё до какой-либо операционной системы, dc стала первой программой на нашем PDP-11, которую можно было тестировать. Не дожидаясь прибытия диска, Томпсон переписал ядро Unix и некоторые основные команды системы на ассемблере PDP-11. На машине с 24K байтами памяти первая система Unix для PDP-11 использовала 12K байт для операционной системы, маленькое пространство для пользовательских программ, а остальное — в качестве RAM-диска. Эта версия была предназначена только для тестирования, но не для реальной работы; машина использовалась для замеров времени перебора задачи о ходе коня для различных размеров шахматной доски. Как только появился диск, мы быстро мигрировали на него после перевода команд, написанных на ассемблере, на диалект PDP-11 и переноса тех, что уже были написаны на B.

В 1971 году у нашего миниатюрного компьютерного центра начали появляться пользователи. Мы хотели сделать интересующее нас программное обеспечение более простым. Использование ассемблера было достаточно скучным, чтобы B, несмотря на его проблемы с производительностью, получил небольшую библиотеку полезных сервисных подпрограмм и использовался для создания всё большего количества новых программ. Одним из заметных результатов этого периода была первая версия парсера–генератора yacc Стива Джонсона [Johnson 79a].

Проблемы языка B

Машины, на которых мы впервые использовали BCPL, а затем B, были с пословной адресацией, их языки имели единственный тип «ячейка», которая удобно совпадала с аппаратным машинным словом. Появление PDP-11 раскрыло некоторые недостатки семантической модели B. Во-первых, его механизмы обработки символов, с некоторыми изменениями унаследованные из BCPL, были неуклюжи: использование библиотечных процедур для распаковки упакованных строк по индивидуальным ячейкам, а затем переупаковки, или для доступа и замены отдельных символов, на байт-ориентированной машине начали казаться неудобными и даже глупыми.

Во-вторых, хотя первый PDP-11 и не поддерживал арифметику с плавающей точкой, производитель обещал, что скоро она будет доступна. Операции для работы с плавающей точкой были добавлены в наши компиляторы для Multics и GCOS с помощью определения специальных операторов, но такой подход был возможен только потому, что на соответствующих машинах единственное слово было достаточно большим, чтобы содержать число с плавающей точкой; для 16-битного PDP-11 это было не так.

И наконец, модель B и BCPL подразумевала издержки при работе с указателями: правила языка, определяя указатель как индекс в массиве слов, делали указатели индексами слов. Каждое обращение к указателю при исполнении генерировало масштабирование указателя в адрес байта, который ожидал процессор.

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

Кроме проблем с самим языком, шитый код компилятора B делал программы очень медленными по сравнению с похожими, написанными на ассемблере, поэтому мы не рассматривали возможность написания операционной системы и её основных утилит на B.

В 1971 году я начал расширять язык B, добавив символьный тип и переписав компилятор так, чтобы он генерировал машинные инструкции PDP-11 вместо шитого кода. Таким образом, переход с B на C происходил одновременно с созданием компилятора способного производить программы достаточно быстрые и небольшие, чтобы составить конкуренцию языку ассемблера. Я назвал этот, немного расширенный язык, NB, сокращение от «new B».

C в зародыше

Существование NB было настолько скоротечным, что полного описания для него так и не было сделано. Он предлагал типы int и char, их массивы и указатели на них, которые объявлялись в таком стиле:

int    i, j;
char   c, d;
int    iarray[10];
int    ipointer[];
char   carray[10];
char   cpointer[];

Семантика массивов осталась точно такой же как и в B и BCPL: декларации iarray и carray создают ячейки, динамически инициализированные значением указателя на первый элемент из последовательности 10 целых или символов соответственно. Декларации ipointer и cpointer опускают размер, указывая, что память автоматически выделяться не должна. Внутри процедур интерпретация указателей была идентична таким переменным массивов: объявление указателя создавало ячейку отличающуюся от простого объявления массива в том, что программист ожидал присвоения референту (ячейкам, на которые ссылается указатель), вместо того, чтобы позволить компилятору выделить память и инициализировать ячейку её адресом.

Значения, хранившиеся в ячейках связанных с именами массива и указателя, были машинными адресами соответствующей области памяти и измерялись в байтах. Поэтому обращение через указатель не требовало при выполнении накладных расходов, связанных с масштабированием указателя из смещения в словах в смещение в байтах. С другой стороны, машинный код для индексирования массива и арифметика указателей теперь зависели от типа массива или указателя: при вычислении iarray[i] или ipointer+i подразумевалось слагаемое i с размером объекта, на который производилась ссылка.

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

struct {
    int    inumber;
    char   name[14];
};

Я хотел, чтобы структура не только характеризовала абстрактный объект, но и описывала набор бит, который мог быть прочитан из каталога. Где компилятор смог бы спрятать указатель, на name, которого требует семантика? Даже если бы структуры были бы задуманы более абстрактными, и место для указателей могло бы быть спрятано где-нибудь, как бы я решил техническую проблему корректной инициализации этих указателей при выделении памяти для сложного объекта, возможно структуры содержащей массивы, которые содержат структуры, и так до произвольной глубины?

Решение состояло в решительном скачке в эволюционной цепочке между безтиповым BCPL и типизированным C. Он исключал материализацию указателя в хранилище, а вместо этого порождал его создание, когда имя массива упоминалось в выражении. Правило, которое сохранилось и в сегодняшнем C, состоит в том, что значения–массивы, когда они упоминаются в выражении, конвертируются в указатели на первый из объектов, составляющих этот массив.

Это решение позволяло продолжать работать большинству существующего кода на B, несмотря на изменения в основе семантики языка. Немногочисленные программы, которые присваивали новые значения имени массива, чтобы изменить адрес его начала — что возможно в B и BCPL, но не имеет смысла в C — были без труда исправлены. Более важно, что новый язык сохранял целостным и работоспособным (хоть и необычным) толкование семантики массивов, при этом открывая путь более общим структурам.

Второе нововведение, которое наиболее ясно отличает C от его предшественников, — вот эта более полная структура типов и особенно её выразительность в синтаксисе деклараций. NB предлагал основные типы int и char совместно с массивами из них и указателями на них, но никаких других способов скомпоновать их. Требовалось обобщение: для объекта любого типа должно быть возможным описать новый объект, который объединяет несколько таких объектов в массив, получает его из функции или является указателем на него.

Для любого объекта такого составного типа, уже был способ указать на объект, который является его частью: индексировать массив, вызвать функцию, использовать с указателем оператор косвенного обращения. Аналогичное рассуждение приводило к синтаксису объявления имён, который отражает синтаксис выражения, где эти имена используются. Так

int i, *pi, **ppi;

объявляет целое, указатель на целое и указатель на указатель на целое. Синтаксис этих объявлений отражает тот факт, что i, *pi, и **ppi все в результате дают тип int, когда используются в выражении. Похожим образом

int f(), *f(), (*f)();

объявляют функцию, возвращающую целое, функцию возвращающую указатель на целое, указатель на функцию возвращающую целое;

int *api[10], (*pai)[10];

объявляют массив указателей на целое, указатель на массив целых. Во всех этих случаях объявление переменной напоминает её использование в выражении, чей тип это то, что находится в начале объявления.

Схема компоновки типа, принятая в C, обязана в значительной степени Algol 68, хотя она используется, возможно, не в той форме, которую одобрили бы приверженцы Algol. Основной идеей, которую я позаимствовал из Algol-а, была структура типов, построенная на элементарных типах (включая структуры), объединениях в массивы, указателях (ссылках) и функциях (процедурах). Понятия объединения и приведения типа (cast) Algol-а 68 тоже повлияли на то, что появилось позже.

После создания системы типов, соответствующего синтаксиса, и компилятора для нового языка, я понял, что он заслуживает нового имени; NB казалось недостаточно выразительным. Я решил следовать однобуквенному стилю и назвал его C, оставив открытым вопрос что собой представляет это имя, следующую букву в алфавите или в буквах BCPL.

Новорождённый C

Бурные изменения продолжались и после того, язык получил своё имя, например, добавлены операторы && и ||. В языках BCPL и B вычисление выражений зависит от контекста: в if и других условных операторах, которые сравнивают значение выражения с нулём, эти языки используют специальную интерпретацию операторов and (&) и or (|). В обычном контексте они оперируют побитово, но в выражении языка B

if (e1 & e2) ...

компилятор должен вычислить e1 и, если оно не нулевое, вычислить e2, и, если оно тоже не нулевое, рассмотреть выражение, которое зависит от if. Условие рекурсивно спускается на операторы & и | в e1 и e2. В таком контексте казалась желательной семантика булевых операторов «значений истинности», но перегрузка смысла операторов была трудна для объяснения и использования. По предложению Алана Снайдера я ввёл операторы && и ||, чтобы сделать механизм более ясным.

Их запоздалое введение объясняет неудачные правила приоритетов C. В языке B можно написать

if (a==b & c) ...

для проверки, равны ли a и b, и c не ноль; в таком условном выражении лучше было бы, если бы & имел меньший приоритет, чем ==. Конвертируя из B в C, можно заменить в этом выражении & на &&; чтобы сделать конвертирование менее болезненным, мы хотели оставить приоритет оператора & на том же уровне, что и ==, и просто слегка отделили приоритет && от &. Сегодня кажется, что лучше было бы сравнять относительные приоритеты & и == и упростить часто используемую идиому C: чтобы сравнить маскированное значение с другим значением, нужно написать

if ((a&mask) == b) ...

где внутренние скобки нужны, но про них достаточно просто забыть.

Примерно в 1972-3-х годах было сделано много изменений, одно из важнейших — добавление препроцессора, отчасти по просьбе Алана Снайдера [Snyder 74], отчасти потому, что в BCPL и PL/I был механизм включения файлов. Первоначальная версия была очень проста и позволяла только включать файлы и делать простые подстановки строк: макросы без параметров #include и #define. Вскоре после этого, он был расширен, в основном Майком Леском, а потом и Джоном Рейзером, добавлением макросов с аргументами и условной компиляцией. Изначально препроцессор рассматривался как необязательное дополнение к языку. Более того, в течение многих лет он даже не вызывался, если в начале программы не было специального маркера. Это объясняет и неполную интеграцию синтаксиса препроцессора с остальным языком, и неполноту его описания в руководствах, вышедших позднее.

Переносимость

В конце 1973 года были заложены основы современного C. Язык и компилятор были достаточно работоспособны, чтобы позволить нам переписать на нём ядро Unix для PDP-11 в течение лета этого года. (Томпсон сделал короткую попытку закодировать систему на ранней версии C — ещё до появления структур — в 1972 году, но ему это удалось с трудом.) В это же время компилятор был перенесён на другие похожие машины, а конкретно, на Honeywell 635 и IBM 360/370; так как язык не мог жить в изоляции, были разработаны прототипы для современных библиотек. Так, Леск писал «переносимый пакет В/В» [Lesk 73], который позже стал «стандартными подпрограммами ввода/вывода» языка C. В 1978 году Брайен Керниган опубликовал вместе со мной The C Programming Language (Язык программирования C) [Kernighan 78]. Хотя книга и не описывала некоторых дополнений, которые вскоре стали обычными, она служила справочником по языку до выхода стандарта, который был утверждён более, чем десять лет, спустя. Хотя мы и работали над книгой достаточно тесно, было чёткое разделение труда: Керниган написал основной пояснительный материал, а я отвечал за приложение, которое содержало справочное руководство, и за главу с описанием взаимодействия с системой Unix.

В 1973–1980-х годах язык немного подрос: структура типов получила беззнаковые, длинные типы, объединение и перечисление, структуры стали близкими к объектам–классам (не хватало только нотации для литералов). Также важны разработки в его окружении и сопутствующих технологиях. Написание ядра Unix на C дало нам достаточную уверенность в полезности и эффективности языка, и мы смогли начать перекодировать и утилиты, и инструменты, а потом переносить наиболее интересные из них на другие платформы. Как написано в [Johnson 78a], мы обнаружили, что наибольшей проблемой при переносе инструментов Unix, является не взаимодействие языка C с новым аппаратным обеспечением, а с адаптацией существующих программ других операционных систем. Поэтому Стив Джонсон начал работу над pcc, компилятором C, который можно было легко перенести на новые машины [Johnson 78b], одновременно он, Томпсон и я начали переносить систему Unix на компьютер Interdata 8/32.

Изменения в языке в течение этого периода, особенно в 1977 году, в основном были связаны с переносимостью и безопасностью типов, с усилиями справиться с проблемами, которые мы предвидели и наблюдали при переносе значительного количества кода на новую платформу Interdata. В это время C по-прежнему демонстрировал наследие своих безтиповых источников. Например, указатели не сильно отличались от целых индексов памяти и в ранних описаниях языка, и в существующем коде; похожесть арифметических свойств указателей на символы и беззнаковых целых делало трудным отказ от соблазна, чтобы их разделить. Типы unsigned были добавлены, чтобы сделать беззнаковую арифметику доступной без смешивания её с манипуляциями с указателями. Аналогично ранние версии языка позволяли присвоение между целыми и указателями, но такая техника обескураживала; нотация для приведения типа (названная «cast» по примеру Algol-а 68) была введена для более явного приведения типов. Обманутый примером PL/I, ранний C не привязывал жёстко указатели на структуры к структурам, на которые они указывали, и позволял программистам писать pointer->member, не обращая внимания на тип самого pointer; такое выражение применялось для доступа к области памяти, на которую ссылался указатель, так как имя члена — всего лишь смещение и тип.

Несмотря на то, что первое издание K&R описывало большинство правил, которые составляли структуру типов C в современной форме, сохранилось много программ, которые были написаны в старом, менее строгом стиле, что заставляло компиляторы быть к ним терпимыми. Чтобы поощрить людей обращать больше внимания на официальные правила языка, чтобы выявить законные, но сомнительные конструкции, и чтобы помочь обнаружить несоответствия в интерфейсе, которые не определяются простыми механизмами раздельной компиляции, Стив Джонсон сделал из своего компилятора pcc программу lint [Johnson_79b], которая сканирует набор файлов и помечает подозрительные конструкции.

Рост использования

Успех нашего эксперимента по переносу на Interdata 8/32 повёл за собой другой — перенос системы на DEC VAX 11/780 Томом Лондоном и Джоном Рейзером. Эта машина становилась значительно более популярной, чем Interdata, и Unix вместе с языком C начал стремительно распространяться и в AT&T, и вне её. Несмотря на то, что в середине 1970-х Unix использовался в различных проектах внутри Bell System и в небольших исследовательских группах в промышленности, академических и правительственных организациях вне компании, его широкое распространение началось только когда была достигнута его переносимость. Следует особо отметить версии System III и System V, созданные в подразделении компьютерных систем AT&T, основанных на работах групп исследователей и разработчиков компании, и серию релизов BSD университета Калифорнии в Беркли, наследников работ исследовательских организаций Bell Laboratories.

В 1980-х использование языка C широко распространилось, и компиляторы стали доступны почти для любой машинной архитектуры и операционной системы; в частности он стал популярным в качестве инструмента для программирования для персональных компьютеров, и для производителей коммерческого программного обеспечения для них, и для конечных пользователей, которые интересовались программированием. В начале десятилетия почти каждый компилятор основывался на pcc Джонсона; после 1985 появилось много компиляторов, разработанных независимо.

Стандартизация

В 1982 году стало ясно, что C нужна формальная стандартизация. Наиболее близкий к стандарту источник, первое издание K&R, уже не описывал язык в том виде, в каком он использовался; В частности в нём не упоминалось о типах void или enum. Хотя они и предвещались новым подходом к структурам, только опубликование стандарта позволяло языку присваивать им, передавать их в и из функций, надёжно связывать с именами членов структуры или объединения, которые их содержали. Несмотря на то, что компиляторы распространяемые AT&T включали в себя эти изменения, а поставщики большинства компиляторов не основанных на pcc быстро подхватили их, всё ещё не было полного, авторитетного описания языка.

Первое издание K&R также было недостаточно точным во многих деталях языка, и становилось всё более непрактичным полагаться на pcc как на «референсный компилятор»; он не полностью реализовывал язык, описанный в K&R, не говоря уже о последующих расширениях. Наконец, начинающееся использование C в коммерческих и правительственных заказах означало важность решения по принятию стандарта. Поэтому (с подачи М.Д. Макилроя) летом 1983 года ANSI организовала комитет X3J11 под управлением CBEMA с целью создания стандарта C. В конце 1989 года X3J11 произвёл свой доклад [ANSI 89], а затем этот стандарт был принят ISO в качестве ISO/IEC 9899-1990.

С самого начала комитет X3J11 относился с осторожностью и консервативностью к расширениям языка. К моему удовлетворению они отнеслись со всей серьёзностью в постановке своей задачи: «разработать ясный, целостный и недвусмысленный Стандарт языка программирования C, который закрепит существующее определение C и который будет содействовать переносимости пользовательских программ между различными средами языка C». [ANSI 89] Комитет понимал, что просто опубликование стандарта не заставит мир измениться.

X3J11 сделал только одно действительно важное изменение с самом языке: он ввёл типы формальных аргументов в объявлении функции, используя синтаксис заимствованный из C++ [Stroustrup 86]. В старом стиле внешние функции объявлялись примерно так:

double sin();

что говорило только о том, что sin это — функция возвращающая double (т.е. значение с плавающей точкой двойной точности). В новом стиле это делалось лучше:

double sin(double);

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

Кроме того, X3J11 ввёл набор небольших добавлений и уточнений, например, квалификаторы типа const и volatile, и немного другие правила преобразования типов. Тем не менее, процесс стандартизации не изменил характер языка. В частности, стандарт C не пытался формально определять семантику языка, поэтому остались спорные вопросы в некоторых тонкостях; однако, он достаточно хорошо учитывал изменения в использовании со времени первого описания и является достаточно точным по отношению к первой реализации.

Таким образом, в процессе стандартизации ядро языка C осталось почти нетронутым, при этом Стандарт стал более, чем просто аккуратное узаконивание нововведений. Более важные изменения коснулись окружения языка: препроцессора и библиотеки. Препроцессор производит макроподстановки, используя соглашения сильно отличающиеся от остального языка. Его взаимодействие с компилятором никогда не было достаточно хорошо описано, и X3J11 попытался исправить эту ситуацию. Результат получился значительно лучше, чем описание в первом издании K&R; будучи более сложным, он предоставляет операции, такие как объединение лексем, которые были доступны только в некоторых реализациях.

X3J11 правильно полагал, что полное и точное описание стандартной библиотеки C было также важно, как и работа над самим языком. Язык C сам по себе не предоставляет никаких средств ввода-вывода либо других способов для взаимодействия с внешним миром и поэтому зависит от набора стандартных процедур. На момент публикации K&R C задумывался в основном как язык системного программирования для Unix; не смотря на то, что мы давали примеры библиотечных процедур, которые можно было легко перенести на другие операционные системы, неявно подразумевалось, что в основе их поддержки лежит Unix. Поэтому комитет X3J11 потратил большую часть времени на разработку и документирование набора библиотечных процедур, которые должны быть доступны во всех реализациях, соответствующих стандарту.

По правилам процесса стандартизации текущая активность комитета X3J11 была ограничена интерпретацией существующего стандарта. Однако неформальная группа NCEG (Numerical C Extensions Group — Группа численных расширений C) организованная Рексом Жешке была официально признана как подгруппа X3J11.1, и они продолжили разработку расширений C. Как можно понять из названия, целью многих расширений было сделать язык более подходящим для работы с числами: например, многомерные массивы с динамическим определением их границ, добавление удобной обработки арифметики IEEE и создание языка более эффективного на машинах с векторной и другой, более развитой архитектурой. Не все расширения были обязательно численными; они также включали нотацию для литералов структур.

Последователи

У C, и даже у B, есть прямые потомки, хотя и не так много, как у Pascal. Сначала было разработано одно ответвление. Когда Стив Джонсон посещал университет Ватерлоо во время своего академического отпуска, он принёс с собой B. Здесь язык стал популярным на машинах Honeywell и позже породил Eh и Zed (канадский ответ на вопрос «а что же после B?»). Вернувшись в 1973 году в Bell Labs, Джонсон был огорчён тем, что язык, чьи семена он посеял в Канаде, проросли и дома; даже его yacc был переписан Аланом Снайдером на C.

Среди более поздних потомков C — Concurrent C [Gehani 89], Objective C [Cox 86], C* [Thinking 90], и главным образом — C++ [Stroustrup 86]. Язык широко использовался в качестве промежуточного представления (по существу, в качестве переносимого языка ассемблера) для создания разнообразных компиляторов, и для прямых потомков наподобие C++, и для независимых языков, таких как Modula 3 [Nelson 91] и Eiffel [Meyer 88].

Критика

Две идеи наиболее сильно характеризуют C вместе с языками его класса: отношение между массивами и указателями, и схожесть синтаксиса деклараций с синтаксисом выражений. При этом они являются наиболее часто критикуемыми возможностями и часто служат камнем преткновения для начинающего. Оба случая обострены историческими случайностями или ошибками. Наиболее важным является терпимость компиляторов C к ошибкам в типе. Как должно быть ясно из ранее рассказанного, C развился из безтиповых языков. Он не возник внезапно перед своими первыми пользователями и разработчиками как полностью новый язык со своими правилами; напротив, нам нужно было постоянно адаптировать существующие программы по мере разработки языка и поддерживать существующий код. (Позже, после стандартизации комитетом X3J11 ANSI, C столкнулся с такими же проблемами.)

В 1977 году и даже позже компиляторы не выражали недовольства при таком использовании как присвоение между целыми и указателями или использовании объектов неправильного типа для ссылки на члены структур. Хотя определение языка, изложенное в первом издании K&R, было достаточно (хоть и не так полно) последовательным в трактовке правил работы с типами, в книге признавалось, что существующие компиляторы их не навязывают. Более того некоторые правила разрабатывались с целью упрощения переноса, что привело к путанице в дальнейшем. Например, пустые квадратные скобки в декларации функции

int f(a) int a[]; { ... }

являются живым ископаемым, пережитком способа декларирования указателя в NB; a в этом специальном случае интерпретируется в C как указатель. Нотация сохранилась отчасти с целью обеспечения совместимости, отчасти для рационализации, которая позволит программистам показать своим читателям, что в f передаётся указатель сгенерированный из массива, а не ссылка на единственное целое. К сожалению, это помогает также сильно смутить обучающегося, как и привлечь внимание читателя.

В C, описанном в K&R, передача аргументов надлежащего типа при вызове функции была обязанностью программиста, и дошедшие до нас компиляторы не делали проверок соответствия типа. Отсутствие в оригинальном языке включения типа аргумента в сигнатуру функции было заметным недостатком, таким, что потребовало для исправления самого яркого и болезненного нововведения комитета X3J11. Первоначальный дизайн объясняется (если не оправдывается) моим стремлением избежать технологических проблем, особенно при кросс-проверке между раздельно компилируемыми исходными файлами, и моим неполным пониманием последствий при переходе от безтипового к типизированному языку. Программа lint, о которой упоминалось выше, пыталась смягчить проблему: среди прочих своих функций lint проверяла целостность и связность всей программы, сканируя набор исходных файлов и сравнивая типы аргументов функции, которые использовались при вызове, с типами в её определении.

Неудачный синтаксис привёл к созданию мнения о сложности языка. Оператор косвенности, который записывается в C как *, синтаксически является унарным префиксным оператором, точно так же, как в BCPL и B. Это хорошо работает в простых выражениях, но в более сложных случаях требуются скобки для управления синтаксическим анализом. Например, чтобы отличить косвенность значения возвращаемого функцией от вызова функции, на которую ссылается указатель, нужно писать *fp() и (*pf)() соответственно. Стиль, который используется в выражениях, переходит в декларации, поэтому имена могут быть объявлены как

int *fp();
int (*pf)();

В более витиеватом, но, тем не менее, реалистичном случае, всё становится ещё хуже:

int *(*pfp)();

является указателем на функцию, возвращающую указатель на целое. Тут имеют место два момента. Самое важное то, что C имеет относительно богатый набор способов описания типов (по сравнению, скажем, с Pascal). Декларации в языках также выразительных как C — Algol 68, например — описывают объекты равно трудно для понимания просто потому, что сами по себе объекты сложны. Другой момент относится к деталям синтаксиса. Декларации в C должны читаться «изнутри наружу», что многие находят трудным для понимания [Anderson 80]. Сети [Sethi 81] заметил, что многие из вложенных деклараций и выражений стали бы проще, если бы оператор косвенности был бы постфиксным, а не префиксным, но в тот момент было уже слишком поздно что-то менять.

Несмотря на свои трудности, я верю, что подход C к декларациям остаётся приемлемым, и мне с ним удобно; это полезный объединяющий принцип.

Другая характерная особенность C, его трактовка массивов, имеет под собой практическую почву, однако также имеет свои достоинства. Несмотря на то, что родство между указателями и массивами необычно, это доступно для понимания. Более того, язык демонстрирует значительную мощь в описании важных понятий, например, векторов, чья длина меняется во время выполнения, при помощи всего нескольких основных правил и соглашений. В частности, строки символов обрабатываются так же, как любые другие массивы, плюс действует соглашение, что нулевой символ ограничивает строку. Будет интересно сравнить подход C с подходом двух других языков, появившихся примерно в тоже время, Algol 68 и Pascal [Jensen 74]. В Algol 68 массивы либо имеют фиксированные границы, либо являются «гибкими»: чтобы их использовать нужны громоздкие конструкции и в определении языка, и в компиляторе (и не все компиляторы полностью реализуют их). Оригинальный Pascal имел массивы и строки только фиксированного размера, и это показывает его ограниченность [Kernighan 81]. Позже это было частично исправлено, но получившийся язык пока не доступен повсеместно.

Для C строки это — массивы символов, которые по соглашению ограничены маркером. Не считая одного специального правила, касающегося инициализации строки при помощи строкового литерала, семантика строк охватывается более общими правилами, регулирующими работу с массивами, и в результате язык становится проще в описании и трансляции, чем язык, в котором строке отводится отдельный тип данных. За такой подход приходится платить дополнительно: некоторые строковые операции более затратны, чем операции со строками с другим дизайном, потому что коду приложения или библиотечной подпрограмме иногда необходимо искать конец строки, потому что доступно мало встроенных операций, и, потому что бремя управления памятью для строк падает в основном на пользователя. Тем не менее, подход C к работе со строками работает хорошо.

С другой стороны, трактовка массивов в C в общем (не только строки) имеет неприятные последствия и для оптимизации, и для будущих расширений. Распространённость указателей в программах на C, либо объявленных явно, либо образующихся из массивов, означает, что оптимизаторы должны быть осторожными и использовать надёжную технику управления потоком для получения хороших результатов. Хорошие компиляторы понимают, что большинство указателей могут измениться, но некоторое важные применения по-прежнему трудно проанализировать. Например, функции с аргументами–указателями, полученными из переменных–массивов, трудно компилировать в эффективный код на машине с векторной архитектурой, потому что редко представляется возможным определить, что один аргумент–указатель не перекрывает данные, на которые ссылается другой аргумент, или данные доступные извне. Более фундаментально, определение C настолько специфично описывает семантику массивов, что изменения или расширения с целью трактовки массивов как более примитивных объектов и разрешение операций над ними как над единым целым становятся трудными для добавления в существующий язык. Даже расширения с целью разрешить объявление и использование многомерных массивов, размер которых определяется динамически, в целом не являются простыми [MacDonald 89] [Ritchie 90], несмотря на то, это сделало бы намного более простым написание численных библиотек на C. Таким образом, C охватывает наиболее важное использование строк и массивов, которое встречается на практике, единым и простым механизмом, но оставляет проблемы для высокоэффективных реализаций и для расширений.

Конечно, кроме описанных выше погрешностей, в языке существует много не таких заметных. Существует также общие критические замечания, обсуждающие эти подробности. Главная из них то, что язык и его обычное окружение предоставляют мало помощи для написания очень больших систем. Структура именования имеет только два основных уровня, «внешний» (видимый везде) и «внутренний» (внутри отдельной процедуры). Промежуточный уровень видимости (внутри отдельного файла данных и процедурах) слабо привязан к определению языка. Поэтому есть лишь небольшая прямая поддержка модульности, и дизайнеры проектов вынуждены принимать собственные соглашения.

Похожим образом, C сам по себе предоставляет две длительности хранения: «автоматические» объекты, которые существуют, пока управление находится в или ниже процедуры, и «статические», существующие в течение всего времени выполнения программы. Внестековая, динамически выделяемая память предоставляется только библиотечными процедурами, и обязанность по управлению ей возложена на программиста: C не дружит с автоматической сборкой мусора.

Причины успеха

Язык C стал успешным, превзойдя любые первоначальные ожидания. Какие качества привели к его широкому использованию?

Несомненно, успех самой Unix был самым важным фактором; он сделал язык доступным сотням и тысячам людей. И наоборот, использование в Unix языка C и его последовавшая переносимость на разнообразные машины было важным для успеха системы. Но вторжение языка в другие окружения предполагает более фундаментальные достоинства.

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

Также важно, что C и его поддержка центральной библиотеки всегда оставались в контакте с реальной средой. Он не разрабатывался в изоляции для доказательства чего-либо или для того, чтобы служить примером, напротив, он разрабатывался как инструмент для написания программ, которые делали полезные вещи; он всегда подразумевал взаимодействие с большей по масштабам операционной системой, и всегда рассматривался в качестве инструмента для создания более масштабных программ. Скупой, прагматичный подход повлиял на то, что вошло в C: он покрывает только необходимые потребности многих программистов, но не пытается предложить слишком много.

Наконец, несмотря изменения которым он подвергся после первого опубликованного описания, которое конечно было неформальным и неполным, актуальный язык C, как видели миллионы пользователей, которые использовали много различных компиляторов, остался на удивление стабильным и цельным по сравнению с языками распространёнными также широко, например, Pascal и Fortran. Существуют отличающиеся диалекты C — они наиболее заметны между описанным в более старой K&R и новым Стандартным C — но в целом C остался более свободным от проприетарных расширений, чем другие языки. Возможно, наиболее заметным расширением являются квалификаторы указателей «far» и «near», предназначенные для работы с особенностями некоторых процессоров Intel. Несмотря на то, что C изначально не разрабатывался с переносимостью в качестве основной цели, он был успешен в написании на нём программ и даже операционных систем для машин от самых маленьких персональных компьютеров до мощнейших суперкомпьютеров.

Язык C — причудливый, не без недостатков и очень успешный. Хотя и не без помощи исторических случайностей, ясно, что он достаточно эффективно удовлетворил потребность в языке для разработки системы, чтобы вытеснить язык ассемблера, будучи при этом достаточно абстрактным и свободным, чтобы описывать алгоритмы и взаимодействия в разнообразных средах.

Благодарности

Стоит подвести небольшой итог о роли прямых участников создания сегодняшнего языка C. Кен Томпсон (Ken Thompson) в 1969–70 гг. создал язык B; он напрямую произошёл от BCPL Мартина Ричардса (Martin Richards). Деннис Ритчи (Dennis Ritchie) в течение 1971–73 гг. превратил B в C, сохранив большинство синтаксиса B, при этом добавив типы, сделав много других изменений и написав первый компилятор. В течение 1972–1977 гг. Ритчи, Алан Снайдер (Alan Snyder), Стивен Джонсон (Steven C. Johnson), Майкл Леск (Michael Lesk) и Томпсон развивали идеи языка, а переносимый компилятор Джонсона продолжал широко использоваться. Благодаря им и многим другим людям из Bell Laboratories в этот период существенно расширилась коллекция библиотечных процедур. В 1978 году Брайен Керниган (Brian Kernighan) и Ритчи написали книгу, которая стала определением языка на протяжении нескольких лет. Начиная с 1983 года, комитет X3J11 ANSI занимался стандартизацией языка. Особой отметки заслуживают его напряженная работа и его члены: Джим Броуди (Jim Brodie), Том Плам (Tom Plum) и П. Плоугер (P. J. Plauger), а также редакторы проекта Ларри Рослер (Larry Rosler) и Дэйв Проссер (Dave Prosser).

Я благодарен Брайену Кернигану, Дугу Макилрою (Doug McIlroy), Дэйву Проссеру, Питеру Нельсону (Peter Nelson), Робу Пайку (Rob Pike), Кену Томпсону и рефери HOPL за помощь в подготовке этой статьи.

Литература

[ANSI 89]
American National Standards Institute, American National Standard for Information Systems—Programming Language C, X3.159-1989.
[Anderson 80]
B. Anderson, `Type syntax in the language C: an object lesson in syntactic innovation,' SIGPLAN Notices 15 (3), March, 1980, pp. 21-27.
[Bell 72]
J. R. Bell, `Threaded Code,' C. ACM 16 (6), pp. 370-372.
[Canaday 69]
R. H. Canaday and D. M. Ritchie, `Bell Laboratories BCPL,' AT&T Bell Laboratories internal memorandum, May, 1969.
[Corbato 62]
F. J. Corbato, M. Merwin-Dagget, R. C. Daley, `An Experimental Time-sharing System,' AFIPS Conf. Proc. SJCC, 1962, pp. 335-344.
[Cox 86]
B. J. Cox and A. J. Novobilski, Object-Oriented Programming: An Evolutionary Approach, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Gehani 89]
N. H. Gehani and W. D. Roome, Concurrent C, Silicon Press: Summit, NJ, 1989.
[Jensen 74]
K. Jensen and N. Wirth, Pascal User Manual and Report, Springer-Verlag: New York, Heidelberg, Berlin. Second Edition, 1974.
[Johnson 73]
S. C. Johnson and B. W. Kernighan, `The Programming Language B,' Comp. Sci. Tech. Report #8, AT&T Bell Laboratories (January 1973).
[Johnson 78a]
S. C. Johnson and D. M. Ritchie, `Portability of C Programs and the UNIX System,' Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Johnson 78b]
S. C. Johnson, `A Portable Compiler: Theory and Practice,' Proc. 5th ACM POPL Symposium (January 1978).
[Johnson 79a]
S. C. Johnson, `Yet another compiler-compiler,' in Unix Programmer's Manual, Seventh Edition, Vol. 2A, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Johnson 79b]
S. C. Johnson, `Lint, a Program Checker,' in Unix Programmer's Manual, Seventh Edition, Vol. 2B, M. D. McIlroy and B. W. Kernighan, eds. AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Kernighan 78]
B. W. Kernighan and D. M. Ritchie, The C Programming Language, Prentice-Hall: Englewood Cliffs, NJ, 1978. Second edition, 1988.
[Kernighan 81]
B. W. Kernighan, `Why Pascal is not my favorite programming language,' Comp. Sci. Tech. Rep. #100, AT&T Bell Laboratories, 1981.
[Lesk 73]
M. E. Lesk, `A Portable I/O Package,' AT&T Bell Laboratories internal memorandum ca. 1973.
[MacDonald 89]
T. MacDonald, `Arrays of variable length,' J. C Lang. Trans 1 (3), Dec. 1989, pp. 215-233.
[McClure 65]
R. M. McClure, `TMG—A Syntax Directed Compiler,' Proc. 20th ACM National Conf. (1965), pp. 262-274.
[McIlroy 60]
M. D. McIlroy, `Macro Instruction Extensions of Compiler Languages,' C. ACM 3 (4), pp. 214-220.
[McIlroy 79]
M. D. McIlroy and B. W. Kernighan, eds, Unix Programmer's Manual, Seventh Edition, Vol. I, AT&T Bell Laboratories: Murray Hill, NJ, 1979.
[Meyer 88]
B. Meyer, Object-oriented Software Construction, Prentice-Hall: Englewood Cliffs, NJ, 1988.
[Nelson 91]
G. Nelson, Systems Programming with Modula-3, Prentice-Hall: Englewood Cliffs, NJ, 1991.
[Organick 75]
E. I. Organick, The Multics System: An Examination of its Structure, MIT Press: Cambridge, Mass., 1975.
[Richards 67]
M. Richards, `The BCPL Reference Manual,' MIT Project MAC Memorandum M-352, July 1967.
[Richards 79]
M. Richards and C. Whitbey-Strevens, BCPL: The Language and its Compiler, Cambridge Univ. Press: Cambridge, 1979.
[Ritchie 78]
D. M. Ritchie, `UNIX: A Retrospective,' Bell Sys. Tech. J. 57 (6) (part 2), July-Aug, 1978.
[Ritchie 84]
D. M. Ritchie, `The Evolution of the UNIX Time-sharing System,' AT&T Bell Labs. Tech. J. 63 (8) (part 2), Oct. 1984.
[Ritchie 90]
D. M. Ritchie, `Variable-size arrays in C,' J. C Lang. Trans. 2 (2), Sept. 1990, pp. 81-86.
[Sethi 81]
R. Sethi, `Uniform syntax for type expressions and declarators,' Softw. Prac. and Exp. 11 (6), June 1981, pp. 623-628.
[Snyder 74]
A. Snyder, A Portable Compiler for the Language C, MIT: Cambridge, Mass., 1974.
[Stoy 72]
J. E. Stoy and C. Strachey, `OS6—An experimental operating system for a small computer. Part I: General principles and structure,' Comp J. 15, (Aug. 1972), pp. 117-124.
[Stroustrup 86]
B. Stroustrup, The C++ Programming Language, Addison-Wesley: Reading, Mass., 1986. Second edition, 1991.
[Thacker 79]
C. P. Thacker, E. M. McCreight, B. W. Lampson, R. F. Sproull, D. R. Boggs, `Alto: A Personal Computer,' in Computer Structures: Principles and Examples, D. Sieworek, C. G. Bell, A. Newell, McGraw-Hill: New York, 1982.
[Thinking 90]
C* Programming Guide, Thinking Machines Corp.: Cambridge Mass., 1990.
[Thompson 69]
K. Thompson, `Bon—an Interactive Language,' undated AT&T Bell Laboratories internal memorandum (ca. 1969).
[Wijngaarden 75]
A. van Wijngaarden, B. J. Mailloux, J. E. Peck, C. H. Koster, M. Sintzoff, C. Lindsey, L. G. Meertens, R. G. Fisker, `Revised report on the algorithmic language Algol 68,' Acta Informatica 5, pp. 1-236.

Copyright © 2003 Lucent Technologies Inc. All rights reserved. (Оригинал)