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

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск
C
The C Programming Language logo.svg
Класс языка процедурный
Тип исполнения компилируемый
Появился в 1972
Автор Деннис Ритчи, Кен Томпсон
Расширение файлов .c - для файлов кода, .h - для заголовочных файлов
Выпуск
  • C11 (декабрь 2011)
Система типов статическая слабая
Основные реализации: GCC, TCC, Turbo C, Watcom, Oracle Solaris Studio C
Диалекты «K&R» C (1978)
ANSI C (1989)
C90 (1990)
C99 (1999)
C11 (2011)
Испытал влияние BCPL, B
Повлиял на C++, Objective-C, C#, Cyclone, Java, BitC, Nim
ОС кроссплатформенное программное обеспечение
Стандартная библиотека
языка программирования С

Си (англ. C) — компилируемый статически типизированный язык программирования общего назначения, разработанный в 1969—1973 годах сотрудником Bell Labs Деннисом Ритчи как развитие языка Би. Первоначально был разработан для реализации операционной системы UNIX, но впоследствии был перенесён на множество других платформ. Согласно дизайну языка, его конструкции близко сопоставляются типичным машинным инструкциям, благодаря чему он нашёл применение в проектах, для которых был свойственен язык ассемблера, в том числе как в операционных системах, так и в различном прикладном программном обеспечении для множества устройств — от суперкомпьютеров до встраиваемых систем. Язык программирования Си оказал существенное влияние на развитие индустрии программного обеспечения, а его синтаксис стал основой для таких языков программирования, как C++, C#, Java и Objective-C.

Содержание

История[править | править код]

Язык был разработан в лабораториях 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);
  • стандартная библиотека, реализующая большую часть функций, введённых различными производителями;
  • перечислимый тип (enum);
  • дробное число одинарной точности (float).

ISO C[править | править код]

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

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

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

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

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

C99[править | править код]

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

Некоторые новые особенности C99:

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

C11[править | править код]

8 декабря 2011 опубликован новый стандарт для языка Си (ISO/IEC 9899:2011)[2]. Основные изменения:

  • поддержка многопоточности;
  • улучшенная поддержка Юникода;
  • обобщённые макросы (type-generic expressions, позволяют статичную перегрузку);
  • анонимные структуры и объединения (упрощают обращение ко вложенным конструкциям);
  • управление выравниванием объектов;
  • статичные утверждения (static assertions);
  • удаление опасной функции gets (в пользу безопасной gets_s);
  • функция quick_exit;
  • спецификатор функции _Noreturn;
  • новый режим эксклюзивного открытия файла.

Принципы[править | править код]

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

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

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

Язык создавался с целью сделать более простым написание больших программ с минимумом ошибок (по сравнению с ассемблером), следуя принципам процедурного программирования, но избегая всего, что приведёт к дополнительным накладным расходам, обычным для языков высокого уровня.

Си имеет следующие основные особенности:

В то же время в Си отсутствуют:

Часть отсутствующих возможностей может имитироваться встроенными средствами (например, сопрограммы можно имитировать с помощью функций setjmp и longjmp), часть добавляется с помощью сторонних библиотек (например, для поддержки многозадачности и для сетевых функций можно использовать библиотеки pthreads, sockets и тому подобными; существуют библиотеки для поддержки автоматической сборки мусора[3]), часть реализуется в некоторых компиляторах в виде расширений языка (например, вложенные функции в GCC). Существует несколько громоздкая, но вполне работоспособная методика, позволяющая имитировать на Си основные механизмы ООП[4], базирующаяся на фактической полиморфности указателей в Си и поддержке в этом языке указателей на функции.

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

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

Синтаксис[править | править код]

Лексемы[править | править код]

В языке используются (являются допустимыми):

  • все символы латинского алфавита
    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
    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
  • и специальные символы
    , (запятая), ;,. (точка), +, -, *, ^, & (амперсанд), =, ~ (тильда), !, /, <, >, (, ), {, }, [, ], |, %, ?, ' (апостроф), " (кавычки), : (двоеточие), _ (знак подчёркивания)

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

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

Также имеется символ #, который не может быть частью никакой лексемы, и используется в препроцессоре[⇨].

Идентификаторы[править | править код]

Допустимый идентификатор — это слово (лексема), составленное из допустимых символов алфавита языка программирования, не являющееся знаком некоторой операции или разделителем.

Идентификаторы — это те имена, которые даются программным объектам — (именованным) константам, переменным, типам и функциям.

Некоторые лексемы являются зарезервированными (или, что то же самое, ключевыми) словами (см. ниже), и, поэтому, не могут быть использованы в программе в качестве идентификаторов программных объектов.

Предопределённые константы[править | править код]

Для введения в программе на Си именованных констант используется директива препроцессора #define:

#define имя константы [значение].

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

#undef имя константы.

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

