Список инициализации в конструкторе класса в с

Обновлено: 28.04.2024

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

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

При объявлении экземпляра класса компилятор выбирает, какой конструктор будет вызываться на основе правил разрешения перегрузки:

  • Конструкторы могут быть объявлены как inline , , explicitfriend или constexpr .
  • Конструктор может инициализировать объект, объявленный как const , volatile или const volatile . Объект становится const после завершения конструктора.
  • Чтобы определить конструктор в файле реализации, присвойте ему полное имя, как и любая другая функция-член: Box::Box() .

Списки инициализаторов элементов

При необходимости конструктор может иметь список инициализаторов элементов, который инициализирует члены класса перед запуском тела конструктора. (Список инициализаторов элементов не совпадает со списком инициализаторов типа std::initializer_list .)

Предпочитать инициализаторы элементов перечисляют значения вместо назначения значений в тексте конструктора. Список инициализаторов элементов напрямую инициализирует элементы. В следующем примере показан список инициализаторов элементов, состоящий из всех identifier(argument) выражений после двоеточия:

Идентификатор должен ссылаться на член класса; он инициализирован со значением аргумента. Аргумент может быть одним из параметров конструктора, вызова функции или . std::initializer_list

const члены и члены ссылочного типа должны быть инициализированы в списке инициализаторов элементов.

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

Конструкторы по умолчанию

Конструкторы по умолчанию обычно не имеют параметров, но они могут иметь параметры со значениями по умолчанию.

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

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

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

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

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

Это утверждение является примером проблемы "Большинство vexing Parse". Можно интерпретировать myclass md(); как объявление функции или как вызов конструктора по умолчанию. Поскольку средства синтаксического анализа C++ предпочитают объявления по сравнению с другими вещами, выражение рассматривается как объявление функции. Дополнительные сведения см. в разделе "Большинство синтаксического анализа".

Если объявлены какие-либо конструкторы, отличные от по умолчанию, компилятор не предоставляет конструктор по умолчанию:

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

Однако для инициализации массива объектов Box можно использовать набор списков инициализаторов:

Дополнительные сведения см. в разделе "Инициализаторы".

Конструкторы копии

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

Конструктор копирования может иметь одну из следующих сигнатур:

При определении конструктора копирования необходимо также определить оператор присваивания копирования (=). Дополнительные сведения см. в разделе "Назначение " и " Копирование конструкторов" и операторов присваивания копирования.

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

При попытке копирования объекта возникает ошибка C2280: попытка ссылаться на удаленную функцию.

Конструкторы перемещения

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

Компилятор выбирает конструктор перемещения, когда объект инициализируется другим объектом того же типа, если другой объект будет уничтожен и больше не нуждается в его ресурсах. В следующем примере показано одно дело, когда конструктор перемещения выбирается с помощью разрешения перегрузки. В конструкторе, который вызывает get_Box() , возвращаемое значение является xvalue (значение eXpiring). Поэтому он не назначается какой-либо переменной и поэтому выходит за пределы области действия. Чтобы обеспечить мотивацию для этого примера, давайте предоставим Box большой вектор строк, представляющих его содержимое. Вместо копирования вектора и его строк конструктор перемещения "крадет" его из значения "box", чтобы вектор теперь принадлежит новому объекту. Вызов std::move необходим, так как оба vector класса string реализуют собственные конструкторы перемещения.

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

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

Дополнительные сведения о написании конструктора нетривиального перемещения см. в разделе "Конструкторы перемещения" и "Операторы присваивания перемещения" (C++).

Явно заданные по умолчанию и удаленные конструкторы

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

Конструкторы constexpr

Конструктор может быть объявлен как constexpr , если

  • он либо объявлен как стандартный, либо удовлетворяет всем условиям для функций constexpr в целом;
  • класс не имеет виртуальных базовых классов;
  • каждый из параметров является литеральным типом;
  • тело не является блоком try-block функции;
  • инициализированы все нестатические члены данных и подобъекты базового класса;
  • Значение , если класс является (a) объединением, имеющим члены варианта, или (б) имеет анонимные объединения, инициализируется только один из членов профсоюза;
  • каждый нестатический член данных типа класса, а все подобъекты базового класса имеют конструктор constexpr.

Конструкторы списков инициализаторов

