373 lines
18 KiB
Markdown
373 lines
18 KiB
Markdown
|
В прошлый раз мы остановились на пункте
|
|||
|
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;
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
Нельзя прыгать с пропуском инициализации.
|