Перейти к основному содержимому

Класс в C++ — this, static, friend и вложенные типы

Разработчику Архитектору
О чём эта статья

Дополнение к ООП в C++: детали объявления класса, которые в учебниках идут сразу после public / private.

Здесь — this, статические члены, friend, вложенные и локальные классы. struct, class, union: типы и строки. friend-функции и спецчлены: функции.


Указатель this

Когда вы вызываете метод объекта:

Counter c;
c.increment();

Разбор:

  • Counter c; создаёт объект на стеке, его время жизни ограничено текущей областью видимости.
  • Вызов c.increment() неявно передаёт адрес объекта как this.
  • Метод получает доступ к полям объекта через this->... (явно или неявно).
  • Такой вызов иллюстрирует базовую объектную модель C++: метод всегда работает с конкретным экземпляром.
  • По завершении блока объект будет автоматически разрушен деструктором.

компилятор неявно передаёт в метод адрес текущего объекта — указатель this типа T*const-методе — const T*).

class Counter {
int value_ = 0;
public:
Counter& increment() {
++value_; // то же, что ++this->value_
return *this; // вернуть сам объект для цепочки
}
int get() const { return value_; }
};

Counter c;
c.increment().increment(); // value_ станет 2

Разбор:

  • Поле value_ инкапсулировано внутри класса и изменяется только через публичный метод increment().
  • return *this; возвращает ссылку на текущий объект, поэтому можно строить цепочки вызовов.
  • В const-методе get() тип this становится const Counter*, изменение полей запрещено.
  • Выражение c.increment().increment() дважды вызывает один и тот же метод над тем же объектом.
  • Такой стиль часто используют в fluent API, где действия читаются как цепочка шагов.

Указатель this - цепочки и различие полей

ПрименениеПримерЗачем
Цепочка вызововreturn *this;obj.a().b().c();
Имя поля = имя параметраthis->name = name;различить поле и аргумент
Передать себя наружуregistry.add(this);колбэки, регистрация (осторожно с временем жизни!)

this недоступен в статических методах: у них нет конкретного объекта-получателя.


this и fluent API в прикладном коде

Возврат *this часто применяют в конфигураторах:

class RequestBuilder {
std::string url_;
int timeout_ms_ = 1000;
public:
RequestBuilder& url(std::string u) {
url_ = std::move(u);
return *this;
}
RequestBuilder& timeout(int ms) {
timeout_ms_ = ms;
return *this;
}
};

Разбор:

  • RequestBuilder инкапсулирует параметры запроса и постепенно накапливает состояние объекта.
  • Методы url() и timeout() возвращают RequestBuilder&, что поддерживает fluent-цепочки.
  • std::move(u) перемещает строку в поле url_, снижая лишние копирования.
  • Поле timeout_ms_ имеет значение по умолчанию и переопределяется только при явном вызове timeout.
  • Шаблон удобен для тестов и конфигурации, когда объект собирается шаг за шагом.

Такой API читается как цепочка шагов и хорошо работает в тестах.


Статические члены класса

Статическое полеодна переменная на весь класс, общая для всех экземпляров. Статический метод — функция, которую вызывают как Class::method(), без объекта.

class Session {
static int active_count_;
std::string id_;
public:
Session(std::string id) : id_(std::move(id)) { ++active_count_; }
~Session() { --active_count_; }

static int active_count() { return active_count_; }
};

int Session::active_count_ = 0;

Разбор:

  • active_count_ — одно общее поле для всех экземпляров Session, а не копия в каждом объекте.
  • Конструктор увеличивает счётчик активных сессий, деструктор уменьшает его при уничтожении объекта.
  • static-метод active_count() можно вызвать без объекта: Session::active_count().
  • Внешнее определение int Session::active_count_ = 0; создаёт реальное хранилище для статического поля.
  • Паттерн полезен для метрик и диагностики жизненного цикла сущностей.

Разбор:

ЭлементСмысл
static int active_count_объявление: "будет общий счётчик"
++active_count_ в конструкторекаждый новый Session увеличивает общий счётчик
static int active_count()можно вызвать Session::active_count() без создания Session
int Session::active_count_ = 0; вне классаопределение и инициализация единственного экземпляра поля (до C++17 обязательно в .cpp)

C++17 — inline static

class Session {
inline static int active_count_ = 0;
// ...
};

Разбор:

  • inline static (C++17+) позволяет и объявить, и инициализировать статическое поле прямо в классе.
  • Это снимает необходимость отдельного определения в .cpp и упрощает header-only код.
  • inline здесь связан с ODR: одинаковые определения в нескольких единицах трансляции корректно сливаются.
  • Подход снижает риск забыть определение и получить linker error.
  • Семантика поля остаётся прежней: один общий счётчик на класс.

Инициализация прямо в заголовке — удобно для header-only библиотек.


Правила и осторожности

  • Статический метод не видит нестатические поля без объекта (Session s; ...).
  • Порядок инициализации статических объектов между разными .cpp не гарантирован — см. C++ - язык системного программирования (static initialization order fiasco).
  • Счётчики и кэши на класс — нормальная задача для static. Глобальное состояние всего приложения лучше выносить в namespace или явный singleton с узким API.

Практика для static в многопоточной среде

Если статическое поле меняется из нескольких потоков, добавляйте синхронизацию:

  • std::atomic<int> для простого счётчика;
  • std::mutex для составных операций;
  • thread_local для данных, привязанных к потоку.

