Почему бы не использовать указатели для всего в C++?


предположим, что я определяю класс:

class Pixel {
    public:
      Pixel(){ x=0; y=0;};
      int x;
      int y;
}

затем написать код, используя его. Зачем мне делать следующее?

Pixel p;
p.x = 2;
p.y = 5;

исходя из мира Java я всегда пишу:

Pixel* p = new Pixel();
p->x = 2;
p->y = 5;

Они в принципе делают то же самое, верно? Один находится в стеке, а другой-в куче, поэтому мне придется удалить его позже. Есть ли принципиальная разница между ними? Почему я должен предпочесть одно другому?

23 73

23 ответа:

да, один находится в стеке, другой в куче. Есть два важных отличия:

  • во-первых, очевидный и менее важный: распределение кучи происходит медленно. Распределение стека происходит быстро.
  • во-вторых, и гораздо важнее RAII. Поскольку версия, выделенная стеком, автоматически очищается, это полезное. Его деструктор вызывается автоматически, что позволяет гарантировать выделение любых ресурсов класс очищаются. Это существенно, как вы избегаете утечки памяти в C++. Вы избегаете их, никогда не называя delete себя, вместо того, чтобы обернуть его в стек выделенных объектов, которые вызывают delete внутренне, обычно в деструкторе. Если вы попытаетесь вручную отслеживать все распределения и вызвать delete в нужное время я Гарантирую Вам, что у вас будет по крайней мере утечка памяти на 100 строк кода.

в качестве небольшого примера рассмотрим это код:

class Pixel {
public:
  Pixel(){ x=0; y=0;};
  int x;
  int y;
};

void foo() {
  Pixel* p = new Pixel();
  p->x = 2;
  p->y = 5;

  bar();

  delete p;
}

довольно невинный код, верно? Мы создаем пиксель, затем вызываем некоторую несвязанную функцию, а затем удаляем пиксель. Есть ли утечка памяти?

и ответ "возможно". Что произойдет, если bar выдает исключение? delete никогда не вызывается, пиксель никогда не удаляется, и мы утечка памяти. Теперь рассмотрим это:

void foo() {
  Pixel p;
  p.x = 2;
  p.y = 5;

  bar();
}

это не утечка памяти. Конечно, в этом простом случае, все находится в стеке, поэтому он очищается автоматически, но даже если Pixel класс сделал динамическое распределение внутри, что тоже не будет протекать. Элемент Pixel класс будет просто дан деструктор, который удаляет его, и этот деструктор будет вызываться независимо от того, как мы оставляем

они не совпадают, пока вы не добавите удаление.
Ваш пример слишком тривиален, но деструктор может фактически содержать код, который выполняет некоторую реальную работу. Это называется RAII.

так что добавьте удаление. Убедитесь, что это происходит даже при распространении исключений.

Pixel* p = NULL; // Must do this. Otherwise new may throw and then
                 // you would be attempting to delete an invalid pointer.
try
{
    p = new Pixel(); 
    p->x = 2;
    p->y = 5;

    // Do Work
    delete p;
}
catch(...)
{
    delete p;
    throw;
}

если вы выбрали что-то более интересное, как файл (который является ресурсом, который должен быть закрыт). Затем сделайте это правильно в Java с указателями, которые вам нужно сделать этот.

File file;
try
{
    file = new File("Plop");
    // Do work with file.
}
finally
{
    try
    {
        file.close();     // Make sure the file handle is closed.
                          // Oherwise the resource will be leaked until
                          // eventual Garbage collection.
    }
    catch(Exception e) {};// Need the extra try catch to catch and discard
                          // Irrelevant exceptions. 

    // Note it is bad practice to allow exceptions to escape a finally block.
    // If they do and there is already an exception propagating you loose the
    // the original exception, which probably has more relevant information
    // about the problem.
}

тот же код в C++

std::fstream  file("Plop");
// Do work with file.

// Destructor automatically closes file and discards irrelevant exceptions.

хотя люди упоминают скорость (из-за поиска/выделения памяти в куче). Лично для меня это не решающий фактор (распределители очень быстры и оптимизированы для использования в C++ небольших объектов, которые постоянно создаются/уничтожаются).

основная причина для меня-это время жизни объекта. Локально определенный объект имеет очень специфическое и четко определенное время жизни, а деструктор гарантированный быть вызванным в конце (и таким образом смогите иметь специфические побочные эффекты). С другой стороны, указатель управляет ресурсом с динамическим сроком службы.

основное различие между C++ и Java:

