← Назад к вопросам

В чём разница между shared_ptr и unique_ptr?

1.3 Junior🔥 192 комментариев
#ООП и проектирование#Умные указатели и управление памятью#Язык C++

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

shared_ptr vs unique_ptr

Определения

unique_ptr — интеллектуальный указатель, который владеет объектом исключительно. Только один unique_ptr может владеть объектом. Когда unique_ptr удаляется, объект удаляется автоматически.

shared_ptr — интеллектуальный указатель, который владеет объектом совместно. Несколько shared_ptr могут указывать на один объект. Объект удаляется, когда последний shared_ptr удаляется (счётчик ссылок достигает нуля).

Сравнительная таблица

Характеристикаunique_ptrshared_ptr
ВладениеИсключительноеСовместное
Количество владельцев1N (счётчик ссылок)
Перемещение (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 когда:

  1. Один объект владеет ресурсом (99% случаев!)
  2. Нужна явная семантика владения
  3. Критична производительность
  4. Объект живёт ровно столько, сколько его владелец

Используйте shared_ptr когда:

  1. Несколько объектов должны совладать
  2. Время жизни неясно заранее
  3. Нужно безопасно передавать указатели между потоками
  4. Граф зависимостей сложный (но используйте 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++. Он:

  1. Быстрый (нулевой оверхед)
  2. Безопасный (RAII)
  3. Выражает намерение (единоличное владение)
  4. Может двигаться между контекстами (move semantics)

shared_ptr — это инструмент для совместного владения. Он:

  1. Медленнее (атомарные операции)
  2. Потребляет больше памяти (control block)
  3. Решает проблему разделённого владения
  4. Требует осторожности с циклическими ссылками (weak_ptr)

Правило большого пальца: попробуйте unique_ptr сначала, и только если не получается — переходите на shared_ptr.

В чём разница между shared_ptr и unique_ptr? | PrepBro