Управление памятью / FAQ C++

Добавлено 26 сентября 2020 в 20:45

Как бороться с утечками памяти?

Писать код, в котором ничего нет. Очевидно, что если ваш код повсюду содержит операции new, операции delete и арифметику указателей, вы где-нибудь облажаетесь и получите утечки, висячие указатели и т.д. Независимо от того, насколько вы сознательно относитесь к выделениям памяти: в конечном итоге сложность кода преодолеет время и усилия, которые вы можете себе позволить.

Отсюда следует, что успешные методы полагаются на сокрытие выделения и освобождения памяти внутри более управляемых типов. Для отдельных объектов предпочтительнее make_unique или make_shared. Для нескольких объектов предпочтительнее использовать стандартные контейнеры, такие как vector и unordered_map, поскольку они управляют памятью для своих элементов лучше, чем могли бы вы без непропорциональных усилий. Подумайте о том, чтобы написать следующий код без помощи string и vector:

#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;

int main()  // небольшая программа, которая возится со строками
{
  cout << "enter some whitespace-separated words:\n";
  vector<string> v;
  string s;
  while (cin>>s) v.push_back(s);
  sort(v.begin(),v.end());
  string cat;
  for (auto & str : v) cat += str+"+";
  cout << cat << '\n';
}

Каковы были бы ваши шансы сделать всё правильно с первого раза? А как узнать, что утечки нет?

Обратите внимание на отсутствие явного управления памятью, макросов, приведений, проверок переполнения, явных ограничений размера и указателей. Используя объект функции и стандартный алгоритм, код мог бы дополнительно исключить использование итератора, похожего на указатель, но это казалось излишним для такой крошечной программы.

Эти методы несовершенны, и их не всегда легко применять систематически. Однако они применяются на удивление широко, и за счет уменьшения количества явных выделений и освобождений памяти вы значительно упрощаете отслеживание оставшихся случаев. Еще в 1981 году Страуструп указал, что, сократив количество объектов, которые он должен был явно отслеживать, с многих десятков тысяч до нескольких десятков, он сократил интеллектуальные усилия, необходимые для того, чтобы перевести программирование из геркулесовой задачи во что-то управляемое или даже легкое.

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

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

Если вы не можете обрабатывать выделение/освобождение памяти неявно как часть объекта, который вам нужен в вашем приложении, вы можете использовать дескриптор ресурса, чтобы минимизировать вероятность утечки. Ниже показан пример, в котором вам нужно вернуть объект, размещенный в свободном хранилище (free store), из функции. Это возможность забыть удалить этот объект. В конце концов, мы не можем сказать, просто глядя на указатель, нужно ли его освободить, и если да, то кто за это отвечает. Использование дескриптора ресурса (здесь стандартная библиотека unique_ptr) проясняет, на ком лежит ответственность:

#include<memory>
#include<iostream>
using namespace std;

struct S {
  S() { cout << "make an S\n"; }
  ~S() { cout << "destroy an S\n"; }
  S(const S&) { cout << "copy initialize an S\n"; }
  S& operator=(const S&) { cout << "copy assign an S\n"; }
};

S* f()
{
  return new S;   // кто ответственен за удаление этого S?
};

unique_ptr<S> g()
{
  return make_unique<S>();    // явно передать ответственность за удаление этого S
}

int main()
{
  cout << "start main\n";
  S* p = f();
  cout << "after f() before g()\n";
  //  S* q = g(); // эта ошибка будет обнаружена компилятором
  unique_ptr<S> q = g();
  cout << "exit main\n";
  // утечка *p
  // неявно удаляет *q
}

Думайте о ресурсах в целом, а не просто о памяти.

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


Могу ли я использовать new просто как в Java?

В некотором роде, но не делайте это вслепую, если вы действительно хотите, чтобы было написано как make_unique или make_shared, и часто есть лучшие альтернативы, которые проще и надежнее, чем любое из этого. Рассмотрим:

void compute(cmplx z, double d)
{
    cmplx z2 = z+d; // c++ style
    z2 = f(z2);     // use z2
    cmplx& z3 = *new cmplx(z+d);    // стиль Java (предполагая, что Java может перегрузить +)
    z3 = f(z3);
    delete &z3; 
}

Неуклюжее использование new для z3 излишне и медленнее по сравнению с характерным использованием локальной переменной (z2). Вам не нужно использовать new для создания объекта, если вы удаляете этот объект в той же области; такой объект должен быть локальной переменной.


Что использовать NULL, 0 или nullptr?

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

Проблема с NULL и 0 в качестве значения нулевого указателя заключается в том, что 0 является специальным значением (может, это целочисленное значение, а может, и указатель). Используйте 0 только для целых чисел, и эта путаница исчезнет.


delete p удаляет указатель p или данные, на которые он указывает, *p?

Данные, на которые он указывает.

Ключевое слово на самом деле должно быть таким delete_the_thing_pointed_to_by (удалить штуку, на которую указывает...). Такое же злоупотребление английским языком происходит при освобождении памяти, на которую указывает указатель в C: free(p) в действительности означает free_the_stuff_pointed_to_by(p) (освободить то, на что ссылается p).


Безопасно ли удалить один и тот же указатель дважды?

Нет! (Предполагая, что в промежутке между удалениями вы не получили этот указатель обратно из new.)

Например, это катастрофа:

class Foo { /*...*/ };

void yourCode()
{
  Foo* p = new Foo();
  delete p;
  delete p;  // КАТАСТРОФА!
  // ...
}

Вторая строка delete p может сделать с вами действительно плохие вещи. Что это может быть, зависит от фазы луны, например, повредить вашу кучу, вывести из строя вашу программу, внести произвольные и причудливые изменения в объекты, которые уже находятся в куче, и т.д. К сожалению, эти симптомы могут появляться и исчезать случайным образом. Согласно закону Мерфи, вы пострадаете сильнее всего в самый неподходящий момент (при сдаче проекта покупателю, при попытке прохождения транзакции с высокой стоимостью т.д.).

Примечание: некоторые системы времени выполнения защищают вас от некоторых очень простых случаев двойного удаления. В зависимости от деталей, и у вас может быть всё в порядке, если вы работаете в одной из таких систем, и если никто никогда не развертывает ваш код в другой системе, которая обрабатывает подобное по-другому, и если вы удаляете то, что не имеет деструктора, и если вы не делаете ничего значительного между этими двумя удалениями, и если никто никогда не изменит ваш код, чтобы выполнить что-то значимое между этими двумя удалениями, и если ваш планировщик потоков (над которым вы, вероятно, не имеете никакого контроля!) не переключается между потоками между этими двумя удалениями, и если, и если, и если... Итак, вернемся к Мерфи: раз что-то может пойти не так, то это случится, и всё пойдет не так в самый неподходящий момент.

НЕ пишите мне о том, что вы протестировали подобный код, и он не дает сбоев. Поймите суть. Отсутствие сбоя не означает отсутствие ошибки; оно просто не может доказать наличие ошибки.

Поверьте мне: двойное удаление – это плохо, плохо, плохо. Просто нет.


Могу ли я освободить через free() указатели, выделенные с помощью new? Могу ли я удалить через delete указатели, выделенные с помощью malloc()?

Нет! Короче говоря, концептуально malloc и new выделяют из разных куч, поэтому не могут освобождать или удалять память друг друга. Они также работают на разных уровнях – необработанная память и созданные объекты.

Вы можете использовать malloc() и new в одной программе. Но вы не можете выделить объект с помощью malloc() и освободить его с помощью delete. Вы также не можете выделить с помощью new и удалить с помощью free() или использовать realloc() для массива, выделенного с помощью new.

Операторы C++ new и delete гарантируют правильное построение и уничтожение; они используются там, где нужно вызывать конструкторы или деструкторы. Функции malloc(), calloc(), free() и realloc() в стиле C этого не гарантируют. Более того, нет гарантии, что механизм, используемый new и delete для получения и освобождения необработанной памяти, совместим с malloc() и free(). Если смешивание стилей работает в вашей системе, вам просто «повезло», пока.

