Как стать автором
Обновить

Объявление и инициализация переменных в C++

Уровень сложностиСредний
Время на прочтение41 мин
Количество просмотров26K


Продолжаем серию «C++, копаем вглубь». Цель этой серии — рассказать максимально подробно о разных особенностях языка, возможно довольно специальных. Это шестая статья из серии, список предыдущих статей приведен в конце в разделе 7. Серия ориентирована на программистов, имеющих определенный опыт работы на C++. Данная статья посвящена объявлению и инициализации переменных.


Переменные являются основой любого языка программирования. В языках со статической типизацией, к которым относится C++, все переменные должны быть объявлены, их тип определяется на стадии компиляции и не может меняться в процессе работы программы. Тип переменных указывается при объявлении явно или выводится компилятором на основе типа инициализирующего выражения.


Во многих языках программирования объявление переменных относится к довольно простым темам, но вот в C++ это не совсем так. Конечно, простые варианты обычно не вызывают затруднений, но вот более сложные случаи могут озадачить даже достаточно опытных программистов. Попробуем разобраться во всех тонкостях объявления переменных. Также рассмотрим тему инициализации, так как чаще всего переменные инициализируются вместе с объявлением.


Оглавление

Оглавление


Введение
1. Базовый синтаксис объявлений
  1.1. Основной тип
  1.2. Cv-квалификатор
  1.3. Pr-спецификаторы
    1.3.1. Спецификаторы указателей
    1.3.2. Спецификаторы ссылок
    1.3.3. Использование пробелов
  1.4. Имя переменной
  1.5. Fa-спецификаторы
    1.5.1. Спецификаторы функций
    1.5.2. Спецификаторы массивов
2. Расширенный синтаксис объявлений
  2.1. Указатели, ссылки на функции и массивы
  2.2. Объявления с двумя fa-спецификаторами
  2.3. Анализ объявлений с несколькими fa-спецификаторами
  2.4. Использование указателей, ссылок на функции и массивы
  2.5. Указатели на члены класса и указатели на функции-члены класса
  2.6. Выражение типа. Простые переменные, массивы, функции
3. Основной тип
  3.1. Использование пользовательских типов
  3.2. Использование void
  3.3. Автоопределение типа
  3.4. Использование псевдонимов
  3.5. Использование decltype
4. Дополнительные подробности
  4.1. Параметры функций
  4.2. Объявление нескольких переменных в одной инструкции
  4.3. Динамические переменные
5. Дополнительные спецификаторы
  5.1. extern
  5.2. static
  5.3. inline
  5.4. thread_local
  5.5. alignas(N)
  5.6. constexpr
  5.7. consteval
  5.8. noexcept
  5.9. mutable
  5.10. Нестатические функции-члены
    5.10.1. Cv-квалификатор
    5.10.2. Ссылочные квалификаторы
    5.10.3. Специальные функции-члены и операторы приведения типа
    5.10.4. Виртуальные функции
  5.11. register
  5.12. Соглашение о вызовах
  5.13. Экспорт/импорт
6. Инициализация переменных
  6.1. Синтаксис инициализации
    6.1.1. Тривиальные типы и инициализация по умолчанию
    6.1.2. Использование символа =
    6.1.3. Агрегатная инициализация
    6.1.4. Использование круглых скобок
    6.1.5. Универсальная инициализация
  6.2. Контекст объявления переменной и инициализация
    6.2.1. Глобальные и локальные переменные
    6.2.2. Динамические переменные
    6.2.3. Члены класса
  6.3. Примеры
7. Список статей серии «C++, копаем вглубь
8. Итоги
Список литературы



Введение


Для начала отметим одну особенность объявлений. Можно написать синтаксически корректное объявление, тип переменной можно описать на естественном языке, но этот тип окажется неразрешенным в C++. Вот некоторые из таких типов: ссылка на void, указатель на ссылку, массив ссылок, массив функций, функция, возвращающая массив. Мы всегда будем описывать подобные случаи.



1. Базовый синтаксис объявлений


Базовый синтаксис инструкции объявления переменной можно описать следующим образом:


type cv-qual pr-specs name fa-specs;


В этом описании
type — основной тип,
cv-qual — сv-квалификатор,
pr-specs — список pr-спецификаторов,
name — имя переменной (идентификатор),
fa-specs — список fa-спецификаторов.


Основной тип и имя переменной являются обязательными элементами объявления, остальные опциональными. Таким образом, простейшее объявление может быть, например, таким:


int x; // x — переменная типа int

Рассмотрим все элементы объявления подробнее.



1.1. Основной тип


Возможны следующие варианты:


  1. Встроенный тип (int, double, etc.);
  2. Пользовательский тип (объявленный с помощью class, struct, union, enum, enum class);
  3. void (с некоторыми ограничениями);
  4. auto (с некоторыми ограничениями);
  5. Псевдоним типа;
  6. Конструкция decltype(expression).

Использование пользовательских типов, void, auto, псевдонимов типа и decltype будет подробнее рассмотрено в разделе 4.



1.2. Cv-квалификатор


Для сv-квалификатора (const-volatile) возможны следующие варианты: const, volatile и их комбинация const volatile. Квалификатор volatile имеет весьма специальное назначение, используется редко и в дальнейших примерах использоваться не будет. Желающие узнать о нем подробнее могут почитать Скотта Мейерса [Meyers] или поискать соответствующие материалы в Интернете. А вот квалификатор const используется весьма часто. Вот пример:


int const x; // константа типа int

Значение константы нельзя изменять после инициализации.


int const x = 42; // константа типа int
x = 0;            // ошибка

Cv-квалификатор может находиться перед основным типом. Следующее объявление эквивалентно предыдущему:


const int x; // константа типа int

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


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



1.3. Pr-спецификаторы


Pr-спецификаторы (pointer-reference) подразделяются на две группы — спецификаторы указателей и спецификаторы ссылок. Pr-спецификаторы применяются справа налево.



1.3.1. Спецификаторы указателей


Спецификатор указателя может иметь две формы:


  1. *;
  2. *cv-qual.

Количество спецификаторов не ограничено, но больше двух используется крайне редко. Обе формы можно произвольно комбинировать, но обычно, если есть спецификатор с cv-квалификатором, то он один и является самым правым в списке. Вот примеры:


int *x;  // указатель на int
int **x; // указатель на указатель на int
int **const x; // константный указатель на указатель на int
const int *x;  // указатель на константу типа int
int *const x;  // константный указатель на int
const int *const x; // константный указатель на константу типа int

Итак, мы видим, что cv-квалификатор при объявлении указателя может появиться более одного раза. Причина кроется в двойственной природе указателя. С одной стороны главная задача указателя служить ссылкой для какой-то переменной (здесь ссылка в широком смысле, то есть некоторый объект, служащий для доступа к другому объекту) и эта переменная может иметь cv-квалификатор. С другой стороны тип указателя является полноценным типом в С++ и, соответственно, переменная типа указатель может иметь свой cv-квалификатор. Разницу покажем на примерах.


Рассмотрим объявления:


const int x = 42;   // константа типа int
const int *px = &x; // указатель на константу типа int

Мы не можем изменить значение x через px, но можем изменить сам указатель px.


*px = 0;      // ошибка
px = nullptr; // OK

Теперь, рассмотрим объявления:


int x = 42;        // переменная типа int
int *const px= &x; // константный указатель на int

Мы можем изменить значение x через px, но не можем изменить сам указатель px.


*px = 0;      // OK
px = nullptr; // ошибка

И, наконец, рассмотрим объявления, в которых объединяются оба варианта константности:


const int x = 42;         // константа типа int
const int *const px = &x; // константный указатель
                          // на константу типа int

Ограничения объединяются — мы не можем изменить значение x через px и не можем изменить сам указатель px.


*px = 0;      // ошибка
px = nullptr; // ошибка


1.3.2. Спецификаторы ссылок


Спецификатор ссылки может иметь две формы:


  1. &;
  2. &&.

