базовые концепции ООП — поляbaseline OOP concepts — fields
Поля — это данные, хранящиеся внутри каждого объекта класса. Три уровня доступа (public/protected/private) реализуют инкапсуляцию на уровне компилятора, а два менее очевидных инструмента — in-class инициализаторы по умолчанию и mutable — решают конкретные практические задачи: не повторять значение по умолчанию в каждом конструкторе и разрешить изменение поля из const-метода.Fields are the data stored inside every object of a class. The three access levels (public/protected/private) implement encapsulation at the compiler level, and two less obvious tools — in-class default member initializers and mutable — solve specific practical problems: not repeating a default value in every constructor, and allowing a field to change from within a const method.
Поле (member variable, data member) — переменная, объявленная внутри класса, существующая как часть каждого объекта этого класса. В отличие от локальных переменных, время жизни поля привязано ко времени жизни объекта, которому оно принадлежит: поле создаётся при создании объекта и разрушается при его разрушении.A field (member variable, data member) is a variable declared inside a class that exists as part of every object of that class. Unlike local variables, a field's lifetime is tied to the lifetime of the object it belongs to: it's created when the object is created and destroyed when the object is destroyed.
class Timer {
std::chrono::steady_clock::time_point start; // поле — часть каждого объекта Timer// a field — part of every Timer object
public:
Timer() : start(std::chrono::steady_clock::now()) {}
};
Три уровня доступа определяют, откуда можно обратиться к полю напрямую: public — из любого кода, protected — из методов самого класса и его наследников, private — только из методов самого класса (наследники доступа не имеют).The three access levels control where a field can be accessed directly from: public — from any code, protected — from the class's own methods and its descendants, private — only from the class's own methods (descendants have no access).
struct Vec2 { double x, y; };
v.x = 999999; // любой код может присвоить что угодно — и это нормально для Vec2// any code can assign anything — and that's fine for Vec2
class Percentage {
double value; // приватно — нельзя обойти проверку// private — the check can't be bypassed
public:
void set(double v) { value = std::clamp(v, 0.0, 100.0); }
};
protected — компромисс, часто оказывающийся хуже обоих крайних вариантов: он открывает поле всем нынешним и будущим наследникам, фактически делая его частью публичного контракта класса (любое изменение имени/типа поля ломает все наследники), но не даёт настоящей инкапсуляции. На практике предпочтительнее private поле плюс protected метод-аксессор для наследников.protected is a compromise that often ends up worse than either extreme: it opens the field to every current and future descendant, effectively making it part of the class's public contract (any change to the field's name/type breaks every descendant), without providing real encapsulation. In practice, a private field plus a protected accessor method for descendants is usually preferable.
С C++11 поле можно инициализировать прямо в объявлении внутри класса — значение применяется автоматически к любому конструктору, который не переопределяет это поле в своём списке инициализации. До C++11 то же значение пришлось бы дублировать в списке инициализации каждого конструктора.Since C++11, a field can be initialized right at its declaration inside the class — the value applies automatically to any constructor that doesn't override that field in its own init list. Before C++11, the same value would need to be duplicated in every constructor's init list.
class Connection {
int timeout_ms = 5000; // значение по умолчанию — один раз, а не в каждом конструкторе// the default — written once, not in every constructor
bool use_tls = true;
public:
Connection() = default; // timeout_ms=5000, use_tls=true// timeout_ms=5000, use_tls=true
Connection(int t) : timeout_ms(t) {} // timeout_ms=t, use_tls всё равно true// timeout_ms=t, use_tls is still true
};
Поле, помеченное mutable, можно изменять даже из const-метода — единственное исключение из правила «const-метод не может менять состояние объекта». Легитимные случаи использования — кэширование результата дорогого вычисления и подсчёт статистики использования (мьютексы для потокобезопасности const-методов, счётчики обращений), то есть изменения, не влияющие на логическое состояние объекта, наблюдаемое извне.A field marked mutable can be changed even from a const method — the one exception to "a const method can't change the object's state." Legitimate uses are caching an expensive computation's result and tracking usage statistics (mutexes for making const methods thread-safe, access counters) — changes that don't affect the object's logical state as observed from outside.
class ExpensiveShape {
mutable std::optional<double> cached_area;
double radius;
public:
double area() const { // метод логически const — не меняет "форму", только кэширует// logically const — doesn't change the "shape", only caches
if (!cached_area) cached_area = 3.14159 * radius * radius;
return *cached_area;
}
};
Злоупотребление mutable (например, чтобы обойти const-проверку и незаметно изменить наблюдаемое состояние) — прямое нарушение контракта, который const обещает вызывающей стороне: «этот вызов не изменит объект».Abusing mutable (say, to sidestep the const check and quietly change observable state) directly violates the contract const promises the caller: "this call won't change the object."
Изменение protected поля в базовом классе может незаметно сломать код во всех классах-наследниках, разбросанных по кодовой базе или даже в разных библиотеках — эффективно то же самое, что менять публичный API, просто без предупреждения об этом со стороны компилятора на уровне интерфейса.Changing a protected field in a base class can silently break code in every descendant class, scattered across the codebase or even across different libraries — effectively the same as changing a public API, just without the compiler flagging it at the interface level.
Помимо порядка вызовов конструкторов (разобрано на странице про определение классов), порядок объявления полей также обычно определяет их физический порядок в памяти объекта — компиляторы вправе добавлять padding между полями для выравнивания, поэтому переупорядочивание полей по убыванию размера иногда уменьшает sizeof объекта.Beyond determining constructor call order (covered on the class-definition page), field declaration order also usually determines their physical order in the object's memory — compilers may insert padding between fields for alignment, so reordering fields from largest to smallest can sometimes shrink an object's sizeof.
const Widget& w не защищает mutable-поля w от изменения — const на ссылке/указателе останавливает изменение обычных полей, но mutable по определению обходит именно эту защиту, независимо от того, через что к объекту обращаются.const Widget& w doesn't protect w's mutable fields from being changed — const on a reference/pointer blocks changes to ordinary fields, but mutable by definition bypasses exactly that protection, regardless of what's used to access the object.