понятие того, кто владеет указателем. Владелец несет ответственность за удаление объекта в соответствующее время. Вот почему вы очень редко видите raw указатели, как это в реальных программах (так как нет информации о собственности, связанной с а raw указатель). Вместо этого указатели обычно завернуты в смарт-указатели. Интеллектуальный указатель определяет семантику того, кто владеет памятью и, следовательно, кто отвечает за ее очистку.

пример:

 std::auto_ptr<Pixel>   p(new Pixel);
 // An auto_ptr has move semantics.
 // When you pass an auto_ptr to a method you are saying here take this. You own it.
 // Delete it when you are finished. If the receiver takes ownership it usually saves
 // it in another auto_ptr and the destructor does the actual dirty work of the delete.
 // If the receiver does not take ownership it is usually deleted.

 std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr
 // A shared ptr has shared ownership.
 // This means it can have multiple owners each using the object simultaneously.
 // As each owner finished with it the shared_ptr decrements the ref count and 
 // when it reaches zero the objects is destroyed.

 boost::scoped_ptr<Pixel>  p(new Pixel);
 // Makes it act like a normal stack variable.
 // Ownership is not transferable.

есть и другие.

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

исходя из фона Java, вы не можете быть полностью готовы к тому, сколько C++ вращается вокруг отслеживания того, что было выделено, и кто несет ответственность за его освобождение.

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

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

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

  • быстрее
  • мне не нужно беспокоиться об освобождении памяти
  • p будет допустимым объектом для всей текущей области

" Почему бы не использовать указатели для всего в C++"

один простой ответ - потому что это становится огромной проблемой управления памятью - выделение и удаление/изменение.

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

Это только первое, что я хотел бы сказать по этому вопросу.

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

код:

Pixel p;
p.x = 2;
p.y = 5;

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

использование new требует всех затрат на управление памятью.

вопрос тогда становится-вы хотите использовать пространство стека или кучи для ваших данных. Стековые (или локальные) переменные, такие как "p", не требуют разыменования, тогда как использование new добавляет слой косвенности.

Да, сначала это имеет смысл, исходя из фона Java или C#. Это не кажется большим делом, чтобы помнить, чтобы освободить память, которую вы выделили. Но когда ты получишь свою первую утечку памяти, ты будешь чесать голову, потому что ты поклялся, что освободил все. Тогда во второй раз это произойдет, а в третий вы получите еще больше разочарования. Наконец, после шести месяцев головных болей из-за проблем с памятью вы начнете уставать от этого, и эта выделенная стеком память будет начинают выглядеть все более и более привлекательным. Как красиво и чисто-просто положите его в стопку и забудьте об этом. Довольно скоро вы будете использовать стек в любое время вы можете уйти с ним.

но -- нет никакой замены для этого опыта. Мой совет? А пока попробуй по-своему. Вот увидишь.

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

Я бы рекомендовал заглянуть в boost умные указатели библиотека для ваших указателей.

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

  {   // block of code that uses file
      File aFile("file.txt");
      ...
  }    // File destructor fires when file goes out of scope, closing the file
  aFile // can't access outside of scope (compiler error)

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

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

эта концепция известна как RAII -- распределение ресурсов является инициализацией и может значительно улучшить вашу способность работать с ресурсами приобретение и утилизация.

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

class Rectangle {
    Pixel top_left;
    Pixel bottom_right;
}

Rectangle r1; // Pixel is allocated on the stack
Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap

основные преимущества переменных стека:

  • можно использовать RAII pattern для управления объектами. Как только объект выходит из области видимости, вызывается деструктор. Вроде как" Использование " шаблона в C#, но автоматически.
  • нет никакой возможности null ссылка.
  • вам не нужно беспокоиться о ручном управлении памятью объекта.
  • это вызывает меньше выделений памяти. Выделение памяти, особенно небольших, вероятно, будет медленнее в C++ , чем Java.

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

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

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

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

Я бы сказал, что это много о Дело вкуса. Если вы создаете интерфейс, позволяющий методам принимать указатели вместо ссылок, вы позволяете вызывающему объекту передавать nil. Поскольку вы разрешаете пользователю переходить в nil, пользователь будет пройти в ноль.

с вас должны спросить себя: "что произойдет, если этот параметр равен nil?", вы должны кодировать более защищенно, заботясь о нулевых проверках все время. Это говорит за использование ссылок.

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

проблема не в указателях per se (помимо введения NULL указатели), но делает управление памятью вручную.

забавная часть, конечно, заключается в том, что каждый учебник Java, который я видел, упоминал, что сборщик мусора-это такая крутая горячность, потому что вам не нужно помнить, чтобы позвонить delete, когда на практике C++ требует только delete когда вы называете newdelete[] когда вы называете new[]).