Затем создайте объекты Box следующим образом:

Явные конструкторы

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

Можно инициализировать Box следующим образом:

Или передать целое значение функции, принимающей объект Box:

В некоторых случаях подобные преобразования могут быть полезны, однако чаще всего они могут привести к незаметным, но серьезным ошибкам в вашем коде. Как правило, необходимо использовать ключевое explicit слово в конструкторе (и определяемых пользователем операторах), чтобы предотвратить такое неявное преобразование типов:

Когда конструктор является явным, эта строка вызывает ошибку компилятора: ShippingOrder so(42, 10.8); . Дополнительные сведения см. в разделе о преобразованиях определяемых пользователем типов.

Порядок строительства

Конструктор выполняет свою работу в следующем порядке.

Вызывает конструкторы базовых классов и членов в порядке объявления.

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

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

Выполняет весь код в теле функции.

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

Выходные данные будут выглядеть следующим образом.

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

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

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

Отменяется код в теле функции конструктора.

Объекты базовых классов и объекты-члены удаляются в порядке, обратном объявлению.

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

Производные конструкторы и расширенная инициализация агрегатов

Если конструктор базового класса не является открытым, но доступен для производного класса, нельзя использовать пустые фигурные скобки для инициализации объекта производного типа в /std:c++17 режиме, а затем в Visual Studio 2017 и более поздних версий.

В следующем примере показана соответствующая реакция на событие в C++14:

В C++17 Derived теперь считается агрегатным типом. Это означает, что инициализация Base через закрытый конструктор по умолчанию происходит непосредственно как часть расширенного правила агрегатной инициализации. Ранее частный Base конструктор был вызван через Derived конструктор, и он был успешно выполнен из-за friend объявления.

В следующем примере показано поведение C++17 в Visual Studio 2017 и более поздних версий в /std:c++17 режиме:

Конструкторы для классов с множественным наследованием

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

Должны выводиться следующие выходные данные:

Делегирующие конструкторы

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

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

Наследование конструкторов (C++11)

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

Visual Studio 2017 и более поздних версий: оператор using в /std:c++17 режиме и более поздних версиях преобразует все конструкторы из базового класса, за исключением тех, которые имеют идентичную сигнатуру конструкторам в производном классе. Как правило, рекомендуется использовать наследуемые конструкторы, когда производный класс не объявляет новые члены данных или конструкторы.

Шаблон класса может наследовать все конструкторы от аргумента типа, если этот тип определяет базовый класс:

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

Конструкторы и составные классы

Классы, содержащие члены типа класса, называются составными классами. При создании члена типа класса составного класса конструктор вызывается перед собственным конструктором класса. Если у содержащегося класса нет конструктора по умолчанию, необходимо использовать список инициализации в конструкторе составного класса. В предыдущем примере StorageBox при присвоении типу переменной-члена m_label нового класса Label необходимо вызвать конструктор базового класса и инициализировать переменную m_label в конструкторе StorageBox :

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

Этот код печатает:

Это также работает и для динамически размещаемых массивов:

В предыдущем уроке мы представили концепцию контейнерных классов и показали пример класса IntArray , который содержит массив чисел int :

Этот код не компилируется, потому что у класса IntArray нет конструктора, который знает, что делать со списком инициализаторов. В результате нам остается инициализировать элементы массива по отдельности:

Это не так уж и хорошо.

Инициализация класса с помощью std::initializer_list

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

std::initializer_list находится в заголовке .

Есть несколько вещей, которые нужно знать о std::initializer_list . Подобно std::array или std::vector , если вы сразу не инициализируете std::initializer_list , то с помощью угловых скобок вы должны сказать объекту std::initializer_list , какой тип данных содержит список. Следовательно, вы почти никогда не увидите простой std::initializer_list . Вместо этого вы увидите что-то вроде std::initializer_list или std::initializer_list .

Во-вторых, std::initializer_list имеет (не совсем правильную) функцию size() , которая возвращает количество элементов в списке. Она полезна, когда нам нужно знать длину переданного списка.

Давайте посмотрим, как обновить наш класс IntArray с помощью конструктора, который принимает std::initializer_list .

Этот код дает ожидаемый результат:

Всё работает! Теперь давайте рассмотрим этот код более подробно.

