c++ - указатели - указатель на указатель




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

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

Object *myObject = new Object;

скорее, чем:

Object myObject;

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

myObject.testFunc();

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

myObject->testFunc();

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


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

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

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, они автоматически управляют временем жизни ваших объектов.


Предисловие

Java не похож на C ++, вопреки шуму. Машина hype для 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, (Именованный) Оптимизация возвращаемого значения

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

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


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

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


Есть много отличных ответов на этот вопрос, в том числе важные варианты использования форвардных деклараций, полиморфизма и т. Д., Но я чувствую, что часть «души» вашего вопроса не отвечает, а именно, что означают разные синтаксисы для 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 #).


Очень жаль, что вы так часто видите динамическое распределение. Это просто показывает, сколько плохих программистов на 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 member. Если библиотека выполняет какое-то выделение для вас, которое ожидает от вас освобождения с помощью дескриптора, вы можете часто обернуть дескриптор в интеллектуальном указателе с помощью настраиваемого делетера, который соответствующим образом освободит объект.


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

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

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

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


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

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

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


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

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

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


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


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

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

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также контейнер, некоторые парни забыли его :)


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


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

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


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


Object *myObject = new Object;

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

Object myObject;

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





c++11