Второй вариант называется rvalue-ссылкой (появился в C++11), первый просто ссылкой или lvalue-ссылкой. Если спецификатор ссылки есть, то он может быть только один, должен быть самым правым в списке pr-спецификаторов, иначе формально мы получим указатель или ссылку на ссылку, а такой тип является в C++ неразрешенным. Вот примеры:


int ℞   // ссылка на int
int &℞  // rvalue-ссылка на int
int *℞  // ссылка на указатель на int
int &*rx;  // ошибка, указатели на ссылку не разрешены
int & ℞ // ошибка, ссылки на ссылку не разрешены

Ссылкам посвящена одна из предыдущих статей серии.



1.3.3. Использование пробелов


В C++ коде символы * и & относятся к классу разделителей и поэтому их можно не отделять от основного типа, cv-квалификатора и идентификатора пробелом. Также можно не разделять пробелом сами pr-спецификаторы (кроме неразрешенного типа ссылки на ссылку). Следующие объявления синтаксически корректны и эквивалентны.


int *x;  // указатель на int
int* x;  // указатель на int
int * x; // указатель на int
int*x;   // указатель на int

Пробелы вставляются для лучшей читаемости кода, правила их расстановки определяются сложившимися традициями, правилами кодирования или просто личным вкусом программиста.



1.4. Имя переменной


Имя переменной (идентификатор) должен отвечать правилам, предусмотренным для идентификаторов.



1.5. Fa-спецификаторы


Fa-спецификаторы (function-array) подразделяются на две группы: спецификаторы функций и спецификаторы массивов.



1.5.1. Спецификаторы функций


Спецификатор функции имеет вид:


(params)


В этом описании params — список параметров (параметры функции подробнее рассмотрены в разделе 3.1). Список параметров может быть пустым. Остальная часть объявления описывает тип возвращаемого значения функции. Возвращаемое значение может иметь тип void, это означает, что функция ничего не возвращает.


Спецификатор функции может быть только один и он не может комбинироваться со спецификатором массива, так как в этом случае формально мы получим функцию, возвращающую либо функцию, либо массив, а такой тип является в C++ неразрешенным (см. алгоритм разбора объявлений с несколькими fa-спецификаторами в разделе 2.3).


Вот примеры:


int Foo(int x); // функция, принимающая int и возвращающая int
void Foo(double y, int x); // функция, принимающая double и int
                           // и ничего не возвращающая
int *Foo(); // функция, ничего не принимающая и возвращающая
            // указатель на int
int Foo(int x)(char y); // ошибка, функция, принимающая int
// и возвращающая функцию, принимающую char и возвращающую int


1.5.2. Спецификаторы массивов


Спецификатор массива может иметь две формы:


  1. [N];
  2. [].

В первом случае N — это целочисленное выражение, вычисляемое при компиляции, его значение называется размером массива, размер должен быть больше нуля. Во втором случае компилятор определяет размер массива на основе списка инициализации (см. раздел 6.1), но и в этом случае он должен быть больше нуля. Остальная часть объявления описывает тип элементов массива. Типом элементов массива не может быть void или ссылочный тип. Размер массива является составной частью типа массива, два массива с элементами одного и того же типа, но с разными размерами являются разными типами.


Спецификатор массива не может комбинироваться со спецификатором функции, так как в этом случае мы формально получим массив функций, а такой тип является в C++ неразрешенным (см. алгоритм разбора объявлений с несколькими fa-спецификаторами в разделе 2.3).


Вот примеры:


int x[4];  // массив размера 4 с элементами типа int
int x[] = {1, 2, 3, 4}; // массив размера 4 с элементами типа int
int *x[4]; // массив размера 4 с элементами типа указатель на int
int &x[4]; // ошибка, массив ссылок
void x[4]; // ошибка, массив void
int x[4](int); // ошибка, массив функций,
               // принимающих int и возвращающих int

Может быть несколько спецификаторов массива, в этом случае объявляется массив массивов или многомерный массив. Спецификаторы применяются слева направо. Например, инструкция


int x[4][2];

объявляет массив размера 4, каждый элемент которого является массивом размера 2 с элементами типа int.


Массивам посвящена одна из предыдущих статей серии.



2. Расширенный синтаксис объявлений


Базовый синтаксис объявлений не позволяет объявлять некоторые типы, например, указатели на функцию. Дело в том, что fa-спецификаторы имеют более высокий приоритет, чем pr-спецификаторы и поэтому инструкция


int *Foo();

объявляет функцию, возвращающую указатель на int, а не указатель на функцию, возвращающую int.
Для решения этой проблемы применяется расширенный синтаксис, в котором используется круглые скобки, определяющие приоритет применения спецификаторов.



2.1. Указатели, ссылки на функции и массивы


Указатели и ссылки на функции являются важной частью системы типов C++, так как функции не могут иметь параметры или возвращаемое значение типа функции (а без этого невозможно функциональное программирование), но вместо этого можно использовать указатели или ссылки на функцию. Также функции не могут иметь параметры или возвращаемое значение типа массива и элементы массивов не могут иметь тип функции. Эти ограничения также можно преодолеть с помощью указателей и ссылок.


Синтаксис инструкции объявления указателя, ссылки на функцию или массив можно описать следующим образом:


type cv-qual pr-specs2(pr-specs name)fa-specs;


В этом описании
type cv-qual pr-specs2 — описывает тип возвращаемого значение функции или тип элемента массива,
pr-specs — список pr-спецификаторов функции или массива,
name — имя функции или массива,
fa-specs — список fa-спецификаторов.


Вот примеры, связанные с функциями:


int (*pf)(int); // указатель на функцию,
                // принимающую int и возвращающую int
int (&rf)(int); // ссылка на функцию,
                // принимающую int и возвращающую int
int (*const cpf)(int); // константный указатель на функцию,
                       // принимающую int и возвращающую int
int (**ppf)(int); // указатель на указатель на функцию,
                  // принимающую int и возвращающую int
int *(*pf)(int); // указатель на функцию,
                 // принимающую int и возвращающую указатель на int

Вот примеры, связанные с массивами:


int (*pa)[4]; // указатель на массив
              // размера 4 с элементами типа int
int (&ra)[4]; // ссылка на массив
              // размера 4 с элементами типа int
int *(*pa)[4]; // указатель на массив размера 4
               // с элементами типа указатель на int


2.2. Объявления с двумя fa-спецификаторами


Наиболее замысловатые объявления получаются, когда в объявлении используются два или более типа с fa-спецификаторами, например, функция, возвращающая указатель на функцию или массив указателей на функцию.


Напомним, что тип возвращаемого значение функции не может быть типом функции или массива, а тип элементов массива не может быть типом функции или типом ссылки.


Рассмотрим достаточно общий случай объявления переменной с двумя fa-спецификаторами. Вот описание инструкции объявления следующих типов: функции, возвращающие указатель или ссылку на функцию; функции, возвращающие указатель или ссылку на массив; массива с элементами типа указатель на функцию или указатель на массив:


type cv-qual pr-specs3(pr-specs2 name fa-specs)fa-specs2;


В этом описании
type cv-qual pr-specs3 — описывает тип возвращаемое значение или тип элемента типа возвращаемого значения или типа элемента,
pr-specs2 — список pr-спецификаторов типа возвращаемого значения или типа элемента,
name — имя переменной,
fa-specs — список fa-спецификаторов (первичных),
fa-specs2 — список fa-спецификаторов типа возвращаемого значения или типа элемента.


Звучит несколько запутанно, но ничего не поделать. Если, например, у нас функция, возвращающая указатель на функцию, мы имеем первичное возвращаемое значение — указатель на функцию, которая, в свою очередь, имеет возвращаемое значение, откуда и получается тип возвращаемого значения типа возвращаемого значения. Основные трудности расшифровки таких объявлений связаны с тем, что fa-спецификаторы типа возвращаемого значения функции или типа элемента массива находятся правее, чем первичные fa-спецификаторы.


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


int (*f(int))(const char*); // функция, принимающая int
                  // и возвращающая указатель на функцию,
                  // принимающую const char* и возвращающую int
int (&f(int))(const char*); // функция, принимающая int
                  // и возвращающая ссылку на функцию,
                  // принимающую const char* и возвращающую int