Если вы чувствуете потребность в realloc() (что встречается у многих), то подумайте об использовании vector из стандартной библиотеки. Например:

// читать слова из ввода в вектор из строк:
vector<string> words;
string s;
while (cin>>s && s!=".") words.push_back(s);

vector расширяется по мере необходимости.

Смотрите также примеры и обсуждение в документе «Изучение стандарта C++ как нового языка», который вы можете скачать из списка публикаций Страуструпа.


В чем разница между new и malloc()?

Во-первых, make_unique (или make_shared) почти всегда превосходит как new, так и malloc(), и полностью исключает delete и free().

Теперь сказав об этом, можно перейти к разнице между этими двумя:

malloc() – это функция, которая принимает в качестве аргумента число (байтов) и возвращает void*, указывающий на неинициализированное хранилище. new – это оператор, который принимает тип и (необязательно) набор инициализаторов для этого типа в качестве аргументов и возвращает указатель на (необязательно) инициализированный объект данного типа. Разница наиболее очевидна, когда вы хотите выделить объект определенного пользователем типа с нетривиальной семантикой инициализации. Примеры:

class Circle : public Shape {
public:
  Circle(Point c, int r);
  // нет конструктора по умолчанию
  // ...
};

class X {
public:
  X();    // конструктор по умолчанию
  // ...
};

void f(int n)
{
  void* p1 = malloc(40);  // выделить 40 (неинициализированных) байт

  int* p2 = new int[10];  // выделить 10 неинициализированных int
  int* p3 = new int(10);  // выделить 1 инициализированный int со значением 10
  int* p4 = new int();    // выделить 1 инициализированный int со значением 0
  int* p4 = new int;      // выделить 1 неинициализированный int

  Circle* pc1 = new Circle(Point(0,0),10); // выделить Circle, созданный
                                           // с указанными аргументами
  Circle* pc2 = new Circle;   // ошибка: нет конструктора по умолчанию

  X* px1 = new X;     // выделить X, созданный по умолчанию
  X* px2 = new X();   // выделить X, созданный по умолчанию
  X* px2 = new X[10]; // выделить 10 X, созданных по умолчанию
  // ...
}

Обратите внимание, что когда вы указываете инициализатор с использованием обозначения «(значение)», вы получаете инициализацию с этим значением. Часто лучшей альтернативой массиву с выделенной памятью в свободном хранилище является vector (например, с учетом безопасности исключений).

Всякий раз, когда вы используете malloc(), вы должны учитывать инициализацию и преобразование возвращаемого указателя в правильный тип. Вам также нужно будет подумать, правильно ли вы получили необходимое количество байтов. Когда вы принимаете во внимание инициализацию, между malloc() и new нет разницы в производительности.

malloc() сообщает об исчерпании памяти, возвращая 0. new сообщает об ошибках выделения и инициализации, генерируя исключения (bad_alloc).

Объекты, созданные с помощью new, уничтожаются с помощью delete. Области памяти, выделенные функцией malloc(), освобождаются функцией free().


Зачем использовать new вместо старого, заслуживающего доверия malloc()?

Во-первых, make_unique (или make_shared) почти всегда превосходит как new, так и malloc(), и полностью исключает delete и free().

Теперь сказав об этом; преимущества использования new вместо malloc следующие: конструкторы/деструкторы, безопасность типов, возможность переопределения.

  • Конструкторы/деструкторы: в отличие от malloc(sizeof (Fred)), new Fred() вызывает конструктор класса Fred. Точно так же delete p вызывает деструктор *p.
  • Безопасность типов: malloc() возвращает void*, что не типобезопасно. new Fred() возвращает указатель правильного типа (Fred*).
  • Возможность переопределения: new – это оператор, который может быть переопределен классом, в то время как malloc() не может быть переопределена для каждого класса.

Могу ли я использовать realloc() для указателей, выделенных через new?

Нет!

Когда realloc() должна скопировать выделенную память, она использует операцию побитового копирования, которая разорвет многие объекты C++ в клочья. Объектам C++ должно быть разрешено копировать себя. Они используют собственный конструктор копирования или оператор присваивания.

Помимо всего этого, куча, которую использует new, может не совпадать с кучей, которую используют malloc() и realloc()!


Почему в C++ нет эквивалента realloc()?

Если вы хотите, вы, конечно, можете использовать realloc(). Однако realloc() гарантированно работает только с массивами, выделенными malloc() (и аналогичными функциями), содержащими объекты без определяемых пользователем конструкторов копирования. Также помните, что вопреки наивным ожиданиям, realloc() иногда копирует свой массив аргументов.

В C++ лучший способ справиться с перераспределением памяти – использовать контейнер стандартной библиотеки, например, vector, и позволить ему расти естественным образом.


Нужно ли мне проверять значение на null после p = new Fred()?

Нет! (Но если у вас есть древний компилятор из каменного века, вам, возможно, придется заставить оператор new генерировать исключение, если ему не хватает памяти.)

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

Fred* p = new Fred();
if (nullptr == p)    // Необходимо, только если ваш компилятор из каменного века!
  throw std::bad_alloc();

Если ваш компилятор не поддерживает (или если вы отказываетесь использовать) исключения, ваш код может быть еще более утомительным:

Fred* p = new Fred();
if (nullptr == p) {    // Необходимо, только если ваш компилятор из каменного века!
  std::cerr << "Couldn't allocate memory for a Fred" << std::endl;
  abort();
}

Мужайтесь. В C++, если система времени выполнения не может выделить sizeof(Fred) байтов памяти во время p = new Fred(), будет выдано исключение std::bad_alloc. В отличие от malloc(), newникогда не возвращает null!

Поэтому вам нужно просто написать:

Fred * p = new Fred();  // Нет необходимости проверять, равен ли p null

Еще одна мысль. Сотрите это. Вам нужно просто написать:

auto p = make_unique<Fred>();  // Нет необходимости проверять, равен ли p null

Так... Теперь намного лучше!

Однако, если у вас старый компилятор, он может еще не поддерживать это. Выясните это, проверив документацию к вашему компилятору в разделе «new». Если он старый, вам, возможно, придется заставить компилятор вести себя так.


Как я могу убедить мой (старый) компилятор автоматически проверять new, чтобы увидеть, возвращает ли он значение null?

В конце концов, ваш компилятор сделает это.

Если у вас старый компилятор, который не выполняет автоматическую проверку на null, вы можете заставить систему выполнения выполнять проверку, установив функцию «обработчика new». Ваша функция «обработчика new» может делать всё, что вы хотите, например, генерировать исключение, удалять какие-либо объекты и возвращать (в этом случае оператор new будет запрашивать выделение памяти), печатать сообщения, и прерывать выполнение программы и т.д.

Ниже показан пример «обработчика new», который печатает сообщение и выдает исключение. Этот обработчик устанавливается с помощью std::set_new_handler():

#include <new>       // Чтобы получить std::set_new_handler
#include <cstdlib>   // Чтобы получить abort()
#include <iostream>  // Чтобы получить std::cerr

class alloc_error : public std::exception {
public:
  alloc_error() : exception() { }
};

void myNewHandler()
{
  // Это ваш обработчик. Он может делать всё, что захотите.
  throw alloc_error();
}

int main()
{
  std::set_new_handler(myNewHandler);   // Установить мой "обработчик new"
  // ...
}

После выполнения строки std::set_new_handler() оператор new вызовет ваш myNewHandler(), если/когда ему не хватит памяти. Это означает, что new никогда не вернет null:

Fred* p = new Fred();   // Нет необходимости проверять, равен ли p null

Примечание. Если ваш компилятор не поддерживает обработку исключений, вы можете, в крайнем случае, изменить строку throw…; на:

std::cerr << "Attempt to allocate memory failed!" << std::endl;
abort();

