Длинная арифметика и классы в C++
Оглавление
Запись занятия
Немного о классах в 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();
}
О более продвинутых возможностях класса мы поговорим на примере длинной арифметики.
Длинная арифметика
Длинное сложение
Сложение двух длинных чисел - самая простая операция. Мы будем складывать “в столбик”. То есть абсолютно аналогично тому, как вы складываете в школе.
К сожалению, если писать этот подход “в лоб”, то получится очень неудобная архитектура, поэтому обсудим как должен выглядеть “правильный” класс длинной арифметики.
- Будем хранить число в обратном порядке. Это позволит увеличивать длину числа в конце, если будет перенос в разряд, которого ещё не было.
- Вместо поразрядного сложения будем складывать сразу большое количество разрядов (по 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!
}
- Когда мы пишем a+b, то вызывается operator+ для переменной a с аргументом b (по сути аналогично методу, просто улученный синтаксис)
- const после метода означает, что я не хочу менять внутренние поля переменной, от которой вызывается оператор (в нашем случае, переменная a и словом const я гарантирую то, что не будет изменений в a.size и a.a)
- Обратите внимание, что я передаю other как копию, так как сама структура весит как int и указатель (8 или 16 байт в зависимости от архитектуры). Иногда структуры тяжелые и нужно передавать их по константной ссылке (const BigInt& other)
Кроме того, есть 2 проблемы:
- А как создавать BigInt? Или как его читать?
- Неудобно обращаться к 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;
}