Вот наш конструктор IntArray , который принимает std::initializer_list .

Строка 2: Как отмечалось выше, мы должны использовать угловые скобки, чтобы обозначить, какой тип элементов мы ожидаем внутри списка. В этом случае, поскольку это IntArray , мы ожидаем, что список будет заполнен значениями int . Обратите внимание, что мы не передаем список по константной ссылке. Как и std::string_view , std::initializer_list очень легкий, и его копии обычно дешевле, чем косвенное обращение.

Строка 4: Мы делегируем выделение памяти для IntArray другому конструктору через делегирующий конструктор (для уменьшения избыточного кода). Этот другой конструктор должен знать длину массива, поэтому мы передаем ему значение list.size() , которое содержит количество элементов в списке. Обратите внимание, что list.size() возвращает size_t (без знака), поэтому здесь нам нужно привести это значение к signed int . Мы используем прямую инициализацию, а не инициализацию с фигурными скобками, потому что для инициализации с фигурными скобками предпочтительны конструкторы со списком инициализаторов. Хотя конструктор будет разрешен правильно, для инициализации классов с конструкторами со списком инициализаторов безопаснее использовать прямую инициализацию, если мы не хотим использовать конструктор со списком инициализаторов.

Тело конструктора зарезервировано для копирования элементов из списка в наш класс IntArray . По какой-то необъяснимой причине std::initializer_list не предоставляет доступ к элементам списка через индексирование ( operator[] ). Об этом упущении много раз сообщали комитету по стандартам.

Однако есть простые способы обойти отсутствие индексов. Самый простой способ – использовать здесь цикл for -each. Цикл for -each проходит по всем элементам списка инициализаторов, и мы можем вручную скопировать эти элементы в наш внутренний массив.

Одно предостережение: списки инициализаторов всегда будут отдавать предпочтение соответствующему конструктору с initializer_list по сравнению с другими потенциально подходящими конструкторами. Таким образом, это определение переменной:

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

Присваивание объектам класса с использованием std::initializer_list

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

Обратите внимание, что если вы реализуете конструктор, который принимает std::initializer_list , вы должны убедиться, что выполняете хотя бы одно из следующих действий:

  1. предоставить перегруженный оператор присваивания со списком инициализаторов;
  2. предоставить правильный оператор присваивания для глубокого копирования.

Почему? Рассмотрим приведенный выше класс (который не имеет перегруженного присваивания со списком инициализаторов или копирующего присваивания) вместе со следующей инструкцией:

Во-первых, компилятор заметит, что функции присваивания, принимающей std::initializer_list , не существует. Затем он будет искать другие функции присваивания, которые он сможет использовать, и обнаружит неявно предоставленный оператор копирующего присваивания. Однако эту функцию можно использовать только в том случае, если она может преобразовать список инициализаторов в IntArray . Поскольку является списком std::initializer_list , компилятор будет использовать конструктор со списком инициализаторов для преобразования списка инициализаторов во временный массив IntArray . Затем он вызовет неявный оператор присваивания, который выполнит поверхностное копирование временного массива IntArray в наш объект массива.

На этом этапе и m_data временного IntArray , и array->m_data указывают на один и тот же адрес (из-за поверхностного копирования). Вы уже можете догадаться, к чему это идет.

В конце инструкции присваивания временный массив IntArray уничтожается. Это вызывает деструктор, который удаляет m_data временного массива IntArray . Это оставляет нашу переменную массива с висячим указателем m_data . Когда вы попытаетесь использовать array->m_data для чего-либо (в том числе, когда массив выходит за пределы области видимости, а деструктор переходит к удалению m_data ), вы получите неопределенное поведение (и, возможно, сбой).

Правило

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

Резюме

Реализация конструктора, который принимает параметр std::initializer_list , позволяет нам использовать инициализацию списком с нашими пользовательскими классами. Мы также можем использовать std::initializer_list для реализации других функций, которые должны использовать список инициализаторов, например, оператора присваивания.

Небольшой тест

Вопрос 1

Используя приведенный выше класс IntArray , реализуйте перегруженный оператор присваивания, который принимает список инициализаторов.

В предыдущем уроке для простоты мы инициализировали члены данных нашего класса в конструкторе с помощью оператора присваивания. Например:

