Классы в C++: объявление, поля, методы и конструкторы

Классы в C++: объявление, поля, методы и конструкторы Полезное

Класс в C++ — это пользовательский тип данных, который объединяет данные (поля) и работающие с ними функции (методы) в одну сущность. Объект — это конкретная переменная такого типа. Аналогия-якорь: класс — это чертеж, объект — построенный по нему дом; чертеж один, домов по нему может быть много.

Ниже разберем именно C++ (не Java и не C#): как объявить class, задать поля и методы, закрыть данные модификаторами доступа, написать конструктор и деструктор, для чего нужен указатель this, чем class отличается от struct и как выглядит простое наследование. Базовый синтаксис классов стабилен, поэтому примеры одинаково работают в C++17, C++20 и C++23.

Минимальный рабочий пример

Начнем с законченного кода, который компилируется и запускается. Класс Counter хранит одно число и умеет его увеличивать.

#include <iostream>

class Counter {
private:
    int value;            // поле, закрытое снаружи

public:
    Counter(int start) {  // конструктор
        value = start;
    }

    void increment() {    // метод: меняет состояние
        value++;
    }

    int get() const {     // метод чтения: объект не меняет
        return value;
    }
};

int main() {
    Counter c(10);        // конструктор ставит value = 10
    c.increment();
    c.increment();
    std::cout << c.get() << "\n";
    return 0;
}

В консоли появится 12: старт 10, два вызова increment() дают 12, get() возвращает результат. Разберем этот код по частям.

Объявление class, поля и методы

Объявление начинается со слова class, за ним идет имя типа и тело в фигурных скобках. После закрывающей скобки обязательна точка с запятой — частая ошибка новичка ее пропустить.

Внутри тела различают две вещи. Поля (данные-члены) — это переменные, которые хранит каждый объект; у Counter это value. Методы (функции-члены) — это функции внутри класса, работающие с его полями; у Counter это increment и get.

Метод, который не изменяет объект, помечают словом const после списка параметров: int get() const. Компилятор тогда запретит случайную запись в поля внутри такого метода — это защита от ошибок, а не просто пометка.

Обращение к членам объекта идет через точку (c.increment()), а через указатель на объект — стрелкой (p->increment()).

Модификаторы доступа: public, private, protected

Модификатор доступа задает, откуда виден член класса. Их три, и они действуют на все члены до следующего модификатора.

Модификатор Доступ изнутри класса Доступ у наследника Доступ извне (через объект)
private да нет нет
protected да да нет
public да да да

private — данные видны только методам самого класса. protected — как private, но еще открыт классам-наследникам. public — открытый интерфейс, доступный из любого места программы.

Попытка обратиться к закрытому полю снаружи не пройдет компиляцию. Покажем ошибку тройкой.

Неверно:

Counter c(5);
c.value = 100;   // прямая запись в приватное поле

Фактический результат — ошибка на этапе компиляции (точная формулировка зависит от компилятора, у GCC и Clang текст немного разный):

error: 'int Counter::value' is private within this context

Исправление — менять поле только через метод, который контролирует значение:

Counter c(5);
c.increment();   // разрешенный путь: публичный метод

Инкапсуляция: почему поля закрывают

Инкапсуляция — это сокрытие внутренних данных за публичными методами, чтобы объект сам следил за корректностью своего состояния. Общепринятый стиль в C++: поля делают private, а доступ к ним дают через методы (их часто зовут геттеры и сеттеры).

Смысл не в самих геттерах, а в контроле: сеттер может проверить значение и не пустить в объект мусор.

void set(int value) {           // метод класса Temperature
    if (value < -273) {         // ниже абсолютного нуля не бывает
        celsius = -273;
    } else {
        celsius = value;
    }
}

Если вызвать t.set(-500), поле celsius станет -273, а не -500 — сеттер отклонил недопустимое значение. При прямом доступе к полю такой защиты бы не было; в этом практическая польза закрытых данных.

Конструкторы и деструкторы

Конструктор — это метод, который вызывается автоматически при создании объекта и приводит его в корректное начальное состояние. Имя конструктора совпадает с именем класса, тип возврата не указывается.

Начальные значения полей лучше задавать через список инициализации — двоеточие после списка параметров: Rectangle(int w, int h) : width(w), height(h) {}. Это эффективнее присваивания в теле и обязательно для полей-констант и ссылок.

Деструктор — это метод, который вызывается автоматически при уничтожении объекта (например, при выходе из области видимости). Его имя — тильда плюс имя класса: ~Rectangle(). Деструктор освобождает то, что объект захватил: память, файл, соединение.

Пример с проговоренным порядком вызовов:

#include <iostream>
#include <string>

class FileGuard {
private:
    std::string name;

public:
    FileGuard(const std::string& fileName) : name(fileName) {
        std::cout << "open " << name << "\n";
    }

    ~FileGuard() {
        std::cout << "close " << name << "\n";
    }
};

int main() {
    FileGuard f("data.txt");
    std::cout << "work\n";
    return 0;
}

Вывод будет таким:

open data.txt
work
close data.txt

Конструктор отработал при создании f, а деструктор — сам, в конце main, когда объект уходит из области видимости. Это идиома RAII (Resource Acquisition Is Initialization): ресурс захватывают в конструкторе, освобождают в деструкторе, и освобождение происходит автоматически, даже если функция завершится из-за исключения. Граница упрощения: для владения динамической памятью в современном C++ обычно берут не ручной new/delete, а готовые типы (std::string, std::vector, std::unique_ptr), которые реализуют RAII за вас.

Указатель this

this — это указатель внутри метода на тот объект, для которого метод вызван. Он нужен, когда имя параметра совпадает с именем поля: this-> явно указывает на поле, а не на параметр.

class Point {
private:
    int x;
    int y;

public:
    Point(int x, int y) {
        this->x = x;   // слева - поле, справа - параметр
        this->y = y;
    }

    int getX() const { return x; }
};

Без this-> строка x = x присвоила бы параметр самому себе, а поле осталось бы неинициализированным. Список инициализации : x(x), y(y) решает ту же задачу без this.

class против struct в C++

В C++ class и struct — почти одно и то же, отличие только в доступе по умолчанию. Это важное различие, которое часто путают.

Свойство class struct
Доступ по умолчанию private public
Наследование по умолчанию private public
Может иметь методы, конструкторы, наследование да да

То есть struct в C++ — это не «структура из чистого Си без методов»; у него есть методы, конструкторы и наследование, просто члены по умолчанию открыты. На практике struct берут для простых наборов данных, а class — когда важна инкапсуляция; это соглашение о стиле, а не требование языка.

Простое наследование

Наследование позволяет одному классу (наследнику) получить поля и методы другого (базового) и добавить свои. Запись class Dog : public Animal означает публичное наследование.

#include <iostream>
#include <string>

class Animal {
protected:
    std::string name;   // protected: открыто наследнику, закрыто извне

public:
    Animal(const std::string& n) : name(n) {}

    void eat() const {
        std::cout << name << " eats\n";
    }
};

class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}  // вызов конструктора базы

    void bark() const {
        std::cout << name << " barks\n";       // доступ к protected-полю базы
    }
};

