Зачем нужны указатели в C++?
Как правило, многие программисты понимают разницу между объектами и указателями на объекты. Тем не менее не всегда ясно, в пользу какого из вышеперечисленных способов обращения делать выбор. Попробуем ответить на этот вопрос.
Однажды на StackOverflow прозвучал вопрос следующего содержания: «Why should I use a pointer rather than the object itself?» — «Почему я должен использовать указатель, а не сам объект?»
И действительно, некоторые программисты применяют указатели на объекты чаще, чем сами объекты, используя такую конструкцию:
Object *myObject=new Object;
а не такую:
ObjectmyObject;
Также и с методами, почему вместо этого:
myObject.testFunc();
следует писать это:
myObject->testFunc();
Разработчик, который задал вопрос, предположил, что это даёт выигрыш в скорости, ведь мы обращаемся напрямую к памяти. К слову, он перешёл в C++-программирование с Java. Что же, посмотрим, что ему ответили.
Ответ
В языке программирования Java указатели в явном виде не используются, то есть разработчик не может обратиться в коде к объекту через указатель на него. Но на деле все типы в Java, за исключением базовых, являются ссылочными, ведь обращение к ним осуществляется по ссылке, хотя явно передать по ссылке параметр нельзя. Также следует заметить, что new в C++, в Java и C# — разные вещи.
Чтобы понять, что же такое указатели в C++, рассмотрим два похожих фрагмента кода. Java:
Object object1 =newObject();// Новый объект Object object2 =newObject();// Очередной новый объект object1 = object2;// Обе переменные ссылаются на объект, на который ранее ссылалась object2 // При изменении объекта, на который ссылается object1, произойдет изменение и // object2, ведь это один и тот же объект
Эквивалентный код на C++:
Object* object1 =newObject();// Память выделена под новый объект // На эту память ссылается object1 Object* object2 =newObject();// Аналогично со вторым объектом delete object1; // В C++ нет системы сборки мусора, поэтому если этого не cделать, // к этой памяти программа уже не сможет получить доступ, // как минимум до перезапуска // Это не что иное, как утечка памяти object1 = object2;// Как и в Java, object1 указывает туда же, куда и object2
Но это – совсем другая вещь (C++):
Object object1;// Новый объект Object object2;// Еще один новый объект object1 = object2;// Полное копирование объекта object2 в object1, // а не переопределение указателя является очень дорогой операцией
Но будет ли выигрыш в скорости, если мы будем обращаться к памяти напрямую?
Оказывается, нет. Дело в том, что работа с указателями оформлена в виде кучи, а работа с объектами представляет собой стек — более простую и быструю структуру. Таким образом, в одном вопросе мы получили два: 1. Когда лучше использовать динамическое распределение памяти? 2. Когда лучше использовать указатели?
Конечно, всегда желательно выбирать для работы наиболее подходящий инструмент. Но почти всегда существует реализация лучше, чем с применением ручного динамического распределения (dynamicallocation) и/или «сырых» указателей.
Динамическое распределение
Формулировка вопроса подразумевает 2 способа создания объекта. Главное различие — срок их жизни (storageduration) в памяти программы.
Применяя
Что касается принудительного установления срока жизни, то оно используется в следующих случаях: • надо, чтобы объект существовал и после выхода из области его видимости. Мы говорим об именно этом объекте и именно в этой области памяти, а не про его копию. Если же для вас это не является принципиальным (а чаще всего это так), следует положиться на автоопределение срока жизни; • надо применять много памяти, которая может переполнить стек. В принципе, с этой проблемой сталкиваются редко, но, бывает, приходится решать и такую задачу; • точно не известен размер массива, который придётся использовать. Вы должны знать, что в C++ массивы имеют фиксированный размер при определении. Это иногда вызывает проблемы, к примеру, во время считывания пользовательского ввода. Указатель же определяет лишь тот участок в памяти, куда записывается начало массива.
Когда применение динамического распределения необходимо, стоит инкапсулировать его посредством умного указателя (о таких указателях мы уже писали) или иного типа, поддерживающего концепцию “Получение ресурса есть инициализация”. Умными указателями, например, являются
Указатели
Иногда применение указателей оправдано не только из-за динамического распределения памяти. Однако помните, что практически всегда существует альтернативный путь, не предполагающий использование указателей — его и следует выбрать. То есть, если особая необходимость использовать указатели отсутствует, всегда выбирайте альтернативу.
Однако рассмотрим случаи, когда применение указателей оправдано: • ссылочная семантика. Иногда нужно обратиться к объекту вне зависимости от того, каким образом под него распределена память, если вы желаете обратиться в функции именно к этому объекту, а не к его копии. То есть речь идёт о случае, когда надо реализовать передачу по ссылке. Но, опять же, чаще всего тут достаточно использовать не указатель, а именно ссылку, ведь как раз для этого и созданы ссылки. Но если есть возможность обратиться к копии объекта, то и ссылку, соответственно, использовать нет необходимости (однако учтите, что копирование объекта является дорогой операцией); • полиморфизм. С помощью ссылки либо указателя возможен вызов функций в рамках полиморфизма. Но и тут использование ссылок предпочтительнее; • необязательный объект. Тут можно применять nullptr, дабы указать, что объект опущен. Но если это аргумент функции, сделайте реализацию с аргументами по умолчанию либо перегрузкой. С другой стороны, вы можете использовать тип, инкапсулирующий такое поведение, допустим, boost::optional (измененный в C++14 std::optional); • повышение скорости компиляции. Иногда нужно разделить единицы компиляции (compilationunits). Один из эффективных вариантов использования указателей — предварительная декларация. В результате вы получите возможность разнести единицы компиляции, что обычно положительно сказывается на ускорении времени компиляции. • взаимодействие с С-подобной библиотекой. Здесь придётся задействовать сырые указатели, освобождение памяти из-под которых вы выполняете в самый последний момент. Сырой указатель можно получить из умного, используя операцию get. Если библиотека использует память, которая потом должна освобождаться вручную, можно оформить в умном указателе деструктор.
По материалам StackOverflow.