Add MIPT CXX lecture notes
This commit is contained in:
parent
3bff372afe
commit
9f11ec59c4
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, потому что она работает как стек, когда программа входит в функцию,
|
||||
на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости
|
||||
они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на
|
||||
стек в произвольном порядке и делать промежутки.
|
||||
|
||||
При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как
|
||||
мы выйдем из функции.
|
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: index.html
|
||||
@mkdir -p $(@D)
|
||||
cp $< $@
|
||||
|
||||
index: $(PREFIX)/index.html
|
||||
|
||||
.PHONY: all index
|
0
pages/mipt_cxx1/index.html
Normal file
0
pages/mipt_cxx1/index.html
Normal file
Loading…
Reference in New Issue
Block a user