# 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 #include int main() { std::vector v(10); v[50'000] = 1; } ``` Возникает из-за обращения к памяти, которую мы не имеем права читать. Floating point exception (FPE), например, деление на ноль, идёт от процессора. Aborted, вызов функции `abort()` из libc, её вызов приводит к аварийному завершению. **Неопреденное поведение** или undefined behaviour -- некорректный код, который приводит к "непредсказуемым" последствиям во время выполнения, в том смысле, что компилятор не может дать никаких гарантий. ```C++ std::vector 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 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, потому что она работает как стек, когда программа входит в функцию, на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на стек в произвольном порядке и делать промежутки. При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как мы выйдем из функции.