Примечание. Если конструктор глобального/статического объекта или объекта какого-либо пространства имен использует new, он может не использовать функцию myNewHandler(), поскольку этот конструктор часто вызывается до начала main(). К сожалению, нет удобного способа гарантировать, что std::set_new_handler() будет вызываться перед первым использованием new. Например, даже если вы поместите вызов std::set_new_handler() в конструктор глобального объекта, вы всё равно не знаете, будет ли модуль («единица компиляции»), содержащий этот глобальный объект, обработан первым, последним или где-то посередине. Следовательно, у вас всё еще нет никакой гарантии, что ваш вызов std::set_new_handler() произойдет до того, как будет вызван любой другой конструктор, глобальный или пространства имен.


Нужно ли мне проверять на null перед delete p?

Нет!

Язык C++ гарантирует, что delete p ничего не сделает, если p имеет значение null. Поскольку вы можете выполнить проверку в обратном порядке, и поскольку большинство методологий тестирования вынуждают вас явно тестировать каждую точку ветвления, вам не следует добавлять избыточную проверку if.

Неправильно:

if (p != nullptr)   // или просто "if (p)"
  delete p;

Правильно:

delete p;

Какие два действия происходят, когда я говорю delete p?

delete p – это двухэтапный процесс: он вызывает деструктор, а затем освобождает память. Код, генерируемый для delete p, функционально аналогичен следующему (при условии, что p имеет тип Fred*):

// Исходный код: delete p;
if (p) {    // или "if (p != nullptr)"
  p->~Fred();
  operator delete(p);
}

Оператор p->~Fred() вызывает деструктор для объекта Fred, на который указывает p.

Выражение operator delete(p) вызывает примитив освобождения памяти, void operator delete(void* p). Этот примитив по духу похож на free(void* p) (однако обратите внимание, что эти двое не являются взаимозаменяемыми; например, нет гарантии, что эти два примитива освобождения памяти используют хотя бы одну и ту же кучу!)


Почему delete не «обнуляет» (не устанавливает в null) свой операнд?

Во-первых, вы обычно должны использовать умные указатели, поэтому вам будет всё равно – вы всё равно не будете писать delete.

В тех редких случаях, когда вы действительно занимаетесь ручным управлением памятью и делаете это осторожно, посмотрите на следующий код:

delete p;
// ...
delete p;

Если пропущенный фрагмент не касается p, то второй delete p; является серьезной ошибкой, от которой реализация C++ не может эффективно защитить себя (без необычных мер предосторожности). Поскольку удаление нулевого указателя безвредно по определению, простым решением было бы, чтобы delete p; выполнял p = nullptr; после того, как он сделает всё, что требуется. Однако C++ не гарантирует этого.

Одна из причин заключается в том, что операнд delete не обязательно должен быть lvalue. Например:

delete p+1;
delete f(x);

Здесь реализация delete не имеет указателя, который он может обнулить. Эти примеры могут быть редкими, но они подразумевают, что невозможно гарантировать, что «любой указатель на удаленный объект равен null». Более простой способ обойти это «правило» – иметь два указателя на объект:

T* p = new T;
T* q = p;
delete p;
delete q;   // упс!

C++ явно позволяет реализации delete обнулить операнд lvalue, но эта идея, похоже, не стала популярной среди разработчиков.

Если вы считаете важным обнуление указателей, подумайте об использовании функции уничтожения:

template<class T> inline void destroy(T*& p) { delete p; p = 0; }

Считайте это еще одной причиной минимизировать явное использование new и delete, полагаясь на умные указатели, контейнеры, дескрипторы стандартной библиотеки и т.д.

Обратите внимание, что передача указателя в качестве ссылки (чтобы разрешить обнуление указателя) имеет дополнительное преимущество в предотвращении вызова destroy() для rvalue:

int* f();
int* p;
// ...
destroy(f());   // ошибка: попытка передать rvalue с помощью неконстантной ссылки
destroy(p+1);   // ошибка: попытка передать rvalue с помощью неконстантной ссылки

Почему деструктор не вызывается в конце области видимости?

Простой ответ: «потому что!», взгляните на пример, который часто сопровождает этот вопрос:

void f()
{
  X* p = new X;
  // используем p
}

То есть было какое-то (ошибочное) предположение, что объект, созданный new, будет уничтожен в конце функции.

По сути, вы должны использовать выделение из кучи только в том случае, если вы хотите, чтобы объект существовал за пределами времени существования области, в которой его создаете. Даже в этом случае вы обычно должны использовать make_unique или make_shared. В тех редких случаях, когда вам действительно нужно выделение из кучи, и вы предпочитаете использовать new, для уничтожения объекта вам нужно использовать delete. Например:

X* g(int i) { /* ... */ return new X(i); }  // X переживет вызов g()

void h(int i)
{
  X* p = g(i);
  // ...
  delete p; // предостережение: небезопасно для исключений
}

Если вы хотите, чтобы объект находился только в области видимости, вообще не используйте выделение из кучи, а просто определите переменную:

{
  ClassName x;
  // используем x
}

Переменная неявно уничтожается в конце области видимости.

Код, который создает объект с помощью new, а затем удаляет его в конце той же области, уродлив, подвержен ошибкам, неэффективен и обычно небезопасен. Например:

void very_bad_func()    // уродливо, подвержено ошибкам, неэффективно
{
  X* p = new X;
  // используем p
  delete p;  // небезопасно для исключений
}

Происходит ли «утечка» памяти Fred в p = new Fred(), если конструктор Fred генерирует исключение?

Нет.

Если во время выполнения конструктора Fred при p = new Fred() возникает исключение, язык C++ гарантирует, что выделенные байты sizeof(Fred) будут автоматически возвращены обратно в кучу.

Подробности: new Fred() – это двухэтапный процесс:

  1. sizeof(Fred) байт памяти выделяется с помощью примитивного оператора void* new(size_t nbytes). Этот примитив по духу похож на malloc(size_t nbytes) (однако обратите внимание, что эти двое не являются взаимозаменяемыми; например, нет никакой гарантии, что эти два примитива выделения памяти используют хотя бы одну и ту же кучу!);
  2. в этой памяти создается объект с помощью вызова конструктора Fred. Указатель, возвращенный на первом этапе, передается конструктору как параметр this. Этот шаг заключен в блок try … catch для обработки случаем, когда на этом шаге генерируется исключение.

Таким образом, фактический сгенерированный код функционально похож на:

// Исходный код: Fred* p = new Fred();
Fred* p;
void* tmp = operator new(sizeof(Fred));
try {
  new(tmp) Fred();  // размещение new
  p = (Fred*)tmp;   // указатель присваивается, только если конструктор успешен
}
catch (...) {
  operator delete(tmp);  // освободить память
  throw;                 // повторно выкинуть исключение
}

Выражение с пометкой «размещение new» вызывает конструктор Fred. Указатель p становится указателем this внутри конструктора Fred::Fred().


Как мне выделить/освободить память для массива?

Используйте p = new T[n] и delete[] p:

Fred* p = new Fred[100];
// ...
delete[] p;

Каждый раз, когда вы выделяете память для массива объектов с помощью new (обычно с [n] в выражении new), вы должны использовать [] в операторе delete. Этот синтаксис необходим, потому что нет синтаксической разницы между указателем на объект и указателем на массив объектов (это мы унаследовали от C).


Что, если я забуду [] при удалении массива, выделенного через new T[n]?

Вся жизнь подойдет к катастрофическому концу.

Задача правильного соединения между new T[n] и delete[] p – это ответственность программиста, а не компилятора. Если вы ошиблись, компилятор не сгенерирует ни сообщения об ошибке времени компиляции, ни сообщения об ошибке времени выполнения. Вероятный результат – повреждение кучи. Или хуже. Ваша программа, вероятно, выдаст сбой.


Могу ли я опустить [] при удалении массива какого-либо встроенного типа (char, int и т.д.)?

Нет!

