Skip to content

Latest commit

 

History

History
187 lines (150 loc) · 7.41 KB

lecture8.md

File metadata and controls

187 lines (150 loc) · 7.41 KB

Лекция №8 Проблемы с точностью. Раздельная компиляция. Перегрузка операторов

Проблемы с точностью.

Тип double хранится в памяти в виде знака, мантисы и порядка, а число представляется, как: .

Понятно, что вещественных чисел бесконечно, а представимых значений конечно, поэтому некоторые числа представить невозможно. Например, для иррациональных чисел хранятся близкие к ним значения, из-за чего некоторые математически верные равенства могут слегка отличаться. Чтобы бороться с этим, сравнивать double нужно с использованием eps (сравнивать модуль разности с эпсилоном, например). Точность double уменьшается при увеличении модуля числа, из-за чего не надо использовать его в качестве счетчика и т.д.

Раздельная компиляция

Объект - сущность, обладающая 3 свойствами: состоянием (поля класса), поведением (методы класса, реакция объекта на получение внешних сообщений) и идентичностью (позволяет отличать объекты друг от друга).

Пример: int x: хранит значение (состояние), операции сложения и т.д. (поведение), обладает адресом в памяти (идентичность).

Класс - множество объектов со схожими свойствами.

Инкапсуляция - отделение интерфейса от реализации.

Разделение на .h и .cpp

Когда происходит подключение header-файла, его содержимое просто вставляется. .cpp файлы компилируются раздельно, образуя .o файлы, при этом файлы повторно компилируются только при изменении. В .o содержатся "заглушки" на местах вызова функций других единиц трансляции, а линкер совмещает эти файлы.

Первый вариант реализации класса:

class A {
    int f() {
        ...
        return 0;
    }
    ...
};

Реализация функции будет inline (в место вызова функции будет вставляться код из f), а значит при изменении f будут перекомпилироваться все файлы, вызывающие эту функцию, но эта реализация незначительно быстрее.

Второй вариант (рекомендуемый) реализации класса:

A.h

class A {
    int f();
};

A.cpp

#include "A.h"

int A::f() {
    ...
    return 0;
}

При изменении функции перекомпилируется только A.cpp, а не все используеющие его файлы. Также библиотеки, подключенные в .cpp, не будут подключаться во всех файлах, использующих этот класс.

Обычно в начале определяют методы, потом поля, сначала public, потом private.

Пример forward declaration для классов:

class B;

class A {
    B* b;
};

class B {
    A* a;
};

Но должен быть понятен размер переменной (у указателя фиксированный размер), так писать нельзя:

class B;

class A {
    B b;
};

Перегрузка операторов

Ограничения на перегрузку операторов: некоторые операторы нельзя перегружать (".", "::", тернарный оператор), нельзя определять новые операторы, нельзя менять приоритеты операций.

Пример:

class A {
public:
    ...
    ...
    int operator[](size_t index) const {
        if (index >= size) {
            return 0;
        }
        return data[index];
    }
private:
    int *data = nullptr;
    size_t size = 0;
};

Работает получение переменной, но ее нельзя менять. Новый вариант:

int& operator[](size_t index) {
    if (index >= size) {
        return 0;
    }
    return data[index];
}

Итераторы

Если мы хотим использовать range-based for, надо реализовать методы .begin(), .end() и сделать класс итераторов. У итераторов должны быть определены операторы: "*", префиксный "++", "!=".

class Array {
public:
    class ArrayIterator {
    public:
        ArrayIterator(Array& array, size_t index) : array(array), index(index) {}

        int& operator*() {
            return array_[index_];
        }

        ArrayIterator& operator++() { //++a
            ++index_;
            return *this;
        }

        ArrayIterator& operator++(int) { //a++
            return ArrayIterator(array_, index_++);
        }

        bool operator!= (const ArrayIterator& rhs) {
            return index_ != rhs.index_; // упрощенный вариант, надо еще сравнивать array_;
        }
    private:
        Array& array_;
        size_t index_;
    };
    
    ArrayIterator begin() {
        return ArrayIterator(*this, 0);
    }

    ArrayIterator end() {
        return ArrayIterator(*this, size);
    }
    ...
    ...
private:
    int *data = nullptr;
    size_t size = 0;
};  

Пока можем реализовывать только перегрузку операторов, где левым операндом является наш класс. Второй вариант перегрузки (в виде свободной функции):

class A {
public:
    ...
    friend A operator+(const A& lhs, size_t rhs);
private:
    int x;
}

A A::operator+(const A& lhs, size_t rhs) {
    return A(lhs.x + rhs);
}

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

Перегрузка оператора вывода (первый аргумент - это поток вывода):

class A {
public:
    ...
    friend std::ostream& operator<<(std::ostream& out, const A& rhs) {
        out << A.x;
        return out;
    }
private:
    int x;
}