Основы объектно-ориентированного программирования на C++

Этот курс охватывает фундаментальные принципы ООП: инкапсуляцию, наследование и полиморфизм, а также их реализацию в современном стандарте C++. Вы научитесь проектировать надежные классы, управлять памятью и использовать механизмы абстракции для создания гибкой архитектуры программ.

1. Введение в классы и объекты: конструкторы, деструкторы и указатель this

Введение в классы и объекты: конструкторы, деструкторы и указатель this

Добро пожаловать в курс «Основы объектно-ориентированного программирования на C++». Это первая статья нашего цикла, и мы начнем с фундамента, на котором строится вся парадигма ООП. До этого момента, вероятно, вы писали программы в процедурном стиле: данные хранились в переменных, а логика — в функциях. Сегодня мы изменим этот подход и научимся объединять данные и поведение в единые сущности.

От процедурного мышления к объектному

В процедурном программировании мы думаем о действиях: «прочитать файл», «вычислить сумму», «вывести результат». В объектно-ориентированном программировании (ООП) мы думаем о сущностях: «Пользователь», «Кнопка», «Банковский счет».

Чтобы понять разницу, представьте завод по производству автомобилей.

!Чертеж — это класс, а реальные машины — это объекты.

Здесь есть два ключевых понятия:

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

    В языке C++ класс определяется ключевым словом class. Внутри него мы описываем:

    * Поля (атрибуты) — переменные, хранящие состояние объекта. * Методы — функции, описывающие поведение объекта.

    Рассмотрим простой пример класса, описывающего точку в двумерном пространстве.

    Обратите внимание на спецификатор доступа public:. Он говорит о том, что все, что написано ниже, доступно для использования извне класса. О защите данных (инкапсуляции) мы поговорим в следующих статьях, а пока будем делать наши поля открытыми для простоты.

    Чтобы создать объект этого класса (инстанцировать его), мы используем имя класса как тип данных:

    Конструкторы: Рождение объекта

    В примере выше мы вручную задавали значения x и y. Но что, если мы забудем это сделать? В C++ переменные, не инициализированные явно, могут содержать «мусор» — случайные значения, оставшиеся в памяти.

    Чтобы гарантировать, что объект всегда создается в корректном состоянии, используются конструкторы.

    Конструктор — это специальный метод, который: * Имеет то же имя, что и класс. * Не имеет возвращаемого типа (даже void). * Вызывается автоматически в момент создания объекта.

    Конструктор по умолчанию и с параметрами

    Теперь создание объектов выглядит так:

    Списки инициализации

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

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

    Деструкторы: Завершение жизненного цикла

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

    Особенности деструктора: * Имя совпадает с именем класса, но перед ним ставится тильда ~. * Не принимает аргументов и не возвращает значения. * У класса может быть только один деструктор.

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

    Указатель this: Самоидентификация

    Внутри любого метода класса существует скрытый указатель под названием this. Он указывает на текущий объект, для которого был вызван метод.

    Представьте, что вы пишете инструкцию для группы людей: «Поднимите правую руку». Каждый человек (объект), выполняющий эту инструкцию, поднимает свою руку. В контексте объекта this — это способ сказать «я» или «мое».

    Зачем нужен this?

    Чаще всего this используется в двух случаях:

  • Устранение неоднозначности имен.
  • Если имя параметра метода совпадает с именем поля класса, параметр «затеняет» поле. Чтобы обратиться именно к полю, используют this->.

  • Цепочки вызовов (Method Chaining).
  • Методы могут возвращать ссылку на текущий объект (*this), что позволяет вызывать их друг за другом.

    Итоговый пример

    Давайте соберем все знания в одном примере. Создадим класс Book (Книга).

    Вывод программы:

    Заключение

    Сегодня мы сделали первый шаг в мир ООП. Мы узнали, что класс — это чертеж, а объект — это реализация этого чертежа. Мы научились управлять рождением объекта с помощью конструкторов и его уничтожением с помощью деструкторов, а также поняли, как объект может ссылаться на самого себя через указатель this.

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

    2. Инкапсуляция и сокрытие данных: модификаторы доступа, геттеры, сеттеры и статические члены

    Инкапсуляция и сокрытие данных: модификаторы доступа, геттеры, сеттеры и статические члены

    Приветствую вас во второй части нашего курса «Основы объектно-ориентированного программирования на C++». В прошлой статье мы научились создавать классы и объекты, словно чертежи и реальные устройства. Однако наши «устройства» имели существенный недостаток: любой желающий мог залезть в их внутренний механизм и все сломать.

    Вспомните наш класс Book или Point. Мы делали все поля public. Это значит, что в функции main кто угодно мог написать book.pages = -500;. С точки зрения синтаксиса C++ это верно (число есть число), но с точки зрения логики — это абсурд. Книга не может иметь отрицательное количество страниц.

    Сегодня мы изучим инкапсуляцию — один из трех китов ООП, который позволяет защитить данные от некорректного использования и скрыть сложность реализации.

    Что такое инкапсуляция?

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

    Представьте себе микроволновую печь.

    !Микроволновая печь: интерфейс (кнопки) доступен пользователю, а реализация (магнетрон) скрыта внутри корпуса.

    У микроволновки есть интерфейс (кнопки, таймер, ручка дверцы) — это то, с чем вы взаимодействуете. И есть реализация (магнетрон, трансформатор, провода) — это то, что скрыто внутри корпуса. Корпус защищает вас от удара током, а детали — от поломки из-за вашего любопытства. В программировании роль такого «корпуса» играют модификаторы доступа.

    Модификаторы доступа в C++

    В C++ существует три уровня доступа к членам класса:

  • public (публичный) — доступ открыт всем. Поля и методы видны из любой части программы.
  • private (приватный) — доступ есть только у самого класса (его методов). Из функции main или других классов эти данные не видны.
  • protected (защищенный) — похож на private, но разрешает доступ классам-наследникам (об этом мы поговорим в статье про наследование).
  • Пример проблемы без инкапсуляции

    Решение с использованием private

    Давайте перепишем этот пример, используя класс и модификаторы доступа.

    Если теперь мы попытаемся написать account.balance = 100; в main, компилятор выдаст ошибку: «member BankAccount::balance is inaccessible». Мы успешно спрятали данные.

    Геттеры и Сеттеры: Управляемый доступ

    Но если данные скрыты (private), как нам с ними работать? Как узнать баланс или изменить его? Для этого создаются специальные публичные методы, которые называют аксессорами (accessors).

    Их делят на два типа: Геттеры (Getters) — методы для чтения* данных (обычно начинаются с get). Сеттеры (Setters) — методы для записи* данных (обычно начинаются с set).

    Зачем они нужны?

    Почему бы просто не сделать переменную public? Сеттеры позволяют нам добавить логику валидации (проверки).

    Доработаем наш банковский счет:

    Преимущества такого подхода:

  • Контроль: Мы не позволяем установить отрицательный баланс.
  • Только чтение: Если мы уберем методы deposit и withdraw и оставим только getBalance, переменная станет доступной только для чтения.
  • Гибкость: Если в будущем мы захотим сохранять историю всех операций в базу данных, мы изменим код внутри методов deposit/withdraw, и это не сломает код в main.
  • Статические члены класса (static)

    Иногда нам нужны данные, которые принадлежат не конкретному объекту (например, конкретному автомобилю), а всему классу в целом (заводу).

    В C++ ключевое слово static используется для объявления членов класса, которые являются общими для всех экземпляров этого класса.

    Статические поля

    Представьте, что мы хотим подсчитать, сколько всего объектов класса User было создано в нашей программе. Если мы создадим обычную переменную int count внутри класса, то у каждого объекта будет своя копия этой переменной, и они не будут знать друг о друге.

    Здесь на помощь приходит static.

    Особенности статических полей:

    * Они существуют в единственном экземпляре для всего класса. * Память под них выделяется сразу при запуске программы, даже если объекты не созданы. * Их нужно инициализировать за пределами класса (в глобальной области видимости).

    Статические методы

    В примере выше мы написали static int getUserCount(). Статические методы:

  • Могут быть вызваны без создания объекта.
  • Имеют доступ только к статическим полям класса. Они не могут использовать обычные поля (например, id), потому что не знают, к какому конкретно объекту обращаться (у них нет указателя this).
  • Пример вызова:

    Вывод:

    Хорошие практики инкапсуляции

    Чтобы писать качественный код на C++, следуйте этим правилам:

  • Все поля делайте private. Если полю нужен доступ извне, напишите геттер. Если его нужно менять — напишите сеттер.
  • Минимизируйте интерфейс. Делайте public только те методы, которые действительно нужны пользователю класса. Вспомогательные методы, которые используются только внутри класса, должны быть private.
  • Используйте const. Если геттер не меняет состояние объекта (а он и не должен), помечайте его как const. Например: int getBalance() const { ... }. Это гарантирует, что метод случайно не изменит данные.
  • Заключение

    Сегодня мы разобрали важнейший принцип ООП — инкапсуляцию. Мы узнали, что: * Инкапсуляция скрывает детали реализации и защищает данные. * Модификаторы доступа (private, public) управляют видимостью членов класса. * Геттеры и сеттеры обеспечивают безопасный доступ к приватным полям. * Статические члены (static) принадлежат всему классу, а не отдельным объектам.

    Теперь вы умеете создавать надежные и защищенные классы. В следующей статье мы перейдем ко второму киту ООП — Наследованию, и узнаем, как создавать новые классы на основе уже существующих, экономя время и строки кода.

    3. Наследование: построение иерархий классов, порядок инициализации и множественное наследование

    Наследование: построение иерархий классов, порядок инициализации и множественное наследование

    Добро пожаловать в третью часть нашего курса «Основы объектно-ориентированного программирования на C++». В предыдущих статьях мы научились создавать отдельные классы, скрывать данные с помощью инкапсуляции и управлять жизненным циклом объектов. Но что делать, если наши классы имеют много общего?

    Представьте, что вы разрабатываете игру. У вас есть классы Warrior (Воин), Mage (Маг) и Archer (Лучник). У всех них есть имя, уровень здоровья и координаты на карте. Если писать этот код для каждого класса отдельно, мы нарушим главный принцип программирования — DRY (Don't Repeat Yourself — Не повторяйся).

    Сегодня мы изучим наследование — механизм, позволяющий создавать новые классы на основе уже существующих, перенимая их свойства и поведение.

    Суть наследования

    Наследование позволяет выделить общие характеристики в базовый класс (родительский), а специфические особенности оставить в производных классах (дочерних).

    В примере с игрой мы можем создать общий класс Unit (Юнит), в который вынесем здоровье и имя. А классы Warrior, Mage и Archer будут наследоваться от него, добавляя свои уникальные способности (например, castSpell для мага).

    !Иерархия классов: Unit является родителем для Warrior, Mage и Archer.

    Отношение наследования часто называют отношением «является» (Is-A). Воин является Юнитом. Собака является Животным.

    Синтаксис наследования в C++

    Чтобы унаследовать класс, после имени нового класса ставится двоеточие, спецификатор доступа и имя родительского класса.

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

    Мы уже знакомы с public и private. При наследовании появляется третий важный игрок — protected.

    Ключевое слово protected

    Члены класса, объявленные как protected (защищенные): * Недоступны извне (как private). * Доступны внутри самого класса (как private). * Доступны внутри классов-наследников (в отличие от private).

    Это идеальный выбор для данных, которые вы хотите скрыть от пользователя, но оставить доступными для расширения в дочерних классах.

    Типы наследования

    В C++ можно наследовать класс тремя способами: public, protected и private. Чаще всего (в 99% случаев) используется public наследование.

    Рассмотрим, как меняется доступ к членам базового класса внутри производного при public наследовании:

    | Доступ в Базовом классе | Доступ в Производном классе (при public наследовании) | | :--- | :--- | | public | public (доступно всем) | | protected | protected (доступно только наследникам) | | private | Недоступно (даже наследникам) |

    > Важно: private поля родителя существуют в памяти дочернего объекта, но компилятор запрещает к ним обращаться напрямую. Для работы с ними родитель должен предоставить public или protected методы.

    Пример кода

    Порядок вызова конструкторов и деструкторов

    Когда вы создаете объект производного класса, происходит целая цепочка событий. Понимание этого порядка критически важно для предотвращения ошибок инициализации.

    Рождение объекта (Конструкторы)

    Строительство объекта всегда начинается с фундамента. Сначала создается базовая часть, затем — надстройка.

  • Вызывается конструктор Базового класса.
  • Инициализируются поля Производного класса.
  • Выполняется тело конструктора Производного класса.
  • Если у базового класса нет конструктора по умолчанию (без параметров), производный класс обязан явно вызвать нужный конструктор родителя в списке инициализации.

    Гибель объекта (Деструкторы)

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

  • Вызывается деструктор Производного класса.
  • Вызывается деструктор Базового класса.
  • Множественное наследование

    C++ — один из немногих языков, поддерживающих множественное наследование. Это означает, что у одного класса может быть несколько родителей.

    Представьте амфибию: это и машина, и лодка.

    Проблема ромба (Diamond Problem)

    Множественное наследование — мощный, но опасный инструмент. Самая известная проблема называется «Проблема ромба».

    Представьте такую иерархию:

  • Есть класс Device (Устройство) с полем id.
  • Классы Scanner и Printer наследуются от Device.
  • Класс Copier (Ксерокс) наследуется сразу от Scanner и Printer.
  • !Проблема ромба: класс Copier получает две копии класса Device.

    В итоге объект Copier будет содержать две копии Device: одну от сканера, другую от принтера. Это приводит к двусмысленности (к какому id мы обращаемся?) и перерасходу памяти.

    Решение: Виртуальное наследование

    Чтобы избежать дублирования базового класса, используется ключевое слово virtual при наследовании.

    Теперь Copier содержит только одну копию Device, и проблема решена.

    Композиция vs Наследование

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

    Существует альтернатива — Композиция (отношение «Имеет» / Has-A).

    Наследование: Dog является* Animal. Композиция: Car имеет* Engine.

    Не нужно наследовать машину от двигателя. Лучше создать поле типа Engine внутри класса Car.

    > «Предпочитайте композицию наследованию» — один из ключевых принципов объектно-ориентированного проектирования.

    Заключение

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

  • Как создавать производные классы с помощью class Derived : public Base.
  • Зачем нужен модификатор protected.
  • Что конструкторы вызываются от родителя к ребенку, а деструкторы — наоборот.
  • Как работает множественное наследование и как избежать проблемы ромба.
  • В следующей статье мы перейдем к самой магической части ООП — Полиморфизму, где узнаем, как объекты разных классов могут реагировать по-разному на одни и те же команды, используя виртуальные функции.

    4. Полиморфизм: виртуальные функции, переопределение методов и абстрактные классы

    Полиморфизм: виртуальные функции, переопределение методов и абстрактные классы

    Приветствую вас в четвертой части нашего курса «Основы объектно-ориентированного программирования на C++». Мы уже научились создавать чертежи объектов (классы), защищать их внутренности (инкапсуляция) и строить иерархии (наследование). Казалось бы, у нас есть все инструменты для создания сложных систем. Но есть одна проблема.

    Вспомните наш пример с игрой из прошлой статьи. У нас есть базовый класс Unit и наследники Warrior, Mage, Archer. Представьте, что у вас есть армия, состоящая из разных типов юнитов. Как управлять ими всеми одновременно? Как заставить всю армию атаковать, если у каждого юнита атака выглядит по-разному (меч, магия, лук)?

    Здесь на сцену выходит полиморфизм — третий и, пожалуй, самый мощный кит ООП. Он позволяет обращаться с объектами разных типов так, как будто это объекты одного типа, при этом сохраняя их уникальное поведение.

    Проблема раннего связывания

    Давайте рассмотрим классический пример с животными, чтобы понять суть проблемы.

    Если вы запустите этот код, вы ожидаете увидеть «Мяу!», ведь мы создали кошку. Но консоль выдаст:

    Почему так произошло? Дело в том, что по умолчанию в C++ используется раннее связывание (static binding). Компилятор видит, что указатель myPet имеет тип Animal*. Он не знает, что именно будет лежать по этому адресу во время выполнения программы. Поэтому он «привязывает» вызов метода voice() к реализации из класса Animal еще на этапе компиляции.

    !Иллюстрация проблемы раннего связывания: компилятор видит тип указателя, а не реальный объект.

    Виртуальные функции: магия позднего связывания

    Чтобы исправить ситуацию, нам нужно сказать компилятору: «Не спеши. Реши, какую функцию вызывать, прямо в момент выполнения программы, посмотрев на реальный объект».

    Для этого используется ключевое слово virtual.

    Теперь, запустив тот же main, мы получим заветное:

    Это называется поздним связыванием (dynamic binding) или полиморфизмом. Указатель одного типа (Animal*) может вести себя по-разному в зависимости от того, на какой объект он указывает.

    Как это работает под капотом?

    Когда в классе появляется хотя бы одна виртуальная функция, компилятор скрытно добавляет в каждый объект этого класса специальный указатель — vptr. Он указывает на таблицу виртуальных методов (vtable). В этой таблице хранятся адреса правильных функций для данного конкретного класса. При вызове метода программа смотрит в таблицу и находит нужный адрес.

    Ключевое слово override

    В современном C++ (начиная со стандарта C++11) появилась отличная практика: явно помечать методы, которые мы переопределяем, словом override.

    Зачем это нужно? Представьте, что вы допустили опечатку в наследнике:

    Компилятор посчитает vaice новым методом. Код скомпилируется, но полиморфизм работать не будет. Если же мы напишем override, компилятор проверит, есть ли такой виртуальный метод у родителя.

    > Правило: Всегда используйте override при переопределении виртуальных функций. Это спасет вас от часов отладки.

    Виртуальный деструктор

    Это одно из самых важных правил в C++. Если в классе есть хоть одна виртуальная функция, деструктор тоже должен быть виртуальным.

    Рассмотрим пример утечки памяти:

    Если деструктор Base не виртуальный, то при delete ptr вызовется только ~Base(). Деструктор ~Derived() вызван не будет, и память, выделенная под массив array, навсегда утечет.

    Исправленный вариант:

    Теперь вызовутся оба деструктора в правильном порядке: сначала Derived, потом Base.

    Абстрактные классы и чисто виртуальные функции

    Иногда базовый класс настолько общий, что создавать его экземпляры не имеет смысла. Например, класс Shape (Фигура). Какая площадь у «просто фигуры»? Как ее нарисовать? Это абстрактное понятие.

    В C++ мы можем запретить создание объектов класса, сделав его абстрактным. Для этого нужно объявить хотя бы одну чисто виртуальную функцию (pure virtual function).

    Синтаксис выглядит немного странно: = 0 в конце объявления.

    Теперь написать Shape s; нельзя — компилятор выдаст ошибку. Мы обязаны создать наследников и реализовать этот метод.

    Пример с математикой

    Давайте реализуем систему геометрических фигур. Нам понадобятся формулы площади.

    Для круга площадь вычисляется по формуле:

    где — площадь круга, — математическая константа (примерно 3.14), а — радиус круга.

    Для прямоугольника формула проще:

    где — площадь, — ширина, а — высота.

    Реализуем это в коде:

    !Иерархия классов фигур: абстрактный родитель и конкретные реализации.

    Зачем это нужно на практике?

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

  • Вы создаете интерфейс Shape с методом draw().
  • Весь ваш код (перемещение, удаление, сохранение) работает с указателями Shape*.
  • Завтра заказчик просит добавить «Треугольник».
  • Вы просто создаете новый класс Triangle : public Shape.
  • Вам не нужно переписывать основной код программы! Он уже умеет работать с любыми фигурами, даже с теми, которых еще не существовало на момент его написания.
  • Это соответствует принципу Open/Closed Principle (Принцип открытости/закрытости): программные сущности должны быть открыты для расширения, но закрыты для модификации.

    Заключение

    Сегодня мы изучили вершину айсберга ООП:

    * Полиморфизм позволяет работать с объектами разных типов через единый интерфейс. * Виртуальные функции (virtual) включают механизм позднего связывания. * Override защищает от ошибок при переопределении. * Виртуальные деструкторы обязательны при наследовании для предотвращения утечек памяти. * Абстрактные классы задают контракт (интерфейс), который обязаны реализовать наследники.

    В следующей статье мы поговорим о перегрузке операторов и узнаем, как научить наши классы складываться, вычитаться и сравниваться так же легко, как обычные числа.

    5. Перегрузка операторов и основы обобщенного программирования с использованием шаблонов

    Перегрузка операторов и основы обобщенного программирования с использованием шаблонов

    Добро пожаловать в пятую часть нашего курса «Основы объектно-ориентированного программирования на C++». В предыдущих статьях мы построили прочный фундамент: научились создавать классы, скрывать данные, наследовать поведение и использовать полиморфизм. Наши объекты уже ведут себя как умные сущности.

    Однако, есть один нюанс. Когда мы работаем со встроенными типами, такими как int или double, мы привыкли писать a + b, a == b или std::cout << a. Это удобно, интуитивно и читаемо. Но как только мы создаем свой класс, например Point (Точка) или Matrix (Матрица), эта магия исчезает. Мы вынуждены писать громоздкие конструкции вроде p1.add(p2) или matrix.print().

    Сегодня мы научим наши классы говорить на одном языке с компилятором. Мы разберем перегрузку операторов, чтобы ваши объекты можно было складывать и сравнивать так же легко, как числа. А затем мы перейдем к шаблонам — инструменту, который позволит написать код один раз и использовать его для любых типов данных.

    Перегрузка операторов: Синтаксический сахар или необходимость?

    В C++ операторы (такие как +, -, *, ==, <<) — это, по сути, просто функции с необычными именами. Когда вы пишете 5 + 3, компилятор понимает это как арифметическую операцию. Но C++ позволяет нам переопределить (перегрузить) поведение этих операторов для наших собственных типов данных.

    Зачем это нужно?

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

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

    Без перегрузки операторов код выглядел бы так:

    С перегрузкой операторов мы можем написать гораздо естественнее:

    Реализация перегрузки

    Чтобы научить класс реагировать на оператор +, нам нужно написать метод со специальным именем operator+. Рассмотрим это на примере класса Vector2D.

    В данном случае выражение v1 + v2 компилятор преобразует в вызов метода v1.operator+(v2).

    Дружественные функции и оператор вывода (<<)

    Одной из самых частых задач является вывод объекта в консоль. Мы хотим писать std::cout << v1, а не v1.print().

    Оператор << (побитовый сдвиг влево, который в контексте потоков означает «поместить в поток») имеет два операнда: слева — поток вывода (std::ostream), справа — наш объект.

    Если мы сделаем перегрузку методом класса Vector2D, то левым операндом должен быть сам вектор. Тогда вызов выглядел бы как v1 << std::cout, что крайне нелогично. Чтобы левым операндом был std::cout, функция перегрузки должна быть внешней по отношению к классу.

    Но если функция внешняя, она не имеет доступа к private полям класса. Тут на помощь приходит ключевое слово friend (друг). Дружественная функция — это обычная функция, которой класс разрешает доступ к своим приватным членам.

    Правила перегрузки

  • Нельзя менять приоритет. Оператор умножения всегда будет выполняться раньше сложения.
  • Нельзя менять количество операндов. Бинарный плюс всегда принимает два аргумента.
  • Нельзя создавать новые операторы. Вы не можете придумать оператор ** или S = \frac{a + b}{2} \cdot h $Sabh$ — высота.
  • Если мы напишем шаблонную функцию для вычисления этой площади:

    То эта функция будет работать и для int, и для double, и даже для нашего класса BigInt (если мы его напишем), при условии, что в этом классе перегружены операторы +, / и *`.

    Заключение

    Сегодня мы значительно расширили арсенал наших возможностей:

  • Перегрузка операторов позволяет интегрировать пользовательские типы в синтаксис языка, делая код чище и выразительнее.
  • Шаблоны открывают дверь в мир обобщенного программирования, позволяя писать алгоритмы, независимые от конкретных типов данных.
  • Эти знания подводят нас к одной из самых мощных частей C++ — Стандартной библиотеке шаблонов (STL). В следующей статье мы узнаем, как не изобретать велосипед, а использовать готовые контейнеры (векторы, списки, словари) и алгоритмы, которые написаны профессионалами с использованием именно тех технологий, которые мы сегодня изучили.