Иногда программисты думают, что [] в delete [] p существует, только чтобы компилятор вызывал соответствующие деструкторы для всех элементов в массиве. По этой причине они предполагают, что массив некоторого встроенного типа, такого как char или int, может быть удален без []. Например, они предполагают, что этот код корректен:

void userCode(int n)
{
  char* p = new char[n];
  // ...
  delete p;     // ← ОШИБКА! должно быть delete[] p !
}

Но приведенный выше код некорректен и может вызвать сбой во время выполнения. В частности, код, вызываемый для delete p, - это operator delete(void*), а код, вызываемый для delete[] p, - это operator delete[](void*). Поведение по умолчанию для последнего – это вызов первого, но пользователям разрешено заменить последнее другим поведением (в этом случае они обычно также заменяют соответствующий код new в operator new[](size_t)). Если они заменили код delete[] так, что он несовместим с кодом delete, и вы вызвали неправильный код (то есть, если вы сказали delete p, а не delete[] p), вы можете столкнуться со сбоем во время выполнения.


После p = new Fred[n], как компилятор узнает, что есть n объектов, которые должны быть уничтожены во время delete[] p?

Краткий ответ: магия.

Длинный ответ: система времени выполнения хранит количество объектов n где-нибудь, откуда его можно получить, если вам известен только указатель p. Это можно сделать двумя популярными методами. Оба эти метода используются компиляторами коммерческого уровня, оба имеют компромиссы, и ни один из них не идеален. Эти методы:

  • избыточное выделение памяти под массив и размещение n слева от первого объекта Fred;
  • использование ассоциативного массива с p в качестве ключа и n в качестве значения.

Законно ли (и морально ли), чтобы функция-член сказала delete this?

Пока вы осторожны, это нормально (не зло) для объекта совершать самоубийство (delete this).

Мое определение «осторожности»:

  1. Вы должны быть абсолютно на 100% уверены, что объект this был выделен с помощью new (не с помощью new[], ни путем размещения new, ни как локальный объект в стеке, ни в области пространства имен / глобального пространства, ни как член другого объекта; а с помощью простого обычного new).
  2. Вы должны быть абсолютно на 100% уверены, что ваша функция-член будет последней функцией-членом, вызванной для этого объекта.
  3. Вы должны быть абсолютно на 100% уверены, что оставшаяся часть вашей функции-члена (после строки delete this) не касается какой-либо части объекта this (включая вызов любых других функций-членов или использование каких-либо элементов данных). Это включает в себя код, который будет запускаться в деструкторах для любых объектов, выделенных в стеке, которые всё еще живы.
  4. Вы должны быть абсолютно на 100% уверены, что никто даже не коснется самого указателя this после строки delete this. Другими словами, вы не должны проверять его, сравнивать с другим указателем, сравнивать с nullptr, печатать, приводить, делать с ним что-либо еще.

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


Как с помощью new выделить память под многомерные массивы?

Есть много способов сделать это в зависимости от того, насколько гибким вы хотите, чтобы был размер массива. С одной стороны, если вы знаете все измерения во время компиляции, вы можете размещать многомерные массивы статически (как в C):

class Fred { /*...*/ };
void someFunction(Fred& fred);

void manipulateArray()
{
  const unsigned nrows = 10;  // Количество строк - это константа времени компиляции
  const unsigned ncols = 20;  // Количество столбцов - это константа времени компиляции
  Fred matrix[nrows][ncols];

  for (unsigned i = 0; i < nrows; ++i) {
    for (unsigned j = 0; j < ncols; ++j) {
      // Вот способ получить доступ к элементу (i,j):
      someFunction( matrix[i][j] );

      // Вы можете безопасно "вернуться" без какого-либо специального кода delete:
      if (today == "Tuesday" && moon.isFull())
        return;     // Выходить раньше по вторникам, когда полная луна
    }
  }

  // В конце функции нет явного кода delete
}

Чаще всего размер матрицы неизвестен до времени выполнения, но вы знаете, что она будет прямоугольной. В этом случае вам нужно использовать кучу (freestore), но, по крайней мере, вы можете разместить все элементы в одном фрагменте freestore.

void manipulateArray(unsigned nrows, unsigned ncols)
{
  Fred* matrix = new Fred[nrows * ncols];

  // Поскольку мы использовали выше простой указатель, мы должны быть ОЧЕНЬ
  // осторожны, чтобы избежать пропуска кода delete.
  // Вот, почему мы должны отлавливать все исключения:
  try {

    // Вот как получить доступ к элементу (i,j):
    for (unsigned i = 0; i < nrows; ++i) {
      for (unsigned j = 0; j < ncols; ++j) {
        someFunction( matrix[i*ncols + j] );
      }
    }

    // Если вы хотите выйти раньше во вторник, когда полная луна,
    // убедитесь, что delete выполняется во ВСЕХ путях выхода:
    if (today == "Tuesday" && moon.isFull()) {
      delete[] matrix;
      return;
    }

    // ...код, который что-то делает с matrix...
  }
  catch (...) {
    // Убедитесь, что выполняете delete, когда выкидывается исключение:
    delete[] matrix;
    throw;    // Снова выбросить текущее исключение
  }

  // Убедитесь, что выполняете delete и в конце функции:
  delete[] matrix;
}

Наконец, с другой стороны, вам может даже не быть гарантировано, что матрица будет прямоугольной. Например, если каждая строка может иметь разную длину, вам нужно будет выделить память под каждую строку отдельно. В следующей функции ncols[i] – это количество столбцов в строке с номером i, где i изменяется от 0 до nrows-1 включительно.

void manipulateArray(unsigned nrows, unsigned ncols[])
{
  typedef Fred* FredPtr;

  // Здесь не будет утечек памяти при выкидывании исключения:
  FredPtr* matrix = new FredPtr[nrows];

  // Установите для каждого элемента значение null на случай, если позже возникнет исключение.
  // (Смотрите объяснение в комментариях вверху блока try.)
  for (unsigned i = 0; i < nrows; ++i)
    matrix[i] = nullptr;

  // Поскольку мы использовали выше простой указатель, мы должны
  // быть ОЧЕНЬ осторожны, чтобы избежать пропуска кода delete.
  // Вот, почему мы должны отлавливать все исключения:
  try {

    // Далее населяем массив. Если один из них выкинет исключение,l
    // все размещенные элементы будут удалены (смотрите ниже, catch).
    for (unsigned i = 0; i < nrows; ++i)
      matrix[i] = new Fred[ ncols[i] ];

    // Вот как получить доступ к элементу (i,j):
    for (unsigned i = 0; i < nrows; ++i) {
      for (unsigned j = 0; j < ncols[i]; ++j) {
        someFunction( matrix[i][j] );
      }
    }

    // Если вы хотите выйти раньше во вторник, когда полная луна,
    // убедитесь, что delete выполняется во ВСЕХ путях выхода:
    if (today == "Tuesday" && moon.isFull()) {
      for (unsigned i = nrows; i > 0; --i)
        delete[] matrix[i-1];
      delete[] matrix;
      return;
    }

    // ...код, который что-то делает с matrix...

  }
  catch (...) {
    // Убедитесь, что выполняете delete, когда выкидывается исключение:
    // Обратите внимание, что некоторые из указателей в matrix[...] могут быть
    // равны null, но всё хорошо, поскольку допускается delete null.
    for (unsigned i = nrows; i > 0; --i)
      delete[] matrix[i-1];
    delete[] matrix;
    throw;    // Снова выбросить текущее исключение
  }

  // Убедитесь, что выполняете delete и в конце функции.
  // Обратите внимание, что удаление выполняется в противоположном порядке, чем размещение:
  for (unsigned i = nrows; i > 0; --i)
    delete[] matrix[i-1];
  delete[] matrix;
}

Обратите внимание на забавное использование matrix[i-1] в процессе удаления. Это предотвращает выход за переделы беззнакового значения, когда i опускается на один шаг ниже нуля.

Наконец, обратите внимание, что указатели и массивы – зло. Обычно гораздо лучше инкапсулировать указатели в классе с безопасным и простым интерфейсом. В следующем ответе показано, как это сделать.


