Конструктор копирования для матрицы с

Обновлено: 16.05.2024

Когда новички изучают программирование, первым делом, при рассмотрении новой темы, возникает вопрос – для чего необходима та или иная “вещь” о которой сейчас предстоит узнать. Ответим сразу на этот вопрос: “Зачем нужен конструктор копирования?”.

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

  • при передаче объекта класса в функцию, как параметра по значению (а не по ссылке);
  • при возвращении из функции объекта класса, как результата её работы;
  • при инициализации одного объекта класса другим объектом этого класса.

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

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

Использование конструктора копирования – прекрасный способ обойти эти ошибки и проблемы. Он создаст “реальную” копию объекта, которая будет иметь личную область динамической памяти.

Конструктор копирования синтаксически выглядит так:

Ниже разберём несложный, но очень показательный пример. В нём будут рассмотрены все 3 случая в которых желательно применять конструктор копирования. Будет создан класс, содержащий конструктор без параметров, конструктор копирования и деструктор.

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

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

Запустив программу увидим в консоли следующее:

конструктор копирования в с++, конструктор копии c++, программирование на с++ с нуля

Посмотрим что программа выдала в консоль. Блок 1 – во время создания нового объекта, сработал конструктор без параметров. В блоке 2 мы разместили функцию showFunc() . Во время передачи в неё “объекта-параметра” по значению, сработал конструктор копирования и создалась “реальная” копия объекта класса OneClass .

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

В блоке 3 размещена функция returnObjectFunc() . Так как в её теле прописано создание нового объекта класса OneClass – сначала сработал конструктор без параметров. Далее выполняется код функции и во время возврата объекта в главную функцию main , сработал конструктор копирования. В конце, как и должно быть, деструктор отработал дважды: для объекта и для его реальной копии.

В четвертом блоке, во время объявления и инициализации нового объекта object2 , сработал конструктор копирования. При завершении работы программы деструктор сработал для копии объекта из четвертого блока и для объекта object1 из первого блока.

Если же мы закомментируем /*конструктор копирования*/ в классе и снова запустим программу – увидим, что конструктор без параметров сработает 2 раза, а деструктор – пять раз отработает.

конструктор копирования в с++, конструктор копии c++, программирование на с++ с нуля

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

Очень рекомендую прочесть тему Конструктор копирования в книге Стивена Прата “Язык программирования С++. Лекции и упражнения. 6-е издание.” Она раскрыта намного глубже и включает все основные нюансы использования конструктора копирования. Подробно рассмотрена операция присваивания = .

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

В частности, я не уверен в двух вещах:

Предполагается ли деструктору освободить память также для памяти, которая выделена в конструкторе копирования?

Что касается линии Mat[i][j]=other[i][j] (см. код ниже), интересно, стоит ли мне писать Mat[i][j]=other.Mat[i][j] вместо?

Решение

1) Я думаю, что деконструктор стирает только тот объект, который принадлежит, потому что объекты, созданные с помощью копирования, имеют свой собственный деструктор.

2) Да, Mat[i][j] = other.Mat[i][j] это правильно, но если вы хотите, чтобы ваша программа работала немного быстрее, попробуйте использовать указатели (вначале это не так просто, я знаю, но когда вы привыкнете, это не так сложно ^^)

Другие решения

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

Создание объекта включает в себя несколько операций, таких как:

  1. Выделение памяти для хранения структуры для нового объекта
  2. Инициализация атрибутов объекта правильно.

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

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

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

Надеюсь это поможет.

Я не уверен, что это полностью отвечает на ваши вопросы, но я надеюсь, что это поможет.

Да. Все, что объект еще выделил, должно быть освобождено в деструкторе. Неважно, какой конструктор выделил его.

Да, вам нужно использовать Mat[i][j]=other.Mat[i][j] тем более что вы еще не определили operator[] для вашего класса.

Вам также нужно добавить оператор копирования копии, в соответствии с «Правило трех» , который в основном гласит:

Если для класса требуется пользовательский деструктор, пользовательский конструктор копирования или пользовательский оператор назначения копирования, то почти наверняка требуются все три.

Лучшее решение — не использовать new[] / delete[] прямо на всех. использование std::vector вместо этого, и пусть он обрабатывает все для вас, позволяя вашему классу следовать «правилу нуля»:

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

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

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

Деструктор (destructor) - это функция-член, имя которой представляет собой ~имя класса, предназначенная для уничтожения переменных (delete).

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

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

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

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

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

Полный конструктор

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

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

Неполный конструктор

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

Инициализация переменных-членов класса в конструкторах может осуществляться не только в теле конструктора, но и после оператора :. При этом, во время присваивания переменной-члену значения, будет вызываться не оператор присваивания, а конструктор. Для встроенных типов данных, таких как double или int, это не существенно, но если членами класса являются абстрактные типы, вызов конструктора вместо оператора присваивания будет выполняться быстрее.

или такой вариант:

Конструктор копирования

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

4.3.2. Деструктор (пример 4.4. Конструктор и деструктор класса Матрица)

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

В классе Lens никакого динамического размещения не происходило, поэтому деструктор будет пустой, но его наличие все равно обязательно. Для примера реализации деструктора, представим, что имеется класс Matrix, который в конструкторе динамически создает двумерный массив размерности n x m. Тогда деструктор должен освобождать память, которую выделяет конструктор.

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

