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

1410

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

Object *myObject = new Object;

а не:

Object myObject;

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

myObject.testFunc();

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

myObject->testFunc();

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

  • 361
    Слава вам за то, что вы ставите под сомнение эту практику, а не просто следуете ей В большинстве случаев указатели используются слишком часто.
  • 98
    Если вы не видите причины использовать указатели, не надо. Предпочитаю объекты. Предпочитайте объекты перед unique_ptr перед shared_ptr перед необработанными указателями.
Показать ещё 17 комментариев
Теги:
pointers
c++11

23 ответа

1473
Лучший ответ

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

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

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

Динамическое размещение

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

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

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

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

Указатели

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

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

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

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

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

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

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

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

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

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

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

Приобретение ресурсов. Создание указателя на ресурс с помощью оператора new - это анти-шаблон в современном С++. Используйте специальный класс ресурсов (один из стандартных контейнеров) или умный указатель (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
}

Необработанный указатель должен использоваться только как "представление" и никоим образом не связан с владением, будь то прямое создание или неявно через возвращаемые значения. См. Также этот Q & A из часто задаваемых вопросов С++.

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

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

Есть много отличных ответов на этот вопрос, в том числе важные варианты использования форвардных деклараций, полиморфизм и т.д., но я чувствую, что часть "души" вашего вопроса не отвечает - а именно, что означают разные синтаксисы для Java и С++.

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

Java:

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

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

С++:

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.

Посмотрите альтернативный способ С++:

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 (неявно) обрабатывает указатели на объекты, тогда как С++ может обрабатывать либо указатели на объекты, либо сами объекты. Есть исключения из этого - например, если вы объявляете "примитивные" типы Java, это фактические значения, которые копируются, а не указатели. Таким образом,

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.

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

