Почему стоит использовать указатель вместо самого объекта в C++?
Когда и зачем использовать указатели вместо объектов
Отличный вопрос! Давайте разберемся по порядку. Основная причина использовать указатели — управление временем жизни объекта и его идентичностью. Когда вы передаете объект по значению, вы создаете копию — это отдельный объект. Указатель же ссылается на оригинал.
1. Избегаем копирования (производительность)
Представьте, что ваш Monster весит 1 МБ (текстуры, анимации). Копирование при каждом вызове функции — катастрофа:
class Monster {
std::vector<Animation> animations; // 100500 кадров
std::string name;
// ...
};
// Плохо: копируем монстра каждый раз
void processMonster(Monster m) {
m.update();
}
// Хорошо: передаем указатель (или ссылку)
void processMonster(Monster* m) {
m->update(); // Работаем с оригиналом
}
int main() {
Monster* boss = new Monster("Boss");
processMonster(boss); // Никакого копирования!
delete boss;
}
2. Полиморфизм (работа с наследниками)
Указатели позволяют работать с производными классами через базовый класс:
class Enemy {
public:
virtual void attack() = 0;
};
class Dragon : public Enemy {
public:
void attack() override { std::cout << "Дракон дышит огнем!\n"; }
};
class Skeleton : public Enemy {
public:
void attack() override { std::cout << "Скелет стреляет из лука!\n"; }
};
void fight(Enemy* e) { // Указатель на базовый класс
e->attack(); // Вызовется правильная версия attack()
}
int main() {
Enemy* enemies[2];
enemies[0] = new Dragon();
enemies[1] = new Skeleton();
for (int i = 0; i < 2; ++i) {
fight(enemies[i]); // Работает для любого врага!
}
for (int i = 0; i < 2; ++i) delete enemies[i];
}
3. Необязательность (nullptr)
Указатель может быть нулевым — это удобно, когда объект может отсутствовать:
class Player {
Weapon* currentWeapon; // Может быть nullptr (без оружия)
public:
void setWeapon(Weapon* w) { currentWeapon = w; }
void attack() {
if (currentWeapon) {
currentWeapon->use(); // Есть оружие
} else {
std::cout << "Бьем кулаками!\n";
}
}
};
4. Владение и динамическое выделение
Когда объект должен жить дольше, чем область видимости, используйте new (но лучше умные указатели):
class Game {
std::vector<Monster*> monsters; // Игра владеет монстрами
public:
void spawnMonster(const std::string& name) {
monsters.push_back(new Monster(name));
}
~Game() {
for (auto m : monsters) delete m; // Чистим память
}
};
Указатель vs Ссылка
- Ссылка (&) — всегда ссылается на существующий объект, не может быть nullptr. Используйте, когда объект гарантированно есть.
- Указатель (*) — может быть nullptr, можно изменить на другой объект. Используйте для опциональных объектов или полиморфизма.
void processRef(Monster& m) { // Обязательно должен быть объект
m.attack();
}
void processPtr(Monster* m) { // Может быть nullptr
if (m) m->attack();
}
Современный C++ (C++11 и новее)
Сегодня вместо сырых указателей часто используют умные указатели:
#include <memory>
std::unique_ptr<Monster> boss = std::make_unique<Monster>("Boss");
// delete не нужен — автоматически удалится
std::shared_ptr<Monster> sharedBoss = std::make_shared<Monster>("Shared");
// Можно копировать, удалится когда последняя копия исчезнет
Итог: Используйте указатели когда:
- Нужен полиморфизм (работа с наследниками через базовый класс)
- Объект может отсутствовать (nullptr)
- Нужно избежать копирования тяжелых объектов
- Объект создается динамически и живет дольше своей области видимости
А ссылки — когда объект гарантированно существует и вы не планируете менять объект, на который ссылаетесь.
<<Дополнение: когда лучше НЕ использовать указатели
Коллега выше отлично объяснил, когда указатели нужны. Я же расскажу, когда их стоит избегать.
1. Для простых данных — используйте стек
Если объект маленький и локальный, не выделяйте его в куче:
// Плохо: лишнее выделение памяти
Monster* m = new Monster("Goblin");
m->attack();
delete m;
// Хорошо: объект на стеке
Monster m("Goblin");
m.attack();
2. Вместо указателя — ссылка (когда можно)
Если функция не может принять nullptr, используйте ссылку — это самодокументируемый код:
// Плохо: можно передать nullptr
void process(Monster* m) {
m->attack(); // Упадет, если m == nullptr
}
// Хорошо: гарантированно есть объект
void process(Monster& m) {
m.attack();
}
3. Сырые указатели для владения — опасны
В современном C++ (C++11+) старайтесь не использовать new/delete напрямую. Умные указатели безопаснее:
// Старый стиль (легко забыть delete)
Monster* m = new Monster("Boss");
// ... код ...
delete m; // А если исключение? Утечка!
// Новый стиль (автоматическое управление)
auto m = std::make_unique<Monster>("Boss");
// delete не нужен — unique_ptr сам удалит
Золотое правило
Начинайте с простого: объекты на стеке и ссылки. Переходите к указателям только когда:
- Нужен полиморфизм
- Объект может отсутствовать
- Нужно управлять временем жизни вручную (но лучше умные указатели)
И помните: если вы используете сырой указатель, вы должны быть уверены, что он не "висит" (указывает на удаленный объект).