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

Обновлено: 02.05.2024

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

Когда выполняется конструктор класса, создаются 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 .


В этой статье я бы хотел рассказать о том, как работают списки инициализации (braced initializer lists) в C++, какие проблемы они были призваны решать, какие проблемы, в свою очередь, вызвали и как не попасть в просак.

Первым делом предлагаю почувствовать себя компилятором (или language lawyer-ом) и понять, компилируются ли следующие примеры, почему, и что они делают:

Современный C++ — безопасный язык, я никогда не выстрелю себе в ногу:

Больше скобочек богу скобочек!

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

Almost Always Auto, говорили они. Это повышает читабельность, говорили они:

Привет из древних времен:

Disclaimers

  1. Эта статья ознакомительная, не претендует на полноту и часто будет жертвовать корректностью в угоду понятности. С другой стороны, у читателя предполагается базовое знание C++.
  2. Я пытался придумывать разумные переводы на русский для англоязычных терминов, но с некоторыми я потерпел полное фиаско. Синтаксические конструкции вида я буду называть braced-init-lists, тип из стандартной библиотеки — std::initializer_list , а вид инициализации, когда мы пишем как-то так: int x — это list-init, также известная как uniform initialization syntax, или универсальный синтаксис инициализации.

Attention!

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

Итак, braced-init-lists (штуки с фигурными скобками, , uniform initialization syntax) и std::initializer_list — разные вещи! Они сильно связаны, между ними происходят всякие тонкие взаимодействия, но любое из них вполне может существовать без другого.

Но сначала — немного предыстории.

Unicorn initialization syntax


В C++98 (и его bugfix-update, C++03) существовало достаточно проблем и непоследовательностей, связанных с инициализацией. Вот некоторые из них:

  • Из C пришел синтаксис инициализации переменных (в том числе, массивов и структур) с использованием фигурных скобок, но он не очень хорошо взаимодействовал с возможностями C++ (например, инициализация структур не была доступна для C++-классов)
  • Часто хочется соорудить какой-нибудь контейнер (например, std::vector ) из заранее известных элементов — в языке не было встроенной возможности для этого, а библиотечные решения ( Boost.Assign ) не отличались изящностью синтаксиса, были не бесплатны с точки зрения скорости работы и не слишком хорошо влияли на время компиляции
  • При инициализации примитивных типов легко случайно потерять информацию при сужающем преобразовании (narrowing conversion) — например, случайно присвоить double в int , которым любят пугать начинающих C++-ников.

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

  • Для случаев, где это применимо в C, новый синтаксис будет работать так же, только лучше
  • Сужающие преобразования при этом мы запретим
  • А если мы пытаемся проинициализировать класс с конструкторами, то мы и конструктор сможем вызывать, с переданными параметрами

Pitfalls

Казалось бы, на этом можно и закончить: инициализация контейнеров должна получиться сама собой, ведь в C++11 появились еще и шаблоны с переменным числом параметров, так что если мы напишем variadic-конструктор… на самом деле, нет, так не получится:

  • Такой конструктор должен быть шаблоном, что часто нежелательно
  • Придется инстанцировать конструкторы со всевозможным числом параметров, что приведет к раздуванию кода и замедлению компиляции
  • Эффективность инициализации, например, для std::vector -а будет все равно не идеальная

Для решения этих проблем придумали std::initializer_list — "магический класс", который представляет собой очень легкую обертку для массива элементов известного размера, а так же умеет конструироваться от braced-init-list-а.

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

Зачем же он нужен? Главным образом, чтобы пользовательские классы могли сказать: "я хочу конструироваться от braced-init-list-а элементов такого-то типа", и им не требовался бы для этого шаблонный конструктор.

(Кстати, к этому моменту должно стать понятно, что std::initializer_list и braced-init-list это разные понятия)

Теперь-то все хорошо? Мы просто добавим в наш контейнер конструктор вида vector(std::initializer_list) и все заработает? Почти.

Рассмотрим такую запись:

Что имелось в виду, v(5) или v() ? Другими словами, хотим ли мы сконструировать вектор из 5 элементов, или из одного элемента со значением 5 ?

