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


1603

Я пришел из Java-фона и начал работать с объектами в C ++. Но одна вещь, которая пришла мне в голову, это то, что люди часто используют указатели на объекты, а не на сами объекты, например, это объявление:

Object *myObject = new Object;

скорее, чем:

Object myObject;

Или вместо использования функции, скажем testFunc()так:

myObject.testFunc();

мы должны написать:

myObject->testFunc();

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


406
Слава вам за то, что вы ставите под сомнение эту практику, а не просто следуете ей В большинстве случаев указатели используются слишком часто.
Лучиан Григоре

120
Если вы не видите причин использовать указатели, не надо. Предпочитаю объекты. Предпочитайте объекты перед unique_ptr перед shared_ptr перед необработанными указателями.
Штефан

113
примечание: в Java все (кроме основных типов) является указателем. поэтому вам лучше спросить обратное: зачем мне простые предметы?
Кароли Хорват

119
Обратите внимание, что в Java указатели скрыты синтаксисом. В C ++ различие между указателем и не указателем делается явным в коде. Java везде использует указатели.
Даниэль Мартин

216
Закрыть как слишком широкий ? Шутки в сторону? Пожалуйста, обратите внимание, что этот способ программирования на Java ++ очень распространен и является одной из наиболее важных проблем в сообществе C ++ . К нему следует относиться серьезно.
Manu343726

Ответы:


1576

Очень жаль, что вы так часто видите динамическое распределение. Это просто показывает, сколько есть плохих программистов на C ++.

В некотором смысле, у вас есть два вопроса, объединенные в один. Во-первых, когда мы должны использовать динамическое распределение (использование new)? Во-вторых, когда мы должны использовать указатели?

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

Динамическое распределение

В своем вопросе вы продемонстрировали два способа создания объекта. Основным отличием является срок хранения объекта. При выполнении Object myObject;внутри блока объект создается с автоматическим сроком хранения, что означает, что он будет автоматически уничтожен, когда выйдет из области видимости. Когда вы это делаете new Object(), объект имеет динамическую длительность хранения, что означает, что он остается в живых, пока вы явно deleteне сделаете это. Вы должны использовать динамическую продолжительность хранения только тогда, когда вам это нужно. То есть вы должны всегда предпочитать создавать объекты с автоматической продолжительностью хранения, когда можете .

Основные две ситуации, в которых вам может потребоваться динамическое размещение:

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

Когда вам абсолютно необходимо динамическое распределение, вы должны инкапсулировать его в умный указатель или какой-то другой тип, который выполняет RAII (например, стандартные контейнеры). Умные указатели обеспечивают семантику владения динамически размещенными объектами. Взгляните на std::unique_ptrи std::shared_ptr, например. Если вы используете их надлежащим образом, вы можете почти полностью избежать выполнения своего собственного управления памятью (см. Правило нуля ).

указатели

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

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

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

  3. Вы хотите представить, что объект является необязательным , позволяя nullptrпередавать его, когда объект опущен. Если это аргумент, вы должны использовать аргументы по умолчанию или перегрузки функций. В противном случае вам лучше использовать тип, который инкапсулирует это поведение, например std::optional(используйте в C ++ 17 - с более ранними стандартами C ++, используйте boost::optional).

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

  5. Вам необходимо взаимодействовать с библиотекой C или библиотекой в стиле C. На этом этапе вы вынуждены использовать сырые указатели. Лучшее, что вы можете сделать, это убедиться, что ваши сырые указатели теряются только в самый последний момент. Вы можете получить необработанный указатель из умного указателя, например, используя его getфункцию-член. Если библиотека выполняет для вас какое-то выделение, которое она ожидает от вас освободить с помощью дескриптора, вы часто можете заключить этот дескриптор в интеллектуальный указатель с помощью специального средства удаления, которое будет соответствующим образом освобождать объект.


83
«Вам нужен объект, чтобы пережить текущую область». - Дополнительное примечание по этому поводу: бывают случаи, когда вам кажется, что вам нужен объект, чтобы пережить текущую область, но на самом деле это не так. Например, если вы поместите свой объект в вектор, объект будет скопирован (или перемещен) в вектор, и исходный объект можно будет уничтожить, когда закончится его область действия.