Если для именованной константы указано некоторое значение, то для константы определяется так же и тип, соответствующий виду задаваемого значения. Различают следующие типы констант:

  • числовые (целочисленные или вещественные);
  • символьные (выделяются знаком апострофа);
  • текстовые строки (выделяются знаком двойных кавычек).

Ключевые слова[править | править код]

Ключевые слова — это лексемы (слова), которые зарезервированы компилятором для обозначения типов переменных, класса хранения, элементов операторов.

Стандартом С89[⇨] предусмотрены следующие ключевые слова:

  • для указания операции получения размера объекта: sizeof;
  • для описания прототипа объекта: typedef;
  • для объявления переменных:
    для обозначения класса хранения переменных: auto, register;
    для обозначения того, что объект описывается в другом месте extern;
    для обозначения того, что объект статический static;
  • для обозначения типа переменных char, short,int, long, signed, unsigned,float, double, void (для указания на произвольный родовой тип);
  • для обозначения специальных типов данных:
    struct (структура), enum (перечисление), union;
  • для обозначения операторов и их элементов:
    для обозначения операторов цикла: do, for, while;
    для обозначения условного оператора: if, else;
    для обозначения оператора выбора: switch, case, default;
  • для обозначения операторов перехода:
    операторы прерывания исполнения кода: break, continue;
    для обозначения оператора безусловного перехода: goto;
  • для обозначения оператора возврата из функции: return.

В стандарте C99[⇨] добавлены следующие ключевые слова:

  • для обозначения того, что функция является встраиваемой: inline;
  • для обозначения типа переменной: _Bool, _Complex, _Imaginary;
  • для обозначения того, что объявляемый указатель указывает на блок памяти, на который не указывает никакой другой указатель: restrict.

Операции[править | править код]

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

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

  • унарные операции — операции вида
    [знак операции] [операнд]
  • бинарные операции
    [операнд] [знак операции] [операнд]
  • и тернарные операции.

Унарные операции[править | править код]

Унарные операции — это операции, содержащие единственный операнд.

К унарным операциям в Си относятся следующие операции:

+ (унарный плюс), - (унарный минус), ~ (взятие обратного кода), ! (логическое отрицание), & (взятие адреса), * (операция разыменовывания указателя), sizeof (операция определения занимаемого объектом объёма памяти).

Бинарные операции[править | править код]

Бинарные операции — это операции, содержащие два операнда, между которыми расположен знак операции.

К бинарным операциям в Си относятся следующие операции:

+ (сложение), - (вычитание), * (умножение), / (деление), % (взятие остатка от деления), & (поразрядное И), | (поразрядное ИЛИ), ^ (поразрядное исключающее ИЛИ), << (логический сдвиг влево), >> (логический сдвиг вправо), && (логическое И), || (логическое ИЛИ).

Также к бинарным операциям в Си относятся операции, по сути представляющие собою присваивание:

+= (добавление к левому операнду значения, представленного правым операндом);
-= (вычитание из левого операнда значения, представленного правым операндом);
*= (умножение левого операнда на значение, представленное правым операндом);
/= (деление левого операнда на значение, представленное правым операндом);
&= (поразрядное логическое И над левым и правым операндом);
|= (поразрядное логическое ИЛИ над левым и правым аргументом);
^= (поразрядное логическое исключающее ИЛИ над левым и правым аргументом);
<<= (поразрядный сдвиг влево левого аргумента на количество бит, заданное правым аргументом);
>>= (поразрядный сдвиг вправо левого аргумента на количество бит, заданное правым аргументом).

Данные операции предполагают, что левый операнд представляет собою лево-допустимое выражение.

Тернарные операции[править | править код]

В Си имеется единственная тернарная операция — условная операция, которая имеет следующий вид:

[условие]? [выражение1] : [выражение2] ;

и которая имеет три операнда:

  • [условие] — логическое условие, которое проверяется на истинность,
  • [выражение1] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие истинно;
  • [выражение2] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие ложно.

Знаком операции здесь служит целое сочетание ? :.

Выражения[править | править код]

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

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

Среди выражений выделяют класс лево-допустимых выражений — выражений, которые могут присутствовать слева от знака присваивания.

Приоритет выполнения операций[править | править код]

Операции в Си выполняются в соответствии следующей таблице приоритетов операций:

