Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Языки и методы программирования - лекция-12: ОО...

Anton
December 09, 2024

Языки и методы программирования - лекция-12: ООП в С++

Лекция курса "Языки и методы программирования"
Лекция-12: ООП в С++
- Объектно-ориентированное программирование (ООП), пример реализации в С++
- Предпосылки развития языков программирования: повышение производительности труда в области разработки программного обеспечения
- Парадигмы программирования
- Объектно-ориентированное программирование (ООП)
- Короткая история ООП
- Парадигма ООП
- Язык C++
- Проектирование в стиле ООП: объектная модель
- Объявление класса
- Выделение сущностей (пример: перфокартная машина для счета статистики)
- Объект: перфокарта
- Реализация методов
- Указатель this (этот)
- Конструктор (constructor)
- Экземпляры класса – объекты
- Объект в памяти
- Доступ к полям: оператор «.» (точка) vs «->» (стрелка)
- Размещение объекта на куче
- Управление доступом (инкапсуляция)
- Статический полиморфизм
- Объект: табуляционная машина, определение методов базового интерфейса
- Наследование
- Порядок вызова конструкторов при наследовании
- Переопределение методов
- Динамический полиморфизм
- Темы для самостоятельного изучения
- Объектно-ориентированное программирование (ООП) vs Объектно-ориентированное проектирование (ООП)
- Задания для самостоятельной работы

Anton

December 09, 2024
Tweet

More Decks by Anton

Other Decks in Education

