Особенности реализации структурироанных объектов, имеющих компонентные данные в виде указателей
Если в структурированном типе определены компонентные данные в виде указателей, то для того чтобы объекты этого типа в программе работали правильно необходимо в 99% случаев обязательно явно написать код конструктора копии, деструктора и оператора-функции operator=().
Рассмотрим пример, приведённый ниже
struct comp{
int Re, Im;
comp(){Re= 0; Im = 0;} //конструктор по умолчанию
comp(int r, int i){Re = r; Im = i;} // конструктор
};
В этом примере кроме двух конструкторов нет больше явно определённых компонентных функций, но есть неявные, т.е. те, которые компилятор генерирует сам по умолчанию. Для данного примера компилятор по умолчанию сгенерирует следующие функции со следующим содержанием
comp (comp& T) { Re = T.Re; Im=T.Im;}//Конструктор копии
const comp& operator=(const comp& T){ // оператор функция
Re = T.Re; Im = T.Im; return *this;}
~comp(){} //деструктор
а также (если исользуется 11-ый стандарт языка Си++)
comp (comp&&T){ //Конструктор перемещения
std::swap(Re, T.Re); std::swap(Im,T.Im);}
const comp& operator=(const comp&& T){ // оператор функция
std::swap(Re, T.Re); std::swap(Im,T.Im); return *this;}
В принципе, поскольку компонентные данные Re и Im не являются указателями, то сгенерированные компилятором функции будут работать правильно, и программисту нет необходимости писать их самостоятельно. Картина сильно изменится, если определить класс comp следующим образом:
struct comp{
int* Re, *Im;
comp() { Re = new int; Im = new int; *Re = 0; *Im = 0; }
comp(int r, int i) { Re = new int; Im = new int;*Re = r; *Im = i; }
};
Для этого примера компилятор по умолчанию сгенерирует те же самые функции с тем же самым содержанием.
comp (comp& T) { Re = T.Re; Im=T.Im;}//Конструктор копии
const comp& operator=(const comp& T){ // оператор функция
Re = T.Re; Im = T.Im; return *this;}
~comp(){} //деструктор
Только в данном случае эти функции будут работать неправильно, и поэтому программисту их необходимо будет написать самостоятельно. Поясним, в чём заключается их неправильность, и покажем, как правильно реализовать эти функции.
Во-первых, поскольку в конструкторах выделяется динамическая память, то в деструкторе она должна освобождаться, т.е. правильный деструктор должен выглядеть следующим образом
~comp(){delete Re; delete Im;}
Во-вторых, если у нас имеется код вида: comp A(1,1), B(2,2); A=B;, то после выполнения операции присваивания с помощью сгенерированной по умолчанию операциии-функциии operator=() объекты A и B становятся взаимозависимыми, и это чревато трудно обнаруживаемыми ошибками. Сказанное поясним с помощью таблицы, приведённой ниже
До операции присваивания | После операции присваивания |
A.Re à 0x00000001 A.Im à 0x00000001 B.Re à 0x00000002 B.Im à 0x00000002 | 0x00000001 //Ошибка 0x00000001 //Утечка памяти //Ошибка А и В взаимозависимы B.Re à 0x00000002 ßA.Re B.Im à 0x00000002 ß A.Im |
A=B;
const comp& operator=(const comp& T){ // оператор функция
*Re = *(T.Re); *Im =*( T.Im); return *this;}
До операции присваивания указатели Re и Im объектов A и В указывали на разные фрагменты динамической памяти, которые содержали числа 1 и 2. После выполнения операции присваивания, с одной стороны, на фрагменты динамической памяти, содержащие единицы не указывает ни один указатель, а это значит, что мы не сможем эту память освободить с помощью операции delete, т.е. произошла утечка памяти. С другой стороны, на фрагменты памяти с 2 указывают как указатели объекта A, так и В, а это значит, что эти объекты стали взаимозависимыми.
Для того чтобы избежать такой ситуации необходимо программисту самостоятельно определить операцию функцию operator=() следующим образом:
const comp& operator=(const comp& T) { // оператор функция
*Re = *T.Re; *Im = *T.Im; return *this;
}
Т.е. необходимо копировать не значения указателей, а значения, хранящиеся во фрагментах динамической памяти, на которые указывают эти указатели. При таком способе реализации operator=() объекты A и B останутся независимыми, что и показано в таблице, приведённой ниже
До операции присваивания | После операции присваивания |
A.Re à 0x00000001 A.Im à 0x00000001 B.Re à 0x00000002 B.Im à 0x00000002 | A.Re à 0x00000002 A.Im à 0x00000002 B.Re à 0x00000002 B.Im à 0x00000002 |
Со сгенерированным по умолчанию конструктором копии схожие проблемы. Поскольку конструктор копии вызывается неявно при передаче или возвращении из функции объекта типа comp по значению, то его неправильная реализация приводит к тому, что фактический параметр и локальный объект становятся взаимозависимыми. При выходе из функции локальные параметры уничтожаются, при этом происходит повреждение и внешних фактических параметров. При попытке обратится к фактическому параметру после того, как он был испорчен, приводит к ошибке времени исполнения программы. Поясним сказанное с помощью таблицы и фрагмента кода, приведённого ниже.
void fun(comp B){ }
comp A(1,1); // неявно вызывается конструктор
fun(A); // неявно вызывается конструктор копии и после выхода из функции деструктор
A.*Re = 3; // Ошибка времени выполнения
До вызова функции fun | Во время вызова функции fun | После выхода из функции fun |
A.Re à 0x00000001 A.Im à 0x00000001 | B.Reà 0x00000001 ßA.Re B.Imà 0x00000001 ß A.Im | A.Reàошиб. указатель A.Imàошиб. указатель |
До вызова функции fun() существует объект A, указатели которого указывают на фрагменты динамической памяти с единицами. При вызове функции fun() конструктор копии создаёт локальный объект B и делает его зависимым от объекта A. После выхода из функции fun() вызывается деструктор, который уничтожает локальный объект B, освобождая с помощью операции delete фрагменты динамической памяти, на которые указывают указатели Re и Im объекта В, но на эти же фрагменты указывают и указатели объекта A. В результате объект A становится повреждённым. Его указатели становятся не действительными, и при попытке записать какое-либо значение в память по адресу в этих указателях возникает ошибка времени выполнения.
Чтобы избежать такой ситуации программист должен самостоятельно написать конструктор копии. Для данного примера он должен выглядеть следующим образом:
comp(comp& T) {
Re = new int; Im = new int; *Re = *T.Re; *Im = *T.Im;
}
При такой реализации зависимость между фактическим А и формальным В параметрами не возникает, что и показано в таблице.
До вызова функции fun | Во время вызова функции fun | После выхода из функции fun |
A.Re à 0x00000001 A.Im à 0x00000001 | B.Re à 0x00000001 B.Im à 0x00000001 A.Re à 0x00000001 A.Im à 0x00000001 | A.Re à 0x00000001 A.Im à 0x00000001 |
У локального параметра В свои фрагменты динамической памяти, которые никак не связаны с фрагментами фактического параметра A.
В 11-ом стандарте Си++ была реализована концепция перемещения. Появилось понятие rvalue, а также конструктора перемещения и оператора перемещения. Рассмотрим пример реализации этих нововведений и сравним код с их использованием и без.
#include "stdafx.h"
#include <iostream>
struct comp {
int* Re, *Im;
comp(){
std::cout << "\n comp()";
Re = new int; Im = new int; *Re = 0; *Im = 0;
}
comp(int r, int i) {
std::cout << "\n comp(int r, int i)";
Re = new int; Im = new int;*Re = r; *Im = i;
}
~comp() {
std::cout << "\n~comp()";
delete Re; delete Im; }
comp& operator=(const comp& T) { //оператор копирования
std::cout << "\n comp& operator=(const comp& T)";
*Re = *T.Re; *Im = *T.Im;
return *this;
}
comp& operator=(comp&& T) { // оператор перемещения
std::cout << "\n comp& operator=(comp&& T)";
std::swap(Re, T.Re); std::swap(Im, T.Im);
return *this;
}
comp(comp& T){ //конструктор копирования
std::cout << "\n comp(comp& T)";
Re = new int; Im = new int; *Re = *T.Re; *Im = *T.Im;
}
comp(comp&& T) { //конструктор перемещения
std::cout << "\n comp(comp&& T)";
Re = T.Re; Im = T.Im; T.Re = nullptr; T.Im = nullptr;
}
comp operator+(const comp& T) {
std::cout << "\n comp operator+(const comp& T)";
comp S( *Re + *T.Re, *Im + *T.Im); //2
return S; //3
} // 5
void display() { std::cout << "\n Re = " << *Re << "\t Im = " << *Im<<"\n"; }
};
void main() {
comp A(1, 1), D; //1
D = A + A;
D.display(); //6
} //7
Результаты выполнения программы с конструктором и оператором перемещения | Результаты выполнения программы без конструктора и оператора перемещения | Комменарии |
comp(int r, int i) | comp(int r, int i) | В строке //1 вызывается конструктор для А |
comp() | comp() | В строке//1 вызывается конструктор по умолчанию для D |
comp operator+(const comp& T) | comp operator+(const comp& T) | В строке C = A + A; Вызывается operator+() |
comp(int r, int i) | comp(int r, int i) | В строке //2 в теле функции operator+() создаётся локальный объект S |
comp(comp&& T) | comp(comp& T) | В строке //3 если в программе нет конструктора перемещения, то вызывается конструктор копии. У них одна задача: в точке вызова (на месте А+А) создать неименованный временный (rvalue) объект, но реализуют они это по-разному. Конструктор копии копирует, а конструктор переноса переносит во временный (rvalue) объект содержимое S |
~comp() | ~comp() | В строке //5 вызывается деструктор, чтобы уничтожить локальный объект S. |
comp& operator=(comp&& T) | comp& operator=(const comp& T) | Далее в строке C = A + A; если в программе нет оператора переноса, то вызывается оператор копирования. У них одна задача: сохранить в D полученный результат, но делают они это по-разному. Оператор копирования копирует в D содержимое временного (rvalue) объекта, а оператор переноса переносит данные из него в D. |
~comp() | ~comp() | Удаление временного (rvalue) объекта. |
Re = 2 Im = 2 | Re = 2 Im = 2 | В строке //6 выводим на экран полученный результат. |
~comp() | ~comp() | В строке //7 уничтожаем объект A |
~comp() | ~comp() | В строке //7 уничтожаем объект D |
Рассмотрим ещё раз поподробнее реализацию конструктора копии и конструктора переноса:
comp(comp& T){ //конструктор копирования
std::cout << "\n comp(comp& T)";
Re = new int; Im = new int; //выделение памяти для нового объекта
*Re = *T.Re; *Im = *T.Im; //копирование данных из старого объекта в новый
}
comp(comp&& T) { //конструктор перемещения
std::cout << "\n comp(comp&& T)";
Re = T.Re; Im = T.Im; //перемещаем ресурсы в новый объект
T.Re = nullptr; T.Im = nullptr; //делаем нулевым старый объект
}
В конструкторе копии с помощью new выделяется память и в эту память копируется данные из старого объекта во вновь создаваемый объект. В общем случае это дорогие операции, особенно если объект большой и данных много.
В конструкторе перемещения память не выделяется и данные не копируются. В нём мы отбираем все ресурсы (выделенную память и данные, которые в ней находятся) у старого объекта и перемещаем их в новый, а старый объект делаем нулевым, т.е. таким, чтобы вызов деструктора для старого объекта не уничтожил ресурсы, которые мы уже передали новому объекту.
Аналогичная ситуация и с оператором перемещения. Рассмотрим поподробнее реализации оператора копирования и оператора перемещения.
comp& operator=(const comp& T) { //оператор копирования
std::cout << "\n comp& operator=(const comp& T)";
*Re = *T.Re; *Im = *T.Im; //копируем данные в D
return *this;
}
comp& operator=(comp&& T) { // оператор перемещения
std::cout << "\n comp& operator=(comp&& T)";
std::swap(Re, T.Re); std::swap(Im, T.Im); //обмениваемся ресурсами
return *this;
}
В операторе копирования данные копируются в D. В данном примере объект D содержит мало данных, но в общем случае это может быть не так.
В операторе перемещения происходит обмен ресурсами (в данном случае обмен значениями указателей). После чего вызывается деструктор, который уничтожает временный (rvalue) объект и соответственно освобождает память, которая ранее была выделена для D, но после обмена ресурсами начала принадлежать временному (rvalue) объекту.
Таким образом, с помощью введения понятия rvalue (объекта типа type&&, т.е. временного объекта) мы можем реализовывать функции с формальными параметрами типа type&&, которые будут автоматически вызываться компилятором для работы с такими объектами. При правильной реализации таких функций мы можем избавиться от лишних операций выделения памяти и копирования.
Дата добавления: 2020-12-11; просмотров: 402;