int (*f(int))[4]; // функция, принимающая int
                  // и возвращающая указатель на массив
                  // размера 4 с элементами типа int
int (&f(int))[4]; // функция, принимающая int
                  // и возвращающая ссылку на массив
                  // размера 4 с элементами типа int
int (*a[2])(const char*); // массив размера 2
                  // с элементами типа указатель на функцию,
                  // принимающую const char* и возвращающую int
int (*a[2])[4]; // массив размера 2
                // с элементами типа указатель на массив
                // размера 4 с элементами типа int


2.3. Анализ объявлений с несколькими fa-спецификаторами


Рассмотрим относительно простой алгоритм, который позволяет разобраться с объявлениями, которые включают несколько fa-спецификаторов. Будем считать, что анализируемые объявления синтаксически корректны и не содержат ненужных скобок. Также будем считать, что число fa-спецификаторов больше одного.


Рассмотренный алгоритм является достаточно формальным и не гарантирует, что полученный тип будет разрешенным в C++. Например, в случае двух fa-спецификаторов возможны следующие неразрешенные типы: функция, возвращающая функцию или массив, массив функций, массив ссылок на функцию или массив (см. пример 3).


Шаг 1. Ищем идентификатор. Если после идентификатора нет закрывающей круглой скобки ), то смотрим fa-спецификатор. Часть объявления, включая идентификатор и следующий за ним fa-спецификатор, назовем корнем объявления. Если после идентификатора есть закрывающая круглая скобка ), то ищем соответствующую открывающую скобку, затем после закрывающей скобки смотрим fa-спецификатор и корнем в этом случае будет часть объявления начиная с открывающей скобки и заканчивая fa-спецификатором. Таким образом, корень объявления будет соответствовать одному из двух описаний:


name fa-specs
(pr-specs name)fa-specs


Шаг 2. Заменяем корень некоторым фиктивным идентификатором. Тип этого идентификатора будет типом возвращаемого значения в случае функции и типом элемента в случае массива. Если полученное объявление содержит один fa-спецификаторы, то процесс завершается, такое объявление является либо объявлением функции, либо объявлением массива, либо объявлением одного из типов, рассмотренных в разделе 2.1. Если полученное объявление содержит более одного fa-спецификатора, то шаг 1 надо еще раз применяется к этому объявлению.


Пример 1.


int (*(*f(char))(int))(const char*);

Корень этого объявления:


f(char)

Это функция, принимающая char. Заменяем корень фиктивной переменной ff и получаем объявление для возвращаемого значения этой функции:


int (*(*ff)(int))(const char*);

Это объявление содержит два fa-спецификатора, поэтому продолжаем анализ. Корень этого объявления:


(*ff)(int)

Это указатель на функцию, принимающую int. Заменяем корень фиктивной переменной fff и получаем объявление для возвращаемого значения этой функции:


int (*fff)(const char*);

Это объявление содержит один fa-спецификатор, оно объявляет указатель на функцию, принимающую const char* и возвращающую int. Анализ закончен.


Собираем все вместе и получаем функцию, которая принимает char и возвращает указатель на функцию, которая принимает int и возвращает указатель на функцию, которая принимает const char* и возвращает int. (Вот кот, который пугает и ловит синицу, которая часто ворует пшеницу, которая в тёмном чулане хранится в доме, который построил Джек. С. Маршак.)
Пример 2.


int (*const a[2])(int);

Корень этого объявления:


a[2]

Это массив размера 2. Заменяем корень фиктивной переменной aa и получаем объявление для элемента этого массива:


int (*const aa)(int);

Это объявление содержит один fa-спецификатор, оно объявляет константный указатель на функцию, принимающую int и возвращающую int. Анализ закончен.


Собираем все вместе и получаем массив размера 2 с элементами типа константный указатель на функцию, принимающую int и возвращающую int.


Пример 3.


int f(int)[4];

Корень этого объявления:


f(int)

Это функция, принимающая int. Заменяем корень фиктивной переменной ff и получаем объявление для возвращаемого значения этой функции:


int ff[4];

Это объявление содержит один fa-спецификатор, оно объявляет массив размера 4 с элементами типа int. Анализ закончен.


Собираем все вместе и получаем функцию, принимающую int и возвращающую массив размера 4 с элементами типа int. Этот тип является неразрешенным в C++.



2.4. Использование указателей, ссылок на функции и массивы


Для вызова функции через указатель можно не разыменовывать этот указатель. Пусть у нас объявлен указатель на функцию


int (*pf)(int);

Вызов соответствующей функции может быть сделан так:


int r = pf(42);

Следующую функцию


int (*f(int))(const char*);

можно использовать так:


int r = f(42)("meow");

А такую функцию


int (*(*f(char))(int))(const char*);

так:


int r = f('S')(42)("meow");

В функциональном программировании такой синтаксис вызова функций называется каррированием.


При инициализации указателя на функцию можно не использовать оператор взятия адреса &.


int Foo(int);
int (*pf)(int) = Foo;

Описанные особенности позволяют отнести указатели на функцию к функциональным типам, то есть типам, экземпляры которых позволяют использовать синтаксис вызова функции. Функциональные типы широко используются при написании шаблонов.


Эти же особенности делают практически ненужными ссылки на функцию, никаких преимуществ у ссылок нет, а ограничения ссылочного типа есть (например, нельзя объявить массив ссылок).


Теперь рассмотрим указатели на массив. В этом случае разыменование необходимо. Например, для указателя на массив


int (*pa)[4];

доступ к элементу осуществляется так:


(*pa)[0] = 125;

Скобки нужны, так как у индексатора более высокий приоритет, чем у оператора разыменования *.


При инициализации указателя на массив также необходимо использовать оператор взятия адреса &.


int a[4];
int (*pa)[4] = &a;

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


int a[4];
int (&ra)[4] = a;
ra[0] = 125;


2.5. Указатели на члены класса и указатели на функции-члены класса


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


Синтаксис инструкции объявления указателя на нестатический член класса можно описать следующим образом:


type cv-qual pr-specs2 class-name::pr-specs1 name;


В этом описании
type cv-qual pr-specs2 — описывает тип члена,
class-name — имя класса,
pr-specs1 — спецификатор указателя на член (обязательный элемент, должен содержать спецификатор указателя),
name — имя переменной.


Если член класса объявлен с использованием fa-спецификаторов (указатель, ссылка на функцию или массив), то необходимо использовать более сложную форму объявления с использованием скобок. Она довольно громоздка и мы не будем ее приводить.
Синтаксис инструкции объявления указателя на нестатическую функцию-член класса можно описать следующим образом:


type cv-qual pr-specs2(class-name::pr-specs1 name)(params)qual;


В этом описании
type cv-qual pr-specs2 — описывает тип возвращаемого значения функции-члена,
class-name — имя класса,
pr-specs1 — спецификатор указателя на функцию-член (обязательный элемент, должен содержать спецификатор указателя),
name — имя переменной,
params — список параметров функции-члена (может быть пустым),
qual — cv-квалификатор или ссылочный квалификатор функции-члена (необязательный, см. разделы 5.9.1, 5.9.2).


Если возвращаемое значение функции-члена описывается с использованием fa-спецификаторов (указатель, ссылка на функцию или массив), то необходимо использовать более сложную форму объявления с дополнительным использованием скобок. Она довольно громоздка и мы не будем ее приводить.


Указатели на члены класса и указатели на функции-члены класса инициализируются именем члена или функции- члена с квалификатором класса и использованием оператора взятия адреса &.


Приведем примеры.


class X
{
public:
    double D;
    int F(int);
    int G(int) const;
// ...
};

double X::*pm = &X::D; // указатель на член класса X типа double
int (X::*pf)(int) = &X::F; // указатель
  // на функцию-член класса X, принимающую int и возвращающую int
int (X::*pfс) (int) const = &X::G; // указатель на константную
  // функцию-член класса X, принимающую int и возвращающую int
int (X::*const cpf)(int) = &X::F; // константный указатель
  // на функцию-член класса X, принимающую int и возвращающую int