Связанный материал про параллелизм: Многопоточность и асинхронное выполнение в C++.


Методы inline в определении класса

Метод, написанный внутри объявления класса, по умолчанию inline (для обычных, не шаблонных случаев):

class Point {
int x_, y_;
public:
Point(int x, int y) : x_(x), y_(y) {}
int x() const { return x_; } // inline по умолчанию
};

Разбор:

  • Метод, определённый внутри класса, считается inline в смысле правила одного определения (ODR).
  • x() const — короткий геттер без побочных эффектов, безопасен для const-объектов.
  • Конструктор инициализирует поля списком инициализации, что корректно и эффективно.
  • Такой формат часто используют в заголовках для тривиальных методов.
  • Реальное "встраивание" в машинный код компилятор решает сам, независимо от inline.

Слово inline сегодня в первую очередь про ODR (одно определение в нескольких единицах трансляции), а не про обещание "встроить в машинный код" — см. Функции и лямбда-выражения в C++. Короткие геттеры в .h — типичный случай.


Дружественные функции и классы (friend)

По умолчанию private и protected видны только методам своего класса. friend открывает доступ конкретной функции или конкретному классу.


friend-функция (часто operator<<)

class Secret {
int code_;
friend std::ostream& operator<<(std::ostream& os, const Secret& s);
public:
explicit Secret(int c) : code_(c) {}
};

std::ostream& operator<<(std::ostream& os, const Secret& s) {
return os << s.code_;
}

Разбор:

  • operator<< объявлен как friend, поэтому имеет доступ к приватному полю code_.
  • Функция свободная (не метод), что соответствует стандартной форме потокового вывода.
  • explicit Secret(int c) предотвращает неявные преобразования числа в Secret.
  • Возврат std::ostream& позволяет цепочки вида std::cout << a << b.
  • Такой шаблон считается каноническим для печати пользовательских типов.

operator<<свободная функция (не метод класса). Без friend она не смогла бы читать code_.


friend-класс

class Engine {
int rpm_ = 0;
friend class EngineTester;
public:
void rev() { rpm_ += 100; }
};

class EngineTester {
public:
static int read_rpm(const Engine& e) { return e.rpm_; }
};

Разбор:

  • friend class EngineTester; открывает всему классу EngineTester доступ к приватным данным Engine.
  • Метод read_rpm читает rpm_ напрямую, обходя публичный интерфейс.
  • Такой доступ удобно использовать для тестов или узкоспециализированной диагностики.
  • Дружба задаётся явно и не распространяется автоматически на другие классы.
  • При злоупотреблении friend ослабляет инкапсуляцию, поэтому его держат локальным и осознанным.

Весь EngineTester видит private у Engine.


Свойства дружбы

  • не наследуется: друг Base не становится другом Derived автоматически;
  • не транзитивна — если A друг B, а B друг C, A не видит C;
  • ломает инкапсуляцию — применяйте точечно (тесты, один модуль).

Альтернатива: публичный узкий API, метод friendForTesting() под #ifdef TESTING.


Когда friend действительно полезен

Хороший пример — оператор сериализации или отладочный дамп структуры. Тогда friend открывает доступ строго одной функции и удерживает основной API компактным.

Слабый пример — массовое объявление дружбы для "удобства". Такой подход размывает границы модуля и усложняет сопровождение.


Вложенные классы (nested)

Класс внутри другого класса — логическая группировка и доступ к private внешнего:

Код ITЗагрузка примера кода…

Разбор:

  • Iterator вложен в HashTable, что логически связывает тип итератора с контейнером.

  • Приватный конструктор Iterator контролирует создание итераторов только через HashTable.

  • friend class HashTable; даёт внешнему классу доступ к приватным деталям итератора.

  • operator!= реализует базовую операцию сравнения для обхода в циклах.

  • Публичные begin()/end() формируют привычный контракт контейнера STL-стиля.

  • Внешний код обращается к типу как HashTable::Iterator.

  • Конструктор Iterator private — создавать итератор может только HashTable (через friend).

  • Вложенный тип можно объявить private, если клиентам нужен только Iterator, а не детали HashTable.


Локальные классы

Класс, объявленный внутри функции:

void process() {
class LocalHelper {
public:
static int transform(int x) { return x * 2; }
};
int v = LocalHelper::transform(21);
}

Разбор:

  • LocalHelper виден только внутри process(), что ограничивает его область применения.
  • static int transform(int x) не требует объекта, это чистая утилитарная операция.
  • Локальный класс помогает не выносить мелкие детали на уровень файла или заголовка.
  • Переменная v получает результат локального преобразования, после выхода из функции всё скрыто.
  • На практике альтернативой часто служит лямбда, если не нужен отдельный именованный тип.

Ограничения:

  • тип нельзя использовать снаружи этой функции (шаблоны, заголовки);
  • нестатические методы локального класса не вызывают нестатические функции охватывающей функции напрямую (в отличие от лямбды с [&]);
  • на практике чаще берут лямбду или класс в анонимном namespace в .cpp.

Где объявлять класс

ОбластьГде объявленКто видит
Глобальный (namespace)вне функцийвезде, где подключён заголовок
Локальныйвнутри функциитолько эта функция
Вложенныйвнутри классачерез Outer::Inner

Класс в анонимном namespace в .cpp скрывает детали модуля (дополняет PIMPL — Идиомы современного C++).


Связанные материалы