Умные указатели в C++: понятное руководство по unique_ptr, shared_ptr и weak_ptr с примерами
Запрос «умные указатели в C++ примеры» стабильно популярен: новичкам сложно сразу понять, чем отличаются unique_ptr, shared_ptr и weak_ptr, а продолжающим — где их применять без лишнего оверхеда. В этом руководстве мы разберем каждую модель владения, типичные ошибки и покажем рабочие примеры, которые пригодятся в реальных проектах.
Что решают умные указатели в C++
Умные указатели — это RAII-обертки вокруг «сырых» указателей (raw pointers), которые автоматически освобождают ресурсы при выходе из области видимости. Это снижает риск утечек памяти, упрощает владение и делает код безопаснее. Главные герои: std::unique_ptr, std::shared_ptr и std::weak_ptr.
std::unique_ptr: единственный владелец
unique_ptr гарантирует уникальное владение объектом. Нельзя копировать, но можно перемещать. Идеален по умолчанию, если объектом владеет ровно одна сущность.
#include <memory>
#include <iostream>
#include <vector>
struct Widget {
int id;
explicit Widget(int i) : id(i) { std::cout << "Widget(" << id << ")n"; }
~Widget() { std::cout << "~Widget(" << id << ")n"; }
};
int main() {
auto p = std::make_unique<Widget>(42); // создание
std::vector<std::unique_ptr<Widget>> v;
v.push_back(std::move(p)); // перенос владения в вектор
// p теперь пуст (nullptr)
}
Передача владения в функцию — по значению unique_ptr (с перемещением):
void take(std::unique_ptr<Widget> w) {
// w - новый владелец
}
void use(const Widget& w) {
// только пользуемся, не владеем
}
int main() {
auto p = std::make_unique<Widget>(1);
use(*p);
take(std::move(p));
}
Можно хранить в контейнерах, использовать полиморфизм и безопасно управлять массивами:
struct Base { virtual ~Base() = default; virtual void f() = 0; };
struct Derived : Base { void f() override { std::cout << "Derivedn"; } };
int main() {
std::unique_ptr<Base> pb = std::make_unique<Derived>();
pb->f();
auto arr = std::make_unique<int[]>(5); // динамический массив
arr[0] = 10;
}
Кастомные делетеры делают unique_ptr универсальным для системных ресурсов:
#include <cstdio>
using FilePtr = std::unique_ptr<FILE, decltype(&std::fclose)>;
FilePtr openFile(const char* path) {
FILE* f = std::fopen(path, "r");
return FilePtr(f, &std::fclose);
}
std::shared_ptr: разделяемое владение
shared_ptr считает количество владельцев и освобождает ресурс, когда счетчик доходит до нуля. Удобен, когда объект делят несколько частей системы (например, граф сцены, кэш, пул).
#include <memory>
#include <iostream>
struct Data { int v; explicit Data(int x) : v(x) {} };
int main() {
auto a = std::make_shared<Data>(10);
auto b = a; // еще один владелец
std::cout << a.use_count() << " ownersn"; // 2
b.reset();
std::cout << a.use_count() << " ownersn"; // 1
}
Важно: увеличение/уменьшение счетчика потокобезопасно, но сам объект нет. Если объект используется из нескольких потоков, защитите доступ мьютексами.
std::weak_ptr: наблюдатель без владения
weak_ptr не продлевает жизнь объекта. Используется, чтобы разорвать циклы владения и устраивать безопасные ссылки-наблюдатели.
#include <memory>
#include <iostream>
int main() {
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(5);
wp = sp; // наблюдаем за sp
if (auto locked = wp.lock()) {
std::cout << *locked << "n";
}
} // sp уничтожен
if (auto locked = wp.lock()) {
std::cout << *locked << "n";
} else {
std::cout << "expiredn";
}
}
Циклические ссылки: типичная ловушка shared_ptr
Два объекта, ссылающихся друг на друга shared_ptr-ами, никогда не освободятся — счетчики не станут нулем. Решение: одна сторона хранит weak_ptr.
#include <memory>
struct Node {
int value{};
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // разрываем цикл слабой ссылкой
};
shared_from_this: безопасно получить shared_ptr из this
Если объект уже управляется shared_ptr, а вам нужен еще один shared_ptr на него внутри метода — используйте enable_shared_from_this.
#include <memory>
#include <iostream>
struct Session : std::enable_shared_from_this<Session> {
void start() {
auto self = shared_from_this(); // безопасно
std::cout << "owners: " << self.use_count() << "n";
}
};
int main() {
auto s = std::make_shared<Session>();
s->start();
}
Нельзя вызывать shared_from_this(), если объект не был создан под управлением shared_ptr — будет исключение.
Как передавать умные указатели в функции
Производительность и практические советы
Мини-чеклист выбора
Типичные вопросы на собеседовании
Где потренироваться и закрыть пробелы
Если хотите быстро отточить навыки и пройти практику с задачами и разбором, рекомендую посмотреть программу курса: Практический интенсив «Программирование на C++ с Нуля до Гуру» — стартуйте и прокачайте C++ до уровня проектов.
Итоги
Умные указатели в C++ — базовый инструмент современного кода. Используйте unique_ptr по умолчанию, подключайте shared_ptr только при реальной необходимости разделения владения, а weak_ptr — для безопасных наблюдений и разрыва циклов. Следуя этим правилам и примерам, вы избежите утечек памяти, упростите дизайн и сделаете код надежнее и быстрее.