int (X::*&rpf)(int) = pf; // ссылка на указатель
  // на функцию-член класса X, принимающую int и возвращающую int

При использовании таких указателей должен быть доступен экземпляр соответствующего класса. Для доступа к члену и функции-члену используется бинарный оператор .*, первый операнд которого имеет тип этого класса, а второй операнд тип указателя на член или функцию-член. Также имеется бинарный оператор ->*, который отличается тем, что первый операнд имеет тип указателя на этот класс.


X x;
x.*pm = 3.14;
int r = (x.*pf)(42);
X *px = &x;
px->*pm = 0.04;
r = (px->*pf)(125);

Для статических членов класса специальные типы не нужны, используются обычные указатели и указатели на функции.



2.6. Выражение типа. Простые переменные, массивы, функции


Если мы возьмем объявление некоторой переменной, удалим из него имя переменной и заключительную точку с запятой, то получим конструкцию, которую можно назвать выражением типа. Это выражение можно использовать в качестве аргумента шаблона, в качестве неименованного параметра функции (см. раздел 4.1), в качестве типа динамической переменой (см. раздел 4.3) и в инструкции объявления псевдонима (см. раздел 3.4). В зависимости от контекста использования в выражении типа может быть запрещено использование auto. Выражение типа также часто встречается в сообщениях компилятора.


Описанный синтаксис объявления переменных позволяет разбить переменные на три группы: простые переменные (объявленные без первичных fa-спецификаторов), массивы и функции. В некоторых случаях, необходимо уточнять для какой группы действуют те или иные правила.



3. Основной тип


В разделе раскрываются некоторые дополнительные детали использования основного типа.



3.1. Использование пользовательских типов


К пользовательским типам относятся типы, определенные с помощью ключевых слов class, struct, union, enum, enum class.


Чаще всего отдельно определяют тип и после этого объявляют переменные, использующие имя этого типа в качестве основного типа. Вот пример:


struct Point
{
    int X;
    int Y;
};

Point pt;

Но можно определение пользовательского типа вставить непосредственно в инструкцию объявление переменной в качестве основного типа, например:


struct { int X; int Y; } pt;

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


Еще один вариант использования пользовательских типов связан с неполными типами. Дело в том, что в ряде случаев компилятору для компиляции правильного кода достаточно знать, что используемое имя является именем какого-то пользовательского типа, а полное определение типа не нужно. В этом случае можно использовать неполное объявление (incomplete declaration), называемое еще упреждающим (forward declaration). Типы с неполным объявлением называются неполными. С некоторыми ограничениями неполные типы можно использовать в качестве основного типа при объявлении переменных. Можно объявлять указатели и ссылки на неполный тип, функции с параметрами и возвращаемым значением неполного типа, указатели на члены и функции-члены неполного класса. Нельзя объявить переменные и массивы неполного типа. Вот примеры:


struct Point; // неполное объявление типа Point
Point *ppt;   // указатель на Point
void Foo(Point pt); // функция, принимающая Point
Point pt;     // ошибка, переменная неполного типа Point


3.2. Использование void


С некоторыми ограничениями тип void можно использовать в качестве основного типа при объявлении переменных. Этот тип может быть использован в качестве типа возвращаемого значения функций, также можно объявлять указатели на void. Нельзя объявлять переменные типа void, массивы с элементами типа void, ссылки на void. Вот примеры:


void Foo(int x); // функция, принимающая int
                 // и ничего не возвращающая
void *pv;        // указатель на void
void &rv;        // ошибка, ссылка на void
void v;          // ошибка, переменная типа void


3.3. Автоопределение типа


Большинство современных языков программирования со статической типизацией имеют возможность не указывать явно тип переменных, а предоставить вывод типа компилятору, который решает эту задачу исходя из типа инициализирующего выражения. В C++11 также появилась такая возможность, для этого в качестве основного типа надо использовать ключевое слово auto. (Это ключевое слово появилось в первых версиях C, но его использование оказалось излишним, и в C++11 его решили использовать по-новому.)


С auto можно использовать сv-квалификатор, pr-спецификаторы и спецификатор функции. Нельзя объявлять массивы, нестатические члены класса, статические члены класса без инициализации. Также нельзя объявлять параметры функций (кроме параметров лямбда-выражения). Если объявляется несколько переменных в одной инструкции, то для них выводимый основной тип должен быть один и тот же.


Использование auto имеет некоторые не вполне очевидные особенности. При выводе типа переменой снимается ссылочность и сv-квалификатор инициализирующего выражения. То есть в объявлении


auto x = expression;


тип переменной x никогда не будет иметь тип ссылки или константы. Если выражение expression имеет ссылочный тип, то будет создана копия экземпляра, на который эта ссылка ссылается. Спецификатор ссылки и сv-квалификатор надо указывать явно.


auto &r = expression;
const auto c = expression;
const auto &rc = expression;


Эту особенность важно учитывать при работе с контейнерами, так как при использовании итераторов перегруженный оператор * будет возвращать ссылку на элемент контейнера. Также возвращают ссылку на элемент такие функции-члены стандартных контейнеров как индексаторы, at(), front(), back().


Инструкция


auto &&rr = expression;


объявляет универсальную ссылку (universal reference), в этом случае тип переменной rr может быть выведен как rvalue-ссылка, так и как lvalue-ссылка в зависимости от категории (lvalue/rvalue) выражения expression, детали можно найти у Скотта Мейерса [Meyers].


Вот как с помощью auto можно создать экземпляра класса, у которого есть доступный конструктор с несколькими параметрами:


auto x = class-name(args);


В C++17 класс class-name не обязан поддерживать копирование или перемещение. Список аргументов args может быть пустым, скобки могут быть фигурными.


Иногда использование auto является безальтернативным, например, при объявлении замыкания с помощью лямбда-выражения. Вот пример:


auto square = [](int x) { return x * x; }

Несколько дополнительных вариантов использования auto появилось в C++14.


Стало возможным использовать auto в качестве возвращаемого значения функции, в этом случае тип возвращаемого значения определяется по типу выражения в инструкции return. Вот пример:


auto Square(int x) { return x * x; }

Понятно, что в этом случае нельзя только объявить функцию, должно быть определение, то есть тело функции.
Стало возможным использовать auto в качестве типа параметров лямбда-выражения. Вот пример:


[](auto x) { return x > 0; }

Появилась еще одна форма объявления функции — это объявление функции с завершающим возвращаемым типом (trailing return type), которое выглядит так:


auto name(params) -> return-type;


На самом деле здесь никакого автоопределения типа нет, возвращаемый тип указан явно. Эта форма нужна для написания некоторых шаблонов, в которых возвращаемый тип зависит от параметров шаблона.


Подробнее про auto можно почитать у Скотта Мейерса [Meyers].



3.4. Использование псевдонимов


В C++ для любого типа можно объявить псевдоним (alias). Есть два способа: первый, унаследованный из C, использует ключевое слово typedef, второй, появившейся в C++11, использует ключевое слово using.


Первый способ можно описать так: надо объявить переменную (без использования auto) и добавить в начало инструкции typedef, после этого имя переменной становится именем псевдонима для типа этой переменной. Вот примеры:


typedef int INT;     // int
typedef int *PINT;   // указатель на int
typedef int INT4[4]; // массив размера 4 с элементами типа int
typedef int (*PF)(int); // указатель на функцию
                        // принимающую int и возвращающую int

С помощью typedef в одной инструкции можно объявить несколько псевдонимов, такой стиль характерен для C.


В инструкции объявления псевдонима с помощью ключевого слова using используется выражения типа (см. раздел 2.6 ). В выражении типа нельзя использовать auto. (Выражение типа получается из инструкции объявления переменной после удаления имени переменной.) Инструкцию объявления псевдонима в этом случае можно описать следующим образом:


using alias-name = type-expression;


В этом описании
alias-name — имя псевдонима,
type-expression — выражение типа.


Предыдущие примеры будут выглядеть так:


using INT = int;
using PINT = int*;
using INT4 = int[4];
using PF = int (*)(int);

При объявлении переменной псевдоним можно использовать в качестве основного типа. В ряде случаев это дает определенные преимущества.


