Вспомним все типы инициализации, которые поддерживает язык C++: прямая инициализация, uniform-инициализация и копирующая инициализация.
Конструктор копирования
Рассмотрим примеры всех вышеприведенных инициализаций на практике, используя следующий класс Drob:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } |
Мы можем выполнить прямую инициализацию:
1 2 |
int a(7); // прямая инициализация целочисленной переменной Drob sixSeven(6, 7); // прямая инициализация объекта класса Drob, вызывается конструктор Drob(int, int) |
В C++11 мы можем выполнить uniform-инициализацию:
1 2 |
int a { 7 }; // uniform-инициализация целочисленной переменной Drob sixSeven {6, 7}; // uniform-инициализация объекта класса Drob, вызывается конструктор Drob(int, int) |
И, наконец, мы можем выполнить копирующую инициализацию:
1 2 3 |
int a = 7; // копирующая инициализация целочисленной переменной Drob eight = Drob(8); // копирующая инициализация объекта класса Drob, вызывается Drob(8, 1) Drob nine = 9; // копирующая инициализация объекта класса Drob. Компилятор будет искать способ конвертации 9 в объект класса Drob, что приведет к вызову конструктора Drob(9, 1) |
С прямой инициализацией и uniform-инициализацией создаваемый объект непосредственно инициализируется. Однако с копирующей инициализацией дела обстоят несколько сложнее. Мы рассмотрим это детально на следующем уроке. Но перед этим нам еще нужно кое в чём разобраться.
Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#include <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // прямая инициализация объекта класса Drob, вызывается конструктор Drob(int, int) Drob dCopy(sixSeven); // прямая инициализация - какой конструктор вызывается здесь? std::cout << dCopy << '\n'; } |
Результат выполнения программы:
6/7
Рассмотрим детально, как работает эта программа.
С объектом sixSeven
выполняется обычная прямая инициализация, которая приводит к вызову конструктора Drob(int, int)
. Здесь нет никаких сюрпризов. А вот инициализация объекта dCopy
также является прямой инициализацией, но какой конструктор вызывается здесь? Ответ: конструктор копирования.
Конструктор копирования — это особый тип конструктора, который используется для создания нового объекта через копирование существующего объекта. И, как в случае с конструктором по умолчанию, если вы не предоставите конструктор копирования для своих классов самостоятельно, то язык C++ создаст public-конструктор копирования автоматически. Поскольку компилятор мало знает о вашем классе, то по умолчанию созданный конструктор копирования будет использовать почленную инициализацию. Почленная инициализация означает, что каждый член объекта-копии инициализируется непосредственно из члена объекта-оригинала. Т.е. в примере, приведенном выше, dCopy.m_numerator
будет иметь значение sixSeven.m_numerator
(6
), а dCopy.m_denominator
будет равен sixSeven.m_ denominator
(7
).
Так же, как мы можем явно определить конструктор по умолчанию, так же мы можем явно определить и конструктор копирования. Конструктор копирования выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
#include <iostream> #include <cassert> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } // Конструктор копирования Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) // Примечание: Мы имеем прямой доступ к членам объекта drob, поскольку мы сейчас находимся внутри класса Drob { // Нет необходимости выполнять проверку denominator здесь, так как эта проверка уже осуществляется в конструкторе класса Drob std::cout << "Copy constructor worked here!\n"; // просто, чтобы показать, что это работает } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // прямая инициализация объекта класса Drob, вызывается конструктор Drob(int, int) Drob dCopy(sixSeven); // прямая инициализация, вызывается конструктор копирования класса Drob std::cout << dCopy << '\n'; } |
Результат выполнения программы:
Copy constructor worked here!
6/7
Конструктор копирования в вышеприведенном примере использует почленную инициализацию и функционально эквивалентен конструктору по умолчанию, за исключением того, что мы добавили стейтмент вывода, в котором указали текст (сработал конструктор копирования).
Предотвращение создания копий объектов
Мы можем предотвратить создание копий объектов наших классов, сделав конструктор копирования закрытым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> #include <cassert> class Drob { private: int m_numerator; int m_denominator; // Конструктор копирования (закрытый) Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) { // Нет необходимости выполнять проверку denominator здесь, так как эта проверка уже осуществляется в конструкторе класса Drob std::cout << "Copy constructor worked here!\n"; // просто, чтобы показать, что это работает } public: // Конструктор по умолчанию Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(6, 7); // прямая инициализация объекта класса Drob, вызывается конструктор Drob(int, int) Drob dCopy(sixSeven); // конструктор копирования является закрытым, поэтому эта строка вызовет ошибку компиляции std::cout << dCopy << '\n'; } |
Здесь мы получим ошибку компиляции, так как dCopy
должен использовать конструктор копирования, но он не видит его, поскольку конструктор копирования является закрытым.
Конструктор копирования может быть проигнорирован
Рассмотрим следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <cassert> #include <iostream> class Drob { private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Drob(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) { assert(denominator != 0); } // Конструктор копирования Drob(const Drob &drob) : m_numerator(drob.m_numerator), m_denominator(drob.m_denominator) { // Нет необходимости выполнять проверку denominator здесь, так как эта проверка уже осуществляется в конструкторе класса Drob std::cout << "Copy constructor worked here!\n"; // просто, чтобы показать, что это работает } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } int main() { Drob sixSeven(Drob(6, 7)); std::cout << sixSeven; return 0; } |
Сначала инициализируется анонимный объект Drob, который приводит к вызову конструктора Drob(int, int)
. Затем этот анонимный объект используется для инициализации объекта sixSeven
класса Drob. Поскольку анонимный объект является объектом класса Drob, как и sixSeven
, то здесь должен вызываться конструктор копирования, верно?
Запустите эту программу самостоятельно. Ожидаемый результат:
Copy constructor worked here!
6/7
Реальный результат:
6/7
Почему наш конструктор копирования не сработал?
Дело в том, что инициализация анонимного объекта, а затем использование этого объекта для прямой инициализации уже не анонимного объекта выполняется в два этапа (первый этап — это создание анонимного объекта, второй этап — это вызов конструктора копирования). Однако, конечный результат по сути идентичен простому выполнению прямой инициализации, которая занимает всего лишь один шаг.
По этой причине в таких случаях компилятору разрешается отказаться от вызова конструктора копирования и просто выполнить прямую инициализацию. Этот процесс называется элизией.
Поэтому, даже если вы напишите:
1 |
Drob sixSeven(Drob(6, 7)); |
Компилятор может изменить это на:
1 |
Drob sixSeven(6, 7); |
В последнем случае с прямой инициализацией потребуется вызов только одного конструктора (Drob(int, int)
). Обратите внимание, в случаях, когда используется элизия, любые стейтменты в теле конструктора копирования не выполняются, даже если они имеют побочные эффекты (например, выводят что-либо на экран)!
Наконец, если вы делаете свой конструктор копирования закрытым, то любая инициализация, использующая этот закрытый конструктор копирования, приведет к ошибкам компиляции, даже если конструктор копирования будет проигнорирован!
По поводу наглядного примера необходимости перегрузки конструктора копирования(КК) можно было бы добавить пример с инициацией одного объекта другим, при том, что в инициализирующем объекте имеется массив.
Инициализируемый объект будет ссылаться на тот же массив, что определён в инициализирующем, вместо его копии.
Тут на сцену должна выходить перегрузка КК =)
В статье не хватает примера, когда без констуктора копирования обойтись невозможно, потому что код породит ошибку.
Например, я хочу использовать адрес первого из последовательного определённых полей в классе для работы с этими полями, как с массивом, но не хочу засорять текст, использующий класс, скобками.
Тогда я ввожу в класс данное- член- адрес первого поля массива, котoрый инициализирую в конструкторе:
пусть есть функции определения расстояния между точками в виде:
ну и функция вывода координат точек:
теперь использую класс:
и хочу воспользоваться функцией вычисления расстояния, вызывая её так:
Запишите в список благодарящих и меня.
Но…. Добавлю немного критики.
Если открыта возможность комментировать и комментарии проходят модерацию, хотелось бы видеть комментарии хотя бы не содержащие явных ошибок. На приведенный выше код компилятор матерится. Отсутствующие ;
Запятые вместо меньше.
Так ac или bc?
По самому примеру.
1. Кто гарантирует возможность работы с набором отдельных переменных как с массивом? Где гарантия, что отдельные переменные будут располагаться подряд и не будет по каким-то причинам использовано выравнивание по адресам? Где гарантия, что компилятор не оптимизирует распределение памяти?
2. Перефразирую начало. Я хочу использовать классы, но использовать классы не хочу.
Хотя в целом мысль идет в правильном направлении.
И сам ошибся 🙂
Так ac или ab?
в названии переменной — ac а вызывается a и b.
Возможность обращения как к массиву и строго последовательное размещение в памяти при описании одним оператором, именно в таком виде- double x,y,z; гарантируется стандартом языка.
Читал про это очень давно, ещё для языка с. Для с++ то же самое вытекает из правила "любая корректная программа на с является корректной на с++". Неоднократно встречал случаи такого использования в реальных программах и применял сам, проблем никогда не было.
Qt 5.14.2 (MSVC 2017, 32 бита) выдает следующее:
1
4199920
2
Так что, в реальных программах такие фокусы лучше не использовать.
может еще описать способ =delete? или если есть уже такая тема, указать ссылку на него( раз способ закрытия конструктора копирования указывается в этой теме)
Спасибо тебе большое за этот титанический труд по переводу! Уроки действительно очень хорошие и легко читаются. Надеюсь, у тебя хватит сил и терпению перевести их до конца. Думаю, они многим помогут выучить С++.
Спасибо за отзыв. Надеюсь, что сил и терпения действительно хватит 🙂
Всячески присоединяюсь к предыдущему комментарию! Спасибо, Юрий, огромное! Тяжелый труд — великолепный результат — безграничная благодарность тебе!
Спасибо и Вам, что читаете 🙂