Лексемы Операция Класс Приоритет Ассоциативность
имена, литералы простые лексемы первичный 16 нет
a[k] индексы постфиксный 16 слева направо
f(…) вызов функции постфиксный 16 слева направо
. прямой выбор постфиксный 16 слева направо
-> опосредованный выбор постфиксный 16 слева направо
++ -- положительное и отрицательное приращение постфиксный 16 слева направо
++ -- положительное и отрицательное приращение префиксный 15 справа налево
(имя типа) {init} составной литерал (C99) постфиксный 15 справа налево
sizeof размер унарный 15 справа налево
~ побитовое НЕ унарный 15 справа налево
! логическое НЕ унарный 15 справа налево
- + изменение знака, плюс унарный 15 справа налево
& адрес унарный 15 справа налево
* опосредование (разыменование) унарный 15 справа налево
(имя типа) приведение типа унарный 15 справа налево
* / % мультипликативные операции бинарный 13 слева направо
+ - аддитивные операции бинарный 12 слева направо
<< >> сдвиг влево и вправо бинарный 11 слева направо
< > <= >= отношения бинарный 10 слева направо
== != равенство/неравенство бинарный 9 слева направо
& побитовое И бинарный 8 слева направо
^ побитовое исключающее ИЛИ бинарный 7 слева направо
| побитовое ИЛИ бинарный 6 слева направо
&& логическое И бинарный 5 слева направо
|| логическое ИЛИ бинарный 4 слева направо
? : условие тернарный 3 справа налево
= += -= *= /= %= <<= >>= &= ^= |= присваивание бинарный 2 справа налево
, последовательное вычисление бинарный 1 слева направо

Операторы[править | править код]

Операторы предназначены для осуществления действий и для управления ходом выполнения программы.

Несколько идущих подряд операторов образуют последовательность операторов.

Пустой оператор[править | править код]

Самая простая языковая конструкция — это пустое выражение, называемое пустым оператором[5][6]:

;

Пустой оператор не совершает никаких действий и может находиться в любом месте программы.

Инструкции[править | править код]

Инструкция — это некое элементарное действие:

(выражение);

Действие этого оператора заключается в выполнении указанного в теле оператора выражения.

Несколько идущих подряд инструкций образуют последовательность инструкций.

Блок вычислений[править | править код]

Инструкции могут быть сгруппированы в специальные вычислительные блоки следующего вида:

{

(последовательность инструкций)

},

ограниченные при помощи двух разделителей:

  • левая фигурная скобка ({) обозначает начало вычислительного блока,
  • правая фигурная скобка (}) обозначает конец вычислительного блока.

Вычислительный блок называют ещё составным оператором.

Каждая функция (см. ниже) содержит такой блок, который обозначает тело функции и является частью определения функции.

Условные операторы[править | править код]

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

  • оператор if, содержащий проверку одного условия;
  • и оператор switch, содержащий проверку нескольких условий.

Самая простая форма оператора if

if((условие)) (оператор)
(следующий оператор)

Оператор if работает следующим образом:

  • если выполнено условие, указанное в скобках, то выполняется первый оператор, и затем выполняется оператор, указанный после оператора if.
  • если условие, указанное в скобках, не выполнено, то сразу выполняется оператор, указанный после оператора if.

В частности, следующий ниже код, в случае выполнения заданного условия, не будет выполнять никаких действий:

if((условие)) ;

поскольку, фактически, выполняется пустой оператор. Более сложная форма оператора if содержит ключевое слово else:

if((условие)) (оператор)
else (альтернативный оператор)
(следующий оператор)

Здесь, если условие, указанное в скобках, не выполнено, то выполняется оператор, указанный после ключевого слова else.

Операторы выполнения цикла[править | править код]

Цикл — это фрагмент программного кода, содержащий

  • условие выполнения цикла — условие, которое постоянно проверяется;
  • и тело цикла — простой или составной оператор, выполнение которого зависит от условия цикла.

В соответствии с этим, различают два вида циклов:

  • цикл с предусловием, где сначала проверяется условие выполнения цикла, и, если условие выполнено, то выполняется тело цикла;
  • цикл с постусловием, где сначала выполняется тело цикла, а уже потом проверяется условие выполнения цикла, и, если условие выполнено, то…;

Тело цикла всегда выполняются до тех пор, пока выполняется указанное в цикле условие, таким образом цикл с постусловием гарантирует, что тело цикла обязательно выполнится хотя бы один раз.

В Си предусмотрен оператор цикла с предусловием:

while(условие) [тело цикла]

и оператор с постусловием do-while:

do [тело цикла] while( условие)

Также имеется оператор

for( блок инициализации;условие;оператор) [тело цикла],

который эквивалентен следующему блоку операторов:

[блок инициализации]
while(условие)
{
[тело цикла]
[оператор]
}

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

Условие цикла — это логическое выражение. Однако неявное приведение типов позволяет использовать в качестве условия цикла арифметическое выражение. Это позволяет организовать т. н. «бесконечный цикл»:

while(1);

или то же самое, но уже с применением оператора for:

for(;;);

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

Операторы безусловного перехода[править | править код]

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

Оператор

goto [метка],

где [метка] — это некоторый (числовой) идентификатор, передаёт управление тому оператору, который помечен в программе указанной меткой:

[метка] : [оператор]

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

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

Другие операторы перехода связаны с циклами и позволяют прервать выполнения тела цикла:

  • оператор break немедленно прерывает выполнение тела цикла, и происходит передача управления на оператор, следующий непосредственно сразу за оператором цикла;
  • оператор continue прерывает выполнение тела цикла и передаёт управление в начало цикла, что инициирует проверку условия цикла.