Псевдоним вбирает в себя все pr-спецификаторы и fa-спецификаторы, поэтому при объявлении нескольких переменных в одной инструкции (см. раздел 4.2) их не нужно повторять для каждой переменной. Вот пример:


using PINT = int*;
PINT x, y, z;

Это эквивалентно объявлению


int *x, *y, *z;

Другое использование псевдонимов — это упрощение сложных объявлений из разделов 2.1 и 2.2.


Объявление


int (*pf)(int);

можно переписать так:


using F = int (int);
F *pf;

Объявление


int (**ppf)(int);

можно переписать так:


using PF = int (*)(int);
PF *ppf;

Объявление


int (*f(int))(const char*);

можно переписать так:


using PF = int (*)(const char*);
PF f(int);

Объявление


int (*(*f(char))(int))(const char*);

можно переписать так:


using PF1 = int (*)(const char*);
using PF2 = PF1 (*)(int);
PF2 f(char);

Подобным способом мы можем повысить читаемость сложных объявлений.


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



3.5. Использование decltype


В C++11 появилась конструкция decltype(expression). Это по существу псевдоним для типа выражения expression, его можно использовать в качестве основного типа при объявлениях. Вот пример:


int k = 0;
decltype(k) *pk; // указатель на int

Это конструкция востребована при написании некоторых шаблонов.


В C++14 в качестве типа возвращаемого значения функции можно использовать decltype(auto), в этом случае вывод типа возвращаемого значения происходит немного по другим правилам, по сравнению с auto, например, ссылочность и константность не снимается.


Подробнее про использование decltype можно почитать у Скотта Мейерса [Meyers].



4. Дополнительные подробности



4.1. Параметры функций


При объявлении функции, указателя или ссылки на функцию нужно указать список параметров в круглых скобках (список может быть пустым). Каждый параметр — это объявление переменной по правилам, описанным выше, если параметров несколько, то они разделяются запятыми. Есть ограничение — при объявлении параметра нельзя использовать auto. (Автоопределение типа параметров реализовано в шаблонах функций.) Вот пример:


int Foo(int x, double *py, int (*pf)(int));

Имя параметра указывать не обязательно, то есть можно использовать только выражение типа. В этом случае это объявление можно переписать так:


int Foo(int, double*, int (*)(int));

Вместо списка параметров может стоять ключевое слово void, это эквивалентно пустому списку параметров.


В конце списка параметров может находиться лексема ... (многоточие, ellipsis), это означает дополнительно любое количество параметров (в том числе и нулевое) любого типа. Если есть явно объявленные параметры, то перед этой лексемой может находиться запятая (это нужно для обратной совместимости с C), но по правилам C++ она не обязательна. Следующие два объявления эквивалентны:


void Foo(int x, ...);
void Foo(int x...);

Для параметров могут быть заданы значения по умолчанию. Такие параметры должны быть расположены подряд и включать последний параметр. Значения по умолчанию задаются с помощью символа = и при вызове функции соответствующие аргументы можно не задавать (опять же, параметры, для которых используются значения по умолчанию, должны идти подряд и включать последний). Для устранения синтаксической неоднозначности перед символом = в ряде случаев требуется пробел. Вот пример:


void Foo(int x = 0, int y = 0);

В этом случае могут быть вызовы:


Foo();   // Foo(0, 0);
Foo(42); // Foo(42, 0);
Foo(42, 125);

Если параметр имеет тип функции или массива, то компилятор автоматически преобразует тип таких параметров: функция преобразуется к указателю на функцию, массив к указателю на элемент. Это называется низведением (decay). (Еще можно встретить термин сведение или разложение.)


Из-за низведения функций следующие два объявления эквивалентны:


void Foo(int f(int));
void Foo(int (*f)(int));

Из-за низведения массивов следующие три объявления эквивалентны:


void Foo(int a[4]);
void Foo(int a[]);
void Foo(int *a);

Низведение функций чаще всего остается незаметным, так как вызов функции через указатель не требует разыменования (см. раздел 2.4), а вот при низведении массива теряется информация о размере и этот размер приходится передавать каким-то способом, например, отдельным параметром.


Для решения проблемы низведения массивов может оказаться полезным шаблон с использованием ссылки на массив:


template<typename T, std::size_t N>
void Foo(T (&a)[N]);

При конкретизации такого шаблона компилятор выводит тип элементов T и размер массива N (который гарантировано больше нуля). В качестве аргументов можно использовать только массивы, указатели будут отвергнуты. Этот прием используется в ряде шаблонов стандартной библиотеки (std::size(), std::begin(), etc.).



4.2. Объявление нескольких переменных в одной инструкции


В одной инструкции можно объявить несколько переменных, эти переменные разделяются запятыми. Вот пример:


const int x, *y, z[4], *f(int);

При этом действует правило: основной тип вместе с сv-квалификатором будет применяться к каждой объявленной переменной, а вот pr-спецификаторы и fa-спецификаторы только к «своей» переменной. (Это правило обосновывает один из стилей расстановки пробелов — pr- и fa-спецификаторы не отделяются от имени переменной пробелом, но pr–спецификаторы отделяются пробелом от типа и сv-квалификатора.) Соответственно, эта инструкция эквивалентна следующим четырем инструкциям:


const int x;    // константа типа int
const int *y;   // указатель на константу типа int
const int z[4]; // массив размера 4 с элементами типа
                // константа типа int
const int *f(int); // функция, принимающая int и возвращающая
                   // указатель на константу типа int

Таким образом, мы имеем возможность объявить в одной инструкции переменные разных типов. Но данную возможность не рекомендуется использовать, в одной инструкции следует объявлять переменные только одного типа. Также не следует объявлять несколько функций в одной инструкции. Эти рекомендации позволят улучшить читаемость кода, уменьшить количество потенциальных ошибок.


Если одни и те же спецификаторы повторяются для каждой переменной, то можно использовать псевдоним, чтобы избежать повторений (см. раздел 3.4).



4.3. Динамические переменные


Память, в которой размещают простые переменные и массивы, подразделяется на статическую, автоматическую (стек) и динамическую. До сих пор мы рассматривали объявления переменных, размещаемых в статической и автоматической памяти, в этом случае процесс создание и удаление переменных берет на себя исполняющая система C++. Динамические переменные создаются и удаляются программистом «вручную». Для создания переменной используется оператор new, который принимает тип переменой и возвращает указатель на созданную переменную. Вот пример:


std::string *d = new std::string;

Для задания типа динамической переменной можно использовать имя встроенного или пользовательского типа, псевдоним типа, а также выражение типа. Выражение типа может содержать auto и decltype(expression). Если выражение типа содержит круглые скобки, то само выражение надо заключить в круглые скобки. Нельзя использовать функции, ссылочные типы, определения пользовательского типа. Для void и auto действуют ограничения на использование, рассмотренные выше. Для объявления переменной, в которой хранится указатель, возвращаемый оператором new, удобно использовать auto. Вот примеры:


auto d1 = new void*; // указатель на void
auto d2 = new (int(*)(int)); // указатель на функцию
                             // принимающую int и возвращающую int
auto d3 = new struct {int X; int Y; }; // ошибка,
                             // определение пользовательского типа

Можно создавать динамические массивы, в этом случае выражение типа должно быть массивом, но его размер не обязательно должен быть известен на стадии компиляции, это может быть любое целочисленное выражение, имеющее неотрицательное значение (ноль является допустимым значением). Оператор new в этом случае возвращает указатель на первый элемент. Если выражение типа содержит несколько спецификаторов массива, то спецификатор с динамическим размером должен быть один и быть самым левым, для остальных спецификаторов размер должен быть известен во время компиляции (то есть массив может быть динамическим только по одному измерению, по первому). Размер динамического массива не является составной частью типа динамического массива, его надо хранить отдельно. Вот примеры:


int n = 12;
int *d1 = new int[n];    // динамический массив элементов типа int
auto d2 = new int[n][4]; // динамический массив,
            // каждый элемент которого является обычным массивом
            // размера 4 с элементами типа int.
auto d3 = new (int(*[n])(int)); // динамический массив элементов
// типа указатель на функцию, принимающую int и возвращающую int.