Transcript

  1. Предпосылки • Машинный код → ассемблер • Ассемблер → языки

    высокого уровня • Языки высокого уровня → эволюция нон-стоп
  2. • Повышение производительности труда в области разработки программного обеспечения •

    Как меньше тратить времени на первичную разработку, развитие и поддержку кодовой базы, решающей проблему в предметной области • Критерии (их множество): количество кода, читабельность, переносимость между платформами, повторное использование и т. п. Предпосылки
  3. Парадигмы программирования • Императивное программирование (последовательное выполнение инструкций) • Модульное

    программирование • Функциональное программирование (программа по форме — множество вложенных функций, похоже на вычисление функции в математическом смысле) • Объектно-ориентированное программирование • И т. п.
  4. Парадигмы программирования • Философия, а не стандарты • (адепты ведут

    «священные войны» — холивары) • Форма воплощения парадигмы может отличаться от языка к языку • Один язык может включать множество парадигм
  5. История • Табличное представление данных • Табуляционные машины — перфокарты

    • Smalltalk (1970-е) • C++ (1983) • Java (1995) • Современные языки (эклектика, смешение стилей)
  6. Парадигма • Мыслить не базовыми примитивами (числами, массивами, строками), •

    А сущностями из предметной области — объектами • Программа — это множество объектов и их отношений • Объекты внутри — обычные переменные (именованные участки памяти) и ассоциированные с объектом подпрограммы
  7. • Набор полей — структура (базовый уровень) • Поведение (методы,

    полиморфизм) • Отношения (ссылки, контроль доступа — инкапсуляция, наследование) • Философия проектирования приложения
  8. Язык C++ • Бьёрн Страуструп • 1983 год • Книга:

    «Язык программирования С++», Бьёрн Страуструп
  9. Язык C++ • C++ — набор объектно-ориентированных расширений для языка

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

    type) • В некотором роде — расширение языка • Атрибуты класса, методы класса • Конструктор, деструктор • Уровень доступа: public, private, protected
  11. Выделение сущностей • Предметная область: парк табуляционных машин для подсчета

    статистических данных, записанных на перфокартах • Сущности: - перфокарта (анкета) - табуляционная машина (подсчет статистики на множестве карт) - сортировальная машина (деление карточек на группы)
  12. Объект: перфокарта • Одна перфокарта — одна анкета • Набор

    полей • Поля — целые числа, числа с плавающей точкой, отдельные символы, строки и т. п. • Каждое поле имеет уникальное имя внутри класса • Названия и тип полей повторяются от анкеты к анкете • Значения каждого поля отличаются в каждой анкете
  13. Возьмем поля • Пол: 'M', 'F' • Раса: "B", "W",

    "Ch", "Jp", ... • Страна рождения: "US", "Ir", "Fr", "Gr", "En", ...
  14. class PunchCard { private: char sex; char race [3]; char

    birth [3]; public: PunchCard(char sex, const char* race, const char* birth); char getSex(); char* getRace(); char* getBirth(); };
  15. Реализация методов • Программный код функций-методов • В С++ —

    за пределами класса • (хороший тон — в отдельном файле «.cpp») • «тип_данных ИмяКласса::имяМетода() {...}» • Но можно и внутри объявленного класса сразу при объявлении • (в других языках всё обычно происходит в одном файле)
  16. char PunchCard::getSex() { return sex; } char* PunchCard::getRace() { return

    race; } char* PunchCard::getBirth() { return birth; }
  17. Указатель this (этот) • Неявно передаётся в функцию-метод объекта 1-м

    параметром • Указывает на текущий объект • Все поля объекта (в т.ч. унаследованные) доступны по имени, как обычные внутренние переменные или параметры функции • Или через указатель this • (через оператор «стрелка»: «this->fieldName») • Применение: если внутренняя переменная или параметр функции называется так же, как и поле объекта
  18. char PunchCard::getSex() { return sex; } char PunchCard::getSex() { return

    this->sex; } char PunchCard::getSex(PunchCard* this) { return this->sex; }
  19. Конструктор (constructor) • Функция объекта (объявляется внутри класса) • Называется

    так же, как класс объекта • Не имеет возвращаемого значения (в коде не указывается) • Вызывается автоматически при создании объекта • Объект может иметь множество конструкторов • Конструктор без параметров — конструктор по умолчанию (default constructor)
  20. #include "string.h" PunchCard::PunchCard(char sex, const char* race, const char* birth)

    { this->sex = sex; strcpy(this->race, race); strcpy(this->birth, birth); }
  21. #include "string.h" PunchCard::PunchCard(PunchCard* this, char sex, const char* race, const

    char* birth) { this->sex = sex; strcpy(this->race, race); strcpy(this->birth, birth); }
  22. Экземпляры класса – объекты • Память выделяется • Размещение объекта

    на стеке • Размещение объекта на куче • Аналогично обычным стандартным типам данных
  23. #include "iostream" using namespace std; int main() { PunchCard card1('M',

    "B", "Fr"); cout << card1.getSex() << ", " << card1.getRace() << ", " << card1.getBirth() << endl; }
  24. • «#include "iostream"» — для ввода/вывода в духе С++ •

    (поток вывода — cout, окончание строки — endl) • «using namespace std;» — подключение пространства имен «std» • (чтобы можно было обращаться к «endl» без уточнения «std::endl»)
  25. Замечание • Размер объекта (экземпляра класса) в памяти — сумма

    размеров всех его полей • Размер объекта PuchCard = sizeof(sex) + sizeof(race) + sizeof(birth) = 1 + 3 + 3 = 7 байт • Давайте возьмем размер массива birth 4 вместо 3: char birth[3] → char birth[4] • Чтобы весь объект занимал в памяти 1 + 3 + 4 = 8 байт • (так будет удобнее рисовать картинки)
  26. class PunchCard { private: char sex; char race [3]; char

    birth [4]; // так будет удобнее // для картинок [...] };
  27. int main() { PunchCard card1('M', "B", "Fr"); cout << card1.getSex()

    << ", " << card1.getRace() << ", " << card1.getBirth() << endl; PunchCard* card2 = new PunchCard('F', "W", "Ir"); cout << card2->getSex() << ", " << card2->getRace() << ", " << card2->getBirth() << endl; delete card2; } Доступ к полям: оператор «.» (точка) vs «->» (стрелка)
  28. int main() { PunchCard* card1 = new PunchCard('M', "B", "Fr");

    cout << card1->getSex() << ", " << card1->getRace() << ", " << card1->getBirth() << endl; PunchCard* card2 = new PunchCard('F', "W", "Ir"); cout << card2->getSex() << ", " << card2->getRace() << ", " << card2->getBirth() << endl; PunchCard* card3 = card1; cout << card3->getSex() << ", " << card3->getRace() << ", " << card3->getBirth() << endl; delete card1; delete card2; }
  29. Кстати, самостоятельно: • Передача массива внутри объекта в качестве параметра

    функции по значению через стек • Возврат массива внутри объекта из функции в качестве возвращаемого значения (через стек?) • Вывести адрес объекта и адрес поля-массива внутри объекта • Посмотреть, к какому сегменту памяти относятся эти адреса
  30. Управление доступом (инкапсуляция) • Все поля объекта — публичные и

    закрытые (приватные), — доступны внутри функций- методов объекта • Публичные поля доступны за пределами объекта • Закрытые (приватные) поля не доступны за пределами объекта • Обратимся к полю private за пределами объекта
  31. int main() { PunchCard card1('M', "B", "Fr"); cout << card1.getSex()

    << ", " << card1.getRace() << ", " << card1.getBirth() << endl; // oop-hollerith.cpp:11:10: // error: ‘char PunchCard::sex’ is private cout << card1.sex << ", " << card1.race << ", " << card1.birth << endl; }
  32. $ g++ oop-hollerith.cpp oop-hollerith.cpp: In function ‘int main()’: oop-hollerith.cpp:11:10: error:

    ‘char PunchCard::sex’ is private char sex; ^ oop-hollerith.cpp:233:19: error: within this context cout << card1.sex << ", " << card1.race << ", " << card1.birth << endl; ^ oop-hollerith.cpp:12:17: error: ‘char PunchCard::race [3]’ is private char race [3]; ^ oop-hollerith.cpp:233:40: error: within this context cout << card1.sex << ", " << card1.race << ", " << card1.birth << endl; ^ oop-hollerith.cpp:13:18: error: ‘char PunchCard::birth [3]’ is private char birth [3]; ^ oop-hollerith.cpp:233:62: error: within this context cout << card1.sex << ", " << card1.race << ", " << card1.birth << endl; ^
  33. Управление доступом • Хороший тон — объявлять все внутренние поля

    объекта закрытыми (private) или защищенными (protected) • Для организации внешнего доступа добавлять к объекту специальные методы — геттеры (для чтения) и сеттеры (для записи) • «type getFieldName()», «void setFieldName(type val)»
  34. Управление доступом • В сеттер можно поместить код, который проверяет

    корректность устанавливаемых извне значений • Если добавить только геттер, то поле будет доступно только для чтения за пределами объекта
  35. Управление доступом • Управление доступом — уровень компилятора (синтаксический сахар)

    • В памяти публичные и закрытые поля — просто участки памяти известной длины с назначенными адресами (как обычные переменные)
  36. Статический полиморфизм • Полиморфизм вообще: одно имя у нескольких функций

    • Статический полиморфизм: одна функция отличается от другой по параметрам: порядок, количество параметров, их тип • По сути, для идентификации функции к её имени добавляются параметры • К какой из функций с одинаковым именем произошло обращение, решает компилятор (поэтому полиморфизм статический)
  37. void PunchCard::setSex(char sex) { this->sex = sex; } void PunchCard::setSex(bool

    isMale) { if(isMale) { this->sex = 'M'; } else { this->sex = 'F'; } } int main() { PunchCard card1('M', "B", "Fr"); card1.setSex('F'); card1.setSex(false); }
  38. • в Си так не получится — у каждой функции

    должно быть уникальное имя • Например: «set_sex (char)», «set_sex_binary(bool)» • Статический полиморфизм здесь — способ избавить разработчика от необходимости плодить для схожих по назначению функций разные имена: func_1, func_2, func_3, ...
  39. Объект: табуляционная машина • Принимает на входе стопку карт •

    Позволяет вычислить один или несколько статистических показателей — провести пакетную обработку карточек • Значения показателей накапливаются в счетчиках (один счетчик — один показатель)
  40. Объект: табуляционная машина • Логика счёта (какие именно показатели и

    по какой логике считать): предварительная настройка машины переключением проводов • Т. е. для каждого целевого набора статистических показателей мы делаем особую версию машины, • Которая с базовой версией машины имеет что-то общее, а кое в чём отличается
  41. class TabulatingMachine { protected: // счетчики (максимум 40) int counters[40];

    // метки счетчиков - строчки, максимум 24 символа (плюс завершающий 0) char counterLabels[40][25]; int counterCount; // перфокарты PunchCard** punchCards; int cardCount; void setupCounters(const char* const counterLabels[], int counterCount); public: TabulatingMachine(); void loadPunchCards(PunchCard** cards, int count); virtual void doCount() = 0; void resetCounters(); void printCounters(); };
  42. class TabulatingMachine { protected: // счетчики (максимум 40) int counters[40];

    // метки счетчиков — строчки, // максимум 24 символа (плюс завершающий 0) char counterLabels[40][25]; int counterCount; // перфокарты PunchCard** punchCards; int cardCount; [...] };
  43. class TabulatingMachine { protected: [...] void setupCounters(const char* const counterLabels[],

    int counterCount); public: TabulatingMachine(); void loadPunchCards(PunchCard** cards, int count); virtual void doCount() = 0; void resetCounters(); void printCounters(); };
  44. TabulatingMachine::TabulatingMachine() { counterCount = 0; cardCount = 0; // забьём

    все счетчики нулями // (иначе там будут не нули) for (int i = 0; i < 40; i++) { counters[i] = 0; counterLabels[i][0] = 0; } } Конструктор
  45. #include "string.h" void TabulatingMachine::setupCounters( const char* const counterLabels[], int counterCount)

    { for (int i = 0; i < counterCount; i++) { strcpy(this->counterLabels[i], counterLabels[i]); } this->counterCount = counterCount; } Настроить счетчики: скопируем настройки из параметров в поля объекта
  46. void TabulatingMachine::resetCounters() { for (int i = 0; i <

    40; i++) { counters[i] = 0; } } Сбросить счетчики
  47. void TabulatingMachine::loadPunchCards( PunchCard** cards, int count) { punchCards = cards;

    cardCount = count; } Загрузить стопку перфокарт: сохраним ссылки на внешние объекты
  48. #include "iostream" void TabulatingMachine::printCounters() { for (int i = 0;

    i < counterCount; i++) { cout << counterLabels[i] << ": " << counters[i] << endl; } } Напечатать значения счетчиков
  49. Итого (промежуточно) • Можем задать количество счетчиков и их имена

    • Сбросить (при необходимости) все счетчики в ноль • «Загрузить» стопку карт для последующей обработки • Вывести текущие значения настроенных счетчиков
  50. Итого (промежуточно) • Это то, что есть общего между любыми

    из возможных конфигураций машины • Теперь определим то, что их отличает — логику обработки стопки карт (счёт статистики) • Обратим еще раз внимание на метод «void doCount()»
  51. Метод doCount() • Объявлен как virtual (виртуальный) • Это такой

    метод, который можно переопределить в классе-наследнике текущего класса • При обращении к этому методу для объекта — экземпляра класса-наследника, будет вызвана новая (переопределенная) версия метода вместо той, которая определена в базовом классе
  52. Метод doCount() • «= 0» в конце виртуального метода (и

    отсутствие кода реализации в базовом классе) говорит о том, что это чистый виртуальный метод • Класс, в котором есть один или больше чистых виртуальных методов, называется абстрактным классом • Для абстрактных классов нельзя создавать их экземпляры — объекты • Их можно использовать в качестве базовых классов для классов-наследников, которые определят для себя все чистые виртуальные методы
  53. Замечание • Модификатор virtual для переопределяемого метода — особенность языка

    С++ • В других языках (например, в Java), все методы могут быть виртуальными по умолчанию • Чтобы запретить переопределение метода (в Java), его можно объявить как final • В других языках могут быть другие конструкции
  54. Итак • Внутри класса TabulatingMachine метод doCount() объявлен, но его

    содержимое не определено • Нам нужно создать новый класс, который будет являться наследником класса TabulatingMachine и определит внутри себя необходимую реализацию метода doCount()
  55. Здесь наследование • Имя класса-наследника • Через двоеточие — имя

    базового (родительского) класса • Все поля и методы из базового класса автоматически становятся частью класса-наследника (дочернего класса) • Модификатор public в этом случае — все поля и методы базового класса передаются в дочерний класс с сохранением настроек публичности
  56. Здесь наследование • Публичные методы (public) остаются публичными • Защищенные

    поля и методы (protected) доступны внутри класса-наследника, но не доступны извне • Закрытые поля и методы (private) всегда доступны только внутри базового класса, в котором они определены, и не доступны так же и для классов-наследников
  57. TabulatingMachineConfig1::TabulatingMachineConfig1() { const char* const labels[] = {"Males", "Females", "Whites"};

    setupCounters(labels, 3); } Конструктор: здесь настраиваем конкретные счетчики (3 штуки)
  58. void TabulatingMachineConfig1::doCount() { for (int i = 0; i <

    cardCount; i++) { if (punchCards[i]->getSex() == 'M') counters[0]++; if (punchCards[i]->getSex() == 'F') counters[1]++; if (strcmp(punchCards[i]->getRace(), "W") == 0) counters[2]++; } } Реализация метода doCount(): реальный подсчет статистики
  59. Здесь реализация: • 1-й счетчик — количество мужчин • 2-й

    счетчик — количество женщин • 3-й счетчик — количество людей (любого пола), у которых в анкете в поле «раса» указано «W» (белый)
  60. Порядок вызова конструкторов • При создании объекта сначала вызывается родительский

    конструктор — конструктор базового класса TabulatingMachine(), • потом конструктор класса-наследника — TabulatingMachineConfig1() • Самостоятельно: явный вызов конструктора базового класса, передача параметров в родительский конструктор из параметров конструктора-наследника
  61. TabulatingMachineConfig3::TabulatingMachineConfig3() { const char* const labels[] = {"Birth France", "Whites",

    "Male chinese"}; setupCounters(labels, 3); } Конструктор: здесь настраиваем конкретные счетчики (тоже 3 штуки)
  62. void TabulatingMachineConfig3::doCount() { for (int i = 0; i <

    cardCount; i++) { if (strcmp(punchCards[i]->getBirth(), "Fr") == 0) counters[0]++; if (strcmp(punchCards[i]->getRace(), "W") == 0) counters[1]++; if (punchCards[i]->getSex() == 'M' && strcmp(punchCards[i]->getRace(), "Ch") == 0) counters[2]++; } } Реализация метода doCount(): реальный подсчет статистики
  63. Здесь реализация: • 1-й счетчик — количество людей, родившихся во

    Франции • 2-й счетчик — количество белых • 3-й счетчик — количество мужчин, родившихся в Китае
  64. int main() { PunchCard* cards[10]; cards[0] = new PunchCard('M', "Jp",

    "Fr"); cards[1] = new PunchCard('F', "Ch", "Fr"); cards[2] = new PunchCard('M', "W", "Fr"); cards[3] = new PunchCard('M', "W", "Ir"); cards[4] = new PunchCard('M', "B", "Ir"); cards[5] = new PunchCard('M', "Ch", "En"); cards[6] = new PunchCard('M', "Ch", "Fr"); int cardCount = 7; [...]
  65. [...] TabulatingMachineConfig1 tabMachine1; tabMachine1.loadPunchCards(cards, cardCount); tabMachine1.doCount(); cout << endl <<

    "Tab machine config-1:" << endl; tabMachine1.printCounters(); TabulatingMachineConfig3 tabMachine3; tabMachine3.loadPunchCards(cards, cardCount); tabMachine3.doCount(); cout << endl << "Tab machine config-3:" << endl; tabMachine3.printCounters(); [...]
  66. $ g++ oop-hollerith.cpp $ ./a.out Tab machine config-1: Males: 6

    Females: 1 Whites: 2 Tab machine config-3: Birth France: 4 Whites: 2 Male chinese: 2
  67. Динамический полиморфизм • В переопределении виртуальных методов класса не было

    бы большого толка, если бы мы просто создавали переменные объекты-наследники и обращались к методу через них • Но: в языке С++ есть возможность настраивать указатель базового класса на объект класса-наследника (любого наследного колена) без приведения типов • Для такого указателя штатно доступны все поля и методы базового класса (для которого он и объявлен), но не доступны расширения класса-наследника
  68. Динамический полиморфизм • При этом: если обратиться к виртуальной функции

    базового класса (это возможно сделать, т. к. она определена в его интерфейсе), будет вызвана актуальная реализация этой же функции, т. е. версия функции, которая переопределена для этого объекта • Один и тот же указатель можно перенастраивать на разные объекты — экземпляры различных классов-наследников с разными реализациями одной и той же виртуальной функции • В каждом из случаев будет вызвана разная версия этой функции — какая именно, будет решено во время выполнения, т. е. динамически
  69. Динамический полиморфизм • Таким образом, у нас есть несколько функций

    с одним и тем же именем — это полиморфизм • Решение, какая из этих функций будет вызвана в той или иной ситуации, принимается во время выполнения программы • Поэтому такой полиморфизм — динамический
  70. class TabulatingMachine { protected: [...] public: [...] int getCounterCount(); char*

    getCounterLabel(int counter); int getCounterValue(int counter); };
  71. int TabulatingMachine::getCounterCount() { return counterCount; } char* TabulatingMachine::getCounterLabel(int counter) {

    return counterLabels[counter]; } int TabulatingMachine::getCounterValue(int counter) { return counters[counter]; }
  72. void tabulate(TabulatingMachine* tabMachine, PunchCard** cards, int cardCount) { cout <<

    endl << "Tab machine:" << endl; tabMachine->loadPunchCards(cards, cardCount); tabMachine->resetCounters(); tabMachine->doCount(); for (int i = 0; i < tabMachine->getCounterCount(); i++) { cout << tabMachine->getCounterLabel(i) << ": " << tabMachine->getCounterValue(i) << endl; } }
  73. Здесь • В параметр «TabulatingMachine* tabMachine» можно передавать адреса объектов

    TabulatingMachineConfig1 или TabulatingMachineConfig3 • Обращение «tabMachine->doCount()» в реальности вызовет реализацию doCount из TabulatingMachineConfig1 или из TabulatingMachineConfig3 • Заранее не известно — зависит от того, какой будет передан в функцию объект
  74. $ g++ oop-hollerith.cpp $ ./a.out Tab machine: Males: 6 Females:

    1 Whites: 2 Tab machine: Birth France: 4 Whites: 2 Male chinese: 2
  75. Динамический полиморфизм • Так это выглядит • TODO: реализация —

    как это работает: указатель на виртуальную функцию в качестве внутреннего поля класса (программисту не видно)
  76. TODO: Деструктор • Добавить динамическую память внутри класса • Например,

    не выделять память под счетчики заранее • Тогда их нужно будет в деструкторе удалить
  77. Самостоятельно • Конструктор копирования • Деструктор • Перегрузка операторов •

    Множественное наследование • Шаблоны классов (templates) • Структура проекта: заголовочные файлы *.h, модули с реализацией — *.cpp • ...
  78. OOP vs OOD • Object oriented programming (OOP) vs Object

    oriented design (OOD) • Объектно-ориентированное программирование (ООП) vs Объектно- ориентированное проектирование (ООП) • См: Шаблоны (паттерны) проектирования
  79. Задание • Воспроизвести код лекции • Реализовать собственную настройку машины

    — наследник класса TabulatingMachine с собственной реализацией «doCount()» • Провести демонстрацию на собственной стопке карточек-анкет
  80. Задание (бонус) • Реализовать сортировальную машину в виде нового класса

    • Подумать: есть ли у неё что-то общее с табуляционной машиной • Если да, можно ли её реализацию наследовать от базового класса TabulatingMachine? • Если нет, как можно переработать код, чтобы создать общий для всех базовый класс?