Но код из предыдущего ответ ТАКОЙ сложный и подвержен ошибкам! Нет более простого способа?

Ага.

Причина, по которой код в предыдущем ответе был настолько сложным и подверженным ошибкам, заключается в том, что он использовал указатели, а мы знаем, что указатели и массивы – это зло. Решение состоит в том, чтобы инкапсулировать указатели в класс с безопасным и простым интерфейсом. Например, мы можем определить класс Matrix, который обрабатывает прямоугольную матрицу, и наш пользовательский код будет значительно упрощен по сравнению с кодом прямоугольной матрицы из предыдущего ответа:

// Код класса Matrix показан ниже...
void someFunction(Fred& fred);

void manipulateArray(unsigned nrows, unsigned ncols)
{
  Matrix matrix(nrows, ncols);   // Создаем Matrix с именем matrix

  for (unsigned i = 0; i < nrows; ++i) {
    for (unsigned j = 0; j < ncols; ++j) {
      // Вот как получить доступ к элементу (i,j):
      someFunction( matrix(i,j) );

      // Вы можете безопасно "вернуться" без какого-либо дополнительного кода delete:
      if (today == "Tuesday" && moon.isFull())
        return;     // Выходить раньше по вторникам, когда полная луна
    }
  }

  // В конце функции нет явного кода delete
}

Главное, на что следует обратить внимание, – это отсутствие кода очистки. Например, в приведенном выше коде нет выражений delete, но утечки памяти не будет, если только деструктор Matrix выполняет свою работу правильно.

Вот код класса Matrix, который делает это возможным:

class Matrix {
public:
  Matrix(unsigned nrows, unsigned ncols);
  // Выбрасывает объект BadSize, если любой размер равен нулю
  class BadSize { };

  // На основе закона большой тройки:
 ~Matrix();
  Matrix(const Matrix& m);
  Matrix& operator= (const Matrix& m);

  // Методы доступа для получения элемента (i,j):
  Fred&       operator() (unsigned i, unsigned j);        // Операторы индекса часто идут в паре
  const Fred& operator() (unsigned i, unsigned j) const;  // Операторы индекса часто идут в паре
  // Они выбрасывают объект BoundsViolation, если  i или j слишком большие
  class BoundsViolation { };

private:
  unsigned nrows_, ncols_;
  Fred* data_;
};

inline Fred& Matrix::operator() (unsigned row, unsigned col)
{
  if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
  return data_[row*ncols_ + col];
}

inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
{
  if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
  return data_[row*ncols_ + col];
}

Matrix::Matrix(unsigned nrows, unsigned ncols)
  : nrows_ (nrows)
  , ncols_ (ncols)
//, data_  ← инициализировано ниже, после выражения if...throw
{
  if (nrows == 0 || ncols == 0)
    throw BadSize();
  data_ = new Fred[nrows * ncols];
}

Matrix::~Matrix()
{
  delete[] data_;
}

Обратите внимание, что приведенный выше класс Matrix выполняет две задачи: он перемещает сложный код управления памятью из пользовательского кода (например, main()) в класс и сокращает общий объем программы. Последний момент важен. Например, если предположить, что Matrix можно даже слегка повторно использовать, перенос сложности от пользователей [множественное число] класса Matrix в сам Matrix [единственное число] эквивалентно перемещению сложности от многих к немногим. Любой, кто видел «Звездный путь 2», знает, что польза для многих перевешивает пользу для нескольких… или для одного.


Но приведенный выше класс Matrix специфичен для Fred! Разве нельзя сделать его обобщенным?

Ага; просто используйте шаблоны.

Вот как это можно сделать:

#include "Fred.h"  // Получить определение класса Fred

// Код Matrix<T> показан ниже...
void someFunction(Fred& fred);

void manipulateArray(unsigned nrows, unsigned ncols)
{
  Matrix<Fred> matrix(nrows, ncols);   // Создаем Matrix<Fred> с именем matrix

  for (unsigned i = 0; i < nrows; ++i) {
    for (unsigned j = 0; j < ncols; ++j) {
      // Вот как получить доступ к элементу (i,j):
      someFunction( matrix(i,j) );

      // Вы можете безопасно "вернуться" без какого-либо дополнительного кода delete:
      if (today == "Tuesday" && moon.isFull())
        return;     // Выходить раньше по вторникам, когда полная луна
    }
  }

  // В конце функции нет явного кода delete
}

Теперь Matrix<T> можно использовать не только для Fred, но и для других целей. Например, в следующем примере используется Matrix для std::string (где std::string – стандартный строковый класс):

#include <string>

void someFunction(std::string& s);

void manipulateArray(unsigned nrows, unsigned ncols)
{
  Matrix<std::string> matrix(nrows, ncols);   // Создаем Matrix<std::string>

  for (unsigned i = 0; i < nrows; ++i) {
    for (unsigned j = 0; j < ncols; ++j) {
      // Вот как получить доступ к элементу (i,j):
      someFunction( matrix(i,j) );

      // Вы можете безопасно "вернуться" без какого-либо дополнительного кода delete:
      if (today == "Tuesday" && moon.isFull())
        return;     // Выходить раньше по вторникам, когда полная луна
    }
  }

  // В конце функции нет явного кода delete
}

Таким образом, из шаблона можно получить целое семейство классов. Например, Matrix<Fred>, Matrix<std::string>, Matrix<Matrix<std::string>> и т.д.

А вот один из способов реализации этого шаблона:

template<typename T>  // Для получения дополнительной информации смотрите раздел Шаблоны
class Matrix {
public:
  Matrix(unsigned nrows, unsigned ncols);
  // Выбрасывает объект BadSize, если любой размер равен нулю
  class BadSize { };

  // На основе закона большой тройки:
 ~Matrix();
  Matrix(const Matrix<T>& m);
  Matrix<T>& operator= (const Matrix<T>& m);

  // Методы доступа для получения элемента (i,j):
  T&       operator() (unsigned i, unsigned j);        // Операторы индекса часто идут в паре
  const T& operator() (unsigned i, unsigned j) const;  // Операторы индекса часто идут в паре
  // Они выбрасывают объект BoundsViolation, если  i или j слишком большие
  class BoundsViolation { };

private:
  unsigned nrows_, ncols_;
  T* data_;
};

template<typename T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
  if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
  return data_[row*ncols_ + col];
}

template<typename T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
  if (row >= nrows_ || col >= ncols_)
    throw BoundsViolation();
  return data_[row*ncols_ + col];
}

template<typename T>
inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
  : nrows_ (nrows)
  , ncols_ (ncols)
//, data_  ← инициализировано ниже, после выражения if...throw
{
  if (nrows == 0 || ncols == 0)
    throw BadSize();
  data_ = new T[nrows * ncols];
}

template<typename T>
inline Matrix<T>::~Matrix()
{
  delete[] data_;
}

Как еще можно создать шаблон Matrix?

Используйте стандартный шаблон vector и сделайте вектор векторов.

В следующем примере используется std::vector<std::vector<T>>.

#include <vector>

template<typename T>  // Для дополнительной информации смотрите раздел "Шаблоны"
class Matrix {
public:
  Matrix(unsigned nrows, unsigned ncols);
  // Выбрасывает объект BadSize, если любой размер равен нулю
  class BadSize { };

  // Из большой тройки больше ничего не нужно!

  // Методы доступа для получения элемента (i,j):
  T&       operator() (unsigned i, unsigned j);        // Операторы индекса часто идут в паре
  const T& operator() (unsigned i, unsigned j) const;  // Операторы индекса часто идут в паре
  // Они выбрасывают объект BoundsViolation, если  i или j слишком большие
  class BoundsViolation { };

  unsigned nrows() const;  // количество строк в данной матрице
  unsigned ncols() const;  // количество столбцов в данной матрице

private:
  std::vector<std::vector<T>> data_;
};

