Рассмотрим следующую строку кода:
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().
Сеттер мы не можем сделать константным, поэтому объект в функцию принимаем просто по ссылке.
На уроке №98 мы рассмотрели преимущества передачи аргументов по константной ссылке, нежели по значению. Если вкратце, то передача аргументов по значению создает копию значения (что является медленным процессом). Большую часть времени нам не нужна копия, а ссылка уже указывает на исходный аргумент и является более эффективной, так как избегает создания и использования ненужной копии. Мы обычно делаем ссылку константной для гарантии того, что функция не изменит значение аргумента и сможет работать с r-values (например, с литералами).
А возврат можно сделать уже и константным, если нужен константный объект.
Константный объект может вызывать только константные методы. Константный метод не может вызывать неконстантные методы.
https://ravesli.com/urok-123-klassy-i-const/#toc-1
Поддержу предыдущего оратора. И добавлю.
Результат 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
. Разве не так?