25
Помните, что s / copy / move / во многих местах сейчас. Возвращение объекта определенно не подразумевает движение. Следует также отметить, что доступ к объекту через указатель ортогонален тому, как он был создан.
Щенок

15
Я пропускаю явную ссылку на RAII на этот ответ. C ++ - это все (почти все) об управлении ресурсами, а RAII - это способ сделать это на C ++ (и главная проблема, которую генерируют необработанные указатели: Breaking RAII)
Manu343726

11
Умные указатели существовали до C ++ 11, например, boost :: shared_ptr и boost :: scoped_ptr. Другие проекты имеют собственный эквивалент. Вы не можете получить семантику перемещения, и назначение std :: auto_ptr является ошибочным, поэтому C ++ 11 улучшает ситуацию, но совет все же хорош. (И грустный придираться, это не достаточно , чтобы иметь доступ к в C ++ 11 компилятора, это необходимо , чтобы все компиляторы вы могли бы , возможно , хотите , чтобы ваш код для работы с поддержкой C ++ 11. Да, Oracle Solaris студия, я смотрит на тебя.)
armb

7
@ MDMoore313 Вы можете написатьObject myObject(param1, etc...)
user000001

174

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

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

class Base { ... };
class Derived : public Base { ... };

void fun(Base b) { ... }
void gun(Base* b) { ... }
void hun(Base& b) { ... }

Derived d;
fun(d);    // oops, all Derived parts silently "sliced" off
gun(&d);   // OK, a Derived object IS-A Base object
hun(d);    // also OK, reference also doesn't slice

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

Base b;
fun(b);  // copies b, potentially expensive 
gun(&b); // takes a pointer to b, no copying
hun(b);  // regular syntax, behaves as a pointer

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

Приобретение ресурса . Создание указателя на ресурс с помощью newоператора является антишаблоном в современном C ++. Используйте специальный класс ресурсов (один из стандартных контейнеров) или умный указатель ( std::unique_ptr<>или std::shared_ptr<>). Рассматривать:

{
    auto b = new Base;
    ...       // oops, if an exception is thrown, destructor not called!
    delete b;
}

против

{
    auto b = std::make_unique<Base>();
    ...       // OK, now exception safe
}

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

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


17
«Создание указателя на ресурс с помощью оператора new является анти-шаблоном». Я думаю, вы могли бы даже улучшить то, что наличие необработанного указателя, владеющего чем-то, является анти-шаблоном . Не только создание, но и передача необработанных указателей в качестве аргументов или возвращаемых значений, подразумевающих передачу права собственности. ИМХО не рекомендуется, так как unique_ptrсемантика / move
dyp

1
@dyp tnx, обновленный и ссылка на часто задаваемые вопросы C ++ по этой теме.
TemplateRex

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

2
@JamesKanze Я не имел в виду, что умные указатели должны использоваться везде, только для владения, а также, что необработанные указатели не должны использоваться для владения, а только для представлений.
TemplateRex

2
@TemplateRex Это кажется немного глупым, учитывая, что это hun(b)также требует знания подписи, если вы не уверены, что не указали неправильный тип до компиляции. Хотя проблема со ссылками обычно не обнаруживается во время компиляции и требует больше усилий для отладки, если вы проверяете подпись, чтобы убедиться в правильности аргументов, вы также сможете увидеть, являются ли какие-либо из аргументов ссылочными. таким образом, бит ссылки становится чем-то без проблем (особенно при использовании IDE или текстовых редакторов, которые показывают сигнатуру выбранных функций). Также const&.
JAB

130

Есть много отличных ответов на этот вопрос, включая важные случаи использования предварительных объявлений, полиморфизма и т. Д., Но я чувствую, что часть «души» вашего вопроса не получила ответа, а именно, что означают различные синтаксисы в Java и C ++.

Давайте рассмотрим ситуацию, сравнивая два языка:

Ява:

Object object1 = new Object(); //A new object is allocated by Java
Object object2 = new Object(); //Another new object is allocated by Java

