Полезно ли указывать новый адрес в свободном хранилище при динамическом размещении?
Ниже приведено упражнение из C++ Primer 5th Edition:
Упражнение 13.22. Предположим, что мы хотим, чтобы HasPtr вел себя как значение. То есть каждый объект должен иметь свою собственную копию строки, на которую указывают объекты. Мы покажем определения членов управления копированием в следующем разделе. Однако вы уже знаете все, что вам нужно знать для реализации этих участников. Напишите конструктор копирования HasPtr и оператор копирования-присвоения, прежде чем читать дальше (стр. 511).
Коды для класса HasPtr
:
class HasPtr
{
public:
//! default constructor
HasPtr(const std::string &s = std::string()):
ps(new std::string(s)), i(0) { }
//! copy constructor
HasPtr(const HasPtr& hp) : ps(new std::string(*hp.ps)), i(hp.i) { }
HasPtr&
operator = (const HasPtr& hp);
~HasPtr()
{
delete ps;
}
private:
std::string *ps;
int i;
};
Мой код для этого оператора копирования-назначения:
HasPtr&
HasPtr::operator = (const HasPtr &hp)
{
delete ps;
ps = new std::string(*hp.ps);
i = hp.i;
return *this;
}
Коды представлены в следующем разделе из этой книги:
HasPtr&
HasPtr::operator = (const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); // copy the underlying string
delete ps; // free the old memory
ps = newp; // copy data from rhs into this object
i = rhs.i;
return *this; // return this object
}
Выполняя шаг за шагом, я обнаружил небольшую разницу между двумя кодами. В моем коде это не меняет адрес, на который ps
баллы, тогда как код из книги делает ps
указать на новый адрес. Мне интересно, имеет ли эта тонкая разница какое-либо существенное значение? Должен ли я всегда менять указатель на новый адрес в подобных ситуациях? Зачем?
4 ответа
В вашем коде есть проблема с самостоятельным назначением и с исключениями: предположим, что распределение памяти вызывает std::bad_alloc
исключение. При кодировании вы всегда должны предполагать, что распределение памяти может идти не так, хотя на самом деле это происходит редко. В коде
delete ps;
ps = new std::string(*hp.ps);
ps
будет указывать на устаревший член, когда вторая строка кода вызывает исключение. Кстати, если вы в конечном итоге самостоятельно назначаете объект, вы на самом деле delete
память только до доступа к нему. Таким образом, хорошей идеей будет сначала скопировать содержимое с правой стороны, затем поместить все на место и, наконец, освободить ресурс.
Как это происходит, это именно операции
- конструктор копирования
swap()
операция, которую вы обычно хотите для любого типа удерживающих ресурсов- деструктор
Способ использовать эти три операции называется идиома копирования и обмена:
T& T::operator=(T other) {
this->swap(other);
return *this;
}
Ваша версия небезопасна для самостоятельного назначения.
delete ps;
ps = new std::string(*hp.ps);
Здесь, если вы выполняете самостоятельное назначение, вы можете удалять ps как в источнике, так и в месте назначения, что делает его использование в новом операторе неправильным (хотя в большинстве случаев это может быть обманчиво).
Вы можете присвоить значение нового непосредственно в вашей переменной-члене, но вы не можете просто произвольно удалить ps
, прежде чем знать, если вам это нужно.
Если вы тестировали для самостоятельного задания, например.if (this!=&hp)
перед выполнением вашего кода, было бы лучше, но все же не идеально (см. комментарии по безопасности исключений в другом месте).
На самом деле в вашем коде есть две проблемы:
- Self-присваивание
- Исключительная безопасность
Обычно вы хотите, чтобы ваши функции-члены обеспечивали строгую гарантию исключений, т. Е. В случае неудачного назначения (в вашем случае это может быть operator new
или конструктор копирования string
), состояние программы не изменяется.
Я думаю, что современная практика заключается в обеспечении swap
Функция и сделать присваивание вызовите конструктор копирования. Что-то вроде:
void HasPtr::swap(HasPtr& rhs)
{
std::swap(this.ps, rhs.ps);
std::swap(this.i, rhs.i);
}
HasPtr(const HasPtr& rhs)
{
ps = new string(*rhs.ps);
i = rhs.i;
}
HasPtr& operator=(const HasPtr& rhs)
{
HasPtr temp(rhs);
this.swap(temp);
return *this;
}
Функционально я вижу только одно отличие.
delete ps;
ps = new std::string(*hp.ps);
Если памяти мало, вызов new std::string
возможно, сгенерирует исключение. В твоем случае, ps
по-прежнему имеет адрес старой удаленной строки - поэтому он искажен. Если вы оправитесь от исключения, кто-то может разыменовать ps
и плохие вещи случатся.
auto newp = new string(*rhs.ps); // copy the underlying string
delete ps; // free the old memory
ps = newp; // copy data from rhs into this object
В коде учебника, ps
не удаляется до тех пор, пока не будет выделена новая строка. На исключение, ps
по-прежнему указывает на допустимую строку, поэтому у вас нет уродливого объекта.
Как много проблемы зависит от нескольких разных вещей, но, как правило, лучше избегать любых шансов на неправильное образование объекта.