### Перестановка значений местами
```
void swap(int *pa, int *pb) {
int t = *pa;
*pa = *pb;
*pb = t;
return;
}
int main(void) {
int a = 21;
int b = 17;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b); // a: 17 b: 21
return 0;
}
```
Параметры pa и pb внутри swap объявлены в виде указателей типа `int` и содержат копии аргументов, переданных этой функции из вызывающего кода (в данном случае main). Эти копии адресов по-прежнему ссылаются натежеобъекты,поэтому,когдафункцияswapменяетэтиобъектыместами, содержимое исходных объектов, объявленных в `main`, тоже меняется. Данный подход имитирует передачу аргументов по ссылке: сначала генерируются адреса объектов, которые передаются по значению, а затем они разыменовываются для доступа к исходным объектам.
### Область видимости
```
int j; // начинается область видимости уровня файла j
void f(int i) { // начинается блочная область видимости i
int j = 1; // начинается блочная область видимости j
// перекрывает j в области видимости уровня файла
i++; // ссылается на параметр функции
for (int i = 0; i < 2; i++) { // начинается блочная область видимости i
// внутри цикла
int j = 2; // начинается блочная область видимости j
// перекрывает внешнюю j
printf("%d\n", j); // внутренняя j в области видимости, выводит 2
} // заканчивается блочная область видимости
// внутренних i и j
printf("%d\n", j); // внешняя j в области видимости, выводит 1
// заканчивается блочная область видимости i и j
}
void g(int j); // j имеет область видимости уровня прототипа
// перекрывает j уровня файла
```
### Срок хранения
Объекты имеют срок хранения, который определяет их время жизни. В целом сроки хранения бывают четырех видов: автоматические, статические, потоковые и выделенные.
Объекты, объявленные на уровне файла, имеют статический срок хранения. Их время жизни охватывает весь период выполнения программы, а значения, которые в них хранятся, инициализируются еще до ее запуска. Вы также можете объявить переменную со статическим сроком хранения внутри блочной области видимости, используя спецификатор класса хранения static.
```
void increment(void) {
static unsigned int counter = 0;
counter++;
printf("%d ", counter);
}
int main(void) {
for (int i = 0; i < 5; i++) {
increment();
}
return 0; }
```
Время жизни counter охватывает весь период выполнения программы, а последнее значение данной переменной будет храниться, пока она существует.
Потоковый срок хранения используется в конкурентном программировании. Динамический срок хранения относится к динамически выделяемой памяти.
### Выравнивание
Типы объектов имеют требования к выравниванию, ограничивающие адреса, которые могут выделяться для объектов этих типов. Выравнивание (alignment) определяет количество байтов между смежными адресами, в которых может быть сохранен заданный объект. Процессор может по-разному обращаться к выровненным и невыровненным данным (адрес выровненных данных может быть, например, кратен их размеру).
В целом программистам на C не нужно беспокоиться об этих требованиях, поскольку компилятор самостоятельно выбирает для своих различных типов подходящее выравнивание. Гарантируется, что память, выделенная с помощью malloc, будет достаточно выровненной для всех стандартных типов, включая массивы и структуры. Но в редких случаях решения компилятора, используемые по умолчанию, приходится переопределять — например, чтобы выровнять данные на границах строк кэша, адреса должны быть кратны степеням двойки.
В С11 появился простой способ указания выравнивания, обладающий совместимостью с будущими версиями стандарта. Выравнивание представляется как неотрицательное целое число типа `size_t`. Корректное выравнивание должно быть степенью двойки.
### Объектные типы
####Перечисляемые типы
Перечисление (enum, от enumeration) позволяет определить тип, который назначает имена (перечислители) целочисленным значениям в ситуациях, когда требуется перечисляемый набор постоянных значений. Примеры перечислений:
```
enum day { sun, mon, tue, wed, thu, fri, sat };
enum cardinal_points { north = 0, east = 90, south = 180, west = 270 };
enum months { jan = 1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov,
dec };
```
Если не присвоить значение первому перечислителю с помощью операции =, то его перечисляемая константа будет равна 0, и каждый следующий перечислитель без знака = будет прибавлять 1 к предыдущей константе. Следовательно, значение sun в перечислении day равно 0, mon равно 1 и т. д.
#### Тип void
Тип void довольно необычен. Ключевое слово `void` (само по себе) означает «не может содержать никаких значений». Например, с его помощью можно сигнализировать о том, что ваша функция не возвращает значение или не принимает аргументов (если указать его в качестве единственного параметра). С другой стороны, производный тип `void *` означает, что указатель может ссылаться на любой объект.
### Производные типы
Производными называют типы, основанные на других типах. В их число входят указатели, массивы, определения типов, структуры и объединения.
#### Указатели
Тип указателя является производным от типа функции или объекта, на которые он указывает. Его также называют ссылочным типом. Указатель предоставляет ссылку на сущность ссылочного типа.
Операция & позволяет получить адрес объекта или функции. Например, если объект имеет тип int, то эта операция вернет указатель типа int:
```
int i = 17;
int *ip = &i;
```
Мы объявляем переменную ip в качестве указателя на int и присваиваем ей адрес i. Вы также можете применить операцию & к результату выполнения операции *:
```
ip = &*ip;
```
В результате разыменования ip с помощью операции косвенного обращения мы получаем сам объект i. Адрес *ip, полученный с помощью &, является указателем, поэтому эти две операции компенсируют друг
друга.
### Структуры
Структура (или struct) содержит последовательно выделенные объекты-члены. Каждый объект обладает собственным именем и может иметь отдельный тип — для сравнения, в массивах все элементы должны быть одного типа. Структуры похожи на тип данных «запись», который встречается в других языках программирования.
```
struct sigrecord {
int signum;
char signame[20];
char sigdesc[100];
} sigline, *sigline_p;
```
У этой структуры есть три объекта-члена: signum (объект типа int), signame (массив из 20 элементов типа char) и sigdesc (массив из 100 элементов типа char).
Определив структуру, вы, вероятно, захотите обращаться к ее членам. Для обращения к членам объекта структурных типов служит операция доступа (.). Если у вас есть указатель на структуру, то для обращения к ее членам предусмотрена операция ->.
```
sigline.signum = 5;
strcpy(sigline.signame, "SIGINT");
strcpy(sigline.sigdesc, "Interrupt from keyboard");
sigline_p = &sigline;
sigline_p->signum = 5;
strcpy(sigline_p->signame, "SIGINT");
strcpy(sigline_p->sigdesc, "Interrupt from keyboard");
```
#### Объединения
Объединения похожи на структуры, только их члены используют одну и ту же память. В один момент времени объединение может содержать объект одного типа, а в другой момент времени — объект другого типа, но никогда не может содержать оба объекта сразу. Объединения используются в основном для экономии памяти.
```
union {
struct {
int type; } n;
struct {
int type;
int intnode;
} ni;
struct {
int type;
double doublenode;
} nf;
} u;
u.nf.type = 1;
u.nf.doublenode = 3.14;
```
Как и в случае со структурами, доступ к членам объединения осуществляется с помощью операции доступа (.). Если у вас есть указатель на объединение, то для обращения к его членам предусмотрена операция ->.
Код, использующий такое объединение, обычно выясняет тип узла, проверяя значение, хранящееся в u.n.type, и затем, в зависимости от результата, обращается либо к intnode, либо к doublenode. Если бы это было реализовано в виде структуры, то в каждом узле было бы выделено место как для intnode, так и для doublenode. Использование объединения позволяет задействовать одно и то же место для обоих членов.
#### Теги
Теги — это специальный механизм именования структур, объединений и перечислений.
Например, идентификатор s, представленный в следу- ющей структуре, является тегом:
```
struct s {
//---snip---
};
```
```
struct s v; // экземпляр структуры s
struct s *p; // указатель на структуру s
```
Названия объединений и перечислений — тоже теги, а не типы. Это значит,
их недостаточно для объявления переменной.
```
enum day { sun, mon, tue, wed, thu, fri, sat };
day today; // ошибка
enum day tomorrow; // правильно
```
Теги структур, объединений и перечислений определяются в собственном пространстве имен, отдельно от обычных идентификаторов. Благодаря этому программа на C может иметь в одной области видимости тег и другой идентификатор, который выглядит точно так же:
```
enum status { ok, fail }; // перечисление
enum status status(void); // функция
```
Вы даже можете объявить объект s типа struct s:
```
struct s s;
```
Это не самое лучшее решение, но в языке C оно является корректным. Теги структур можно считать именами типов и определять для них псевдонимы с помощью typedef. Например:
```
typedef struct s { int x; } t;
```
Теперь вы можете объявлять переменные типа t, а не struct s. Имя тега в struct, union и enum указывать не обязательно, поэтому можете его опустить:
```
typedef struct { int x; } t;
```
### Квалификаторы типов
Тип можно сделать квалифицированным за счет использования одного или нескольких квалификаторов, таких как const, volatile и restrict.
Объекты, объявленные с использованием квалификатора const (const- квалифицированные объекты), не подлежат изменению.
```
const int i = 1; // константа
int i = 2; // ошибка: i — это константа
```
Статические объекты с квалификатором volatile используются для представления портов ввода/вывода, отображенных в память, а статические объекты сразу с двумя квалификаторами, const и volatile, могут представлять порты ввода, отображенные в память, такие как часы реального времени.
Значения, хранящиеся в этих объектах, могут изменяться без ведома компилятора. Например, значение часов реального времени может ме- няться при каждом обращении, даже если сама программа это значение не меняет. Использование volatile-квалифицированного типа сообщает компилятору о том, что значение может измениться, и гарантирует, что код будет каждый раз обращаться к часам реального времени (в про- тивном случае эти обращения могут быть удалены в ходе оптимизации или заменены уже считанным и закэшированным значением).
Например, при обработке следующего кода компилятор должен сгенерировать инструкции, которые будут читать значение из port и затем записывать его обратно в port:
```
volatile int port;
port = port;
```
Если бы не было квалификатора volatile, то компилятор воспринял бы этот оператор как фиктивный (такой, который ничего не делает) и мог бы удалить как чтение, так и запись.
restrict-квалифицированный указатель используется для поддержки оптимизации. Объекты, к которым обращаются косвенно через указатели, зачастую не удается оптимизировать должным образом из-за того, что на один и тот же объект теоретически могут ссылаться сразу несколько указателей.
Следующая функция копирует n байтов с участка памяти, на который указывает q, на участок, на который указывает p. Оба параметра функции, p и q, являются restrict-квалифицированными указателями:
```
void f(unsigned int n, int * restrict p, int * restrict q) {
while (n-- > 0) {
*p++ = *q++;
}
}
```
Использование указателей с квалификатором restrict делает код более эффективным, но, чтобы избежать неопределен- ного поведения, программист обязан обеспечить, чтобы участки памяти, на которые они ссылаются, не пересекались.
### Арифметические типы
#### Целые числа
Целочисленные знаковые типы допу- скают положительные, отрицательные и нулевые значения; беззнаковые типы допускают только положительные и нулевые значения.
Все целочисленные типы, кроме char, signed char и unsigned char, могут содержать неиспользуемые биты, которые называются заполнением и позволяют реализации обходить причуды аппаратной платформы (такие как пропуск бита со знаком посреди представления из нескольких машинных слов) или оптимально поддерживать целевую архитектуру. Количество битов, используемых для представления значения заданного типа, не считая заполнения, но включая знак, называется шириной и зачастую обозначается как N. Точность — это количество битов, используемых для представления значения, не считая знака и заполнения.
#### Циклический перенос
Циклический перенос (wraparound) происходит при выполнении арифметической операции, результат которой слишком маленький (меньше 0) или большой (больше 2N – 1), чтобы его можно было представить в виде конкретного беззнакового целочисленного типа. В этом случае берется остаток от деления значения на N, которое на единицу больше максимально допустимого значения итогового типа. В языке C циклический перенос является определенным поведением. Вызван ли он дефектом в вашем коде, зависит от контекста. Если вы что-то подсчитываете и значение переносится, то это, вероятно, ошибка. Однако в некоторых алгоритмах шифрования циклический перенос используется намеренно.
```
unsigned int ui = UINT_MAX; // 4,294,967,295 в x86
ui++;
printf("ui = %u\n", ui); // ui равно 0
ui--;
printf("ui = %u\n", ui); // ui равно 4,294,967,295
```
Ввиду циклического переноса беззнаковое целочисленное выражение никогда не может быть меньше 0. Об этом можно легко забыть и реализовать сравнения, которые всегда остаются истинными.
#### Знаковые целые
Целочисленные типы со знаком имеют более сложное представление, чем их беззнаковые аналоги. Язык C традиционно поддерживает три варианта представления таких значений:
* прямой код — старший разряд обозначает знак, а остальные разряды представляют величину значения в обычной двоичной системе;
* обратный код — разряду со знаком назначается вес –(2N – 1 – 1), а остальные разряды значения имеют те же веса, что и в беззнаковом типе;
* дополнительный код — разряду со знаком назначается вес –(2N – 1), а остальные разряды значения имеют те же веса, что и в беззнаковом типе.
Вы не можете выбрать конкретное представление; оно определяется теми, кто реализует язык C для различных систем.
#### Переполнение
Переполнение происходит, когда операция со знаковым целым возвращает значение, которое не может быть представлено итоговым типом. Например, следующая реализация функционального макроса, которая возвращает модуль целочисленного значения, может переполниться:
```
// не определено или неверно для самого отрицательного значения
#define Abs(i) ((i) < 0 ? -(i) : (i))
signed int si = -25;
signed int abs_si = Abs(si);
printf("%d\n", abs_si); // выводит 25
```
Пока все хорошо. Но проблема в том, что результат смены знака для самого маленького отрицательного значения заданного типа в дополнительном коде
не может быть представлен этим типом, поэтому такое применение функции Abs приводит к переполнению знакового целого.
Следовательно, данная реализация Abs является дефектной и может сделать что угодно, в том числе неожиданно вернуть отрицательное значение:
```
signed int si = INT_MIN;
signed int abs_si = Abs(si); // неопределенное поведение
printf("%d\n", abs_si); // веренет -2147483648
```
С точки зрения языка C переполнение целочисленных типов со знаком является неопределенным поведением, что позволяет реализациям молча выполнять циклический перенос (происходит чаще всего), прерывание или и то и другое. Прерывания останавливают программу, не давая выполнить последующие операции. Поскольку это неопределенное поведение, у данной проблемы нет какого-то единого общепринятого решения. Но мы можем как минимум проверить возможность переполнения до того, как оно случится, и принять соответствующие меры.
Чтобы этот макрос мог возвращать модуль для разных типов, мы создадим для него еще один аргумент, flag, который зависит от типа. Данный флаг представляет макрос *_MIN, который соответствует типу первого аргумента, и возвращается в случае возникновения проблемы:
```
#define AbsM(i, flag) ((i) >= 0 ? (i) : ((i)==(flag) ? (flag) : -(i)))
signed int si = -25; // попробуйте INT_MIN, чтобы спровоцировать
// проблемный случай
signed int abs_si = AbsM(si, INT_MIN);
if (abs_si == INT_MIN)
goto recover; // особый случай
else
printf("%d\n", abs_si); // выводит 25
```
Макрос AbsM проверяет наличие самого маленького отрицательного значения и, если оно обнаружено, просто возвращает его, не пытаясь его инвертировать. Это позволяет избежать неопределенного поведения.
#### Числа с плавающей запятой
Язык C поддерживает три типа с плавающей запятой: float, double и long double.
Тип float можно использовать в вычислениях с плавающей запятой, в которых результат можно адекватно представить с одинарной точностью. При кодировании типа float один разряд выделяется для знака, восемь — для порядка и еще 23 — для мантиссы.
Тип double имеет более высокую точность, но занимает дополнительное место. При его кодировании один разряд отводится для знака, 11 — для порядка и еще 52 — для мантиссы.
Во всех реализациях тип long double рекомендуется использовать формат числа четверной точности IEC 60559 (или binary128).
#### Ранг преобразования целочисленных типов
У каждого целочисленного типа есть ранг преобразования — порядковый номер, который определяет, как и при каких условиях происходит неявное приведение типа.
Rаждый целочисленный тип имеет ранг преобразования, на который распространяются следующие правила:
- все целочисленные типы со знаком имеют разный ранг, даже если у них одинаковое представление;
- целочисленный тип со знаком всегда имеет больший ранг, чем любой беззнаковый целочисленный тип с меньшей точностью;
- у типа long long int более высокий ранг, чем у типа long int; у long int более высокий ранг, чем у int; у int более высокий ранг, чем у short int; у short int более высокий ранг, чем у signed char;
- ранг любого беззнакового целочисленного типа равен рангу соответ- ствующего целочисленного типа со знаком, если таковой имеется;
- тип char имеет тот же ранг, что signed char и unsigned char;
- тип _Bool имеет меньший ранг, чем любой другой стандартный целочисленный тип;
- ранг любого перечисляемого типа равен рангу совместимого целочисленного типа. Любой перечисляемый тип совместим с signed int и unsinged int;
- относительные ранги преобразования любых расширенных целочис- ленных типов со знаком, имеющих одинаковую точность, определяются на уровне реализации, но при этом должны соблюдать все остальные правила порядка приведения целочисленных типов.
#### Повышение разрядности целочисленных значений
Повышение разрядности служит двум основным целям. Во-первых, оно поощряет проведение операций с использованием естественного для архитектуры размера, что улучшает производительность. Во-вторых, помогает избежать арифметических ошибок, которые возникают из-за переполнения промежуточных значений.
```
signed char cresult, c1, c2, c3;
c1 = 100; c2 = 3; c3 = 4;
cresult = c1 * c2 / c3;
```
Без повышения разрядности выражение c1 * c2 привело бы к переполнению типа signed char на тех платформах, где он представлен восьмибитным значением в дополнительном коде, так как 300 выходит за пределы диапазона значений, которые могут быть представлены объектом этого типа (от –128 до 127). Но благодаря повышению разрядности c1, c2 и c3 автоматически преобразуются в объекты типа signed int, и операции умножения и деления выполняются в данном размере. При этом не может произойти переполнение, поскольку итоговые значения всегда могут быть представлены типом signed int (объекты которого имеют диапазон от −2N – 1 до 2N − 1 – 1). В этом конкретном примере результат всего выражения равен 75; он находится в диапазоне типа signed char, поэтому, когда он присваивается переменной cresult, его значение сохраняется.
#### Обычные арифметические преобразования
Обычные арифметические преобразования — это правила выбора общего типа за счет балансирования обоих операндов бинарной операции или путем балансирования второго и третьего аргументов условной операции (? :).
Балансирующее преобразование приводит один или оба операнда, имеющие разные типы, к одному и тому же типу. Большинство операций, которые принимают целочисленные операнды, включая *, /, %, +, -, <, >, <=, >=, ==, != , &, ^, | и ? :, совершают обычные арифметические преобра- зования. Эти преобразования применяются к операндам с повышенной разрядностью.
Обычные арифметические преобразования сначала проверяют, имеет ли один из операндов тип с плавающей запятой. Если да, то действуют следующие правила.
1. Если любой из операндов имеет тип long double, то другой тоже приводится к long double.
2. Иначе если любой из операндов имеет тип double, то другой тоже приводится к double.
3. В противном случае если любой из операндов имеет тип float, то другой тоже приводится к float.
4. В противном случае повышение разрядности выполняется для обоих операндов.
Например, если один операнд имеет тип double, а другой int, то последний преобразуется в объект типа double. Если один операнд имеет тип float, а другой double, то первый преобразуется в объект типа double.
Если ни один из операндов не является значением типа с плавающей запятой, то к целочисленным операндам с повышенной разрядностью применяются следующие правила обычного арифметического преобразования.
1. Если оба операнда имеют один и тот же тип, то дальнейших преобразований не требуется.
2. В противном случае если оба операнда знаковые целые или оба беззнаковые целые, то операнд с меньшим рангом преобразования приводится к типу операнда с более высоким рангом. Например, если один операнд имеет тип int, а другой — long, то операнд типа int приводится к long.
3. В противном случае если операнд с беззнаковым целочисленным типом имеет ранг больший или равный рангу типа другого операнда, то операнд со знаком приводится к беззнаковому целочисленному типу.
Например, если один операнд имеет тип signed int, а другой — unsigned int, то операнд типа signed int приводится к unsigned int.
В противном случае если тип целочисленного операнда со знаком может представить все значения операнда с беззнаковым целочисленным типом, то последний приводится к целочисленному типу со знаком. Например, если один операнд имеет тип unsigned int, а другой — signed long long и если второй может представить все значения первого, то операнд типа unsigned int преобразуется в объект типа signed long long. Это происходит в реализациях с 32-битным типом int и 64-битным типом long long, таких как x86-32 и x86-64.
В противном случае операнды приводятся к беззнаковым версиям своих целочисленных типов со знаком.
### Безопасное приведение типов
Чтобы избежать преобразований, операции желательно проводить с объектами одного типа. Но если функция возвращает или принимает объект другого типа, то без приведения не обойтись. В таких ситуациях следите за тем, чтобы преобразование выполнялось корректно.
#### Приведение целочисленных значений
Приведение этого вида происходит, когда значение целочисленного типа приводится к другому целочисленному типу. Приведение к типам большей разрядности без добавления или удаления знака всегда является безопасным, и его не нужно проверять. Большинство других преобразований может давать непредсказуемые результаты, если результат нельзя представить с помощью итогового типа. Для их корректного выполнения необходимо убедиться, что значение, хранящееся в исходном целочисленном типе, находится в допустимом диапазоне итогового целочисленного типа.
Безопасное приведение типов
```
#include <errno.h>
#include <limits.h>
errno_t do_stuff(signed long value)
{
if ((value < SCHAR_MIN) || (value > SCHAR_MAX))
{
return ERANGE;
}
signed char sc = (signed char)value; // приведение типов глушит
//---snip---
}
```
#### Приведение целочисленных значений к типам с плавающей запятой
Если целочисленное значение, приводимое к типу с плавающей запятой, может быть точно представлено этим типом, то оно не меняется. Если приводимое значение находится в диапазоне, который может быть представлен, но не точно, то результат округляется к ближайшему большему или меньшему допустимому значению в зависимости от реализации. Если приводимое значение находится вне допустимого диапазона, то поведение не определено.
#### Приведение значений с плавающей запятой к целочисленным типам
Когда конечное значение с плавающей запятой приводится к любому целочисленному типу (кроме bool), дробная часть отбрасывается. Если значение целой части нельзя представить с помощью целочисленного типа, то поведение не определено.
#### Уменьшение разрядности значений с плавающей запятой
Приведение чисел с плавающей запятой к более крупным вещественным типам всегда является безопасным. Уменьшение разрядности значения с плавающей запятой (то есть приведение к меньшему типу с плавающей запятой) похоже на приведение целого числа к типу с плавающей запятой.
### Порядок вычисления
Порядок вычисления операндов в любой операции языка C, включая по- рядок выполнения любых вложенных выражений, обычно не уточняется. Компилятор может вычислить их в любом порядке, который к тому же может меняться при повторном выполнении того же выражения. Такая неопределенность позволяет компилятору генерировать более произво- дительный код за счет выбора наиболее эффективного порядка. Однако этот порядок ограничен приоритетом и ассоциативностью операций.
```
int glob; // статическое хранилище инициализируется с помощью 0
int f(void)
{
return glob + 10;
}
int g(void)
{
glob = 42;
return glob;
}
int main(void)
{
int max_value = max(f(), g());
// ---snip---
}
```
Обе функции (f и g) обращаются к глобальной переменной glob; это значит, они зависят от разделяемого состояния. Когда они вычисляют свои результаты, значение, передаваемое функции max, может изменяться при каждой компиляции. Если функция f вызывается первой, то вернет 10, а если последней, то ее возвращаемым значением будет 52. Функция g всегда возвращает 42, вне зависимости от порядка вычислений. Таким образом, функция max (возвращающая большее из двух значений) может вернуть как 42, так и 52, в зависимости от того, в каком порядке были вы- числены ее аргументы. Единственная гарантия относительно порядка выполнения, которую дает этот код, состоит в том, что f и g всегда вызываются до max и выполняются исключительно по очереди.
Этот код можно переписать так, чтобы он был переносимым и всегда выполнялся предсказуемо:
```
int f_val = f();
int g_val = g();
int max_value = max(f_val, g_val);
```