int main() {
    Dog d("Rex");
    d.eat();    // Rex eats
    d.bark();   // Rex barks
    return 0;
}

В консоли появится две строки: Rex eats и Rex barks. Наследник Dog получил метод eat от Animal и добавил свой bark; поле name доступно ему, потому что оно protected, а конструктор наследника передает имя в конструктор базы через список инициализации.

Это минимальный случай. Полиморфизм через virtual-методы и абстрактные классы — отдельная большая тема, которую здесь не раскрываем.

Выводы

  • Класс в C++ объявляют словом class, он объединяет поля (данные) и методы (функции); объект — это конкретная переменная этого типа.
  • Модификаторы private, protected и public задают видимость членов; типичный стиль — закрытые поля и публичные методы.
  • Конструктор задает начальное состояние объекта, деструктор освобождает ресурсы; связка «захват в конструкторе — освобождение в деструкторе» называется RAII.
  • Указатель this различает поле и одноименный параметр; список инициализации часто решает ту же задачу чище.
  • В C++ class и struct различаются только доступом по умолчанию: private у class, public у struct.

Где применяется / связь с практикой

Классы — это фундамент любого нетривиального кода на C++: движки игр, встраиваемые системы, торговые и высоконагруженные сервисы строятся на пользовательских типах с инкапсуляцией и RAII. Умение грамотно закрыть данные, написать корректный конструктор и деструктор и не утечь ресурсами — базовый навык C++-разработчика, который проверяют на собеседованиях.

Освойте тему на практике

Если хотите системно пройти путь от синтаксиса классов до управления памятью и многопоточности, посмотрите курс C++ Basic — там разбор классов идет на практических задачах с проверкой кода. Понять формат преподавания и уровень требований помогают открытые уроки Otus — это бесплатные занятия по темам курсов.

FAQ

Обязательно ли писать конструктор?
Нет. Если не объявить ни одного конструктора, компилятор сгенерирует конструктор по умолчанию. Но как только вы напишете свой конструктор с параметрами, автоматический конструктор без параметров пропадет — его придется объявить явно, если он нужен.

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

Можно ли методам класса обращаться к приватным полям другого объекта того же класса?
Да. Модификатор доступа действует на уровне класса, а не отдельного объекта. Метод класса Counter может читать приватные поля любого переданного ему объекта Counter, например при сравнении двух счетчиков.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)