Часть 1. Вспоминаем основы C++

В этом разделе мы освежим в памяти ключевые определения C++: указатели, динамическое управление памятью, функции, модификаторы видимости и классы.

1. Ссылки и указатели, работа с динамической памятью

Работа с указателями и динамической памятью — одна из основ C++, так как это напрямую влияет на производительность и безопасность программ.

Умные указатели — это современная концепция в C++, которая автоматизирует управление памятью, значительно снижая вероятность утечек памяти. Базовая информация о "умных указателях" хорошо разобрана в видеоуроке.

Основные понятия:

  • Сырые указатели: Хранят прямой адрес в памяти.

    int* p = new int(10); // Динамическое выделение памяти
    delete p; // Очистка выделенной памяти
    
  • Умные указатели: Упрощают управление памятью с помощью классов std::unique_ptr и std::shared_ptr, что исключает необходимость ручного удаления памяти.

    // Память автоматически освободится при выходе за область видимости
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    

Практическое задание: решаем Задачу 3 из раздела по матрицам.

2. Подпрограмммы

Пример простой подпрограммы:

#include <iostream>

// Функция для сложения двух чисел
int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5);
    std::cout << "Сумма: " << result << std::endl;
    return 0;
}

Методы и подпрограммы: в чем разница?

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

3. Модификаторы видимости

Модификаторы видимости контролируют доступ к членам класса или структуры (переменным и функциям). C++ предоставляет три основных модификатора:

  • public: Члены класса доступны извне.
  • private: Члены класса доступны только внутри самого класса.
  • protected: Члены класса доступны внутри класса и его дочерних классов.

4. Структуры

Структуры в C++ — это пользовательские типы данных, которые группируют переменные под одним именем.

В структурах все члены по умолчанию имеют модификатор видимости public.

Пример простой структуры:

struct Point {
    int x;
    int y;
};

int main() {
    Point p;
    p.x = 10;
    p.y = 20;
    std::cout << "Точка: (" << p.x << ", " << p.y << ")" << std::endl;
    return 0;
}

5. Классы и объекты

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

Конструкторы

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

Виды конструкторов:

1. Конструктор по умолчанию

Не принимает аргументов и используется для инициализации объекта без входных данных.

class Rectangle {
private:
    int width;
    int height;

public:
    Rectangle() : width(0), height(0) {}  // Конструктор по умолчанию
};

2. Конструктор с параметрами

Принимает параметры и инициализирует члены класса конкретными значениями.

class Rectangle {
private:
    int width;
    int height;

public:
    Rectangle(int w, int h) : width(w), height(h) {}  // Конструктор с параметрами
};

3. Конструктор копирования

Создаёт новый объект на основе существующего.

class Rectangle {
private:
    int width;
    int height;

public:
    Rectangle(const Rectangle& rect) : width(rect.width), height(rect.height) {}  // Конструктор копирования
};

4. Конструктор перемещения

Перемещает ресурсы от одного объекта к другому без их копирования (C++11 и выше).

class Rectangle {
private:
    int* data;

public:
    Rectangle(Rectangle&& rect) noexcept : data(rect.data) {
        rect.data = nullptr;
    }  // Конструктор перемещения
};

Операторы присваивания

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

1. Оператор присваивания копирования

Осуществляет копирование данных одного объекта в другой.

class Rectangle {
private:
    int* data;

public:
    Rectangle& operator=(const Rectangle& rect) {
        if (this == &rect) return *this;  // Защита от самоприсваивания
        delete data;  // Очистка предыдущих данных
        data = new int(*rect.data);  // Копирование
        return *this;
    }
};

2. Оператор присваивания перемещения

Перемещает ресурсы от одного объекта к другому без копирования (C++11 и выше).

class Rectangle {
private:
    int* data;

public:
    Rectangle& operator=(Rectangle&& rect) noexcept {
        if (this == &rect) return *this;
        delete data;  // Очистка предыдущих данных
        data = rect.data;  // Перемещение данных
        rect.data = nullptr;  // Очищаем исходный объект
        return *this;
    }
};

Деструктор

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

class Rectangle {
private:
    int* data;

public:
    ~Rectangle() {
        delete data;  // Освобождение динамической памяти
        std::cout << "Прямоугольник уничтожен" << std::endl;
    }
};

Для подробного изучения конструкторов и деструкторов см. видеоурок.

Листы инициализации

Листы инициализации позволяют инициализировать члены класса до выполнения тела конструктора.

class Rectangle {
private:
    const int width;
    int height;

public:
    Rectangle(int w, int h) : width(w), height(h) {}
};

Правило "Трёх" и "Пяти"

Если класс управляет динамическими ресурсами, необходимо следовать правилу трёх или пяти:

  1. Конструктор копирования.
  2. Оператор присваивания копирования.
  3. Деструктор.
  4. Конструктор перемещения (C++11).
  5. Оператор присваивания перемещения (C++11).

