Явный и неявный вызов конструктора

Обновлено: 22.04.2024

По умолчанию C++ обрабатывает любой конструктор как оператор неявного преобразования. Рассмотрим следующий случай:

Хотя функция printFraction() ожидает объект Fraction , вместо этого мы передали ей целочисленный литерал 6. Поскольку у Fraction есть конструктор, который принимает одно число int , компилятор неявно преобразует литерал 6 в объект Fraction . Для этого он инициализирует параметр f функции printFraction() с помощью конструктора Fraction(int, int) .

Следовательно, показанная выше программа печатает:

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

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

Ключевое слово explicit

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

В приведенном выше примере пользователь пытается инициализировать строку с помощью char . Поскольку char является частью семейства целочисленных типов, компилятор будет использовать конструктор преобразования MyString(int) , чтобы неявно преобразовать char в MyString . Затем программа напечатает этот объект MyString , что приведет к неожиданным результатам. Точно так же вызов printString('x') вызывает неявное преобразование, которое приводит к той же проблеме.

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

Приведенная выше программа не будет компилироваться, так как MyString(int) был сделан явным, и не удалось найти соответствующий конструктор преобразования для неявного преобразования ' x ' в MyString .

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

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

Правило

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

Ключевое слово delete

В нашем случае с MyString мы на самом деле хотим полностью запретить преобразование ' x ' в MyString (явное или неявное, поскольку результаты не будут интуитивно понятными). Один из способов частично сделать это – добавить конструктор MyString(char) и сделать его закрытым:

Однако этот конструктор по-прежнему можно использовать изнутри класса (закрытый доступ мешает только нечленам класса вызывать эту функцию).

Лучший способ решить проблему – использовать ключевое слово delete (введенное в C++11) для удаления функции:

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

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

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

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

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

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

Дополнительные сведения см. в разделе Закрытые конструкторы.

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

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

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

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

Оба класса и structs могут определять конструкторы, принимающие параметры. Конструкторы, принимающие параметры, необходимо вызывать с помощью оператора new или base. Классы и structs могут определять также несколько конструкторов; для определения конструктора без параметров ни один их них не требуется. Пример:

Этот класс можно создать, воспользовавшись одним из следующих операторов:

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

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

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

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

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

Применение ключевого слова this в приведенном выше примере привело к вызову конструктора:

Конструкторы могут иметь пометку public, private, protected, internal, protected internal или private protected. Эти модификаторы доступа определяют, каким образом пользователи класса смогут создавать класс. Дополнительные сведения см. в статье Модификаторы доступа.

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

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

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

Стандартные преобразования выполняют преобразование между встроенными типами, между указателями или ссылками на типы, связанные наследованием, в и из указателей void и в пустой указатель. Дополнительные сведения см. в разделе "Стандартные преобразования". Пользовательские преобразования выполняют преобразование между пользовательскими типами или между пользовательскими и встроенными типами. Их можно реализовать как конструкторы преобразования или как функции преобразования.

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

Попытка неявного преобразования выполняется, когда

тип аргумента, предоставленного для функции, не совпадает с соответствующим параметром;

тип значения, возвращаемого функцией, не совпадает с типом возвращаемого значения функции;

тип выражения инициализатора не совпадает с типом инициализируемого объекта;

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

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

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

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

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

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

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

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

Ключевое слово explicit и проблемы с неявным преобразованием

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

Одним из известных примеров неявного преобразования, которое может вызвать проблемы, является преобразование в bool . Существует множество причин, по которым может потребоваться создать тип класса, который можно использовать в логическом контексте( например, для управления оператором if или циклом), но когда компилятор выполняет пользовательское преобразование во встроенный тип, компилятор может применить дополнительное стандартное преобразование. Цель этого дополнительного стандартного преобразования заключается в том, чтобы разрешить такие вещи, как повышение до short int , но он также открывает дверь для менее очевидных преобразований, например от bool к int , что позволяет использовать тип класса в целочисленных контекстах, которые вы никогда не намеревались. Эта конкретная проблема известна как Сейф bool Problem. Эта проблема заключается в том, что ключевое explicit слово может помочь.

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

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

Конструкторы преобразования

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

Обратите внимание, что первый вызов функции display_balance , которая принимает аргументы типа Money , не требует преобразования, так как аргумент принадлежит к правильному типу. Однако при втором вызове display_balance требуется преобразование, так как тип аргумента со значением 49.95 , а не то, double что ожидает функция. Функция не может использовать это значение напрямую, но так как имеется преобразование из типа аргумента ( double в тип соответствующего параметра) Money — временное значение типа Money создается из аргумента и используется для завершения вызова функции. В третьем вызове display_balance обратите внимание, что аргумент не является аргументом double , а имеет float значение 9.99 ,и все же вызов функции может быть завершен, так как компилятор может выполнить стандартное преобразование ( в данном случае — от double float ) и затем выполнить определяемое пользователем преобразование double для Money завершения необходимого преобразования.

Объявление конструкторов преобразования

Следующие правила применяются к объявлению конструктора преобразования.

Целевым типом преобразования является сконструированный пользовательский тип.

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

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

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

Явные конструкторы преобразования

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

В этом примере обратите внимание, что явный конструктор преобразования можно использовать для выполнения прямой инициализации типа payable . Если же вы попытаетесь выполнить инициализацию копирования Money payable = 79.99; , это приведет к ошибке. Первый вызов display_balance не включает преобразование, так как указан аргумент правильного типа. Второй вызов display_balance является ошибкой, так как конструктор преобразования нельзя использовать для выполнения неявного преобразования. Третий вызов является законным из-за явного приведения Money , но обратите внимание, что компилятор по-прежнему display_balance помог завершить приведение путем вставки неявного приведения из float . double

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