template<typename T>
inline unsigned Matrix<T>::nrows() const
{ return data_.size(); }

template<typename T>
inline unsigned Matrix<T>::ncols() const
{ return data_[0].size(); }

template<typename T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
  if (row >= nrows() || col >= ncols()) throw BoundsViolation();
  return data_[row][col];
}

template<typename T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
  if (row >= nrows() || col >= ncols()) throw BoundsViolation();
  return data_[row][col];
}

template<typename T>
Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
  : data_ (nrows)
{
  if (nrows == 0 || ncols == 0)
    throw BadSize();
  for (unsigned i = 0; i < nrows; ++i)
    data_[i].resize(ncols);
}

Обратите внимание, насколько это проще, чем предыдущее: нет явного new в конструкторе, и нет необходимости в чем-либо из «большой тройки» (деструктор, конструктор копирования или оператор присваивания). Проще говоря, вероятность утечки памяти в вашем коде значительно ниже, если вы используете std::vector, чем если бы вы явно использовали new T[n] и delete[] p.

Также обратите внимание, что std::vector не заставляет вас выделять большое количество блоков памяти. Если вы предпочитаете выделять только один фрагмент памяти для всей матрицы, как это было сделано в предыдущем случае, просто измените тип data_ на std::vector<T> и добавьте переменные-члены nrows_ и ncols_. С остальным вы разберетесь: инициализируйте data_ с помощью data_ (nrows * ncols), измените operator()() для возврата data_ [row * ncols_ + col]; и т.д.


Есть ли в C++ массивы, длину которых можно указать во время выполнения?

Да, в том смысле, что в стандартной библиотеке есть шаблон std::vector, который обеспечивает такое поведение.

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

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

const unsigned ncols = 100;           // ncols = количество столбцов в массиве

class Fred { /*...*/ };

void manipulateArray(unsigned nrows)  // nrows = количество строк в массиве
{
  Fred (*matrix)[ncols] = new Fred[nrows][ncols];
  // ...
  delete[] matrix;
}

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

Но, пожалуйста, не используйте массивы без необходимости. Массивы – это зло. Если можете, используйте какой-нибудь объект какого-то класса. Используйте массивы только тогда, когда это необходимо.


Как я могу заставить объекты моего класса всегда создаваться через new, а не как локальные, в области пространства имен, глобальные или статические?

Используйте идиому именованного конструктора.

Как обычно с идиомой именованных конструкторов, все конструкторы являются private или protected, и существует один или несколько общедоступных статических методов public static create() (так называемые «именованные конструкторы»), по одному на конструктор. В этом случае методы create() размещают объекты через new. Поскольку сами конструкторы не являются общедоступными, другого способа создания объектов класса нет.

class Fred {
public:
  // Методы create() - это "именованные конструкторы":
  static Fred* create()                 { return new Fred();     }
  static Fred* create(int i)            { return new Fred(i);    }
  static Fred* create(const Fred& fred) { return new Fred(fred); }
  // ...

private:
  // Сами конструкторы являются частными или защищенными:
  Fred();
  Fred(int i);
  Fred(const Fred& fred);
  // ...
};

Теперь единственный способ создавать объекты Fred – через Fred::create():

int main()
{
  Fred* p = Fred::create(5);
  // ...
  delete p;
  // ...
}

Убедитесь, что ваши конструкторы находятся в разделе private, если ожидаете, что у Fred будут производные классы.

Также обратите внимание, что вы можете сделать другой класс Wilma дружественным классу Fred, если хотите разрешить Wilma иметь объект-член класса Fred, но, конечно, это смягчает первоначальную цель, а именно, чтобы заставить объекты Fred размещаться через new.


Как сделать простой подсчет ссылок?

Если всё, что вам нужно, это возможность передавать кучу указателей на один и тот же объект, с возможностью автоматического удаления объекта при исчезновении последнего указателя на него, вы можете использовать что-то вроде следующего класса «умного указателя»:

// Fred.h

class FredPtr;

class Fred {
public:
  Fred() : count_(0) /*...*/ { }  // Все конструкторы устанавливают count_ в значение 0 !
  // ...
private:
  friend class FredPtr;     // Дружественный класс
  unsigned count_;
  // count_ должен быть инициализирован в значение 0 всеми конструкторами
  // count_ это количество объектов FredPtr, которые указывают на this
};

class FredPtr {
public:
  Fred* operator-> () { return p_; }
  Fred& operator* ()  { return *p_; }
  FredPtr(Fred* p)    : p_(p) { ++p_->count_; }  // p must not be null
 ~FredPtr()           { if (--p_->count_ == 0) delete p_; }
  FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
  FredPtr& operator= (const FredPtr& p)
        { // НЕ МЕНЯЙТЕ ПОРЯДОК ЭТИХ ВЫРАЖЕНИЙ!
          // (Данный порядок правильно обрабатывает самоприсваивание)
          // (Данный порядок также правильно обрабатывает рекурсию, например, если Fred содержит объекты FredPtr)
          Fred* const old = p_;
          p_ = p.p_;
          ++p_->count_;
          if (--old->count_ == 0) delete old;
          return *this;
        }
private:
  Fred* p_;    // p_ никогда не равен NULL
};

Естественно, вы можете использовать вложенные классы для переименования FredPtr в Fred::Ptr.

Обратите внимание, что вы можете смягчить приведенное выше правило «никогда не NULL», немного изменив конструктор, конструктор копирования, оператор присваивания и деструктор. Если вы это сделаете, вы также можете поставить проверку p_! = NULL в операторы "*" и "->" (по крайней мере, как assert()). Я бы не рекомендовал использовать метод operator Fred*(), поскольку это позволит людям случайно добраться до Fred*.

Одно из неявных ограничений FredPtr состоит в том, что он должен указывать только на объекты Fred, которые были выделены с помощью new. Если вы хотите быть по-настоящему в безопасности, вы можете усилить это ограничение, сделав все конструкторы Fred закрытыми, и для каждого конструктора добавить общедоступный (статический) метод create(), который размещает объект Fred через new и возвращает FredPtr (не Fred*). Таким образом, единственный способ создать объект Fred – получить FredPtr (Fred* p = new Fred() будет заменено на FredPtr p = Fred::create()). Таким образом, никто не сможет случайно нарушить механизм подсчета ссылок.

Например, если бы у Fred были Fred::Fred() и Fred::Fred(int i, int j), изменения в классе Fred были бы такими:

class Fred {
public:
  static FredPtr create();              // Определенный ниже класс FredPtr {...};
  static FredPtr create(int i, int j);  // Определенный ниже класс FredPtr {...};
  // ...
private:
  Fred();
  Fred(int i, int j);
  // ...
};

class FredPtr { /* ... */ };

inline FredPtr Fred::create()             { return new Fred(); }
inline FredPtr Fred::create(int i, int j) { return new Fred(i,j); }

Конечным результатом является то, что теперь у вас есть способ использовать простой подсчет ссылок для обеспечения «семантики указателя» для данного объекта. Пользователи вашего класса Fred явно используют объекты FredPtr, которые действуют более или менее как указатели Fred*. Преимущество состоит в том, что пользователи могут делать множество копий своих объектов «умного указателя» FredPtr, а объект Fred, на который они указывают, будет автоматически удален, когда исчезнет последний такой объект FredPtr.

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


Как обеспечить подсчет ссылок с семантикой «копирование при записи»?

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

Основная идея состоит в том, чтобы позволить пользователям думать, что они копируют ваши объекты Fred, но на самом деле лежащая в основе реализация фактически не выполняет никакого копирования, пока какой-либо пользователь не попытается изменить базовый объект Fred.

Класс Fred::Data содержит все данные, которые обычно входят в класс Fred. Fred::Data также имеет дополнительный элемент данных count_ для управления подсчетом ссылок. Класс Fred становится «умной ссылкой», которая (внутренне) указывает на Fred::Data.

class Fred {
public:

  Fred();                               // Конструктор по умолчанию
  Fred(int i, int j);                   // Обычный конструктор

