【问题标题】:C++ Destructor is not called未调用 C++ 析构函数
【发布时间】:2017-04-28 11:24:27
【问题描述】:

我有三个类:房间、门和世界

#include <set>;

using namespace std;

class Door; // Forward declaration

class Room {
public:
    Door* door1;
    Door* door2;

    Room(){}

    ~Room() {
        delete door1;
        door1 = 0;
        delete door2;
        door2 = 0;
    }
};

class Door {
public:
    Room* roomA;
    Room* roomB;

    Door(Room* roomA, Room* roomB) {
        this->roomA = roomA;
        this->roomB = roomB;
        linkRooms(); // This sets up the Door-Pointers in the Rooms
                     // so they know about the new door.
    }    

    ~Door() {
        // Returns the room-owned pointer pointing at this door
        getMyRoomPointer(roomA) = 0;
        getMyRoomPointer(roomB) = 0;
    }    

    Door * & getMyRoomPointer(Room * const & room) {
        if (room->door1 == this) return room->door1;
        else return room->door2;
    }

    void linkRooms() {
        roomA->door1 = this;
        roomB->door2 = this;
    }

};

class World {
public:
    std::set<Room*> rooms;

    World() {
        // Set up two rooms and link them using a door
        Room* newRoom = new Room();
        rooms.insert(newRoom);
        Room* anotherNewRoom = new Room();
        rooms.insert(anotherNewRoom);

        new Door(newRoom, anotherNewRoom);
    }
    ~World() {
        // Iterate over the rooms and call delete on all of them
        for (std::set<Room*>::iterator it = rooms.begin(); it != rooms.end(); ++it) {
            delete *it;
        }
    }
};

int main() {

    World world;

    return 0;
}

当运行 main 时,构造函数只用两个房间和一扇门作为它们之间的链接来填充世界。主要返回后,世界应该被删除,所有的房间和门也应该被照顾。

问题是,我的 Door 析构函数永远不会被调用。所以房间内的门指针没有设置为空,当房间“在另一边”试图删除同一个门时我得到一个错误。

当我只创建一个 Door 实例,然后立即删除它时,我没有遇到任何问题:

int main(){

    Room oneRoom;
    Room anotherRoom;

    Door* door = new Door(&oneRoom, &anotherRoom);
    delete door; // works just fine

    return 0;
}

问题:为什么不调用 Door 构造函数?是否允许像第一个示例中那样设置门?

我知道,我要双重删除房间的 Door 指针,并且我可以(并且应该)使用 SmartPointers。现在我只是想知道为什么我会面临这种行为。毕竟我还是 C++ 新手。

我现在确实设置了一个可运行的示例,它重现了错误。

【问题讨论】:

  • 您可能应该附上 应该 删除 Rooms 的代码 - 否则我们不知道要诊断什么;)顺便说一句,可能每个 Door 将是删除了两次,因为每个Room 都有自己的指针
  • minimal reproducible example,拜托。例如,如果不知道getMyRoomPointer 是如何工作的,我们几乎无法对此进行分析。
  • 你可能应该使用智能指针而不是原始指针。
  • AFAICT,这应该可以工作,除非您从“世界”类发布的代码都在构造函数中,在这种情况下,您的 rooms 声明隐藏了成员变量。
  • 这不是问题,但析构函数中的door1= 0; 毫无意义。该对象正在消失,没有人会再看到door1 的实例。

标签: c++ destructor dynamic-allocation


【解决方案1】:

在定义Door 之前调用delete。因此程序的行为是未定义的,也不保证会调用析构函数。

引用标准(草案)[expr.delete]

  1. 如果要删除的对象在删除时具有不完整的类类型,并且完整的类具有重要的析构函数或释放函数,则行为未定义。

解决方案:如果类型的析构函数是非平凡的(即用户定义的,例如~Door,则在类型完成之前永远不要删除此类对象)。在这种情况下,请在调用 delete 的函数之前定义 Door

作为一般规则,除非类类型完整,否则永远不能调用成员函数。不幸的是,在析构函数的情况下,编译器可能并不总是能够捕捉到错误。 PS。 g++ 确实会警告您的程序:warning: possible problem detected in invocation of delete operator: [-Wdelete-incomplete]