4.3.3. Проверка правильности параметров. Исключительные ситуации

Конструкторы должны проверять передаваемые им аргументы на корректность значений. Например, показатель преломления не может быть меньше 1. Что делать, если в конструктор были переданы неправильные параметры? Для этого в языке С++ существуют исключительные ситуации.

Класс exception является стандартным базовым классом C++ для всех исключений. Исключения можно сгенерировать в случае возникновения непредвиденной ошибки, например мы предполагаем что при вызове класса Lens никто не будет пытаться задать показатель преломления меньше 1, но при этом такая ситуация возможна, и это может привести к ошибке. Сгенерировать исключительную ситуацию можно при помощи оператора throw:

Для обработки возникшей исключительной ситуации используются try и catch блоки.

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

Если никаких исключений в try-блоке не происходит, программа игнорирует его catch-обработчик.

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

По условию задания запрещено использовать STL-контейнеры, я должен сам реализовать элементы управления

Добавлена ​​семантика перемещения

Вам лучше использовать одномерный массив, к которому вы используете T& operator()(size_t row, size_t col) < return matrix[row * cols + col]; >для доступа. Копирование одного из них, вероятно, настолько быстро, насколько это возможно. Используйте vector вместо matrix , и вы получите большую часть бесплатно.

Вам действительно нужно менять столбцы и строки во время выполнения? Если нет, рассмотрите возможность их объявления в параметрах шаблона и используйте std::array , вам не понадобится настраивать конструктор копирования и оператор присваивания.

Есть ли у вас причина использовать ручное управление памятью в классе Matrix? Это какое-то задание (без каламбура) или что-то в этом роде? Если нет, то предложение @ted имеет много достоинств.

Как и @ 273K, если на то пошло, но по другим причинам.

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

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

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

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

@ЕлизаветаТараненко Пожалуйста, проверьте мой ответ. Я думаю, это то, что вы хотите. Я отредактировал, чтобы использовать ручное управление памятью вместо std::vector Конечно, понадобятся конструкторы и деструкторы.

Не похоже, что эти static_cast нужны. Приведения должны заставить вас заподозрить, что вы делаете что-то не так.

Указана ли в присваивании семантика присваивания матриц для матриц разной размерности?

@einpoklum, ты имеешь в виду не только квадратные матрицы? Если так, то да, матрицы могут быть разного размера

@ЕлизаветаТараненко: Я имею в виду, что должно произойти, если вы присвоите матрице размера 10x20 матрицу размера 1x1? Это должно потерпеть неудачу, или целевая матрица теперь должна стать матрицей 10x20?

@ЕлизаветаТараненко Что должно произойти, если вы назначите матрицу 2x3 матрице 3x2?

Goswin von Brederlow

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

Зачем вам использовать семантику перемещения с примитивами? Это не имеет никакого смысла.

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

@Doug Этого недостаточно. Также требуется знать размер матрицы во время компиляции.

@simre Меня беспокоило то, что без возможности назначать матрицы с разными строками / столбцами такие вещи, как std::swap(mat1, mat2); , не будут работать. И это используется вектором.

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

Конструктор перемещения должен по сути переназначать указатели на внутреннее содержимое. Чтобы переназначить эти члены нужно видеть определение класса Matrix .

2 ответа 2

Конструктор может выглядеть следующим образом

У вас может возникать проблема из-за неправильно определенного деструктора. Как я понимаю, в деструкторе, прежде чем удалять каждый из элементов массива, вы должны проверить, не равен ли указатель m_matrix nullptr . Например

В вашем случае вы сначала присваиваете указателю (подозреваю, что m_matrix это указатель) на матрицу текущего объекта указатель из копируемого, то есть далее в цикле в строке m_matrix[i] = matrix.m_matrix[i]; вы присваиваете значение само себе, а в следующей строке обнуляете его, причем в обоих объектах.

Кроме того, во многих случаях, где копирующий конструктор должен выполнять глубокое копирование, стандартный перемещающий конструктор является пригодным, т.к. применяет функцию move к полям переносимого класса, поэтому, возможно и в вашей ситуации достаточно объявить его как Matrix::Matrix(Matrix &&matrix) =default;

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

@AnT, перемещающий конструктор по умолчанию применяет к полям объекта move , а не копирует. При применении move объект-источник перестает быть валидным. Про конструкторы по умолчанию.

Во-первых, "перестает быть валидным" - это концепция уровня пользователя. Это задача автора класса - обсепечить поддержание и распознание "невалидного" состояния после move. На уровне ядра языка же нет понятия какого-то детерминистского состояния "невалидного объекта". Если вы сделаете move из одного голого указателя в другой, исходный указатель никак не изменится и никаким магическим образом не станет "невалидным". Он просто скопируется и все.

Во-вторых, судя по всему, в данном случае полями объекта являются поля фундаментальных типов: указатель и два целых числа. Для фундаментальных типов операция move эквивалентна операции copy: они просто копируются и все. Они сам по себе никак не "перестают быть валидными". В данном случае defaulted move constructor будет просто прямым эквивалентом shallow copy constructor. Со всем вытекающими.

Вот если бы m_matrix был стандартным smart pointer, то тогда надежда на работоспособность defaulted move constructor была бы. Но это, очевидно, не так.

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