Полезно ли указывать новый адрес в свободном хранилище при динамическом размещении?

Ниже приведено упражнение от C ++ Primer 5-е издание:

Упражнение 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 указать на новый адрес. Мне интересно, имеет ли эта тонкая разница какое-либо существенное значение? Должен ли я всегда менять указатель на новый адрес в подобных ситуациях? Зачем?

1

Решение

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

delete ps;
ps = new std::string(*hp.ps);

ps будет указывать на устаревший член, когда вторая строка кода вызывает исключение. Кстати, если вы в конечном итоге самостоятельно назначаете объект, вы на самом деле delete память только до доступа к ней. Таким образом, хорошей идеей будет сначала скопировать содержимое правой части, затем поместить все на место и, наконец, освободить ресурс.

Как это происходит, это именно операции

  1. конструктор копирования
  2. swap() операция, которую вы обычно хотите для любого типа удерживающих ресурсов
  3. деструктор

Способ использовать эти три операции известен как копировать и менять местами идиома:

T& T::operator=(T other) {
this->swap(other);
return *this;
}
2

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

Ваша версия небезопасна для самостоятельного назначения.

delete ps;
ps = new std::string(*hp.ps);

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

Вы можете присвоить значение нового непосредственно в вашей переменной-члене, но вы не можете просто произвольно удалить ps, прежде чем знать, если вам это нужно.

Если вы тестировали для самостоятельного задания, например.if (this!=&hp) перед выполнением вашего кода, было бы лучше, но все же не идеально (см. комментарии по безопасности исключений в другом месте).

2

Функционально я вижу только одно отличие.

    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 по-прежнему указывает на допустимую строку, поэтому у вас нет уродливого объекта.

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

1

На самом деле в вашем коде есть две проблемы:

  • 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;
}
1