object1 = object2; 
//object1 now points to the object originally allocated for object2
//The object originally allocated for object1 is now "dead" - nothing points to it, so it
//will be reclaimed by the Garbage Collector.
//If either object1 or object2 is changed, the change will be reflected to the other

Ближайший эквивалент этому:

C ++:

Object * object1 = new Object(); //A new object is allocated on the heap
Object * object2 = new Object(); //Another new object is allocated on the heap
delete object1;
//Since C++ does not have a garbage collector, if we don't do that, the next line would 
//cause a "memory leak", i.e. a piece of claimed memory that the app cannot use 
//and that we have no way to reclaim...

object1 = object2; //Same as Java, object1 points to object2.

Давайте посмотрим альтернативный путь C ++:

Object object1; //A new object is allocated on the STACK
Object object2; //Another new object is allocated on the STACK
object1 = object2;//!!!! This is different! The CONTENTS of object2 are COPIED onto object1,
//using the "copy assignment operator", the definition of operator =.
//But, the two objects are still different. Change one, the other remains unchanged.
//Also, the objects get automatically destroyed once the function returns...

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

Ява:

int object1; //An integer is allocated on the stack.
int object2; //Another integer is allocated on the stack.
object1 = object2; //The value of object2 is copied to object1.

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

Вернемся к сути - эта Object * object = new Object()конструкция на самом деле является самой близкой к типичной семантике Java (или C #).


7
Object2 is now "dead"Я думаю, что вы имеете в виду myObject1или точнее the object pointed to by myObject1.
Clément

2
Верно! Перефразировано немного.
Gerasimos R

2
Object object1 = new Object(); Object object2 = new Object();очень плохой код Второй новый или второй конструктор Object может выдать, и теперь object1 просочился. Если вы используете raw news, вы должны обернуть newобъекты ed в оболочки RAII как можно скорее.
PSkocik

8
Действительно, было бы, если бы это была программа, и больше ничего не происходило вокруг нее. К счастью, это всего лишь фрагмент объяснения, показывающий, как ведет себя указатель в C ++ - и одно из немногих мест, где объект RAII не может быть заменен необработанным указателем, изучает и изучает необработанные указатели ...
Gerasimos R

80

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


7
это действительно добавляет к смеси полезной информации, так что рад, что вы сделали это ответ!
TemplateRex

3
std :: shared_ptr <T> также работает с предварительными объявлениями T. (std :: unique_ptr <T> нет )
berkus

13
@berkus: std::unique_ptr<T>работает с предварительными декларациями T. Вам просто нужно убедиться, что когда std::unique_ptr<T>вызывается деструктор , Tэто полный тип. Обычно это означает, что ваш класс, содержащий объявление, std::unique_ptr<T>объявляет свой деструктор в заголовочном файле и реализует его в файле cpp (даже если реализация пуста).
Дэвид Стоун

Будут ли модули это исправлять?
Тревор Хикки

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

79

Предисловие

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

Вперед мы идем

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

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

{
    std::string s;
}
// s is destroyed here

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

{
    std::string* s = new std::string;
}
delete s; // destructor called

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

Преимущества динамического распределения

1. Вам не нужно заранее знать размер массива

Одна из первых проблем, с которыми сталкиваются многие программисты на С ++, заключается в том, что когда они принимают произвольные данные от пользователей, вы можете выделить фиксированный размер только для переменной стека. Вы также не можете изменить размер массивов. Например:

char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow

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

int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];

Примечание : одна ошибка, которую допускают многие новички, - использование массивов переменной длины. Это расширение GNU, а также расширение в Clang, поскольку они отражают многие расширения GCC. Поэтому int arr[n]не следует полагаться на следующее.

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

2. Массивы не указатели

Какую пользу вы спрашиваете? Ответ станет ясен, как только вы поймете путаницу / миф о массивах и указателях. Обычно считается, что они одинаковы, но это не так. Этот миф проистекает из того факта, что указатели могут быть подписаны так же, как массивы, и из-за того, что массивы распадаются на указатели на верхнем уровне в объявлении функции. Однако, когда массив распадается на указатель, указатель теряет свою sizeofинформацию. Таким образом, sizeof(pointer)будет указан размер указателя в байтах, который обычно составляет 8 байтов в 64-битной системе.

