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