Для удаления динамических переменных используется оператор delete и для массивов оператор delete[].



5. Дополнительные спецификаторы


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



5.1. extern


Этот спецификатор означает, что объявление является по существу ссылкой на определение переменной, которое должно быть где-то в том же модуле. Применяется к простым переменным и массивам, которые объявлены глобально, в области видимости пространства имен или локально. Пусть, например, есть объявление


extern int x;

В этом случае компоновщик будет искать в модуле определение


int x;

Если определение не найдено, или их будет больше одного, то компоновщик выдаст ошибку.


Этот спецификатор можно применить к объявлениям функций, но это практически ничего не дает, так как функции и без того используются описанным образом.



5.2. static


Смысл этого спецификатора зависит от контекста объявления переменной.


Если переменная объявлена глобально или в области видимости пространства имен, то этот спецификатор означает локальное связывание переменной, то есть эта переменная не будет видима из других единиц компиляции.


При объявлении простой переменной или массива внутри функции этот спецификатор означает, что эта переменная сохраняет свое состояние между вызовами этой функции.


При объявлении в классе этот спецификатор означает, что член или функция-член, являются общими для всех экземпляров класса, то есть не используют скрытый параметр this.



5.3. inline


Применяется к функциям, а в C++17 и к статическим членам класса.


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


Если этот спецификатор применить к статическому члену класса, то такой член можно инициализировать при объявлении независимо от типа, а если есть определение, то компоновщик в этом случае не будет считать ошибкой наличие несколько определений этого члена в разных единицах компиляции и будет использовать одно из них во всем модуле.



5.4. thread_local


Этот спецификатор появился в C++11. Применяется к простым переменным и массивам, которые объявлены глобально или в области видимости пространства имен. Переменная, объявленная с этим спецификатором, доступна только в потоке, в котором она создана. Переменная создается при создании потока и удаляется при уничтожении потока. Каждый поток имеет свою собственную копию переменной.



5.5. alignas(N)


Этот спецификатор появился в C++11. Применяется к простым переменным, массивам и классам. N — это выражение, вычисляемое при компиляции, его значение должно быть степенью двойки. Переменная при этом будет размещена по адресу, кратному значению N. Например:


alignas(64) char cacheline[64];


5.6. constexpr


Этот спецификатор появился в C++11. Применяется к простым переменным, массивам, функциям и функциям-членам. Для простой переменной или массива это означает, что ее значение вычисляется на этапе компиляции и не может быть изменено. Для функции это означает, что ее возвращаемое значение вычисляется на этапе компиляции, если значения аргументов известно на стадии компиляции. (Но такую функцию можно использовать и с обычными аргументами.) Вот пример:


constexpr double PI = 3.1415926535897932;
constexpr int Square(int x) { return x * x; }


5.7. consteval


Этот спецификатор появился в C++20. Применяется к функциям и функциям-членам. Это более строгий вариант constexpr, при вызове такой функции значения аргументов должны быть всегда известны на стадии компиляции.



5.8. noexcept


Этот спецификатор появился в C++11. Применяется к функциям и функциям-членам и располагается в конце инструкции, после списка параметров. Этот спецификатор гарантирует отсутствие исключений в процессе выполнения тела функции.



5.9. mutable


Применяются к нестатическим членам класса, такие члены можно изменять в константных функциях-членах (см. раздел 5.10.1).



5.10. Нестатические функции-члены


Нестатические функции-члены могут иметь несколько особых спецификаторов и квалификаторов. Все они, кроме virtual и explicit, располагаются в конце инструкции, после списка параметров.



5.10.1. Cv-квалификатор


Cv-квалификатор функции-члена будет относится к значению, на которое указывает скрытый параметр this, то есть значение будет иметь этот квалификатор временно, только во время выполнения тела функции. Квалификатор const будет означать, что функция не может изменять члены класса, то есть не может изменять битовый образ переменной, для которой вызывается. Можно сказать, что это read-only функция. Вот пример:


class X
{
public:
    int GetCount() const;
// ...
};

Спецификатор mutable, примененный к нестатическим членам, снимает это ограничение, то есть такие члены разрешено модифицировать в константных функциях-членах (см. раздел 5.8).



5.10.2. Ссылочные квалификаторы


Ссылочные квалификаторы & и && (появились в C++11) позволяют перегружать функции по категории (lvalue/rvalue) значения, на которое указывает скрытый параметр this.


class X
{
public:
    X();
    void Foo() &;  // будет вызвана для lvalue
    void Foo() &&; // будет вызвана для rvalue
// ...
};

X x;
x.Foo();   // Foo() &, lvalue
X().Foo(); // Foo() &&, rvalue


5.10.3. Специальные функции-члены и операторы приведения типа


К специальным функциям-членам относятся конструктор, деструктор, копирующий конструктор и оператор копирующего присваивания. В C++11 к ним добавились перемещающий конструктор и оператор перемещающего присваивания.


Спецификатор =default (появился в C++11) означает, что данная функция должна быть сгенерирована компилятором.


Спецификатор =delete (появился в C++11) означает, что вызовы данной функции запрещены, в том числе и скрытые от программиста. (Спецификатор =delete также можно применять к любым функциям-членам и свободным функциям, это используется при тонкой настройке вызовов перегруженных функций.)


Спецификатор explicit (располагается в начале инструкции) применяется к конструктору, допускающему вызов с одним аргументом. Это означает, что запрещено неявное преобразование типа соответствующего параметра в тип класса, в котором этот конструктор определен. В C++11 explicit можно применять к любому конструктору, в этом случае будут запрещены некоторые варианты универсальной инициализации с использованием списка в фигурных скобках (см. раздел 6.1.5). Также этот спецификатор можно применить к оператору приведения типа. В этом случае запрещено неявное преобразование типа, определяемое этим оператором.



5.10.4. Виртуальные функции


Есть несколько спецификаторов, которые используются только с виртуальными функциями. Спецификатор virtual (располагается в начале инструкции) означает, что эта функция виртуальная и ее можно переопределять в производных классах. Для такой виртуальной функции можно использовать дополнительный спецификатор =0, который означает, что функция объявлена чисто виртуальной и требует обязательного переопределения в производных классах. Класс, имеющий чисто виртуальные функции, называется абстрактным, нельзя создать экземпляр такого класса. Спецификатор override (появился в C++11) означает, что это виртуальная функция, которая переопределяет виртуальную функцию, объявленную в базовом классе. Спецификатор final (появился в C++11) означает, что эту виртуальную функцию нельзя переопределять в производных классах. Вот примеры:


class B
{
public:
    B();
    virtual int Foo() = 0;
// ...
};

B b; // ошибка, экземпляр абстрактного класса

class D : public B
{
public:
    int Foo() override final;
// ...
};

class E : public D
{
public:
    int Foo() override; // ошибка, переопределение final
// ...
};


5.11. register


Этот спецификатор унаследован из C, он применяется к простым переменным. В C++ этот спецификатор практически не использовался и в C++17 стал просто игнорироваться (хотя остался в списке ключевых слов).



5.12. Соглашение о вызовах


При объявлении функции, указателя или ссылки на функцию некоторые компиляторы (например MSVC) позволяют дополнительно указывать соглашение о вызовах (calling convention). Вот примеры:


int __stdcall Foo(int);
int (__stdcall *pf)(int) = Foo;

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



5.13. Экспорт/импорт


Спецификаторы экспорта/импорта можно применять к переменным, функциям и классам, они не стандартизированы и зависят от платформы и компилятора. Вот примеры из MSVC:


__declspec(dllexport) int Foo(int);
__declspec(dllexport) int Val;

Эти спецификаторы используются при формировании таблиц импорта/экспорта модуля.



6. Инициализация переменных


Почти во всех современных языках программирования имеется возможность при объявлении переменной задать начальное ее значение. Это называется инициализацией и чаще всего происходит в той же инструкции, что и объявление. Современный стиль кодирования рекомендует по возможности всегда инициализировать переменные и практически все языки программирования тем или иным способом поддерживают этот стиль. Это особенно важно для C++, так как в нем могут существовать неинициализированные переменные, содержащие случайный набор битов (тривиальные типы, привет от C), что может привести к трудно обнаруживаемым ошибкам. Переменные некоторых типов — константы, ссылки и переменные, объявленные с использованием auto, — требуют обязательной инициализации, за этим следит компилятор.