Функции преобразования

Функции преобразования определяют преобразования из пользовательского в другие типы. Эти функции иногда называют "операторами приведения", так как они, наряду с конструкторами преобразования, вызываются, когда значение приводится к другому типу. В следующем примере демонстрируется функция преобразования, которая преобразуется из определяемого пользователем типа в Money встроенный тип double :

Обратите внимание, что переменная-член amount является закрытой и что общедоступная функция преобразования в тип double вводится только для возврата значения amount . В функции display_balance неявное преобразование возникает, когда значение balance направляется в стандартный вывод с помощью оператора вставки в поток double компилятор может использовать функцию Money double преобразования для удовлетворения оператора вставки потока.

Функции преобразования наследуются производными классами. Функции преобразования в производном классе переопределяют наследуемую функцию преобразования, только когда выполняют преобразование в точно такой же тип. Например, определяемая пользователем функция преобразования производного оператора класса int не переопределяет (или даже не влияет) определяемую пользователем функцию преобразования оператора базового класса short, даже если стандартные преобразования определяют связь преобразования между int и short .

Объявление функций преобразования

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

Целевой тип преобразования должен быть объявлен до объявления функции преобразования. Классы, структуры, перечисления и определения типа нельзя объявлять в объявлении функции преобразования.

Функции преобразования не принимают аргументов. Указание любых параметров в объявлении является ошибкой.

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

Функции преобразования могут быть виртуальными.

Функции преобразования могут быть явными.

Явные функции преобразования

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

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

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

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

Итак, как же инициализировать класс с закрытыми переменными-членами? Ответ: через конструкторы.

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

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

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

  1. конструкторы должны иметь то же имя, что и класс (с такими же заглавными буквами);
  2. конструкторы не имеют возвращаемого типа (даже не void ).

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

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

Вот пример класса, у которого есть конструктор по умолчанию:

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

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

Эта программа дает следующий результат:

Обратите внимание, что числитель и знаменатель были инициализированы значениями, которые мы установили в конструкторе по умолчанию! Без конструктора по умолчанию числитель и знаменатель будут иметь мусорные значения, пока мы явно не присвоим им осмысленные значения или не инициализируем их другими способами (помните: переменные базовых типов не инициализируются значениями по умолчанию).

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

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

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

Итак, как нам использовать этот конструктор с параметрами? Это просто! Мы можем использовать инициализацию списком или прямую инициализацию:

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

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

Значения по умолчанию для конструкторов работают точно так же, как и с любыми другими функциями, поэтому в приведенном выше случае, когда мы вызываем six , функция Fraction(int, int) вызывается со вторым параметром, по умолчанию равным 1.

Правило

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

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

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

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

Уменьшение количества конструкторов

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

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

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

Напоминание о параметрах по умолчанию

Правила определения и вызова функций с параметрами по умолчанию (описанные в уроке «8.12 – Аргументы по умолчанию») применимы и к конструкторам. Напомним, что при определении функции с параметрами по умолчанию все параметры по умолчанию должны следовать после любых параметров, отличных от параметров по умолчанию, т.е. после параметра по умолчанию не может быть параметров, не заданных по умолчанию.

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

В s4 мы попытались создать Something , предоставив только double . Это не будет компилироваться, поскольку правила соответствия аргументов параметрам по умолчанию не позволят нам пропустить не крайний правый параметр (в данном случае крайний левый параметр типа int ).

Если мы хотим иметь возможность создать Something только c double , нам нужно добавить второй (не используемый по умолчанию) конструктор:

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

Если в вашем классе нет конструкторов, C++ автоматически сгенерирует для вас открытый конструктор по умолчанию. Иногда это называют неявным конструктором («implicit constructor», или неявно сгенерированным конструктором).

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

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

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

Чтобы обеспечить инициализацию переменных-членов, мы можем инициализировать их при их объявлении.

Хотя вы не видите неявно созданный конструктор, вы можете доказать, что он существует:

Приведенный выше код компилируется, потому что объект Date будет использовать неявный конструктор (который является открытым).

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

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

Использование = default – это почти то же самое, что добавление конструктора по умолчанию с пустым телом. Единственное отличие состоит в том, что = default позволяет нам безопасно инициализировать переменные-члены, даже если у них нет инициализатора:

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

Правило

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

Классы, содержащие классы

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

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

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

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

Это имеет смысл, если подумать, что конструктор B() может захотеть использовать переменную m_a , поэтому сначала лучше инициализировать m_a !

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

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

Замечания о конструкторах

Многие начинающие программисты не понимают, создают ли конструкторы объекты или нет. Они этого не делают – компилятор выполняет выделение памяти для объекта до вызова конструктора.

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

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

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

Лучшая практика

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

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

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

Вопрос 1

Напишите класс мяча с именем Ball . Ball должен иметь две закрытые переменные-члены со значениями по умолчанию: m_color (" black ") и m_radius (10.0). Ball должен предоставить конструкторы для установки только m_color , установки только m_radius , установки обоих или ни одного из значений. В этом вопросе теста не используйте параметры по умолчанию для ваших конструкторов. Также напишите функцию для печати цвета и радиуса мяча.

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

и выдавать следующий результат:

b) Обновите свой ответ на предыдущий вопрос, чтобы использовать конструкторы с параметрами по умолчанию. Используйте как можно меньше конструкторов.

Вопрос 2

Что произойдет, если вы не объявите конструктор по умолчанию?

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

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

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