Когда выполняется конструктор класса, создаются m_value1 , m_value2 и m_value3 . Затем запускается тело конструктора, в котором переменным-членам данных присваиваются значения. Это похоже на выполнение следующего кода в не объектно-ориентированном C++:

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

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

Это создает код, подобный следующему:

Присваивание значений константным или ссылочным переменным-членам в теле конструктора в некоторых случаях невозможно.

Списки инициализаторов членов

Чтобы решить эту проблему, C++ предоставляет метод инициализации переменных-членов класса (вместо присваивания им значений после их создания) через список инициализаторов членов (часто называемый «списком инициализации членов»). Не путайте их с похоже называющимся списком инициализаторов, который мы можем использовать для присваивания значений массивам.

В уроке «1.4 – Присваивание и инициализация переменных» вы узнали, что переменные можно инициализировать тремя способами: через копирующую, прямую и унифицированную инициализацию.

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

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

Теперь давайте напишем тот же код, используя список инициализации:

Эта программа печатает:

Список инициализаторов членов класса вставляется после параметров конструктора. Он начинается с двоеточия ( : ), а затем перечисляет через запятые все иницализируемые переменные вместе со значениями этих переменных.

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

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

Эта программа печатает:

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

Вот пример класса с константной переменной-членом:

Это работает, потому что нам разрешено инициализировать константные переменные (но не присваивать им значения!).

Правило

Для инициализации переменных-членов вашего класса вместо присваивания используйте списки инициализаторов членов.

Инициализация элементов массива списками инициализаторов членов

Рассмотрим класс с членом-массивом:

До C++11 член-массив с помощью списка инициализации членов класса можно было только обнулить:

Однако, начиная с C++11, вы можете полностью инициализировать член-массив, используя унифицированную инициализацию:

Инициализация переменных-членов, которые являются классами

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

Эта программа печатает:

Когда создается переменная b , конструктор B(int) вызывается со значением 5. Перед выполнением тела конструктора инициализируется m_a , вызывая конструктор A(int) со значением 4. Это печатает " A 4 ". Затем управление возвращается конструктору B , и тело конструктора B выполняется с выводом " B 5 ".

Форматирование списков инициализаторов

C++ дает вам большую гибкость в том, как форматировать списки инициализаторов, и вам решать, как вы хотите действовать. Но вот несколько рекомендаций:

Если список инициализаторов умещается в той же строке, что и имя функции, то можно разместить всё в одной строке:

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

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

Порядок в списке инициализаторов

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

Для достижения наилучших результатов следует соблюдать следующие рекомендации:

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

Резюме

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

Небольшой тест

Вопрос 1

Если вам нужно напоминание о том, как использовать целые числа фиксированной ширины, просмотрите урок «4.6 – Целочисленные типы фиксированной ширины и size_t».

Подсказка: если ваша функция print() работает некорректно, убедитесь, что вы приводите uint_fast8_t к типу int .

В последних двух уроках мы изучили основы наследования в C++ и порядок инициализации производных классов. В этом уроке мы более подробно рассмотрим роль конструкторов в инициализации производных классов. Для этого мы продолжим использовать простые классы Base и Derived , которые мы разработали в предыдущем уроке:

В случае классов, не являющихся производными, конструкторам нужно беспокоиться только о своих членах. Например, рассмотрим Base . Мы можем создать объект Base следующим образом:

Вот что на самом деле происходит при создании экземпляра Base :

  1. выделяется память для Base ;
  2. вызывается соответствующий конструктор Base ;
  3. список инициализации инициализирует переменные;
  4. выполняется тело конструктора;
  5. управление возвращается вызывающей функции.

Всё довольно просто. С производными классами всё немного сложнее:

Вот что на самом деле происходит при создании экземпляра Derived :

  1. выделяется память для Derived (достаточная и для части Base , и для части Derived );
  2. вызывается соответствующий конструктор Derived ;
  3. сначала создается объект Base с использованием соответствующего конструктора Base . Если конструктор Base не указан, будет использоваться конструктор по умолчанию;
  4. список инициализации инициализирует переменные;
  5. выполняется тело конструктора;
  6. управление возвращается вызывающей функции.