Вы не можете назначать массивы, только инициализировать их. Например:

int arr[5] = {1, 2, 3, 4, 5}; // initialization 
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
                             // be given by the amount of members in the initializer  
arr = { 1, 2, 3, 4, 5 }; // ERROR

С другой стороны, вы можете делать что угодно с указателями. К сожалению, поскольку в Java и C # различие между указателями и массивами выполняется вручную, новички не понимают разницы.

3. Полиморфизм

Java и C # имеют средства, которые позволяют вам рассматривать объекты как другие, например, используя asключевое слово. Поэтому, если кто-то хочет рассматривать Entityобъект как Playerобъект, это можно сделать. Player player = Entity as Player;Это очень полезно, если вы собираетесь вызывать функции в однородном контейнере, который должен применяться только к определенному типу. Функциональность может быть достигнута аналогичным образом ниже:

std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
     auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
     if (!test) // not a triangle
        e.GenericFunction();
     else
        e.TriangleOnlyMagic();
}

Так, скажем, если бы только у треугольников была функция Rotate, это было бы ошибкой компилятора, если бы вы попытались вызвать ее для всех объектов класса. Используя dynamic_cast, вы можете моделироватьas ключевое слово. Для ясности, если приведение не выполнено, возвращается неверный указатель. Таким образом, !testэто, по сути, сокращение для проверки, testявляется ли NULL или недопустимый указатель, что означает, что приведение не удалось.

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

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

  • Это подвержено ошибкам. Ручное распределение памяти опасно, и вы склонны к утечкам. Если вы не разбираетесь в использовании отладчика илиvalgrind (средством утечки памяти), вы можете вырвать волосы из головы. К счастью, идиомы RAII и умные указатели немного смягчают это, но вы должны быть знакомы с такими практиками, как Правило Трех и Правило Пяти. Здесь много информации, и новички, которые не знают или не заботятся, попадут в эту ловушку.

  • Это не обязательно. В отличие от Java и C #, где использование newключевого слова везде идиоматично , в C ++ его следует использовать только при необходимости. Общая фраза гласит: все выглядит как гвоздь, если у вас есть молоток. В то время как новички, начинающие с C ++, боятся указателей и учатся использовать переменные стека по привычке, программисты на Java и C # начинают с использования указателей, не понимая этого! Это буквально ступает не на ту ногу. Вы должны отказаться от всего, что вы знаете, потому что синтаксис - это одно, а изучение языка - это другое.

1. (N) RVO - Aka, (Named) Оптимизация возвращаемого значения

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

Если вы используете указатели, (N) RVO НЕ происходит. Более выгодно и менее подвержено ошибкам использовать преимущество (N) RVO, а не возвращать или передавать указатели, если вы беспокоитесь об оптимизации. Утечка ошибок может произойти, если вызывающая функция ответственна за deleteдинамически размещенный объект и тому подобное. Может быть трудно отследить право собственности на объект, если указатели передаются как горячий картофель. Просто используйте переменные стека, потому что это проще и лучше.


«Итак! Test - это, по сути, сокращение для проверки, имеет ли тест значение NULL или недопустимый указатель, что означает, что приведение не выполнено». Я думаю, что это предложение должно быть переписано для ясности.
berkus

4
«Явная машина Java хотела бы, чтобы вы верили» - возможно, в 1997 году, но теперь это анахронизм, больше нет мотивации сравнивать Java с C ++ в 2014 году.
Мэтт Р

15
Старый вопрос, но в сегменте кода { std::string* s = new std::string; } delete s; // destructor called.... конечно, это deleteне будет работать, потому что компилятор не будет знать, что sбольше?
badger5000

2
Я НЕ даю -1, но я не согласен с вступительными заявлениями, как написано. Во-первых, я не согласен, что есть какая-то «шумиха» - возможно, это было около 2000 года, но теперь оба языка хорошо понятны. Во-вторых, я бы сказал, что они очень похожи - C ++ является потомком C, женатого на Simula, Java добавляет Virtual Machine, Garbage Collector, а HEAVILY сокращает функциональные возможности, а C # упрощает и вновь вводит отсутствующие функции в Java. Да, это делает шаблоны и действительное использование по-разному, но полезно понимать общую инфраструктуру / дизайн, чтобы можно было увидеть различия.
Р