【讨论】:

  • 谢谢你的回答,它确实解决了我的问题!只是为了确保我正确理解了这一点:在编写任何“删除 X”之前,我必须确保直到那时我已经写下/包含了定义 X 的所有内容,所以所有字段、方法、构造函数、依赖项/标题,等等?
  • @MaXL130 删除前类的定义就足够了。
  • 那么基本上包含一个在另一个类的析构函数中使用的类的头文件是强制性的?
  • @MaXL130 总之,是的。
【解决方案2】:

您应该注意编译器警告消息:

g++ -std=c++17 -fPIC -g -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Weffc++      41120443.cpp    -o 41120443
41120443.cpp:1:15: warning: extra tokens at end of #include directive
 #include <set>;
               ^
41120443.cpp:7:7: warning: ‘class Room’ has pointer data members [-Weffc++]
 class Room {
       ^~~~
41120443.cpp:7:7: warning:   but does not override ‘Room(const Room&)’ [-Weffc++]
41120443.cpp:7:7: warning:   or ‘operator=(const Room&)’ [-Weffc++]
41120443.cpp: In constructor ‘Room::Room()’:
41120443.cpp:12:5: warning: ‘Room::door1’ should be initialized in the member initialization list [-Weffc++]
     Room(){}
     ^~~~
41120443.cpp:12:5: warning: ‘Room::door2’ should be initialized in the member initialization list [-Weffc++]
41120443.cpp: In destructor ‘Room::~Room()’:
41120443.cpp:15:16: warning: possible problem detected in invocation of delete operator: [-Wdelete-incomplete]
         delete door1;
                ^~~~~
41120443.cpp:15:16: warning: invalid use of incomplete type ‘class Door’
41120443.cpp:5:7: note: forward declaration of ‘class Door’
 class Door; // Forward declaration
       ^~~~
41120443.cpp:15:16: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
         delete door1;
                ^~~~~
41120443.cpp:17:16: warning: possible problem detected in invocation of delete operator: [-Wdelete-incomplete]
         delete door2;
                ^~~~~
41120443.cpp:17:16: warning: invalid use of incomplete type ‘class Door’
41120443.cpp:5:7: note: forward declaration of ‘class Door’
 class Door; // Forward declaration
       ^~~~
41120443.cpp:17:16: note: neither the destructor nor the class-specific operator delete will be called, even if they are declared when the class is defined
         delete door2;
                ^~~~~
41120443.cpp: At global scope:
41120443.cpp:22:7: warning: ‘class Door’ has pointer data members [-Weffc++]
 class Door {
       ^~~~
41120443.cpp:22:7: warning:   but does not override ‘Door(const Door&)’ [-Weffc++]
41120443.cpp:22:7: warning:   or ‘operator=(const Door&)’ [-Weffc++]
41120443.cpp: In constructor ‘Door::Door(Room*, Room*)’:
41120443.cpp:27:5: warning: ‘Door::roomA’ should be initialized in the member initialization list [-Weffc++]
     Door(Room* roomA, Room* roomB) {
     ^~~~
41120443.cpp:27:5: warning: ‘Door::roomB’ should be initialized in the member initialization list [-Weffc++]
41120443.cpp: In constructor ‘World::World()’:
41120443.cpp:56:5: warning: ‘World::rooms’ should be initialized in the member initialization list [-Weffc++]
     World() {
     ^~~~~

和 Valgrind 输出:

valgrind  ./41120443 
==2864== Memcheck, a memory error detector
==2864== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==2864== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==2864== Command: ./41120443
==2864== 
==2864== Conditional jump or move depends on uninitialised value(s)
==2864==    at 0x4C2C291: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2864==    by 0x108BC3: Room::~Room() (41120443.cpp:17)
==2864==    by 0x108D67: World::~World() (41120443.cpp:68)
==2864==    by 0x108B65: main (41120443.cpp:75)
==2864== 
==2864== Conditional jump or move depends on uninitialised value(s)
==2864==    at 0x4C2C291: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2864==    by 0x108BA8: Room::~Room() (41120443.cpp:15)
==2864==    by 0x108D67: World::~World() (41120443.cpp:68)
==2864==    by 0x108B65: main (41120443.cpp:75)
==2864== 
==2864== Invalid free() / delete / delete[] / realloc()
==2864==    at 0x4C2C2DB: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2864==    by 0x108BC3: Room::~Room() (41120443.cpp:17)
==2864==    by 0x108D67: World::~World() (41120443.cpp:68)
==2864==    by 0x108B65: main (41120443.cpp:75)
==2864==  Address 0x5a82e00 is 0 bytes inside a block of size 16 free'd
==2864==    at 0x4C2C2DB: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2864==    by 0x108BA8: Room::~Room() (41120443.cpp:15)
==2864==    by 0x108D67: World::~World() (41120443.cpp:68)
==2864==    by 0x108B65: main (41120443.cpp:75)
==2864==  Block was alloc'd at
==2864==    at 0x4C2B21F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==2864==    by 0x108CCE: World::World() (41120443.cpp:63)
==2864==    by 0x108B54: main (41120443.cpp:75)

明智地使用智能指针可以让自己的生活更轻松:

#include <memory>
#include <set>

class Door; // Forward declaration

struct Room {
    std::shared_ptr<Door> door1 = {};
    std::shared_ptr<Door> door2 = {};

    ~Room();
};

struct Door {
    const std::weak_ptr<Room> roomA;
    const std::weak_ptr<Room> roomB;

    Door(std::shared_ptr<Room> roomA, std::shared_ptr<Room> roomB)
        : roomA(roomA),
          roomB(roomB)
    {
        roomA->door1 = roomB->door1 = std::shared_ptr<Door>{this};
    }

    ~Door() = default;
};

// Now that Door is complete, we can define ~Room
Room::~Room() = default;

struct World {
    std::set<std::shared_ptr<Room>> rooms = {};

    World() {
        // Set up two rooms and link them using a door
        auto newRoom = std::make_shared<Room>();
        rooms.insert(newRoom);
        auto anotherNewRoom = std::make_shared<Room>();
        rooms.insert(anotherNewRoom);

        new Door(newRoom, anotherNewRoom);
    }

    ~World() = default;
};

int main() {

    World world;

    return 0;
}

这可以干净地构建和运行:

g++ -std=c++17 -fPIC -g -Wall -Wextra -Wwrite-strings -Wno-parentheses -Wpedantic -Warray-bounds -Weffc++      41120443.cpp    -o 41120443
valgrind  ./41120443 
==3254== Memcheck, a memory error detector
==3254== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==3254== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==3254== Command: ./41120443
==3254== 
==3254== 
==3254== HEAP SUMMARY:
==3254==     in use at exit: 0 bytes in 0 blocks
==3254==   total heap usage: 7 allocs, 7 frees, 72,952 bytes allocated
==3254== 
==3254== All heap blocks were freed -- no leaks are possible
==3254== 
==3254== For counts of detected and suppressed errors, rerun with: -v
==3254== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

【讨论】:

  • 感谢您的帮助,智能指针示例对我有很大帮助!我实际上没有收到任何上面的编译器消息,我一定会检查 Valgrind。
  • 如果您没有收到编译错误/警告,这可能表明您没有添加足够的-W 选项 - 所以我通常设置的第一行。 -Wall-Wextra 应该是强制性的!
【解决方案3】:

你永远不会删除门

new Door(newRoom, anotherNewRoom);

你应该在世界中添加Door *door

public:
  Door *door;

...

  door = new Door(newRoom, anotherNewRoom);

并在世界的析构函数中调用delete:

delete door;

请注意,您的代码有很多未定义的行为。


std::set<Room *> rooms;

std::set 是一个关联容器,其中包含一组有序的 Key 类型的唯一对象。

你应该使用列表:

std::list<Room *> rooms;

【讨论】:

  • 它可以包含指针作为键就好了。我同意使用另一个容器可能更有意义,但这并不能回答问题
  • @StoryTeller 我的错,这个问题让我很困惑。我添加答案。
猜你喜欢
  • 2014-05-03
  • 2019-11-09
  • 2023-04-08
  • 2013-07-07
  • 2013-09-19
  • 1970-01-01
  • 1970-01-01
  • 2021-07-19
  • 2013-09-06
相关资源
最近更新 更多