Единственное реальное различие между этим случаем и случаем без наследования состоит в том, что прежде, чем конструктор Derived сможет сделать что-либо существенное, сначала вызывается конструктор Base . Конструктор Base создает часть Base объекта, управление возвращается конструктору Derived , и конструктору Derived разрешается завершить свою работу.

Инициализация членов базового класса

Один из текущих недостатков нашего класса Derived в том виде, в котором он написан, заключается в том, что при создании объекта Derived нет возможности инициализировать m_id . Что, если при создании объекта Derived мы хотим установить и m_cost (из части Derived объекта), и m_id (из части Base объекта)?

Начинающие программисты часто пытаются решить эту проблему следующим образом:

Это хорошая попытка и почти правильная идея. Нам обязательно нужно добавить в наш конструктор еще один параметр, иначе C++ не сможет узнать, каким значением мы хотим инициализировать m_id .

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

Почему C++ так делает? Ответ связан с константными и ссылочными переменными. Подумайте, что бы произошло, если бы m_id был const . Поскольку константные переменные должны быть инициализированы значением во время создания, конструктор базового класса при создании переменной должен установить ее значение. Однако списки инициализации конструкторов производного класса выполняются после завершения работы конструктора базового класса. А если у каждого производного класса будет возможность инициализировать эту переменную, он потенциально сможет изменить ее значение! Ограничивая инициализацию переменных конструктором класса, к которому эти переменные принадлежат, C++ гарантирует, что все переменные инициализируются только один раз.

Конечным результатом является то, что приведенный выше пример не работает, потому что m_id был унаследован от Base , и только ненаследуемые переменные могут быть инициализированы в списке инициализации.

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

Хотя в данном случае это действительно работает, это не сработало бы, если бы m_id был константой или ссылкой (потому что константные значения и ссылки должны быть инициализированы в списке инициализации конструктора). Это также неэффективно, потому что переменной m_id значение присваивается дважды: один раз в списке инициализации конструктора класса Base , а затем снова в теле конструктора класса Derived . И, наконец, что, если классу Base потребовался бы доступ к этому значению во время создания? У него нет возможности получить доступ к этому значению, поскольку оно не устанавливается до тех пор, пока не будет выполнен конструктор Derived (что происходит в последнюю очередь).

Итак, как правильно инициализировать m_id при создании объекта класса Derived ?

До сих пор во всех примерах, когда мы создавали экземпляр объекта класса Derived , часть Base класса создавалась с использованием конструктора Base по умолчанию. Почему он всегда использовал конструктор Base по умолчанию? Потому что мы никогда не указывали иное!

К счастью, C++ дает нам возможность явно выбирать, какой конструктор класса Base будет вызываться! Для этого просто добавьте вызов конструктора класса Base в список инициализации класса Derived :

Теперь, когда мы выполняем этот код:

Конструктор базового класса Base(int) будет использоваться для инициализации m_id значением 5, а конструктор производного класса будет использоваться для инициализации m_cost значением 1.3!

Таким образом, программа напечатает:

Вот что происходит более подробно:

  1. выделяется память для Derived ;
  2. вызывается конструктор Derived(double, int) , где cost = 1.3, а id = 5;
  3. компилятор проверяет, запрашивали ли мы конкретный конструктор для класса Base . Так и есть! Поэтому он вызывает Base(int) с id = 5;
  4. список инициализации конструктора класса Base устанавливает m_id равным 5;
  5. выполняется тело конструктора класса Base , которое ничего не делает;
  6. конструктор класса Base возвращает выполнение;
  7. список инициализации конструктора класса Derived устанавливает m_cost равным 1,3;
  8. выполняется тело конструктора класса Derived , которое ничего не делает;
  9. конструктор класса Derived возвращает выполнение.

Это может показаться несколько сложным, но на самом деле всё очень просто. Всё, что происходит, – это то, что конструктор Derived вызывает конкретный конструктор Base для инициализации части Base объекта. Поскольку m_id находится в части Base объекта, конструктор Base является единственным конструктором, который может инициализировать это значение.

Обратите внимание, что не имеет значения, где в списке инициализации конструктора Derived вызывается конструктор Base – он всегда будет выполняться первым.

Теперь мы можем сделать наши члены закрытыми

Теперь, когда вы знаете, как инициализировать члены базового класса, нет необходимости держать наши переменные-члены открытыми. Мы снова делаем наши переменные-члены закрытыми, как и должно быть.

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