1
@James Matta: Вы, конечно, правы в том, что память - это память, и они оба выделены из одной физической памяти, но следует учитывать, что очень часто можно получить лучшие характеристики производительности при работе с объектами, выделенными из стека, потому что стек - или, по крайней мере, его самые высокие уровни - очень высока вероятность того, что в кеше будут «горячие» функции при входе и выходе из функции, в то время как куча не дает такого преимущества, поэтому, если вы гоняетесь по указателю в куче, вы можете получить многократные ошибки кэша, которые Вы, вероятно , не в стеке. Но вся эта «случайность» обычно благоприятствует стеку.
Герасим R

23

C ++ дает вам три способа передачи объекта: по указателю, по ссылке и по значению. Java ограничивает вас последним (единственным исключением являются примитивные типы, такие как int, boolean и т. Д.). Если вы хотите использовать C ++ не просто как странную игрушку, вам лучше узнать разницу между этими тремя способами.

Java делает вид, что нет такой проблемы, как «кто и когда должен это уничтожить?». Ответ: Сборщик мусора, Великий и Ужасный. Тем не менее, он не может обеспечить 100% защиту от утечек памяти (да, Java может утечка памяти ). На самом деле, GC дает вам ложное чувство безопасности. Чем больше ваш внедорожник, тем дольше вы добираетесь до эвакуатора.

C ++ оставляет вас лицом к лицу с управлением жизненным циклом объекта. Что ж, есть способы справиться с этим ( семейство умных указателей , QObject в Qt и т. Д.), Но ни один из них не может быть использован способом «забыл и забыл», как GC: вы всегда должны помнить об обработке памяти. Вы должны заботиться не только об уничтожении объекта, но и об уничтожении одного и того же объекта более одного раза.

Еще не напуган? ОК: циклические ссылки - обращайтесь с ними сами, человек. И помните: убивайте каждый объект ровно один раз, нам, во время выполнения C ++, не нравятся те, кто возится с трупами, оставляя мертвых в покое.

Итак, вернемся к вашему вопросу.

Когда вы передаете свой объект по значению, а не по указателю или по ссылке, вы копируете объект (весь объект, будь то пара байтов или огромный дамп базы данных) - вы достаточно умен, чтобы избежать последнего, не так ли? не так ли?) каждый раз, когда вы делаете '='. И для доступа к членам объекта вы используете '.' (Точка).

Когда вы передаете ваш объект по указателю, вы копируете всего несколько байтов (4 в 32-битных системах, 8 в 64-битных), а именно - адрес этого объекта. И чтобы показать это всем, вы используете этот причудливый оператор «->» при доступе к участникам. Или вы можете использовать комбинацию «*» и «.».

Когда вы используете ссылки, вы получаете указатель, который претендует на значение. Это указатель, но вы получаете доступ к членам через «.».

И, чтобы поразить вас еще раз: когда вы объявляете несколько переменных, разделенных запятыми, тогда (следите за руками):

  • Тип дается каждому
  • Модификатор значения / указателя / ссылки индивидуален

Пример:

struct MyStruct
{
    int* someIntPointer, someInt; //here comes the surprise
    MyStruct *somePointer;
    MyStruct &someReference;
};

MyStruct s1; //we allocated an object on stack, not in heap

s1.someInt = 1; //someInt is of type 'int', not 'int*' - value/pointer modifier is individual
s1.someIntPointer = &s1.someInt;
*s1.someIntPointer = 2; //now s1.someInt has value '2'
s1.somePointer = &s1;
s1.someReference = s1; //note there is no '&' operator: reference tries to look like value
s1.somePointer->someInt = 3; //now s1.someInt has value '3'
*(s1.somePointer).someInt = 3; //same as above line
*s1.somePointer->someIntPointer = 4; //now s1.someInt has value '4'

s1.someReference.someInt = 5; //now s1.someInt has value '5'
                              //although someReference is not value, it's members are accessed through '.'

MyStruct s2 = s1; //'NO WAY' the compiler will say. Go define your '=' operator and come back.