используйте указатели и динамически выделенные объекты только тогда, когда это необходимо. По возможности используйте статически выделенные (глобальные или стековые) объекты.

  • статические объекты быстрее (нет новых/удалить, ни косвенного доступа к ним)
  • нет объекта жизни, чтобы волноваться о
  • меньше нажатий клавиш более читаемый
  • гораздо надежнее. Каждый "- > " - это потенциальный доступ к нулевой или недопустимой памяти

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

почему бы не использовать указатели на все?

они медленнее.

оптимизация компилятора не будет столь эффективной с помощью symantics доступа к указателю, вы можете прочитать об этом на любом количестве веб-сайтов, но вот приличный pdf от Intel.

Проверьте страницы, 13,14,17,28,32,36;

обнаружение ненужной памяти ссылки в цикле обозначения:

for (i = j + 1; i <= *n; ++i) { 
X(i) -= temp * AP(k); } 

обозначение для цикла границы содержит указатель или память ссылка. Компилятор не имеет любые средства, чтобы предсказать, является ли значение ссылка на указатель n выполняется изменено с помощью итераций цикла некоторыми другие задания. При этом используется петля перезагрузить значение, на которое ссылается Н для каждой итерации. Генератор кода двигатель также может отказать в планирование программное обеспечение конвейерный цикл, когда потенциальный найдено сглаживание указателя. Поскольку значение, на которое ссылается указатель N не в anging в петли и инвариантный к индексу цикла, загрузка *n s для перевозки за пределами границ цикла для более простое планирование и указатель устранение неоднозначности.

... ряд вариаций на эту тему....

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

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

В C++ вы можете ссылаться на объекты с помощью указателей (Foo *) и справки (Foo &). Везде, где это возможно, я использую ссылку вместо указателя. Например, при передаче по ссылке в функцию/метод, использование ссылок позволяет коду (надеюсь) сделаем следующие предположения:

  • объект, на который ссылаются, не принадлежит функции / методу, поэтому не должен delete объект. Это как сказать:"Вот, используйте эти данные, но верните их, когда закончите".
  • ссылки на нулевые указатели менее вероятны. Можно передать нулевую ссылку, но, по крайней мере, это не будет ошибкой функции/метода. Ссылка не может быть переназначена на новый адрес указателя, поэтому ваш код не мог случайно переназначить его на NULL или какой-либо другой недопустимый адрес указателя, вызывая ошибку страницы.

вопрос в том, почему вы используете указатели для всего? Стек выделенных объектов не только безопаснее и быстрее создавать, но есть еще меньше ввода и код выглядит лучше.

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

Pixel p;

будет использовать 8 байт, и

Pixel* p = new Pixel();

будет использовать 12 байт, увеличение на 50%. Это не звучит как много, пока вы не выделите достаточно для изображения 512x512. Тогда вы говорите 2 МБ вместо 3 МБ. Это игнорирует накладные расходы на управление кучей со всеми этими объектами на них.

объекты, созданные в стеке, создаются быстрее, чем объекты, выделенные.

Почему?

потому что выделение памяти (с диспетчером памяти по умолчанию) занимает некоторое время (чтобы найти какой-то пустой блок или даже выделить этот блок).

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

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

Я сам не усложнять проблему с помощью смарт-указатели.

OTOH я немного поработал во встроенном поле и создание объектов в стеке не очень умно (так как стек, выделенный для каждой задачи/потока, не очень большой - вы должны быть осторожны).

Так что это вопрос выбора и ограничений, нет ответа, чтобы соответствовать им всем.

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

в принципе, когда вы используете необработанные указатели, у вас нет RAII.

Это сильно смутило меня, когда я был новым программистом на C++ (и это был мой первый язык). Есть много очень плохих учебников C++, которые, как правило, попадают в одну из двух категорий: учебники "C / C++", что на самом деле означает, что это учебник C (возможно, с классами), и учебники C++, которые думают, что C++ - это Java с delete.

Я думаю, что мне потребовалось около 1-1,5 лет (по крайней мере), чтобы ввести "new" в любом месте моего кода. Я часто использовал контейнеры STL, такие как vector, которые позаботились это для меня.

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

практически для любой ситуации там, где это не сработает (например, если вы рискуете исчерпать пространство стека), вы, вероятно, должны использовать один из стандартных контейнеров в любом случае: std::string, std::vector и std::map-это три, которые я использую чаще всего, но std::deque и std::list также довольно распространены. Остальные (такие вещи, как std::set и нестандартные трос) не используются так много, но ведут себя аналогично. Все они выделяются из свободного хранилища (язык C++ для "кучи" на некоторых других языках), см.: C++ STL вопрос: распределители

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