Оператор continue может быть использован только внутри операторов do, while и for; оператор break также может использоваться внутри оператора switch.

Существует два особых случая применения операторов break и continue:

  • если оператор continue встретился в теле оператора for, то сначала происходит выполнение оператора, а уже затем происходит проверка условия цикла, таким образом оператор continue предназначен для немедленного перехода к следующей итерации выполнения цикла;
  • если имеется несколько вложенных циклов, то оператор break , в зависимости от реализации, либо полностью прерывает выполнение всех вложенных циклов, либо прерывает выполнение только того цикла, в котором расположен сам оператор break.

Оператор возврата из функции[править | править код]

В Си определён оператор return, который прерывает выполнение функции, где использован данный оператор. Если функция не должна возвращать значение, то используется вызов

return;

Если функция должна возвращать какое-либо значение, то использует вызов

return[значение];

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

Функции[править | править код]

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

Для того, чтобы задать функцию в Си, необходимо её объявить:

  • сообщить имя (идентификатор) функции,
  • перечислить входные параметры (аргументы)
  • и указать тип возвращаемого значения,

Также необходимо привести определение функции, которое содержит блок операторов, реализующих поведение функции.

Отсутствие определения ранее определённой функции является ошибкой, что, в зависимости от реализации, приводит к выдаче сообщений или предупреждений.

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

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

Объявление функции[править | править код]

Объявление функции имеет следующий формат:

[описатель] [имя] ( [список] );,

где

  • [описатель] — описатель типа возвращаемого функцией значения;
  • [имя] — имя функции (уникальный идентификатор функции);
  • [список] — список (формальных) параметров функции.

Признаком объявления функции является символ «;», таким образом, объявление функции — это инструкция.

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

void

При необходимости в описателе могут присутствовать дополнительные элементы:

  • модификатор extern указывает на то, что определение функции находится в другом модуле;
  • модификатор static задаёт статическую функцию;
  • модификаторы pascal или cdecl влияют на обработку формальных параметров и связаны с подключением внешних модулей.

Список параметров функции задаёт сигнатуру функции.

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

Определение функции[править | править код]

Определение функции имеет следующий формат:

[описатель] [имя] ( [список] ) [тело]

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

Тело функции имеет следующий вид:

{
[последовательность операторов]
return ([возвращаемое значение]) ;
}

Вызов функции[править | править код]

Вызов функции заключается в выполнении следующих действий:

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

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

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

void example_func(int array[]); // array — указатель на первый элемент массива типа int

Си допускает вложенные вызовы. Глубина вложенности вызовов имеет очевидное ограничение, связанное с размером выделяемого программе стека. Поэтому в реализациях Си устанавливается некое предельное значение для глубины вложенности.

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

Возврат из функции[править | править код]

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

Структура программы[править | править код]

Комментарии[править | править код]

Текст программы на Си может содержать фрагменты, которые не являются частью программного кода (комментарии). Комментарии специальным образом помечаются в тексте программы и пропускаются при компиляции.

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

Следующий стандарт (стандарт C99) ввёл ещё один способ оформления комментариев: комментарием считается текст, начинающийся с символа // и заканчивающийся в конце строки.

Типы данных[править | править код]

Примитивные типы[править | править код]

Целые числа[править | править код]

Размер целочисленных типов данных варьируется от не менее 8 до не менее 32 бит. Стандарт C99 увеличивает максимальный размер целого числа — не менее 64 бит. Целочисленные типы данных используются для хранения целых чисел (тип char также используется для хранения ASCII-символов). Все размеры диапазонов представленных ниже типов данных минимальны и на отдельно взятой платформе могут быть больше.[8]

Стандарт не требует, чтобы два разных типа имели разный размер: sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)[9]. Таким образом, даже типы char и long могут иметь одинаковый размер, но такие платформы очень редки. Стандарт гарантирует, что тип char всегда равен 1 байту.

Минимальный диапазон значений целых типов по стандарту определяется с -(2N-1) по 2N-1, где N — разрядность типа. Реализация компиляторов может расширять этот диапазон по своему усмотрению. На практике чаще используется диапазон с -2N по 2N-1. Минимальное и максимальное значения каждого типа указывается в файле limits.h в виде макросов.

Отдельное внимание стоит уделить типу char. Формально это отдельный тип, но фактически char эквивалентен либо signed char, либо unsigned char, в зависимости от компилятора.

Для того, чтобы избежать путаницы между размерами типов стандарт C99 ввел новые типы данных, описанные в файле stdint.h. Среди них такие типы как: intN_t, int_leastN_t, int_fastN_t, где N = 8, 16, 32 или 64. Приставка least- обозначает минимальный тип, способный вместить N бит, приставка fast- обозначает тип размером не менее 16 бит, работа с которым наиболее быстрая на данной платформе. Типы без приставок обозначают типы с фиксированном размером, равным N бит.

Типы с приставками least- и fast- можно считать заменой типам int, short, long, с той лишь разницей, что первые дают программисту выбрать между скоростью и размером.

