Введение
Статья 521 разобрала struct как способ группировки данных — C++ расширяет эту концепцию через class, добавляя методы и контроль доступа. В этой статье разберём, чем class отличается от struct в C++ (отличие минимальнее, чем многие ожидают), и что такое инкапсуляция как практический принцип, а не просто абстрактный термин.
Концепция
В C++ (в отличие от чистого C, где struct может содержать только данные) и struct, и class могут содержать как поля, так и методы, и единственное формальное различие между ними — уровень доступа по умолчанию: члены struct по умолчанию публичны (public), тогда как члены class по умолчанию приватны (private). Инкапсуляция — принцип сокрытия внутренней реализации класса (приватных полей) от внешнего кода, предоставляющий доступ к данным только через явно определённый, контролируемый публичный интерфейс (методы) — это позволяет изменять внутреннюю реализацию класса без необходимости менять код, использующий этот класс через его публичный интерфейс, при условии, что сам интерфейс остаётся неизменным.
Пример кода: Классы в C++
#include <iostream>
// struct — поля публичны ПО УМОЛЧАНИЮ
struct PointStruct {
int x, y; // публичные поля, доступные напрямую снаружи без ограничений
};
// class — поля приватны ПО УМОЛЧАНИЮ
class PointClass {
int x, y; // ПРИВАТНЫЕ поля — недоступны напрямую снаружи класса!
public:
PointClass(int xVal, int yVal) : x(xVal), y(yVal) {}
int getX() const { return x; } // доступ ТОЛЬКО через явно предоставленный публичный метод
int getY() const { return y; }
};
int main()
{
PointStruct ps;
ps.x = 10; // прямой доступ — допустимо для struct (публичные поля по умолчанию)
PointClass pc(10, 20);
// pc.x = 10; // ОШИБКА КОМПИЛЯЦИИ — x приватно, недоступно напрямую
std::cout << pc.getX() << std::endl; // доступ ТОЛЬКО через публичный метод getX()
return 0;
}
// Инкапсуляция в действии — изменение внутренней реализации без изменения публичного интерфейса
class Temperature {
public:
Temperature(double celsius) : m_celsius(celsius) {}
double getCelsius() const { return m_celsius; }
double getFahrenheit() const { return m_celsius * 9.0 / 5.0 + 32.0; } // вычисляется "на лету"
void setCelsius(double value)
{
if (value < -273.15) { // защита инварианта — нельзя установить температуру ниже абсолютного нуля
m_celsius = -273.15;
} else {
m_celsius = value;
}
}
private:
double m_celsius; // ВНУТРЕННЕЕ представление — пользователь класса не обязан знать об этой детали
};
Пояснения к коду
Сравнение PointStruct/PointClass показывает единственное, формальное различие — для struct поля x/y доступны напрямую (ps.x = 10), тогда как для class те же поля недоступны напрямую снаружи и требуют явного публичного метода (getX()) для чтения, что демонстрирует именно разный уровень доступа по умолчанию, а не какое-либо более глубокое техническое различие между двумя ключевыми словами. Temperature::setCelsius() показывает практическую ценность инкапсуляции — метод проверяет инвариант (температура не может быть ниже абсолютного нуля) перед изменением внутреннего поля, что было бы невозможно гарантировать, если бы m_celsius было публичным полем, доступным для прямого, неконтролируемого изменения снаружи класса в произвольное, в том числе физически невозможное значение.
Подводные камни
- Использование
structдля классов с нетривиальной логикой и инвариантами, требующими защиты через приватность, по привычке или из соображений «более короткого» синтаксиса (без необходимости писатьpublic:явно) — хотя техническиstructспособен на то же самое, что иclass(включая приватные поля при явном указанииprivate:), общепринятое соглашение современного C++ — использоватьstructдля простых, преимущественно «плоских» структур данных без существенной инкапсулированной логики (какPoint), иclass— для сущностей с поведением и защищаемыми инвариантами, и нарушение этого соглашения (хотя формально допустимое) может сбить с толку других разработчиков, ожидающих, чтоstructсигнализирует именно о простой структуре данных. - Избыточная инкапсуляция через создание геттера/сеттера для абсолютно каждого приватного поля без какой-либо дополнительной логики или защиты инварианта — если сеттер просто присваивает значение без какой-либо проверки или дополнительной логики (
void setX(int value) { x = value; }), это формально соблюдает синтаксис инкапсуляции, но не даёт никакой реальной практической выгоды по сравнению с простым публичным полем, и осознанная инкапсуляция должна применяться там, где она действительно даёт ценность (защита инварианта, скрытие деталей реализации, возможность будущего изменения внутреннего представления), а не механически, для абсолютно каждого поля без разбора. - Прямой доступ к приватным полям другого экземпляра ТОГО ЖЕ класса из метода этого класса, что технически разрешено в C++ (приватность действует на уровне класса, а не отдельного объекта), но может вводить в заблуждение разработчиков, ожидающих более строгой, «пообъектной» модели приватности по аналогии с некоторыми другими языками программирования — методы класса в C++ имеют доступ к приватным полям ЛЮБОГО объекта того же класса, а не только текущего объекта (
this), что иногда используется намеренно (например, для реализации оператора сравнения двух объектов того же класса), но требует понимания этого менее интуитивного аспекта модели доступа C++. - Игнорирование того, что инкапсуляция защищает от случайных, неосторожных ошибок использования класса, но не является механизмом безопасности против намеренного обхода, поскольку приватность в C++ — это конструкция, проверяемая компилятором на этапе компиляции, а не механизм защиты данных во время выполнения, и обращение к приватным членам класса всё же технически возможно через явные, низкоуровневые трюки (приведение указателей, доступ через friend, статья 536, или прямую манипуляцию памятью) — инкапсуляция C++ — инструмент дисциплины и архитектурной чистоты кода, а не криптографической защиты данных от действительно намеренного, целенаправленного обхода.