Синтаксис и семантика языка Си (Vnumgtvnv n vybgumntg x[dtg Vn)

Перейти к навигации Перейти к поиску

Синтаксис определяет то, как должны правильно записываться языковые конструкции, в то время как семантика определяет значения языковых конструкций[1]. Синтаксис языка Си достаточно сложный, а семантика неоднозначная[2]. Основными двумя особенностями языка на момент его появления были унифицирование работы с массивами и указателями, а также схожесть того, как что-либо объявляется, с тем, как это в дальнейшем используется в выражениях[3]. Однако в последующем эти две особенности языка были в числе наиболее критикуемых[3], и обе являются сложными для понимания среди начинающих программистов[4]. Стандарт языка, определяя его семантику, не стал слишком сильно ограничивать реализации языка компиляторами, но этим самым сделал семантику недостаточно определённой. В частности, в стандарте есть 3 типа недостаточно определённой семантики: определяемое реализацией поведение, не заданное стандартом поведение и неопределённое поведение[5].

В языке используются все символы латинского алфавита, цифры и некоторые специальные символы[6].

Состав алфавита[6]
Символы латинского алфавита

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

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

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

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

Идентификаторы

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

Допустимый идентификатор — это слово, в состав которого могут входить символы латинского алфавита, цифры и знак подчёркивания[7]. Идентификаторы даются операторам, константам, переменным, типам и функциям.

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

Встроенный идентификатор только один — __func__, который определяется как константная строка, неявно объявляемая в каждой функции и содержащая её название[7].

Литеральные константы

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

Специально оформленные литералы в Си принято называть константами. Литеральные константы могут быть целочисленными, вещественными, символьными[8] и строковыми[9].

Целые числа по умолчанию задаются в десятичной системе счисления. Если указан префикс 0x, то — в шестнадцатеричной системе. Префикс в виде цифры 0 указывает, что число задаётся в восьмеричной системе. Суффикс определяет минимальный размер типа константы, а также определяет, является ли число знаковым или беззнаковым. В качестве итогового типа берётся такой минимально возможный, в котором данную константу можно представить[10].

Порядок назначения типов данных целым константам согласно их значению[10]
Суффикс Для десятичных Для восьмеричных и шестнадцатеричных
Нет int

long

long long

int

unsigned int

long

unsigned long

long long

unsigned long long

u или U unsigned int

unsigned long

unsigned long long

unsigned int

unsigned long

unsigned long long

l или L long

long long

long

unsigned long

long long

unsigned long long

u или U вместе с l или L unsigned long

unsigned long long

unsigned long

unsigned long long

ll или LL long long long long

unsigned long long

u или U вместе с ll или LL unsigned long long unsigned long long
Примеры записи вещественного числа 1.5
Десятичный

формат

С экспонентой Шестнадцатеричный

формат

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.

Префиксы символьных констант[12]
Префикс Тип данных Кодировка
Нет int ASCII
u char16_t Кодировка 16-битных многобайтовых строк
U char32_t Кодировка 32-битных многобайтовых строк
L wchar_t Кодировка широких строк

Строковые литералы заключаются в двойные кавычки и могут иметь префикс, определяющий тип данных строки и её кодировку. Строковые литералы представляют собой обычные массивы. При этом в многобайтовых кодировках, таких как UTF-8, один символ может занимать более одного элемента массива. По факту строковые литералы являются константными[13], но в отличие от C++ их типы данных не содержат модификатор const.

Префиксы строковых констант[14]
Префикс Тип данных Кодировка
Нет char * ASCII или многобайтовая кодировка
u8 char * UTF-8
u char16_t * 16-битная многобайтовая кодировка
U char32_t * 32-битная многобайтовая кодировка
L wchar_t * Кодировка широких строк

Несколько подряд идущих строковых констант, разделённых пробельными символами или переводами строк объединяются в одну строку при компиляции, что часто используется для оформления кода строки путём разделения частей строковой константы по разным строкам для повышения читабельности[15].

Именованные константы

[править | править код]
Сравнение способов задания констант[16]
Макрос
#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.

Ключевые слова

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

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

Ключевые слова языка Си[18]
Ключевые слова Назначение Стандарт
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] Объявление локальной для потока переменной

Зарезервированные идентификаторы

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

Помимо ключевых слов стандарт языка определяет зарезервированные идентификаторы, использование которых может привести к несовместимости с будущими версиями стандарта. Зарезервированными являются все, за исключением ключевых, слова, начинающиеся со знака подчёркивания (_), после которого идёт либо заглавная буква (AZ), либо другой знак подчёркивания[20]. В стандартах С99 и С11 часть таких идентификаторов была использована под новые ключевые слова языка.

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

Также зарезервированными идентификаторами являются все макросы стандартной библиотеки и связываемые на этапе линковки названия из неё[20].

Использование зарезервированных идентификаторов в программах стандарт определяет как неопределённое поведение. Попытка отмены любого стандартного макроса через #undef также повлечёт за собой неопределённое поведение[20].

Комментарии

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

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

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

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

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

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

Унарные операторы

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

Унарные операторы выполняют операцию над единственным аргументом и имеют следующий формат операции:

[оператор] [операнд]

Операции постфиксного инкремента и декремента имеют обратный формат:

[операнд] [оператор]
Унарные операторы языка Си[21]
+ Унарный плюс ~ Взятие обратного кода & Взятие адреса ++ Префиксный или постфиксный инкремент sizeof Получение количества байт, занимаемого объектом в памяти; может использоваться и как операция, и как оператор
- Унарный минус ! логическое отрицание * Разыменовывание указателя -- Префиксный или постфиксный декремент _Alignof Получение выравнивания для заданного типа данных

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

Бинарные операторы

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

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

[операнд] [оператор] [операнд]
Базовые бинарные операторы[22]
+ Сложение % Взятие остатка от деления << Поразрядный сдвиг влево > Больше == Равно
- Вычитание & Поразрядное И >> Поразрядный сдвиг вправо < Меньше != Не равно
* Умножение | Поразрядное ИЛИ && Логическое И >= Больше либо равно , Последовательное вычисление
/ Деление ^ Поразрядное исключающее ИЛИ || Логическое ИЛИ <= Меньше либо равно

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

Лево-присваивающие бинарные операторы[23]
= Присвоение значения правого аргумента левому %= Остаток от деления левого операнда на правый ^= Поразрядное исключающее ИЛИ правого операнда к левому
+= Прибавление к левому операнду правого /= Деление левого операнда на правый <<= Поразрядный сдвиг левого операнда влево на количество бит, заданное правым операндом
-= Вычитание из левого операнда правого &= Поразрядное И правого операнда к левому >>= Поразрядный сдвиг левого операнда вправо на количество бит, заданное правым операндом
*= Умножение левого операнда на правый |= Порязрядное ИЛИ правого операнда к левому

Тернарные операторы

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

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

[условие] ? [выражение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].

Точки следования, определённые стандартом[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

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

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

Примечания

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

Комментарии

[править | править код]
  1. Макрос bool из заголовочного файла stdbool.h является обёрткой над ключевым словом _Bool.
  2. Макрос complex из заголовочного файла complex.h является обёрткой над ключевым словом _Complex.
  3. Макрос imaginary из заголовочного файла complex.h является обёрткой над ключевым словом _Imaginary.
  4. Макрос alignas из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignas.
  5. 1 2 Макрос alignof из заголовочного файла stdalign.h является обёрткой над ключевым словом _Alignof.
  6. Макрос noreturn из заголовочного файла stdnoreturn.h является обёрткой над ключевым словом _Noreturn.
  7. Макрос static_assert из заголовочного файла assert.h является обёрткой над ключевым словом _Static_assert.
  8. Макрос thread_local из заголовочного файла threads.h является обёрткой над ключевым словом _Thread_local.
  1. Papaspyrou, 1998, 1.2 Programming language semantics, p. 5.
  2. 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.
  3. 1 2 Papaspyrou, 1998, 1.1 The C programming language, p. 4.
  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.
  5. Papaspyrou, 1998, 2.1 Selected issues from the syntax and semantics of C, p. 17-18.
  6. 1 2 Черновик стандарта C17, 5.2.1 Character sets, с. 17.
  7. 1 2 Черновик стандарта C17, 6.4.2 Identifiers, с. 43—44.
  8. Черновик стандарта C17, 6.4.4 Constants, с. 45—50.
  9. Подбельский, Фомин, 2012, с. 19.
  10. 1 2 Черновик стандарта C17, 6.4.4.1 Integer constants, с. 46.
  11. Черновик стандарта C17, 6.4.4.2 Floating constants, с. 47—48.
  12. 1 2 Черновик стандарта C17, 6.4.4.4 Character constants, с. 49—50.
  13. STR30-C. Do not attempt to modify string literals - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 27 мая 2019. Архивировано 27 мая 2019 года.
  14. Черновик стандарта C17, 6.4.5 String literals, с. 50—52.
  15. Clang-Format Style Options — Clang 9 documentation (англ.). clang.llvm.org. Дата обращения: 19 мая 2019. Архивировано 20 мая 2019 года.
  16. 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 года.
  17. Черновик стандарта C17, с. 84.
  18. Черновик стандарта C17, 6.4.1 Keywords, с. 42.
  19. 1 2 Free Software Foundation (FSF). Status of C99 features in GCC (англ.). GNU Project. gcc.gnu.org. Дата обращения: 31 мая 2019. Архивировано 3 июня 2019 года.
  20. 1 2 3 4 Черновик стандарта C17, 7.1.3 Reserved identifiers, с. 132.
  21. Черновик стандарта C17, 6.5.3 Unary operators, с. 63—65.
  22. Черновик стандарта C17, 6.5 Expressions, с. 66—72.
  23. Черновик стандарта C17, 6.5.16 Assignment operators, с. 72—74.
  24. Черновик стандарта C17, с. 55—75.
  25. 1 2 The GNU C Reference Manual. 3.19 Operator Precedence (англ.). www.gnu.org. Дата обращения: 13 февраля 2019. Архивировано 7 февраля 2019 года.
  26. 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 года.
  27. 1 2 BB. Definitions - SEI CERT C Coding Standard - Confluence (англ.). wiki.sei.cmu.edu. Дата обращения: 16 февраля 2019. Архивировано 16 февраля 2019 года.
  28. Подбельский, Фомин, 2012, 1.4. Операции, с. 42.
  29. Подбельский, Фомин, 2012, 2.3. Операторы цикла, с. 78.
  30. 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 года.
  31. Dynamically Loaded (DL) Libraries (англ.). tldp.org. Дата обращения: 18 февраля 2019. Архивировано 12 ноября 2020 года.
  32. 1 2 Черновик стандарта C17, 6.7.4 Function specifiers, с. 90—91.
  33. 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 года.
  34. Черновик стандарта C17, 6.11 Future language directions, с. 130.
  35. 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.