Тип данных Размер Минимальный диапазон значений Первое появление
signed char минимум 8 бит от −127 (= -(28−1)) до 127 K&R C
unsigned char минимум 8 бит от 0 до 255 (=28−1) K&R C
char минимум 8 бит от −127 до 127 или от 0 до 255 в зависимости от компилятора K&R C
short int минимум 16 бит от −32,767 (= -(215−1)) до 32,767 K&R C
unsigned short int минимум 16 бит от 0 до 65,535 (= 216−1) K&R C
int минимум 16 бит от −32,767 до 32,767 K&R C
unsigned int минимум 16 бит от 0 до 65,535 (= 216−1) K&R C
long int минимум 32 бита от −2,147,483,647 до 2,147,483,647 K&R C
unsigned long int минимум 32 бита от 0 до 4,294,967,295 (= 232−1) K&R C
long long int минимум 64 бита от −9,223,372,036,854,775,807 до 9,223,372,036,854,775,807 C99
unsigned long long int минимум 64 бита от 0 до 18,446,744,073,709,551,615 (= 264−1) C99
int8_t 8 бит от −127 до 127 C99
uint8_t 8 бит от 0 до 255 (=28−1) C99
int16_t 16 бит от −32,767 до 32,767 C99
uint16_t 16 бит от 0 до 65,535 (= 216−1) C99
int32_t 32 бита от −2,147,483,647 до 2,147,483,647 C99
uin32_t 32 бита от 0 до 4,294,967,295 (= 232−1) C99
int64_t 64 бита от −9,223,372,036,854,775,807 до 9,223,372,036,854,775,807 C99
uint64_t 64 бита от 0 до 18,446,744,073,709,551,615 (= 264−1) C99
Типы int_leastN_t, uint_leastN_t, int_fastN_t и uint_fastN_t (N = 8, 16, 32 или 64), введенные стандартом C99,

размером и диапазоном совпадают с соответствующими типами char, short, int и long.

Элементы Си[править | править код]


Хранение данных[править | править код]

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

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

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

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

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

Представление в памяти[править | править код]

Представление в памяти

В данном разделе рассмотрено представление в памяти программы в операционной системе GNU/Linux и архитектуре amd64. Порядок расположения кучи, стека и других областей может отличаться в других архитектурах и операционных системах.

При запуске программы из исполняемого файла в оперативную память импортируются инструкции процессора (машинный код) и инициализированные данные. В то же время в старшие адреса импортируются аргументы командной строки (доступные в функции main со следующей сигнатурой во втором аргументе int argc, char ** argv) и переменные окружения.

В области инициализированных данных хранятся неизменяемые данные. Это могут быть, например, строковые литералы.

Глобальные переменные хранятся в области неинициализированных данных. В процессе запуска эта область изначально инициализируется нулевыми значениями.

Память, выделенная динамически, предоставляется из кучи (heap). Для выделения памяти во время работы программы используется функция malloc из stdlib.h. Для освобождения функция free.

Область стека предназначена для размещения вызовов функций. Каждый раз, как вызывается какая-либо функция, стек увеличивается. После того как функция завершила свою работу стек уменьшается. Функции объявленные с квалификатором inline могут не использовать стек, а подставляться в код (компилятор волен игнорировать inline).

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

Динамически подключаемые библиотеки и отображения файлов с файловой системы находится между стеком и кучей.

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

Примеры на Си[править | править код]

Простейшая программа на Си[править | править код]

Простейшая программа на Си имеет следующий вид:

main() {}

По умолчанию предполагается, что основная функция программы (функция main()) возвращает целое число, поэтому такая программа должна компилироваться (возможно, с выдачей одного или нескольких предупреждений), если компилятор реализует стандарт ANSI C. Если, однако, компилятор следует стандарту C99, то такой код не будет компилироваться, и потребуется явное описание типа возвращаемого функцией main() значения. Допускается не писать оператор return у функции main(). В таком случае, согласно стандарту, функция main() возвращает 0 (включая исполнение всех обработчиков, назначенных на exit()), подразумевая, что программа успешно завершилась[10].

Hello, world![править | править код]

Программа Hello, world! приведена ещё в первом издании книги «Язык программирования Си» Кернигана и Ритчи:

#include <stdio.h>

int
main(void) // Не принимает аргументы.
{  
    printf("Hello, world!\n"); // "\n" - новая строка.
    return 0; // Удачное завершение программы.
}

Эта программа печатает сообщение «Hello, world!» на стандартном устройстве вывода.

Функциональное программирование[править | править код]

Пример программы, которая использует так называемые «функции высшего порядка» и «чистые функции»:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int add_one(const int)                        __attribute__((const));
 5 int subtract_one(const int)                   __attribute__((const));
 6 int modify(const int, int (*f)(const int));
 7 
 8 int add_one(const int num) return num + 1;
 9 int subtract_one(const int num)	return num - 1;
