--- 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 = &pi; 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`**