Для решения этого конфликта разрешение перегрузок (overload resolution, выбор нужной функции по переданным аргументам) в случае list-initialization происходит в два этапа:

  1. Сначала рассматриваются только конструкторы с единственным параметром типа std::initializer_list (это один из главных моментов, когда компилятор таки генерирует std::initializer_list по содержимому фигурных скобочек). Разрешение перегрузок происходит между ними.
  2. Если ни один конструктор не подходит, то дальше все как обычно — разворачиваем braced-init-list в список аргументов и проводим разрешение перегрузок среди всех доступных конструкторов.

Отметим, что конструктор, который проиграл на первом этапе, вполне может подойти на втором. Это объясняет пример с избытком скобочек для инициализации вектора из начала статьи. Для понятности удалим один из вложенных шаблонов, а также заменим std::vector на свой класс:

Под пункт 1 наш конструктор не подходит — >> не похож на std::initializer_list , потому что int нельзя проинициализировать с помощью > . Однако <> — вполне себе zero-initialization, поэтому конструктор принимается на втором шаге.

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

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

Классы-агрегаты

Ну теперь-то все? Не совсем. Старый синтаксис инициализации структур, доставшийся нам от C, никуда не делся, и можно делать так:

Как видим, при иницализации агрегатов (грубо говоря, C-подобных структур, не путать с POD, POD — это про другое) можно и пропускать вложенные скобочки, и выкидывать часть инициализаторов. Все это поведение было аккуратно перенесено в C++.

Казалось бы, какой бред, зачем это в современном языке? Давайте хотя бы предупреждения компилятора будем на это выводить, подумали разработчики GCC и clang, и были бы правы, не будь std::array классом-агрегатом, содержащим внутри себя массив. Таким образом, предупреждение про выкидывание вложенных скобок по понятным причинам срабатывает на вот таком невинном коде:

Проблему эту GCC "решил" выключением соответствующего предупреждения в режиме -Wall , в clang-е же уже три года все по-прежнему.

Кстати, тот факт, что std::array — агрегат, не прихоть безумных авторов стандарта или ленивых разработчиков стандартных библиотек: достичь требуемой семантики этого класса просто невозможно средствами языка, не теряя в эффективности. Еще один привет от C и его странных массивов.

Возможно, большая проблема с классами-агрегатами — это не самое удачное взаимодействие с обобщенными функциями (в том числе) из стандартной библиотеки. На данный момент функции, которые конструируют объект из переданных параметров (например, vector::emplace_back или make_unique ), вызывают обычную инициализацию, не "универсальную". Вызвано это тем, что использование list-initialization не позволяет никаким нормальным способом вызвать "обычный" контруктор вместо принимающего std::initializer_list (примерно та же проблема, что и с инициализацией в не-шаблонном коде, только тут пользователь не может обойти ее вызовом другого конструктора). Работа в этом направлении ведется, но пока мы имеем то, что имеем.

Almost Always Auto

Как же braced-init-list-ы ведут себя в сочетании с выводом типов? Что будет, если я напишу auto x = ; auto y = ; ? Можно придумать несколько разумных стратегий:

  1. Запретить такую инициализацию вообще (в самом деле, что программист хочет этим сказать?)
  2. Вывести тип первой переменной как int , а второй вариант запретить
  3. Сделать так, чтобы и x, и y имели тип std::initializer_lits

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

Промежуточные итоги

Хотя универсальный синтаксис инициализации и std::initializer_list — возможности языка, добавленные из благих и правильных побуждений, мне кажется, что из-за извечной необходимости в обратной совместимости и не всегда дальновидных решениях на ранних этапах вся ситуация вокруг них на данный момент излишне сложная, вымученная и не самая приятная для всех вовлеченных сторон — авторов стандарта, компиляторов, библиотек и прикладных разработчиков. Хотели как лучше, а получилось, как в известном комиксе:


