Урок №116. Конструкторы

  Юрий  | 

  |

  Обновл. 4 Сен 2021  | 

 192592

 ǀ   34 

На этом уроке мы рассмотрим конструкторы в языке С++.

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

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

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

Как тогда инициализировать класс с закрытыми переменными-членами? Использовать конструкторы.

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

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

   конструкторы всегда должны иметь то же имя, что и класс (учитываются верхний и нижний регистры);

   конструкторы не имеют типа возврата (даже void).

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

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


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

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

Результат выполнения программы:

0/1

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

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

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

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

Как использовать конструктор с параметрами? Всё просто! Прямая инициализация:

Здесь мы инициализировали нашу дробь числами 4 и 5, результат — 4/5!

В C++11 мы также можем использовать uniform-инициализацию:

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

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

Правило: Используйте прямую инициализацию или uniform-инициализацию с объектами ваших классов.

Копирующая инициализация


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

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

Правило: Не используйте копирующую инициализацию с объектами ваших классов.

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

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

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

На практике старайтесь сокращать количество конструкторов вашего класса.

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


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

У этого класса нет конструктора, поэтому компилятор сгенерирует следующий конструктор:

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

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

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

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

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

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

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

Результат выполнения программы:

A
B

При создании переменной b вызывается конструктор B(). Прежде чем тело конструктора выполнится, m_a инициализируется, вызывая конструктор по умолчанию класса A. Таким образом выведется A. Затем управление возвратится обратно к конструктору B, и тело конструктора B начнет свое выполнение.

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

Тест

Задание №1

a) Напишите класс Ball, который должен иметь следующие две закрытые переменные-члены со значениями по умолчанию:

   m_color (Red);

   m_radius (20.0).

В классе Ball должны быть следующие конструкторы:

   для установления значения только для m_color;

   для установления значения только для m_radius;

   для установления значений и для m_radius, и для m_color;

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

Не используйте параметры по умолчанию для конструкторов. Напишите еще одну функцию для вывода цвета (m_color) и радиуса (m_radius) шара (объекта класса Ball).

Следующий код функции main():

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

color: red, radius: 20
color: black, radius: 20
color: red, radius: 30
color: black, radius: 30

Ответ №1.а)

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

Ответ №1.b)

Задание №2

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

Ответ №2

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (320 оценок, среднее: 4,88 из 5)
Загрузка...

