【问题标题】:Good or bad: Calling destructor in constructor [closed]好或坏:在构造函数中调用析构函数[关闭]
【发布时间】:2018-12-09 15:28:34
【问题描述】:

Break:我认为这实际上不是同一个问题,另一个问题是关于手动调用析构函数的一般问题。这是在类本身内部的创建过程中。仍然想知道当你这样做时会发生什么,如下面的问题所述。


起初,我认为这很糟糕,真的很糟糕。只需分析这段由两个人制作的构造函数代码(见下文),需要将其转换为 Delphi 对象 Pascal。它的行为必须与 C 版本相同。我不喜欢这种风格,很丑但没关系。

另一件事,在代码的两个阶段,它在失败时调用析构函数(我想关闭连接,但是在删除时会自动调用析构函数,为什么还要这样做?)。我认为这不是这样做的方法,还是确实错过了一些东西?

另外,在调用析构函数后,他们想抛出一个异常(嗯?)但是我认为这永远不会被执行,并且当您手动想要访问它或想要删除它时会导致另一个异常。


Serial::Serial(
  std::string &commPortName,
  int bitRate,
  bool testOnStartup,
  bool cycleDtrOnStartup
) {
  std::wstring com_name_ws = s2ws(commPortName);

  commHandle =
    CreateFileW(
      com_name_ws.c_str(),
      GENERIC_READ | GENERIC_WRITE,
      0,
      NULL,
      OPEN_EXISTING,
      0,
      NULL
    );

  if(commHandle == INVALID_HANDLE_VALUE)
    throw("ERROR: Could not open com port");
  else {
    // set timeouts
    COMMTIMEOUTS timeouts;

    /* Blocking:
        timeouts.ReadIntervalTimeout = MAXDWORD;
        timeouts.ReadTotalTimeoutConstant = 0;
        timeouts.ReadTotalTimeoutMultiplier = 0;
       Non-blocking:
        timeouts = { MAXDWORD, 0, 0, 0, 0}; */

    // Non-blocking with short timeouts
    timeouts.ReadIntervalTimeout = 1;
    timeouts.ReadTotalTimeoutMultiplier = 1;
    timeouts.ReadTotalTimeoutConstant = 1;
    timeouts.WriteTotalTimeoutMultiplier = 1;
    timeouts.WriteTotalTimeoutConstant = 1;

    DCB dcb;
    if(!SetCommTimeouts(commHandle, &timeouts)) {
      Serial::~Serial();                                      <- Calls destructor!
      throw("ERROR: Could not set com port time-outs");
    }

    // set DCB; disabling harware flow control; setting 1N8 mode
    memset(&dcb, 0, sizeof(dcb));
    dcb.DCBlength = sizeof(dcb);
    dcb.BaudRate = bitRate;
    dcb.fBinary = 1;
    dcb.fDtrControl = DTR_CONTROL_DISABLE;
    dcb.fRtsControl = RTS_CONTROL_DISABLE;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    dcb.ByteSize = 8;

    if(!SetCommState(commHandle, &dcb)) {
      Serial::~Serial();                                    <- Calls destructor!
      throw("ERROR: Could not set com port parameters");
    }
  }

  if(cycleDtrOnStartup) {
    if(!EscapeCommFunction(commHandle, CLRDTR))
      throw("ERROR: clearing DTR");
    Sleep(200);
    if(!EscapeCommFunction(commHandle, SETDTR))
      throw("ERROR: setting DTR");
  }

  if(testOnStartup) {
    DWORD numWritten;
    char init[] = "PJON-python init";
    if(!WriteFile(commHandle, init, sizeof(init), &numWritten, NULL))
      throw("writing initial data to port failed");
    if(numWritten != sizeof(init))
      throw("ERROR: not all test data written to port");
  }
};

Serial::~Serial() {
  CloseHandle(commHandle);
};

// and there is more etc .......
// .............

下一个问题,当执行这段代码并调用析构函数时,内存中实际会发生什么?我无法执行和调试它。

【问题讨论】:

  • IMO,这些代码都不应该在构造函数中。构造函数应该是简单的成员初始化。任何需要错误处理的东西最好放在单独的方法中。
  • @user3386109:我完全同意,这就是我称代码丑陋的原因,我永远不会那样做。
  • @Jean-FrançoisFabre :实际上这不是同一个问题,这是关于手动调用析构函数的一般问题。仍然想知道当你这样做时会发生什么,就像问题中所说的那样。
  • @tadman 是的,这真的很糟糕。我已经创建了一个包含所有这些内容的 open() 函数,并创建了一些无压力的构造函数,因此程序员可以决定使用哪个构造函数以及如何使用该对象。它仍然与 C++ 版本“兼容”。

标签: c++ windows constructor destructor


【解决方案1】:

这段代码丑陋但合法。当构造函数抛出异常时,永远不会调用相应的析构函数。所以需要在抛出前手动调用,防止资源泄露。这里真正的错误是 not 在抛出异常之前在其他情况下手动调用析构函数。

当然,更好的方法是使用一个单独的 RAII 对象来封装commHandle。带有自定义删除器的unique_ptr 可以担任此角色。

