---
title: Обобщенное программирование, введение в классы
tags: cpp, mipt, 3sem, teaching
---
# Первое занятие
#### 2022-09-03
## Обобщенное программирование
### Напоминание о типах данных
| type | size | value range |
| ---- | ---- | --- |
| short int | 16 | -32768 -- 32767 |
| int | 32 | -2147483648 -- 2147483647 |
| long int | 64 | -9223372036854775808 -- 9223372036854775807 |
| float | 32 | 1.17549e-38 3.40282e+38 |
| double | 64 | 2.22507e-308 1.79769e+308 |
Узнать лимиты для типа можно с помощью `std::numeric_limits<type>::min() / std::numeric_limits<type>::max()`, которая находится в библиотеке (заголовочном файле) `#include <limits>`. Размер типа `type` определяяется с помощью стандартной функции `sizeof(type)` ([напоминание о работе `sizeof`](https://en.cppreference.com/w/cpp/language/sizeof)).
### Принцип обобщенного программирования
Главная суть -- написать алгоритм, который бы взаимодействовал с объектами не зная про их тип. Обобщить алгоритм для работы с любым типом.
#### Доступ к элементам
Для обобщения типов средствами языка Си может использоваться тип `void*` (Void * This is where the real fun starts. There is too much coding everywhere else!). Принци работы с `void*` следующий:
```cpp=
#include <iostream>
int main() {
int pi = 12;
char first = 'A';
void* general_pointer;
general_pointer = π
std::cout << general_pointer << std::endl;
general_pointer = &first;
std::cout << general_pointer << std::endl;
return 0;
}
```
С помощью `void*` может быть обобщен любая переменная кода. При этом работа с ней осуществляется на уровне указателей.
При попытке разыменовать `void*` можно получить ошибку (ответ крайне прост).
#### Доступ к функциям
Обобщенный доступ к функциям может потребоваться для компараторов (функций сравнения) или же для проведения каких-либо операций. Объявление такого указателя на функцию делается следующим образом:
```cpp=
double (*ptr_sqrt) (double);
double (*ptr_power) (int, int);
```
Например, первая строка объявляет указатель на функцию, которая на вход принимает переменную типа `double`и возвращает `double`. Вторая же функция принимает на вход две переменные типа `int` и возвращает `double`.
Пример использования следующий:
```cpp=
#include <iostream>
double power(int base, int n) {
double res = base;
for (int i = 0; i < n; ++i) {
res *= base;
}
return res;
}
int main() {
int base = 0, n = 0;
double (*ptr_func) (int, int);
std::cin >> base >> n;
ptr_func = power;
std::cout << ptr_func(base, n) << std::endl;
return 0;
}
```
Использование функции по указателю позволяет передавать ее в качестве аргумента другой функции
```cpp=
#include <iostream>
#include <cmath>
double integrate(
double left_bound,
double right_bound,
double step,
double (*func) (double)
) {
double res = 0.;
for (double x = left_bound; x < right_bound; x += step) {
res += func(x) * step;
}
return res;
}
int main() {
double left_bound = 0., right_bound = 0., step = 0.;
std::cin >> left_bound >> right_bound >> step;
std::cout << integrate(left_bound, right_bound, step, std::sin) << std::endl;
std::cout << integrate(left_bound, right_bound, step, std::cos) << std::endl;
return 0;
}
```
Если несколько функций имеют одинаковую сигнатуру, то их можно объединить в массив функций.
```cpp=
int compare_double(void* first, void* second) {
...;
}
int compare_int(void* first, void* second) {
...;
}
...
int (*compare[])(void*, void*) = {compare_double, compare_int};
```
#### Приведение типов
В С++ существует несколько способов привести переменную одного типа к другому. Делается это с помощью следующего набора функций:
- `static_cast<T>(smth)` -- безопасное приведение типов.
- `dynamic_cast<T>(smth)` -- позволяет привести к типу, который находится ниже в иерархии наследования.
- `reinterpret_cast<T>(smth)` -- рисковое приведение типов. При этом компилятору сообщается, что вы точно уверены в конечном типе выражения. *Понадобится для приведения из `void*` в любой другой тип указателя.*
### Промежуточное задание
`blank` -- какой-то тип, нужно написать правильный
**Файлы сохраняются в репозитории под названием `classwork01/smth.cpp`**
1. (**A**) Написать обобщенный `void swap(blank, blank, int type_size)`. На вход функции подается два элемента, которые нужно поменять местами.
2. (**B**) Написать обобщенный поиск минимального значения следующей сигнатуры `int min(blank arr, int arr_size, int type_size, int (*compare)(void *, void *))`. На вход получает массив, размер массива, и функцию сравнения (компоратор). Функция должна вернуть индекс минимального элемента. Сделать аналогичный поиск максимума с использованием того же куска кода.
3. (**C**) Написать быструю сортировку с использованием произвольного компоратора. Сигнатура сортировки следующая `void qsort(blank arr, int left, int right, int type_size, int (*compare)(void *, void *))`. Реализовать несколько типов компораторов. Компораторы использовать из массива указателей на функции.
4. (**D**) Напишите простую программу, которая выводит размер типа в битах и его минимальное и максимальное значение. Вывести значения для `char, short int, int, long int, float, double`.
:::spoiler По поводу swap, qsort, etc.
#### swap
```cpp=
void swap(void* lha, void* rha, std::size_t type_size) {
void* tmp = malloc(type_size);
std::memcpy(tmp, lha, type_size);
std::memcpy(rha, lha, type_size);
std::memcpy(lha, tmp, type_size);
free(tmp);
}
```
Функция, которая описана выше, позволяет производить изменение элементов в ячейках памяти `rha` и `lha`. [`memcpy`](https://en.cppreference.com/w/cpp/string/byte/memcpy) -- Си функция, которая копирует информацию из второго аргумента в первый. При этом байты информации интерпретируются как массивы `unsigned char`.
```cpp=
void swap(void** a, void** b) {
void* tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int a = 0, b = 2;
void* ptr_a = &a;
void* ptr_b = &b;
swap(&ptr_a, &ptr_b);
std::cout << (*reinterpret_cast<int*>(ptr_a)) << " " << (*reinterpret_cast<int*>(ptr_b)) << std::endl;
return 0;
}
```
Функция позволяет поменять местами данные по указателям, но не меняет изначальные переменные. Почему? Ответ на строках 9-10.
Теперь вернемся к написанию обобщенного алгоритма.
1. На вход такого алгоритма должен попасть первый элемент массива. Этот первый элемент типа `void*` (обобщенный способ получить любой элемент).
2. Нужно подать размер типа, с которым мы работаем. Это позволит нам копировать данные и проходиться по элементам массива.
3. Указать количество элементов в массиве. Это позволит избежать попадение за пределы памяти.
## Принципы ООП
### Парадигмы
Есть 3+1 основных парадигмы ООП, которым стоит следовать:
1. **Инкапсуляция**: все, что требуется для работы объекта/класса, хранится внутри самого класса (является полем класса). Изменения полей класса производится строго внутри самого класса. Никакая логика обработки полей не уходит во внешний мир. Инкапсуляция осуществляется по средствам модификаторов доступа: `private`, `protected`, `public`.
2. **Наследование**: основа для построения классов и их взаимоотношений. Правильное наследование позволяет логически выделить одинаковые объекты кода и единожды прописать их логику, которая будет использоваться объектами-наследниками.
3. **Полиморфизм**: возможность *перегружать* методы для использования обработки разных объектов.
4. *Абстракция*: возможность писать общие прототипы классов с последующим наследованием и подробным описанием их логики.
### С++ и ООП
В С++ большинство объектов, с которыми еще предстоит работать, являются классами. Приведем простой пример класса:
```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=
#include <iostream>
class Cube {
public:
double edge;
Cube(int 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);
std::cout << tesseract.edge;
std::cout << tesseract.getFacetArea() << std::endl;
return 0;
}
```
### Финальное задание пары
::: spoiler Deprecated
Напишите класс матриц 3х3. У класса должно быть одно приватное поле `mat` (оно может быть и одномерным). У класса должен быть конструктор, который на вход принимает одномерный массив значений, внутри конструктора массив превращается в матрицу. Напишите методы для вычисления детерминанта, вывода матрицы и вывода квадрата матрицы.
Сигнатура класса строго следующая:
```cpp=
class Matrix {
public:
Matrix(int arr[9]) {
...
}
void printMatrix() {
...
}
int calcDeterminant() {
...
}
void calcSquare() {
...
}
private:
int mat[9];
}
```
:::
---
Для работы с числами иногда полезно представлять их в виде дробей. Напишите свой класс дробей следующей сигнатуры
```cpp=
class Fraction {
public:
Fraction(int numerator, int denominator) {
...
}
void printFraction() { // Выводит дробь в формате 'numerator / denominator'
...
}
double calc() { // Возвращает значение дроби в десятичном виде
...
}
int getNumerator() { // Возвращает значение числителя
...
}
int getDenominator() { // Возвращает знамечение знаменателя
...
}
private:
int _numerator;
int _denominator;
}
```
**Файл сохраняется в репозитории под названием `classwork01/fraction.cpp`**