10 int modify(const int num, int (*f)(const int)) return f(num);
11 
12 int main()
13 {
14 	int num_1 = modify(10, *(add_one));
15 	printf("%d\n", num_1);
16 
17 	num_1 = modify(10, *(subtract_one));
18 	printf("%d\n", num_1);
19 	
20 	return EXIT_SUCCESS;
21 }
22 
23 // stdout:
24 // 11
25 // 9

В приведенном выше примере атрибут const указывает компилятору, что возвращаемое функцией значение зависит только от входных параметров и функция не использует глобальные переменные. Более «мягким» аналогом атрибутом const является атрибут pure, который позволяет функции использовать глобальные переменные.[11]

Си и другие языки программирования[править | править код]

Си и C++[править | править код]

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

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

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

  • Интерпретация объявления подставляемых функций. В C++ inline-функции существуют в глобальном пространстве программы, а в Си — в статическом пространстве (пространстве файла). Это значит, что в C++ все определения одной и той же подставляемой функции подчиняются правилу одного определения, требующему их полной идентичности. В Си одна и та же подставляемая функция может быть определена по-разному в разных компилируемых файлах одной программы.
  • Не полностью совместимые логические типы. C++ содержит встроенный логический тип bool. Си до стандарта C99 не имеет логического типа, а логические значения представляются любыми целыми или приводимыми к целым значениями. C99 определяет макрос bool, но его применение требует включения соответствующего заголовочного файла stdbool.h. Также в стандарте C99 определён собственный тип логических данных _Bool.
  • Символьные константы (заключённые в одинарные кавычки) по умолчанию имеют: тип int в Си и тип char в C++. Поэтому в Си справедливо равенство sizeof('a') == sizeof(int), а в C++ — равенство sizeof('a') == sizeof(char).[12]
  • Константа нулевого указателя. В Си в этом качестве традиционно используется макрос NULL, часто определяемый как (*void)0. В C++ первоначально в качестве нулевого указателя использовалась константа 0. Для Си это различие малозначительно, но в C++ появление первого определения NULL (например, из-за включения заголовочного файла Си-программы) может сделать некорректными некоторые выражения. Стандарт C++11 вводит для нулевого указателя новое ключевое слово nullptr.
  • Некоторые новые возможности C99, в первую очередь, restrict, не включены в C++.

Objective-C[править | править код]

Ещё одним вариантом расширения Си объектными средствами является язык Objective-C, созданный в 1983-м году. Объектная подсистема была заимствована из Smalltalk, причём все элементы, связанные с этой подсистемой, реализованы в собственном синтаксисе, достаточно резко отличающемся от синтаксиса Си (вплоть до того, что в описании классов синтаксис объявления полей противоположен синтаксису описания переменных в Си: сначала пишется имя поля, затем его тип). В отличие от C++, Objective-C является надмножеством классического Си, то есть сохраняет совместимость с исходным языком; правильная программа на Си является правильной программой на Objective-C. Другим существенным отличием от идеологии C++ является то, что Objective-C реализует взаимодействие объектов путём обмена полноценными сообщениями, тогда как в C++ реализована концепция «отправка сообщения как вызов метода». Полноценная обработка сообщений является значительно более гибкой, к тому же она естественным образом сочетается с параллельными вычислениями. Objective-C, а также его прямой потомок Swift являются одними из самых популярных на платформах, поддерживаемых Apple.

Java и C#[править | править код]

Говоря о потомках Си, обычно упоминают языки Java и C#. Однако представление о сильной преемственности этих языков по отношению к Си не вполне верно. Они унаследовали лишь основные синтаксические особенности Си (использование фигурных скобок в качестве ограничителей блоков кода, описание переменных, характерные формы операторов for, while, if, switch с параметрами в скобках, характерный синтаксический сахар, такой как операторы ++, --, +=, -= и другие), из-за чего программы на этих языках имеют характерный внешний вид, ассоциирующийся именно с Си. В действительности структура и семантика этих языков сильно отличается от Си, в этом отношении они ближе к таким языкам как Оберон.

Реализация Си[править | править код]

Некоторые компиляторы идут в комплекте с компиляторами других языков программирования (включая C++) или являются составной частью среды разработки программного обеспечения.

Компиляторы на динамические языки и платформы[править | править код]

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

Проблемы и критика[править | править код]

Язык Си уникален с той точки зрения, что именно он стал первым языком высокого уровня, всерьёз потеснившим ассемблер в разработке системного программного обеспечения. Он остаётся языком, реализованным на максимальном количестве аппаратных платформ, и одним из самых популярных языков программирования, особенно в мире свободного программного обеспечения[17]. Тем не менее, язык имеет множество недостатков, он с момента появления подвергается критике многих специалистов.

Общая критика[править | править код]

