析构函数如何知道何时激活自身?它可以依靠吗?

时间:2010-04-08 01:26:31

标签: c++ memory-leaks destructor

比如说我有以下代码(纯粹的例子):

class a {
   int * p;
public:
   a() {
      p = new int;
   }
   ~a() {
      delete p;
   }
};

a * returnnew() {
   a retval;
   return(&retval);
}

int main() {
   a * foo = returnnew();
   return 0;
}

在returnnew()中,在函数返回后(当retval超出范围时)会对retval进行破坏吗?或者它会在我返回地址后禁用自动销毁,我可以说删除foo;在main()的末尾?或者,以类似的方式(伪代码):

void foo(void* arg) {
   bar = (a*)arg;
   //do stuff
   exit_thread();
}

int main() {
   while(true) {
      a asdf;
      create_thread(foo, (void*)&asdf);
   }
   return 0;
}

析构函数会去哪里?在哪里我要说删除?或者这是未定义的行为?唯一可行的解​​决方案是使用STL引用计数指针吗?这将如何实施?

谢谢 - 我已经使用了C ++一段时间但从未遇到过这种情况,并且不想创建内存泄漏。

6 个答案:

答案 0 :(得分:10)

对于堆栈创建的对象,当对象超出范围时,将自动调用析构函数。

对于在堆上创建的对象,只有在显式调用delete时才会释放内存。

是否从函数返回堆栈创建对象的地址无关紧要。当项目超出范围时,仍将调用析构函数。

所以对于你的代码示例:

a * returnnew() 
{
   a retval;
   return(&retval);
}
在代码跳转回调用a的代码之前,会调用

returnnew()的析构函数。您返回该对象的地址,但该地址指向内存中不再属于您的位置。

  

我要说删除哪里?

使用delete时,您只能使用new 如果您使用delete[]

,则只能使用new[]
  

还是这种未定义的行为?

如果对不属于您的内存地址执行的操作将是未定义的行为。但这不是正确的代码。

  

唯一可行的解​​决方案是使用STL引用计数指针吗?

您可以按值返回对象,也可以在堆上创建新对象。您还可以通过参数将对象传递给函数,并要求函数更改它。

  

如何实施?

//Shows how to fill an object's value by reference
void fillA(a& mya) 
{
   mya.var = 3;
}

//Shows how to return a value on the heap
a* returnNewA() 
{
  //Caller is responsible for deleting the returned pointer.
  return new a();
}

//Shows how to return by value, memory should not be freed, copy will be returned
a returnA() 
{
  return a();
}

答案 1 :(得分:1)

a * returnnew() {
   a retval;
   return(&retval);
}

此处,retval具有自动存储持续时间,这意味着当语言超出范围时,语言将自动销毁它。您返回的地址是指一个不再存在的对象,并且尝试使用返回值将是一个错误。

当您想要控制对象的生命周期时,您必须使用new运算符来创建它。

a* returnnew() 
{ 
   a* retval = new a();  
   return retval;  
}

在这里,您现在可以完全控制此a的生命周期。它会一直存在,直到你明确delete它,或者你的程序结束。

您还可以为a类提出有意义的复制语义,并按值返回,在这种情况下,您的调用者将获得自己的副本,与原始副本不同。然后,您的来电者不在乎原件何时消失。

class a 
{
   int * p;
public:
   a(a const& rhs) 
   {
      p = new int(rhs.p)
   }
   a() 
   {
      p = new int;
   }
   ~a() 
   {
      delete p;
   }
};

现在,您可以构建一个新的a作为现有a的副本。因此,您的函数可以按值返回a,如下所示:

a returnnew() 
{ 
   a retval;  
   return retval; 
}

这里retval的生命周期将在函数返回时结束,并且它将被语言自动销毁,并且不会泄漏任何资源。而且你的来电者将拥有自己的副本,并拥有自己的生命。

根据我的经验,大多数类应该具有合理的复制语义,并且您不应该害怕通过值传递和返回它们。这种方式更简单,你可以避免悬挂指针问题。

C ++最大的优势之一是当自动存储持续时间对象超出范围时,语言自动调用析构函数的方式。如果确保程序中的每个资源都归这样的对象所有,那么泄漏资源就会困难得多。

答案 2 :(得分:1)

作为一般规则,避免C ++内存泄漏的最简单方法是尽量避免使用new和delete。

如果必须有指向某事物的指针,请使用智能指针(例如boost scoped_ptr或shared_ptr)。这样你仍然可以在堆上拥有你的对象,但是当智能指针超出范围时,它将被调用解构器。否则,尝试确保在每种情况下调用delete都会很麻烦并导致大量额外代码。

答案 3 :(得分:0)

returnnew()将在返回时销毁变量,因为您正在返回指向局部变量的指针。如果你想从那里返回retval,请以dinamically方式分配它,例如:

a * returnnew() {
    a * retval = new a;
    return retval;
}

或者只是:

a * returnnew() {
    return new a;
}

动态分配的内存没有作用域,因此在你这样说之前不会被释放,通过删除/删除[] / free,或者直到程序退出(和其他一些与问题无关的情况) 。在此之前有人评论之前,我的“直到你这样说”还包括共享/智能/等指针行为。

至于你的第二个代码&问题,如果你在线程之外分配变量,但只是从内部使用它,你可以让线程在不再需要它时解除分配(删除)它。但是,如果您计划从多个点访问该变​​量,并且无法猜测何时可以安全地销毁它,请务必使用智能或共享指针。

答案 4 :(得分:0)

+1给Brian的回答,只是想添加一个关于线程方面的评论。

创建线程的代码将破坏传递给线程函数的asdf对象而不管子进程,因为asdf在父堆栈上。在堆上创建asdf,或者按值传递。否则,父节点将销毁asdf,并使您的子线程指向父节点堆栈上的错误地址。在任何情况下都不好,析构函数或没有析构函数。你安全地将asdf传递给线程中的函数的唯一方法是先创建线程,asdf是ITS堆栈中的堆栈对象,而不是父堆栈。

void foo(void* arg) { 
   bar = (a*)arg; // child has reference to a parent stack address!
   //do stuff 
   exit_thread(); 
} 

int main() { 
   while(true) { 
      a asdf; // parent stack
      create_thread(foo, (void*)&asdf); // parent and child diverge here, asdf auto-destroyed
   } 
   return 0; 
} 

答案 5 :(得分:0)

如果是以下代码:

void foo(void* arg) {
   bar = (a*)arg;
   //do stuff
   exit_thread();
}

int main() {
   while(true) {
      a asdf;
      create_thread(foo, (void*)&asdf);
   }
   return 0;
}

析构函数在while循环的右括号中调用。这意味着它将被称为循环的每个迭代(并且将在下一次迭代时再次构造)。

作为探索构造函数和析构函数和范围的细微差别的有用工具,请考虑使用以下类来帮助您将来自己回答这些问题:

class trace {
private:
  std::string msg_;
public:
  explicit trace(const std::string &msg) : msg_(msg) {
    std::cerr << "Constructing: " << msg_ << std::endl;
  }
  ~trace() {
    std::cerr << "Destructing: " << msg_ << std::endl;
  }
};

这样使用它:

trace glb("global");

main() {
  trace t1("top of main");

  for(int i = 0; i < 10; ++i)
  {
    trace t2("inside for");
  }

  return 0;
}

结果可能让您感到惊讶。