Рассмотрим следующий код:

В приведенном выше коде мы сделали m_id и m_cost закрытыми. Это нормально, поскольку мы используем соответствующие конструкторы для их инициализации и открытые методы доступа для получения значений.

Этот код печатает следующее, как и ожидалось:

Подробнее о спецификаторах доступа мы поговорим в следующем уроке.

Еще один пример

Давайте посмотрим на еще одну пару классов, с которыми мы ранее работали:

Как мы уже писали ранее, BaseballPlayer инициализирует только свои собственные члены и не указывает, какой конструктор Person использовать. Это означает, что каждый созданный нами BaseballPlayer будет использовать конструктор Person по умолчанию, который инициализирует имя пустой строкой и возраст значением 0. Поскольку имеет смысл дать нашему BaseballPlayer имя и возраст при его создании, мы должны изменить его конструктор, чтобы добавить эти параметры.

Вот наши обновленные классы, которые используют закрытые члены, причем класс BaseballPlayer вызывает соответствующий конструктор Person для инициализации унаследованных переменных-членов Person :

Теперь мы можем создавать бейсболистов так:

Этот код выводит:

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

Цепочки наследования

Классы в цепочке наследования работают точно так же.

В этом примере класс C является производным от класса B , который является производным от класса A . Итак, что происходит, когда мы создаем экземпляр объекта класса C ?

Сначала main() вызывает C(int, double, char) . Конструктор C вызывает B(int, double) . Конструктор B вызывает A(int) . Поскольку A ни от кого не наследуется, это первый класс, который мы создадим. A создается, печатает значение 5 и возвращает управление B . B создается, печатает значение 4.3 и возвращает управление C . C создается, печатает значение ' R ' и возвращает управление main() . Готово!

Таким образом, эта программа печатает:

Стоит отметить, что конструкторы могут вызывать конструкторы только их непосредственного родительского/базового класса. Следовательно, конструктор C не может напрямую вызывать или передавать параметры конструктору A . Конструктор C может вызывать только конструктор B (который отвечает за вызов конструктора A ).

Деструкторы

Когда производный класс уничтожается, каждый деструктор вызывается в порядке, обратном созданию. В приведенном выше примере, когда c уничтожается, сначала вызывается деструктор C , затем деструктор B , а затем деструктор A .

Резюме

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

На этом этапе вы достаточно понимаете наследование в C++, чтобы создавать свои собственные наследованные классы!

Небольшой тест

Вопрос 1

Давайте реализуем наш пример с фруктами, о котором мы говорили во введении в наследование. Создайте базовый класс Fruit , содержащий два закрытых члена: имя, name , ( std::string ) и цвет, color , ( std::string ). Создайте класс для яблока, Apple , наследованный от Fruit . У Apple должен быть дополнительный закрытый член: клетчатка, fiber , ( double ). Создайте класс для банана, Banana , который также наследуется от Fruit . У Banana нет дополнительных членов.

Должна запуститься следующая программа:

Она должна напечатать следующее:

Подсказка: поскольку a и b являются константами, вам нужно помнить о константности. Убедитесь, что ваши параметры и функции имеют значение const .

Инициализатор определяет начальное значение переменной. Можно инициализировать переменные в этих контекстах:

В определении переменной:

В качестве одного из параметров функции:

В качестве возвращаемого типа функции:

Инициализаторы могут принимать эти формы:

Выражение (или разделенный запятыми список выражений) в скобках:

Знак равенства с последующим выражением:

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

Типы инициализации

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

Нулевая инициализация

Нулевая инициализация — задание для переменной нулевого значения, неявно преобразованного в тип:

Числовые переменные инициализируются значением 0 (или 0,0; 0,0000000000 и т.п.).

Переменные char инициализируются в '\0' .

Указатели инициализируются в nullptr .

Массивы, классы POD , структуры и объединения инициализируют свои члены равным нулю.

Нулевая инициализация выполняется в разное время:

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

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

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

Ниже приведены некоторые примеры нулевой инициализации:

Инициализация по умолчанию

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

Если класс, структура или объединение не имеет конструктор по умолчанию, компилятор выдает ошибку.

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

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

Если члены массива не имеют конструктор по умолчанию, компилятор выдает ошибку.