  Fred(const Fred& f);
  Fred& operator= (const Fred& f);
 ~Fred();

  void sampleInspectorMethod() const;   // Не изменяет данный объект
  void sampleMutatorMethod();           // Изменяет данный объект

  // ...

private:

  class Data {
  public:
    Data();
    Data(int i, int j);
    Data(const Data& d);

    // Поскольку только Fred имеет доступ к объекту Fred::Data,
    // вы можете сделать данные Fred::Data сделать общедоступными, если хотите.
    // Но если вам от этого не комфортно, сделайте данные закрытыми
    // и сделайте Fred дружественным классом, с помощью friend class Fred;

    // ...здесь объявлены ваши члены данных...

    unsigned count_;
    // count_ это количество объектов Fred, которые указывают на данный объект
    // count_ должен инициализироваться в значение 1 всеми конструкторами
    // (он начинается с 1, поскольку он указывает на объект Fred, который создал его)
  };

  Data* data_;
};

Fred::Data::Data()              : count_(1) /*инициализация остальных данных*/ { }
Fred::Data::Data(int i, int j)  : count_(1) /*инициализация остальных данных*/ { }
Fred::Data::Data(const Data& d) : count_(1) /*инициализация остальных данных*/ { }

Fred::Fred()             : data_(new Data()) { }
Fred::Fred(int i, int j) : data_(new Data(i, j)) { }

Fred::Fred(const Fred& f)
  : data_(f.data_)
{
  ++data_->count_;
}

Fred& Fred::operator= (const Fred& f)
{
  // НЕ МЕНЯЙТЕ ПОРЯДОК ЭТИХ ВЫРАЖЕНИЙ!
  // (Этот порядок правильно обрабатывает самоприсваивание)
  // (Этот порядок также правильно обрабатывает рекурсию, например, если Fred::Data содержит объекты Fred)
  Data* const old = data_;
  data_ = f.data_;
  ++data_->count_;
  if (--old->count_ == 0) delete old;
  return *this;
}

Fred::~Fred()
{
  if (--data_->count_ == 0) delete data_;
}

void Fred::sampleInspectorMethod() const
{
  // Этот метод обещает ("const") ничего не менять в *data_
  // В остальном любой доступ к данным будет просто использовать "data_-> ..."
}

void Fred::sampleMutatorMethod()
{
  // Этот метод может при необходимости что-то менять в *data_
  // Таким образом, он сначала проверяет, является ли this единственным указателем на *data_
  if (data_->count_ > 1) {
    Data* d = new Data(*data_);    // Вызов конструктора копирования Fred::Data
    --data_->count_;
    data_ = d;
  }
  assert(data_->count_ == 1);

  // Теперь метод переходит к обычному доступу к "data_->..."
}

Если вызов конструктора Fred по умолчанию является довольно распространенным явлением, вы можете избежать всех этих вызовов new, используя общий объект Fred::Data для всех объектов Fred, созданных с помощью Fred::Fred(). Чтобы избежать проблем с порядком статической инициализации, этот общий объект Fred::Data создается внутри функции «при первом использовании». Ниже показаны изменения, которые будут внесены в приведенный выше код (обратите внимание, что деструктор общего объекта Fred::Data никогда не вызывается; если это для вас является проблемой, либо надейтесь, что у вас нет проблем с порядком статической инициализации, либо вернитесь к подходу, описанному выше):

class Fred {
public:
  // ...
private:
  // ...
  static Data* defaultData();
};

Fred::Fred()
  : data_(defaultData())
{
  ++data_->count_;
}

Fred::Data* Fred::defaultData()
{
  static Data* p = nullptr;
  if (p == nullptr) {
    p = new Data();
    ++p->count_;    // Убедитесь, что он никогда не доходит до нуля
  }
  return p;
}

Примечание. Вы также можете обеспечить подсчет ссылок для иерархии классов, если ваш класс Fred обычно является базовым классом.


Как обеспечить подсчет ссылок с семантикой копирования при записи для иерархии классов?

В предыдущем ответе представлена схема подсчета ссылок, которая предоставляет пользователям семантику ссылок, но делает это для одного класса, а не для иерархии классов. Данный ответ расширяет предыдущую методику, позволяя использовать иерархию классов. Основное отличие состоит в том, что Fred::Data теперь является корнем иерархии классов, что, вероятно, заставляет его иметь некоторые виртуальные функции. Обратите внимание, что сам класс Fred по-прежнему не будет иметь никаких виртуальных функций.

Для создания копий объектов Fred::Data используется идиома виртуального конструктора. Чтобы выбрать производный класс для создания, в приведенном ниже примере кода используется идиома именованного конструктора, но возможны и другие методы (оператор switch в конструкторе и т.п.). Пример кода предполагает два производных класса: Der1 и Der2. Методы в производных классах не знают о подсчете ссылок.

class Fred {
public:

  static Fred create1(const std::string& s, int i);
  static Fred create2(float x, float y);

  Fred(const Fred& f);
  Fred& operator= (const Fred& f);
 ~Fred();

  void sampleInspectorMethod() const;   // Не изменяет данный объект
  void sampleMutatorMethod();           // Изменяет данный объект

  // ...

private:

  class Data {
  public:
    Data() : count_(1) { }
    Data(const Data& d) : count_(1) { }              // НЕ копируйте член 'count_'!
    Data& operator= (const Data&) { return *this; }  // НЕ копируйте член 'count_'!
    virtual ~Data() { assert(count_ == 0); }         // Виртуальный деструктор
    virtual Data* clone() const = 0;                 // Виртуальный конструктор
    virtual void sampleInspectorMethod() const = 0;  // Чисто виртуальная функция
    virtual void sampleMutatorMethod() = 0;
  private:
    unsigned count_;   // count_ не должен быть protected
    friend class Fred; // Даем доступ Fred к count_
  };

  class Der1 : public Data {
  public:
    Der1(const std::string& s, int i);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual Data* clone() const;
    // ...
  };

  class Der2 : public Data {
  public:
    Der2(float x, float y);
    virtual void sampleInspectorMethod() const;
    virtual void sampleMutatorMethod();
    virtual Data* clone() const;
    // ...
  };

  Fred(Data* data);
  // Создает умную ссылку Fred, которая владеет *data
  // Закрытый, чтобы заставить пользователей использовать метод createXXX()
  // Требование: data не должна быть NULL

  Data* data_;   // data_ никогда не равна NULL
};

Fred::Fred(Data* data) : data_(data)  { assert(data != nullptr); }

Fred Fred::create1(const std::string& s, int i) { return Fred(new Der1(s, i)); }
Fred Fred::create2(float x, float y)            { return Fred(new Der2(x, y)); }

Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }

Fred::Fred(const Fred& f)
  : data_(f.data_)
{
  ++data_->count_;
}

Fred& Fred::operator= (const Fred& f)
{
  // НЕ МЕНЯЙТЕ ПОРЯДОК ЭТИХ ВЫРАЖЕНИЙ!
  // (Этот порядок правильно обрабатывает самоприсваивание)
  // (Этот порядок также правильно обрабатывает рекурсию, например, если Fred::Data содержит объекты Fred)
  Data* const old = data_;
  data_ = f.data_;
  ++data_->count_;
  if (--old->count_ == 0) delete old;
  return *this;
}

Fred::~Fred()
{
  if (--data_->count_ == 0) delete data_;
}

void Fred::sampleInspectorMethod() const
{
  // Этот метод обещает ("const") не изменять что-либо в *data_
  // Следовательно, мы просто "передаем этот метод насквозь" к *data_:
  data_->sampleInspectorMethod();
}

void Fred::sampleMutatorMethod()
{
  // Этот метод может при необходимости изменить что-либо в *data_
  // Таким образом, он сначала проверяет, является ли он единственным указателем на *data_
  if (data_->count_ > 1) {
    Data* d = data_->clone();   // идиома виртуального конструктора
    --data_->count_;
    data_ = d;
  }
  assert(data_->count_ == 1);
  // Теперь мы "передаем этот метод насквозь" к *data_:
  data_->sampleMutatorMethod();
}