Удаление стандартных конструкторов

В некоторых случаях необходимо удалить автоматические версии стандартных конструкторов. Это делается для предотвращения копирования или перемещения объектов класса. Обычно это требуется для классов, управляющих уникальными ресурсами, такими как файлы, сокеты или потоки.

Пример удаления конструкторов:

class NonCopyable {
public:
    NonCopyable() = default;  // Конструктор по умолчанию
    NonCopyable(const NonCopyable&) = delete;  // Удалённый конструктор копирования
    NonCopyable& operator=(const NonCopyable&) = delete;  // Удалённый оператор присваивания
};

Задания для самостоятельной работы

Задание 1: Реализация класса Book

Создайте класс Book, который моделирует книгу в библиотеке.

Требования:

  1. У класса должны быть члены данных:
    • Название книги (строка).
    • Автор (строка).
    • Год издания (целое число).
    • Количество страниц (целое число).
  2. Конструктор должен принимать название книги, автора и год издания. Количество страниц задаётся по умолчанию равным 0.
  3. Реализуйте методы:
    • setPages(int pages): метод для установки количества страниц.
    • getDescription(): метод для вывода информации о книге в формате: "Название: ..., Автор: ..., Год: ..., Страниц: ...".
  4. Реализуйте конструктор копирования и оператор присваивания для класса.

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

int main() {
    Book book1("Война и мир", "Лев Толстой", 1869);
    book1.setPages(1225);
    book1.getDescription();  // Ожидаемый вывод: Название: Война и мир, Автор: Лев Толстой, Год: 1869, Страниц: 1225

    Book book2 = book1;  // Копирование
    book2.getDescription();  // Ожидаемый вывод: Название: Война и мир, Автор: Лев Толстой, Год: 1869, Страниц: 1225
    return 0;
}
Вариант реализации (стараемся сильно не смотреть сюда):
#include <iostream>
#include <string>

class Book {
private:
    std::string m_title;
    std::string m_author;
    int m_year;
    int m_pages;

public:
    Book(const std::string& title, const std::string& author, int year, int pages = 0)
        : m_title(title), m_author(author), m_year(year), m_pages(pages) {}

    Book(const Book& other)
        : m_title(other.m_title), m_author(other.m_author), m_year(other.m_year), m_pages(other.m_pages) {}

    Book& operator=(const Book& other) {
        if (this == &other) return *this;
        m_title = other.m_title;
        m_author = other.m_author;
        m_year = other.m_year;
        m_pages = other.m_pages;
        return *this;
    }

    void setPages(int pages) {
        m_pages = pages;
    }

    void getDescription() const {
        std::cout << "Название: " << m_title
                  << ", Автор: " << m_author
                  << ", Год: " << m_year
                  << ", Страниц: " << m_pages << std::endl;
    }
};

int main() {
    Book book1("Война и мир", "Лев Толстой", 1869);
    book1.setPages(1225);
    book1.getDescription();

    Book book2 = book1;
    book2.getDescription();

    return 0;
}

Задание 2: Класс Vector с управлением ресурсами

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

Требования:

  1. Конструктор с параметром: Принимает размер массива и инициализирует его элементами (например, нулями).
  2. Конструктор копирования: Создайте конструктор копирования, который делает глубокую копию массива.
  3. Конструктор перемещения: Реализуйте конструктор перемещения, который перемещает данные из одного объекта в другой, освобождая исходный объект.
  4. Оператор присваивания копированием: Реализуйте оператор присваивания, который корректно копирует данные с проверкой на самоприсваивание.
  5. Оператор присваивания перемещением: Реализуйте оператор присваивания перемещением для оптимизации работы с временными объектами.
  6. Метод sum(): Напишите метод, который вычисляет сумму всех элементов в массиве.
  7. Деструктор: Освобождает ресурсы (память), выделенные для массива.

Дополнительные задачи (рекомендуется):

  1. Метод resize(size_t new_size): Добавьте возможность изменять размер массива. Если новый размер больше старого, новые элементы инициализируются нулями. Если меньше — лишние элементы удаляются.
  2. Метод slice(size_t start, size_t end): Реализуйте метод, который возвращает подмассив (срез) от индекса start до индекса end (включительно). Срез должен возвращаться как новый объект Vector.
  3. Итераторы: Реализуйте методы begin() и end() для поддержки диапазонных циклов (range-based for loops). Это позволит использовать объект Vector в стандартных конструкциях цикла.
  4. Метод push_back(int value): Добавьте возможность добавлять элемент в конец массива, динамически увеличивая его размер.
  5. Метод find(int value): Реализуйте метод поиска элемента. Возвращает индекс первого найденного элемента с данным значением или -1, если элемент не найден.

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

