Рассмотрим следующую строку кода:
1 |
int a = 7; |
Здесь используется копирующая инициализация для инициализации целочисленной переменной a
значением 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 |
#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); } 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 seven = Drob(7); std::cout << seven; return 0; } |
Результат выполнения программы:
7/1
Форма копирующей инициализации в языке C++ в вышеприведенном примере обрабатывается точно так же, как и следующая:
1 |
Drob seven(Drob(7)); |
А, как мы уже знаем из предыдущего урока, это может привести к вызову как Drob(int, int)
, так и конструктора копирования Drob (который может быть проигнорирован). Однако, поскольку гарантии на 100% игнорирования конструктора копирования не предоставляется, то лучше избегать использования копирующей инициализации при работе с классами и вместо нее использовать прямую инициализацию или uniform-инициализацию, так как в случае с использованием конструктора копирования у вас может получиться следующий результат:
7
Вместо необходимого:
7/1
Так как в конструкторе копирования (который язык C++ предоставит автоматически) значения по умолчанию для m_denominator
не будет.
Правило: Избегайте использования копирующей инициализации при работе с классами, вместо нее используйте 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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#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 ©) : m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) { // Нет необходимости выполнять проверку denominator здесь, так как она осуществляется в конструкторе по умолчанию std::cout << "Copy constructor worked here!\n"; // просто чтобы показать, что это работает } friend std::ostream& operator<<(std::ostream& out, const Drob &d1); int getNumerator() { return m_numerator; } void setNumerator(int numerator) { m_numerator = numerator; } }; std::ostream& operator<<(std::ostream& out, const Drob &d1) { out << d1.m_numerator << "/" << d1.m_denominator; return out; } Drob makeNegative(Drob d) // правильно было бы здесь использовать константную ссылку { d.setNumerator(-d.getNumerator()); return d; } int main() { Drob sixSeven(6, 7); std::cout << makeNegative(sixSeven); return 0; } |
Здесь функция makeNegative() принимает объект класса Drob по значению и возвращает его так же по значению. Результат выполнения программы:
Copy constructor worked here!
Copy constructor worked here!
-6/7
Первый вызов конструктора копирования выполнится при передаче sixSeven
в качестве аргумента в параметр d
функции makeNegative(). Второй вызов выполнится при возврате объекта из функции makeNegative() обратно в функцию main(). Таким образом, объект sixSeven
копируется дважды.
В примере, приведенном выше, компилятор не может проигнорировать использование конструктора копирования как в передаче аргумента по значению, так и в его возврате. Однако в некоторых случаях, если аргумент или возвращаемое значение соответствуют определенным критериям, компилятор может проигнорировать использование конструктора копирования. Например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> class Something { }; Something boo() { Something x; return x; } int main() { Something x = boo(); return 0; } |
В этом случае компилятор, скорее всего, проигнорирует использование конструктора копирования, хоть объект x
и возвращается по значению.
Не совсем ясно, почему "в конструкторе копирования (который язык C++ предоставит автоматически) значения по умолчанию для m_denominator не будет." если будет создан анонимный объект, для чего будет вызван конструктор по умолчанию с проверкой на 0. Даже в предыдущем уроке в явно предоставленном конструкторе копирования есть комментарий "// Нет необходимости выполнять проверку denominator здесь, так как она осуществляется в конструкторе по умолчанию".
Дело в почленной инициализации, объект копия копирует члены объекта оригинала
В разделе: "Другие применения копирующей инициализации"
в коде на 37 строке есть комментарий: "// правильно было бы здесь использовать константную ссылку". Вопрос: Почему?
Константная ссылка не позволит нам использовать функцию-член setNumerator().
Поддержу предыдущего оратора. И добавлю.
Результат 7 получиться никак не может.
Кто откусил вывод косой и знаменателя. Оператор << никак не меняется.
Скорее всего речь идет о выводе 7/0.
Да что-то и 7/0, наверное, никогда не получится. В случае конструктора копирования будет создан анонимный объект, конструктор для него знаменатель укажет 1 (по умолчанию), а потом конструктор копирования (компилятора) сделает присвоение из анонимного объекта каждому члену (числителю и знаменателю) для вновь создаваемого объекта .
В Debug версии вызывается конструктор по умолчанию один раз, а в Release версии даже конструктор вызываться не будет, все сразу на консоль выведется, судя Disassembly в VS2019. Сложно вот так на вид судить, что там компилятор вызывает, пока сам не посмотришь.
Немного не понятно, как следующий код:
может выдать результат 7 в случае если не будет оптимизации?
При вызове
Drob(int, int)
получимm_numerator = 7
иm_denominator = 1
в анонимном объекте, которые почленно скопируются дефолтным конструктором копирования в sevenНаверное этот парень уже прошел все уроки, но отвечу все равно.
Речь шла не об:
а об:
в последнем случае результат может быть 7, так как конструктор который мы указали не вызовется(вызовется копирующий конструктор, в котором мы не указали параметры по умолчанию).
Копирующий конструктор, который не явно будет предоставлен классу, выполняет почленное копирование. Значит Drob seven должна быть
7/1
. Разве не так?