Инициализация по умолчанию константных переменных

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

Инициализация по умолчанию статических переменных

Статические переменные, объявленные без инициализатора, инициализируются значением 0 (с неявным преобразованием к соответствующему типу).

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

Инициализация значения

Инициализация значения происходит в следующих случаях:

Именованное значение инициализируется с использованием пустых фигурных скобок.

Анонимный временный объект инициализируется с помощью пустых круглых или фигурных скобок.

Объект инициализируется ключевым словом new и пустыми скобками или фигурными скобками

При инициализации значения выполняются следующие действия:

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

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

В случае массивов каждый элемент инициализируется значением.

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

Инициализация копированием

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

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

Аргумент передается в функцию.

Объект возвращается функцией.

Возникает или перехватывается исключение.

Нестатический элемент данных инициализируется с помощью знака равенства.

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

Следующий код демонстрирует несколько примеров инициализации копированием.

Инициализация копированием не может вызывать явные конструкторы.

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

Прямая инициализация

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

Переменная инициализируется с помощью непустых круглых или фигурных скобок.

Переменная инициализируется ключевым словом new плюс непустые скобки или скобки

Переменная инициализируется с помощью . static_cast

В конструкторе базовые классы и нестатические члены инициализируются с помощью списка инициализации.

В копии захваченной переменной в лямбда-выражении.

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

Инициализация списка

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

Класс инициализируется с помощью ключевого new слова

Объект возвращается функцией.

Аргумент передается функции.

Один из аргументов при прямой инициализации.

В инициализаторе нестатических элементов данных.

В списке инициализации конструктора.

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

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

Агрегатная инициализация — форма инициализации списка для массивов и типов классов (часто структур и объединений), со следующими характеристиками:

Отсутствие закрытых или защищенных членов.

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

Отсутствие базовых классов.

Отсутствие виртуальных функций-членов.

В Visual Studio 2015 и более ранних версиях агрегат не может иметь инициализаторы фигурных скобок или равных значений для нестатических элементов. Это ограничение было удалено в стандарте C++14 и реализовано в Visual Studio 2017 г.

Агрегатные инициализаторы состоят из списка инициализации в фигурных скобках со знаком равенства или без него как в приведенном ниже примере:

Вы должны увидеть следующий результат.

Элементы массива, объявленные, но не инициализированные явно во время статистической инициализации, инициализируются с нуля, как показано myArr3 выше.

Инициализация объединений и структур

Если объединение не имеет конструктора, его можно инициализировать одним значением (или другим экземпляром объединения). Значение используется для инициализации первого нестатического поля. Это отличается от инициализации структур, где первое значение в инициализаторе используется для инициализации первого поля, второе — для инициализации второго поля и т. д. Сравните инициализацию объединений и структур в следующем примере:

Инициализация статистических выражений, содержащих статистические выражения

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

Инициализация ссылок

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

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

Хотя синтаксис может быть одинаковым, инициализация переменных ссылочного типа и присваивание значений переменным ссылочного типа семантически различаются. В предыдущем примере присваивания, которые изменяют значения переменных iVar и lVar , выглядят аналогично инициализации, но имеют другой эффект. Инициализация определяет объект, на который указывает переменная ссылочного типа; при присваивании через ссылку производится присваивание значения объекту, на который указывает ссылка.

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

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

Объявления функций (прототипы). Пример:

Объявления типов значений, возвращаемых функцией. Пример:

Объявления члена класса ссылочного типа. Пример:

Объявление переменной, явно указанной как extern . Пример:

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


Граф принятия решений для инициализации ссылочных типов

Ссылки на volatile типы (объявленные как volatile typenameidentifier &) можно инициализировать с volatile объектами того же типа или с объектами, которые не были объявлены как volatile . Однако они не могут быть инициализированы объектами const этого типа. Аналогичным образом ссылки на const типы (объявленные как const typenameidentifier &) можно инициализировать с const объектами того же типа (или с объектами, которые не были объявлены как const ). Однако они не могут быть инициализированы объектами volatile этого типа.

Ссылки, которые не соответствуют ни ключевому слову const volatile , либо могут быть инициализированы только с объектами, объявленными как ни один из const volatile них.

Инициализация внешних переменных

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

Читайте также: