Программирование на основе классов и шаблонов

Описание и реализация классов

Аладин Дмитрий Владимирович

iu5edu.ru/wiki/cpp2

План

  1. Различия подходов к представлению данных и процедур.
  2. Использование UML для представления объектов и классов.
  3. Типы C++.
  4. Классы и структуры.
  5. Спецификация класса.
  6. Специальные члены класса.
  7. Управление доступом к классу.
  8. Дружественность.
  9. Статические члены класса.

В предыдущих лекциях

Разговоры об объектно-ориентированном проектировании (OOD) и программировании (OOP).

center

Шутки кончились, теперь все серьезно!

center

Все, что будет далее сказано, будет на экзамене!

Посадил дед репку. Выросла репка большая-пребольшая.

center

Пошел дед репку рвать: тянет-потянет, вытянуть не может!

center

Позвал дед бабку: бабка за дедку, дедка за репку — тянут-потянут, вытянуть не могут!

center

Позвала бабка внучку: внучка за бабку, бабка за дедку, дедка за репку — тянут-потянут, вытянуть не могут!

center

А может что-то посложнее?

Серьезные программисты используют для рассказа сказки UML!

Унифицированный язык моделирования (UML) играет важную роль во многих отраслях, поскольку он дает возможность визуально показать поведение и структуру системы или процесса.

UML помогает продемонстрировать возможные ошибки в структурах приложений, поведении системы и других бизнес-процессах.

Сказка про репку на языке UML

Позаимствовано из поста, основано на материале.

center

Что такое диаграмма последовательности?

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

Нотация: линия жизни

Последовательность состоит из lifeline, которые должны быть расположены горизонтально в верхней части диаграммы.

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

Нотация: агент

Нотация жизненной линии с символом элемента actor используется в том случае, если конкретная диаграмма последовательности принадлежит случаю использования - в интересах каких лиц происходит выполнение того или иного действия.

Нотация: сущность

Линия жизни с элементом entity представляет системные данные.

Например, в приложении "Обслуживание клиентов" организация-заказчик будет управлять всеми данными, относящимися к клиенту.

Нотация: граница

Линия жизни с boundary элементом обозначает системную границу/программный элемент в системе.

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

Нотация: управление

Линия жизни с элементом control указывает на контролирующую организацию или менеджера.

Он организует и составляет график взаимодействия между границами и субъектами и выступает в качестве посредника между ними.

А сказку про процедурный подход можно?🤓

center

Позаимствовано из поста.

К чему это было?

К различиям подходов к представлению данных и процедур!

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

Абстракции и классы

Абстракции позволяют легко перейти к определяем пользователем типам, которые в C++ представлены классами, реализующий абстрактный интерфейс!

Что такое тип C++

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

Вы можете создать собственный тип, определив class или struct.

Тип задает:

  • объем памяти, выделяемой для переменной (или результата выражения);
  • вид хранимых значений;
  • способ интерпретации значений компилятором битовых шаблонов в этих значениях;
  • операции, которые можно выполнять со значением.

Скалярный тип

— тип, содержащий одно значение определенного диапазона.

Скаляры включают:

  • арифметические типы (целочисленные или значения с плавающей запятой);
  • элементы типа перечисления;
  • типы указателей;
  • типы указателей на члены;
  • std::nullptr_t.

Основными типами обычно являются скалярные типы.

Составной тип

— тип, который не является скалярным типом 🤓

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

Переменная

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

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

Объект

— экземпляр класса или структуры. При использовании в общем смысле он включает все типы, даже скалярные переменные.

center

Классы и структуры

Классы и структуры являются конструкциями, в которых пользователь определяет собственные типы.

Классы и структуры могут включать данные-члены и функции-члены, позволяющие описывать состояние и поведение данного типа.

Существует три типа классов: структура, класс и объединение.

Они объявляются с помощью ключевых слов struct, class и union. В следующей таблице показаны различия между этими тремя типами классов.

Управление доступом и ограничения для структур, классов и объединений

Структуры Классы Объединения
struct class union
Доступ по умолчанию: public (открытый). Доступ по умолчанию: private (закрытый). Доступ по умолчанию: public (открытый).
Нет ограничений на использование Нет ограничений на использование Используется только один член за один раз

Спецификация класса

  • Объявление класса, описывающее компоненты данных в терминах членов данных, а также открытый интерфейс в терминах функций-членов, называемых методами.
  • Определения методов класса, которое описывают, как реализованы определенные функции-члены.

Грубо говоря:

  • объявление класса = общий обзор класса;
  • определение методов класса = детали класса.

Что такое интерфейс?

Интерфейс — это какая-то штука, которая помогает взаимодействовать двум системам или, условно говоря, двум другим штуками. (С) 🤡

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

P.S. Мы не говорим про то, как интерфейсы реализованы Go, C# или Java. В C++ интерфейсов, строго говоря, нет вообще ¯\_(ツ)_/¯.

Пример объявления класса

// TestRun.h
class TestRun {
public:
    TestRun() = default; // Используется конструктор по умолчанию, сгенерированный компилятором:
    TestRun(const TestRun &) = delete; // Не использовать конструктор копирования
    TestRun(std::string name);
    void DoSomething();
    int Calculate(int a, double d);
    virtual ~TestRun(); // Деструктор
    enum class State { Active, Suspended }; // Вложенное определение пользовательских типов
protected:
    virtual void Initialize();
    virtual void Suspend();
    State GetState();
private:
    State _state{State::Suspended};
    std::string _testName{""};
    int _index{0};
    static int _instances;
};

int TestRun::_instances{0}; // определить и инициализировать статический элемент.

Займемся изучением анатомии класса!

center

Следующий материал основывается на материале от @ashtanyuk.
Исходный материал публикуется с сохранением условий распространения.

Доступность членов

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

  • private - закрытый;
  • protected - защищенный
  • public - открытый

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

Обычно используют заголовочные файлы .h, в которые помещают описание класса. Реализация методов класса обычно размещается в .cpp.

Пример объявления класса в Counter.h

#ifndef _COUNTER_H
#define _COUNTER_H_

typedef unsigned int count_t;

class Counter {
private:
    count_t count;
public:
    void reset();
    void inc();
    count_t get() const;
};
#endif

Пример реализации класса в Counter.cpp

#include "Counter.h"

void Counter::reset() {
    count = 0;
}

void Counter::inc() {
    count++;
}

count_t Counter::get() const {
    return count;
}

Флешбеки сборки через консоль

В проекте, где необходимо использовать класс Counter, выполняется включение заголовочного файла в те модули, которые используют класс, а далее выполняется компоновка с откомпилированной версией Counter.cpp.

Можно использовать следующую команду при работе с g++ ( предполагается, что в main основная программа):

g++ -o app main.cpp Counter.cpp

Создание экземпляров класса (на стеке)

Экземпляры класса (объекты) создаются различными способами. Если мы хотим создать их в стеке, подобно обычным автоматическим переменным, то пишем так:

int main() {
    Counter pass;
    ...
}

Также создаются и массивы объектов:

    Counter pass[10];

Вспомним про стек и кучу

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

center

Создание экземпляров класса (на куче)

Самый популярный способ - создание динамических объектов (в куче):

int main() {
    Counter *pass = new Counter;     // одиночный объект
    Counter *zoo = new Counter[10];  // массив из объектов
    ...
}

Не забываем об освобождении динамической памяти:

    ...
    delete pass;
    delete[] zoo;
    ...

Не забывайте корректно работать с памятью!

center

И читайте хорошие книги!

center

Специальные члены класса

Как же я люблю СПЕЦИАЛЬНЫЕ ЧЛЕНЫ КЛАССА. Вот они слева направо:

  • конструкторы;
  • деструкторы;
  • указатель this.

center

Конструктор

Конструктор - это метод класса, который вызывается при создании объекта, сразу же после выделения объекту памяти.
Основное назначение этого метода - провести инициализацию полей класса и выделить дополнительную память под внутренние переменные или массивы. Конструкторы бывают:

  • По-умолчанию - это конструктор без параметров в случае описания и неявный конструктор, назначаемый самой программой в случае отсутствия явного описания.
  • Обычный - конструктор с параметрами.
  • Копирующий - создающий копию имеющегося объекта.
  • Перемещающий - выполняющий действия по перемещению данных их одного объекта в другой (С++11).

Инициализация полей класса непосредственно при объявлении

В стандарте С++11 появилась возможность инициализировать поля класса непосредственно при объявлении:

class Counter {
private:
    count_t count = 0;
public:
    ...
};

Инициализация полей класса непосредственно при объявлении

При наличии нескольких полей, при инициализации можно ссылаться на значения ранее инициализированных полей:

class A {
private:
    count_t count1 = 0;
    count_t count2 = count_1 + 1;
    count_t count3 = count_2 + 1;
public:
    ...
};

P.S. Инициализация в конструкторе будет в приоритете.

Пример объявления разных конструкторов

class String {
private:
    char *buf; // поле для хранения символьного массива
    size_t len; // длина строки
public:
    String(size_t); // конструктор с параметром числового типа
    String(const char * = nullptr); // конструктор с параметром-указателем
    String(const String &); // конструктор копирования
    String(String &&); // конструктор перемещения
    ...
};

Пример реализации разных конструкторов

String::String(size_t len) {
    this->len = len;
    buf = new char[len];
    *buf = 0;
}

String::String(const char *str) : String(strlen(str) + 1) {
    strcpy(buf, str);
}

String::String(const String &s) : String(s.len) {
    strcpy(buf, s.buf);
}

String::String(String &&s) {
    buf = s.buf;
    len = s.len;
    s.buf = nullptr;
    s.len = 0;
}

Явный вызов конструктора

Явный вызов конструкторов, то есть операция выделения памяти находится в первом конструкторе и мы вызываем его явно из других:

String::String(const char *str) : String(strlen(str) + 1) {
    strcpy(buf, str);
}

String::String(const String &s) : String(s.len) {
    strcpy(buf, s.buf);
}

Перемещающий конструктор

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

String::String(String &&s) {
    buf = s.buf;
    len = s.len;
    s.buf = nullptr;
    s.len = 0;
}

Деструктор класса

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

class String {
    ...
public:
    ...
    ~String();
    ...
};

String::~String() {
    delete[] buf;
}

Указатель this

Указатель this позволяет объекту узнать свой адрес или сослаться на члены класса в случае неоднозначности имен-параметров:

String(int len) {
    this->len = len; // уточняем имена
}
...
String &get() {
    return *this; // возвращаем ссылку на себя
}   

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

Еще одной интересной возможностью стандарта С++11 является явное указание того, что тело стандартного метода должно быть выбрано по-умолчанию. Для этого существует ключевое слово default:

class Foo {
public:
    Foo() = default;
    Foo(int x) {/* ... */}
};

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

Спецификатор default может применяться только к специальным функциям-членам:

  • конструктор по-умолчанию;
  • конструктор копий;
  • конструктор перемещения;
  • оператор присваивания;
  • оператор перемещения;
  • деструктор.

Спецификатор delete

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

class Foo {
public:
    Foo() = default;
    Foo(const Foo &) = delete;
    void bar(int) = delete;
    void bar(double) {}
};

// ...
Foo obj;
obj.bar(5);     // Call to deleted member function 'bar'
obj.bar(5.42);  // ok

Пример использования класса String

int main() {
    String a("abc");
    String b{"qwerty"};
    String t{move(a)};
    a = move(b);
    b = move(t);
    ...
}

a создается традиционным способом, а b использует список инициализации {}. В следующих трех строчках реализуется алгоритм обмена содержимым между a и b. Для задействования конструктора перемещения мы используем функцию move из std.

Присваивание и инициализация

После того, как переменная была определена, вы можете присвоить ей значение через = - копирующая инициализация или присваивание. Для этого нужны как минимум две инструкции: определения переменной и присвоения значения. Эти два шага можно совместить.

Переменные можно инициализировать тремя способами:

int value1 = 1;     // копирующая инициализация
double value2(2.2); // прямая инициализация
char value3 {'c'};  // унифицированная инициализация

Модификатор конструктора explicit

Конструктор с таким модификатором explicit запрещает создание объекта с преобразованием типа:

class String {
    ...
public:
    explicit String(const char *);   // конструктор с параметром числового типа
    ...
};

int main() {
    String a("abc");     // разрешено
    String b{"qwerty"};  // разрешено
    String c = {"123"};  // запрещено! Неявное преобразование строки в объект String
    String d = "zxc";    // запрещено! Неявное преобразование строки в объект String
    ...

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

Инициализация в конструкторе (константы, ссылки)

class Mathem {
private:
    const double pi_;
public:
    Mathem(const double pi) {
        pi_ = pi;
    }
};

int main() {
    Mathem m{3.14159};
    return 0;
}

При компиляции возникает ошибка:

error: constructor for 'Mathem' must explicitly initialize
    the const member 'pi_'

Списки инициализаторов членов в конструкторах

class Mathem {
private:
    const double pi_;
public:
    Mathem(const double pi) : pi_{pi} {}
};

int main() {
    Mathem m{3.14159};
    return 0;
}

Можно и проще...

class Mathem {
private:
    const double pi_ = 3.14159;
public:
};

int main() {
    Mathem m;
    return 0;
}

Но тогда не сможете реализовать свою альтернативную математику для подгонки лабы по физике 😈

Пример с ссылками

class A {
};

class B {
private:
    const A &a_;
public:
    B(const A &a) : a_{a} {}
};

int main() {
    A a1;
    B b1{a1};
    return 0;
}

Управление доступом к классу (инкапсуляция)

Главная забота класса - скрыть как можно больше информации. Существует 4 вида пользователей класса:

  1. сам класс;
  2. обычные пользователи (другие классы);
  3. производные классы;
  4. дружественные классы;

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

Правила доступа к элементам класса

  • Сам класс имеет полный доступ ко всем своим элементам;
  • Обычные пользователи (другие классы) имеют полный доступ только к открытому (public) разделу;
  • Производные классы имеют доступ к public-разделу и к разделу protected;
  • Дружественные классы и функции имеют полный доступ ко всем разделам.

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

Полное и неполное объявление класса

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

class MyClass; // ПРЕДВАРИТЕЛЬНОЕ ОБЪЯВЛЕНИЕ

MyClass *mc; // для этого мы добавили неполное объявление

class MyClass { // ПОЛНОЕ ОПИСАНИЕ
    ......
};

Дружественность

Дружественный класс имеет доступ как к общедоступным, так и к закрытым членам другого класса. С ключевым словом friend может быть объявлен как целый класс, так и отдельная функция.

class MyClass1 {
    ...
};

class MyClass2 {
    friend MyClass1;
    ...
};

Отношение дружественности не наследуется!

Статические члены класса

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

Статические переменные должны инициализироваться вне классов, вне зависимости от уровня доступа.

Пример использования статических членов класса

#include <iostream>

class Something {
private:
    static int s_value;
public:
    static int get() {
        return s_value;
    }
};

int Something::s_value{1};

int main() {
    std::cout << Something::get() << '\n';
    return 0;
}

Популярный пример использования статических членов класса

Один из популярных примеров использования статических переменных - счетчики объектов.

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

P.S. Узнаете вторую лабораторную работу?

Вопросы?

center

If not, just clap your hands!