我应该在析构函数中重置原始成员变量吗?

时间:2017-12-21 04:46:30

标签: c++

请参阅以下代码,

class MyClass
{
public: 
       int i;
       MyClass()
       {
              i = 10;
       }
};

MyClass* pObj = nullptr;

int main()
{
       {
              MyClass obj;
              pObj = &obj;
       }

       while (1)
       {
              cout << pObj->i; //pObj is dangling pointer, still no crash.
              Sleep(1000);
       }      

       return 0;
}
一旦它超出范围,obj将会死亡。但我在VS 2017中测试过,即使使用它,我也看不到崩溃。

重置int成员varialbe i是不错的做法?

2 个答案:

答案 0 :(得分:2)

在对象被销毁后访问成员是未定义的行为。将析构函数中的成员设置为可预测且最可能意外的值,例如,相当大的值或具有特定位模式的值,使得很容易识别出值中的值,这似乎似乎。调试器。

然而,这个想法在系统中存在缺陷和相形见绌:

  1. 所有类都需要发挥作用,而不是专注于创建正确的代码,开发人员会花时间(开发时间和运行时间)进行无意义的更改。
  2. 编译器发生变得相当聪明,可以检测到不需要析构函数的变化。由于正确的程序无法检测是否进行了更改,因此根本无法进行更改。这种效果对于安全应用程序来说是一个实际问题,例如,密码应该从内存中删除,因此无法读取(使用某些非便携式方法)。
  3. 即使将值设置为特定值,也会重用内存并覆盖值。特别是对于堆栈中的对象,在调试器中看到错误的值之前,很可能是将内存用于其他内容。
  4. 即使重置值,您也一定会看到“崩溃”:崩溃是由设置某些内容以防止无效的事情引起的。在您的示例中,您正在访问堆栈上的int:从CPU的角度来看,堆栈仍然可以访问,并且最多会得到一个意外的值。使用异常指针值通常会导致崩溃,因为内存管理系统尝试访问未映射的位置,但即使这样也无法保证:在繁忙的32位系统上,几乎所有内存都可能正在使用中。也就是说,试图依赖未定义的行为被检测也是徒劳的。
  5. 相应地,使用良好的编码实践更好,避免立即悬挂引用并集中精力使用它们。例如,我始终初始化成员初始化列表中的成员,即使在极少数情况下它们最终会在构造函数的主体中被更改(即,你' d将构造函数写为MyClass(): i() {})。

    作为一个调试工具,可能可以合理地替换分配函数(理想情况下是分配器对象,但可能是全局operator new() / operator delete()和具有不具备版本的系列不会快速分发已释放的内存,而是以可预测的模式填充已释放的内存。由于这些操作会减慢程序的速度,因此您只需在调试版本中使用此代码,但实现起来相对简单且易于启用/禁用在实践中,我不认为即使是这样的系统也能得到回报,因为使用托管指针和正确的所有权设计和生命周期可以避免由于悬空引用造成的大多数错误。

答案 1 :(得分:1)

您提供的代码行为未定义。未定义行为的部分情况正如预期的那样工作,因此代码的工作原理并不奇怪。代码现在可以工作,它可以随时打破,具体取决于编译器版本,编译器选项,堆栈内容和月相。

首先,最重要的是避免在任何地方悬挂指针(以及所有其他类型的未定义行为)。

如果在析构函数中清除变量,我找到了最佳实践:

  1. 遵循编码规则,使我免于访问未分配或销毁的对象的错误。我不能用几句话来描述它,但规则很常见(见here和任何地方)。

  2. 按人类(代码审查)或静态分析器(如cppcheckPVS-Studio或其他)分析代码,以避免与上述情况类似的情况。

  3. 不要手动调用delete,更好地使用scoped_ptr或类似的对象生命周期管理器。当delete合理时,我通常(通常)在删除后将指针设置为nullptr以防止错误。

  4. 尽可能少使用指针。参考文献是优先考虑的。

  5. 当我的班级的对象在外面使用时,我怀疑有人可以在删除后访问它,我可以将签名字段放在里面,在destuctor中设置为0xDEAD,并检查输入或每个公共方法。请注意不要将代码速度降低到不可接受的速度。

  6. 完成所有这些设置i从您的示例到0或-1是多余的。至于我,你不应该集中注意力。