Предположим, у меня есть распределение динамической памяти через p1 следующим образом:

int* p1 = new int;
*p1 = 1;

Я знаю, что память, на которую ссылается p1, можно освободить, используя

delete p1;
p1 = nullptr;

Но мне интересно, если есть еще один указатель p2, указывающий на 1, могу ли я delete этот указатель, чтобы освободить память? А что будет с указателем p1? Кроме того, какова по существу связь между p1 и p2? Например,

int* p1 = new int;
*p1 = 1;
int* p2 = p1;
// Can I delete p2 like this? And what would happen to p1?
delete p2;
p2 = nullptr;
3
Nicholas 27 Ноя 2016 в 18:43

6 ответов

Лучший ответ

Вы можете удалить p2, но разыменование p1 приведет к неопределенному поведению и возможной ошибке сегментации.

Это работает так:

  1. Память выделена по какому-то адресу.
  2. Оба p1 и p2 указывают на эту ячейку памяти.
  3. После удаления p2 - p1 все еще указывает на эту ячейку памяти. Утечки нет, все в порядке - только не разыменовывать p1. Вы можете свободно делать p1 = nullptr, но не можете *p1 = 1. Кроме того, вы не можете удалить p1, так как он уже удален, и вы, вероятно, поймаете segfault.
8
Starl1ght 27 Ноя 2016 в 15:47

Вы описываете очень известную проблему в (старом) C ++: когда несколько указателей указывают на одну и ту же динамическую память, какой из них ее удаляет?

Если вы удаляете как p1, так и p2, вы дважды удаляете память, которая имеет неопределенное поведение (в лучшем случае сбой), если вы удаляете p1 или p2 и вы продолжаете использовать память через другой указатель - вы используете висящий указатель, что является неопределенным поведением (в лучшем случае сбой). вам нужно убедиться, что при удалении одного указателя вы не можете использовать эту память в других указателях.

C ++ 11 представил стандартный способ решения этой проблемы: использование указателя с самосчетом, при котором только последний указатель удаляет память:

auto p1 = std::make_shared<int>(0);
auto p2 = p1;

Теперь последний активный указатель удалит выделенную память, и вам вообще не нужно беспокоиться, кто что удаляет.

4
David Haim 27 Ноя 2016 в 15:49

А что будет с указателем p1? Кроме того, какова по существу связь между p1 и p2?

Их основная взаимосвязь состоит в том, что они указывают на тот же адрес, полученный в результате распределения динамической памяти после присвоения int* p2 = p1;.

Таким образом, удаление любого из них освободит выделенную память. Однако установка одного из них на nullptr не повлияет на другой.

Итак, у вас остался болтающийся указатель, который нельзя безопасно удалить.

2
πάντα ῥεῖ 27 Ноя 2016 в 15:47

Вы можете удалить либо p1, либо p2. Никакой разницы не будет. Но не следует удалять оба. Кроме того, если вы удалили один, вы не должны использовать другой. За это отвечает программист. Сам по себе язык не поможет. Здесь есть масса разных способов написать плохой код.

Есть несколько методов / шаблонов для решения этой проблемы. Очень часто для этого используются умные указатели. См. Документацию std :: shared_ptr. Не используйте устаревшие auto_ptr.

Мой любимый паттерн - «владение». Это означает, что один указатель «владеет» выделением, а все остальные просто используют. Это требует определенной дисциплины при программировании, но если приложить все усилия, полученный код будет ясным и простым. Например:

class MyClass
{
public:  ~MyClass() { for(char *p: myStringsDict) delete p; }

private:
    std::unordered_set<char*> myStringsDict;
};

Глядя на этот класс, становится ясно (хотя было бы неплохо добавить соответствующий комментарий), что он владеет словарем строк, и эти строки действительны, пока существует экземпляр этого класса. Эти указатели могут использоваться в структурах, принадлежащих этому классу, их можно передавать в качестве параметров функциям и т. Д. Ясно, когда их больше не следует использовать.

В серверном программировании, когда выполняется несколько потоков, двойное удаление может быть очень опасным и трудным для отслеживания. Потому что после удаления первого указателя память становится свободной и может быть выделена для каких-то других целей в другом потоке. Когда второй указатель освобождается, может случиться так, что он удаляет допустимое выделение, в то время как другой фрагмент кода не знает об этом и продолжает использовать этот фрагмент памяти.

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

2
Kirill Kobelev 27 Ноя 2016 в 17:49

Давайте рассмотрим аналогию с недвижимостью, где память играет роль земли, а указатели, что неудивительно, действуют как адреса.

Переменная-указатель - это заметка желтого цвета. Вы можете написать на нем почтовый адрес. Переменная, выделенная из бесплатного магазина, - это участок земли по некоторому адресу.

 int *p = new int;

Вы просите город найти где-нибудь небольшой неиспользуемый участок земли и присвоить себе право собственности. Вы записываете его адрес на желтой записке.

 *p = 1;

Вы строите аккуратный домик по этому адресу.

  int *q = p;

Вы делаете копию желтой заметки. Вы забываете об этом на некоторое время.

 delete p;

Вы сносите здание и отказываетесь от своих прав на участок земли. Город может передать его кому-нибудь другому. Возможно, кто-то захочет построить там еще одно небольшое здание, а может быть, проложить железнодорожные пути или устроить бассейн с акулами. Обратите внимание, что это ничего не делает ни с одной из ваших желтых заметок.

p = nullptr;

Вы стираете желтую записку начисто. Другая желтая записка остается.

*q = 2;

Вы находите другую желтую записку, читаете на ней серьезный адрес и предполагаете, что земля принадлежит вам. Плохой ход. Вы приступаете к постройке аккуратного домика на чужой земле. Новым владельцам наплевать (они не могут знать). Завтра они могут снести ваше здание и установить собственное, или захватить вас поездом, или, возможно, вылить на вас 100000 тонн воды и 3 мако. Это довольно неприятно! Не трогай то, что тебе не принадлежит.

1
n. 'pronouns' m. 27 Ноя 2016 в 16:27

При выделении динамической памяти с помощью new она должна быть освобождена с помощью delete, если вы создаете p1 с помощью new, а затем освободите его с помощью delete. вы объявляете p2 как указатель, указывающий на ту же память, на которую указывает p1, тогда, если вы хотите освободить вызов памяти, delete на p1 не p2 для чтения, однако эту память можно освободить, вызвав delete на p1 или p2.

Если вы вызываете delete на p1, тогда заставьте p2 указывать на null, чтобы не разыменовать его по ошибке, потому что написано:

delete p1;
*p2 = 1;

Вызовет неопределенное поведение.

0
Raindrop7 27 Ноя 2016 в 18:04