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

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

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;

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

Ответ 1

Да, один находится в стеке, другой - в куче. Существуют два важных отличия:

  • Во-первых, очевидное и менее важное: распределение кучи происходит медленно. Распределение стеков происходит быстро.
  • Во-вторых, и гораздо важнее RAII. Поскольку выбранная стеком версия автоматически очищается, она полезна. Его деструктор автоматически вызывается, что позволяет гарантировать, что все ресурсы, выделенные классом, будут очищены. Это очень важно, как вы избегаете утечек памяти на С++. Вы избегаете их, никогда не вызывая 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 просто получил бы деструктор, который удалит его, и этот деструктор будет вызван независимо от того, как мы оставим функцию foo. Даже если мы оставим его, потому что bar выбрасывает исключение. Следующий, слегка надуманный пример показывает это:

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

  ~Pixel() {
    delete x;
    delete y;
  }
};

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

  bar();
}

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

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

Ответ 2

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

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

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

Тот же код в С++

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

// Destructor automatically closes file and discards irrelevant exceptions.

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

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

Основное отличие между С++ и Java:

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

Примеры:

 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.

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

Ответ 6

Код:

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

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

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

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

Ответ 7

Хорошим общим правилом является НИКОГДА не использовать новое, если вам не обязательно. Ваши программы будут легче поддерживать и меньше подвергать ошибкам, если вы не используете новые, так как вам не нужно беспокоиться о том, где их очистить.

Ответ 8

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

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

Ответ 9

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

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

Ответ 10

Лучшая причина не для нового - это то, что вы можете очень детерминированную очистку, когда все в стеке. В случае с 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 - Инициализация распределения ресурсов, и она может значительно улучшить вашу способность справляться с приобретением и удалением ресурсов.

Ответ 11

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

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 для управления объектами. Как только объект выходит из области действия, он вызывается деструктором. Вид, как "использование" шаблона в С#, но автоматический.
  • Нет возможности нулевой ссылки.
  • Вам не нужно беспокоиться о ручном управлении памятью объекта.
  • Это приводит к меньшему распределению памяти. Распределение памяти, особенно малые, скорее всего, будет более медленным в С++, чем Java.

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

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

Ответ 12

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

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

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

Ответ 13

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

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

Ответ 14

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

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

Ответ 15

Используйте указатели и динамически распределенные объекты ТОЛЬКО КОГДА ВЫ ДОЛЖНЫ. Используйте по возможности статически распределенные (глобальные или стек) объекты.

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

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

Ответ 16

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

Они медленнее.

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

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

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

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

Обозначения для границ цикла содержит указатель или память Справка. У компилятора нет любые средства для прогнозирования того, является ли значение ссылка на указатель n является изменено с помощью итераций цикла некоторыми другое назначение. Это использует цикл для перезагрузки значения, на которое ссылается n для каждой итерации. Генератор кода двигатель также может запретить планирование программный конвейерный контур, когда потенциал найдено наложение указателя. Поскольку значение, на которое ссылается указатель n, не является поворот в петле, и это инвариантный к индексу цикла, загрузка * n s, подлежащих перевозке вне границ цикла для более простое планирование и указатель неоднозначности.

... несколько вариантов этой темы....

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

Ответ 17

Взгляд на вопрос под другим углом...

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

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

Ответ 18

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

Ответ 19

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

Pixel p;

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

Pixel* p = new Pixel();

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

Ответ 20

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

Почему?

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

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

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

Я сам не затруднял бы проблему с помощью интеллектуальных указателей.

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

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

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

Ответ 21

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

Ответ 22

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

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

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

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

Ответ 23

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