В чём разница между shared_ptr и unique_ptr?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
shared_ptr vs unique_ptr
Определения
unique_ptr — интеллектуальный указатель, который владеет объектом исключительно. Только один unique_ptr может владеть объектом. Когда unique_ptr удаляется, объект удаляется автоматически.
shared_ptr — интеллектуальный указатель, который владеет объектом совместно. Несколько shared_ptr могут указывать на один объект. Объект удаляется, когда последний shared_ptr удаляется (счётчик ссылок достигает нуля).
Сравнительная таблица
| Характеристика | unique_ptr | shared_ptr |
|---|---|---|
| Владение | Исключительное | Совместное |
| Количество владельцев | 1 | N (счётчик ссылок) |
| Перемещение (move) | Да | Нет (нельзя переместить) |
| Копирование (copy) | Нет (delete) | Да (увеличивает счётчик) |
| Оверхед памяти | Минимальный | Больше (счётчик + control block) |
| Оверхед производительности | Нет | Есть (атомарные операции) |
| Потокобезопасность | Нет | Частично (счётчик потокобезопасен) |
| Размер объекта | Размер указателя (8B) | 2 указателя (16B) |
| Когда использовать | Чаще (по умолчанию) | Когда нужно совместное владение |
unique_ptr подробно
Базовое использование
#include <memory>
#include <iostream>
using namespace std;
class Resource {
public:
Resource(const string& name) : name(name) {
cout << "Resource " << name << " created" << endl;
}
~Resource() {
cout << "Resource " << name << " destroyed" << endl;
}
private:
string name;
};
int main() {
{
unique_ptr<Resource> ptr1(new Resource("A"));
// unique_ptr<Resource> ptr2 = ptr1; // ERROR: deleted copy!
unique_ptr<Resource> ptr2 = move(ptr1); // OK: move
cout << "ptr1 is null: " << (ptr1 == nullptr) << endl;
cout << "ptr2 has resource" << endl;
} // ptr2 удаляется → Resource destroyed
// ptr1 был пустой, ничего не происходит
return 0;
}
Вывод:
Resource A created
ptr1 is null: 1
ptr2 has resource
Resource A destroyed
unique_ptr в контейнерах
#include <memory>
#include <vector>
#include <iostream>
using namespace std;
int main() {
// Контейнер unique_ptr работает идеально
vector<unique_ptr<int>> vec;
vec.push_back(make_unique<int>(42));
vec.push_back(make_unique<int>(100));
vec.push_back(make_unique<int>(200));
for (const auto& ptr : vec)
cout << *ptr << " ";
cout << endl;
// При удалении вектора все unique_ptr удаляются
// и их объекты освобождаются автоматически
return 0;
}
unique_ptr с кастомным deleter
#include <memory>
#include <iostream>
using namespace std;
class MyResource {
public:
MyResource() { cout << "Allocated" << endl; }
~MyResource() { cout << "Destroyed" << endl; }
};
int main() {
// Кастомный deleter
auto custom_deleter = [](MyResource* ptr) {
cout << "Custom deletion logic" << endl;
delete ptr;
};
unique_ptr<MyResource, decltype(custom_deleter)> ptr(
new MyResource(),
custom_deleter
);
return 0;
} // Вывод: Custom deletion logic, Destroyed
shared_ptr подробно
Базовое использование
#include <memory>
#include <iostream>
using namespace std;
class Resource {
public:
Resource(const string& name) : name(name) {
cout << "Resource " << name << " created" << endl;
}
~Resource() {
cout << "Resource " << name << " destroyed" << endl;
}
private:
string name;
};
int main() {
{
shared_ptr<Resource> ptr1 = make_shared<Resource>("A");
{
shared_ptr<Resource> ptr2 = ptr1; // OK: copy!
cout << "Use count: " << ptr1.use_count() << endl; // 2
} // ptr2 удаляется, счётчик: 2 → 1
cout << "Use count: " << ptr1.use_count() << endl; // 1
} // ptr1 удаляется, счётчик: 1 → 0 → Resource destroyed
return 0;
}
Вывод:
Resource A created
Use count: 2
Use count: 1
Resource A destroyed
shared_ptr в контейнерах
#include <memory>
#include <vector>
#include <iostream>
using namespace std;
int main() {
shared_ptr<int> original = make_shared<int>(42);
{
vector<shared_ptr<int>> vec;
vec.push_back(original); // Копирование
vec.push_back(original);
vec.push_back(original);
cout << "Use count inside: " << original.use_count() << endl; // 4
} // Вектор удаляется, все копии shared_ptr удаляются
cout << "Use count outside: " << original.use_count() << endl; // 1
// original.get() всё ещё указывает на 42
return 0;
}
shared_ptr с weak_ptr (избежание циклических ссылок)
#include <memory>
#include <iostream>
using namespace std;
class Node {
public:
Node(int val) : value(val) {}
~Node() { cout << "Node " << value << " deleted" << endl; }
int value;
shared_ptr<Node> next;
weak_ptr<Node> previous; // Используем weak_ptr для избежания цикла!
};
int main() {
{
shared_ptr<Node> node1 = make_shared<Node>(1);
shared_ptr<Node> node2 = make_shared<Node>(2);
node1->next = node2; // node1 → node2
node2->previous = node1; // node2 → node1 (weak, не увеличивает счётчик)
cout << "node2 use count: " << node2.use_count() << endl; // 2 (не 3!)
} // Оба узла удаляются без утечек
return 0;
}
Вывод:
node2 use count: 2
Node 2 deleted
Node 1 deleted
Без weak_ptr была бы утечка:
- node1 удаляется → счётчик node2 = 1
- node2 удаляется → счётчик node1 = 1
- Циклическая ссылка → утечка памяти!
Оверхед памяти
unique_ptr
#include <memory>
#include <iostream>
using namespace std;
int main() {
// unique_ptr: размер = размер обычного указателя
cout << "sizeof(int*): " << sizeof(int*) << " bytes" << endl; // 8
cout << "sizeof(unique_ptr<int>): " << sizeof(unique_ptr<int>) << " bytes" << endl; // 8
cout << "Оверхед: 0 байт!" << endl;
return 0;
}
shared_ptr
#include <memory>
#include <iostream>
using namespace std;
int main() {
// shared_ptr: размер = 2 указателя + control block
cout << "sizeof(int*): " << sizeof(int*) << " bytes" << endl; // 8
cout << "sizeof(shared_ptr<int>): " << sizeof(shared_ptr<int>) << " bytes" << endl; // 16
cout << "Control block: 32+ bytes (счётчики, deleter и т.д.)" << endl;
// Визуализация памяти:
// shared_ptr: [ptr к object] [ptr к control block]
// Control block: [reference count] [weak count] [deleter] [allocator]
return 0;
}
Производительность
Тест производительности
#include <memory>
#include <chrono>
#include <iostream>
using namespace std;
int main() {
const int N = 10000000;
// Тест unique_ptr
auto start = chrono::high_resolution_clock::now();
{
vector<unique_ptr<int>> vec;
for (int i = 0; i < N; ++i)
vec.push_back(make_unique<int>(i));
}
auto unique_time = chrono::high_resolution_clock::now() - start;
// Тест shared_ptr
start = chrono::high_resolution_clock::now();
{
vector<shared_ptr<int>> vec;
for (int i = 0; i < N; ++i)
vec.push_back(make_shared<int>(i));
}
auto shared_time = chrono::high_resolution_clock::now() - start;
cout << "unique_ptr: " << chrono::duration_cast<chrono::milliseconds>(unique_time).count() << "ms" << endl;
cout << "shared_ptr: " << chrono::duration_cast<chrono::milliseconds>(shared_time).count() << "ms" << endl;
cout << "shared_ptr медленнее на " << (double)shared_time.count() / unique_time.count() << "x" << endl;
return 0;
}
Примерный результат:
unique_ptr: 150ms
shared_ptr: 450ms
shared_ptr медленнее на 3.0x
Практические примеры
Пример 1: Владелец объекта (unique_ptr)
#include <memory>
using namespace std;
class Repository {
private:
unique_ptr<DatabaseConnection> connection;
public:
Repository() : connection(make_unique<DatabaseConnection>()) {}
// Передача права собственности
unique_ptr<DatabaseConnection> releaseConnection() {
return move(connection);
}
};
Пример 2: Совместный доступ (shared_ptr)
#include <memory>
using namespace std;
class CacheEntry {
public:
shared_ptr<Data> getData() { return data; } // Копируем shared_ptr
private:
shared_ptr<Data> data;
};
int main() {
shared_ptr<Data> data1 = entry.getData();
shared_ptr<Data> data2 = entry.getData();
// Оба data1 и data2 указывают на один объект
return 0;
}
Пример 3: Observer pattern (shared_ptr)
#include <memory>
#include <vector>
using namespace std;
class Subject {
private:
vector<shared_ptr<Observer>> observers;
public:
void subscribe(shared_ptr<Observer> obs) {
observers.push_back(obs);
}
void notify() {
for (auto& obs : observers)
obs->update(); // Все observers живут, пока subscribed
}
};
Правила выбора
Используйте unique_ptr когда:
- Один объект владеет ресурсом (99% случаев!)
- Нужна явная семантика владения
- Критична производительность
- Объект живёт ровно столько, сколько его владелец
Используйте shared_ptr когда:
- Несколько объектов должны совладать
- Время жизни неясно заранее
- Нужно безопасно передавать указатели между потоками
- Граф зависимостей сложный (но используйте weak_ptr!)
Современные best practices
// Вместо raw new/delete:
int* ptr = new int(42); // ❌ Плохо: утечка при исключении
// Правильно:
auto ptr = make_unique<int>(42); // ✅ Хорошо: RAII
Цитата из C++ говорителя Герба Саттера:
"Use unique_ptr. Use unique_ptr. Use unique_ptr. And if you really need shared ownership, use shared_ptr."
Итоговые рекомендации
unique_ptr — это основной выбор для управления памятью в современном C++. Он:
- Быстрый (нулевой оверхед)
- Безопасный (RAII)
- Выражает намерение (единоличное владение)
- Может двигаться между контекстами (move semantics)
shared_ptr — это инструмент для совместного владения. Он:
- Медленнее (атомарные операции)
- Потребляет больше памяти (control block)
- Решает проблему разделённого владения
- Требует осторожности с циклическими ссылками (weak_ptr)
Правило большого пальца: попробуйте unique_ptr сначала, и только если не получается — переходите на shared_ptr.