Compare commits
10 Commits
3bff372afe
...
0a66856500
Author | SHA1 | Date | |
---|---|---|---|
0a66856500 | |||
72f0941bb1 | |||
62601523d4 | |||
112832ea4b | |||
5e199a9c83 | |||
6228e4a161 | |||
e79edfb21b | |||
846363cd4c | |||
149b450b41 | |||
9f11ec59c4 |
@ -1,9 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
|
<title> thematdev wiki </title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
Конспекты:
|
||||||
|
<br>
|
||||||
|
<a href="/page/mipt_cxx1">Конспекты лекций ФПМИ МФТИ по C++ (продвинутый поток, осенний семестр 2023)</a>
|
||||||
|
<br>
|
||||||
Статьи:
|
Статьи:
|
||||||
<br>
|
<br>
|
||||||
<a href="/page/linear_nt">Линейные алгоритмы в теории чисел</a>
|
<a href="/page/linear_nt">Линейные алгоритмы в теории чисел</a>
|
||||||
|
3
pages/inplace_merge/Makefile
Normal file
3
pages/inplace_merge/Makefile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default makefile for algorithmica-like pages
|
||||||
|
|
||||||
|
include $(SCRIPT_DIR)/mk/algorithmica.mk
|
96
pages/inplace_merge/main.md
Normal file
96
pages/inplace_merge/main.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# 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 <= t + 1$, что и требовалось.
|
||||||
|
|
||||||
|
**Шаг 2.** Согласно следствию нам достаточно итеративно сливать соседние блоки. Каждое такое слияние можно выполнить без дополнительной
|
||||||
|
памяти с помощью буфера из максимумов, которым мы запаслись в начале. Ясно, что суммарно такие слияния работают за $O(n)$.
|
||||||
|
|
||||||
|
**Шаг 3.** Квадратично отсортируем буфер из максимумов, и, если требуется, дольем неполные блоки с помощью Леммы 2. Ясно, что эта
|
||||||
|
процедура работает за $O(n)$ и не требует дополнительной памяти.
|
||||||
|
|
||||||
|
Тем самым мы научились решать исходную задачу.
|
||||||
|
|
||||||
|
## Полезные ссылки и источники
|
||||||
|
* [Лекция Ф.Д.Руховича по алгоритмам и структурам данных](https://www.youtube.com/watch?v=V-kWungDekU&t=4536s)
|
5
pages/inplace_merge/metadata.md
Normal file
5
pages/inplace_merge/metadata.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
credits:
|
||||||
|
- Никифор Кузнецов
|
||||||
|
title: Inplace merge
|
||||||
|
---
|
126
pages/mipt_cxx1/01_imported.md
Normal file
126
pages/mipt_cxx1/01_imported.md
Normal 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
372
pages/mipt_cxx1/02.md
Normal 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
228
pages/mipt_cxx1/03.md
Normal 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 Fermat’s 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, потому что она работает как стек, когда программа входит в функцию,
|
||||||
|
на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости
|
||||||
|
они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на
|
||||||
|
стек в произвольном порядке и делать промежутки.
|
||||||
|
|
||||||
|
При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как
|
||||||
|
мы выйдем из функции.
|
338
pages/mipt_cxx1/04_merged.md
Normal file
338
pages/mipt_cxx1/04_merged.md
Normal 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
196
pages/mipt_cxx1/06.md
Normal 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
324
pages/mipt_cxx1/08.md
Normal 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
307
pages/mipt_cxx1/09.md
Normal 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
115
pages/mipt_cxx1/11.md
Normal 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
22
pages/mipt_cxx1/Makefile
Normal 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
|
18
pages/mipt_cxx1/generate_index.sh
Executable file
18
pages/mipt_cxx1/generate_index.sh
Executable 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
|
Loading…
x
Reference in New Issue
Block a user