Си (язык программирования)

Си
Семантика: процедурный
Тип исполнения: компилируемый
Появился в: 19691973 г.
Автор(ы): Кен Томпсон, Денис Ритчи
Типизация данных: статическая
Основные реализации: gcc, Microsoft Visual C++, Borland C++ Builder, Watcom
Диалекты: «K&R» C (1978)
ANSI C (1989)
C99 (1999)
Создан под влиянием: не известно
Оказал влияние на: нет или не известно

Си (англ. C) — стандартизованный процедурный язык программирования, разработанный в начале 1970-х годов сотрудниками Bell Labs Кеном Томпсоном и Денисом Ритчи как развитие языка Би. Си был создан для использования в операционной системе UNIX. С тех пор он был портирован на многие другие операционные системы и стал одним из самых используемых языков программирования. Си ценят за его эффективность; он является самым популярным языком для создания системного программного обеспечения. Его также часто используют для создания прикладных программ. Несмотря на то, что Си не разрабатывался для новичков, он активно используется для обучения программированию.

Для языка Си характерны лаконичность, современный набор конструкций управления потоком выполнения, структур данных и обширный набор операций.

Содержание

Особенности

Обзор

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

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

Си (как и ОС UNIX, с которой он долгое время был связан) создавался программистами и для программистов, круг которых был бы ненамного шире круга разработчиков языка. Несмотря на это, область использования языка значительно шире задач системного программирования.

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

Вот некоторые особенности других языков программирования, которых не имеет Си:

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

Одним из последствий высокой эффективности и переносимости Си стало то, что многие компиляторы, интерпретаторы и библиотеки других языков высокого уровня часто выполнены на языке Си.

Программа «Hello, World!»

Эта простая программа, появившаяся в первом издании книги «Язык программирования Си» Кернигана и Ритчи, обычно является первой программой большинства учебников Си. Она печатает сообщение «Hello World!» на стандартном устройстве вывода (которым, как правило, является монитор (дисплей), но может быть и файл, какое-либо устройство или область в памяти, в зависимости от того, как отражается стандартное устройство вывода на данной платформе).

main()
{
    printf("Hello, World!\n");
}

Несмотря на то, что на большинстве современных компиляторов эта программа может быть корректно скомпилирована, она порождает несколько предупреждений на компиляторах стандарта ANSI C. Кроме того, этот код не будет компилироваться, если компилятор жёстко следует стандарту C99, так как в этом случае по умолчанию больше не принимается тип int в качестве возвращаемого значения. Эти сообщения можно убрать, если внести в эту программу несколько небольших изменений:

#include <stdio.h>
int main(void)
{
    printf("Hello, World!\n");
    return 0;
}

В первой строке программы расположена директива препроцессора #include, встретив которую, компилятор заменяет её на полный текст файла, на который она ссылается. В данном случае эта строка будет заменена стандартным заголовочным файлом stdio.h. Угловые скобки указывают компилятору искать файл stdio.h в каталоге стандартных заголовочных файлов.

Следующая строка является объявлением функции с именем main. Эта функция в программе Си является особенной, так как выполняется первой при запуске программы, то есть служит так называемой точкой входа в программу. Фигурные скобки после функции main обозначают её определение. Слово int говорит, что функция main возвращает (вычисляет) целое число. Слово void показывает, что функция main не требует от вызывающего ни параметров, ни аргументов.

Следующая строка «вызывает» или исполняет функцию printf. Включаемый заголовочный файл stdio.h содержит информацию, описывающую то, как нужно вызывать эту функцию. В данном примере этой функции передаётся единственный аргумент, содержащий текстовую строку «Hello, World!\n». Последовательность \n транслируется в символ «новая строка», который при отображении соответственно обозначает разрыв строки. Функция printf вообще возвращает значение типа int, которое в этом примере полностью отбрасывается.

Выражение return заставляет программу прекратить выполнение данной функции (main в этом случае), возвращая вызвавшей функции значение, указанное после ключевого слова return (0 в этом случае). Так как текущая функция — это main, то вызывающим является то, что запустило программу. Последняя фигурная скобка обозначает конец определения функции main.

Комментарии

Текст, заключённый в служебные символы /* и */ в этом порядке, полностью игнорируется компилятором. Компиляторы, совместимые со стандартом C99, также позволяют использовать комментарии, начинающиеся с символов // и заканчивающиеся переводом строки.

Типы