//OK, assume we have '=' defined in MyStruct

s2.someInt = 0; //s2.someInt == 0, but s1.someInt is still 5 - it's two completely different objects, not the references to the same one

1
std::auto_ptrустарела, пожалуйста, не используйте его.
Нил

2
Уверен, что вы не можете иметь ссылку в качестве члена, не предоставив конструктору список инициализации, который включает переменную ссылки. (Ссылка должна быть инициализирована немедленно. Даже тело конструктора слишком поздно для ее установки, IIRC.)
cHao

20

В C ++ объекты, размещенные в стеке (используя Object object;оператор внутри блока), будут жить только в той области, в которой они объявлены. Когда блок кода завершает выполнение, объявленный объект уничтожается. Принимая во внимание, что если вы выделяете память в куче, используя Object* obj = new Object(), они продолжают жить в куче, пока вы не позвоните delete obj.

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


6
Object objне всегда в стеке - например, глобальные переменные или переменные-члены.
десять

2
@LightnessRacesinOrbit Я упоминал только об объектах, размещенных в блоке, а не о глобальных переменных и переменных-членах. Дело в том, что это было непонятно, сейчас это исправили - добавили «в блоке» в ответ. Надеюсь, что это не ложная информация сейчас :)
Karthik Kalyanasundaram

20

Но я не могу понять, почему мы должны использовать это так?

Я сравню, как это работает внутри тела функции, если вы используете:

Object myObject;

Внутри функции ваш myObject будет уничтожен, как только эта функция вернется. Так что это полезно, если вам не нужен ваш объект вне вашей функции. Этот объект будет помещен в стек текущего потока.

Если вы напишите внутри тела функции:

 Object *myObject = new Object;

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

