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

Классы, структуры и инкапсуляция

Поймите разницу между структурой и классом, спецификаторами доступа, инкапсуляцией, функциями членов const, статичными членами и современной агрегированной инициализацией.

Что такое класс?

класс в C++ представляет собой тип, определяемый пользователем, который объединяет данные (переменные члены) и операции с этими данными (функции членов) в единое целое. Это объединение называется инкапсуляция — один из четырех столпов ООП.

Ключевое понимание заключается в том, что класс определяет инвариант: набор правил, которые всегда должны быть верными о его внутреннем состоянии. Например, для класса Date может потребоваться, чтобы месяц всегда был между 1 и 12. Инкапсуляция позволяет применять эти инварианты, контролируя доступ к внутреннему состоянию.

Класс vs класс

В C++, struct и class почти идентичны**. Единственным отличием является спецификатор доступа по умолчанию: struct по умолчанию public, а class по умолчанию private. Конвенция: используйте struct для простых агрегатов данных, class, когда у вас есть инварианты для обслуживания.

struct_vs_class.cpp
#include <iostream>
#include <string>

// struct: members are public by default
struct Point {
    double x;  // public
    double y;  // public
};

// class: members are private by default
class BankAccount {
    std::string owner;   // private
    double balance;      // private

public:
    BankAccount(const std::string& name, double initial)
        : owner{name}, balance{initial} {}

    double get_balance() const { return balance; }

    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
};

int main() {
    Point p{3.0, 4.0};  // aggregate initialization
    std::cout << p.x << ", " << p.y << '\n';

    BankAccount acct{"Alice", 1000.0};
    acct.deposit(500.0);
    std::cout << acct.get_balance() << '\n';  // 1500
    // acct.balance = -999;  // ERROR: private member

    return 0;
}

Указатели доступа и этот указатель

C++ обеспечивает три уровня доступа: public (каждый может получить доступ), private (только класс и его друзья) и protected (класс, его друзья и производные классы). Указатель this является неявным указателем на текущий объект, доступный внутри каждой нестатической функции-члена.

access_specifiers.cpp
#include <iostream>
#include <string>

class Employee {
private:
    std::string name_;
    int id_;

protected:
    double salary_;  // accessible to derived classes

public:
    Employee(const std::string& name, int id, double salary)
        : name_{name}, id_{id}, salary_{salary} {}

    // const member function: promises not to modify the object
    const std::string& get_name() const { return name_; }
    int get_id() const { return id_; }

    // 'this' pointer usage — return *this for method chaining
    Employee& set_name(const std::string& name) {
        this->name_ = name;  // 'this->' disambiguates
        return *this;
    }

    Employee& set_id(int id) {
        this->id_ = id;
        return *this;
    }

    void print() const {
        std::cout << "Employee{" << name_ << ", #" << id_
                  << ", $" << salary_ << "}\n";
    }
};

int main() {
    Employee e{"Bob", 42, 75000.0};
    e.set_name("Robert").set_id(43);  // method chaining via *this
    e.print();  // Employee{Robert, #43, $75000}
    return 0;
}

Статические члены и друзья

Статические члены принадлежат к классу, а не к какому-либо случаю. Используйте их для общих функций состояния или полезности. Ключевое слово friend предоставляет нечлену функцию или другой класс доступа к частным членам - используйте экономно, поскольку это нарушает инкапсуляцию.

static_friend.cpp
#include <iostream>

class Widget {
    int value_;

    // C++17: inline static — define in header, no .cpp needed
    inline static int count_ = 0;

public:
    explicit Widget(int v) : value_{v} { ++count_; }
    ~Widget() { --count_; }

    static int get_count() { return count_; }

    // friend function can access private members
    friend std::ostream& operator<<(std::ostream& os, const Widget& w);
};

std::ostream& operator<<(std::ostream& os, const Widget& w) {
    return os << "Widget(" << w.value_ << ')';  // accesses private value_
}

int main() {
    Widget a{10}, b{20};
    std::cout << Widget::get_count() << '\n';  // 2
    std::cout << a << '\n';  // Widget(10)
    {
        Widget c{30};
        std::cout << Widget::get_count() << '\n';  // 3
    }
    std::cout << Widget::get_count() << '\n';  // 2
    return 0;
}

Агрегированная инициализация и назначенные инициализаторы (C++20)

aggregate — это класс или структура, в которой нет объявленных пользователем конструкторов, нет частных/защищенных нестатических элементов данных, нет виртуальных функций и нет виртуальных базовых классов. C++20 добавляет обозначенные инициализаторы , позволяя вам назвать, какие члены вы инициализируете.

designated_init.cpp
#include <iostream>
#include <string>

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool use_tls = false;
    int max_connections = 100;
};

int main() {
    // Traditional aggregate initialization
    Config c1{"example.com", 443, true, 200};

    // C++20 designated initializers — much more readable
    Config c2{
        .host = "api.example.com",
        .port = 443,
        .use_tls = true
        // .max_connections uses default value 100
    };

    std::cout << c2.host << ':' << c2.port << '\n';
    std::cout << "TLS: " << std::boolalpha << c2.use_tls << '\n';
    std::cout << "Max: " << c2.max_connections << '\n';  // 100
    return 0;
}
Лучшая практика

Сделайте членов данных приватными по умолчанию и обнародуйте только то, что необходимо через минимальный публичный интерфейс. Если в классе нет инварианта для поддержания (это просто набор данных), используйте вместо него struct. Не добавляйте геттеров и сеттеров для каждого поля, что нарушает цель инкапсуляции. Вместо этого предоставьте значимые операции, такие как deposit() и withdraw(), а не set_balance().

Подводный камень

Если функция-член не изменяет состояние объекта, пометьте его const. Без const вы не можете вызвать функцию на объекте const или через ссылку const, что происходит все время при прохождении объектов const&. Отсутствующий const является распространенным источником ошибок компиляции в больших кодовых базах.

Ключевые выводы
  • struct и class отличаются только доступом по умолчанию: struct является общедоступным, class является частным.
  • Инкапсуляция защищает классовые инварианты — используйте данные private с значимым публичным интерфейсом
  • Немутирующие функции const для работы с ссылками const
  • Использовать inline static (C++17) для статических элементов, определенных в заголовках
  • Назначенные инициализаторы C++20 делают агрегированную инициализацию читаемой и самодокументирующейся.
  • friend должен использоваться экономно — как правило, для перегрузок операторов, которые требуют частного доступа.

Тест — проверьте свои знания

(15 XP)

1. В чем разница между `struct` и `class` в C++?

2. Почему вы должны пометить функции участника доступа как `const`?

3. Что делает `inline static int count_ = 0;` в C++17?