Классы в C++
Оглавление
Классы в языке C++
Вы уже умеете пользоваться стандартными классами, такими как string
, vector
и другие. Пора научиться писать нечто похожее на примере класса для работы с арифметикой остатков. Но для начала немного синтаксиса:
1
2
3
class SimpleClass {
// привет, я пример самого простого класса
};
Публичные и приватные переменные
В классах можно хранить любые переменные, например:
1
2
3
4
class Point {
int x;
int 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 size() {
return sqrt(x * x + y * y);
}
}
int main() {
SimpleMethod t = { 1, 1 };
cout << t.size();
}
О более продвинутых возможностях класса мы поговорим на примере арифметики остатков.
Арифметика остатков
Часто в задачах по олимпиадному программировнию требуется посчитать ответ по модулю некоторого простого числа. Например, если вам нужно посчитать число Фибоначчи по модулю 1000000007
, то вам нужно считать ответ по модулю 1000000007
после каждой операции. Для этого идеально подходит класс для работы с арифметикой остатков.
Как сделать удобный класс для работы с арифметикой остатков? Он должен:
- Удобно создаваться
- Уметь складывать, вычитать, умножать и делить остатки
- Уметь выводиться в поток вывода
- Быть очень быстрым
То есть, в идеале мы должны уметь в следующий набор операций:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const int MOD = 11;
Mint a = 5;
Mint b = 7;
Mint c = a + b; // 5 + 7 = 1
Mint d = a * b; // 5 * 7 = 8
Mint e = a / b; // 5 / 7 = 8
c += 2; // 1 + 2 = 3
cout << c; // 3
Mint f = a + 7; // 5 + 7 = 1
Mint g = 7 + a; // 7 + 5 = 1
Мы будем писать наиболее эффективный код, поэтому будем по минимуму использовать операции взятия по модулю. Например, если мы складываем два числа и берем остаток, то взятие остатка можно заменить на одно вычитание в предположении, что числа меньше чем остаток!
Конструктор
Для начала нам нужно создать конструктор, который будет принимать число и модуль:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Mint {
public:
Modulo(int value_, int mod_) {
value = value_; // внутри класса можно обращаться к приватным переменным
mod = mod_;
assert(value >= 0 && value < mod); // убедимся, что числа изначально лежат в нужном диапазоне
}
private:
int value;
int mod;
};
int main() {
const int MOD = 11;
Mint a(5, MOD);
Mint b(7, MOD);
}
Начиная с C++11 появились списки инициализации. Они упрощают написание конструкторов, а так же делают их более эффективными с точки зрения производительности, а именно избавляют от лишних копирований: внутри конструктора переменные инициализилизруются сразу, а не сначала создаются, а потом присваиваются. Синтаксис следующий:
1
2
3
4
5
6
7
8
9
10
class Mint {
public:
Modulo(int value, int mod) : value(value), mod(mod) {
assert(value >= 0 && value < mod);
} // С++ сам поймет, где value - это поле класса, а где - аргумент функции
private:
int value;
const int mod;
}
Несколько замечаний:
- У списка инициализации есть небольшое ограничение: инициализировать можно только в порядке объявления переменных в классе (но некоторые поля можно просто пропустить)
- Обратите внимание, что в этот раз mod - константа! В прошлый раз мы не могли бы перезаписать константу в теле конструктора, а в списке инициализации можем. Ещё раз задумайтесь, почему!
Конструткоров может быть сколько угодно, они могут различаться по количеству аргументов и их типам. Например, вот так можно создать конструктор по умолчанию:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Mint {
public:
Modulo() : value(0), mod(1) {
}
Modulo(int mod) : value(0), mod(mod) {
assert(value >= 0 && value < mod);
}
Modulo(int value, int mod) : value(value), mod(mod) {
assert(value >= 0 && value < mod);
}
private:
int value;
const int mod;
}
Mint a; // value = 0, mod = 1
Mint b(11); // value = 0, mod = 11
Mint c(5, 11); // value = 5, mod = 11
Однако неудобно каждый раз передавать модуль в качестве аргумента. Есть несколько способов сделать это удобнее, например, воспользоваться аргументами по умолчанию:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const int MOD = 11;
class Mint {
public:
Modulo(int value = 0, int mod = MOD) : value(value), mod(MOD) {
assert(value >= 0 && value < mod);
}
private:
int value;
const int mod;
}
Mint a; // value = 0, mod = 11
Mint b(5); // value = 0, mod = 5
Mint c(5, 13); // value = 5, mod = 13
Важно знать, что аргументы по умолчанию должны быть справа от всех остальных аргументов (то есть суффиксом аргументов).
Другой способ - это использовать шаблоны. Этот способ мне нравится больше, поэтому мы будем использовать его.
Шаблонный класс
Шаблонные классы позволяют создавать классы, которые могут принимать в качестве аргументов другие классы. Например, вектор - это шаблонный класс динамического массива, который принимает в качестве аргумента тип элемента массива: vector<int>
или vector<string>
. Синтаксис шаблонного класса следующий:
1
2
3
4
5
6
7
8
9
10
template<class T>
class Vector {
T* data; // указатель на массив элементов
int size; // размер массива
};
int main() {
Vector<int> a;
Vector<string> b;
}
Как работают шаблонные классы? Всё магия произойдет ещё на этапе компиляции. Компилятор создает новый класс для каждого типа, который вы передаете в качестве аргумента. Например, если вы создаете Vector<int>
, то компилятор создает класс VectorInt
, если вы создаете Vector<string>
, то компилятор создает класс VectorString
.
На самом деле шаблоны позволяют принимать не только типы, но и значения. Например, это используется в array
из стандартной библиотеки:
1
2
array<int, 10> a;
array<int, 100> b;
Давайте создадим шаблонный класс, который будет принимать модуль в качестве шаблонного параметра:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<int mod>
class Modulo {
int value;
public:
Modulo(int value) : value(value) {
assert(value >= 0 && value < mod);
}
};
using Mint = Modulo<1000000007>;
int main() {
Mint a = 5;
Mint b = 7;
}
Операторы
Чтобы зарабаотала арифметика остатков, в C++ нужно перегрузить операторы. Например, чтобы заработал оператор сложения, нужно перегрузить operator+
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<int mod>
class Modulo {
int value;
public:
Modulo operator+(const Modulo& other) const {
int result = value + other.value;
if (result > mod) {
result -= mod;
}
return result;
}
};
using Mint = Modulo<1000000007>;
int main() {
Mint a = 5;
Mint b = 7;
Mint c = a + b;
}
Когда мы пишем a+b
, то вызывается operator+
для a
с аргументом b
. Операторы можно перегружать для любых типов, но есть некоторые ограничения. Например, операторы .
и ::
нельзя перегружать, а так же нельзя создавать новые операторы.
Операторы можно перегружать как внутри класса, так и снаружи. Например, если вы хотите, чтобы оператор +
работал для int
и Modulo
, то вы можете создать новый класс, который будет принимать int
и перегрузить оператор +
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template<int mod>
class Modulo {
int value;
public:
Modulo operator+(const Modulo& other) const {
int result = value + other.value;
if (result > mod) {
result -= mod;
}
return result;
}
};
template<int mod>
Modulo<mod> operator+(int a, const Modulo<mod>& b) {
return Modulo<mod>(a) + b;
}
using Mint = Modulo<1000000007>;
int main() {
Mint a = 5;
Mint b = 7;
Mint c = a + b;
Mint d = 5 + b;
}
Обратите внимание, что без такой перегрузки мы бы не могли сложить int
(слева) и Modulo
(справа), так как компилятор не знает, как это делать. А вот в обратном порядке смогли бы, потому что у нас есть:
- Конструктор
Modulo(int)
- Сложение
Modulo + Modulo
То есть копмпилятор бы догадался, что надо создать Modulo
из int
и сложить два Modulo
.
Операция добавления имеет чуть-чуть другой синтаксис, однако может быть написана еще более просто:
1
2
3
4
5
6
7
8
9
template<int mod>
class Modulo {
int value;
public:
Modulo& operator+=(const Modulo& other) const {
return *this = *this + other;
}
};
this
- это указатель на само значение переменной, над которой вызывается оператор. Так мы можем переиспользовать уже готовый operator+
, чтобы не дублировать код.
Операторы ввода-вывода
Чтобы заработал вывод в поток, нужно перегрузить оператор <<
:
1
2
3
ostream& operator<<(ostream& out, const Modulo& a) {
return out << a.value;
}
Прежде чем читать дальше, рекомендую подумать самостоятельно, что значит данная запись и зачем возвращать поток вывода.
ostream
- это общий базовый класс для любого потока вывода. Оператор вывода обязан возвращать измененный поток, чтобы можно было писать цепочки ввода:
1
cout << x << ' ' << y;
Однако это не сработает, так как a.value
- это приватный член класса. Чтобы это заработало, нужно сделать operator<<
другом класса. Функции-друзья имеют доступ к приватным полям:
1
2
3
4
5
6
7
template<int mod>
class Modulo {
int value;
public:
friend ostream& operator<<(ostream& out, const Modulo& a);
}
Оператор ввода аналогичен, только работает с переменной типа istream
- входного потока, а переменная типа Module передается по неконстантной ссылке (ведь в операторе ввода мы считаем данную переменную и изменим её).
Соединяем всё вместе
Выше я везде передавал объекты типа Module по константной ссылке. На самом деле я делал это по привычке. Однако напомню, что если объект класса меньше ~16 байт, то эффективнее передавать его по значению (с копированием). Поэтому в финальной версии мы будем передавать объекты по значению.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
template<int mod>
class Modulo {
public:
Modulo(int value = 0) : value(value) {
if (value < 0) {
value += mod;
}
}
Modulo operator+(Modulo other) const {
int result = value + other.value;
if (result >= mod) {
result -= mod;
}
return result;
}
Modulo operator-(Modulo other) const {
int result = value - other.value;
if (result < 0) {
result += mod;
}
return result;
}
Modulo operator*(Modulo other) const {
return (int64_t(value) * other.value) % mod;
}
Modulo operator/(Modulo other) const {
return *this * other.inv();
}
Modulo& operator+=(Modulo other) {
return *this = *this + other;
}
Modulo& operator-=(Modulo other) {
return *this = *this - other;
}
Modulo& operator*=(Modulo other) {
return *this = *this * other;
}
Modulo& operator/=(Modulo other) {
return *this = *this / other;
}
Modulo inv() const {
return binpow(*this, mod - 2);
}
bool operator==(Modulo other) const {
return value == other.value;
}
friend ostream& operator<<(ostream& out, const Modulo& a) {
return out << a.value;
}
friend istream& operator>>(istream& in, Modulo& a) {
return in >> a.value;
}
friend Modulo operator+(int a, Modulo b) {
return Modulo(a) + b;
}
friend Modulo operator-(int a, Modulo b) {
return Modulo(a) - b;
}
friend Modulo operator*(int a, Modulo b) {
return Modulo(a) * b;
}
friend Modulo operator/(int a, Modulo b) {
return Modulo(a) / b;
}
private:
int value;
static Modulo binpow(Modulo a, int n) {
Modulo result = 1;
while (n) {
if (n & 1) {
result *= a;
}
a *= a;
n >>= 1;
}
return result;
}
};