Комментариев: 34

  1. kolya:

    Здравствуйте. У меня такой вопрос. Когда мы заходим в конструктор (в любой), перед этим программа пробегается по коду класса и инициализирует поля. Например, у нашего класса есть только 2 поля: int a и int b. Оба неинициализированы. Если попытаться вывести их на экран методом print, выведется мусор, и это понятно. Но если мы к нашему классу добавим еще одно поле-указатель и его тоже не будем инициализировать, после чего попробуем вывести те 2 интовых поля, которые все также остаются неинициализированными, нам выведутся нули. Почему наш указатель инициализирует каким-то образом две другие переменные? Пожалуйста, объясните.

    Вот мы пишем наш класс и выводится мусор:

    Но если написать с указателем, то выведется 0   0 (нули)

    1. Daniil:

      Привет! Разница в поведении, которую вы замечаете, обусловлена тем, что в C++ инициализация полей класса с указателями инициирует их значением nullptr, что является нулевым указателем.

      В первом примере, где у вас только два целочисленных поля int a и int b, эти поля не инициализированы явно, поэтому они содержат случайные значения из памяти, что и приводит к выводу мусора при попытке их печати.

      Однако, во втором примере, когда вы добавляете указатель int* pa, он автоматически инициализируется значением nullptr. Таким образом, при создании объекта classik A, указатель pa становится равным nullptr, а целочисленные поля a и b по-прежнему остаются неинициализированными.

      В методе Print(), когда вы пытаетесь вывести значения полей a и b, они все еще содержат случайные значения. Однако при печати значения указателя pa выводится 0, что является значением nullptr.

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

  2. Максим:

    В начале статьи в разделе Конструкторы, в примере, там класс полностью публичный, без наследования, без virtual. Разве там не агрегатная инициализация будет использована в место uniform инициализации?

  3. Сергей:

    Прочитал задание и решил что Red это перечислитель, и автор напоминает что там фигурировало слово "class" и надо отлистать к главе 59
    https://ravesli.com/urok-59-klassy-enum/
    "Хотя классы enum используют ключевое слово class, в C++ они не считаются традиционными «классами»."

    В связи с этим решил всё же опубликовать получившийся вариант задания (хотя вызовы в main пришлось тоже изменить):

  4. Захар:

    Удалось сделать 1 задание с использованием одного конструктора.
    Результат такой же:

  5. DevDev:

    А почему интересно нужно в каждом конструкторе прописывать значения по умолчанию? Почему нельзя прописать их в каком-нибудь одном, и пусть компилятор берёт оттуда недостающие.

    1. Ninedenon:

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

  6. Антон:

    В задаче 1b, мы вызываем конструктор с одним аргументом типа String
    Ball black("black");
    А в определении конструкторов у нас нет конструктора с одним параметром типа String, есть только с радиусом или с двумя параметрами.
    Как это работает?

    1. Андрей:

      Это работает так же, как и параметры по умолчанию в функциях (см. урок № 103).
      Когда Вы создаете объект Ball black("black"), значения радиуса берется из второго параметра конструктора по умолчанию

      Можно сказать, что создается объект Ball black("black", 20.0) (параметр color мы переопределяем, radius (20.0) остается по умолчанию).

      Вам не нужно писать отдельный конструктор с единственным параметром по умолчанию color (Ball(const string &color)).
      Но нужно обязательно создать конструктор Ball(double radius), т. к. если бы был только единственный конструктор

      то при создании объекта Ball(30.0) компилятор пытался бы "запихнуть"(переопределить) значение double 30.0 в значение типа std::string.

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

  7. Александр:

    "Обратите внимание, наш числитель (m_numerator) и знаменатель (m_denominator) были инициализированы значениями, которые мы задали в конструкторе по умолчанию! Это настолько полезная особенность, что почти каждый класс имеет свой конструктор по умолчанию. Без него значениями нашего числителя и знаменателя был бы мусор до тех пор, пока мы явно не присвоили бы им нормальные значения."
    Насчет последнего предложения, а какая в этом проблема? Если мы все равно не читаем значение из переменной до того, как присвоим ей что-то, какая нам разница, будет там хранится условный 0 или мусор?

    1. Grave18:

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

  8. Кирилл:

    Правильно ли я понимаю, что конструкторы выступают в роли Сеттеров? Раз Сеттеры присваивают значения параметров функции переменных-членов классов, то, получается, что конструкторы и есть сеттеры, тк делают то же самое. Если нет — объясните пожалуйста в чём их различия. Спасибо.

    1. Анастасия:

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

    2. Сергей:

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

  9. Александр:

    Хотелось бы выяснить, почему компилятор ругается на указанный в уроке пример Fraction seven(7);
    Мне вот он сообщил, что не может преобразовать аргумент из int в const Fraction. Это вообще нормально или мне просто так не везёт?

  10. Анастасия:

    Здравствуйте. Конструктор должен быть только в public?

    1. madmanutdfan:

      Нет, можно объявить и в private, тогда:
      1) создавать объекты данного класса через данный конструктор можно будет через отдельную статическую функцию данного класса;
      2) нельзя создавать объекты данного класса через данный конструктор;

  11. Арсений:

    Я не совсем понимаю, как тут пользователь может менять цвет, если string — это константа?

    И можно поподробней о том, какую роль здесь играет создание ссылки на color?

    1. Александр:

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

    2. Артур:

      Арсений, Вы путаете переменные. color — это константная ссылка, в которой записан цвет на который Вы хотите поменять, но за сам цвет отвечает переменная m_color, которая не является константой и которой присваивается значение переменной color:

  12. Влад:

    double getValue() { return static_cast<double>(m_numerator) / m_denominator; } Что означает?

    1. Алексей:

      Это сокращённое определение метода, который возвращает значение double, т.е. частное m_numerator и m_denominator, которые являются членами класса.(В примере, приведённом в этом уроке)

  13. kmish:

    Второе решение не совсем точное:
    если я не знаю порядка предоставления m_color и m_radius?
    Допустим я введу:

    Точнее будет:

    Результат:

    color: green, radius: 40

    1. Никита:

      Спасибо, полезная информация

  14. Ануар:

    Не могу понять вот это предложение:
    "Здесь есть смысл, так как конструктор B() может захотеть использовать переменную m_a, поэтому сначала нужно инициализировать m_a!"

    Если я не инициализирую m_a, каким образом конструктор B() сможет захотеть использовать m_a, если ее нет?

    Значит открыл я компилятор, и попробовал сделать так, чтобы конструктор B() захотел использовать переменную m_a; Ну наверн тогда надо int m_a , но тогда нету класса внутри класса.

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

    Вобщем, надеюсь что я просто слишком умный чтобы понять это предложение)

    1. Алена:

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

      Т.е. при создании класса создаются члены класса (поэтому идет переход в А), затем выполняется тело конструктора.

  15. Torgu:

    никак не могу понять, зачем в задании №1 использовать ссылку на color в методе? Да еще и константную. А если все-таки этому есть практической применение, то почему только для color? Почему radius тоже не сделать константной ссылкой?

    1. ufo:

      "никак не могу понять, зачем в задании №1 использовать ссылку на color в методе? Да еще и константную. А если все-таки этому есть практической применение, то почему только для color? Почему radius тоже не сделать константной ссылкой?"

      потому как передача radius происходит по значению (и класс если что с ним и сделает, то испортит только копию значения)
      При передаче по ссылке или по указателю возможна порча оригинальных данных в памяти поэтому пишется конст.
      Строка может занимать значительный объем памяти, поэтому передавать ее по значению нерационально и снижает быстродействие, а вот double radius что адрес что переменная занимает 4 байта, хотите пишите и ее через конст. & но выхлопа от этого не будет.

      1. Борис:

        Сути не меняет, но double занимает 8 байт.

  16. Torgu:

    Зачем использовать конструкторы, когда дефолтные значения можно еще в начале класса проставить? А если потребуется изменить, можно создать специальный метод (сеттер)

    1. madmanutdfan:

      "Зачем использовать конструкторы, когда дефолтные значения можно еще в начале класса проставить?"
      Дефолтные значения могут зависеть от других данных.

      "если потребуется изменить, можно создать специальный метод (сеттер)"
      Каждый раз вызывать сеттер для каждого поля каждого объекта неудобно — занимает лишнее место в коде, всегда надо помнить про сеттер, можно забыть установить, если забыть задать дефолтное значение и обратиться к данному полю через геттер, то …

  17. Анатолий:

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

    1. Фото аватара Юрий:

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

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *