C ++ Destructor вызвал не тот объект? - PullRequest
0 голосов
/ 16 января 2019

Я новичок в C ++, и я написал небольшую программу, чтобы узнать, как присваивание работает с объектами. Мне было предложено сделать это из документации cpp на этой странице (http://www.cplusplus.com/doc/tutorial/classes2/). На этой странице указано:

Неявная версия [оператора назначения копирования] выполняет поверхностное копирование, которое подходит для многих классов, но не для классов с указателями на объекты, которые они обрабатывают в своем хранилище. В этом случае не только класс подвергается риску удаления указанного объекта дважды, но назначение создает утечки памяти, не удаляя объект, указанный объектом до назначения .

Последняя часть, которую я выделил жирным шрифтом, - это то, почему я решил проверить вещи. Я думал, что эта проблема может быть решена путем обработки удаления указанных объектов в деструкторе (что является стандартным?), Вместо того, чтобы перегружать оператор присваивания копии. Если деструктор не вызывается, разве это не неудобно? Скажем, у меня было несколько ссылочных объектов, я должен был бы поместить все удаления как в деструктор (для большинства случаев перераспределения), так и в перегрузку присваивания.

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

Вот мой код:

class Test{
public:
    int id;
    explicit Test(int id) : id(id) {
        cout << "Created " << id << endl;
    }
    ~Test() {
        cout << "Destroyed " << id << endl;
    }
};

int main() {
    Test x = Test(1);
    x = Test(2);

    cout << x.id << endl;
    return 0;
}

Результат, который я ожидал, должен был быть:

1: Created 1
2: Destroyed 1? (это был тот, в котором я не был уверен, так как веб-сайт намекал, что этот деструктор не будет вызываться, если объект будет «заменен» другим, а не выходит из области видимости).
3: Created 2 Объект 2 «заменяет» объект 1, поскольку он назначен для x
4: 2 значение идентификатора объекта 2 распечатано
5: Destroyed 2 Объект 2 уничтожен, когда он выходит из области видимости

Вместо этого я получил следующий вывод:

Created 1
Created 2
Destroyed 2
2
Destroyed 2

Это действительно не имеет смысла для меня.

Используя отладчик, Created 2 и Destroyed 2 оба отображаются, когда вызывается строка x = Test(2);. Если мы просто присвоили x объекту 2, почему его деструктор вызывается немедленно? Это следует к следующей части.

Во-вторых, поскольку был вызван деструктор для объекта 2, мы можем предположить, что он был уничтожен. Следующий вывод 2, кажется, противоречит этому, поскольку предполагает, что x все еще содержит Объект 2 (ожидаемый, но противоречащий вызову его деструктора).

Я не совсем уверен, почему это происходит.

Наконец, Destroyed 2 выводится. Это имело бы смысл, если бы мы не видели это раньше. Объект 2 хранится в x, поэтому, когда он выходит из области видимости, вызывается деструктор.

По какой-то причине мы получаем деструктор, вызываемый дважды, и Объект 1, который «переопределяется» назначением Объекта 2 на x, никогда не вызывает своего деструктора, но вместо этого деструктор объекта, который мы только что создали, имеет свой вызван деструктор.

Итак ... это сводится к вопросу из двух частей:

1: Почему происходит это странное поведение, и есть ли логическая причина, почему это так?
2: приводит ли «перезапись» объекта (например, объекта 1) к другому объекту (объекту 2) посредством присваивания к его деструктору (в данном случае деструктору объекта 1) для вызова или нет?

Заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 16 января 2019
Test x = Test(1);

Создает новый объект со значением «1».

x = Test(2);

Сначала создается новый объект со значением «2», после чего ему будет присвоен к первому объекту с оператором присваивания, который неявно создан для вашего класса! В этот момент у вас есть два объекта, каждый из которых имеет значение 2!

Чтобы получить лучшее представление, вы можете сделать это:

class Test{
    public:
        static int instanceCount;
        int id;
        int count;

        explicit Test(int id) : id{id}, count{instanceCount++} {
            std::cout << "Created " << id << " " << count << std::endl;
        }

        ~Test() {
            std::cout << "Destroyed " << id << " " << count << std::endl;
        }

        //Test& operator=(const Test&) = delete;
        Test& operator=(const Test& ex) 
        {
            id=ex.id;
            return *this;
        }
};  


int Test::instanceCount = 0;

int main() {
    Test x = Test{1};
    x = Test{2};

    std::cout << x.id << std::endl; 
    return 0;
}  

Теперь вы можете видеть, когда создается новый экземпляр. Если вы удалите оператор присваивания для своего класса, вы увидите, что первая написанная вами инструкция «Test x = Test {1};» это не задание, а конструкция. Второй "x = Test {2};" потерпит неудачу, так как вы удалили оператора сейчас.

Вывод выглядит следующим образом:

Created 1 0
Created 2 1
Destroyed 2 1
2
Destroyed 2 0

Как видите, вы получаете первый экземпляр с номером 0 и вашим значением 1. Второй временный экземпляр создается как счетчик 1 с вашим значением 2. Затем этот будет назначен первому, а временный экземпляр будет удален до того, как произойдет ваш std :: cout! В тот момент, когда вы покинули область действия основной функции, первый экземпляр будет удален!

Что вы можете выучить:

  • создание объекта с X x=X(3); аналогично написанию X x(3);
  • если вы не написали вручную оператор присваивания, вы можете получить оператор по умолчанию, в зависимости от некоторых других правил (для более широкого здесь).
  • вы должны увидеть, что вы создаете здесь временные объекты, которые будут создавать и удалять «на лету», но при этом иметь цену, которой в большинстве случаев можно избежать!
  • вы должны не использовать using namespace std!
  • вы должны написать X x{3} instead of X x (3) `
  • запись X x=X(3); полностью сбивает с толку, поскольку похоже, что вы создаете временный объект, а затем назначаете его для созданного по умолчанию. Но этого не произойдет, и вместо этого вы должны писать свой код проще!
0 голосов
/ 16 января 2019

Используя отладчик, Created 2 и Destroyed 2 оба отображаются, когда строка x = Test (2); называется. Если мы просто присвоили x объекту 2, почему его деструктор вызывается немедленно? Это следует к следующей части.

Строка x = Test(2); сначала создает Test с аргументом конструктора 2. Это то, что производит Created 2. Этот безымянный Test затем присваивается x, что дает x.id значение 2. Этот безымянный Test затем уничтожается в конце выражения, создавая "Уничтожено 2".

Во-вторых, поскольку был вызван деструктор для объекта 2, мы можем предположить, что он был уничтожен. Следующий вывод 2, кажется, противоречит этому, поскольку предполагает, что x все еще содержит объект 2 (ожидаемый, но противоречащий вызову его деструктора).

Как указано в первой части этого ответа, уничтожается не x, а временный Temp. x.id все еще действует и выдаст новое значение, 2.

Наконец выводится Destroyed 2. Это имело бы смысл, если бы мы не видели это раньше. Объект 2 хранится в x, поэтому, когда он выходит из области видимости, вызывается деструктор.

Это происходит, когда x уничтожается в конце функции. Это значение id было изменено на 2 предыдущим присваиванием, поэтому оно выдает «Destroyed 2».

1: Почему происходит это странное поведение, и есть ли логическая причина, почему это так?

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

2: приводит ли «переопределение» объекта (например, объекта 1) к другому объекту (объекту 2) посредством присваивания к его деструктору (в данном случае деструктору объекта 1) для вызова или нет?

Назначение объекта не разрушает его. Он заменяет его значение новым и в этом смысле «уничтожает» значение , которое ранее помогло, но фактический экземпляр объекта не уничтожен, а деструктор не задействован.

Редактировать: Возможно, вас беспокоит утечка ресурсов. Поскольку Test не управляет никакими ресурсами, утечек не будет, и сгенерированные компилятором члены будут работать нормально. Если ваш класс управляет ресурсами (обычно в форме динамически выделяемой памяти), вам нужно будет применить правило 3/5/0 . В частности, вам нужно будет самостоятельно реализовать оператор присваивания, чтобы он очищал любые ранее удерживаемые ресурсы. Недостаточно реализовать только деструктор, поскольку он не участвует в назначении.

...