Compare commits

...

14 Commits

16 changed files with 2205 additions and 16 deletions

View File

@ -2,18 +2,32 @@ DISTDIR ?= $(shell pwd)
TEMPLATE_DIR ?= $(DISTDIR)/templates
SCRIPT_DIR ?= $(DISTDIR)/scripts
BUILDDIR ?= $(DISTDIR)/build
DESTDIR ?= /var/www/wikipages
export
PAGES := $(wildcard pages/*)
PAGE_TARGETS := $(patsubst pages/%,$(BUILDDIR)/page/%,$(wildcard pages/*))
all: $(PAGES) static
all: $(PAGE_TARGETS) $(BUILDDIR)/index.html $(BUILDDIR)/static
$(PAGES):
$(MAKE) -C $@
$(BUILDDIR)/page/%:
@$(MAKE) -C pages/$*
static:
$(BUILDDIR)/static:
@mkdir -p $(BUILDDIR)
cp -r static $(BUILDDIR)
.PHONY: all $(PAGES) static
$(BUILDDIR)/index.html:
cp index.html $(BUILDDIR)
clean:
rm -rfI $(BUILDDIR)/
install:
@mkdir -p $(DESTDIR)
cp -r -T $(BUILDDIR)/ $(DESTDIR)/
uninstall:
rm -rfI $(DESTDIR)/
.PHONY: all clean install uninstall

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<title> thematdev wiki </title>
</head>
<body>
Конспекты:
<br>
<a href="/page/mipt_cxx1">Конспекты лекций ФПМИ МФТИ по C++ (продвинутый поток, осенний семестр 2023)</a>
<br>
Статьи:
<br>
<a href="/page/linear_nt">Линейные алгоритмы в теории чисел</a>
<br>
<a href="/page/quadratic_forms">Квадратичные вычеты и корни по простому модулю</a>
<br>
<a href="/page/inplace_merge">Inplace merge</a>
</body>
</html>

View File

@ -0,0 +1,3 @@
# Default makefile for algorithmica-like pages
include $(SCRIPT_DIR)/mk/algorithmica.mk

View File

@ -0,0 +1,97 @@
# Inplace merge
В данной заметке мы поговорим о задаче слияния двух отсортированных массивов
с использованием $O(1)$ дополнительной памяти. Данная задача естественно возникает
при попытке сделать сортировку слиянием так, чтобы она не делала никаких аллокаций.
Считается, что читатель знаком с сортировкой слиянием.
**Теорема.** Существует алгоритм, который сливает два подряд идущих отсортированных массива $A, B$,
используя $O(|A| + |B|)$ времени без дополнительной памяти.
Более того, вся дополнительная память будет использована под _счетчики_, числа
размера $O(\log n)$, которыми мы индексируем массив. Далее мы будем пренебрегать
константным количеством оных, то есть фраза _без дополнительной памяти_ значит,
что мы имеем право завести константное число _счетчиков_. Тем самым _дополнительная память_
измеряется элементами массивов.
Мы также считаем, что своп двух элементов также делается _без дополнительной памяти_, в крайнем
случае с помощью $1$, чем мы тоже пренебрегаем.
## Предварительные сведения
**Утверждение.** Циклический сдвиг массива $A$ на любую величину можно сделать за $O(|A|)$ времени и без дополнительной
памяти.
Читателю предлагается попробовать сделать это разными способами, в качестве какого-то из них
предлагается $\text{reverse}(A[0, n)) \circ \text{reverse}(A[k, n)) \circ \text{reverse}(A[0..k))$.
**Лемма 1.** Существует алгоритм, который сливает два подряд идущих массива $A, B$,
используя $O(|A| + |B|)$ времени и $\min(|A|, |B|)$ дополнительной памяти.
**Доказательство.** Исходя из прошлого утверждения мы можем считать, что $A$ -- меньший из массивов
лежит в начале общего буфера. Пусть `a, b` -- указатели на начало $A, B$ соответственно, `c` -- указатель
на начало общего буфера(то есть исходно `c = a`). Скопируем `a` в отдельный буфер `a'`.
Далее сольем `a', b` в `c` как будто бы они не пересекались вовсе.
Заметим, что в процессе работы алгоритма `b - c >= 0`. Действительно, если мы берем очередной элемент из `b`,
то эта разница не меняется, а если из `a`, то уменьшается на $1$, и изначально `b - c = |A|`. Откуда
алгоритм действительно отработает как будто бы буферы не пересекались.
## Основной алгоритм
**Утверждение**. Существует алгоритм, который выделяет $k$ максимальных элементов
из двух подряд идущих отсортированных массивов $A, B$ и передвигает их в конец не меняя относительный порядок остальных,
используя $O(|A| + |B|)$ времени без дополнительной памяти.
Это несложно сделать с помощью циклического сдвига.
**Лемма 2.** Существует алгоритм, который сливает два подряд идущих отсортированных массива $A, B$,
используя $O(|A| + |B| + \min(|A|, |B|)^2)$ времени без дополнительной памяти.
**Доказательство**. Обозначим $k = \min(|A|, |B|)^2$. Выделим $k$ максимумов и переместим их в конец. Пусть
`c = a, b` -- начало $A^\prime, B^\prime$(с вытащенными максимальными элементами) соответственно, `buf` -- указатель на начало максимумов
соответственно. Попробуем слить `a, b` в `c` с помощью буфера `buf` как в Лемме 1. Только теперь нам нельзя присваивать во время слияния,
так как иначе мы потеряем информацию, поэтому будем свапать. Из корректности алгоритма в Лемме 1, мы получаем, что
в начале исходного буфера будет записан результат слияния $A^\prime, B^\prime$. Но так как мы нигде не теряли информацию, в конце также
должны остаться все максимальные элементы, возможно, в каком-то другом порядке. Отсортируем их любой квадратичной сортировкой и получим
требуемое.
Далее обозначим $n = |A| + |B|$ и будем решать исходную задачу.
**Следствие.** Мы научились решать исходную задачу если $\min(|A|, |B|) = O\left( \sqrt{n} \right)$.
Выберем $\beta = \Theta(\sqrt{n})$. Выделим $\beta$ максимальных элементов и переместим их в конец. Это пространство мы будем
использовать как буфер. Далее разобъём $A, B$ на блоки размера $\beta$. В силу Леммы 2 можно считать, что блоки полные.
**Шаг 1.** Итого у нас получилось порядка $\frac{n}{\beta}$ блоков размера $\beta$. Отсортируем их **выбором** по минимальному элементу(при
равенстве сравним еще и по максимальному). Сортировка выбором делает порядка $\left( \frac{n}{\beta} \right)^2$ сравнений и $\frac{n}{\beta}$ свопов.
Стоимость одного сравнения $O(1)$, а свопа $O(\beta)$, поэтому суммарно этот шаг отработает за $O\left( \frac{n^2}{\beta^2} +
\frac{n}{\beta} \beta \right) = O(n)$ в силу выбора $\beta$.
**Лемма 3.** Если отсортировать первые $(t + 1)\beta$ элементов, то на первых $t\beta$ местах будут стоять $t\beta$ минимумов всего
массива.
**Следствие.** Если мы отсортировали первые $(t + 1)\beta$ элементов, то чтобы после этого отсортировать первые $(t + 2)\beta$ достаточно
слить подотрезки $[t\beta, (t + 1)\beta), [(t + 1)\beta, (t + 2)\beta)$.
**Доказательство.** Выделим $t\beta$ минимумов. Все блоки делятся на два типа: из $A$ и из $B$. Рассмотрим последний блок из $A$, в
котором что-то выделено. Утверждается, что в блоках из $A$, которые идут после него не выделено ничего(проверьте!), а если такого нет, то нигде не выделены элементы из $A$. Также выделенные блоки из $B$ после него сразу идут подряд и все, кроме, может быть последнего выделены полностью. Аналогично для блоков из
$B$. Здесь мы как раз пользуемся отсортированностью по минимуму.
Тем самым выделенные элементы должны попасть в первые $(t + 1)$ блоков. Действительно, рассмотрим последний из 'крайних' блоков. Из
структурного утверждения, в каждом блоке до него также что-то выделено.
Пусть до него включительно есть $k$ блоков. Так как частичных блоков не более двух, то хотя бы $k - 2$ заполнены полностью, то есть
выделено хотя бы $(k - 2)\beta + 1$ элементов. Откуда $k \leqslant t + 1$, что и требовалось.
**Шаг 2.** Согласно следствию нам достаточно итеративно сливать соседние блоки. Каждое такое слияние можно выполнить без дополнительной
памяти с помощью буфера из максимумов, которым мы запаслись в начале. Ясно, что суммарно такие слияния работают за $O(n)$.
**Шаг 3.** Квадратично отсортируем буфер из максимумов, и, если требуется, дольем неполные блоки с помощью Леммы 2. Ясно, что эта
процедура работает за $O(n)$ и не требует дополнительной памяти.
Тем самым мы научились решать исходную задачу.
## Полезные ссылки и источники
* [Оригинальная статья Кронрода](https://www.mathnet.ru/php/archive.phtml?wshow=paper&jrnid=dan&paperid=34705&option_lang=rus)
* [Лекция Ф.Д.Руховича по алгоритмам и структурам данных](https://www.youtube.com/watch?v=V-kWungDekU&t=4536s)

View File

@ -0,0 +1,5 @@
---
credits:
- Никифор Кузнецов
title: Inplace merge
---

View File

@ -0,0 +1,126 @@
# $I$. Introduction to C++
## § 1.2 Types and their supported operations.
static typization $:=$ тип переменной определяется на этапе компиляции
Integral types:
```C++
int x; //4
long x; //4
long long x; //8
short x; //2
char x; //? 1
bool x; //?? 1
int8_t x;
int16_t x;
int32_t x;
int64_t x; //стандартизованные типы про которые гарантируется их ширина
int128_t x; //есть аналоги с u
std::byte x; //аналогично char
size_t x; //==ull
```
Floating point types:
```C++
float x; //4b
double x; //8b
long double x; //16b
```
Хранятся они в формате sign (1 bit) mantissa exponent, то есть число хранится как mantissa * 2^exponent * sign
Все, что выше - это фундаментальные типы. Рассмотрим что-то более комплексное
```C++
std::string s = "abc";
s[1] == 'b';
s[3] == (char)0; //string - это null termindated string - после данных стандарт гарантирует присутствие символа 0
s[4]; //ub
.at()
+ +=
.push_back()
.pop_back()
.front()
.back()
.size()
std::vector<int> v;
//push_back работает амортизированно за O(1), но гарантируется за O(n)
//аналогично string без + и +=
.shrink_to_fit()
.clear()
//просто делает из старых элементов ub, но память никак не освобождает
.reserve(n)
//предывделить память до n, но не кладет туда элементы (обращение туда все еще ub)
.resize(n, x)
//дропунть лишнее, если есть, если нет, то заполнить все остальное нулями
.capacity()
//size под выделенную память
std::list<int> l; //useless
std::forward_list<int> fl; //useless
std::deque<int> d; //страшная вещь внутри, которую нужно будет реализовать
std::stack<int> s; //deque внутри
std::queue<int> q; //deque внутри
std::priority_queue<int> pq; //deque с make_heap-ом внутри
std::map<int, int> m; //rbt
m.at(key); //возвращает exception, если нет ключа
m.find(key); //возвращает итератор или m.end, если не находит
m.insert(pair<key, val>);
erase(map::iter);
std::multimap<key, val> mm; //это multiset, который map.........
std::unordered_map<key, val> um; //хэш-таблица
//работает В СРЕДНЕМ (не амортизированно) за O(1)
//то есть гарантируется, что работает за O(1) на рандомном наборе данных, а амортизированно - это значит, что работает быстро гарантированно
std::istream is; //поток ввода
std::ostream os; //поток вывода
```
"Эффективность дороже безопасности". Любая проверка на ub отсутствует, потому что это дорого).
Литералы
```C++
5u;
1'000'000'057;
0.2f;
"abc";
'a';
//integer promotions - это когда ты складываешь int и long long, то получится long long и все будет корректно
//Есть аналогичное floating point promotion
//int + uint = uint
0xff == 255; //16-иричный
0123 == 83; //8-иричный
0b101 == 5; //2-ичный
```
## §1.1 Знакомство с терминалом
```
pwd - print working directory
cd - вернуться в предыдущее пространство
```
## §1.3 Declarations, definitions and scopes.
На глобальном уровне (вне какой-либо другой сущности) все действия - это объявления чего либо (класса, функции, переменной etc)
```C++
#include <iostream>
int a;
void f(int x);
class C;
struct S;
enum E;
union U;
namespace N {
int x;
}
namespace N {
int y;
}
using ll = long long;
int main() {
;
}
```

372
pages/mipt_cxx1/02.md Normal file
View File

@ -0,0 +1,372 @@
В прошлый раз мы остановились на пункте
Declarations, definitions and scopes
Объявления
```C++
int a;
void f(int x);
class C;
struct S;
enum E;
union U;
namespace N {
int x;
}
namespace N {
int y;
}
```
Чтобы сделать элемент видимым из другого namespace,
можно использовать using
```C++
int main() {
// std::cout << x << std::endl // CE
std::cout << N::x << std::endl // OK
using N::x; // теперь объявление x есть в нашей области видимости(scope)
std::cout << x << std::endl; // OK
}
```
Также using можно также добавить в глобальную область видимости и в другой namespace
Можно использовать сразу все пространство имен через `using namespace N`.
Один из примеров использования: `using namespace std`, но это **крайне не рекомендуется**,
по крайней мере глобально.
Причина тому то, что `std` крайне огромен, соответственно в нем очень много имен,
особенно опасно привнести функцию с одинаковым именем и другими типами,
например, `std::distance`.
Еще один вид конструкции `using` это алиасинг типов, например, некоторые из смертных грехов
```C++
using vi = std::vector<int>;
using ll = long long;
```
Стоит понимать, что `using` не создает новый тип. `using` также можно использовать с шаблонами
```C++
template <typename T>
using v = std::vector<T>;
```
Ключевые слова `typedef` и `using` семантически делают одно и то же, но `typedef` это C-style,
а значит, например, не умеет в шаблоны.
Напомним, что _ключевыми словами_ называются слова, смысл которых закреплен компилятором,
и как следствие, их нельзя использовать, например, в качестве имён.
Каждый блок(из фигурных скобок) создает новую область видимости, то есть
они образуют дерево по вложенности фигурных скобок(прим. лектора:
концептуально это так, но как всегда в C++, наверняка можно найти
несколько крайних случаев)
По умолчанию определения берется из минимальной по включению области видимости,
но всегда можно обратиться к глобальной через `::`, например
```C++
#include <iostream>
int x = 0;
int main() {
int x = 1;
for (int i = 0; i < 10; ++i) {
int x = i;
// int x = 5; // CE, redeclaration in same scope
std::cout << x << std::endl; // i
std::cout << ::x << std::endl; // 0, if global doesn't exist will throw CE
}
}
```
Обратим внимание, что всегда можно создать блок и затемнить объекты
```C++
int x = 5;
{
int x = 6;
// now x is 6
}
```
Стоит обратить внимание на следующую "подлянку":
```C++
namespace N {
int x = 7;
}
using namespace N;
// WARNING: uncommenting this line will result to 0 as output
// By standard this is not redeclaration but using N::x is
// int x = 0;
int main() {
int x = 1;
{
int x = 5;
std::cout << x << std::endl; // will print 7, as global scope includes x
}
}
```
Если `x` объявлен в двух различных пространствах `N, M`, подключенных
через `using namespace N; using namespace M;`, то при попытке использования `x`
компилятор выкинет ошибку `ambigious reference to variable x`.
В первом приближении есть условно два типа, которые разрешаются в следующем приоритете:
1. Имена объявленные "явно", через `T x;` или `using N::x`.
2. Имена подключенные из другого пространства имен через `using namespace N;`
Также можно получить ошибку `different kind of entity`, если определить
символ как тип и как переменную одновременно, например
```C++
using N::x; // int
using x = std::vector<int>; // Type
```
Если начать заниматься еще большей херней, например `int x = x;`, то из-за
такой концепции как point of declaration, данный код эквивалентен `int x;`,
соответственно в `x` будет лежать мусор, а совсем не то, что будет в высшей
области видимости или пространстве имен.
**One definition rule(ODR)**. Каждая сущность в программе должна быть определена ровно один раз.
Это верно в C, но не совсем верно для классов. Класс можно определить несколько раз, если
все определения абсолютно идентично.
_Объявлений_ функций может быть сколько угодно, но _определение_ ровно одно.
Любое определение является объявлением.
C++ разрешает более одной функции с одним и тем же именем.
```C++
void f();
int f(int x) {
return x + 1;
}
int f(double x) {
return x + 2;
}
```
`f(0.5) = 2`, так как вызовется от `double`, `f(0) = 1`, так как вызовется от `int`.
`f(0.0f) = 2`, ведь `float -> double` -- promotion, а `float -> int` -- conversion.
**promotion лучше, чем стандартный conversion, лучше чем тот, что определен пользователем**.
Если бы определили `int f(float x);`, а вызывали бы `f(0.0) // no suffix defines double`,
то компилятор бы выкинул ошибку, так как он не может выбрать между двумя conversion.
Следующий код также вызовет ошибку компиляции
```
void f();
int f();
```
Это переобъявление(redeclaration) с одинаковым типом принимаемых значений, но разным возвращаемых.
Необходимо различать (не)квалифицированные идентификаторы.
```
x; // unqualified-id
N::x; // qualified-id
```
По стандарту `int x;` также считается определением, только если не стоит
ключевое слово `extern`. Выражение `extern int x;` является объявлением,
но не считается определением, линковщик будет искать этот символ.
`static` делает переменную видимой только в данном translation unit,
но также является определением(которое затемняет другие).
# 1.4 Expressions and operators
В локальных областях видимости появляются такие сущности, как
_выражения_(expressions) и _инструкции_(control flow statements), про
последние мы поговорим в следующем параграфе.
Формальное определение выражения занимает одну-две страницы.
Неформально это просто набор некоторых переменных и литералов,
соединенных между собой операторами и скобками.
**Пример.** `x + 5` или `cout << x` являются выражениями.
Все мы знаем (бинарные) операторы `+, -, *, /, %, <<, >>, |, &, ^, &&, ||, <, >, !=, ==, <=, >=, <=>(since C++-20)`
Побитовые операторы(`&, |, ^`) вычисляют выражение побитово.
Логические операторы(`&&, ||`) принимают `bool` и возвращают `bool`, **и притом вычисляются лениво**.
Стандарт гарантирует, что если `a` ложно, то в выражении `a && b` выражение `b` вычислено не будет.
Аналогично, если `a` истинно, то в выражении `a || b` выражение `b` вычислено не будет.
**Пример.** Выражение `v.size() >= 5 && v[4] == 1` безопасно и не вызывает UB из-за ленивости `&&`.
Рассмотрим оператор присваивания `=` и операторы составного присваивания `+=, -=, *=, /=, %=, <<=, >>=, |=, ^=, &=`.
`x = y` возвращает `y`. `(x = y) = z` возвращает `z`, все они lvalue.
На интуитивном уровне, lvalue можно что-то присвоить, а rvalue нет, то есть `x + y` это rvalue, а `x = y` это lvalue.
Первая группа операторов возвращает lvalue, а вторая rvalue.
Операторы присваивания правоассоциативны, то есть `x = y = z` эквивалентно `x = (y = z)`.
Вроде как все операторы из первой группы левоассоциативны, то есть `x + y + z` эквивалентно `(x + y) + z`.
Для `float` уже есть разница в каком порядке их складывать из-за точности(лучше в отсортированном).
Лево(право)ассоциативность сохраняется в независимости от того, как оператор был перегружен.
До C++-17 операторы `&&, ||` при перегрузке теряет ленивость и гарантию на порядок вычисления.
Начиная с C++-17 гарантируется, что при вычислении `a && b` выражение `b` будет
вычислено после выражения `a`.
Ещё один класс операторов -- это декременты и инкременты, `a++, ++a, --a, --a`.
Разница между `a++, ++a` заключается в том, что первый(постфиксный) создает новое
значение и кладет туда результат, тем самым возвращает rvalue. Второй(префиксный) увеличивает
переменную "на месте" и возвращает lvalue. Аналогично `a--, --a`.
Инкременты и декременты применимы только к lvalue, то есть выражение `--(4 + 5)` некорректно,
а `++(x=5)` корректно.
По стандарту постфиксные операции приоритетнее, и `++a++` эквивалентно `++(a++)`, а следовательно
некорректно.
**Важный пример**. Чему эквивалентно выражение `a+++++b`?
**Лексический парсер(лексер) всегда пытается взять наибольший "осмысленный" токен**,
поэтому он разобъет его следующим образом: `((a++)++)+b`, но выражение `(a++)` является rvalue,
а инкремент можно применить только к lvalue.
**Тернарный оператор**. `a ? b : c`. Пытается преобразовать `a` к `bool`, если истинно, то вычисляет `b` и
возвращает его, иначе вычисляет `c` и возвращает его. Гарантируется, что будет вычисляться ровно один из них.
Вид возвращаемого value зависит от вида value `b` и `c`. Но вид value, как и тип, должен быть
известен на этапе компиляции, поэтому если хоть одно из них rvalue, то вид всего выражения
это rvalue, иначе lvalue.
**Пример**. `(false ? a++ : ++a) = 1`, даже несмотря на то, что оно всегда равно `++a`, которое lvalue,
из рассуждения выше левая часть все равно будет rvalue, откуда присваивание некорректно.
Если для `b` и `c` не найдется общий тип, но будет ошибка компиляции, иначе тип выражения равен общему типу.
**Пример**. `f(b ? 1 : 0.5)` всегда вызовет `f(double)`.
Оператор `,` делает следующее: выражение `a, b` вычисляет сначала `a`, потом `b`,
и возвращает `b`. Тип value такой же как у `b`.
**Не всякая запятая как символ является оператором**. Запятая в `void f(int, int)` очевидно не
оператор, ведь это объявление, а не выражение, равно как и `<` в `#include <header.h>`, которая
вообще является командой препроцессора.
**Важное замечание**. `T x = y` это не оператор присваивания и не выражения, это синтаксис объявления.
**Важный пример**. При вызове функции `f(x, y, z)` **нет никакой гарантии на порядок вычисления аргументов**,
`x, y, z` это не выражение, а перечисление аргументов. А если написать `f((x, y, z))`, то это будет
эквивалентно вызову функции `f` от одного аргумента `z`, выражения `x`, `y`, `z` будут вычислены
именно в таком порядке.
Оператор `sizeof`. Возвращает количество байт, которое занимает переменная/типа. Так как тип
переменной `x` известен во время компиляции, то `sizeof(x)` также известен во время компиляции.
`sizeof` от VLA(variable length array) является неопределенным поведением. VLA есть в
стандарте C99, но нет в стандарте C++, тем не менее, компиляторы его поддерживают.
**Замечание**. `sizeof(v)` это не `v.size()`. `sizeof(arr)`, где `arr` --
статически выделенный массив, это сумма размеров его элементов,
но от указателя он равен размеру самого указателя непосредственно.
[Приоритеты операторов](https://en.cppreference.com/w/cpp/language/operator_precedence)
[Порядок выполнения](https://en.cppreference.com/w/cpp/language/eval_order)
Ниже приведены некоторые примеры:
Нельзя сказать, чему равно значение `x++ * y++ + ++x`. Несмотря на то, что
умножение "выполняется" раньше, чем сложение, порядок вычисления операндов
не гарантируется.
```C++
int f() { std::cout << 1; return 1;}
int g() { std::cout << 2; return 2;}
int h() { std::cout << 3; return 3;}
```
В таком случае значение `f() * g() + h()` имеет однозначно определенное значения,
но вот результат его вычисления не определен.
В выражении `a*b + c*d + e*f` нет никаких гарантий, что сначала вычислится результат всех умножений,
а только потом они будут складываться.
Есть шедевральная глава "Sequenced before" rules, которое задает некоторые правила
порядка вычислений внутри одного потока, более точно, транзитивное антисимметричное бинарное отношение.
Порядок выражений стал более строгим начиная с C++-17, тем самым некоторые выражения, вызывающие
неопределнное поведение до C++-17, хорошо определены после.
Control statements
if:
```C++
if (/* bool-expression */) {
} else if (...) {
} else {
}
```
switch:
```C++
switch (/* bool-expression */) {
case 1:
std::cout << "AAAA";
/* FALLTHROUGH */
case 2:
std::cout << "BBBB";
break;
case 3:
std::cout << "CCCC";
default:
std::cout << "DEFAULT";
}
```
**Замечание**. Cases in switch **fall through**, например при `expression == 1` он выведет `AAAABBBB`, на `2` выведет `BBBB`, на
`3` выведет `CCCCDEFAULT`, иначе `DEFAULT`.
loops:
```C++
// First check, then do
while (/* bool-expression */) {
}
// First do, then check
do {
} while (/* bool-expression */);
// first follows declaration, then check expression, and if true
// evaluates expression and inner block and repeats it
for (declaration | expression; bool-expression; expression) {
}
// empty bool-expression is always true
```
Начиная с C++-17 можно делать объявление в ife: `if (int x = 0; y > x)`.
Метки:
```
label:
// do some stuff
if (bool-expression) {
goto label;
}
```
Нельзя прыгать с пропуском инициализации.

228
pages/mipt_cxx1/03.md Normal file
View File

@ -0,0 +1,228 @@
# 1.6. Compile-time errors, runtime errors and undefined behaviour
Мы уже встречались с ошибками компиляции.
Ошибки можно условно разделить на лексические, синтаксические и
семантические.
Мы уже затрагивали лексический парсер, он разбивает программу на
токены, например `std::cin >> x;` разбивается на 6 токенов
`std :: cin >> x ;`.
Далее происходит синтаксический разбор, и компилятор
начинает заниматься семантикой.
**Пример**.
* `\\;` будет лексической ошибкой, потому что лексер не справится разобрать символ `\` с `error: stray '\' in program`
* `6abcde;` будет семантической ошибкой, потому что компилятор подумает, что `abcde` -- литеральный суффикс, а потом его не найдет(начиная
с C++-11 литеральные суффиксы можно определять свои)
* `std::cout << x + ;` будет синтаксической ошибкой, `expected primary-expression before ';' token`, так как нет выражения после плюса.
* `"abc" + 5.0f;` будет семантической ошибкой, `invalid operands of types ...`.
_Ошибка времени выполнения_ или runtime error это когда
программа успешно скомпилирована, но непредвиденно завершается во время
выполнения(падает).
Одна из самых частых -- segmentation fault.
```C++
#include <iostream>
#include <vector>
int main() {
std::vector<int> v(10);
v[50'000] = 1;
}
```
Возникает из-за обращения к памяти, которую мы не имеем права читать.
Floating point exception (FPE), например, деление на ноль, идёт от процессора.
Aborted, вызов функции `abort()` из libc, её вызов приводит к аварийному завершению.
**Неопреденное поведение** или undefined behaviour -- некорректный код, который
приводит к "непредсказуемым" последствиям во время выполнения, в том смысле,
что компилятор не может дать никаких гарантий.
```C++
std::vector<int> v(10);
v[10] = 1; // UB
// Может упасть, а может и повредить очень важную область памяти
// у другой переменной
int x;
std::cout << x; // UB
x++ + ++x; // UB, как говорилось раньше, нет гарантий на порядок
```
Signed integer overflow это также UB. В том смысле, что **компилятор вправе
считать, что в коде нет переполнений знаковых чисел и делать оптимизации
исходя из этого**.
```C++
for (int i = 0; i < 300; ++i) {
std::cout << i << ' ' << i * 12345678 << std::endl;
}
```
при компиляции с `g++ -O2` получается вечный цикл. Но почему? Ведь при проверке не возникает переполнение
Компилятор считает, что выражение `i * 12345678` не переполняется, а значит `i < 174`, а значит **проверку можно убрать**.
Также компилятор может убрать `assert(a + 100 > a)`, так как он вправе считать, что `a + 100 > a` всегда.
Бесконечный цикл без побочных эффектов тоже является UB, компилятор имеет право сделать с ним что угодно,
см [C Compilers Disprove Fermats Last Theorem](https://blog.regehr.org/archives/140).
Помимо неопределенного поведения есть ещё **unspecified behaviour**. Это значит, что
стандарт не говорит, что конкретно должно быть, например, порядок вычислений.
То есть `f(a(), b(), c())` это не undefined behaviour, но unspecified, потому
что неясен порядок вычислений.
**Implementation-defined behaviour** это то, что зависит от реализации и окружения, например,
от компилятора, локали, архитектуры, etc.
За счёт undefined behaviour, компилятор может делать агрессивные оптимизации,
что ускоряет "корректный" код, зато с "некорректным" компилятор может
сделать что угодно.
**Лирическое отступление**.
**Warning** -- замечание компилятора относительно вашего кода, не обязательно нарушение стандарта.
**Примеры**:
```C++
if (x = 0) {
// do something
}
```
Это формально корректный код, и даже используемая идиома, но `clang` кидает предупреждение, и
просит обернуть в скобки, если это сделано специально.
```C
pid_t pid;
if ((pid = fork()) == -1) {
// error while forking, should handle
}
```
Ещё один пример, это unused value. Компилятор предупреждает, не забыли ли вы случайно использовать
значение, например `f();` если `f` возвращает не `void`.
**Некоторые флаги**
Флаг `-Wall` позволяет предупреждать "обо всём", `-Wextra` о том, о чём не предупреждает `-Wall`.
Флаг `-pedantic` говорит компилятору строго чтить стандарт и не использовать свои расширения, по типу
VLA в C++.
Флаг `-Werror` превращает все предупреждения в ошибки.
Подробнее см. `man gcc`, `info gcc`(боже упаси) или [онлайн-документацию](https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html).
# II. Compound types
## 2.1. Pointers
Пусть есть какая-то переменная `T x`. У нее есть какой-то адрес, `&x`. Это не буквально её адрес на плашке
оперативной памяти, но по этому числу программа может обращаться к памяти. Для этого не нужно
вдаваться в подробности того, как работает операционная система.
```C++
int main() {
int x;
std::cout << &x << std::endl; // may be different at different runs
```
А какой тип у `&x`? Это `T*`. Ясно, что должна существовать обратная операция к взятию адреса, _разыменование_.
`*p` по указателю `T*` возвращает то, что лежит под ним.
```C++
int x = 0;
int* p = &x;
*p = 2;
```
**Важно**. Несмотря на то, что `*` это часть типа, при объявлении нескольких указателей
её надо каждый раз проставлять заново
```C++
int *a, *b, c;
// a, b are pointers, c is integer
```
Самое главное, **к указателям можно прибавлять числа**. Этим и отличаются типы `T*, U*`, если `T` и `U` разных размеров.
Указатель `p + n` ссылается "на `n` элементов типа `T` дальше, чем `p`". То есть к адресу прибавляется `n * sizeof(T)`.
```C++
std::vector<int> v = {1, 2, 3, 4, 5}; // std::vector guarantees that they're aligned in a row
int *p = &v[0];
std::cout << *(p + 3) << std::endl; // 4
std::cout << *++p << std::endl; //2
```
Указатели можно вычитать(конечно, если они одного типа),
например `&v[3] - &v[0]` равно `3`, а `&v[0] - &v[4]` равно `-4`.
Но если вычесть два указателя, которые лежат не в одном куске, то получится
абсолютно случайное значение.
Грубо говоря, разность это `(address(p) - address(q)) / sizeof(T)`.
Ничего не мешает брать указатели на указатели.
На 64-битной архитектуре размер указателя это 8 байт. Размер указателя не зависит
от типа, на который он указывает.
Унарная `*` это lvalue(объект), а `&` это rvalue(его адрес), и его аргумент
должен быть lvalue(чтобы у него был адрес).
```C++
int a = 1;
int *p = &a;
{
int b = 2;
p = &b;
} // lifetime of b ends here, so p points to trash
std::cout << *p << std::endl; // UB, but most likely 2
```
Память может также переиспользоваться, если потом создать `int c = 3;`, например.
Если вы работаете с указателями разных типов, то скорее всего вы делаете что-то не то.
Их нельзя даже сравнить, будет ошибка компиляции.
Есть особый тип -- `void*`, указатель на непонятно что, любой указатель можно привести к нему,
но обратно без явного приведения нельзя. Но у него нет операций прибавления, разности, разыменовывания.
В C++ есть ключевое слово `nullptr`(константа `NULL` в Си, равная нулю), указатель в никуда.
При его разыменовывании происходит неопреденное поведение.
## 2.2 Kinds of memory
data, text, stack
data -- область памяти, в которой лежат глобальные переменные
text -- область памяти, куда загружена сама программа
stack -- область памяти, где хранятся локальные переменные
Она называется stack, потому что она работает как стек, когда программа входит в функцию,
на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости
они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на
стек в произвольном порядке и делать промежутки.
При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как
мы выйдем из функции.

View File

@ -0,0 +1,338 @@
# 2.2 Kinds of memory
Условно в первом приближении при запуске программы ей дают непрервыный кусок памяти по адресам которых она может обращаться.
Условно эта память делится на три части - data, text и stack.
`data` - область памяти, где хранится все, что определено на всей области программы - то есть, в частности, глобальные переменные.
`text` - машинный код вашей программы.
`stack` - по дефолту примерно 8 MiB. Область ответственная за уровни вложенности программы, которая "как бы" хранит все в том порядке в котором оно определяется. (Хотя на самом деле компилятор вам не обязан ничего и может переопределять порядок или добавление, видоизменение, игнорирование как хочет)
При какой-либо рекурсии мы на стек добавляем не только все плокальные переменные функции, но и указатель на то место, где он был вызыван, иначе
мы не узнаем куда вернуться.
Сам стек примерно вмещает 1 миллион запусков пустой "рекурсии".
Динамическая память - память выделяемая в процессе выполнения программы
```C++
int *p = new int; // выделение памяти на int и получение его адреса
```
`new` -- создает объект данного типа в динамической памяти.
> new - это какое-то шаманство, которое пока разобрано не будет.
> Выполняется в 200-300 раз дольше чем сложение в хорошем случае.
> А в плохом сколь угодно плохо) ($\geq 10000$)
Утечка памяти(memory leak)
```C++
void foo() {
int* p = new int(5);
...
// without delete
return;
}
```
Если не вызвать `delete`, то память останется условно занятой, но провзаимодействовать мы с ней никак не сможем, так как потеряли адреса.
В отличие от многих других языков в C++ нет сборщика мусора, поэтому надо убирать за собой.
Дважды удалять один и тот же указатель - UB(double free or corruption).
Провзаимодействовать с указателем и вызвать `delete` - UB.
Вызвать `delete` не от указателя созданного `new` - UB.
После `delete p` разыменовать `p` - UB.
Статическая память
```C++
{
static int z;
}
```
Теперь `z` лежит в вышеупомянутой `data`, ей предвыделило чуть больше памяти на этапе компиляции, специально для `z` и оно будет храниться там на протяжении всей программы (но доступно только из своей области видимости).
**Важно**. Ключевое слово `static` имеет разный смысл для переменных внутри функций и глобальных переменных.
Для глобальных переменных `static` влияет на линковку(internal linkage, что бы это ни значило), противоположность
ключевому слову `extern`.
Ниже приведены примеры переполнения стека(из-за рекурсии)
```C++
#include <iostream>
int f(int x) {
std::cout << '\n';
++x;
f(x);
}
int main() {
f(0);
}
// переполнение стека
```
```C++
#include <iostream>
int f() {
static int x = 0;
std::cout << '\n';
++x;
f();
}
int main() {
f();
}
// переполнение стека, but cooler
```
```C++
#include <iostream>
int f() {
int *p = new int(5);
std::cout << p << ' ' << *p << std::endl;
delete (++p);
f();
}
int main() {
f(0);
}
// переполнение стека
```
# 2.3 Arrays
```C++
#include <iostream>
int main() {
int a[10];
int b[5] = {1, 2, 3, 4, 5};
int c[5]{};
std::cout << *(a + 3) << '\n'; // 4
int *p = a + 3;
// p[i] == *(p + i), причем i[p] == p[i], потому что в терминах указателей это буквально i + p == p + i
std::cout << p[-2] << '\n'; // 2
}
```
`int*` и `int[]` взаимозаменяемы, поэтому следующий код вызовет ошибку компиляции
```C++
#include <iostream>
void f(int a[5]) {
;
}
void f(int* p) {
;
}
int main() {
int a[5] = {1, 2, 3, 4, 5};
int* b[5]; // массив из 5 указателей на int*, а не указатель на массив из 5 int
}
```
Массив можно выделить в динамической памяти (который на самом деле указатель на начало)
с помощью конструкции `new T[]`.
```C++
#include <iostream>
int main() {
int *p = new int[100];
delete[] p;
}
```
Если пытаться сделать `delete[]` от обычного указателя или наоборот - это runtime error
```C++
#include <iostream>
int main() {
std::vector<int> v(10);
v[-1] = 10000; // надругательство
return 0;
delete[] &v[0]; // надругательство
return 0;
}
```
```C++
#include <iostream>
int main() {
// Variable lenght array (VLA)
int n = 100;
std::cin >> n;
int a[n]; // с cin нельзя, с n = 100 - можно
}
```
Убрана обратная совместимость с C, чтобы не сдвигать переменные на стеке на какое-то неопредленное число, которое мы узнаем только в
рантайме. Но большинство компиляторов таки поддерживают VLA.
```C++
#include <iostream>
int main() {
const char* s = "abcdef"; // сам строковый литерал всегда хранится в статической памяти, сам указатель на него - на стеке
std::cout << (int)(s[4]) << '\n';
}
```
**Идентификатор `const`**.
Запись `const char *` значит, что мы вправе менять указатель, но не вправе менять то, что лежит под ним.
[Здесь][rt-lt] подробно описано, как читать такие объявления.
**Null-terminated strings**. В языке Си есть конвенция отождествлять строку с последовательностью байт, которая заканчивается на `0`
(для записи NUL символа можно использовать литерал `'\0'`).
Если нам дан указатель на начало, легко, например узнать длину строки, или скопировать одну в другую
```C
size_t
my_strlen(const char *s)
{
size_t len = 0;
while (*s++) {
len++;
}
// если гарантируется, что s заканчивается нулевым байтом, то
// мы никогда не выйдем за границы выделенной памяти, то есть данный код корректный
return len;
}
void
my_strcpy(char *dst, const char *src)
{
while ((*dst++ = *src++));
// Для того, чтобы не было UB, нужно, чтобы dst, src
// заканчивались нулем и в dst было достаточно места
}
```
```C++
#include <iostream>
#include <cstring>
int main() {
const char* s = "abc\0def"; // сам строковый литерал всегда хранится в статической памяти, сам указатель на него - на стеке
std::cout << strlen(s) << ' ' << s << '\n'; // выведет '3 abc', потому что из-за поддержки C-style string все пытается прыгать вокруг присутствия /0 ровно в конце строки
}
```
Под каждым `std::string` лежит C-style null-terminated строка, указатель на начало которой
получить вызвав метод `.c_str()`.
# 2.4 Functions and pointers to functions
Указатель на функцию это "указатель на код", его также можно передавать в другие функции.
Объявляется он как `return-type (*name)(arguments)`.
Опять же, по [этой ссылке][rt-lt] можно найти гайд по чтению этих записей.
```C++
#include <iostream>
int sqr(int x) {
return x * x;
}
int f(int x);
void sort(int *begin, int *end, bool(*cmp)(int, int));
int main() {
int x = 0;
std::cout << &x << '\n';
int(*p)(int) &sqr; // синтаксически корректное определние ссылки на функцию
std::cout << (void*)&sqr; // адрес sqr в памяти
int (*q)(int) = &f; // нельзя, так как функция не определена
}
```
Вызов функции по указателю медленнее, чем ее вызов напрямую, поэтому лучше
избегать их если можно(например, с помощью шаблонов).
```C++
#include <iostream>
int sqr(int x) {
return x * x;
}
double sqr(double x) {
return x * x;
}
int main() {
int (*p)(int) = sqr;
double (*pp)(double) = sqr;
double (*ppp)(double) = (double(*)(double))(p);
std::cout << (void*)p << ' ' << (void*)pp << '\n'; // выведет 2 разных адреса, потому что оно взяло разные функции
}
```
Существуют также variadic функции. Ниже адаптирован пример
[отсюда](https://www.gnu.org/software/libc/manual/html_node/Variadic-Example.html)
**TL;DR** Не пишите C-style variadic функции в C++. Будьте ОЧЕНЬ аккуратны при использовании variadic функций в C/C++.
```C
// C-style variadic function
int
variadic_sum(size_t count, ...)
{
va_list ap;
size_t i;
int sum;
va_start(ap, count);
sum = 0;
for (i = 0; i < count; ++i) {
sum += va_arg(ap, int); // макрос принимает va_list и тип аргумента, который мы ждём
}
va_end(ap); // может не делать ничего, но по стандарту нужно
return sum;
}
```
Данный код очень небезопасен по многим причинам. Первый аргумент `count` лежит на стеке, поэтому компилятор знает, как его достать. А
дальше могут лежать аргументы произвольных типов, которые могут занимать разное количество памяти.
Как тогда вытащить их со стека? `ap`
указывает на текущую позицию(сокращение от `argument pointer`), а `va_arg` приводит `ap` к типу, в нашем случае `int`, и прибавляет
количество байт, которое этот тип занимает.
Можно подумать, что в `va_start` мы передаем количество аргументов, но это неправда. `va_start` это **макрос**, а не функция, поэтому у
него есть немного контекста, например, адрес переменной `count`.
Он берет указатель на `count` и прибавляет к нему число байт, которое `count` занимает, чтобы указать на первый
variadic аргумент.
Тем самым, вторым аргументом в `va_start` нужно указать **последний не variadic аргумент функции**.
Одна из самых известных variadic функций с которой вы наверняка сталкивались, это `printf`.
Если передать в `printf` недостаточное число аргументов, слишком много аргументов, или тип аргумента
не совпадет со спецификатором(например, вы передали `char*` в `%d`), то **произойдет UB** как раз из
механизма работы `va`. Например, оно может брать аргументы выше по стеку, поэтому **никогда не передавайте
в `printf` недоверенный спецификатор формата, например, `prinf(username);` из соображений безопасности**.
[rt-lt]: https://cseweb.ucsd.edu/~gbournou/CSE131/rt_lt.rule.html

196
pages/mipt_cxx1/06.md Normal file
View File

@ -0,0 +1,196 @@
## 2.7 Type conversions
`static_cast` -- ключевое слово языка C++
```C++
T x;
static_cast<U>(x);
```
С помощью него неявные преобразования можно сделать явно через `static_cast`,
такие как `double -> int, A* -> void*`.
`reinterpret_cast` -- говорит компилятору трактовать
переменную `x` как переменную типа `U` побитово. То
есть интерпретировать память как будто бы там лежит
другой тип.
1. `reinterpret_cast` нужно делать от ссылки, например,
`reinterpret_cast<double&>(y)`.
```C++
long long y;
double &d = reinterpret_cast<double&>(y); // UB
d = 3.14;
std::cout << y << std::endl; // какая-то дичь
```
2. По умолчанию это делать нельзя, поэтому в примере выше это UB.
Это можно делать, например, если есть две структуры в которых
типы расположены в одном и том же порядке.
3. `reinterpret_cast` также можно применять к указателям,
но только к совместимым
4. `reinterpret_cast` не позволяет обходить `const`.
Для обхода есть `const_cast`. Он позволяет снять константность.
```C++
const int c = 5;
int &cc = const_cast<int&>(c); // также нужно давать ссылку
cc = 7; // UB
// и также const_cast от ссылки и указателя это две разные сущности
std::cout << c << ' ' << cc << std::endl;
```
Попытка изменить переменную, которая изначально была константой это UB.
C-style cast. Всегда бан в программах на C++. По сути он последовательно перебирает все возможные касты, пока он не подойдет. Например,
вы можете не заметить, как случайно сделаете `const_cast` и словите UB.
Байка от Страуструпа: названия кастов специально сделаны слишком большими, чтобы их меньше хотелось писать.
Ещё есть `dynamic_cast`, но мы пока про него не говорим.
**Стадии сборки**
1. Препроцессинг
2. Компиляция
3. Ассемблирование
4. Линковка
Препроцессор обрабатывает команды препроцессора по типу `#include, #define, ...`. Это не компиляция, а просто обработка текста.
Например, `#include "file"` будет искать файл, `file` в директории файла и в специально указанных путях. Потом он просто заменит эту
строчку содержимым файла. `#include <header>` говорит, что `header` нужно искать __в системе__.
**Упражнение**. Понять, где у вас лежит `iostream`.
Далее компилятор переделывает код(уже без директив с решеткой) в ассемблерный код(`.s`). Далее с помощью ассемблера он уже преобразуется
в объектный файл(уже с машинными инструкциями, `.o`). Далее линкер
преобразует его в исполняемый файл.
В чём разница между `1.cpp -> 1.o` и `a.out`. Линковщик говорит, где нужно искать функции(символы)
# 3 Basics of OOP
## 3.1 Classes and structures, encapsulation
Типы -- классы, данные -- поля классов, операции -- методы классов,
объекты -- экземпляры классов.
Класс можно объявить с помощью ключевого слова `class`, структуру
с помощью ключевого слова `struct`.
Пока мы будем использовать ключевое слово `struct`.
```C++
struct S {
int x; // поле структуры
};
int main() {
S s;
s.x; // обращение к полю x структуры s
std::cout << s.x << std::endl; // UB, так как не инициализировано
}
```
```C++
struct S {
int x = 1;
double d = 3.14;
};
int main() {
S s;
std::cout << s.x << std::endl; // ok, x = 1
}
```
Размер структуры равен сумме полей с точностью до _выравнивания_.
Например `sizeof(S) == 16`, несмотря на то, что `sizeof(int) + sizeof(double) = 12`. Её байты заполнены так: `IIII....DDDDDDDD`, это
сделано в силу того, что восьмибайтные переменные кладутся по адресам кратным 8. Сам объект `S` тоже хочется положить по адресу кратному
8, чтобы их можно было класть подряд. Если бы `S` выглядела так: `IIIIDDDDDDDD`, то две такие нельзя было бы поставить подряд.
Например, можно сделать `reinterpret_cast<int&>(s)`, тогда мы прочитаем `int` с первых четырех байт `S`.
Структуры можно инициализировать агрегатно, `S s{2, 2.718}`.
Внутри структур нельзя писать выражения и объявлять пространства имен. Но можно создавать методы и использовать `using`.
```C++
struct S {
...
void f(int y) {
std::cout << x + y << std::endl;
}
}
int main() {
S s;
s.f(228);
}
```
Внутри структур можно использовать методы до того, как они объявлены в коде.
Можно объявлять методы вне структуры
```C++
void S::f(int y) {
std::cout << x + y;
ff();
}
```
Ключевое слово `this` -- возвращает указатель на объект, в котором мы сейчас находимся.
```C++
struct S {
int x;
double d;
void ff(int x) {
// x and this->x are not the same
}
}
```
Можно объявлять структуры внутри структур(inner class).
Можно анонимно объявлять структуры
```C++
struct {
char c;
} a;
```
Можно объявить структуру прямо внутри функции(local class).
## 3.2 Access modifiers
Одним из основных отличий классаот структуры является возможность
объявить приватное поле/метод. Все поля в структурах по умолчанию публичные, а в классах наоборот приватные, к ним нельзя обратиться
извне.
Ясно, что ошибки доступа проверяются на этапе компиляции.
Модификаторы доступа можно поменять в классе ключевыми словами `public, private`
```C++
class C {
public:
int x;
private:
int y;
public:
int z;
private:
int t;
}
```

324
pages/mipt_cxx1/08.md Normal file
View File

@ -0,0 +1,324 @@
## 3.5 Const, static and explicit methods
```C++
struct S {
void f() {
std::cout << "Hi!";
}
};
int main() {
const S s;
s.f(); // CE
}
```
По умолчанию методы у классов считаются неконстантными. То есть `S::f()` не определена для `const S`,
потому что она неконстанта. Константность нужно явно уточнить.
```C++
void f() const { // Now OK
std::cout << "Hi!";
}
```
Можно делать перегрузку по признаку константности, ровно как можно делать перегрузку по константности аргумента,
ведь объект класса это неявный "нулевой" аргумент.
```C++
struct S {
void f() const {
std::cout << 1;
}
void f() {
std::cout << 2;
}
};
int main() {
S s;
const S& r = s;
r.f(); // 1
}
```
**Пример**. Квадратные скобки для строки
```C++
struct S {
char arr[10];
char& operator[](size_t index) { // for non-const access
return arr[index];
}
const char& operator[](size_t index) const { // for const access
return arr[index];
}
};
```
Почему нельзя заменить `const char&` на `char`?
```C++
int main() {
String s = "abcd";
const String &cs = s;
const char &c = cs[0];
s[0] = 'b';
// if operator[] returns const char&, c will be 'b', otherwise 'a'
}
```
Почему `char& operator[](size_t index) const` вообще скомпилируется?
`char *arr` превращается в `char* const arr`, а не в `const char* arr`,
поэтому при обращении к `arr` по индексу мы получаем неконстантную ссылку,
поэтому всё компилируется.
К сожалению, следующий код компилируется
```C++
int x = 0;
struct S {
int& r = x;
void f(int y) const {
r = y;
}
};
```
Поскольку ссылка это по факту указатель, `const` на него не влияет.
А что если мы хотим менять поле у константного объекта? Для этого есть ключевое слово `mutable`.
```C++
struct S {
mutable int n_calls = 0;
void f() {
++n_calls;
std::cout << "Hello!" << std::endl;
}
};
```
Это даже может быть полезно, например, при реализации сплей-дерева в виде класса. Сплей-дерево
после вызова `find` вызывает `splay` и перестраивает дерево. Но как быть, ведь `find` по-хорошему должен быть
константным. Здесь как раз помогает ключевое слово `mutable`.
`static` методы -- те, которые "относятся к классу в целом". Для полей оно значит то же, что и для глобальных
переменных.
```C++
struct S {
static int x; // will be in static memory
// Can be accessed via S::f();
static void f() {
std::cout << "Hi!" << std::endl;
}
};
```
Классический пример -- синглтон, класс, который должен существовать ровно один в программе,
например, соединение с базой данных.
```C++
class Singleton {
private:
Singleton() { /* i.e open connection */ }
static Singleton* ptr = nullptr;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getObject() {
if (ptr == nullptr) {
ptr = new Singleton();
}
return *ptr;
}
};
```
Ключевое слово `explicit` запрещает вызывать методы неявно.
```C++
struct Latitude {
double value;
// Latitude(double value_) : value(value_) {}
// f(Latitude) can be called by f(0.5) via implicit conversion
// and we can confuse it with longitude
explicit Latitude(double value_) : value(value_) {} // now implicit conversion is forbidden
};
```
А что если мы хотим приведение от `Latitude` к `double`?
```C++
operator double() const { // f(double) can be called via f(Latitude)
return value;
}
// starting from C++-11 cast operators can be made explicit
```
В случае если оператор приведения был объявлен `explicit` можно воспользоваться `static_cast`.
Конструкторы из одного аргумента или операторы приведения типов лучше делать `explicit`, но не всегда.
**Важный пример.** Даже оператор конверсии `BigInteger` в `bool` нужно будет сделать `explicit`. Для if-ов
добавили специальный костыль под названием contextual conversion, она происходит под `if, while` и тернарным оператором.
Это особый вид конверсии, который разрешает рассматривать `explicit` конверсии.
Начиная с C++-11 можно определять свои литеральные суффиксы.
```C++
class BigInteger {};
// either unsigned long long, long double, const char*
BigInteger operator""_bi(unsigned long long x) {
return BigInteger(x);
};
1329_bi; // valid BigInteger
```
Стандартная строка также обладает литеральным суффиксом, `"abcdef"s` будет `std::string`, а не `const char*`.
## 3.6 Operators overloading
```C++
struct Complex {
double re = 0.0;
double im = 0.0;
Complex(double re_) : re(re_) {}
Complex(double re_, double im_) : re(re_), im(im_) {}
Complex operator+(const Complex &other) const {
return Complex{re + other.re, im + other.im};
}
};
int main() {
Complex c(1.0);
c + 3.14; // OK, c.operator+(3.14)
3.14 + c; // CE, ???
}
```
Если нужно определить бинарный арифметический оператор, то лучше объявить его вне класса.
```C++
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.re + b.re, a.im + b.im);
}
```
`operator+=` уже нужно определять внутри класса
```C++
Complex& operator+=(const Complex &other) {
*this = *this + other;
return *this;
}
```
Это **очень** плохой код, в том смысле, что он неэффективный. Например, для строки это в два раза хуже,
чем обычная реализация. Лучше реализовать `+` через `+=`.
```C++
Complex operator+(const Complex &a, const Complex &b) {
Complex result = a;
result += b;
return result;
}
```
А не надо ли поставить `const`?
```C++
int main() {
Complex a(1.0), b(2.0), c(3.0);
a + b = c; // Why it compiles?
}
```
Но слева же `rvalue`. А с чего мы взяли, что нельзя присваивать что-то `rvalue` для нестандартных типов?
Один из способов решения это поставить `const` перед возвращаемым значением.
**Но это было актуально до C++-11, сейчас так лучше не делать**
```C++
struct Complex {
Complex& operator=(const Complex& other) & {
} // now it can be applied only to lvalue
Complex &operator=(const Complex &other) && {
} // only to rvalue
};
```
**Внимание**. В коде ниже происходит лишнее копирование
```C++
Complex operator+(Complex a, const Complex &b) {
return a += b;
}
```
Так как компилятор в первом случае применит return value optimization, а здесь нет.
Можно также перегружать оператор вывода
```C++
std::ostream& operator<<(std::ostream &out, const String &str) {
}
```
Аналогично оператор ввода из потока
```C++
std::istream& operator>>(std::istream &in, String &str) {
}
```
Оператор ввода иногда разумно сделать `friend`.
Оператор возвращает тот же поток чтобы можно было использовать синтаксис `std::cout << x << y`.
Нельзя определить символ для обозначения оператора, приоритет оператора, порядок вычисления.
```C++
bool operator<(const Complex &a, const Complex &b) {
return a.re < b.re || (a.re == b.re && a.im < b.im);
}
bool operator>(const Complex &a, const Complex &b) {
return b < a; // same time as operator<
}
bool operator<=(const Complex &a, const Complex &b) {
return !(a > b);
}
```
То есть желательно всё, кроме равенства выразить через `operator<`.
Начиная с C++-20 есть оператор "космический корабль", также известный как three-way comparison.
```C++
??? operator<=>(const Complex &other) = default; // automatic deduction lexicographically
```
Возвращает один из трех типов `std::weak_ordering, std::strong_ordering, std::partial_ordering`.
`std::partial_ordering: less, greater, equivalent, unordered`.
Разница между `std::strong_ordering, std::weak_ordering` в том, когда достигается равенство.
`std::strong_ordering` значит, что `a == b => forall f: f(a) == f(b)`.
В строке, например, стандартный оператор `<=>` так как надо сравнивать не указатели, а значения под ними.
Все эти вещи определены в заголовочном файле `<compare>`.

307
pages/mipt_cxx1/09.md Normal file
View File

@ -0,0 +1,307 @@
## 3.7 Pointers to members
```C++
struct S {
int x;
double y;
void f(int z) {
std::cout << x + z << std::endl;
}
};
int main() {
int S::* p = &S::x;
S s;
s.*p; // returns s.x;
S* ps = &s;
ps->*p; // returns s.x;
void (S::* pf)(int) = &S::f; // pointer to method
(s.*pf)(3);
(ps->*pf)(5)
}
```
Интуитивно указатель на поле хранит "сдвиг относительно начала структуры".
## 3.8 Enums and enum classes
Дословно перечислимый тип
```C++
enum E {
White, Gray, Black
};
int main() {
int e = White;
std::cout << e << std::endl; // 0
}
```
Можно использовать как отдельный тип `E`, но он хранится в памяти как `int` и нумеруется начиная с нуля.
В C++-11 появились `enum class`. Она не вносит интовые константы и запрещает неявные конверсии.
С помощью двоеточия можно описать каким типом он должен представляться(обязательно целочисленным)
```C++
enum class E : int8_t {}
```
# IV. Inheritance
## 4.1 Public, private and protected inheritance
```C++
class Base {
protected:
int x;
public:
void f() {}
};
class Derived : Base {
int y;
void g() {
std::cout << x << std::endl; // x is inherited from Base, and protected
}
};
int main() {
Derived d;
// std::cout << d.x << std::endl; CE, x is protected
}
```
Ключевое слово `protected` означает то, что в отличие от `private` данный член класса будет доступен еще и его наследникам.
Наследование также бывает публичным, приватным, и защищенным.
По умолчанию у структур оно публичное, у классов приватное.
Публичное наследование значит, что "все знают" о том, что `Derived` является наследником `Base`, а приватное значит, что "никто не
знает".
Например, если мы сделаем `Derived : private Base`, то из `main` мы не сможем вызвать `d.f()`, так как хоть `f` и публично в `Base`, мы
унаследовали его приватно.
Если мы сделаем `Derived : public Base`, то мы все равно не сможем обратится к `d.x`, так как `d.x` приватно в `Base`.
Защищенное наследование значит, что только лишь друзья, наследники, и сам `Derived` имеет доступ к родительским полям.
По сути права доступа к полю "перемножаются" на тип наследования, нужно "пройти" оба модификатора.
```C++
struct Granny {
int x;
void f() {}
};
struct Mom : protected Granny {
int y;
void g() {
std::cout << x << std::endl;
}
};
struct Son : Mom {
int z;
void h() {
std::cout << x << std::endl // OK, can pass protected modifier
}
};
int main() {
Son s;
// s.x; cannot pass protected modifier from Mom to Granny
s.y; // OK, default inheritance for struct is public
}
```
Как работает дружба при наследовании?
Допустим в `struct Granny` мы объявили `main` своим другом.
Тогда теперь мы всё ещё не сможем обратиться к `s.x`. И это логично, ведь `friend` снимает все ограничения, которые ты наложил, но не те,
которые наложил кто-то другой, например, твой наследник.
> Строгая мама запрещает общаться с доброй бабушкой
## 4.2 Visibility
Что происходит если есть конфликт имён?
```C++
struct Base {
int x;
void f() {
std::cout << 1 << std::endl;
}
};
struct Derived : Base {
int y;
void f() {
std::cout << 2 << std::endl;
}
};
int main() {
Derived d;
d.f() // OK, 2
}
```
Главный принцип: астное главнее общего_.
А что если
```C++
struct Base {
int x;
void f(int) {
std::cout << 1 << std::endl;
}
};
struct Derived : Base {
int y;
void f(double) {
std::cout << 2 << std::endl;
}
};
int main() {
Derived d;
d.f(0) // ???
}
```
Программа выведет 2. Более того, если бы `f` не принимала аргументов, то была бы ошибка компиляции. Полезно думать об этом так,
`Derived::f` затмевает `Base::f` как будто бы это более локальная область видимости.
Если мы хотим явно вызваться от родителя, то можно написать `d.Base::f(0);`.
Приватность и публичность также не влияет на то, какой метод мы будем вызывать, свой или родительский.
Чтобы научиться выбирать, нужно написать `using Base::f` внутри `Derived`. Более того, `using Base::f` игнорирует родительский
модификатор доступа, что логично.
Сначала создается область видимости, потом проверяются права доступа.
Сам `using` можно сделать приватным
```C++
private:
using Base::x;
```
Самый кринж
```C++
struct Granny {
int x;
void f() {}
};
struct Mom: private Granny {
friend int main();
int x;
};
struct Son : Mom {
int x;
void f(Granny& g) {
std::cout << g.x << std::endl;
}
};
```
Оно не скомпилируется, так как `Granny` из области видимости сына приватное и он не имеет к нему доступа. Поэтому нужно писать `::Granny &g`, дабы подчеркнуть, что имя берется из глобальной области.
## 4.3 Memory layout, constructors and destructors in case of inheritance.
```C++
struct Base {
int x;
};
struct Derived : Base {
double y;
};
int main() {
sizeof(Derived); // OK, 16. First x, then y, according to padding it's 2 * 8 = 16
}
```
А что если `Base` вообще не содержит полей, только, возможно, методы. `sizeof(Base) > 0`, так как структура должна иметь хоть какой-то
размер, чтобы разные размеры имели разные адреса.
Но тем не менее, `sizeof(Derived) == 8`. Данный феномен именуется EBO (Empty Base Optimization). Пустому `Base` разрешается ничего не
занимать в памяти.
При конструкции всегда должен **сначала** инициализироваться родитель. То есть либо у `Base` есть дефолтный конструктор, либо нужно
писать что-то типа
```C++
struct A {
A(int) { std::cout << "A " << x << std::endl; }
};
struct Base {
A x;
Base(int x): x(x) { std::cout << "Base" << std::endl; }
};
struct Derived : Base {
A y;
Derived(double y): Base(0), y(y) { std::cout << "Derived" << std::endl; }
}
int main() {
Derived d = 1;
}
```
Программа выведет
```
A 0
Base
A 1
Derived
```
Если написать деструкторы, то так как сначала выполняется тело деструктора, а потом уничтожаются поля, причём в обратном порядке, то выведется
```
~Derived
~A 1
~Base
~A 0
```
## 4.4 Casts in case of inheritance
```C++
struct Base {
int x = 1;
};
struct Derived : Base {
int y = 2;
};
void f(Base& b) {
std::cout << b.x << std::endl;
}
int main() {
Derived d;
f(d); // OK, can cast Derived to Base
}
```
Суть в том, что наследника можно кастовать к родителям, а вот обратное, ясное дело нельзя, ведь наследник может больше, чем родитель.

115
pages/mipt_cxx1/11.md Normal file
View File

@ -0,0 +1,115 @@
## 5.4 RTTI and dynamic cast.
```C++
struct Base {
int x = 0;
virtual void f() {}
virtual ~Base() = default;
};
struct Derived : Base {
int y = 0;
void f() override {}
};
int main() {
Derived d;
Base& b = d;
dynamic_cast<Derived&>(b); // an example of correct dynamic_cast
// if we know, that Base was from Derived, we can (dynamic) cast Base to Derived.
// dynamic_cast will throw std::bad_cast if cast failed
Derived* pd = dynamic_cast<Derived*>(b) // will return pointer to Derived in case
// of success, and nullptr otherwise
if (pd) {
// OK
}
}
```
`dynamic_cast` работает только для типов с виртуальными функциями(полиморфных). Для типов у которых нет виртуальных функций
нет способов узнать по родителю, что за базовый класс лежит по этому адресу. Для классов с виртуальными
функциями поддерживается специальная информация(vtable), который нужен для того, чтобы понять
какую функцию вызывать. По ней в рантайме можно восстановить тип.
В случае если тип не полиморфный, код с `dynamic_cast` не скомпилируется.
Этот механизм называется RTTI(Runtime type information).
Для полиморфных объектов можно в рантайме узнать информацию о типе явно.
```C++
std::type_info info = typeid(b);
std::cout << info.name() << std::endl; // will print MANGLED type name
// std::type_info can(and should) be compared
```
## 5.5 Memory layout of polymorphic objects
То, что будет рассказываться в этом параграфе формально не является частью стандарта C++,
но является частью стандарта ABI и по факту почти наверняка так и есть.
Полиморфные объекты хранят __указатель на vtable__. vtable(виртуальная таблица)
-- это структура данных, хранящаяся в статической памяти одна на тип,
которая хранит адреса виртуальных методов и предков, предков мы обсудим чуть позже.
```C++
struct Base {
virtual void f() {}
void h() {}
int x;
}; // sizeof(Base) == 16 due to padding, Base is [vptr, int]
```
Что хранится в vtable для Base? Там хранится `type_info` для `Base`, а затем указатель на функцию `f`. По нулевому смещению
хранится `Base::f`, информация о типе лежит "сзади". Пусть мы ещё определили `Derived` следующим образом
```C++
struct Derived : Base {
void f() override {}
virtual void g() {}
int y;
}; // Derived is [vptr, x(from Base), y]
```
Таблица для Derived выглядит как `[&typeinfo, &f, &g]`, по нулевому смещению находится `f`. Как тогда выглядит вызов
виртуальной функции? Пусть у нас есть `b.f()`. Компилятор видит, что она виртуальная, поэтому это не просто `call` по
константому адресу, как это было бы, например, при вызове `b.h()`. Поэтому при вызове `b.f()` мы разыменовываем
указатель на виртуальную таблицу. Компилятор уже знает, на каком месте в таблице стоит `f`, поэтому он может ее вызвать.
Если бы под `b` лежал на самом деле `Derived`, то под `b` был бы указатель на другую таблицу, так и происходит разрешение.
`dynamic_cast`, соответственно, использует `type_info` чтобы понять, что ему надо делать.
Рассмотрим пример наследования `Son -> Mom -> Granny` с полями `s, m, g`, причем `Granny` не полиморфная,
а `Mom` полиморфная. Тогда `dynamic_cast` от `Son` к `Granny` возможен, в памяти `[ptr, g, m, s]` нужно сдвинуть
указатель на начало к `g`, так как у `Granny` нет `ptr` в силу того, что она не полиморфна. А вот обратно `dynamic_cast`
невозможен, т.к `Granny` не является полиморфным типом.
А что происходит при множественном наследовании?
```
/ -> Mom -> Granny
Son --/
\ -> Dad -> Granny
```
Для простоты положим `Granny` полиморфной, а наследование не виртуальным. Как выглядит `Son`?
`[ptr, g, m, ptr(from Dad), g, d, s]`. Для `Dad` нужен второй указатель, так как у `Son` и `Mom` начало общее, а
у `Son` и `Dad` уже нет.
Положим `Granny` имеет функцию `f`. Вызов `son.f()` неоднозначен. Но если мы переопределим `f` в `Son`, то
вызов `f` в любой из двух `Granny` будет приводить к вызову нужной нам `f`, неоднозначности нет.
Теперь добавим щепотку виртуального наследования. Если по умолчанию в виртуальной таблице хранится
`top_offset` -- то, как далеко мы находимся от начала объекта, то при виртуальном наследовании
в таблице хранится ещё `virtual_offset`, по какому смещению хранится предок.
Представим, что `Granny` виртуальная. Тогда `Son` выглядит в памяти как `[ptr, m, ptr, d, s, ptr, g]`.
Таблица зависит не только от типа, но и от его положения в графе наследований, ведь теперь у нас есть
разные смещения(`virtual_offset`) у разных объектов наследующих один и тот же тип.
## 5.7 Non obvious problems with virtual functions
Виртуальные функции очевидно не могут быть `static`. Виртуальные функции нельзя оставлять
без определения, ведь нужны указатели на нее, чтобы создать vtable, будет ошибка линковки.
На доске был какой-то пример, который __часто дают на собесах(c)__, поэтому в данном конспекте он отсутствует.
Стоит гуглить pure virtual function call example.

22
pages/mipt_cxx1/Makefile Normal file
View File

@ -0,0 +1,22 @@
BUILDDIR ?= build
PAGE := $(shell basename $(shell pwd))
PREFIX := $(BUILDDIR)/page/$(PAGE)
MARKDOWN_FILES := $(wildcard *.md)
MARKDOWN_TARGETS = $(patsubst %.md,$(PREFIX)/%.html,$(MARKDOWN_FILES))
all: $(MARKDOWN_TARGETS) index
$(PREFIX)/%.html: %.md
@mkdir -p $(@D)
pandoc $< --to html --output $@ --standalone
$(PREFIX)/index.html:
@mkdir -p $(@D)
./generate_index.sh > $@
index: $(PREFIX)/index.html
.PHONY: all index

View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
cat <<-_EOF
<html>
<head>
<title>Конспекты по плюсам</title>
</head>
<body>
_EOF
for page in *.md; do
p=${page%.md}
echo "<a href=\"$p.html\">$p</a><br>"
done
cat <<-_EOF
</body>
</html>
_EOF

View File

@ -1,17 +1,17 @@
---
header-includes: |
\newcommand{\legendre}[2]{\begin{pmatrix} #1\cr \hdashline #2\cr \end{pmatrix}}
\newcommand{\Zp}{\mathbb{Z}_p}
\newcommand{\Fp}{\mathbb{F}_p}
...
# Квадратичные вычеты и корни по простому модулю
На данный момент статья находится в разработке.
В этой статье вы узнаете всё о квадратичных вычетах по простому модулю и разложении числа в сумму двух квадратов.
Будем считать, что арифметические операции в $\Zp$ работают за константое время или что то же самое, мерять все в арифметических
Будем обозначать множество остатков по модулю $p$ со сложением и умножением как $\Fp$.
Будем считать, что арифметические операции в $\Fp$ работают за константое время или что то же самое, мерять все в арифметических
операциях(сложение, умножение, вычитание).
@ -19,13 +19,13 @@ header-includes: |
Здесь и далее $p$ -- простое число.
**Определение**. $p \nmid a \leftarrow \Zp$ называется _квадратичным вычетом_ если существует такое $z$, что $z^2 \equiv a \pmod{p}$, а
**Определение**. $a \in \Fp$ называется _квадратичным вычетом_ если существует такое $z$, что $z^2 \equiv a \pmod{p}$, а
иначе евычетом_.
**Определение**. Символом Лежандра числа $a$ называется величина $\legendre{a}{p}$, равная единице, если $a$ является квадратичным
вычетом по модулю $p$, нулю если $a \equiv 0$, и $-1$ иначе.
**Лемма**. В $\Zp$ поровну квадратичных вычетов и невычетов, а именно $\frac{p - 1}{2}$.
**Лемма**. В $\Fp$ поровну квадратичных вычетов и невычетов, а именно $\frac{p - 1}{2}$.
**Доказательство**. Посмотрим на числа $1^2, 2^2, \ldots, (p - 1)^2$. Заметим, что $a^2 \equiv b^2$ равносильно тому что $p \mid (a - b)(a + b)$, то есть
либо $a \equiv b$, либо $a \equiv -b$. А это значит, что среди первых $\frac{p - 1}{2}$ квадратов нет повторяющихся, а поскольку $a^2
@ -52,7 +52,7 @@ $\frac{y}{x} = yx^{-1}$, а $x^{-1}$ обратный остаток к $x$.
**Доказательство**. Заметим, что если $a \equiv x^2$, то $a^{\frac{p - 1}{2}} \equiv x^{p - 1} \equiv 1$ по малой теореме Ферма, то есть
для вычетов утверждение верно.
Можно было бы сказать, что многочлен $x^{\frac{p - 1}{2}} - 1$ имеет не более $\frac{p - 1}{2}$ корней, потому что $\Zp$ -- поле, но у
Можно было бы сказать, что многочлен $x^{\frac{p - 1}{2}} - 1$ имеет не более $\frac{p - 1}{2}$ корней, потому что $\Fp$ -- поле, но у
этого факта есть более элементарное доказательство.
Проделаем тот же трюк, что и в доказательстве мультипликативности для двух невычетов. Тогда произведение всех чисел в парах с одной
@ -143,8 +143,8 @@ ab - bx + ax - x^2 \equiv ab + bx - ax - x^2
**Лемма**. Существует алгоритм, который для вычета $n$ ищет такой $z$, что $z^2 \equiv n \pmod{p}$ за ожидаемое время $O(\log p)$.
**Доказательство**. Найдём такое $a$, что $a^2 - n$ является невычетом за ожидаемое время $O(\log p)$. Обозначим за $\omega = \sqrt{a^2 - n}$.
Строго говоря, посмотрим на все многочлены в $\Zp$ от $\omega$. и посмотрим на них по модулю $\omega^2 - (a^2 - n)$. Получившееся
множество $\{ a + bw \mid a, b \leftarrow \Zp \}$ будем обозначать $T = \Zp[w]/(w^2 - (a^2 - n))$.
Строго говоря, посмотрим на все многочлены в $\Fp$ от $\omega$. и посмотрим на них по модулю $\omega^2 - (a^2 - n)$. Получившееся
множество $\{ a + bw \mid a, b \leftarrow \Fp \}$ будем обозначать $T = \Fp[w]/(w^2 - (a^2 - n))$.
Заметим, что числа из $T$ можно складывать и умножать: $$(a + bw) + (c + dw) = (a + c) + (b + d)w, (a + bw)(c + dw) = (ac + bd(a^2 - n)) +
(ad + bc)w$$
@ -191,7 +191,7 @@ ab - bx + ax - x^2 \equiv ab + bx - ax - x^2
**Лемма**. Любое простое $p$ вида $4k + 1$ представляется в виде суммы двух квадратов.
Попробуем конструктивно научиться искать разложение для $p = 4k + 1$. Для начала поймем как "перемножать" квадратичные формы,
Попробуем конструктивно научиться искать разложение для $p = 4k + 1$. Для начала поймем как "перемножать" такие разложения,
это также поможет нам далее, когда мы перейдем в составному $n$.
Пусть $a^2 + b^2 = x, c^2 + d^2 = y$. Тогда
@ -208,6 +208,9 @@ a^2c^2 + a^2d^2 + b^2c^2 + b^2d^2 = xy
(ac + bd)^2 + (ad - bc)^2 = xy
\end{equation*}
То же самое можно увидеть, если рассмотреть $z = a + bi, w = c + di$, тогда
$\conj{z}w = (ac + bd) + (ad - bc)i$, а $xy = |z|^2|w|^2 = |\conj{z}|^2|w|^2 = |\conj{z}w|^2|$.
То есть мы доказали, что если $x, y$ представимы в виде суммы двух квадратов, то $xy$ тоже представимо в виде суммы двух квадратов.
Попробуем представить число $p$, пользуясь предыдущим знанием. Пусть для начала у нас есть разбиение числа $mp$ для какого-то $m$, то
@ -252,3 +255,4 @@ $a \equiv b \pmod{m}$.
a, b$. Тода $p^2 \mid a^2 + b^2$ и $\left( \frac{a}{p} \right)^2 + \left( \frac{b}{p} \right)^2 = \frac{a^2 + b^2}{p^2}$. Применяя то же
самое рассуждение получаем требуемое.
Тем самым мы доказали рождественскую теорему Ферма и научились раскладывать числа в суммы двух квадратов.