Синтаксис и семантика языка Си
Синтаксис определяет то, как должны правильно записываться языковые конструкции, в то время как семантика определяет значения языковых конструкций[1]. Синтаксис языка Си достаточно сложный, а семантика неоднозначная[2]. Основными двумя особенностями языка на момент его появления были унифицирование работы с массивами и указателями, а также схожесть того, как что-либо объявляется, с тем, как это в дальнейшем используется в выражениях[3]. Однако в последующем эти две особенности языка были в числе наиболее критикуемых[3], и обе являются сложными для понимания среди начинающих программистов[4]. Стандарт языка, определяя его семантику, не стал слишком сильно ограничивать реализации языка компиляторами, но этим самым сделал семантику недостаточно определённой. В частности, в стандарте есть 3 типа недостаточно определённой семантики: определяемое реализацией поведение, не заданное стандартом поведение и неопределённое поведение[5].
Лексемы
В языке используются все символы латинского алфавита, цифры и некоторые специальные символы[6].
| Символы латинского алфавита |
|
| Цифры | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 |
| Специальные символы | , (запятая), ;,. (точка), +, -, *, ^, & (амперсанд), =, ~ (тильда), !, /, <, >, (, ), {, }, [, ], |, %, ?, ' (апостроф), " (кавычки), : (двоеточие), _ (знак подчёркивания), \, # |
Из допустимых символов формируются лексемы — предопределённые константы, идентификаторы и знаки операций. В свою очередь, лексемы являются частью выражений; а из выражений составляются инструкции и операторы.
При трансляции программы на Си из программного кода выделяются лексемы максимальной длины, содержащие допустимые символы. Если в программе имеется недопустимый символ, то лексический анализатор (или компилятор) выдаст ошибку, и трансляция программы окажется невозможной.
Символ # не может быть частью никакой лексемы и используется в препроцессоре.
Идентификаторы
Допустимый идентификатор — это слово, в состав которого могут входить символы латинского алфавита, цифры и знак подчёркивания[7]. Идентификаторы даются операторам, константам, переменным, типам и функциям.
В качестве идентификаторов программных объектов не могут использоваться идентификаторы ключевых слов и встроенные идентификаторы. Существуют и зарезервированные идентификаторы, на использование которых компилятор не выдаст ошибок, но которые в будущем могут стать ключевыми словами, что повлечёт за собой несовместимость.
Встроенный идентификатор только один — __func__, который определяется как константная строка, неявно объявляемая в каждой функции и содержащая её название[7].
Литеральные константы
Специально оформленные литералы в Си принято называть константами. Литеральные константы могут быть целочисленными, вещественными, символьными[8] и строковыми[9].
Целые числа по умолчанию задаются в десятичной системе счисления. Если указан префикс 0x, то — в шестнадцатеричной системе. Префикс в виде цифры 0 указывает, что число задаётся в восьмеричной системе. Суффикс определяет минимальный размер типа константы, а также определяет, является ли число знаковым или беззнаковым. В качестве итогового типа берётся такой минимально возможный, в котором данную константу можно представить[10].
| Суффикс | Для десятичных | Для восьмеричных и шестнадцатеричных |
|---|---|---|
| Нет | int
|
int
|
u или U |
unsigned int
|
unsigned int
|
l или L |
long
|
long
|
u или U вместе с l или L |
unsigned long
|
unsigned long
|
ll или LL |
long long |
long long
|
u или U вместе с ll или LL |
unsigned long long |
unsigned long long |
| Десятичный
формат |
С экспонентой | Шестнадцатеричный
формат |
|---|---|---|
1.5 |
1.5e+0 |
0x1.8p+0 |
15e-1 |
0x3.0p-1 | |
0.15e+1 |
0x0.cp+1 |
Константы вещественных чисел по умолчанию имеют тип double. При указании суффикса f константе назначается тип float, а при указании l или L — long double. Константа будет считаться вещественной, если в ней присутствует знак точки, либо буквы p или P в случае шестнадцатеричной записи с префиксом 0x. Десятичная запись может включать экспоненту, указываемую после букв e или E. В случае шестнадцатеричной записи экспонента указывается после букв p или P в обязательном порядке, что отличает вещественные шестнадцатеричные константы от целых. В шестнадцатеричном виде экспонента является степенью числа 2[11].
Символьные константы заключаются в одинарные кавычки ('), а префикс задаёт как тип данных символьной константы, так и кодировку, в которой символ будет представлен. В Си символьная константа без префикса имеет тип int[12], в отличие от C++, в котором символьной константе соответствует char.
| Префикс | Тип данных | Кодировка |
|---|---|---|
| Нет | int |
ASCII |
u |
char16_t |
Кодировка 16-битных многобайтовых строк |
U |
char32_t |
Кодировка 32-битных многобайтовых строк |
L |
wchar_t |
Кодировка широких строк |
Строковые литералы заключаются в двойные кавычки и могут иметь префикс, определяющий тип данных строки и её кодировку. Строковые литералы представляют собой обычные массивы. При этом в многобайтовых кодировках, таких как UTF-8, один символ может занимать более одного элемента массива. По факту строковые литералы являются константными[13], но в отличие от C++ их типы данных не содержат модификатор const.
| Префикс | Тип данных | Кодировка |
|---|---|---|
| Нет | char * |
ASCII или многобайтовая кодировка |
u8 |
char * |
UTF-8 |
u |
char16_t * |
16-битная многобайтовая кодировка |
U |
char32_t * |
32-битная многобайтовая кодировка |
L |
wchar_t * |
Кодировка широких строк |
Несколько подряд идущих строковых констант, разделённых пробельными символами или переводами строк объединяются в одну строку при компиляции, что часто используется для оформления кода строки путём разделения частей строковой константы по разным строкам для повышения читабельности[15].
Именованные константы
| Макрос | # define BUFFER_SIZE 1024
|
| Анонимное перечисление |
enum {
BUFFER_SIZE = 1024
};
|
| Переменная в роли константы |
const int
buffer_size = 1024;
extern const int
buffer_size;
|
В языке Си для задания констант принято использовать макроопределения, объявляемые с помощью директивы препроцессора #define[16]:
#defineимя константы [значение]
Введённая таким образом константа будет действовать в области своей видимости, начиная с момента задания константы и до конца программного кода или до тех пор, пока действие заданной константы не будет отменено директивой #undef:
#undefимя константы
Как и для всякого макроса, для именованной константы происходит автоматическая подстановка значения константы в программном коде всюду, где употреблено имя константы. Поэтому при объявлении внутри макроса целых или вещественных чисел может понадобиться явно указывать тип данных с помощью соответствующего суффикса литерала, иначе число по умолчанию будет иметь тип int в случае целого или тип double — в случае вещественного.
Для целых чисел существует другой способ создания именованных констант — через перечисления оператора enum[16]. Однако данный метод подходит только для типов, размером меньших либо равных типу int, и не используется в стандартной библиотеке[17].
Также можно создавать константы в виде переменных с квалификатором const, но в отличие от двух других способов, такие константы потребляют память, на них можно получить указатель, и их нельзя использовать на этапе компиляции[16]:
- для указания размера битовых полей,
- для задания размера массива (за исключением массивов переменной длины),
- для задания значения элемента перечисления,
- в качестве значения оператора
case.
Ключевые слова
Ключевые слова — это идентификаторы, предназначенные для выполнения той или иной задачи на этапе компиляции, либо для подсказок и указаний компилятору.
| Ключевые слова | Назначение | Стандарт |
|---|---|---|
sizeof |
Получение размера объекта на этапе компиляции | C89 |
typedef |
Задание альтернативного имени типу | |
auto, register |
Подсказки компилятору по месту хранения переменных | |
extern |
Указание компилятору искать объект вне текущего файла | |
static |
Объявление статического объекта | |
void |
Маркер отсутствия значения; в указателях означает произвольные данные | |
char, short,int, long |
Целочисленные типы и модификаторы их размера | |
signed, unsigned |
Модификаторы целочисленных типов, определяющие их как знаковые или беззнаковые | |
float, double |
Вещественные типы данных | |
const |
Модификатор типа данных, указывающий компилятору, что переменные этого типа доступны только для чтения | |
volatile |
Указание компилятору на возможность изменения значения переменной извне | |
struct |
Тип данных в виде структуры с набором полей | |
enum |
Тип данных, хранящий одно из набора целочисленных значений | |
union |
Тип данных, в котором можно хранить данные в представлениях разных типов данных | |
do, for, while |
Операторы цикла | |
if, else |
Условный оператор | |
switch, case, default |
Оператор выбора по целочисленному параметру | |
break, continue |
Операторы прерывания цикла | |
goto |
Оператор безусловного перехода | |
return |
Возврат из функции | |
inline |
Объявление встраиваемой функции | C99[19] |
restrict |
Объявление указателя, который ссылается на блок памяти, на который не ссылается никакой другой указатель | |
_Bool[a] |
Булев тип данных | |
_Complex[b], _Imaginary[c] |
Типы, используемые для вычислений с комплексными числами | |
_Atomic |
Модификатор типа, делающий его атомарным | C11 |
_Alignas[d] |
Явное задание выравнивания в байтах для типа данных | |
_Alignof[e] |
Получение выравнивания для заданного типа данных на этапе компиляции | |
_Generic |
Выбор одного из набора значений на этапе компиляции, исходя из контролируемого типа данных | |
_Noreturn[f] |
Указание компилятору, что функция не может завершаться нормальным образом (то есть по return) | |
_Static_assert[g] |
Указание утверждений, проверяемых на этапе компиляции | |
_Thread_local[h] |
Объявление локальной для потока переменной |
Зарезервированные идентификаторы
Помимо ключевых слов стандарт языка определяет зарезервированные идентификаторы, использование которых может привести к несовместимости с будущими версиями стандарта. Зарезервированными являются все, за исключением ключевых, слова, начинающиеся со знака подчёркивания (_), после которого идёт либо заглавная буква (A—Z), либо другой знак подчёркивания[20]. В стандартах С99 и С11 часть таких идентификаторов была использована под новые ключевые слова языка.
В области видимости файла зарезервировано использование любых имён, начинающихся со знака подчёркивания (_)[20], то есть со знака подчёркивания допускается именовать типы, константы и переменные, объявленные в рамках какого-либо блока инструкций, например, внутри функций.
Также зарезервированными идентификаторами являются все макросы стандартной библиотеки и связываемые на этапе линковки названия из неё[20].
Использование зарезервированных идентификаторов в программах стандарт определяет как неопределённое поведение. Попытка отмены любого стандартного макроса через #undef также повлечёт за собой неопределённое поведение[20].
Комментарии
Текст программы на Си может содержать фрагменты, которые не являются частью программного кода, — комментарии. Комментарии специальным образом помечаются в тексте программы и пропускаются при компиляции.
Первоначально, в стандарте C89, были доступны встраиваемые комментарии, которые могли помещаться между последовательностями символов /* и */. При этом невозможно вложить один комментарий в другой, поскольку первая встреченная последовательность */ завершит комментарий, а текст, следующий непосредственно за обозначением */, будет воспринят компилятором как исходный текст программы.
Следующий стандарт, C99, ввёл ещё один способ оформления комментариев: комментарием считается текст, начинающийся с последовательности символов // и заканчивающийся концом строки[19].
Комментарии часто используются для самодокументирования исходного кода, поясняя работу сложных частей, описывая назначение тех или иных файлов, а также описывая правила использования и работу тех или иных функций, макросов, типов данных и переменных. Существуют постпроцессоры, которые умеют преобразовывать специально оформленные комментарии в документацию. Среди таких постпроцессоров с языком Си умеет работать система документирования Doxygen.
Операторы
Операторы, применяемые в выражениях, представляют собой некоторую операцию, которая выполняется над операндами и которая возвращает вычисленное значение — результат выполнения операции. В качестве операнда может выступать константа, переменная, выражение или вызов функции. Оператор может представлять собой специальный символ, набор специальных символов или служебное слово. Операторы различают по количеству задействованных операндов, а именно — различают унарные операторы, бинарные операторы и тернарные операторы.
Унарные операторы
Унарные операторы выполняют операцию над единственным аргументом и имеют следующий формат операции:
- [оператор] [операнд]
Операции постфиксного инкремента и декремента имеют обратный формат:
- [операнд] [оператор]
+ |
Унарный плюс | ~ |
Взятие обратного кода | & |
Взятие адреса | ++ |
Префиксный или постфиксный инкремент | sizeof |
Получение количества байт, занимаемого объектом в памяти; может использоваться и как операция, и как оператор |
- |
Унарный минус | ! |
логическое отрицание | * |
Разыменовывание указателя | -- |
Префиксный или постфиксный декремент | _Alignof |
Получение выравнивания для заданного типа данных |
Операторы инкремента и декремента, в отличие от остальных унарных операторов, изменяют значение своего операнда. Префиксный оператор сначала изменяет значение, а затем возвращает его. Постфиксный же сначала возвращает значение, а только потом его изменяет.
Бинарные операторы
Бинарные операторы располагаются между двумя аргументами и осуществляют операцию над ними:
- [операнд] [оператор] [операнд]
+ |
Сложение | % |
Взятие остатка от деления | << |
Поразрядный сдвиг влево | > |
Больше | == |
Равно |
- |
Вычитание | & |
Поразрядное И | >> |
Поразрядный сдвиг вправо | < |
Меньше | != |
Не равно |
* |
Умножение | | |
Поразрядное ИЛИ | && |
Логическое И | >= |
Больше либо равно | , | Последовательное вычисление |
/ |
Деление | ^ |
Поразрядное исключающее ИЛИ | || |
Логическое ИЛИ | <= |
Меньше либо равно |
Также к бинарным операторам в Си относятся лево-присваивающие операторы, которые производят операцию над левым и правым аргументом и заносят результат в левый аргумент.
= |
Присвоение значения правого аргумента левому | %= |
Остаток от деления левого операнда на правый | ^= |
Поразрядное исключающее ИЛИ правого операнда к левому |
+= |
Прибавление к левому операнду правого | /= |
Деление левого операнда на правый | <<= |
Поразрядный сдвиг левого операнда влево на количество бит, заданное правым операндом |
-= |
Вычитание из левого операнда правого | &= |
Поразрядное И правого операнда к левому | >>= |
Поразрядный сдвиг левого операнда вправо на количество бит, заданное правым операндом |
*= |
Умножение левого операнда на правый | |= |
Порязрядное ИЛИ правого операнда к левому | ||
Тернарные операторы
В Си имеется единственный тернарный оператор — сокращённый условный оператор, который имеет следующий вид:
- [условие]
?[выражение1]:[выражение2]
Сокращённый условный оператор имеет три операнда:
- [условие] — логическое условие, которое проверяется на истинность,
- [выражение1] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие истинно;
- [выражение2] — выражение, значение которого возвращается в качестве результата выполнения операции, если условие ложно.
Оператором в данном случае является сочетание знаков ? и :.
Выражения
Выражение — это упорядоченный набор операций над константами, переменными и функциями. Выражения содержат операции, состоящие из операндов и операторов. Порядок выполнения операций зависит от формы записи и от приоритета выполнения операций. У каждого выражения имеется значение — результат выполнения всех операций, входящих в выражение. В ходе вычисления выражения в зависимости от операций могут изменяться значения переменных, а также могут исполняться функции, если их вызовы присутствуют в выражении.
Среди выражений выделяют класс лево-допустимых выражений — выражений, которые могут присутствовать слева от знака присваивания.
Приоритет выполнения операций
Приоритет операций определяется стандартом и задаёт порядок, в котором операции будут производиться. Операции в Си выполняются в соответствии приведённой ниже таблице приоритетов[24][25].
| Приоритет | Лексемы | Операция | Класс | Ассоциативность |
|---|---|---|---|---|
| 1 | a[индекс] | Обращение по индексу | постфиксный | слева направо → |
f(аргументы) | Вызов функции | |||
. | Доступ к полю | |||
-> | Доступ к полю по указателю | |||
++ -- | Положительное и отрицательное приращение | |||
(имя типа) {инициализатор} | Составной литерал (C99) | |||
(имя типа) {инициализатор,} | ||||
| 2 | ++ -- |
Положительное и отрицательное префиксные приращения | унарный | ← справа налево |
sizeof | Получение размера | |||
_Alignof[e] |
Получение выравнивания (C11) | |||
~ | Побитовое НЕ | |||
! | Логическое НЕ | |||
- + | Указание знака (минус или плюс) | |||
& | Получение адреса | |||
* | Обращение по указателю (разыменовывание) | |||
(имя типа) | Приведение типа | |||
| 3 | * / % | Умножение, деление и получение остатка | бинарный | слева направо → |
| 4 | + - | Сложение и вычитание | ||
| 5 | << >> | Сдвиг влево и вправо | ||
| 6 | < > <= >= | Операции сравнения | ||
| 7 | == != | Проверка на равенство или неравенство | ||
| 8 | & | Побитовое И | ||
| 9 | ^ | Побитовое исключающее ИЛИ | ||
| 10 | | | Побитовое ИЛИ | ||
| 11 | && | Логическое И | ||
| 12 | || | Логическое ИЛИ | ||
| 13 | ? : | Условие | тернарный | ← справа налево |
| 14 | = | Присвоение значения | бинарный | |
+= -= *= /= %= <<= >>= &= ^= |= |
Операции изменения левого значения | |||
| 15 | , | Последовательное вычисление | слева направо → |
Приоритеты операций в Си не всегда себя оправдывают и иногда приводят к интуитивно трудно предсказуемым результатам. Например, поскольку унарные операторы имеют ассоциативность справа налево, то вычисление выражения *p++ приведёт к увеличению указателя с последующим разыменовыванием (*(p++)), а не к увеличению значения по указателю ((*p)++). Поэтому в случае сложных для понимания ситуаций рекомендуется явно группировать выражения с помощью скобок[25].
Другой важной особенностью языка Си является то, что вычисление значений аргументов, передаваемых в вызов функции не является последовательным[26], то есть запятая, разделяющая аргументы, не соответствует последовательному вычислению из таблицы приоритетов. В следующем примере вызовы функций, указываемые в качестве аргументов другой функции, могут идти в произвольном порядке:
int x;
x = compute(get_arg1(), get_arg2()); // первым может быть вызов get_arg2()
Также нельзя полагаться на приоритет операций в случае наличия побочных эффектов, появляющихся в ходе вычисления выражения, поскольку это будет приводить к неопределённому поведению[26].
Точки следования и побочные эффекты
Приложение C стандарта языка определяет набор точек следования, в которых гарантируется отсутствие текущих побочных эффектов от вычислений. То есть точка следования — это этап вычислений, который разделяет вычисление выражений между собой так, что произошедшие до точки следования вычисления, включая побочные эффекты, уже закончились, а после точки следования — ещё не начинались[27]. Побочным эффектом может быть изменение значения переменной в ходе вычисления выражения. Изменение значения, участвующего в вычислениях, вместе с побочным изменением этого же значения до следующей точки следования будет приводить к неопределённому поведению. То же самое будет, если происходит два или более побочных изменений одного и того же значения, участвующего в вычислениях[26].
| Точка следования | Событие до | Событие после |
|---|---|---|
| Вызов функции | Вычисление указателя на функцию и её аргументов | Вызов функции |
Операторы логического И (&&), ИЛИ (||) и последовательное вычисление (,) |
Вычисление первого операнда | Вычисление второго операнда |
Сокращённый оператор условия (?:) |
Вычисление операнда, выступающего условием | Вычисление 2-го или 3-го операндов |
| Между двумя полными выражениями (не вложенными) | Одно полное выражение | Следующее полное выражение |
| Законченный полный описатель | ||
| Сразу перед возвратом из библиотечной функции | ||
| После каждого преобразования, связанного со спецификатором форматированного ввода-вывода | ||
| Сразу перед и сразу после каждого вызова функции сравнения, а также между вызовом функции сравнения и любыми перемещениями, выполняемыми над передаваемыми в функцию сравнения аргументами | ||
Полными выражениями считаются[26]:
- инициализатор, не являющийся частью составного литерала;
- обособленное выражение;
- выражение, указанное в качестве условия условного оператора (
if) или оператора выбора (switch); - выражение, указанное в качестве условия цикла
whileс предусловием или с постусловием; - каждый из параметров цикла
for, если таковой указан; - выражение оператора
return, если таковое указано.
В следующем примере переменная изменяется трижды между точками следования, что приводит к неопределённому результату:
int i = 1; // Описатель - первая точка следования, полное выражение - вторая
i += ++i + 1; // Полное выражение - третья точка следования
printf("%d\n", i); // Может быть выведено как 4, так и 5
Другие простые примеры неопределённого поведения, которого необходимо избегать:
i = i++ + 1; // неопределённое поведение
i = ++i + 1; // тоже неопределённое поведение
printf("%d, %d\n", --i, ++i); // неопределённое поведение
printf("%d, %d\n", ++i, ++i); // тоже неопределённое поведение
printf("%d, %d\n", i = 0, i = 1); // неопределённое поведение
printf("%d, %d\n", i = 0, i = 0); // тоже неопределённое поведение
a[i] = i++; // неопределённое поведение
a[i++] = i; // тоже неопределённое поведение
Управляющие операторы
Управляющие операторы предназначены для осуществления действий и для управления ходом выполнения программы. Несколько идущих подряд операторов образуют последовательность операторов.
Пустой оператор
Самая простая языковая конструкция — это пустое выражение, называемое пустым оператором[28]:
;
Пустой оператор не совершает никаких действий и может находиться в любом месте программы. Обычно используется в циклах с отсутствующим телом[29].
Инструкции
Инструкция — это некое элементарное действие:
- (выражение)
;
Действие этого оператора заключается в выполнении указанного в теле оператора выражения.
Несколько идущих подряд инструкций образуют последовательность инструкций.
Блок инструкций
Инструкции могут быть сгруппированы в специальные блоки следующего вида:
{
- (последовательность инструкций)
},
Блок инструкций, также иногда называемый составным оператором, ограничивается левой фигурной скобкой ({) в начале и правой фигурной скобкой (}) — в конце.
В функциях блок инструкций обозначает тело функции и является частью определения функции. Также составной оператор может использоваться в операторах циклов, условия и выбора.
Условные операторы
В языке существует два условных оператора, реализующих ветвление программы:
- оператор
if, содержащий проверку одного условия, - и оператор
switch, содержащий проверку нескольких условий.
Самая простая форма оператора if
if((условие))(оператор)- (следующий оператор)
Оператор if работает следующим образом:
- если выполнено условие, указанное в скобках, то выполняется первый оператор, и затем выполняется оператор, указанный после оператора
if. - если условие, указанное в скобках, не выполнено, то сразу выполняется оператор, указанный после оператора
if.
В частности, следующий ниже код, в случае выполнения заданного условия, не будет выполнять никаких действий, поскольку, фактически, выполняется пустой оператор:
if((условие));
Более сложная форма оператора if содержит ключевое слово else:
if((условие))(оператор)else(альтернативный оператор)- (следующий оператор)
Здесь, если условие, указанное в скобках, не выполнено, то выполняется оператор, указанный после ключевого слова else.
Несмотря на то, что стандарт допускает указание тела операторов if или else одной строкой, это считается плохим стилем, снижающим читабельность кода. В качестве тела рекомендуется всегда указывать блок инструкций с помощью фигурный скобок[30].
Операторы выполнения цикла
Цикл — это фрагмент программного кода, содержащий
- условие выполнения цикла — условие, которое постоянно проверяется;
- и тело цикла — простой или составной оператор, выполнение которого зависит от условия цикла.
В соответствии с этим, различают два вида циклов:
- цикл с предусловием, где сначала проверяется условие выполнения цикла, и, если условие выполнено, то выполняется тело цикла;
- цикл с постусловием, где проверка условия продолжения цикла происходит после исполнения тела цикла.
Цикл с постусловием гарантирует, что тело цикла выполнится по крайней мере один раз.
В языке Си предусмотрено два варианта циклов с предусловием: while и for.
while(условие)[тело цикла]for(блок инициализации;условие;оператор)[тело цикла],
Цикл for ещё называется параметрическим, он эквивалентен следующему блоку операторов:
- [блок инициализации]
while(условие){- [тело цикла]
- [оператор]
}
В обычной ситуации блок инициализации содержит задание начального значения переменной, которая называется переменной цикла, а оператор, который выполняется сразу после тела цикла, меняет значения используемой переменной, условие содержит сравнение значения используемой переменной цикла с некоторым заранее заданным значением, и, как только сравнение перестаёт выполняться, цикл прерывается, и начинает выполняться программный код, следующий сразу за оператором цикла.
У цикла do-while условие указывается после тела цикла:
do[тело цикла]while(условие)
Условие цикла — это логическое выражение. Однако неявное приведение типов позволяет использовать в качестве условия цикла арифметическое выражение. Это позволяет организовать так называемый «бесконечный цикл»:
while(1);
То же самое можно сделать и с применением оператора for:
for(;;);
На практике такие бесконечные циклы обычно используются совместно с операторами break, goto или return, которые осуществляют прерывание работы цикла разными способами.
Как и для оператора условия, использование однострочного тела без заключения его в блок инструкций с помощью фигурных скобок считается плохим стилем, снижающим читабельность кода[30].
Операторы безусловного перехода
Операторы безусловного перехода позволяют прервать выполнение любого блока вычислений и перейти в другое место программы в рамках текущей функции. Операторы безусловного перехода обычно используются совместно с условными операторами.
goto[метка],
Метка — это некоторый идентификатор, передаёт управление тому оператору, который помечен в программе указанной меткой:
- [метка]
:[оператор]
Если указанная метка отсутствует в программе или если существует несколько операторов с одной и той же меткой, компилятор сообщает об ошибке.
Передача управления возможна только в пределах той функции, где используется оператор перехода, следовательно, при помощи оператора goto нельзя передать управление в другую функцию.
Другие операторы перехода связаны с циклами и позволяют прервать выполнения тела цикла:
- оператор
breakнемедленно прерывает выполнение тела цикла, и происходит передача управления на оператор, следующий непосредственно сразу за циклом; - оператор
continueпрерывает выполнение текущей итерации цикла и инициирует попытку перехода к следующей.
Оператор break также может прерывать работу оператора switch, поэтому внутри оператора switch, запущенного в цикле, оператор break не сможет прервать работу цикла. Указанный в теле цикла, он прерывает работу ближайшего вложенного цикла.
Оператор continue может быть использован только внутри операторов do, while и for. У циклов while и do-while оператор continue вызывает проверку условия цикла, а в случае цикла for — исполнение оператора, заданного в 3-м параметре цикла, перед проверкой условия продолжения цикла.
Оператор возврата из функции
Оператор return прерывает выполнение той функции, в которой использован. Если функция не должна возвращать значение, то используется вызов без возвращаемого значения:
return;
Если функция должна возвращать какое-либо значение, то после оператора указывается возвращаемое значения:
return[значение];
Если после оператора возврата в теле функции имеются ещё какие-то операторы, то эти операторы никогда не будут выполняться, и в этом случае компилятор может выдать предупреждение. Однако после оператора return могут указываться инструкции для альтернативного завершения функции, например, по ошибке, а переход к этим операторам можно осуществлять с помощью оператора goto согласно каким-либо условиям.
Переменные
При объявлении переменной указывается её тип и название, а также может указываться начальное значение:
- [описатель] [имя]
;
или
- [описатель] [имя]
=[инициализатор];,
где
- [описатель] — тип переменной и предшествующие типу необязательные модификаторы;
- [имя] — имя переменной;
- [инициализатор] — начальное значение переменной, присваиваемое при её создании.
Если переменной не присвоено начальное значение, то в случае глобальной переменной её значение заполняется нулями, а для локальной переменной начальное значение будет неопределённым.
В описателе переменной можно обозначать переменную как глобальную, но ограниченную областью видимости файла или функции, с помощью ключевого слова static. Если переменная объявлена глобальной без ключевого слова static, то обращаться к ней возможно и из других файлов, где требуется объявить данную переменную без инициализатора, но с ключевым словом extern. Адреса таких переменных определяются на этапе компоновки.
Функции
Функция — это самостоятельный фрагмент программного кода, который может многократно использоваться в программе. Функции могут иметь аргументы и могут возвращать значения. Также функции могут иметь побочные эффекты при своём исполнении: изменение глобальных переменных, работа с файлами, взаимодействие с операционной системой или оборудованием[27].
Для того, чтобы задать функцию в Си, необходимо её объявить:
- сообщить имя (идентификатор) функции,
- перечислить входные параметры (аргументы)
- и указать тип возвращаемого значения.
Также необходимо привести определение функции, которое содержит блок операторов, реализующих поведение функции.
Отсутствие объявления определённой функции является ошибкой, если функция используется вне области видимости определения, что, в зависимости от реализации, приводит к выдаче сообщений или предупреждений.
Для вызова функции достаточно указать её имя с параметрами, указанными в скобках. При этом адрес точки вызова помещается в стек, создаются и инициализируются переменные, отвечающие за параметры функции, и передаётся управление коду, реализующему вызываемую функцию. После выполнения функции происходит освобождение памяти, выделенной при вызове функции, возврат в точку вызова и, если вызов функции является частью некоторого выражения, передача в точку возврата вычисленного внутри функции значения.
Если после функции не указаны скобки, то компилятор интерпретирует это как получение адреса функции. Адрес функции можно заносить в указатель и в последующем вызывать функцию посредством указателя на неё, что активно используется, например, в системах плагинов[31].
С помощью ключевого слова inline можно помечать функции, вызовы которых требуется исполнять как можно быстрее. Компилятор может подставлять код таких функций непосредственно в точку их вызова[32]. С одной стороны, это увеличивает объём исполняемого кода, но, с другой, — позволяет экономить время его выполнения, поскольку не используется дорогостоящая по времени операция вызова функции. Однако из-за особенностей построения архитектуры компьютеров, встраивание функций может приводить как к ускорению, так и к замедлению работы приложения в целом. Тем не менее во многих случаях встраиваемые функции являются предпочтительной заменой макросам[33].
Объявление функции
Объявление функции имеет следующий формат:
- [описатель] [имя]
([список]);,
где
- [описатель] — описатель типа возвращаемого функцией значения;
- [имя] — имя функции (уникальный идентификатор функции);
- [список] — список (формальных) параметров функции или
voidпри их отсутствии[34].
Признаком объявления функции является символ «;», таким образом, объявление функции — это инструкция.
В самом простом случае [описатель] содержит указание на конкретный тип возвращаемого значения. Функция, которая не должна возвращать никакого значения, объявляется как имеющая тип void.
При необходимости в описателе могут присутствовать модификаторы, задаваемые с помощью ключевых слов:
externуказывает на то, что определение функции находится в другом модуле;staticзадаёт статическую функцию, которая может быть использована только в текущем модуле.
Список параметров функции задаёт сигнатуру функции.
Си не допускает объявление нескольких функций, имеющих одно и то же имя, перегрузка функций не поддерживается[35].
Определение функции
Определение функции имеет следующий формат:
- [описатель] [имя]
([список])[тело]
Где [описатель], [имя] и [список] — те же, что и в объявлении, а [тело] — это составной оператор, который представляет собою конкретную реализацию функции. Компилятор различает определения одноимённых функций по их сигнатуре, и таким образом (по сигнатуре) устанавливается связь между определением и соответствующим ему объявлением.
Тело функции имеет следующий вид:
{- [последовательность операторов]
return([возвращаемое значение]);
}
Возврат из функции осуществляется с помощью оператора return, у которого либо указывается возвращаемое значение, либо не указывается, в зависимости от возвращаемого функцией типа данных. В редких случаях функция может быть помечена как не делающая возврат с помощью макроса noreturn из заголовочного файла stdnoreturn.h, в таких случаях оператор return не требуется. Например, подобным образом можно помечать функции, безусловно вызывающие внутри себя abort()[32].
Вызов функции
Вызов функции заключается в выполнении следующих действий:
- сохранение точки вызова в стеке;
- автоматическое выделение памяти под переменные, соответствующие формальным параметрам функции;
- инициализация переменных значениями переменных (фактических параметров функции), переданных в функцию при её вызове, а также инициализация тех переменных, для которых в объявлении функции указаны значения по умолчанию, но для которых при вызове не были указаны соответствующие им фактические параметры;
- передача управления в тело функции.
В зависимости от реализации, компилятор либо строго следит за тем, чтобы тип фактического параметра совпадал с типом формального параметра, либо, если существует такая возможность, осуществляет неявное преобразование типа, что, очевидно, приводит к побочным эффектам.
Если в функцию передаётся переменная, то при вызове функции создаётся её копия (в стеке выделяется память и копируется значение). Например, передача структуры в функцию вызовет копирование всей структуры целиком. Если же передаётся указатель на структуру, то копируется только значение указателя. Передача в функцию массива также вызывает лишь копирование указателя на его первый элемент. При этом для явного обозначения того, что на вход функции принимается адрес начала массива, а не указатель на единичную переменную, вместо объявления указателя после названия переменной можно поставить квадратные скобки, например:
void example_func(int array[]); // array — указатель на первый элемент массива типа int
Си допускает вложенные вызовы. Глубина вложенности вызовов имеет очевидное ограничение, связанное с размером выделяемого программе стека. Поэтому в реализациях Си устанавливается некое предельное значение для глубины вложенности.
Частный случай вложенного вызова — это вызов функции внутри тела вызываемой функции. Такой вызов называется рекурсивным, и применяется для организации единообразных вычислений. Учитывая естественное ограничение на вложенные вызовы, рекурсивную реализацию заменяют на реализацию при помощи циклов.
Примечания
Комментарии
- ↑ Макрос
boolиз заголовочного файлаstdbool.hявляется обёрткой над ключевым словом_Bool. - ↑ Макрос
complexиз заголовочного файлаcomplex.hявляется обёрткой над ключевым словом_Complex. - ↑ Макрос
imaginaryиз заголовочного файлаcomplex.hявляется обёрткой над ключевым словом_Imaginary. - ↑ Макрос
alignasиз заголовочного файлаstdalign.hявляется обёрткой над ключевым словом_Alignas. - 1 2 Макрос
alignofиз заголовочного файлаstdalign.hявляется обёрткой над ключевым словом_Alignof. - ↑ Макрос
noreturnиз заголовочного файлаstdnoreturn.hявляется обёрткой над ключевым словом_Noreturn. - ↑ Макрос
static_assertиз заголовочного файлаassert.hявляется обёрткой над ключевым словом_Static_assert. - ↑ Макрос
thread_localиз заголовочного файлаthreads.hявляется обёрткой над ключевым словом_Thread_local.
Источники
- ↑ Papaspyrou, 1998, 1.2 Programming language semantics, p. 5.
- ↑ David R Sutton. The syntax and semantics of the PROforma guideline modeling language : [англ.] / David R Sutton, John Fox // Journal of the American Medical Informatics Association. — 2003, 4 June. — Vol. 10, iss. 5. — P. 433—443. — ISSN 1067-5027, 1527-974X. — doi:10.1197/jamia.m1264. — PMID 12807812. — WD Q36247140.
- 1 2 Papaspyrou, 1998, 1.1 The C programming language, p. 4.
- ↑ Ritchie Dennis M. The Development of the C Language : [англ.] / Ed.: Richard L. Wexelblat // ACM SIGPLAN Notices. — 1993, 1 March. — Vol. 28, iss. 3. — P. 201–208. — Дата обращения: 22 января 2023. — ISSN 0362-1340. — doi:10.1145/155360.155580. — WD Q55869040.
- ↑ Papaspyrou, 1998, 2.1 Selected issues from the syntax and semantics of C, p. 17-18.
- 1 2 Черновик стандарта C17, 5.2.1 Character sets, с. 17.
- 1 2 Черновик стандарта C17, 6.4.2 Identifiers, с. 43—44.
- ↑ Черновик стандарта C17, 6.4.4 Constants, с. 45—50.
- ↑ Подбельский, Фомин, 2012, с. 19.
- 1 2 Черновик стандарта C17, 6.4.4.1 Integer constants, с. 46.
- ↑ Черновик стандарта C17, 6.4.4.2 Floating constants, с. 47—48.
- 1 2 Черновик стандарта C17, 6.4.4.4 Character constants, с. 49—50.
- ↑ STR30-C. Do not attempt to modify string literals - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 27 мая 2019. Архивировано 27 мая 2019 года.
- ↑ Черновик стандарта C17, 6.4.5 String literals, с. 50—52.
- ↑ Clang-Format Style Options — Clang 9 documentation (англ.). clang.llvm.org. Дата обращения: 19 мая 2019. Архивировано 20 мая 2019 года.
- 1 2 3 4 DCL06-C. Use meaningful symbolic constants to represent literal values - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 6 февраля 2019. Архивировано 7 февраля 2019 года.
- ↑ Черновик стандарта C17, с. 84.
- ↑ Черновик стандарта C17, 6.4.1 Keywords, с. 42.
- 1 2 Free Software Foundation (FSF). Status of C99 features in GCC (англ.). GNU Project. gcc.gnu.org. Дата обращения: 31 мая 2019. Архивировано 3 июня 2019 года.
- 1 2 3 4 Черновик стандарта C17, 7.1.3 Reserved identifiers, с. 132.
- ↑ Черновик стандарта C17, 6.5.3 Unary operators, с. 63—65.
- ↑ Черновик стандарта C17, 6.5 Expressions, с. 66—72.
- ↑ Черновик стандарта C17, 6.5.16 Assignment operators, с. 72—74.
- ↑ Черновик стандарта C17, с. 55—75.
- 1 2 The GNU C Reference Manual. 3.19 Operator Precedence (англ.). www.gnu.org. Дата обращения: 13 февраля 2019. Архивировано 7 февраля 2019 года.
- 1 2 3 4 5 EXP30-C. Do not depend on the order of evaluation for side effects - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 14 февраля 2019. Архивировано 15 февраля 2019 года.
- 1 2 BB. Definitions - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 16 февраля 2019. Архивировано 16 февраля 2019 года.
- ↑ Подбельский, Фомин, 2012, 1.4. Операции, с. 42.
- ↑ Подбельский, Фомин, 2012, 2.3. Операторы цикла, с. 78.
- 1 2 EXP19-C. Use braces for the body of an if, for, or while statement - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 2 июня 2019. Архивировано 2 июня 2019 года.
- ↑ Dynamically Loaded (DL) Libraries (англ.). tldp.org. Дата обращения: 18 февраля 2019. Архивировано 12 ноября 2020 года.
- 1 2 Черновик стандарта C17, 6.7.4 Function specifiers, с. 90—91.
- ↑ PRE00-C. Prefer inline or static functions to function-like macros - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 4 июня 2019. Архивировано 7 августа 2021 года.
- ↑ Черновик стандарта C17, 6.11 Future language directions, с. 130.
- ↑ Does C support function overloading? | GeeksforGeeks. Дата обращения: 15 декабря 2013. Архивировано 15 декабря 2013 года.
Литература
- ISO/IEC. ISO/IEC9899:2017. Programming languages — C. www.open-std.org (2017). Дата обращения: 3 декабря 2018. Архивировано из оригинала 24 октября 2018 года.
- Подбельский В. В., Фомин С. С. Курс программирования на языке Си: учебник. — М.: ДМК Пресс, 2012. — 318 с. — ISBN 978-5-94074-449-8.
- Papaspyrou N. S. A Formal Semantics for the C Programming Language : [англ.] : [арх. 22 января 2023] : doctoral dissertation. — National Technical University of Athens, 1998, February.