В C++11 для инициализации указателей можно использовать специальное значение nullptr. Это относится ко всем типам указателей, включая указатели на функцию и массив, указатели на члены и функции-члены класса. Использование макроса NULL является устаревшим и его следует избегать. Подробнее см. [Meyers].


Нельзя инициализировать функции. Для функции может быть либо чистое объявление, в котором указывается только имя, возвращаемое значение и параметры, либо определение, в котором добавляется тело функции.


Наконец отметим, что инициализация — это не присваивание, хотя в ряде случаев используется символ =. Присвоить значение можно только ранее созданной переменной, а инициализация происходит во время создания.



6.1. Синтаксис инициализации


В C++ есть несколько синтаксических конструкций, используемых для инициализации переменной. Почти в каждом случае программист может выбирать из нескольких возможных. Конкретный выбор определяется сложившимися традициями, правилами кодирования или просто личным вкусом программиста.



6.1.1. Тривиальные типы и инициализация по умолчанию


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


std::string s; // пустая строка

Но это не так для тривиальных типов. К тривиальным типам (эти типы введены для обратной совместимости с C) относятся числовые типы, указатели, перечисления, а также классы, структуры, объединения и массивы, состоящие из тривиальных типов. Классы и структуры должны удовлетворять некоторым дополнительным условиям: отсутствие пользовательского конструктора, деструктора, копирования, виртуальных функций. Для переменных тривиального типа, объявленных без явной инициализации, конструктор по умолчанию не вызывается (хотя компилятор умеет его генерировать) и переменные получают статус неинициализированных. В этом случае переменные содержат либо нулевые биты, либо случайный набор битов, это зависит от контекста объявления (см. раздел 6.2). Компилятор иногда определяет неинициализированные переменные и выдает предупреждение или даже ошибку. Еще лучше неинициализированные переменные находят статические анализаторы кода.


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


В стандартной библиотеке С++11 есть шаблоны, называемые свойствами типов (заголовочный файл <type_traits>). Один из них позволяет определить, является ли тип тривиальным. Выражение std::is_trivial<Т>::value имеет значение true, если T тривиальный тип и false в противном случае (см. [VJG]).


Будем называть инициализацией по умолчанию применение конструктора по умолчанию к переменной, в том числе к переменной тривиального типа. Для тривиальных типов компилятор умеет генерировать конструктор по умолчанию. Он использует соответствующий вариант нуля для переменных встроенного типа, указателей, перечислений, а для классов, структур, объединений и массивов это правило будет применяться почленно-рекурсивно. Как следует из содержания раздела, для гарантированной инициализации по умолчанию требуются какая-то явная инициализация, которая зависит от выбранного синтаксиса инициализации и будет рассмотрена в соответствующих разделах. Отметим, что инициализация по умолчанию возможна не для всех типов, тип должен иметь доступный конструктор по умолчанию, пользовательский или сгенерированный компилятором, в противном случае компилятор выдаст ошибку. Компилятор генерирует конструктор по умолчанию, когда нет пользовательских конструкторов, либо когда конструктор по умолчанию объявлен со спецификатором =default.


В отличие от простых и динамических массивов, для элементов стандартных контейнеров гарантируется инициализация по умолчанию в случае, когда не заданы значения для элементов, например:


std::vector<int> x(10); // x — это вектор размера 10,
                        // каждый элемент которого равен 0

Естественно, что в этом случае тип, для которого конкретизируется контейнер, должен иметь доступный конструктор по умолчанию.



6.1.2. Использование символа =


Таким способом традиционно инициализируются переменные встроенного типа, указатели, ссылки, перечисления. Вот примеры:


int x = 42;
int *px = &x;
int &rx = x;
int (*pf)(int) = nullptr;
auto y = 125;
bool flag = true;

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


std::string s = "meow";

Обратим внимание, что в данном случае символ = не является оператором присваивания.


Большинство примеров, приведенных выше, используют этот синтаксис.



6.1.3. Агрегатная инициализация


Для простых C-структур и массивов предназначена агрегатная инициализация (aggregate initialization), унаследованная из C. В этом случае используются символ = и фигурные скобки (скобки могут быть вложенными). Вот примеры:


struct X
{
    double D;
    int S[2];
};

X x = { 3.14, { 10, 20 } };

При агрегатной инициализации массива можно не указывать его размер.


int a[] = { 1, 2, 3, 4 }; // массив размера 4 с элементами типа int

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


Если элементов списка меньше, чем членов структуры или элементов массива, то для остальных членов или элементов гарантируется инициализация по умолчанию.


int a[4] = { 1 }; // 1, 0, 0, 0

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


В C++20 для агрегатных типов появилась еще назначенная инициализация (designated initialization):


struct Point
{
    int X;
    int Y;
};

Point pt = { .X = 1, .Y = 2 };

Эта инициализация позаимствована из C. Инициализируемые члены могут быть опущены и не только в конце, для них гарантируется инициализация по умолчанию. В отличие от C, порядок членов изменять нельзя, он должен быть таким же, как и при объявлении.


В C++11 можно опустить символ =.


В C++17 появилось свойство типов, которое позволяет определить, является ли тип агрегатным. Выражение std::is_aggregate<Т>::value имеет значение true, если T агрегатный тип и false в противном случае (заголовочный файл <type_traits>, см. [VJG]).



6.1.4. Использование круглых скобок


Таким способом обычно инициализируются экземпляры класса, у которого есть доступный конструктор с несколькими параметрами. Вот примеры:


std::string s("meow");
std::string w(3, 'W');     // "WWW"
std::string u("12345", 3); // "123"

Так же таким способом могут быть инициализированы переменные встроенного типа, указатели, ссылки, перечисления. Вот примеры:


int x(42);
int *px(&x);
int &rx(x);
int (*pf)(int)(nullptr);
auto y(125);
bool flag(true);

В C++20 появилась возможность использовать круглые скобки и для агрегатных типов, например:


struct Point
{
    int X;
    int Y;
};

Point pt(1, 2);
int a[](1, 2);

Пустые скобки означают инициализацию по умолчанию, но в ряде случаев их нельзя использовать, так как такая инструкция будет интерпретироваться как объявление функции без параметров (см. раздел 6.2.1).



6.1.5. Универсальная инициализация


В C++11 появилась универсальная инициализация (uniform initialization), которая использует фигурные скобки, может применяться практически в любом случае, то есть способна заменить (за некоторыми исключениями) все предыдущие варианты инициализации. Вот примеры:


int x{ 42 };
int *px{ &x };
int &rx{ x };
int (*pf)(int){ nullptr };
bool flag{ true };
int a[]{ 1, 2, 3, 4 };
std::string s{ "meow" };
std::string u{ "12345", 3 }; // "123"

Скобки могут быть пустыми, что означает инициализацию по умолчанию. В случае массива по умолчанию будет инициализирован каждый элемент.


int x{};          // 0
int *px{};        // nullptr
int (*pf)(int){}; // nullptr
bool flag{};      // false
int a[4]{};       // 0 для всех элементов массива
std::string s{};  // пустая строка

Во многих случаях фигурные скобки можно сочетать с символом =.


int a[] = { 1, 2, 3, 4 };
std::string u = { "12345", 3 };

Но эта форма становится недопустимой, если инициализируется экземпляр класса и соответствующий конструктор объявлен как explicit.


Как мы писали выше, универсальная инициализация может заменить другие варианты инициализации, но все-таки есть исключения. Вот традиционный пример:


std::vector<int> x(10, 3), y{ 10, 3 };

Для инициализации x используются круглые скобки, в результате x — это вектор размера 10 (первый аргумент), каждый элемент которого равен 3 (второй аргумент). Для инициализации y используется универсальная инициализация, в результате y — это вектор размера 2 (размер списка) с элементами 10 и 3 (элементы списка). Такая ситуация может возникнуть, когда класс (обычно это контейнер) имеет конструктор, принимающий специальный тип std::initializer_list<>, который появился вместе с универсальной инициализацией.


