Глобальная переменная — переменная, определённая вне функций и доступная из разных функций.
Локальная переменная — переменная, определённая внутри некоторой функции и доступная только из неё.
Класс хранения переменной — способ выделения памяти под переменную, также определяет время её жизни.
Модификатор доступа переменной — особенности или ограничения, накладываемые на действия с переменной.
Утечка памяти memory leak — уменьшение доступной программе памяти, вызванное ошибочными действиями самой программы. Как правило, происходит при выделении блоков динамической памяти (см. ниже), адреса которых затем по какой-то причине теряются, из-за чего программа не может ни использовать их, ни вернуть системе эти блоки памяти.
constexpr
можно использовать везде, где требуется константа времени компиляции, при этом само это значение может и не существовать как переменная и не занимать память во время исполнения программы.extern
переменная будет считаться определённой только в случае наличия инициализатора, в противном случае переменная будет считаться объявленной. Комбинация extern const делает константу видимой в других единицах трансляции (снимает эффект подразумеваемого static
).new
-delete
или стандартных функций.Во время исполнения программы разным классам хранения могут соответствовать физически различные области памяти.
static
к глобальной переменной (т.е. итак уже статически размещаемой), то эта переменная будет невидима из других единиц трансляции.// Определение статической константы.
const char SPACE = ' ';
// (Неконстантный) глобальный указатель на статическую константу.
const char* ATK_MI = "Missiles incoming!";
// Константный глобальный указатель на статическую константу, доступный из других единиц трансляции.
extern const char* const EMPTY = "";
// Определение глобальной переменной, размещённой в этой единице трансляции.
int x;
// Объявление глобальной переменной, размещённой в какой-то другой единице трансляции.
extern float y;
// Определение глобальной переменной, невидимой компоновщику,
// т.е. не доступной из других единиц трансляции.
static double z = 1.0; // Инициализируем ненулевым значением.
// Данная функция сохраняет в своей статической локальной переменной значение аргумента
// первого вызова и возвращает его (и затем только его).
int return_its_first_argument(int arg)
{
// Статическая переменная, доступная только внутри этой функции.
// Инициализируется при первом вызове функции.
static int first_arg = arg;
return first_arg;
}
int x_plus(int y)
{
// y -- локальная автоматическая переменная (параметр),
// "затеняет" глобальную переменную y (к которой, впрочем, можно обратиться через ::y),
// а вот x здесь -- та самая глобальная переменная, определённая выше
return x + y;
}
double z_plus_other_z()
{
// Объявление некоторой глобальной переменной z, определённой в другой единице трансляции.
// "Затеняет" статическую z, определённую выше (в этой единице трансляции).
extern double z;
// Однако, мы можем обратиться к той z, что определена в этой единице трансляции через ::z.
return z + ::z;
}
В языке C++ определены два модификатора доступа, которые являются частью системы типов. Т.е. их применение изменяет формальный тип переменной, что может сказываться в случае использования производных типов и перегруженных функций.
const
с указателем обычно используется для того, чтобы показать, что нельзя изменять значение, на которое указывает указатель. При этом обычно можно изменять сам указатель.volatile
переменных. В современных условиях volatile
требуется использовать только в особых достаточно редких случаях, поэтому далее он не рассматривается.Хотя стек вызовов как способ организации вызовов функций и управления автоматическими переменными не закреплён в стандартах C и C++, он используется повсеместно. Понимание принципов его работы является важным для программиста.
Слово “стек” stack можно перевести как “стопка”. Объекты добавляются на стек сверху и снимаются потом в обратном порядке.
Возвращаясь к воображаемому компьютеру, использовавшемуся для управления роботом во введении, попробуем организовать на нём вычисление факториала F(n) натурального числа n по определению:
F(0) = 1,
F(n) = n F(n–1).
Факториал — излюбленный модельный объект при демонстрации или исследовании свойств рекурсии ввиду простоты. Другой подобный объект — последовательность чисел Фибоначчи, также традиционно задаваемая рекурсивным определением.
Итак, пусть есть переменные n и fact (результат вычисления факториала). Нам понадобится вспомогательная переменная n_1 и команды:
Итак, программа может начинаться следующим образом.
Адрес | Команда |
---|---|
0 | beq n, 0, 13 |
– | // общий случай, n > 0 |
4 | sub n_1, n, 1 |
– | // здесь надо как-то вычислить fact = F(n_1) |
8 | mul fact, n |
11 | j 16 |
13 | store fact, 1 |
16 | // выход из программы |
В зависимости от значения n нам понадобится повторно заходить в код, вычисляющий факториал. Поэтому мы оформим этот код в виде вызываемой функции. Надо как-то организовать возможность вызова и возврата из функции результата. Для простоты будем считать, что результат будет записываться в переменную fact, а аргумент функции будет находиться в переменной n.
Создадим стек вызовов. Для этого определим новую переменную sp (от англ. stack pointer), будем считать, что в ней хранится адрес свободной позиции в массиве (стеке), созданном как раз для сохранения адресов возврата из функций.
При осуществлении вызова надо положить адрес возврата на стек вызовов:
При осуществлении возврата из функции:
Для реализации данного протокола вызова понадобится добавить ещё команды:
Итак, оформим программу как рекурсивную функцию. Вспомогательная переменная ra будет использоваться для загрузки адреса возврата из стека.
Адрес | Команда |
---|---|
0 | beq n, 0, 32 |
– | // общий случай, n > 0 |
4 | store n_1, n |
7 | sub n, n, 1 |
11 | store [sp], 20 |
14 | add sp, sp, 1 |
18 | j 0 |
– | // сюда происходит возврат |
20 | mul fact, n_1 |
– | // возврат из функции |
23 | sub sp, sp, 1 |
27 | load ra, [sp] |
30 | jr ra |
– | // случай n = 0 |
32 | store fact, 1 |
35 | j 23 |
Упражнение. Попробуйте промоделировать поведение второго варианта программы Факториал на бумаге хотя бы для n = 2.
Полученный результат совсем не тот, что должен был быть. Если вы промоделировали работу программы, то ошибка должна быть очевидна — мы не сохраняем промежуточные значения переменной n (параметра функции), а они свои для каждого отдельного вызова (в переменной n_1 сохраняется только предпоследнее значение n). Что же делать? Ответ простой — использовать стек. Перед вызовом будем сохранять туда значение n, а после возврата — восстанавливать его.
Адрес | Команда |
---|---|
0 | beq n, 0, 47 |
– | // общий случай, n > 0 |
4 | store n_1, n |
7 | sub n, n, 1 |
11 | store [sp], n_1 |
14 | add sp, sp, 1 |
18 | store [sp], 28 |
22 | add sp, sp, 1 |
26 | j 0 |
– | // сюда происходит возврат |
28 | sub sp, sp, 1 |
32 | load n_1, [sp] |
35 | mul fact, n_1 |
– | // возврат из функции |
38 | sub sp, sp, 1 |
42 | load ra, [sp] |
45 | jr ra |
– | // случай n = 0 |
47 | store fact, 1 |
50 | j 38 |
Упражнение. Промоделируйте работу третьего варианта программы Факториал для n = 3.
Кадр стека вызовов stack frame — кусок стека вызовов, сформированный одним вызовом функции.
В кадре стека размещаются все временные значения, которые необходимо хранить, а также значения локальных переменных. Выделение и освобождение памяти в стеке, происходит простым изменением указателя на текущую вершину стека (sp). С точки зрения операций центрального процессора это очень дешёвая операция, кроме того, освобождение памяти происходит автоматически (например, при возврате из функции). Но когда требуется большие объёмы памяти, то стека может не хватить (особенно при рекурсивных вызовах), в таких случаях приходится пользоваться динамической памятью.
Во время отладки отладчик позволяет “проходить” по кадрам стека вызовов, сформированных на данный момент. Таким образом можно понять, в каком порядке вызывались функции, из каких мест в исходном коде, каковы значения локальных переменных в каждом вложенном вызове. Эта информация может быть весьма полезна при поиске ошибок в логике программы.
Размер стека задаётся статически. В случае если во время исполнения программа попробует выйти за его пределы (что возможно при слишком глубокой рекурсии), поведение не определено (ситуация UB). В операционных системах с защитой памяти (в частности, современных версиях Windows и в большинстве Unix-подобных систем) программа будет остановлена с ошибкой. Если это произойдёт во время отладки, то отладчик, скорее всего, сообщит причину: “переполнение стека (вызовов)” (call) stack overflow.
Соглашение вызова calling convention — правила, по которым осуществляется вызов функции на конкретной программно-аппаратной платформе (задаётся связкой конкретной архитектуры команд центрального процессора и операционной системы, например, на x86-32 и Windows NT одни правила, а на ARMv7 и GNU/Linux другие правила). Соглашение вызова — часть определяемого системой двоичного интерфейса приложения application binary interface, ABI, низкоуровневых особенностей функционирования программ на данной системе. Знание ABI может потребоваться в том числе для “склеивания” программ, написанных на разных языках программирования или откомпилированных разными компиляторами.
Соглашение вызова определяет (некоторые из этих элементов могут быть не стандартизованы, что делает разные компиляторы или даже разные версии одного компилятора не полностью совместимыми даже на одной платформе — впрочем, эта проблема не касается компиляторов C, если не предполагается использовать отладчик):
Конкретные соглашения вызова здесь рассматриваться не будут, так как это довольно узкая низкоуровневая специфика. Но представление об их существовании иметь следует. В качестве некоторых примеров соглашений вызова на существующих актуальных платформах можно привести ссылки на Википедию (для серьёзного изучения требуется изучать официальные спецификации, выпущенные для каждой системы, т.к. сведения Википедии могут быть недостаточно точны и полны): x86, краткий общий обзор.
Стандартная библиотека C предлагает всего пять функций для управления динамической памятью (выделение и освобождение блоков произвольного размера).
void*
malloc (size_t bytes)
выделяет блок размера не менее bytes
байт и возвращает его адрес. Если блок такого размера выделить невозможно, возвращает нулевой указатель. Если bytes
равно нулю, поведение определяется реализацией.void*
calloc (size_t n, size_t bytes)
— вспомогательная функция, определённая для удобства. Если число n * bytes
представимо в виде значения типа size_t
, то вызывает malloc(n * bytes)
, т.е. создаёт массив из n
элементов, где каждый элемент размера bytes
байт, затем зануляет выделенную память. В случае, если n * bytes
не может быть представлено в виде значения типа size_t
или память не может быть выделена, возвращает нулевой указатель. В коде на C рекомендуется по возможности использовать calloc
вместо аналогичного malloc
.void*
realloc (void *block, size_t new_bytes)
позволяет изменить размер ранее выделенного блока памяти. Если возможно просто сдвинуть правую границу блока по адресу block
(ранее выделенного вызовом malloc
, calloc
или realloc
) так, чтобы его размер стал не менее new_bytes
байт, то это делается, и функция возвращает адрес block
(эта функция может как расширять, так и сужать блок). Если же блок невозможно расширить на месте (например, память за ним уже занята), то realloc
пытается создать новый блок размера не менее new_bytes
и, если это удалось, копирует содержимое исходного блока в новую память, освобождает старый блок и возвращает указатель на новый блок. Наконец, если невозможно ни расширить исходный блок, ни создать новый блок размера new_bytes
, то realloc
возвращает нулевой указатель. В случае, если block
был нулевым указателем, realloc
возвращает результат malloc(new_bytes)
.void*
aligned_alloc (size_t alignment, size_t bytes)
(C11, в стандарт C++ пока не включена) — функция, выделяющая блок памяти размера не менее bytes
байт, адрес которого, взятый как целое число, кратен числу alignment
(которое, как правило, должно быть степенью двойки — ограничения на значения alignment
определяются реализацией), т.е. выровнен по границе alignment
байт. Число bytes
также должно делиться нацело на alignment
. Данная функция несовместима с realloc
.void
free (void* block)
освобождает блок динамической памяти по адресу block
, полученный ранее вызовом одной из вышеперечисленных функций. Если block
является нулевым указателем, то функция ничего не делает.При использовании функции realloc
нередко допускают следующую ошибку: присваивают результат realloc
исходному указателю на блок:
char *mem;
...
// плохой код!
mem = realloc(mem, 2*size);
Так делать не следует из-за того, что realloc
в случае невозможности выделить новый блок может вернуть нулевой указатель. При этом старый блок остаётся нетронутым, но указатель на него (mem
) мы потеряли. Имеем ситуацию утечки памяти.
// корректный код, C++
if (auto new_mem = (char*)realloc(mem, 2*size))
mem = new_mem;
else // невозможно выделить память
...
В отличие от C, в C++ встроены операторы выделения (new) и освобождения (delete) динамической памяти.
Пусть T
— имя некоторого типа.
T
создаёт объект типа T
в динамической памяти и возвращает его адрес. Если выделить память не получилось, происходит ошибка (исключение bad_alloc
). Таким образом, если удалось получить результат new
, то это корректный адрес блока динамической памяти. Инициализация объекта в памяти зависит от типа T
и по сути не отличается от инициализации переменной типа T
в автоматической памяти. Инициализирующее выражение можно передать в скобках:auto ps = new string; // инициализированный объект -- пустая строка
assert(ps->empty());
auto pn = new int; // неинициализированный объект int, значение может быть любым
auto pi = new int(10); // инициализированный константой 10 объект int
auto i = int(10); // то же, что int i = 10;
// впрочем, синтаксис C++ не позволяет написать auto n = int;
delete object
, где object
— адрес объекта, созданного оператором new
. Корректно удаляет ранее созданный объект и освобождает занимаемую им память. Вызов delete
для нулевого указателя допустим и не выполняет никаких действий.
new(
nothrow ) T
аналогично функции malloc
возвращает нулевой указатель в случае невозможности выделить память. Для того, чтобы использовать такой вариант, следует подключить стандартный заголовочный файл new. Идентификатор nothrow
определён в пространстве имён std.
new T[n]
создаёт в динамической памяти массив из n объектов типа T
(“динамический массив”). В отличие от статических массивов n не обязано быть константой времени компиляции. Значение n не должно быть равно нулю. Кратко данный оператор обозначают new[]
, чтобы отличать его от первой формы new
.
delete [ ] array
, где array
— результат new[]
. Корректно удаляет массив по адресу array
и освобождает занимаемую им память. Вызов delete[]
для нулевого указателя допустим и не выполняет никаких действий.
Нельзя смешивать разные методы выделения и освобождения блоков динамической памяти. Если память получена функциями malloc
, calloc
, realloc
или aligned_alloc
, то освобождать её надо только функцией free
. Если память получена “обычным” new
(включая вариант с nothrow
) для одиночного объекта, то освобождать её надо только вызовом delete
. Если память получена оператором создания динамического массива new[]
, то освобождать её необходимо вызовом delete[]
. Функцию realloc
нельзя использовать с блоками, выделенными new
или new[]
.
В данном курсе термины указатель pointer и адрес address используются как синонимы. На основных современных платформах (x86-64, ARMv8, MIPS64, PowerPC и др.) с точки зрения C и C++ это действительно так. Однако сам термин “указатель” подразумевает нечто большее, чем просто “адрес”. Указатель — это набор данных, необходимых для обращения к некоторому значению. В ряде случаев представление указателя может содержать дополнительные данные помимо собственно адреса в памяти процессора. Это может быть расширение адреса (например, индекс сегмента в системах с сегментированием памяти), это может быть дополнительная информация о типе значения (тег) или что-то ещё, что зависит от конкретной системы. Поэтому не следует отождествлять понятия “указатель” и “адрес” во всех контекстах.
В C и C++ любая переменная или объект в памяти должны иметь адрес (за исключением переменных с классом хранения register). И этот адрес не равен специальному нулевому указателю null pointer, который используется как признак отсутствия некоторого объекта или значения. При преобразовании к типу указателя нулевой указатель порождается константой ноль (в случае C++ из чисел только литерал 0 приводится к указателю неявно). Во многих стандартных заголовочных файлах C определён стандартный макрос NULL
. Его определение может выглядеть следующим образом:
#ifdef __cplusplus // случай для C++
#define NULL 0
#else // случай для C
#define NULL ((void*)0)
#endif
В C++11 концепция нулевого указателя была несколько развита введением специального стандартного типа nullptr_t
и литерала nullptr, представляющего единственное значение этого типа. Таким образом, стало возможным использовать защиту на уровне системы типов при манипулировании указателями (ведь 0 — просто число, которое может пройти во многих контекстах) или при имитации модели нулевого указателя (например, инициализация своих объектов значением nullptr
).
Итак, в качестве нулевого указателя в программах на C рекомендуется использовать стандартный макрос NULL
(определён в stdlib.h и других заголовках), а в программах на C++ — ключевое слово nullptr
.
Разыменование нулевого указателя порождает неопределённое поведение.
В отличие от указателей ссылки не могут быть “свободными”. Ссылка обязана быть привязана к существующему в памяти значению, в языке C++ нет никакой “нулевой ссылки”. Нарушение этого правила ведёт к неопределённому поведению.
Исторически в различных семействах вычислительных систем использовались разные порядки отдельных байт многобайтных значений при укладке их в памяти. Как правило, используется либо порядок “младшие байты по младшим адресам” little-endian, LE, либо порядок “старшие байты по младшим адресам” big-endian, BE. В редких случаях встречаются некие другие варианты (“смешанные порядки”). Существующие основные процессорные архитектуры включают средства для работы с двумя указанными порядками, при этом обычно только один из них считается “основным”, используемым по умолчанию.
Например, пусть есть значение 1234567890 = 49’96’02’D216 типа uint32_t (32 бита = 4 байта). Как оно будет уложено в памяти на машине с поддержкой адресации отдельных байт? Для примера разместим его по адресу 16.
Байт | 16 | 17 | 18 | 19 |
---|---|---|---|---|
BE-порядок | 49 | 96 | 02 | D2 |
LE-порядок | D2 | 02 | 96 | 49 |
LE-порядок несколько естественнее с вычислительной точки зрения, BE-порядок удобнее читать человеку при просмотре значений памяти или файлов в двоичной форме (например, в “шестнадцатеричном редакторе”). BE-порядок традиционно используется в сетевых протоколах и рядом процессорных архитектур (MIPS, PowerPC, ARM, хотя они предоставляют возможность переключаться между LE и BE). LE-порядок традиционно используется процессорами семейства x86.
Выравнивание alignment — размещение значений в памяти по адресам, кратным некоторому целому числу, большему единицы.
Причина, по которой существует такое понятие как выравнивание, заключается в том, что процессорам проще оперировать выровненными значениями.
Естественное выравнивание natural alignment — выравнивание значений встроенных типов (как правило, поддерживаемых процессором непосредственно) по адресам, кратным размеру этого типа. Например, 4-байтные целые размещаются по адресам, кратным четырём (0, 4, 8, 12, …), а 8-байтные значения типа double размещаются по адресам, кратным восьми (0, 8, 16, 24, …).
Далее для краткости слово “выравнивание” будет использоваться также для обозначения самого значения (числа байт), которому должен быть кратен адрес.
Размер объекта типа (возвращаемый оператором sizeof
) должен делиться на выравнивание нацело.
Можно получить (C++11) выравнивание типа как константу времени компиляции с помощью оператора alignof, аргументом которого служит имя типа.
В стандартном заголовочном файле cstddef, начиная со стандарта C++11, объявлен тип max_align_t, естественное выравнивание которого является максимальным среди всех стандартных встроенных типов. Выравнивание блоков динамической памяти, выделяемых стандартными средствами C и C++, не меньше значения alignof(max_align_t)
. По поводу выделения блоков динамической памяти с заданным выравниванием см. здесь.
Спецификатор alignas (C++11) позволяет задать желаемое выравнивание переменной или пользовательского типа (не нарушающее, впрочем, естественное выравнивание, т.е. кратное ему, в противном случае спецификатор игнорируется).
alignas(64) char cache_line[64]; // Хотим, чтобы адрес cache_line был кратен 64.
alignas(double) char bytes[8]; // Хотим, чтобы bytes имело выравнивание alignof(double).
// Переменные типа Double_2 должны иметь адреса, кратные 16.
struct alignas(16) Double_2
{
double part[2];
};
Использование спецификатора alignas(0)
не изменяет выравнивание переменной.
Все элементы массива имеют одинаковый тип и, соответственно, удовлетворяют всем свойствам значений этого типа. По умолчанию, выравнивание массива совпадает с выравниванием типа элемента массива. Размер массива равен произведению количества элементов на размер элемента. Массив располагается в памяти единым блоком, без разрывов.
По умолчанию, каждое поле структуры может считаться независимой переменной и выравнивается аналогично. Чтобы соблюсти это требование, объекты структур получают выравнивание, равное наименьшему общему кратному выравниваний своих полей. Так как обычно поддерживается только выравнивание по степеням двойки, НОК заменяется простым максимумом.
Для обеспечения выравнивания полей вставляются пропуски padding — байты, которые не используются и не принадлежат ни одному полю. Значения пропусков не определены, поэтому объекты структур с пропусками нельзя сравнивать на равенство побайтно (например, с помощью функции memcmp).
В случае форсирования выравнивания по 1 (т.е. отсутствии ограничений на адреса), структура называется упакованной. Нет стандартных средств для упаковки структур, но компиляторы предоставляют нестандартные средства. Для упакованных структур снимаются требования по выравниванию полей, что нередко приводит к меньшему итоговому размеру объекта структуры из-за отсутствия пропусков. Впрочем, такая экономия памяти обычно негативно сказывается на производительности.
Даже без форсирования какого-либо выравнивания, размер структуры при перестановке полей может изменяться (из-за соблюдения естественного выравнивания полей). Рассмотрим следующий пример:
struct S1
{
char a, b;
int32_t c;
};
struct S2
{
int32_t c;
char a, b;
};
struct S3
{
char a;
uint32_t c;
char b;
};
// Упакованная структура.
// Нестандартное средство: компилятор MSVC.
#pragma pack(1)
struct S4
{
char a, b;
int32_t c;
};
Адрес | S1 | S2 | S3 | S4 |
---|---|---|---|---|
0 | a | c-0 | a | a |
1 | b | c-1 | пропуск | b |
2 | пропуск | c-2 | пропуск | c-0 |
3 | пропуск | c-3 | пропуск | c-1 |
4 | c-0 | a | c-0 | c-2 |
5 | c-1 | b | c-1 | c-3 |
6 | c-2 | пропуск | c-2 | |
7 | c-3 | пропуск | c-3 | |
8 | b | |||
9 | пропуск | |||
10 | пропуск | |||
11 | пропуск |
Структура | alignof | sizeof |
---|---|---|
S1 | 4 | 8 |
S2 | 4 | 8 |
S3 | 4 | 12 |
S4 | 1 | 6 |
Ключевое слово union вводит тип, называемый объединение. Объединения напоминают структуры и могут содержать именованные поля разных типов. Однако, в отличие от структур все поля объединений имеют один и тот же адрес — адрес объекта объединения, т.е. все поля объединений разделяют одну и ту же память. Причины для использования объединений могут быть следующие:
Выравнивание объединения задаётся так же, как выравнивание структуры с теми же полями. Размер объединения равен наименьшему кратному его выравнивания, достаточному для того, чтобы поместилось поле самого большого размера. Например, следующее объединение имеет выравнивание 4 и размер 8 (предполагая, что float
имеет размер 4), последний байт не используется:
union
{
float value;
char bytes[3 + sizeof float];
} data;
Битовое поле — поле структуры с заданным размером в битах. Битовые поля могут быть только целочисленных типов. Ниже приведён пример синтаксиса, задающего битовые поля.
// 16-битный RGB цвет.
struct Color_565
{
unsigned red : 5; // 5 бит на интенсивность красного
unsigned green : 6; // 6 бит на интенсивность зелёного
unsigned blue : 5; // 5 бит на интенсивность синего
};
Ширина битового поля должна быть константой времени компиляции, неотрицательным целым числом.
При обращении к битовому полю, значение приводится к указанному целочисленному типу. Если поле шире в битах, чем использованный тип, то лишние биты отбрасываются (путём обращения к полю их значение нельзя ни получить ни изменить). Если поле у́же, чем использованный тип, то происходит расширение значения до ширины типа. Для беззнаковых типов оставшиеся старшие биты заполняются нулями. Способ расширения значений знаковых типов определяется реализацией. Если использован целочисленный тип (например, int
) без явного указания signed
реализация может истолковать тип поля как беззнаковый (в то время как обычные поля или переменные будут знакового типа, так как для них signed
подразумевается по умолчанию). При записи в битовое поле значения более широкого в битах типа старшие “лишние” биты значения отбрасываются.
Способ размещения битовых полей в памяти определяется реализацией. Поэтому при написании кросс-платформенного кода для обеспечения конкретного расположения битовых полей в памяти вместо синтаксиса битовых полей рекомендуется использовать поразрядные операции (сдвиги, наложения масок) явно.
Битовое поле может быть безымянным. Такие поля можно использовать для выравнивания. Безымянное битовое поле может иметь условную ширину 0 бит. Такое поле указывает компилятору, что следующее поле начинается, по крайней мере, с нового байта (зависит от реализации).
Нельзя получить адрес битового поля или ссылку на него.
Язык C (и, по наследству, C++) допускает определение функций, принимающих переменное количество параметров. Это довольно низкоуровневый механизм, и не существует стандартного способа определить, сколько параметров было помещено в стек вызовов вызывающей функцией или какого они типа, поэтому обычно эта информация в некоторой форме передаётся первым параметром. Примерами таких функций являются стандартные функции printf и scanf. Общий синтаксис заголовка функции с переменным числом параметров имеет следующий вид:
Тип-результата Имя-функции (
Обязательные-параметры , ...)
Обязательные параметры объявляются так же, как в обычных функциях. Формально C++ (но не C) позволяет объявлять функции с переменным числом параметров, которые не принимают обязательных параметров, но определить такую функцию в рамках стандарта языка не получится.
При вызове функции с переменным числом параметров необходимо указать значения всех обязательных параметров и далее можно указать произвольное (от нуля до некоторого максимума, зафиксированного компилятором) количество дополнительных параметров, которые могут иметь разный тип. При этом при вызове значения типа float автоматически приводятся к double, значения целочисленных типов меньшей ширины, чем int (например, bool, char, short), приводятся к int.
Для извлечения дополнительных параметров из стека вызовов стандартом предусмотрен следующий набор определений, размещённый в заголовочном файле cstdarg:
(args, last)
— макрос, инициализирующий переменную args
типа va_list, last
— имя последнего из обязательных параметров. Данный макрос можно применить лишь однажды за вызов функции (иначе UB).(args1, args2)
[C99, C++11] — макрос, выполняющий копирование args2
в args1
(оба параметра — переменные типа типа va_list). Копирование позволяет повторно пройти по параметрам.(args, T)
— макрос, извлекающий следующий (в соответствии с состоянием, хранящимся в переменной args
) параметр типа T
из стека вызовов.(args)
— макрос, корректно завершающий извлечение параметров из стека. Этот макрос должен быть применён к каждой переменной типа va_list, которая до того была инициализирована с помощью va_start или va_copy (иначе UB).Пример реализации функции с переменным числом параметров типа double:
/// Длина вектора с количеством компонент comps.
/// Компоненты вектора передаются параметрами функции.
double vec_len(int comps, ...)
{
std::va_list args;
va_start(args, comps);
double s = 0.0;
while (comps-- > 0)
{
const double arg = va_arg(args, double);
s += arg * arg;
}
va_end(args);
return std::sqrt(s);
}
Кувшинов Д.Р. © 2015