229 lines
12 KiB
Markdown
229 lines
12 KiB
Markdown
# 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, потому что она работает как стек, когда программа входит в функцию,
|
||
на стек кладутся аргументы, далее другие локальные переменные. После выхода из функции/области видимости
|
||
они извлекаются из стека, и.т.д. В рамках одной локальной области компилятор может класть переменные на
|
||
стек в произвольном порядке и делать промежутки.
|
||
|
||
При входе в функцию также кладется "адрес возврата", откуда надо продолжить выполнять код, после того как
|
||
мы выйдем из функции.
|