В качестве примера возьмем, например, историю с [over.best.ics]/4.5, который сначала добавили в стандарт, потом, не подумав, удалили, как избыточный, а потом добавили обратно в измененном виде — как описание крайнего случая с пятью (!) условиями.

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

  1. Потратьте некоторое время на то, чтобы ознакомиться с тем, что на самом деле происходит (я рекомендую прочитать параграф стандарта — на удивление понятный и не слишком зависимый от остальных)
  2. Не используйте std::initializer_list , кроме как в параметре конструктора
  3. Да и в параметре конструктора используйте, только если вы понимаете, что происходит (если не уверены — сконструируйтесь лучше от вектора, пары итераторов или range-а)
  4. Не используйте классы-агрегаты без крайней необходимости, напишите лучше конструктор, инициализирующий все поля
  5. Не используйте braced-init-list в сочетании с auto
  6. Прочитайте эту статью про то, что делать с пустыми списками инициализации (у меня руки чешутся ее перевести и запостить, может быть, вскоре займусь)
  7. И, как я уже писал в самом начале, имейте в виду, что braced-init-list и std::initializer_list — это разные концепции, весьма хитро взаимодействующие друг с другом

Давайте помечтаем

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

Мне кажется, что переиспользовать фигурные скобки для создания std::initializer_list во время инициализации — ошибка дизайна языка. Я был бы очень рад, если бы вместо этого мы получили бы более явный и отдельный синтаксис (пусть и более уродливый, например, какие-нибудь странные скобки типа или встроенный интринзик вроде std::of(. ) ). То есть инициализируем вектор как-то так: std::vector> x = std::of(std::of(1, 2), std::of(3, 4));

Что бы это дало? Новый способ инициализации (с защитой от most vexing parse и сужающих преобразований) оказался бы отвязан от std::initializer_list , не потребовалось бы вводить отдельный шаг для разрешения перегрузок, ушла бы проблема с конструктором vector или vector , новый синтаксис инициализации можно было бы использовать в обобщенном коде безо всяких проблем.

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

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

Заключение

В прошлой теме был разработан следующий класс:

И мы можем установить значения для переменных класса Person, можем получить их значения во внешние переменные. Однако если мы попробуем получить значения переменных name и age до их установки, то результаты будут неопределенными:

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

Теперь в классе Person определен конструктор:

По сути конструктор представляет функцию, которая может принимать параметры и которая должна называться по имени класса. В данном случае конструктор принимает два параметра и передает их значения полям name и age.

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

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

После этого вызова у объекта person для поля name будет определено значение "Tom", а для поля age - значение 22. Вполедствии мы также сможем обращаться к этим полям и переустанавливать их значения.

Тажке можно использовать сокращенную форму инициализации:

По сути она будет эквивалетна предыдущей.

Консольный вывод определенной выше программы:

Подобным образом мы можем определить несколько конструкторов и затем их использовать:

В классе Person определено три конструктора, и в функции все эти конструкторы используются для создания объектов:

Хотя пример выше прекрасно работает, однако мы можем заметить, что все три конструктора выполняют фактически одни и те же действия - устанавливают значения переменных name и age. И в C++ можем сократить их определения, вызова из одного конструктора другой и тем самым уменьшить объем кода:

Запись Person(string n): Person(n, 18) представляет вызов конструктора, которому передается значение параметра n и число 18. То есть второй конструктор делегирует действия по инициализации переменных первому конструктору. При этом второй конструктор может дополнительно определять какие-то свои действия.

Таким образом, следующее создание объекта

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

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

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

Этот класс не будет компилироваться, так как здесь есть две ошибки - отсутствие инициализации константы name и ссылки ageRef. Хотяя их значения устанавливаются в конструкторе, но к моменту, когда код инструкции из тела конструктора начнут выполняться, константы и ссылки уже должны быть инициализированы. И для этого необходимо использовать списки инициализации:

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

Таким образом, все переменные, константы и ссылки получат значение, и никакой ошибки не возникнет.

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

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

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

В предыдущем уроке мы представили концепцию контейнерных классов и показали пример класса 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 , реализуйте перегруженный оператор присваивания, который принимает список инициализаторов.

В последних двух уроках мы изучили основы наследования в 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 .

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