Как деструктор узнает, когда активировать себя?На что можно положиться? - PullRequest
3 голосов
/ 08 апреля 2010

Скажите, например, у меня есть следующий код (чистый пример):

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 () будет ли уничтожено возвращение после возврата функции (когда возвращение выходит из области видимости)? Или это отключит автоматическое уничтожение после того, как я верну адрес, и я смогу сказать, удалить 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 ]

10 голосов
/ 08 апреля 2010

Для объекта, созданного в стеке, деструктор автоматически вызывается, когда объект выходит из области видимости.

Для объекта, созданного в куче, память будет освобождена только при явном вызове 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 голос
/ 08 апреля 2010

Как правило, самый простой способ избежать утечек памяти в C ++ - это избегать использования new и удалять как можно больше.

Если вам нужен указатель на что-то, используйте умный указатель (например, повышение scoped_ptr или shared_ptr). Таким образом, вы все равно можете поместить свой объект в кучу, но его деконструктор будет вызываться, когда умный указатель выходит из области видимости. В противном случае попытка убедиться, что вы вызываете delete в каждом случае, может быть головной болью и вызывать много лишнего кода.

1 голос
/ 08 апреля 2010
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 ++ - это то, что деструкторы автоматически вызываются языком, когда объекты с длительностью автоматического хранения выходят из области видимости.Если вы обеспечите, чтобы каждый ресурс в вашей программе принадлежал такому объекту, вам будет гораздо сложнее просочиться.

0 голосов
/ 08 апреля 2010

В случае следующего кода:

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;
}

Результаты могут вас удивить.

0 голосов
/ 08 апреля 2010

+ 1 к ответу Брайана, просто хочу добавить комментарий об аспекте потоков.

Ваш код для создания потока будет уничтожать объект 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; 
} 
0 голосов
/ 08 апреля 2010

returnnew () уничтожит переменную по возвращении, потому что вы возвращаете указатель на локальную переменную. Если вы хотите вернуть оттуда ответ, выделите его динамически, например:

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

Или просто:

a * returnnew() {
    return new a;
}

Динамически распределенная память не имеет области видимости и, следовательно, не будет освобождена до тех пор, пока вы не скажете это, с помощью delete / delete [] / free или до выхода из программы (и несколько других случаев, не связанных с вопросом) , Прежде, чем кто-либо прокомментирует здесь, мое "пока вы так говорите" также включает поведение указателя shared / smart / etc.

Что касается вашего второго кода и вопросов, если вы размещаете переменную вне потока, но ТОЛЬКО используете ее изнутри, вы можете просто заставить поток освободить (удалить) его, когда он больше не нужен. Однако, если вы планируете получить доступ к переменной из нескольких точек и не можете догадаться, когда вы можете уничтожить ее, обязательно используйте интеллектуальный или общий указатель.

...