Если вы программист на Java, то второй пример ближе к тому, как работает распределение объектов в Java. Эта строка: Object *myObject = new Object;эквивалентно Java: Object myObject = new Object();. Разница в том, что под java myObject будет собирать мусор, а под c ++ он не будет освобожден, вы должны где-то явно вызывать `delete myObject; ' в противном случае вы будете вводить утечки памяти.

Начиная с c ++ 11 вы можете использовать безопасные способы динамического распределения: new Objectпутем хранения значений в shared_ptr / unique_ptr.

std::shared_ptr<std::string> safe_str = make_shared<std::string>("make_shared");

// since c++14
std::unique_ptr<std::string> safe_str = make_unique<std::string>("make_shared"); 

Кроме того, объекты очень часто хранятся в контейнерах, таких как map-s или vector-s, они автоматически управляют временем жизни ваших объектов.


1
then myObject will not get destroyed once function endsЭто абсолютно будет.
Гонки легкости на орбите

6
В случае указателя myObjectвсе равно будет уничтожено, как и любая другая локальная переменная. Разница в том, что его значение является указателем на объект, а не на сам объект, и уничтожение тупого указателя не влияет на его объект. Так что объект переживет указанное разрушение.
cHao

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

13

Технически это проблема выделения памяти, однако здесь есть еще два практических аспекта этого. Это связано с двумя вещами: 1) Область действия: когда вы определяете объект без указателя, вы больше не сможете получить к нему доступ после того блока кода, в котором он определен, тогда как если вы определите указатель с помощью «new», то вы можете получить к нему доступ из любого места, где у вас есть указатель на эту память, пока вы не вызовете «delete» для того же указателя. 2) Если вы хотите передать аргументы функции, вы хотите передать указатель или ссылку, чтобы быть более эффективным. Когда вы передаете Object, тогда объект копируется, если это объект, который использует много памяти, это может потреблять процессор (например, вы копируете вектор, полный данных). Когда вы передаете указатель, вы просто передаете один int (в зависимости от реализации, но большинство из них - один int).

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


6

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

Еще одна вещь, которую вы упомянули в своем вопросе:

Object *myObject = new Object;

Как это работает? Он создает указатель Objectтипа, выделяет память под один объект и вызывает конструктор по умолчанию, хорошо звучит, верно? Но на самом деле это не так хорошо, если вы динамически распределяете память (используемое ключевое слово new), вам также необходимо освободить память вручную, это означает, что в коде вы должны иметь:

delete myObject;

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


И теперь некоторое введение закончено и вернемся к вопросу.

Вы можете использовать указатели вместо объектов для повышения производительности при передаче данных между функциями.

Посмотрите, у вас есть std::string(это также объект), и он содержит действительно много данных, например, большой XML, теперь вам нужно его проанализировать, но для этого у вас есть функция, void foo(...)которая может быть объявлена ​​различными способами:

  1. void foo(std::string xml); В этом случае вы скопируете все данные из вашей переменной в стек функций, это займет некоторое время, поэтому ваша производительность будет низкой.
  2. void foo(std::string* xml); В этом случае вы передадите указатель на объект с той же скоростью, что и передаваемая size_tпеременная, однако это объявление подвержено ошибкам, потому что вы можете передать NULLуказатель или неверный указатель. Указатели обычно используются, Cпотому что у них нет ссылок.
  3. void foo(std::string& xml); Здесь вы передаете ссылку, в основном это то же самое, что и передающий указатель, но компилятор делает некоторые вещи, и вы не можете передать недопустимую ссылку (на самом деле можно создать ситуацию с недопустимой ссылкой, но это обманывает компилятор).
  4. void foo(const std::string* xml); Здесь то же самое, что и второе, просто значение указателя не может быть изменено.
  5. void foo(const std::string& xml); Здесь то же самое, что и третье, но значение объекта не может быть изменено.

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


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


5

Допустим, у вас есть class Aэто содержимое. class BКогда вы захотите вызвать некоторую функцию class Bизвне, class Aвы просто получите указатель на этот класс, и вы можете делать все, что захотите, и это также изменит контекст class Bвашегоclass A

Но будьте осторожны с динамическим объектом


5

Есть много преимуществ использования указателей на объект -

  1. Эффективность (как вы уже указали). Передача объектов в функции означает создание новых копий объекта.
  2. Работа с объектами из сторонних библиотек. Если ваш объект принадлежит стороннему коду, и авторы намерены использовать свои объекты только через указатели (без конструкторов копирования и т. Д.), Единственный способ обойти этот объект - использовать указатели. Передача по значению может вызвать проблемы. (Глубокое копирование / мелкое копирование).
  3. если объект владеет ресурсом, и вы хотите, чтобы право владения не передавалось другим объектам.

3

Это было обсуждено подробно, но в Java все является указателем. Он не делает различий между распределением стека и кучи (все объекты размещаются в куче), поэтому вы не понимаете, что используете указатели. В C ++ вы можете смешивать их в зависимости от ваших требований к памяти. Производительность и использование памяти более детерминированы в C ++ (дух).


3
Object *myObject = new Object;

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

Object myObject;

Это создаст объект (myObject) автоматического типа (в стеке), который будет автоматически удален, когда объект (myObject) выйдет из области видимости.


1

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

Чтобы ответить на ваш вопрос, это просто ваши предпочтения. Я предпочитаю использовать Java-подобный синтаксис.


Хеш-таблицы? Может быть, в некоторых JVM, но не рассчитывайте на это.
Zan Lynx

Как насчет JVM, который поставляется с Java? Конечно, вы можете реализовать ВСЕ, что вы можете себе представить, как JVM, которая использует указатели напрямую, или метод, который выполняет математику указателя. Это все равно что сказать «люди не умирают от простуды» и получить ответ: «Может быть, большинство людей этого не делают, но не рассчитывают на это!» Ха ха
RioRicoRick

2
@RioRicoRick HotSpot реализует ссылки Java как родные указатели, см. Docs.oracle.com/javase/7/docs/technotes/guides/vm/… Насколько я вижу, JRockit делает то же самое. Они оба поддерживают сжатие ООП, но никогда не используют хеш-таблицы. Последствия производительности, вероятно, будут катастрофическими. Кроме того, «это просто ваши предпочтения», по-видимому, подразумевают, что оба являются просто разными синтаксисами для эквивалентного поведения, что, конечно, не так.
Макс Барракло


0

С указателями ,

  • можно напрямую общаться с памятью.

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


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

0

Одной из причин использования указателей является взаимодействие с функциями Си. Другая причина - сохранить память; Например: вместо того, чтобы передавать объект, который содержит много данных и имеет процессорно-интенсивный конструктор копирования, просто передайте указатель на объект, экономя память и скорость, особенно если вы находитесь в цикле, однако ссылка будет лучше в этом случае, если вы не используете массив в стиле C.


0

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


0

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

Class Base1 {
};

Class Derived1 : public Base1 {
};


Class Base2 {
  Base *bObj;
  virtual void createMemerObects() = 0;
};

Class Derived2 {
  virtual void createMemerObects() {
    bObj = new Derived1();
  }
};

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


-5

"Голь на выдумки хитра." Самым важным отличием, на которое я хотел бы обратить внимание, является результат моего собственного опыта программирования. Иногда вам нужно передать объекты в функции. В этом случае, если ваш объект имеет очень большой класс, то передача его в качестве объекта скопирует его состояние (которое вы, возможно, не захотите .. И МОЖЕТ БЫТЬ БОЛЬШИМ ЗАГЛАВНЫМ), что приведет к накладным расходам на копирование объекта. Пока указатель исправлен 4-байтовый размер (при условии 32-битного). Другие причины уже упомянуты выше ...


14
предпочитаете проходить по ссылке
болов

2
Я рекомендую проходить по константе-ссылке, как для имеющейся std::string test;у нас переменной , void func(const std::string &) {}но если функции не нужно изменять ввод, в этом случае я рекомендую использовать указатели (чтобы любой, кто читает код, заметил &и понял, что функция может изменить свой ввод)
Top- Мастер

-7

Уже есть много отличных ответов, но позвольте мне привести один пример:

У меня есть простой класс предметов:

 class Item
    {
    public: 
      std::string name;
      int weight;
      int price;
    };

Я делаю вектор, чтобы держать их несколько.

std::vector<Item> inventory;

Я создаю миллион объектов Item и помещаю их обратно в вектор. Я сортирую вектор по имени, а затем выполняю простой итеративный двоичный поиск определенного имени элемента. Я тестирую программу, и выполнение ее занимает более 8 минут. Затем я изменяю свой инвентарный вектор следующим образом:

std::vector<Item *> inventory;

... и создать мой миллион объектов Item с помощью нового. Единственные изменения, которые я делаю в своем коде, это использование указателей на Items, за исключением цикла, который я добавляю для очистки памяти в конце. Эта программа выполняется менее чем за 40 секунд, или лучше, чем увеличение скорости в 10 раз. РЕДАКТИРОВАТЬ: код находится на http://pastebin.com/DK24SPeW С оптимизацией компилятора он показывает только 3,4-кратное увеличение на машине, на которой я только что проверил, что все еще значительно.


2
Ну, тогда вы сравниваете указатели или вы все еще сравниваете реальные объекты? Я очень сомневаюсь, что другой уровень косвенности может улучшить производительность. Пожалуйста, предоставьте код! Вы правильно убираете потом?
Стефан

1
@stefan Я сравниваю данные (в частности, поле имени) объектов как для сортировки, так и для поиска. Я убираюсь должным образом, как я уже упоминал в посте. ускорение, вероятно, связано с двумя факторами: 1) std :: vector push_back () копирует объекты, поэтому версия указателя должна копировать только один указатель на объект. Это оказывает многократное влияние на производительность, поскольку не только меньше копируется данных, но и распределитель памяти векторного класса уменьшается.
Даррен

2
Вот код, показывающий практически никакой разницы для вашего примера: сортировка. Код указателя на 6% быстрее, чем код без указателя только для сортировки, но в целом он на 10% медленнее, чем код без указателя. ideone.com/G0c7zw
Штефан

3
Ключевые слова: push_back. Конечно это копии. Вы должны были быть emplaceна месте при создании ваших объектов (если вам не нужно, чтобы они были кэшированы в другом месте).
underscore_d

1
Векторы указателей почти всегда неверны. Пожалуйста, не рекомендуйте их, не объясняя подробно подробности, плюсы и минусы. Похоже, вы нашли одного профессионала, который является лишь следствием плохо закодированного контрпримера, и исказили его
гонки на
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.