任何超出低级库的析构函数都是现代 C++ 中的代码异味。

【讨论】:

【解决方案2】:

好吧,让我们从显而易见的开始:不要以这种方式编写代码。我可以看到为什么他们这样做了 - 手动调用析构函数是在抛出异常之前进行清理的一种方便方法,但为什么这是一个坏主意?

好吧,析构函数通常只有在构造函数运行完成时才会被调用(所以它不会运行,以正常的方式,如果构造函数抛出),这是故意的它允许析构函数假定对象已完全初始化。任何复杂的析构函数如果试图拆除未完全初始化的对象,都可能会遇到麻烦。

现在,在编写的代码中,这些都无关紧要,因为我们这里只有一个 tinpot 析构函数,它只是关闭句柄,所以这里的代码 正确投掷前清理干净(有时,谢谢尤金),我们都可以坐下来放松。但是作为一种编程模式,它很臭,而且,既然您知道它实际上做了什么,那么当您将它移到 Delphi 时,您应该对其进行整理。

所以,讲完一些细节(不分先后):

  • 当您手动调用析构函数时,它就像调用任何其他函数一样 - 它被执行并返回并且生命继续。具体来说,对象本身不会被释放。在使用展示位置 new 时这样做很有价值。
  • 从上面可以看出,对 throw 的调用 将在析构函数返回后执行(无论如何都会执行)。
  • 重复一遍,当构造函数抛出时,析构函数不会被调用。我相信,该对象随后会在异常被捕获之前(如果有的话)被释放。

如果您必须转换的其余代码以如此草率的方式编写,我不羡慕您。构造函数无论如何都不应该失败,在一般情况下,只需在对象启动并运行后以单独的方法打开端口即可。

【讨论】:

  • 您好,感谢您的精彩回答。是的,这太可怕了。我已经创建了一个包含所有这些内容的 open() 函数,并创建了一些无压力的构造函数,因此程序员可以决定使用哪个构造函数以及如何使用该对象。它仍然与 C++ 版本“兼容”,但是带有一个很大的警告作为注释。
  • 通常当你在pascal中调用构造函数内部的析构函数时,代码变得不稳定,因为内存被释放并且内存位置无效。这就像访问一个空指针。所以在帕斯卡中这是不可能的,你会直接注意到一个例外,一个耳光;-)
  • 我不同意Constructors shouldn't fail an[y]way。在任何使用异常的源中,失败的构造函数都可以。
  • 对。是的,我看到了你提出的open () 方法,完全同意。我只是确保您完全理解该类作为一个整体的作用(包括错误!),然后使用适当的 API 制作适当的 Delphi 类。我相信所有参与的人(包括你未来的自己)都会感谢你。
  • @Codebeat(和 Paul Sanders)是的,这段代码很糟糕,但原因不同。抛出异常以报告一般错误,特别是在构造函数中发生的错误是好的和正常的 - 除非由于某种原因您根本不能使用异常(例如,您处于处理异常的时间不可接受的实时环境中) .
【解决方案3】:

当你从构造函数中抛出时,它将调用迄今为止构造的任何对象的析构函数:成员变量和继承的类(第 15.2/2 节)。

  1. 初始化或销毁因异常终止的任何存储持续时间的对象都将为其所有完全构造的子对象执行析构函数

如果您手动调用析构函数,它们的析构函数也会被调用(第 12.4/8 节)。

  1. 在执行析构函数的主体并销毁主体内分配的所有自动对象后, 类 X 的析构函数调用 X 的直接非变体非静态数据成员的析构函数,即析构函数 对于 X 的直接基类 ...

因此成员变量的析构函数会被调用两次。形式上,调用两次析构函数是未定义的行为。 (如果他们都有空的析构函数,你可能会侥幸逃脱。)

如果你真的需要一个干净的解决方案,把需要清洗的部分包装成一个类,并使其成为一个成员变量。从它调用初始化,如果你抛出,你保证它会被清理。 您甚至可以通过申请 RAII 获得荣誉积分。

【讨论】:

  • 是的,尽管这些似乎都不适用于这里。这里有一个很好的答案:stackoverflow.com/questions/32323406/…。不管怎样,代码太糟糕了,扔掉它。
  • 您好,感谢您的回答。我怀疑它会在构造函数中引发异常时调用析构函数,更像是@Eugene 的答案,引入了内存泄漏,因为它们因此不使用邪恶的部分。
  • @PaulSanders :可悲的是我不能把它扔掉,需要解决它。 ;-) 不过理解你的感受。
  • @Codebeat 没错,这里构造函数抛出异常时不会调用析构函数。我所说的“垃圾代码”是指重新开始并正确编写它,当然是那一点,因为我现在知道你打算这样做。
  • 构造函数异常时不会调用析构函数,但成员的析构函数会。现在我看到有很多关注代码的质量,然后重写你的问题。
猜你喜欢
  • 2011-04-16
  • 2013-08-26
  • 1970-01-01
  • 2011-12-24
  • 1970-01-01
  • 1970-01-01
  • 2010-09-30
  • 2017-02-08
  • 2015-02-21
相关资源
最近更新 更多