Не только критики, но и апологеты Си признают, что этот язык весьма сложен и наполнен опасными элементами, которые очень легко использовать неправильно. Своей структурой и правилами он никак не поддерживает программирование, нацеленное на создание надёжного, удобного в сопровождении программного кода, напротив, рождённый в среде хакеров, язык стимулирует соответствующий стиль программирования, часто небезопасный, и поощряющий написание запутанного кода (см. также write-only language[en]). Керниган говорит: «Си — инструмент, острый, как бритва: с его помощью можно создать и элегантную программу, и кровавое месиво». По выражению Алена Голуба[18], Си и Си++ «… дают вам столько гибкости, что что если у вас нет желания и способности призвать себя к порядку, то в итоге вы можете получить гигантский модуль не поддающейся сопровождению тарабарщины, притворяющийся к тому же компьютерной программой. Вы можете поистине делать всё при помощи этих языков, даже если вы этого не хотите».

Многие элементы Си потенциально опасны, а последствия неправильного использования этих элементов зачастую непредсказуемы. В связи со сравнительно низким уровнем языка многие случаи неправильного использования опасных элементов не обнаруживаются и не могут быть обнаружены ни при компиляции, ни во время исполнения (до момента, когда они приведут к явной ошибке). Это часто приводит к непредсказуемому поведению программы. Результатом неграмотного программирования на Си могут стать уязвимости в системе безопасности.

У Си достаточно высокий порог вхождения, что затрудняет его использование в обучении в качестве первого языка программирования. Наконец, за более чем 40 лет существования, язык успел несколько устареть, и в нём достаточно проблематично использовать многие современные приёмы и парадигмы программирования[17].

Недостатки отдельных элементов языка[править | править код]

Отсутствие инициализации переменных по умолчанию[править | править код]

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

Указатели и адресная арифметика[править | править код]

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

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

Массивы[править | править код]

Недостатки реализации доступа к массивам связаны с проблемами адресной арифметики, описанными выше. Си (до стандарта C99) непосредственно поддерживает только статические массивы, размер которых фиксируется на момент компиляции, но при этом он принципиально не имеет средств проверки корректности обращения к элементам массива по индексу (даже в синтаксисе a[i], не говоря уже о варианте обращения *(a + i)). Обращение по индексу, превышающему верхнюю границу массива, приводит к обращению к данным, размещённым в памяти после массива. Частный случай такой ошибки называется ошибкой переполнения буфера. Ошибки такого рода приводят к большинству проблем с безопасностью. Когда подобное обращение происходит ошибочно, оно может привести к непредсказуемому поведению программы, а на некоторых архитектурах — даже к краху операционной системы. Намеренное использование этой особенности может быть хакерским приёмом, используемым для нелегального доступа к памяти другого приложения или памяти ядра операционной системы.

Динамически выделяемая память[править | править код]

В синтаксисе Си нет средств работы с динамически выделяемой памятью, данный механизм вынесен в системную библиотеку. Тем не менее, он входит в стандарт языка. Имеющиеся средства работы с динамической памятью (функции malloc(), calloc(), realloc(), free()) никак не обеспечивают контроля за правильностью и своевременностью выделения и освобождения памяти. Соблюдение правильного порядка работы с динамической памятью полностью возлагается на программиста. Его ошибки, соответственно, приводят к обращению по некорректным («повисшим») ссылкам, когда память освобождается преждевременно, либо к утечке памяти, когда память не освобождена, но указатель на неё утрачен, и память невозможно ни освободить, ни использовать до завершения работы программы. Утечки памяти в циклическом коде приводят к непрерывному росту объёма занимаемой программой памяти и, при достаточно длительной работе, к аварийному завершению из-за исчерпания всей доступной памяти.

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

Функции с переменным числом аргументов[править | править код]

В отличие от обычных функций, имеющих прототип, стандарт не предусматривает механизма определения типов и количества фактически использованных в вызове аргументов, как и механизма безопасного доступа к самим этим аргументам. Сообщение функции числа и типов переданных параметров, как и соответствие данного описания реальности полностью лежит на программисте. Единственный способ доступа к значению произвольных параметров — это отсчитать правильное количество байтов от адреса последнего фиксированного аргумента функции в стеке. Это можно сделать вручную, либо пользуясь набором макросов va_arg из библиотеки <stdarg.h>, но ни тот, ни другой способ не являются безопасными и в случае ошибки в описании, списке параметров или в коде доступа к параметрам внутри самой функции приведут к непредсказуемым последствиям, от чтения неверных данных до разрушения стека.

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

Способы преодоления недостатков языка[править | править код]

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

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

Выпущено значительное количество исследований о правильном программировании на Си, от небольших статей до объёмных книг. Для поддержания качества кода на Си принимаются корпоративные или отраслевые правила и стандарты. Так, известен стандарт MISRA C, первоначально разработанный Motor Industry Software Reliability Association для использования Си в создании программного обеспечения встроенных систем транспортных средств. Сейчас MISRA C используется во многих отраслях, в том числе в военной, медицинской и аэрокосмической. Редакция 2013 года содержит 16 директив и 143 правила, включающие требования к коду и ограничения на использование определённых языковых средств. Так, например, MISRA C запрещает использование функций с переменным числом параметров. На рынке имеется около десятка инструментов проверки кода на соответствие MISRA C и несколько компиляторов Си, проверяющих ограничения этого стандарта на этапе компиляции.