int main() {
    Vector v1(5);  // Вектор из 5 элементов
    v1.push_back(10);  // Добавление элемента в конец
    Vector v2 = v1.slice(1, 3);  // Создание среза вектора

    Vector v3 = v2;  // Копирование
    Vector v4 = std::move(v2);  // Перемещение

    for (int x : v4) {  // Использование итераторов
        std::cout << x << " ";
    }

    std::cout << "Сумма элементов v3: " << v3.sum() << std::endl;
    std::cout << "Индекс элемента 10 в v4: " << v4.find(10) << std::endl;

    return 0;
}
Вариант реализации основных требований (стараемся сильно не смотреть сюда):
#include <iostream>
#include <algorithm>
#include <numeric>

class Vector {
private:
    int* m_data;
    size_t m_size;

public:
    Vector(size_t size) : m_size(size), m_data(new int[size]) {
        std::fill(m_data, m_data + m_size, 0);
    }

    Vector(const Vector& other) : m_size(other.m_size), m_data(new int[other.m_size]) {
        std::copy(other.m_data, other.m_data + other.m_size, m_data);
    }

    Vector(Vector&& other) noexcept : m_size(other.m_size), m_data(other.m_data) {
        other.m_data = nullptr;
        other.m_size = 0;
    }

    Vector& operator=(const Vector& other) {
        if (this == &other) return *this;
        delete[] m_data;
        m_size = other.m_size;
        m_data = new int[m_size];
        std::copy(other.m_data, other.m_data + m_size, m_data);
        return *this;
    }

    Vector& operator=(Vector&& other) noexcept {
        if (this == &other) return *this;
        delete[] m_data;
        m_data = other.m_data;
        m_size = other.m_size;
        other.m_data = nullptr;
        other.m_size = 0;
        return *this;
    }

    int sum() const {
        return std::accumulate(m_data, m_data + m_size, 0);
    }

    ~Vector() {
        delete[] m_data;
    }
};

int main() {
    Vector v1(5);
    std::cout << "Сумма элементов: " << v1.sum() << std::endl;

    Vector v2 = v1;
    std::cout << "Сумма элементов (копия): " << v2.sum() << std::endl;

    Vector v3 = std::move(v1);
    std::cout << "Сумма элементов (перемещение): " << v3.sum() << std::endl;

    return 0;
}

Задание 3: Класс Matrix с использованием Vector

Создайте класс Matrix, который будет динамически управлять двумерным массивом (матрицей) целых чисел, используя класс Vector, разработанный в задании 2, для хранения строк матрицы.

Требования:

  1. Конструктор с параметрами: Принимает количество строк и столбцов и инициализирует их элементами, используя объекты Vector для хранения строк.
  2. Конструктор копирования: Создайте конструктор копирования, который делает глубокую копию матрицы, используя глубокое копирование векторов.
  3. Конструктор перемещения: Реализуйте конструктор перемещения, который перемещает данные из одного объекта в другой, освобождая исходный объект.
  4. Оператор присваивания копированием: Реализуйте оператор присваивания, который корректно копирует данные матрицы с проверкой на самоприсваивание.
  5. Оператор присваивания перемещением: Реализуйте оператор присваивания перемещением для оптимизации работы с временными объектами.
  6. Метод transpose(): Напишите метод, который транспонирует матрицу, меняя строки и столбцы местами (выполняйте перестановку векторов).
  7. Деструктор: Освобождает ресурсы (память), выделенные для матрицы, а также освобождает все используемые векторы.

Дополнительные задачи (рекомендуется):

  1. Метод resize(size_t new_rows, size_t new_cols): Используйте метод resize() класса Vector для изменения размеров матрицы. Если новые размеры больше старых, новые элементы инициализируются нулями.
  2. Метод slice(size_t row_start, size_t row_end, size_t col_start, size_t col_end): Реализуйте метод, который возвращает подматрицу (срез), используя срезы векторов (метод slice() класса Vector).
  3. Метод determinant(): Реализуйте метод для вычисления детерминанта матрицы (для квадратных матриц).
  4. Метод multiply(const Matrix& other): Реализуйте метод, который умножает матрицу на другую матрицу. Проверьте совместимость размеров для умножения.
  5. Метод find(int value): Реализуйте метод поиска элемента в матрице, используя метод find() класса Vector. Возвращает пару индексов (строка, столбец) первого найденного элемента.

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

int main() {
    Matrix m1(3, 3);  // Матрица 3x3
    m1.resize(4, 4);  // Изменение размера матрицы на 4x4
    Matrix m2 = m1.slice(1, 3, 1, 3);  // Создание подматрицы

    Matrix m3 = m2;  // Копирование
    Matrix m4 = std::move(m2);  // Перемещение

    m4.transpose();  // Транспонирование матрицы

    std::pair<int, int> pos = m4.find(10);  // Поиск элемента в матрице
    std::cout << "Позиция элемента 10: (" << pos.first << ", " << pos.second << ")" << std::endl;

    return 0;
}