---
title: ООП, классы
tags: cpp, mipt, 3sem, teaching, OOP
---
# Второе занятие
#### 2022-09-10
## Принципы ООП
### Парадигмы
Есть 3+1 основных парадигмы ООП, которым стоит следовать:
1. **Инкапсуляция**: все, что требуется для работы объекта/класса, хранится внутри самого класса (является полем класса). Изменения полей класса производится строго внутри самого класса. Никакая логика обработки полей не уходит во внешний мир. Инкапсуляция осуществляется по средствам модификаторов доступа: `private`, `protected`, `public`.
2. **Наследование**: основа для построения классов и их взаимоотношений. Правильное наследование позволяет логически выделить одинаковые объекты кода и единожды прописать их логику, которая будет использоваться объектами-наследниками.
3. **Полиморфизм**: возможность *перегружать* методы для использования обработки разных объектов.
4. *Абстракция*: возможность писать общие прототипы классов с последующим наследованием и подробным описанием их логики.
### Классы и структуры
В прошлом семестре мы часто обращались к структурам. Главное отличие структуры от класса может продемонстриорвать следующий код:
```cpp=
#include <iostream>
struct Vector3 {
int x = 0, y = 0, z = 0;
void print() {
std::cout << x << " " << y <<
" " << z << std::endl;
}
};
class Vector4 {
int x = 0, y = 0, z = 0, t = 0;
void print() {
std::cout << x << " " << y <<
" " << z << " " << t << std::endl;
}
};
int main() {
Vector3 R3;
Vector4 R4;
R3.print();
R4.print();
return 0;
}
```
Код для `struct` тработает без ошибок, а вот с `class` будут проблемы. Главное отличие `struct` от `class` это модификатор доступа по умолчанию. В `struct` все поля/методы по умочанию являются `public`, в `class` -- `private`.
Существует три типа модификаторов доступа:
- `public` -- поля и методы могут быть доступны вне класса/структуры.
- `private` -- поля и методы могут быть доступны только внутри класса/структуры.
- `protected` -- модификатор позволяет для классов-наследников использовать ряд полей/методов родителей.
### С++ и ООП
В С++ большинство объектов, с которыми еще предстоит работать, являются классами. Приведем простой пример класса:
```cpp=
class Cube {
public:
double edge; // поле класса
double caclulateVolume() { // метод класса
return edge * edge * edge;
}
}; // Не забывайте про точка-запятая в конце определения класса
```
Класс `Cube` реализует объект куб с полем `edge` -- длина стороны куба и методом вычисления объема `calculateVolume()`. **Учтите**, что в конце определния класса всегла есть знак `;`.
Попробуем добавить метод вычисления площади куба:
```cpp=
#include <iostream>
class Cube {
public:
double edge; // поле класса
double caclulateVolume() { // метод класса
return edge * edge * edge;
}
double calculateArea() {
return 6 * edge * edge;
}
};
int main() {
Cube tesseract;
std::cin >> tesseract.edge;
std::cout << tesseract.edge;
return 0;
}
```
**Не очень хорошая практика**: как видно, оба метода в своем выражении используют квадрат стороны куба в своих расчетах. Поместим в `private` поля значение площади стороны квадрата (ради мнимого ускорения расчетов):
```cpp=
#include <iostream>
class Cube {
public:
double edge; // поле класса
double caclulateVolume() { // метод класса
return edge * edge * edge;
}
double calculateArea() {
return 6 * edge * edge;
}
private:
double facet_area;
};
int main() {
Cube tesseract;
std::cin >> tesseract.edge;
std::cout << tesseract.edge;
std::cin >> tesseract.facet_area;
std::cout << tesseract.facet_area;
return 0;
}
```
Компилятор вас отругает за такое, ведь у обычного пользователя нет доступа снаружи к полям в `private` области. Соответственно, `private` используется для ограничения внешнего доступа к полям класса. Обычно, чтобы получить доступ к приватным полям используют следущую конструкцию:
```cpp=
#include <iostream>
class Cube {
public:
double edge; // поле класса
double caclulateVolume() { // метод класса
return edge * edge * edge;
}
double calculateArea() {
return 6 * edge * edge;
}
double getFacetArea() {
return facet_area;
}
private:
double facet_area;
};
int main() {
Cube tesseract;
std::cin >> tesseract.edge;
std::cout << tesseract.edge;
std::cout << tesseract.getFacetArea() << std::endl;
return 0;
}
```
Вопросы-и-ответы:
- Как изменить `private` поле? Только внутри класса.
- Можно ли `private` полю придать зачение по умалчанию?
- Может ли быть метод (внутренняя функция класса) быть `private`методом? Да, может
- Может ли значение одного поля зависеть от значения другого поля? Да, может. И об этом следующий пункт
### Конструктор класса
Как и в Python есть магический метод `__init__`, так и в С++ есть конструктор класса. Конструктор может выглядить следующим образом:
```cpp=
class Cube {
public:
int edge;
Cube(double value) {
edge = value;
facet_area = edge * edge;
}
double caclulateVolume() {
return edge * edge * edge;
}
double calculateArea() {
return 6 * edge * edge;
}
double getFacetArea() {
return facet_area;
}
private:
double facet_area;
};
int main() {
Cube tesseract(5);
Cube tesseract; \\ Compilation error
std::cout << tesseract.edge;
std::cout << tesseract.getFacetArea() << std::endl;
return 0;
}
```
Первое объявление конструктора сразу же уничтожает конструктор по умолчанию.
Передавать параметры в конструктор можно с помощью списка инициализации и при этом появляется возможность использовать делигирующий конструктор:
```cpp=
class Cube {
public:
double edge;
// default constructor
Cube() : edge(0), facet_area(0), mass(0) { }
// initialization list
Cube(double edge, double mass) : edge(edge), facet_area(edge * edge),
mass(mass){ }
// delegating constructor
Cube(double edge) : Cube(edge, 1.d) { }
// deleted constructor
Cube(double edge, double facet_area, double mass) = delete;
double caclulateVolume(){
return edge * edge * edge;
}
double calculateArea() {
return 6 * edge * edge;
}
double getFacetArea() {
return facet_area;
}
private:
double facet_area;
double mass;
};
```
Конструктор **не инициализирует** поля, он лишь является допонительной связующей логикой.
```cpp=
class String {
private:
char* str; // buffer of string
size_t sz; // size of string
public:
String(size_t sz, char ch) {
this->sz = sz; // this -- pointer to member of class, "self" Python
str = new char[sz];
for (size_t i = 0; i < sz; ++i) {
str[i] = ch;
}
// memset(str, ch, sz); // <cstring> more optimal way
}
};
```
### Модификация полей, значения по умолчанию
Как и в Python, есть возможность проинициализировать поля класса изначальными значениями. Пример на основе предыдущего класса строки:
```cpp=
class String {
private:
char* str = nullptr;
size_t sz = 0;
};
```
Указывать аргументы по умолчанию может быть воспринято не всегда хорошо. Рассмотрим пример класса, который в себе содержит общее число экземпляров класса:
```cpp=
#include <iostream>
class Dog {
public:
Dog() : Dog(0) { }
Dog(int raiting) : raiting(raiting) {
++population;
}
~Dog() {
--population;
}
public:
static unsigned int population;
private:
int raiting = 0;
};
unsigned int Dog::population = 0;
int main() {
Dog Haski = Dog(10);
std::cout << Haski.population << std::endl;
Dog Labrador = Dog(5);
std::cout << Haski.population << std::endl;
return 0;
}
```
Попытка инициализировать `static` поле класса приведет к ошибке.
Если же декларировать у класса `const` или ссылки в виде полей, то потребуется инициализировать их на месте.
```cpp=
#include <iostream>
class Dog {
public:
Dog() : Dog(0) { }
Dog(int raiting) : raiting(raiting) {
++population;
}
~Dog() {
--population;
}
public:
static unsigned int population;
private:
int raiting = 0;
const int legs = 4;
};
unsigned int Dog::population = 0;
int main() {
Dog Haski = Dog(10);
std::cout << Haski.population << std::endl;
Dog Labrador = Dog(5);
std::cout << Haski.population << std::endl;
return 0;
}
```
### Деструктор класса
При завершении работы блока кода, или же времени жизни объекта, вызывается специальный метод называемый деструктором. Деструктор осуществляет корректное завершение объекта. Если была динамически выделена память для внутренних объектов класса, то в деструкторе необходимо освободить ее. Деструктор **указывает алгоритм действия с полями** в случаае их скорого уничтожения.
Пример деструктора:
```cpp=
class CubeCore {
public:
CubicCore() {
std::cout << "Constructor called :)" << std::endl;
}
~CubeCore() {
std::cout << "Destructor called :(" << std::endl;
}
// ~CubeCore() = delete;
};
int main() {
CubeCore flatEarth;
return 0;
}
```
Получаем, что деструктор будет вызван после окончания `main()`. При этом конструктор вызывается до вызова деструктора, очевидно.
- Деструктор не может быть приватным
- Деструктор не принимает аргументов
- Деструктор должен быть
Попробуем внутри куба разместить класс `CubicCore` и посмотреть на порядок вызова конструкторов и деструкторов:
```cpp=
#include <iostream>
class CubicCore {
public:
CubicCore() {
std::cout << "CubicCore()" << std::endl;
}
~CubicCore() {
std::cout << "~CubicCore()" << std::endl;
}
};
class Cube {
public:
CubicCore core;
int mass;
Cube() : mass(0) {
std::cout << "Cube()" << std::endl;
}
~Cube() {
std::cout << "~Cube()" << std::endl;
}
};
int main() {
Cube tessaract = Cube();
return 0;
}
```
Из примера должно было стать ясно, что сначала конструируются внутренние объекты. Деструктор вызывается в обратном порядке.
### Default методы
По умолчанию компилятор реализует у класса ряд методов/конструкторов:
- Конструктор по умолчанию (без аргументов);
- Деструктор по умолчанию (без умного освобождения памяти, если она была выделена);
- Конструктор копирования (`ClassName(const ClassName& other)`); копирование является поверхностным (shallow);
- Оператор присваивания.
#### Конструктор копирования
На вход конструктора копирования подается константная ссылка на объект.
- Констатнтная чтобы не изменить случайно первоначальный объект;
- Ссылка для избежания копирования объекта при передаче его в качестве аргумента конструктора.
Пример с классом `String`:
```cpp=
class String {
private:
char* str;
size_t sz;
public:
String(size_t sz, char ch) {
this->sz = sz;
str = new char[sz];
for (size_t i = 0; i < sz; ++i) {
str[i] = ch;
}
}
String(const String& other) {
sz = other.sz;
str = new char[sz];
memcpy(str, other.str, sz);
}
~String() {
delete [] str;
}
};
```
Разберем на следующем примере:
```cpp=
#include <iostream>
class Dog() {
}
int main() {
Dog Haski = Dog();
Dog Labrador = Dog();
return 0;
}
```
### Deep и Shallow копии
Под термином deep понимается полное копирование объекта с инициализацией его полей копиями исходного объекта. Shallow копия -- копия по типу ссылки, новая память для объекта не выделяется.
Если какое-то из полей класса является указателем на массив, то при копировании мы желаем полностью перенести данную информацию.
### Финальное задание пары
::: spoiler Deprecated
Напишите класс матриц `NxN`. У класса должно быть одно приватное поле `mat` (оно может быть и одномерным). У класса должен быть конструктор, который на вход принимает одномерный массив значений, внутри конструктора массив превращается в матрицу. Напишите методы для вычисления детерминанта, вывода матрицы и вывода квадрата матрицы.
Сигнатура класса строго следующая:
```cpp=
class Matrix {
public:
Matrix(int arr[], size_t n_elements) {
... // n_elements -- квадрат натурального числа
}
Matrix()
void printMatrix() {
...
}
int calcDeterminant() {
...
}
void calcSquare() {
...
}
private:
int** mat;
};
```
:::
---
__Примерный план оценивания__
При оценивании будет делаться упор на следующие пункты (приоритетность в порядке убывания)
1. Разумное использование конструктора (помним про списки инициализации)
2. Корректная работа с памятью (касается второго задания)
3. Использование методов/конструкторов по умолчанию (касается копирования; не стоит писать велосипед, если все должно работать из коробки)
4. Если придумаете интересный метод, то `welcome`
__Задание__
1. Для работы с числами иногда полезно представлять их в виде дробей. Напишите свой класс дробей следующей сигнатуры
```cpp=
class Fraction {
public:
Fraction(int numerator, int denominator) {
...
}
Fraction(int numerator) {
... // Делигирующий конструктор, знаменатель 1
}
Fraction() { // Конструктор по умолчанию должен быть удален
...
}
Fraction(const Fraction& other) {
... // Конструктор копирования deep
}
void printFraction() {
... // Выводит дробь в формате 'numerator / denominator'
}
void fracReverse() { // Перевернуть дробь, знаменатель<->числитель
...
}
void fracSimplify() { // Сокращает дробь по возможности
...
}
void multByNumber(int value) { // Умножить дробь на число
...
}
void multByFrac(const Fraction& other) {
... // Умножить дробь на другую дробь, результат оставить в вызывающей (в this)
}
Fraction multByFracReturn(const Fraction& other) const {
... // умножает дроби и возвращает результат в третью
}
void sumFrac(const Fraction& other) {
... // Просуммировать дробь на другую дробь, результат оставить в вызывающей (в this)
}
Fraction sumFracReturn(const Fraction& other) const {
... // просуммировать дроби и вернуть результат в третью
}
void diffFrac(const Fraction& other) {
... // Взять разность дробей, результат оставить в вызывающей (в this)
}
Fraction diffFracReturn(const Fraction& other) const {
... // Взять разность дробей, результат в третью
}
double calc() { // Возвращает значение дроби в десятичном виде
...
}
int getNumerator() { // Возвращает значение числителя
...
}
int getDenominator() { // Возвращает знамечение знаменателя
...
}
private:
int numerator;
int denominator;
}
```
**Файл сохраняется в репозитории под названием `classwork02/fraction.cpp`**
2. Реализовать класс квадратных матриц. Конструктор принимает на вход массив размера `n_elements`, `n_elements` -- квадрат натурального числа
```cpp=
class Matrix {
public:
Matrix(int arr[], size_t n_elements) {
... // Заполнение матрицы из массива
}
Matrix(int element, size_t n_elements) {
... // Заполнение матрицы элементом element; n_elements -- количество элементов
}
Matrix() {
... // Определить конструктор по умолчанию, как матрицу размера 1 с элементом равным 1
}
Matrix(const Matrix& other) {
... // Определить копирующий конструктор
}
void printMatrix() {
... // Вывести матрицу: каждое значение в строке разделено пробелом, строки отделены \n
}
int calcDeterminant() {
... // Вычислить детерминант
}
void calcSquare() {
... // Вычислить квадрат матрицы, результат оставить в том же экземпляре класса
}
void matTranspose() {
... // Осуществляет транспонирование матрицы, результат сохраняется тут же
}
~Matrix() {
... // Определить деструктор
}
private:
int** mat; // массив для хранения значений матрицы
int size; // размер стороны матрицы
};
```
**Файл сохраняется в репозитории под названием `classwork02/matrix.cpp`**