Естественно, конструкторы и методы sampleXXX для Fred::Der1 и Fred::Der2 нужно будет реализовать любым подходящим способом.


Могу ли я полностью предотвратить использование людьми механизма подсчета ссылок, и если да, то должен ли я так делать?

Нет, и (обычно) нет.

Есть два основных подхода к подрыву механизма подсчета ссылок:

  1. Эта схема может быть нарушена, если кто-то получил Fred* (вместо того, чтобы быть вынужденным использовать FredPtr). Кто-то может получить Fred*, если в классе FredPtr есть operator*(), который возвращает Fred&: FredPtr p = Fred::create(); Fred* p2 = &*p;. Да, это странно и неожиданно, но такое бывает. Эту дыру можно закрыть двумя способами: перегрузить Fred::operator&(), чтобы он возвращал FredPtr, или изменить тип возвращаемого значения FredPtr::operator*(), чтобы он возвращал FredRef (FredRef будет классом, имитирующим ссылку; он должен иметь все методы, которые есть у Fred, и он должен будет перенаправить все эти вызовы методов в базовый объект Fred; из-за этого второго вызова возможно снижение производительности в зависимости от того, насколько хорош компилятор при встраивании методов). Другой способ исправить это – исключить FredPtr::operator*() и потерять соответствующую возможность получать и использовать Fred&. Но даже если вы всё это сделаете, кто-то всё равно сможет сгенерировать Fred*, явно вызвав operator->(): FredPtr p = Fred::create(); Fred* p2 = p.operator->();.
  2. Эта схема может быть нарушена, если у кого-то была утечка и/или висячий указатель на FredPtr. В основном мы говорим здесь, что Fred теперь в безопасности, но мы каким-то образом хотим помешать людям делать глупые вещи с объектами FredPtr. (И если бы мы могли решить это с помощью объектов FredPtrPtr, у нас снова возникла бы та же проблема уже с ними). Одна дыра здесь заключается в том, что кто-то создаст FredPtr с помощью new, а затем позволит FredPtr утечь (в худшем случае это утечка, что плохо, но обычно немного лучше, чем висячий указатель). Эту дыру можно закрыть, объявив FredPtr::operator new() закрытым, что не позволит кому-либо сказать new FredPtr(). Еще одна дыра здесь заключается в том, что если кто-то создает локальный объект FredPtr, затем берет адрес этого FredPtr и передает FredPtr*. Если бы этот FredPtr* жил дольше, чем FredPtr, у вас мог бы появиться висячий указатель – жуть. Эту дыру можно закрыть, не позволяя людям брать адрес FredPtr (путем перегрузки FredPtr::operator&() как закрытого) с соответствующей потерей функциональности. Но даже если бы вы всё это сделали, они всё равно могли бы создать FredPtr&, который почти так же опасен, как FredPtr*, просто сделав следующее: FredPtr p; ... FredPtr& q = p; (или передав FredPtr& кому-то другому).

И даже если мы закроем все эти дыры, в C++ есть замечательные элементы синтаксиса, называемые приведением указателей. Используя одно или два приведения указателя, достаточно мотивированный программист обычно может создать дыру, достаточно большую, чтобы через нее проехал пресловутый грузовик. (Между прочим, приведение указателей – это зло.)

Итак, вот уроки, которые можно извлечь из этого: (а) вы не можете предотвратить шпионаж, как бы сильно вы ни старались, и (б) вы можете легко предотвратить ошибки.

Поэтому я рекомендую довольствоваться «низко висящими фруктами»: используйте простые в создании и использовании механизмы, предотвращающие ошибки, и не пытайтесь предотвратить шпионаж. Вы не добьетесь успеха, и даже если вы это сделаете, это (вероятно) будет стоить вам больше, чем оно реально стоит.

Итак, если мы не можем использовать сам язык C++ для предотвращения шпионажа, есть ли другие способы сделать это? Да. Я лично использую для этого старомодные обзоры кода. А поскольку методы шпионажа обычно включают в себя странный синтаксис и/или использование приведения указателей и объединений, вы можете использовать подобный инструмент, чтобы найти большинство «горячих точек».


Могу ли я использовать в C++ сборщик мусора?

Да.

Если вам нужна автоматическая сборка мусора, для C++ существуют хорошие коммерческие и общедоступные сборщики мусора. C++ отлично подходит для приложений, где предпочтительно использование сборки мусора, а его производительность выгодно отличается от других языков со сборкой мусора. Смотрите обсуждение автоматической сборки мусора в C++ в книге «Язык программирования C++» (4-е издание). Также смотрите сайт о сборке мусора на языках C и C++ от Hans-J. Boehm.

Кроме того, C++ поддерживает методы программирования, которые позволяют управлять памятью безопасно и неявно без сборщика мусора. Сборка мусора полезна для конкретных нужд, например, внутри реализации структур данных без блокировок, чтобы избежать проблем ABA, но не как универсальный способ обработки по умолчанию для управления ресурсами. Мы не говорим, что сборщик мусора бесполезен, просто во многих ситуациях есть лучшие подходы.

C++ 11 предлагает GC ABI.

По сравнению с методами «умного указателя» эти два вида метода сборки мусора:

  • менее портатируемы;
  • обычно более эффективны (особенно при небольшом среднем размере объекта или в многопоточных средах);
  • способны обрабатывать «циклы» в данных (методы подсчета ссылок обычно «дают утечку», если структуры данных могут образовывать цикл);
  • иногда «дают утечку» других объектов (поскольку сборщики мусора обязательно консервативны, они иногда видят случайный битовый шаблон, который кажется указателем на выделение памяти, особенно если выделение велико; это может привести к утечке выделения памяти);
  • лучше работают с существующими библиотеками (поскольку умные указатели должны использоваться явно, их может быть сложно интегрировать с существующими библиотеками)

Что за два типа сборщиков мусора для C++?

В общем, для C++ существует два вида сборщиков мусора:

  1. Консервативные сборщики мусора. Они почти ничего не знают о структуре стека или объектов C++ и просто ищут битовые шаблоны, которые кажутся указателями. На практике кажется, что они работают как с кодом C, так и с кодом C++, особенно когда средний размер объекта невелик. Вот несколько примеров в алфавитном порядке:
  2. Гибридные сборщики мусора. Обычно они сканируют стек консервативно, но требуют, чтобы программист предоставил информацию о макете для объектов кучи. Это требует от программиста дополнительной работы, но может привести к повышению производительности. Вот несколько примеров в алфавитном порядке:
    • Attardi and Flagella’s CMM
    • Bartlett’s mostly copying collector

Поскольку сборщики мусора для C++ обычно консервативны, они иногда могут давать утечки, если битовый шаблон «выглядит» как указатель на неиспользуемый блок. Кроме того, они иногда путаются, когда указатели на блок фактически указывают за пределы продолжения блока (что незаконно, но некоторые программисты просто должны выйти за рамки дозволенного; вздох), и (редко) когда указатель скрыт при оптимизации компилятора. На практике эти проблемы обычно не являются серьезными, однако предоставление сборщику подсказок о расположении объектов может иногда решить эти проблемы.


Где я могу получить дополнительную информацию о сборщиках мусора для C++?

Для получения дополнительной информации смотрите FAQ по сборщикам мусора.


Что такое auto_ptr, и почему нет auto_array?

Теперь он пишется как unique_ptr, который поддерживает как отдельные объекты, так и массивы.

auto_ptr – это старый стандартный умный указатель, который устарел и сохраняется в стандарте только для обратной совместимости со старым кодом. Его не следует использовать в новом коде.

Теги

C++ / CppFAQfree()malloc()new / deleterealloc()Высокоуровневые языки программированияМногомерный массивПрограммированиеУказатель / Pointer (программирование)Управление памятьюЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.