Возьмите главную точку - конструкция Object * object = new Object() на самом деле является тем, что ближе всего к семантике Java (или С#).

  • 7
    Object2 is now "dead" : я думаю, что вы имеете в виду myObject1 или, точнее, the object pointed to by myObject1 .
  • 2
    В самом деле! Перефразировано немного.
Показать ещё 2 комментария
73

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

  • 7
    это действительно добавляет к смеси полезной информации, так что рад, что вы сделали это ответ!
  • 3
    Ссылки работают для этого тоже.
Показать ещё 5 комментариев
59

Предисловие

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

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

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

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

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

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

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

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

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

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

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

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 и С#, новички не понимают разницы.

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

Java и С# имеют средства, позволяющие рассматривать объекты как другие, например, используя ключевое слово 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 и С#, где идиоматично использовать new ключевое слово во всем мире, в C++ вы должны использовать его, если вам нужно. Обычная фраза идет, все выглядит как гвоздь, если у вас есть молоток. В то время как начинающие, начинающие с C++, боятся указателей и учатся использовать переменные стека по привычке, программисты Java и С# начинают с использования указателей, не понимая этого! Это буквально отступает на неправильную ногу. Вы должны отказаться от всего, что знаете, потому что синтаксис - это одно, а изучение языка - другое.

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

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

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

  • 0
    «Итак! Test - это, по сути, сокращение для проверки, является ли test NULL или недопустимым указателем, что означает, что приведение не выполнено». Я думаю, что это предложение должно быть переписано для ясности.
  • 4
    «Явная машина Java хотела бы, чтобы вы поверили» - возможно, в 1997 году, но теперь это анахронизм, больше нет мотивации сравнивать Java с C ++ в 2014 году.
Показать ещё 7 комментариев
20

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

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

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

Не испугался? Хорошо: циклические ссылки - обрабатывайте их самостоятельно, человек. И помните: убивайте каждый объект ровно один раз, мы С++ runtimes не любим тех, кто возится с трупами, оставляют мертвых только.

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

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

Когда вы передаете свой объект по указателю, вы копируете только несколько байтов (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 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 two completely different objects, not the references to the same one
  • 1
    std::auto_ptr устарела, пожалуйста, не используйте его.
  • 14
    Спасибо за std :: указывает на это, я отредактировал ответ
Показать ещё 1 комментарий
19

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

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

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

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

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

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 все равно будет уничтожен, как и любая другая локальная переменная. Разница в том, что его значение является указателем на объект, а не на сам объект, и уничтожение тупого указателя не влияет на его объект. Так что объект переживет указанное разрушение.
Показать ещё 1 комментарий
12

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

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

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); Здесь вы передаете ссылку, в основном это то же самое, что и передающий указатель, но компилятор делает некоторые вещи, и вы не можете передать недопустимую ссылку (на самом деле можно создать ситуацию с недопустимой ссылкой, но это компилятор tricking).
  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

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

  • Эффективность (как вы уже указывали). Передача объектов в функции означают создание новых копий объекта.
  • Работа с объектами из сторонних библиотек. Если ваш объект принадлежит стороннему коду, и авторы намереваются использовать свои объекты только с помощью указателей (без конструкторов копирования и т.д.), единственный способ, которым вы можете обойти это объект использует указатели. Передача по значению может вызвать проблемы. (Deep копия/мелкая копия).
  • если объект владеет ресурсом, и вы хотите, чтобы собственность не была защищена другими объектами.
3
Object *myObject = new Object;

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

Object myObject;

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

3

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

2

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

Затем спросите его его домашний адрес, он даст вам адрес. Теперь вы можете посетить его дом, это Pass by Pointer.

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

1

Вы не должны. Люди (многие, к сожалению) пишут это из-за невежества.

Иногда динамическое распределение имеет свое место, но в приведенных вами примерах это неверно.

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

1

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

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

  • 0
    Хеш-таблицы? Может быть, в некоторых JVM, но не рассчитывайте на это.
  • 1
    Вы можете использовать MyClass[] для эмуляции указателя
Показать ещё 3 комментария
0

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

Class Base1 {
};

Class Derived1 : public Base1 {
};


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

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

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

0

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

0

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

0

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

  • может напрямую разговаривать с памятью.

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

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

"Голь на выдумки хитра." Самое важное отличие, которое я хотел бы отметить, - это результат моего собственного опыта кодирования. Иногда вам нужно передать объекты в функции. В этом случае, если ваш объект имеет очень большой класс, то передача его в виде объекта будет скопировать его состояние (которое вам может не понадобиться..AND CAN BIG OVERHEAD), что приводит к накладным расходам на копирование объекта. 4-байтовый размер (предполагается 32 бит). Другие причины уже упомянуты выше...

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

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

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

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

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

std::vector<Item> inventory;

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

std::vector<Item *> inventory;

... и создайте мои объекты Item Item через new. ТОЛЬКО изменения, которые я делаю для моего кода, - это использовать указатели на Элементы, за исключением цикла, который я добавляю для очистки памяти в конце. Эта программа работает менее чем за 40 секунд или лучше, чем 10-кратное увеличение скорости. EDIT: код находится в http://pastebin.com/DK24SPeW При оптимизации компилятора он показывает только увеличение на 3,4 раза на машине, на которой я только что протестировал ее, что по-прежнему значительно.

  • 2
    Ну, тогда вы сравниваете указатели или вы все еще сравниваете реальные объекты? Я очень сомневаюсь, что другой уровень косвенности может улучшить производительность. Пожалуйста, предоставьте код! Вы правильно убираете потом?
  • 0
    @stefan Я сравниваю данные (в частности, поле имени) объектов как для сортировки, так и для поиска. Я убираюсь должным образом, как я уже упоминал в посте. ускорение, вероятно, связано с двумя факторами: 1) std :: vector push_back () копирует объекты, поэтому версия указателя должна копировать только один указатель на объект. Это оказывает многократное влияние на производительность, поскольку не только меньше копируется данных, но и распределитель памяти векторного класса уменьшается.
Показать ещё 7 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню