Почему следует использовать указатель вместо самого объекта?
Я пришёл из мира Java и начал работать с объектами в C++. Но меня смутило, что многие разработчики часто используют указатели на объекты, а не сами объекты. Например, вот так:
Object *myObject = new Object;
вместо этого:
Object myObject;
Также, вместо того чтобы использовать функцию, скажем, testFunc()
, как это:
myObject.testFunc();
нам нужно писать:
myObject->testFunc();
Я не могу понять, почему нужно использовать указатели. Я предполагал, что это связано с эффективностью и скоростью, ведь мы получаем прямой доступ к адресу в памяти. Прав ли я?
5 ответ(ов)
На этот вопрос уже дано множество отличных ответов, включая важные случаи использования предварительных деклараций, полиморфизма и т. д., но мне кажется, что часть "души" вашего вопроса не была затронута – а именно, что означают различные синтаксисы в Java и C++.
Давайте рассмотрим ситуацию, сравнивая эти два языка:
Java:
Object object1 = new Object(); // Создается новый объект
Object object2 = new Object(); // Создается еще один новый объект
object1 = object2;
// object1 теперь указывает на объект, который изначально был выделен для object2
// Объект, который изначально был выделен для object1, теперь "мертв" – на него больше ничего не указывает,
// поэтому он будет освобожден сборщиком мусора.
// Если изменится либо object1, либо object2, изменения отразятся на другом объекте
Ближайший эквивалент этому в C++:
C++:
Object * object1 = new Object(); // Создается новый объект в куче
Object * object2 = new Object(); // Создается еще один новый объект в куче
delete object1;
// Поскольку в C++ нет сборщика мусора, если мы этого не сделаем, следующая строка вызовет
// "утечку памяти", то есть часть занятой памяти, которую приложение не сможет использовать
// и которую мы не можем вернуть...
object1 = object2; // То же самое, что и в Java, object1 указывает на object2.
Посмотрим на альтернативный способ в C++:
Object object1; // Создается новый объект в СТЕКЕ
Object object2; // Создается еще один новый объект в СТЕКЕ
object1 = object2; // !!!! Это другое! СОДЕРЖИМОЕ object2 КОПИРУЕТСЯ в object1,
// с использованием "оператора присваивания копированием", определения оператора =.
// Но оба объекта все еще разные. Измените один – другой останется неизменным.
// Кроме того, объекты автоматически удаляются, когда функция возвращает управление...
Лучше всего думать об этом так — более или менее — Java (неявно) работает с указателями на объекты, в то время как C++ может работать либо с указателями на объекты, либо с самими объектами. Есть исключения из этого правила — например, если вы объявляете "примитивные" типы в Java, это фактические значения, которые копируются, а не указатели.
Так что,
Java:
int object1; // Целое число выделяется в стеке.
int object2; // Другое целое число выделяется в стеке.
object1 = object2; // Значение object2 копируется в object1.
Сказав это, использование указателей не обязательно является ни правильным, ни неправильным способом решения задач; однако другие ответы уже достаточно хорошо это осветили. Общая идея в том, что в C++ у вас гораздо больше контроля над временем жизни объектов и над тем, где они существуют.
Главный вывод – конструкция Object * object = new Object()
на самом деле ближе всего к типичной семантике Java (или C#) в этом отношении.
В C++ объекты, выделенные в стеке (с помощью оператора Object object;
внутри блока), будут существовать только в пределах области видимости, в которой они были объявлены. Когда блок кода завершает выполнение, объекты, объявленные в этом блоке, уничтожаются.
С другой стороны, если вы выделяете память в динамической памяти (куче), используя Object* obj = new Object()
, они будут существовать на куче до тех пор, пока вы не вызовете delete obj
.
Я бы создал объект в куче, когда хочу использовать этот объект не только в том блоке кода, в котором он был объявлен или выделен.
Когда вы спрашиваете, зачем использовать такой подход, нужно рассмотреть, как это работает внутри тела функции.
Если вы объявляете объект следующим образом:
Object myObject;
то ваш myObject
будет уничтожен, как только функция завершится. Это полезно, если вам не нужен объект за пределами функции. В этом случае объект размещается в стеке текущего потока.
Если же вы напишете внутри функции:
Object *myObject = new Object;
то экземпляр класса Object
, на который указывает myObject
, не будет уничтожен после завершения функции, и память будет выделена в куче.
Если вы программист на Java, то второй пример ближе к тому, как происходит выделение объектов в Java. Строка Object *myObject = new Object;
эквивалентна Object myObject = new Object();
в Java. Разница заключается в том, что в Java myObject
будет собран сборщиком мусора, тогда как в C++ он не освобождается автоматически; вам нужно явно вызвать delete myObject;
, иначе у вас возникнут утечки памяти.
С C++11 вы можете использовать более безопасные способы динамического выделения памяти, такие как new Object
, сохраняя значения в shared_ptr
или unique_ptr
.
std::shared_ptr<std::string> safe_str = std::make_shared<std::string>("make_shared");
// начиная с C++14
std::unique_ptr<std::string> safe_str = std::make_unique<std::string>("make_shared");
Кроме того, объекты очень часто хранятся в контейнерах, таких как map
или vector
, которые автоматически управляют временем жизни ваших объектов.
Технически, это вопрос выделения памяти, однако есть два практических аспекта, на которые стоит обратить внимание.
Область видимости. Когда вы определяете объект без указателя, вы больше не сможете к нему обратиться после завершения блока кода, в котором он был определён. В то время как если вы определяете указатель с помощью оператора "new", вы можете обращаться к выделенной памяти из любой части программы, пока у вас есть указатель на эту память, и до тех пор, пока вы не вызовете "delete".
Если вам нужно передать аргументы в функцию, вы хотите передавать указатель или ссылку для повышения эффективности. При передаче объекта происходит его копирование, и если это объект, занимающий много памяти, то это может потреблять значительные ресурсы ЦП (например, если вы копируете вектор, заполненный данными). В случае передачи указателя вы передаёте лишь одно значение типа int (в зависимости от реализации, но в большинстве случаев это одно int).
Кроме того, стоит помнить, что "new" выделяет память в куче, которую необходимо освободить в какой-то момент. Когда вы можете обойтись без "new", я настоятельно рекомендую использовать обычное определение объекта "в стеке".
Когда у вас есть класс A
, содержащий класс B
, и вы хотите вызвать какую-либо функцию класса B
из вне класса A
, вам нужно получить указатель на экземпляр класса B
. После этого вы сможете вызывать необходимые методы и, как следствие, изменять состояние объекта класса B
, который находится в классе A
.
Однако будьте осторожны с динамическими объектами. Если вы создаете объекты класса B
динамически (например, с помощью оператора new
), не забудьте правильно управлять памятью, чтобы избежать утечек. Убедитесь, что вы удаляете эти объекты, когда они больше не нужны, чтобы избежать неопределенного поведения программы.
Пример кода на C++ может выглядеть так:
class B {
public:
void someFunction() {
// Логика функции
}
};
class A {
private:
B* b; // Указатель на динамический объект класса B
public:
A() {
b = new B(); // Создание объекта B динамически
}
~A() {
delete b; // Освобождение памяти
}
B* getB() {
return b; // Возвращаем указатель на объект B
}
};
// Использование
A aInstance;
B* bInstance = aInstance.getB();
bInstance->someFunction();
В этом примере мы создаем объект класса B
динамически внутри класса A
и предоставляем доступ к его методам через указатель. Не забывайте следить за управлением памятью при использовании динамических объектов.
Что такое лямбда-выражение и когда его следует использовать?
Разница между const int*, const int * const и int * const?
Что такое std::move() и когда его следует использовать?
Что означает T&& (двойной амперсанд) в C++11?
Какова разница между 'typedef' и 'using'?