См. также[править | править код]

Примечания[править | править код]

  1. Programming Community Index for April 2012
  2. ISO/IEC 9899:2011 — Information technology — Programming languages — C
  3. A garbage collector for C and C++ (англ.)
  4. Object-Oriented Programming With ANSI-C (англ.)
  5. Подбельский, Фомин, 2004, с. 82.
  6. Романов, Си/Си++, 1.5. Алгоритм. Операторы.
  7. Does C support function overloading? | GeeksforGeeks
  8. The GNU C Reference Manual. www.gnu.org. Проверено 21 мая 2017.
  9. Fundamental types - cppreference.com (англ.). en.cppreference.com. Проверено 21 мая 2017.
  10. Joint Technical Committee ISO/IEC JTC 1. ISO/IEC 9899:201x. Programming languages — C. — ISO/IEC, 2011. — С. 14. — 678 с.
  11. Using the GNU Compiler Collection (GCC): Common Function Attributes. gcc.gnu.org. Проверено 21 мая 2017.
  12. Annex 0: Compatibility. 1.2. C++ and ISO C. Working Paper for Draft Proposed International Standard for Information Systems — Programming Language C++ (2 декабря 1996). — см. 1.2.1p3 (параграф 3 в разделе 1.2.1). Проверено 6 июня 2009. Архивировано 22 августа 2011 года.
  13. Emscripten LLVM-to-JavaScript compiler
  14. Flash C++ Compiler
  15. Проект Clue на сайте SourceForge.net
  16. Axiomatic Solutions Sdn Bhd
  17. 1 2 Столяров, 2010, 1. Предисловие, p. 79.
  18. «Верёвка достаточной длины, чтобы выстрелить себе в ногу. Правила программирования на C и C++»

Литература[править | править код]

  • Керниган Б., Ритчи Д. Язык программирования Си = The C programming language. — 2-е изд. — М.: Вильямс, 2007. — С. 304. — ISBN 0-13-110362-8.
  • Гукин Д. Язык программирования Си для «чайников» = C For Dummies. — М.: Диалектика, 2006. — С. 352. — ISBN 0-7645-7068-4.
  • Подбельский В. В., Фомин С. С. Программирование на языке Си. — 2-е доп. изд. — М.: Финансы и статистика, 2004. — 600 с. — ISBN 5-279-02180-6.
  • Прата С. Язык программирования С: Лекции и упражнения = C Primer Plus. — М.: Вильямс, 2006. — С. 960. — ISBN 5-8459-0986-4.
  • Прата С. Язык программирования C (C11). Лекции и упражнения, 6-е издание = C Primer Plus, 6th Edition. — М.: Вильямс, 2015. — 928 с. — ISBN 978-5-8459-1950-2.
  • Столяров А. В. Язык Си и начальное обучение программированию // Сборник статей молодых учёных факультета ВМК МГУ. — Издательский отдел факультета ВМК МГУ, 2010. — № 7. — С. 78—90.
  • Шилдт Г. C: полное руководство, классическое издание = C: The Complete Reference, 4th Edition. — М.: Вильямс, 2010. — С. 704. — ISBN 978-5-8459-1709-6.
  • Языки программирования Ада, Си, Паскаль = Comparing and Assessong Programming Languages Ada, C, and Pascal / А. Фьюэр, Н. Джехани. — М.: Радио и Саязь, 1989. — 368 с. — 50 000 экз. — ISBN 5-256-00309-7.
  • Axel-Tobias Schreiner. Object oriented programming with ANSI-C. — Hanser, 2011. — 223 p. — ISBN 3-446-17426-5.

Ссылки[править | править код]

  • ISO/IEC JTC1/SC22/WG14 official home (англ.). — Официальная страница международной рабочей группы по стандартизации языка программирования Си. Проверено 20 февраля 2009. Архивировано 22 августа 2011 года.
  • WG14 N1124 (англ.). ISO/IEC 9899 — Programming languages — C — Approved standards. ISO/IEC JTC1/SC22/WG14 (6 мая 2005). — Стандарт ISO/IEC 9899:1999 (C99) + ISO/IEC 9899:1999 Cor. 1:2001(E) (TC1 — Technical Corrigendum 1 от 2001 года) + ISO/IEC 9899:1999 Cor. 2:2004(E) (TC2 — Technical Corrigendum 2 от 2004 года). Проверено 20 февраля 2009. Архивировано 22 августа 2011 года.
  • C — The ISO Standard — Rationale, Revision 5.10 (англ.) (апрель 2004). — Обоснование и пояснения для стандарта C99. Проверено 20 февраля 2009. Архивировано 22 августа 2011 года.