Также есть специальные правила при использовании auto (эти правила были уточнены в C++17). В этом случае появляется разница в использовании фигурных скобок без знака = и с ним. Вот примеры:


auto x{ 125 }; // x имеет тип int и значение 125
auto y = { 125 }; // y имеет тип std::initializer_list<int>
                // и содержит один элемент со значением 125
auto x2{ 125, 78 }; // ошибка
auto y2 = { 125, 78 }; // y2 имеет тип std::initializer_list<int>
               // и содержит два элемента со значениями 125 и 78
auto y3 = { 125, 78.0 }; // ошибка, элементы списка имеют разный тип

Подробнее про универсальную инициализацию (включая некоторые «подводные камни») можно почитать у Скотта Мейерса [Meyers].



6.2. Контекст объявления переменной и инициализация


В этом разделе уточняются правила инициализации в зависимости от контекста объявления переменной.



6.2.1. Глобальные и локальные переменные


Рассмотрим переменные, которые объявлены глобально, в области видимости пространства имен или локально. Такие переменные, если они не имеют спецификатора extern, инициализируются непосредственно в инструкции объявления. В этом случае можно использовать любой синтаксис инициализации (все примеры раздела 6.1), за одним исключением — для инициализации по умолчанию нельзя использовать пустые круглые скобки, так как в этом контексте такое объявление будет интерпретироваться как объявление функции без параметров. Вот пример:


std::string s(); // s — это функция без параметров,
                 // возвращающая std::string

В этом случае для инициализации по умолчанию можно использовать пустые фигурные скобки, вариант с auto и для нетривиальных типов объявление без инициализации, например:


std::string s1{};        // пустая строка
auto s2 = std::string(); // пустая строка
std::string s3;          // пустая строка

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



6.2.2. Динамические переменные


Динамические переменные создаются с помощью оператора new, который принимает тип переменной и возвращает указатель на эту переменную (см. раздел 4.3). После типа может находиться инициализирующее выражение, которое состоит из списка значений в круглых или фигурных скобках без символа =. Эти значения являются либо начальными значениями для переменных встроенного типа, указателей, перечислений, либо аргументами конструктора, либо элементами списка агрегатной инициализации для агрегатных типов. (Круглые скобки для агрегатной инициализации, а также назначенную инициализацию можно использовать только в C++20.) Пустые скобки означают инициализацию по умолчанию. Вот примеры:


int *d1 = new int(5);
int **d2 = new int*{ nullptr };
std::string *d3 = new std::string(3, 'W');

Можно создавать динамические массивы, в этом случае оператора new возвращает указатель на первый элемент. Вот пример:


int *d4 = new int[4]{ 1, 2, 3, 4 };

Возможность инициализировать динамические массивы появилась в C++11. Для инициализации можно использовать список значений в фигурных скобках (а в C++20 и в круглых) без символа =. Если элементов списка меньше, чем размер массива, то для остальных элементов выполняется инициализация по умолчанию, соответственно, пустые фигурные скобки обеспечивают инициализацию по умолчанию для всех элементов массива. Если используется инициализация с непустым списком, то размер массива указывать не обязательно.


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



6.2.3. Члены класса


Немного сложнее правила инициализации членов класса. Инициализация членов класса — это единственный случай, когда объявление и инициализация могут находиться в разных инструкциях.


Нестатические члены класса могут быть инициализированы двумя способами: либо в списке инициализации конструктора, либо при объявлении. При инициализации членов в списке инициализации конструктора используются круглые скобки (а в C++11 и фигурные) без символа =. Вот пример:


class Point
{
    int m_X;
    int m_Y;
public:
    Point(int x, int y) : m_X(x), m_Y(y){}
// ...
};

Этот вариант является безальтернативным, если для инициализации нужно использовать параметры конструктора. Пустые скобки означают инициализацию по умолчанию.


В этом варианте есть один «подводный камень», дело в том, что инициализация членов происходит в порядке объявления членов, а не в порядке расположения в списке. Это может привести к ошибке, если элементы списка расположены не в порядке объявления и ссылаются друг на друга.


В C++11 появилась возможность инициализировать нестатические члены при объявлении.


class X
{
    int m_Length{ 0 };
// ...
};

В этом случае можно использовать синтаксис инициализации с фигурными скобками и с символом =, а вот круглые скобки использовать нельзя.


Если для нестатического члена не используется какой-либо вариант явной инициализации, то для нетривиальных типов будет применен конструктор по умолчанию. Для тривиальных типов без инициализации при объявлении надо учитывать контекст объявления и инициализации экземпляра класса. Если экземпляр класса инициализирован с помощью конструктора по умолчанию, генерируемым компилятором, то и член также будет инициализирован по умолчанию. Если экземпляр класса не инициализирован или при инициализации применен пользовательский конструктор, в котором нет явной инициализации члена, то этот член окажется неинициализированным и будет содержать либо нулевые биты, либо случайный набор битов, это зависит от контекста объявления экземпляра класса.


Теперь рассмотрим статические члены. Статические константные члены целочисленного типа можно инициализировать при объявлении. В C++11 статические члены, объявленные как constexpr, должны быть инициализированы при объявлении, они могут иметь любой тип, допускающий создание constexpr экземпляров. В C++17 статические члены со спецификатором inline можно инициализировать при объявлении вне зависимости от типа.


class X
{
    static const int Coeff = 125;
    static constexpr double Radius = 13.5;
    static inline double Factor{ 0.04 };
    static std::string M;
// ...
};

В остальных случаях надо использовать инициализацию при определении члена.


std::string X::M = "meow";

При инициализации при объявлении можно использовать синтаксис инициализации с фигурными скобками и с символом =, а вот круглые скобки использовать нельзя. При инициализации при определении можно использовать любой синтаксис инициализации.


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



6.3. Примеры


В заключении приведем варианты инициализации для некоторых переменных, объявленных глобально или локально.


Вот примеры для переменной типа int, инициализируемой значением 5.


int x1 = 5;
int x2(5);
int x3{ 5 };
int x4 = { 5 };
auto x5 = 5;
auto x6(5);
auto x7{ 5 };
auto x8 = int(5);
auto x9 = int{ 5 };

Теперь рассмотрим инициализацию экземпляров класса, имеющего конструктор с параметром.


class Int
{
    int m_Value;
public:
    Int(int x) : m_Value(x) {}
// ...
};

Int i1 = 5;
Int i2(5);
Int i3{ 5 };
Int i4 = { 5 };
auto i5 = Int(5);
auto i6 = Int{ 5 };

Если конструктор класса Int объявить как explicit, то объявления i1 и i4 окажутся ошибочными.



7. Список статей серии «C++, копаем вглубь»


  1. Перегрузка в C++. Часть I. Перегрузка функций и шаблонов.
  2. Перегрузка в C++. Часть II. Перегрузка операторов.
  3. Перегрузка в C++. Часть III. Перегрузка операторов new/delete.
  4. Массивы в C++.
  5. Ссылки и ссылочные типы в C++.


8. Итоги


  1. Не объявляйте переменные разных типов в одной инструкции.
  2. Не объявляйте несколько функций в одной инструкции.
  3. Используйте псевдонимы для упрощения объявлений.
  4. Инициализируйте объявленные переменные.


Список литературы


[VJG]
Вандевурд, Дэвид, Джосаттис, Николаи М., Грегор, Дуглас. Шаблоны C++. Справочник разработчика, 2-е изд.: Пер. с англ. — СПб.: ООО «Диалектика», 2020.


[Meyers]
Мейерс, Скотт. Эффективный и современный C++: 42 рекомендаций по использованию C++11 и C ++14.: Пер. с англ. — М.: ООО «И.Д. Вильямс», 2016.

Теги:
Хабы:
Всего голосов 27: ↑26 и ↓1+31
Комментарии48

Публикации

Истории

Работа

QT разработчик
3 вакансии
Программист C++
103 вакансии

Ближайшие события