Что такое счетчик ссылок (reference counting) в C++?

Ответ

Счетчик ссылок — это техника управления временем жизни объекта, при которой сам объект (или связанный с ним управляющий блок) хранит целочисленный счетчик активных ссылок на себя. При создании новой ссылки счетчик увеличивается, при уничтожении ссылки — уменьшается. Когда счетчик достигает нуля, память объекта освобождается.

Реализация в стандартной библиотеке: В C++ эта концепция лежит в основе умного указателя std::shared_ptr.

#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource acquiredn"; }
    ~Resource() { std::cout << "Resource destroyedn"; }
    void use() { std::cout << "Resource usedn"; }
};

int main() {
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>(); // Счетчик = 1
    {
        std::shared_ptr<Resource> ptr2 = ptr1; // Счетчик увеличивается до 2
        ptr2->use();
    } // ptr2 выходит из области видимости, счетчик уменьшается до 1
    ptr1->use();
    // ptr1 выходит из области видимости, счетчик становится 0, ресурс уничтожается
    return 0;
}

Вывод:

Resource acquired
Resource used
Resource used
Resource destroyed

Преимущества и недостатки:

  • Плюсы: Предсказуемое время освобождения памяти (детерминированное), отсутствие пауз на сборку мусора.
  • Минусы:
    1. Циклические ссылки: Если два shared_ptr ссылаются друг на друга, их счетчики никогда не обнулятся, что приводит к утечке памяти. Решение — использование std::weak_ptr для разрыва циклов.
    2. Накладные расходы: Необходимость атомарного увеличения/уменьшения счетчика в многопоточных сценариях.
    3. Дополнительная память: Хранение счетчика (обычно в управляющем блоке).

Ответ 18+ 🔞

А, ну это же классика, ёпта! Счётчик ссылок — это как будто твой объект завёл себе личного бухгалтера, который сидит и считает, сколько сейчас чуваков на него пальцем показывает. Каждый раз, когда новый приятель появляется и говорит «я тоже на него ссылаюсь!», бухгалтер ставит галочку. А как только приятель отваливает — галочку вычёркивает. Когда все галочки кончились, объекту приходит пиздец, и память освобождают. Просто и гениально, как палка-копалка.

Где это встроено в язык, чтобы не париться? В C++ за тебя всё уже сделали, это умный указатель std::shared_ptr. Он и есть тот самый бухгалтер с блокнотиком.

#include <iostream>
#include <memory>

class Resource {
public:
    Resource() { std::cout << "Resource acquiredn"; }
    ~Resource() { std::cout << "Resource destroyedn"; }
    void use() { std::cout << "Resource usedn"; }
};

int main() {
    // Создаём ресурс. Бухгалтер просыпается и пишет в блокнот: "ссылок = 1"
    std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>();
    {
        // А вот его клон! Бухгалтер вздыхает и исправляет на "ссылок = 2"
        std::shared_ptr<Resource> ptr2 = ptr1;
        ptr2->use();
    } // ptr2 сдох, выйдя за скобки. Бухгалтер радостно вычёркивает: "ссылок = 1"
    ptr1->use();
    // И вот ptr1 тоже кончился. Бухгалтер смотрит: ноль. Делает пометку "мёртв" и вызывает санитаров.
    return 0;
}

Что на экране:

Resource acquired
Resource used
Resource used
Resource destroyed

Чем это хорошо и чем — пиздец как плохо:

  • Плюсы: Всё чётко и по расписанию. Объект удалится ровно тогда, когда на него все забили. Никакого мусорщика, который приходит непонятно когда и всё тормозит.
  • Минусы:
    1. Циклические ссылки — это ядрёна вошь. Представь, два объекта взялись за руки и друг на друга ссылаются. Их бухгалтеры сидят и ждут, когда другой объект умрёт первым. Ждут, ждут... а они оба бессмертные получились, утечка памяти! Спасение — std::weak_ptr, это как слабая ссылка, которая не считается в общем зачёте и цикл рвёт.
    2. Тормоза в многопоточке. Чтобы два потока одновременно не начали править один счётчик и не накосячили, его увеличение/уменьшение делают атомарным. А это не бесплатно, волнение ебать.
    3. Жрёт память. Самому бухгалтеру (управляющему блоку) тоже надо где-то жить. Так что за удобство платим лишними байтами.