Си имеет ту же систему типов, что и другие потомки Алгола, такие как Паскаль. Существуют типы для целых чисел различных размеров, имеющих знак и не имеющих его, чисел с плавающей запятой, символов, перечисляемых типов (enum) и записей (struct). Кроме того, язык Си имеет тип объединения (union), позволяющий программисту создавать структуры, способные хранить данные разных типов, но только одного типа единовременно.

Хранение данных

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

В Си есть три разных способа выделения памяти для объектов:

  • Статическое выделение памяти: пространство для объектов создаётся в области хранения данных кода программы в момент компиляции; время жизни таких объектов совпадает со временем жизни этого кода.
  • Автоматическое выделение памяти: объекты можно временно хранить в стеке; эта память затем автоматически освобождается и может быть использована снова, после того, как программа выходит из блока, использующего её.
  • Динамическое выделение памяти: блоки памяти нужного размера могут запрашиваться во время выполнения программы с помощью библиотечных функций malloc, realloc и free из области памяти, называемой кучей. Эти блоки освобождаются и могут быть использованы снова после вызова для них функции free.

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

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

Набор используемых символов

Язык Си был создан уже после внедрения стандарта ASCII, поэтому использует почти все его графические символы (нет только $ @ `). Более старые языки вынуждены были обходиться более скромным набором — так, Фортран, Лисп и Кобол использовали только круглые скобки ( ), а в Си есть и круглые ( ), и квадратные [ ], и фигурные { }. Кроме того, в Си различаются заглавные и строчные буквы, а более старые языки использовали только заглавные.

Проблемы

Многие элементы Си потенциально опасны, а последствия неправильного использования этих элементов зачастую непредсказуемы. Керниган говорит: «Си — инструмент, острый, как бритва: с его помощью можно создать и элегантную программу, и кровавое месиво». В связи со сравнительно низким уровнем языка многие случаи неправильного использования опасных элементов не обнаруживаются и не могут быть обнаружены ни при компиляции, ни во время исполнения. Они часто приводит к непредсказуемому поведению программы. Иногда в результате неграмотного использования элементов языка появляются уязвимости в системе безопасности. Необходимо заметить, что использования многих таких элементов можно избежать.

Чаще всего источником ошибки является обращение к несуществующему элементу массива. Несмотря на то, что Си непосредственно поддерживает статические массивы, он не имеет средств проверки индексов массивов (проверки границ). Например, возможна запись в шестой элемент массива из пяти элементов, что, естественно, приведёт к непредсказуемым результатам. Частный случай такой ошибки называтся ошибкой переполнения буфера. Ошибки такого рода приводят к большинству проблем с безопасностью.

Другим потенциальным источником опасных ситуаций служит механизм указателей. Указатель может указывать на абсолютно любой объект в памяти, включая даже и сам машинный код программы, что может приводить к непредсказуемым эффектам. Несмотря на то, что большинство указателей, как правило, указывают на безопасные места, они легко могут быть передвинуты в уже небезопасные области памяти с помощью арифметики указателей; память, на которую они указывают, может быть освобождена и использована по-другому («висячие указатели»); они могут быть не инициализированы («дикие указатели»); или же они просто могут получить любое значение путём приведения типов или присваивания значения другого повреждённого указателя. Другие языки пытаются решить эти проблемы путём использования более ограниченных типов ссылок.

Одна из таких проблем — то, что автоматически и динамически создаваемые объекты не инициализируются, поэтому в начале они имеют такое значение, какое осталось в памяти, выделенной для них, от ранее удалённых объектов. Такое значение полностью непредсказуемо, оно меняется от одной машины к другой, от запуска к запуску, от вызова функции к вызову. Если программа попытается использовать такое неинициализированное значение, то придёт к непредсказуемому результату. Большинство современных компиляторов пытаются обнаружить эту проблему в некоторых случаях.

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

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

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

История

Ранние разработки

Язык программирования Си был разработан в лабораториях Bell Labs в период с 1969 по 1973 год. Согласно Ритчи, самый активный период творчества пришёлся на 1972 год. Язык назвали «Си» (C — третья буква латинского алфавита), потому что многие его особенности берут начало от старого языка «Би» (B — вторая буква латинского алфавита). Существует несколько различных версий происхождения названия языка Би. Кен Томпсон указывает на язык программирования BCPL, однако существует ещё и язык Bon, также созданный им, и названный так в честь его жены Бонни.

Существует несколько легенд, касающихся причин разработки Си и его отношения к операционной системе UNIX, включая следующие:

  • Разработка Си стала результатом того, что его будущие авторы любили компьютерную игру, подобную популярной игре Asteroids (Астероиды). Они уже давно играли в неё на главном сервере компании, который был недостаточно мощным и должен был обслуживать около ста пользователей. Томпсон и Ритчи посчитали, что им не хватает контроля над космическим кораблём для того, чтобы избегать столкновений с некоторыми камнями. Поэтому они решили перенести игру на свободный PDP-7, стоящий в офисе. Однако этот компьютер не имел операционной системы, что заставило их её написать. В конце концов, они решили перенести эту операционную систему ещё и на офисный PDP-11, что было очень тяжело, потому что её код был целиком написан на ассемблере. Было вынесено предложение использовать какой-нибудь высокоуровневый портативный язык, чтобы можно было легко переносить ОС с одного компьютера на другой. Язык Би, который они хотели сначала задействовать для этого, оказался лишён функциональности, способной использовать новые возможности PDP-11. Поэтому они и остановились на разработке языка Си.
  • Самый первый компьютер, для которого была первоначально написана UNIX, предназначался для создания системы автоматического заполнения документов. Первая версия UNIX была написана на ассемблере. Позднее для того, чтобы переписать эту операционную систему, был разработан язык Си.

К 1973 язык Си стал достаточно силён, и большая часть ядра UNIX, первоначально написанная на ассемблере PDP-11/20, была переписана на Си. Это было одно из самых первых ядер операционных систем, написанное на языке, отличном от ассемблера; более ранними были лишь системы Multics (написана на ПЛ/1) и TRIPOS (написана на BCPL).

K&R C

В 1978 году Ритчи и Керниган опубликовали первую редакцию книги «Язык программирования Си». Эта книга, известная среди программистов как «K&R», служила многие годы неформальной спецификацией языка. Версию языка Си, описанную в ней, часто называют «K&R C». (Вторая редакция этой книги посвящена более позднему стандарту ANSI C, описанному ниже.)

K&R ввёл следующие следующие особенности языка:

  • записи (тип данных struct);
  • длинное целое (тип данных long int);
  • целое без знака (тип данных unsigned int);
  • оператор += и подобные ему (старые операторы =+ вводили анализатор лексики компилятора Си в заблуждение, например, при сравнении выражений i =+ 10 и i = +10).

K&R C часто считают самой главной частью языка, которую должен поддерживать компилятор Си. Многие годы даже после выхода ANSI C, он считался минимальным уровнем, которого следовало придерживаться программистам, желающим добиться от своих программ максимальной портативности, потому что не все компиляторы тогда поддерживали ANSI C, а хороший код на K&R C был верен и для ANSI C.

После публикации K&R C в язык было добавлено несколько «неофициальных» возможностей, поддерживаемый компиляторами AT&T и некоторых других производителей:

  • функции, не возвращающие значение (с типом void) и указатели, не имеющие типа (с типом void *);
  • функции, возвращающие объединения и записи;
  • имена полей данных записей в разных пространствах имён для каждой записи;
  • присваивания записей;
  • спецификатор констант (const);
  • стандартная библиотека, реализующая большую часть функций, введённых различными производителями;
  • перечислимый тип;
  • дробное число единичной точности (float).

ANSI C и ISO C

В конце 1970-х годов Си начал вытеснять Бейсик с позиции ведущего языка для программирования микрокомпьютеров. В 1980-х годах он был адаптирован для использования в IBM PC, что привело к резкому росту его популярности. В то же время Бьярне Строуструп и другие в лабораториях Bell Labs начали работу по добавлению в Си возможностей объектно-ориентированного программирования. Язык, который они в итоге сделали, C++, в настоящее время является самым распространённым языком программирования для платформы Microsoft Windows. Си остаётся более популярным в UNIX-подобных системах.

В 1983 году Американский Национальный Институт Стандартизации (ANSI) сформировал комитет для разработки стандартной спецификации Си. По окончании этого долгого и сложного процесса в 1989 году он был наконец утверждён как «Язык программирования Си» ANSI X3.159-1989. Эту версию языка принято называть ANSI C. В 1990 году стандарт ANSI C был принят с небольшими изменениями Международной Организацией по Стандартизации (ISO) как ISO/IEC 9899:1990.

Одной из целей этого стандарта была разработка надмножества K&R C, включающего многие особенности языка, созданные позднее. Однако комитет по стандартизации также включил в него и несколько новых возможностей, таких как прототипы функций (заимствованные из С++) и более сложный препроцессор.

ANSI C сейчас поддерживают почти все существующие компиляторы. Почти весь код Си, написанный в последнее время, соответствует ANSI C. Любая программа, написанная только на стандартном Си, гарантированно будет правильно выполняться на любой платформе, имеющей соответствующую реализацию Си. Однако большинство программ написаны так, что они будут компилироваться только определённым компилятором, потому, что:

  1. они используют нестандартные библиотеки, например, для графических дисплеев;
  2. некоторые компиляторы не придерживаются по умолчанию стандарта ANSI C, или его преемника; или
  3. они рассчитаны на определённое значение размера некоторых типов данных или на определённый способ хранения этих данных в памяти для конкретной платформы.

C99

После стандартизации в ANSI спецификация языка Си оставалась относительно неизменной в течение долгого времени, в то время как C++ продолжал развиваться (в 1995 году в стандарт Си была внесена Первая Нормативная Поправка, но её почти никто не признавал). Однако в конце 1990-х годов стандарт подвергся пересмотру, что привело к публикации ISO 9899:1999 в 1999 году. Этот стандарт обычно называют «С99». В марте 2000 года он был принят и адаптирован ANSI.

Вот некоторые новые особенности С99:

  • подставляемые функции (inline);
  • отсутствие ограничений на объявление локальных переменных (как и в С++);
  • новые типы данных, такие как long long int (для облегчения перехода от 32-х битных к 64-х битным числам), явный булевый тип данных и тип complex для представления комплексных чисел;
  • массивы переменной размерности;
  • поддержка ограниченных указателей (restrict);
  • именованная инициализация структур: struct { int x, y, z; } point = { .y=10, .z=20, .x=30 };
  • поддержка однострочных комментариев, начинающихся на //, заимствованных из С++ (многие компиляторы Си поддерживали их и ранее в качестве дополнения);
  • несколько новых библиотечных функций, таких как snprintf;
  • несколько новых заголовочных файлов, таких как stdint.h.

Интерес к поддержке новых особенностей С99 в настоящее время смешан. В то время как GCC и некоторые другие компиляторы в настоящее время поддерживают большую часть новых особенностей С99, компиляторы компаний Borland и Microsoft не делают этого, причём похоже, что две эти компании и не думают их добавлять.

Связь с С++

Язык программирования С++ произошел от Си. Однако, Си и С++ развивались независимо, что привело к росту несовместимостей между ними. Последняя редакция Си, С99, добавила в язык несколько конфликтующих с С++ особенностей. Эти различия затрудняют написание программ и библиотек, которые могли бы нормально компилироваться и работать одинаково и в компиляторах Си, и в компиляторах С++, что, конечно, запутывает тех, кто программирует на обоих языках.

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

Вот различия между этими языками, существующие на сегодня:

  • inline — подставляемые функции существуют в глобальном пространстве С++, а в Си — в пространстве файла (статическом пространстве). Другими словами, это значит, что в С++ любое определение подставляемой функции (независимо от переопределения функций) должно соответствовать правилу одного определения, требующего того, чтобы любая подставляемая функция была определена только один раз. В Си же одна и та же подставляемая функция может быть определена по-разному в разных компилируемых файлах одной программы.
  • В отличие от С++, ключевое слово bool в С99 требует включения соответствующего заголовочного файла stdbool.h. Предыдущие стандарты Си не определяли булевый тип вообще, поэтому для этого часто использовались различные (а значит, несовместимые) методы.
  • Символьные константы (заключённые в одинарные кавычки) имеют размер как у типа int в Си и как у типа char в С++. Поэтому в Си справедливо равенство sizeof('a') == sizeof(int), а в С++ — равенство sizeof('a') == sizeof(char).
  • Некоторые новые возможности C99, в первую очередь, restrict, не включены в стандарт C++.

Си перенял от С++ ряд особенностей:

  • прототипы объявления функций;
  • однострочные комментарии, начинающиеся на // и заканчивающиеся символом перевода строки;
  • ключевое слово inline;
  • более сильную проверку типов, включая добавление типа void, спецификатора const и удаление принятия по умолчанию типа int в качестве возвращаемого значения.

Ссылки


 
Начальная страница  » 
А Б В Г Д Е Ж З И Й К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ы Э Ю Я
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9 Home