Необычное поведение деструктора при копировании через переменные стека - PullRequest
2 голосов
/ 07 октября 2009

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

Это мой тест (в режиме выпуска Visual C ++ 2008):

#include <iostream>
class C {
public:
 char* ptr;
 C(char p) { ptr = new char[100]; ptr[0] = p;}
 ~C() { std::cout << ptr[0] << ' '; delete [] ptr; }
};

int _tmain(int argc, _TCHAR* argv[])
{
 {
  C s('a');
  s = C('b');
  s = C('c');
  s = C('d');
 }
 std::cin.get();
 return 0;
}

Я ожидал получить либо «a b c d», если моя гипотеза была верна, либо просто «d», если ложь. Вместо этого я получаю «b c d x». «X» изменяется в зависимости от того, сколько памяти выделено для ptr, что указывает на то, что он читает случайные значения кучи.

Я полагаю, что происходит (поправьте меня, если я ошибаюсь), что каждый вызов конструктора создает новое значение стека (давайте назовем их s1, s2, s3, s4), а затем присваивания оставляют s1.ptr перезаписанным s4.ptr. Затем s4 уничтожается сразу после копирования, но s1 (с висящим ptr) уничтожается при выходе из области действия, вызывая двойное удаление s4.ptr и не удаляя исходный s1.ptr.

Есть ли способ обойти это бесполезное поведение, которое не связано с использованием shared_ptrs?

изменить: заменить «удалить» на «удалить []»

Ответы [ 7 ]

11 голосов
/ 07 октября 2009

Правило трех

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

Правило трех состояний гласит, что каждый раз, когда вы определяете одно из:

  • Копировать конструктор
  • оператор присваивания
  • деструктор

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

РЕДАКТИРОВАТЬ специальное исключение :
иногда вы определяете деструктор только потому, что хотите, чтобы он был виртуальным, или потому что он что-то регистрирует, а не потому, что есть какая-то особая обработка ваших атрибутов;)

3 голосов
/ 07 октября 2009

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

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

следующее использование

delete [] ptr; 

вместо удаления

2 голосов
/ 07 октября 2009

Добавьте другие методы, определенные компилятором:

class C
{
    public:
      char* ptr;
      C(char p)                { ptr = new char[100]; ptr[0] = p;}
     ~C()                      { std::cout << ptr[0] << ' '; delete [] ptr; }
      C(C const& c)            { ptr = new char[100]; ptr[0] = c.ptr[0];}
      C& operator=(C const& c) { ptr[0] = c.ptr[0]; return *this;}
};

int _tmain(int argc, _TCHAR* argv[])
{
  {
      C s('a');
      s = C('b');
      s = C('c');
      s = C('d');
  }
  std::cin.get();
  return 0;
}

Теперь он должен распечатать:

b c d d

Каждый временный объект уничтожается в конце выражения. Затем s уничтожается последним (после копирования 'd' в ptr [0]). Если вы добавите оператор print в каждый метод, вам будет легче увидеть, что происходит:

>>           C s('a');
Construct 'a'

>>           s = C('b');
Construct 'b'  
Assign 'b' onto 'a'  
Destroy 'b'         (Temporary destroyed at ';')  

>>          s = C('c');
Construct 'c'  
Assign 'c' onto 'b' (was 'a' but has already been assigned over)  
Destroy 'c'         (Temporary destroyed at ';')

>>          s = C('d');  
Construct 'd'  
Assign 'd' onto 'c'  
Destroy 'd'         (Temporary destroyed at ';')  

>> End of scope.
Destroy 'd'         (Object s destroyed at '}')  

Поскольку компилятором определены 4 метода, применяется «правило четырех».
Если ваш класс содержит указатель RAW, который принадлежит классу (ведомый означает, что ваш объект определяет продолжительность жизни). Затем вы должны переопределить все 4 метода, сгенерированных компилятором.

Поскольку вы создаете и уничтожаете член 'ptr', это принадлежащий ему ptr. Таким образом, должны быть определены все четыре метода.

1 голос
/ 07 октября 2009

Проблема в том, что вам нужны конструкторы копирования и операторы присваивания. Из-за того, что вы назначаете один класс другому, создается мелкая копия. Это приведет к тому, что оба класса будут иметь один и тот же указатель ptr. Если затем один из них удаляется, другой указывает на вершину уже освобожденной памяти

1 голос
/ 07 октября 2009

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

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

1 голос
/ 07 октября 2009

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

0 голосов
/ 07 октября 2009

Вы не определили оператор присваивания или копирования. Так что происходит, что-то вроде:

C s('a');

Экземпляр 's' создается и инициализируется с помощью 'a'.

s = C('b');

Это создает временный объект, инициализирует его с помощью 'b', а затем запускает оператор присваивания по умолчанию, который выполняет побитовое копирование всех переменных, перезаписывая ptr of s. Временный объект уничтожен. Излучение 'b' и удаление 'ptr' (перевод ptr в s недействительным).

s = C('c');
s = C('d');

То же самое снова. Временный объект создается, инициализируется с помощью «c», а «ptr» в s перезаписывается ptr, выделенным во временном объекте. Временный объект уничтожается, испуская 'c' и оставляя ptr в s недействительным. Повторите для д.

  return 0;
}

Наконец s выходит из области видимости, его деструктор пытается выдать первый символ ptr, но это мусор, потому что ptr был выделен и удален последним ('d') временным объектом. Попытка удалить ptr должна завершиться неудачей, так как эта память уже удалена.

Чтобы решить это? Определите явные конструкторы копирования и операторы присваивания.

class C {
  // other stuff
  C(const C&rhs); // copy constructor
  C& operator=(const c& rhs){ // assignment operator
    a[0] = rhs.a[0];
    return *this;
  }
};
...