pandoc-pages/pages/mipt_cxx1/03.md

229 lines
12 KiB
Markdown
Raw Permalink Normal View History

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