Синтаксис и семантика языка Си
Синтаксис определяет то, как должны правильно записываться языковые конструкции, в то время как семантика определяет значения языковых конструкций[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[d]. — 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 : [англ.] // ACM SIGPLAN Notices[d] : Proceedings of the 32nd ACM SIGPLAN-SIGACT symposium on Principles of programming languages. — 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.