Немного о классах в C++

Вы постоянно пользуетесь уже готовыми классами, такими как string, vector, set, map и другие. Пора научиться писать нечто похожее на примере класса для работы с длинной арифметикой. Но для начала немного синтаксиса:

1
2
3
class SimpleClass {
    // привет, я пример самого простого класса
};

В классах можно хранить любые переменные, например:

1
2
3
class Point {
    int x, y;
}

Но если вы попробуете обратиться к полям x и y, то у вас ничего не выйдет:

1
2
3
4
int main() {
    Point p = { 1, 2 };
    cout << p.x << ' ' << p.y; // Compile Error
}

Всё дело в том, что по умолчанию любая переменная (а далее, и любой метод) являются приватными. То есть пользователь класса не имеет к ним доступ, а разработчик класса - имеет. Это сделано для того, чтобы пользователи класса не стреляли себе в ногу при использовании чужого класса и не лезли в “скрытые” поля. Вы можете написать слово public, чтобы переменная была публичной, иначе говоря, вы имели к ней доступ из программы. Ниже вы можете увидеть, как работают ключевые слова public и private в C++:

1
2
3
4
5
6
7
8
9
class PublicPrivateClass {
    int this_is_a_private_variable;
    double this_is_private_variable_too;
public:
    int this_is_a_first_public_variable_in_this_class;
    int this_is_a_second_public_variable_in_this_class;
private:
    int this_is_a_private_variable_again;
};

Кроме того, в C++ есть ещё и структуры (struct), которые почти ничем не отличаются за исключением того, что объекты в нем по умолчанию являются public:

1
2
3
4
5
6
7
8
struct PublicPrivateClass {
    int this_is_a_public_variable;
    double this_is_public_variable_too;
private:
    int this_is_a_private_variable;
public:
    int this_is_a_public_variable_again;
};

У классов (и структур) можно создавать методы, например:

1
2
3
4
5
6
7
8
9
10
11
12
struct SimpleMethod {
    int x, y;

    double vector_size() {
        return sqrt(x * x + y * y);
    }
}

int main() {
    SimpleMethod t = { 1, 1 };
    cout << t.vector_size();
}

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

Длинная арифметика

Длинное сложение

Сложение двух длинных чисел - самая простая операция. Мы будем складывать “в столбик”. То есть абсолютно аналогично тому, как вы складываете в школе.

К сожалению, если писать этот подход “в лоб”, то получится очень неудобная архитектура, поэтому обсудим как должен выглядеть “правильный” класс длинной арифметики.

  1. Будем хранить число в обратном порядке. Это позволит увеличивать длину числа в конце, если будет перенос в разряд, которого ещё не было.
  2. Вместо поразрядного сложения будем складывать сразу большое количество разрядов (по 9). Это ускорит ваш код в 9 раз.

Например, если у вас есть число 1’124’899’823’759’823, то вы будете хранить его в виде последовательности: [823’759’823, 1’124’899]

1
2
3
4
5
6
const int RADIX = 1e9;
const int LOG = 100; // храним числа до 10^900
class BigInt {
    int size;
    int a[LOG];
}

В C++ есть возможность перегрузки операторов, таких как operator+ и другие. Это позволит работать с BigInt как с обычным числом. Перегрузим оператор сложения, а далее оставим комментарии:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class BigInt {
    int size;
    int a[LOG];
public:
    BigInt operator+(const BigInt& other) const {
        BigInt result;
        result.size = max(size, other.size);
        int carry = 0;
        for (int i = 0; i < result.size || carry; ++i) {
            int t = a[i] + other.a[i] + carry;
            result.a[i] = t % RADIX;
            carry = t / RADIX;

            if (i >= result.size)
                result.size = i + 1;
        }
        return result;
    }
}

int main() {
    BigInt a, b;
    cout << a + b; // that's working!
}
  1. Когда мы пишем a+b, то вызывается operator+ для переменной a с аргументом b (по сути аналогично методу, просто улученный синтаксис)
  2. const после метода означает, что я не хочу менять внутренние поля переменной, от которой вызывается оператор (в нашем случае, переменная a и словом const я гарантирую то, что не будет изменений в a.size и a.a)
  3. Обратите внимание, что я передаю other как копию, так как сама структура весит как int и указатель (8 или 16 байт в зависимости от архитектуры). Иногда структуры тяжелые и нужно передавать их по константной ссылке (const BigInt& other)

Кроме того, есть 2 проблемы:

  1. А как создавать BigInt? Или как его читать?
  2. Неудобно обращаться к i-той цифре числа

Operator[]

Быстро избавимся от второй проблемы:

1
2
3
4
5
6
7
8
9
10
11
class BigInt {
    // ...
    int operator[](int pos) const {
        return a[pos];
    }
    int& operator[](int pos) {
        return a[pos];
    }

    // обращение к i-й цифре теперь осуществляется через other[i]
}

Почему две версии? В чём разница?

Первая версия возвращает копию i-й цифры. Иногда это необходимо. Вторая версия позволяет поменять i-ю цифру числа, например: other[0] = 228;.

Конструктор

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BigInt {
    // ...
    BigInt() {
        size = 1;
        memset(a, 0, sizeof a); // #include <cstring>, зануляет весь массив
    }
    BigInt(int x) {
        size = 1;
        memset(a, 0, sizeof a);
        a[0] = x;
    }
}

int main() {
    BigInt a; // zero
    BigInt b = 1; // one
}

Либо можно воспользоваться синтаксисом значений переменных по умолчанию:

1
2
3
4
5
6
7
8
9
10
11
12
13
class BigInt {
    // ...
    BigInt(int x = 0) {
        size = 1;
        memset(a, 0, sizeof a);
        a[0] = x;
    }
}

int main() {
    BigInt a; // zero
    BigInt b = 1; // one
}

Ввод и вывод

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

1
2
3
4
5
#include <iomanip> // не забудьте!

int main() {
    cout << setfill('0') << setw(9) << 5 << '\n'; // 000000005
}

Для перегрузки вводы-вывода необходимо написать такой код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
istream& operator>>(istream& in, BigInt& a) {
    string s;
    in >> s;

    memset(a.a, 0, sizeof a.a);
    a.len = 0;

    for (int i = s.size() - 1; i >= 0; i -= 9) {
        a[a.len] = stoi(s.substr(max(0, i - 8), i - max(0, i - 8) + 1));
        a.len++;
    }
    
    return in;
}

ostream& operator<<(ostream& out, const BigInt& a) {
    out << a[a.len - 1];
    for (int i = a.len - 2; i >= 0; --i) {
        out << setfill('0') << setw(9) << a[i];
    }

    return out;
}

Операция деления на короткое

Операция умножения

В столбик

Алгоритм Карацубы

Метод Ньютона