# 02 лекция - C, подробно
> [TOC]
## За жизнь
Rust - язык без стандарта, свой компилятор языка без стандарта создать невозможно, тк то, как работает компилятор разработчика языка и является фактически стандартом.
## Переменные и Операторы
`int a, b = 5; // переменная a не инициализируется`
> Инициализуется только переменные те у которых стоит `=`
Локальные переменные без инициализации содержат мусор (рандомное значение), тк под локальные переменные место на стеке выделяется, но ничем не заполняется, может случиться 0, может не 0, из разных запусков - может случиться разное, из разных компиляций (release/debug) может случиться разное.
Вообще, ошибки обращения к неиницилизированным переменным - это очень частые ошибки.
Как искать обращение к неинициализированным переменным
1) Статические ситемы (анализаторы кода в онлайне, привязан к среде разроботки, замедляет её)
2) Динамические ситемы (может быть не привязан к среде, проверка при запуске под ним, бешенное замедление работы программы при запуске под ним, для больших программ можно не дождаться)
например [Intel Inspector](https://www.intel.com/content/www/us/en/developer/tools/oneapi/inspector.html)
---
```c
int main(void)
{
int a, b = 0, c; // b - инициализируется
// Выделилось место на стеке для a, b и c
return 0;
}
```
> `//` и `///` - это комментарий, начиная от этого места до конца строки (до перевода строки), компилятор игнорирует, можно писать что угодно
> если пишите комментарии на русском, надо посмотреть, чтобы кодировка файла в среде (текстовом редакторе) была `UTF-8`
```c
a = b + 3; //a == b + 3
a += b; // полный синоним a = a + b; //результат a == b + 3 + b
// (как и многие другие бинарные операции просто короткая запись)
// выдает результат в си значение a, после операции
```
---
-многие вещи в C/C++ имеют результат, некое значение, например операция `=` (присваивания) тоже имеет результат, почему можно писать `a = b = c`? (что это значит?)
```c
a = b = c; // эквивалент a = (b = c) или b = c; a = c;
```
> то есть операция `=` вместо того, чтобы просто присваивать ещё и отдает значение которое она присвоила.
`a = b;`, `a`, слево от `=` - то куда присваиваем, `b`, справо от `=` - значение, которое мы присваиваем, операция присваивания всегда левостороняя.
`a = b = c`, тоже самое `a = (b = c);` - всегда читается справа налево!
```
(a = b) = c; // в си НЕ СКОМПИЛИРУЕТСЯ
// НЕльзя в значение записать значение, например 5 = 3;
```
> В значение нельзя записать значение, можно только в обьект (переменную)
> Разница между объектом и значением видна только еcли мы туда пишем, если нет - нет
В си++ - в результате операции присваивания `=` отдается обьект, то есть `(a = b) = c;` будет эквивалентом `a = b; a = c;`, но `a = b = c` всё равно юудет читаться как в си, справа налево!
> Кажется, что это довольно глупая операция, ведь значение `b` сразу затрётся, но это если `a` и `b` простые типы, а в C++ всё может быть гораздо хитрее. Можно переобозначить операцию `=`, и он может выполнять совсем нетривиальные вещи.
---
Надо поподробнее остановиться на `a++` (постфиксный инкремент) и `++a` (префиксный инкремент)
```c
a++; // - постфиксная запись a = a + 1;
++a; // - префиксная запись a = a + 1;
```
> С точки зрения `a` разницы никакой (в си), а вот отдают они разное значение.
`a++;` результат - значение `a` до увеличения\
`++a;` результат - значение `a` после увеличения
```c
a = 2;
b = a++; // a=3, b=2 // сначала отдает значение, затем увеличивает
```
```c
a = 2;
b = ++a; // a=3, b=3 // сначала увеличивает, затем отдает значение
```
> Класс - обьединение переменных и функций, как структуры в си, только ещё со своими функциями, методами.
> В C++ если обьекты обычные (простые типы) - разници нет никакой, если это какие-то классы, то для них могут быть переопредленны операторы `a++` или `++a`, и они могут означать разное. В плюсах `a` может оказаться классом, а если так - `a++` будет создавать временный обьект, копию класса, что может сильно замедлять программу, например цикл. В двух словах: для сложных объектов в C++ вполне возможно, что будет оверхед (`i++` может создать временный объект), для простых int'ов – нет, для отдельно стоящего `i++` точно такой же код, как и `++i`.
**+, =, ++ и прочие операторы в C++ можно перопределить!**
(нельзя переопределить `.` и `:`)
### На что обратить внимание?
Один и тот же символ может означать разные операции, в зависимости от контекста, например `-`, может быть вычитанием, когда он используется в бинарном контексте, и может быть инвертированием знака, когда используется в унарном контексте.
```c
c = a - b; //вычитание, бинарный контекст
a = -a; //инвертирование знака, унарный контекст
```
У `*` вообще три разных контекста, бинарный, унарный и при создании переменной.
```c
c = a * b; //умножение, бинарный контекст
с = *ptr; //разыменовка, унарный контекст
int *d; //создание указателя, контекст при создании переменной
```
Хитрых операций много, а вот символов нет, в те времена (написания си) даже большие/маленькие буквы не везде различали
**У операций есть приоритет!** При одинаковом приоритете операции выполняются в нужном порядке, например для `+-` это слева направо, для `=` справа налево. То есть порядок зависит ещё и от контекста. (правила - [Operator Precedence](https://en.cppreference.com/w/cpp/language/operator_precedence)
>-`a+++++a` что это означает в си?
>
>`(a++)+(++a)`, НЕ `((a++)++) + a` потому что в си операция `++` отдает значение, а не обьект!
В C++ `i++` и `++i` могут вернут объект
Не надо писать так`c = (a++)+(++a);` Нельзя в одной строке менять 2 раза переменную, выдаст неопределенное значение (зависит от порядка в моменте), тк порядок выполнения в си неопределён!
**Никогда не меняйте в выражение дважды одну переменную!**
Для большинства бинарных операций порядок выполнения не определен
Это справедливо и вызова функции нескольких аргументов: для аргументов функции, никто не обещает в каком порядке посчитаются аргументы функции, гарантируют, что они посчитаются перед непосредственном вызовом функции
`c = f() + g();` никто не обещает какая функция будет выполнена первой!
```c
int sum(int a, int b)
{
return a + b;
}
int print(int a)
{
printf("%d\n", a);
return a;
}
int main(void)
{
int result = sum(print(4), print(3)); //result == 7
// неопределенно в какой порядке напечатается на экран
// неопределенно вызовется сначала print(4) или print(3)
// гарантируется выполнение print(4) и print(3) до непосредственного вызова sum()
int result_2 = print(4) + print(3);
// в такой записи порядок тоже неопределён
// гарантируется выполнение print(4) и print(3) до сложения
}
```
## Деление
> В си все операции происходят без изменения типов (делим целое на целое - получаем целое)
> При делении целового на дробное - целое будет приведено к дробному, рузультат дробный
**Округление к 0** (в отличие от Python)
```
int a = 7/3; // a = 2
int a = -7/3; // a = -2
```
**Правило: `a/b * b + a%b == a`**
```
int a = 7%3; // a = 1
int a = -7%3; // a = -1
```
Проверка на нечетность
`a%2 == 1` **только для полож**\
`a%2 != 0` **для любых**
При деление на 0 - [Undefined Behavior](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5) (Может быть всё, что угодно)
## Побитовые операции
### Побитовое и (AND)
Бинарная
`a & b`
`a = 12 & 6; // a == 4`
```
1100 //== 12
&
0110 //== 6
==
0100 //== 4
----
3210 - степень двойки за которое овечает бит
----
8421 - число за которое отвечает бит
----
```
Таблица истинности:
```
0 0 -> 0
0 1 -> 0
1 0 -> 0
1 1 -> 1
```
### Побитовое или (OR)
Бинарная
`a | b`
`a = 12 | 6; // a == 14`
```
1100 //== 12
|
0110 //== 6
==
1110 //== 14
```
Таблица истинности:
```
0 0 -> 0
0 1 -> 1
1 0 -> 1
1 1 -> 1
```
### Побитовое отрицание (NOT)
Унарная
`~a`
`a = ~12; // a == 3`
```
~
1100 //== 12
=
0011 //== 3
```
Таблица истинности:
```
0 -> 1
1 -> 1
```
### Побитовое Исключающее ИЛИ (XOR)
Бинарная
`a ^ b`
`a = 12 ^ 6; // a == 6`
```
1100 //== 12
^
0110 //== 6
==
0110 //== 6
```
Таблица истинности:
```
0 0 -> 0
0 1 -> 1
1 0 -> 1
1 1 -> 0
```
### Штрих Шеффера (NAND - NOT AND - НЕ И)
Бинарная
`a = ~(12 & 6); //a == 3
Таблица истинности:
```
0 0 -> 1
0 1 -> 1
1 0 -> 1
1 1 -> 0
```
### Стрелка Пирса (NOR - NOT OR - НЕ ИЛИ)
Бинарная
`a = ~(12 | 6); //a == 1
```
1100 //== 12
^
0110 //== 6
==
0110 //== 6
```
Таблица истинности:
```
0 0 -> 1
0 1 -> 0
1 0 -> 0
1 1 -> 0
```
[Подробнее про побитывые операции](https://www.geeksforgeeks.org/bitwise-operators-in-c-cpp)
Проверка на нечетность
`(a&1) == 1` (если без "()" будет a&(1 == 1))
```
1100 == 12
&
0001 == 1
=
0000 == 0
```
```
1101 == 13
&
0001 == 1
=
0001 == 1
```
> К сожалению в стандарте приоритеты побитывых операций не те - которые хотелось бы, по этому можно часто встретить скобки рядом с такими операциями
## Логические операции
### Правило конвертирования:
* 0 - false, не 0 - true
* true - 1, false - 0
### Логическое и
`a = 0 && 0; // a == 0`
`a = 0 && 6; // a == 0`
`a = 12 && 0; // a == 0`
`a = 12 && 6; // a == 1`
### Логическое или
`a = 0 || 0; // a == 0`
`a = 0 || 12; // a == 1`
`a = 6 || 0; // a == 1`
`a = 6 || 12; // a == 1`
### Логическое не
`a = !true; // a == false`
[Подробнее про логические операции](en.cppreference.com/w/cpp/language/operator_logical)
### Приведение к bool
`!!a` (Если a = 0, то 0. есть a!=0, то 1) (два раза логическое не)
### Порядок вычисления аргументов
*Сокращенное вычисление*
Логическое и/или имеют определенный порядок вычисления аргументов!
При логических операциях гарантируется:\
* `f() && g()` выполнение f вначале и проверка f не false\
(***если f() - FALSE, то `g()` считаться не будет! (`g()` не будет вызванно)***)
* `f() || g()` выполнение f вначале и проверка f не true\
(***если f() - TRUE, то `g()` считаться не будет! (`g()` не будет вызванно)***)
## Целочисленные простые типы
- В стандарте `int` определен как не хуже чем - `(-32767 ... 32767)` (`2^16-1`), именно в десятичной системе счисления, такой диапазон, не важно сколько битов в одном байте\
В реальности `int` соответвует диапазону - `-2^32 ... 2^32-1`
- Модификатор (типа) `short int` по стандарту не шире `int` (32 бита)\
по факту - 16 бит
- Модификатор (типа) `long int` по стандарту не меньше `int` (32/64 бита)\
по факту - 32/64 бита (Козлы тут Microsoft, под win при компиляции под 32-64битную программу `long int` соответсвует 32 битам, в Linux - при компиляции под 32 - 32, под 64 - 64)
- Модификатор (типа) `long long int` по стандарту не меньше `long int` (64 бита)\
по факту - 64 бита
> "long long long" is too long for gcc
---
- Модификатор (типa) `unsigned int`, беззнаковое `int`, может применятся с любыми другими\
(по факту (для `int`) - `0 ... 2^32 - 1`)
> `int` можно опустить в любых модификаторах для `int`
---
- `char`, целочисленный тип, может быть знаковым или нет (`-128..127 / 0..255`, 1 байт или 8 бит)\
`unsigned char` -> `0..255`\
`signed char` -> `-128..127` (ключивое слово `signed` в си имеет смысл только тут)
* Значения от 0 до 127 полностью кодируют ASCII, вторую часть бита занимает текущая кодовая страница системы.
* `signed` можно применять к любым целочисленным переменным (но все кроме `char` по умолчанию - `signed`)
---
- `_Bool` занимает **1 БАЙТ** (минимально адресуемая ячейка памяти), можно написать `bool` подключив заголовочный файл `#include <stdbool.h>` (также получите `true` и `false`)
* В си изначально не было bool, true и false - из-за ненадобности
> В стандарте Си нет ничего про `true` и `false`, только про (`1` и `0`)
### Создание своих типов
(создание синонимов типов)
```c
typedef unsigned int uint; //задать тип с названием uint, и смыслом unsigned int
uint a, b; //можно использовать как полный синоним unsigned int
```
> отличается от `define` тем, что это не простая текстовая замена, а создание нового типа, например:
```c=
#define X int // X заменяет на int
unsigned X a; //скомпилируется
//-------------------------------------//
typedef int y;
//unsigned y b; //не скомпилируется
```
> К `typedef` новому типу (синониму) нельзя применить никакие модификаторы (`unsigned`, `long` и др.)
## Операции сдвига
Применяется только по целочисленным переменным
> Модулярная арифметика (целочисленная) по стандарту определенна только в беззнаковых числах
Сдвиг налево `<<` все битики сдвинуть на один влево, старший (самый левый) стереть, младший (самый правый) в 0, - **эквивалентен умножению на 2**
* В беззнаковых числах гарантируется модулярная арифметика
* В знаковых числах ничего не гарантируется модулярная арифметика
Сдвиг направо `>>` все битики сдвинуть на один вправо, младший (самый правый) стереть, сдвигов `>>` (направо) есть 2:
1) Логический сдвиг направо - эквивалентный беззнаковому делению на 2 с округ вниз (беззнаковые числа) - **старший бит (самый правый) делает 0**
2) Арифмитический сдвиг направо - эквивалентный знаковому делению на 2 с округ вниз (знаковые числа) (проблема в том, что первый бит так или иначе используют для знака и непонятно куда его девать, сдвигать или нет) - **старший бит (самый правый) делает оставляет**
Так происходит из-за представления чисел в компьютере
```c
5 >> 1; // == 2 //101 >> 1 = 10 == 2
-5 >> 1; // == -3 //11111011 >> 1 = 10111101 == -3 (Арифметический сдвиг)
```
---
> То есть если мы напишем `int a = -140; a /= 8;` - компилятор не имеет права съоптимизировать это до `a >>= 3;`, тк округление у `>>` идет вниз!
В стандарте операция побитового сдвига направо для отрицательных чисел не определенна! (в 23 стандарте скорее всего поправять, тк признают числа - в качестве формы `дополнение до 2х`), то есть компилятор может посмотреть что здесь есть [Undefined Behavior](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5) и просто выкинуть эту строчку.
### Переполнение
Знаковые переполнение в стандарте неопределенно:
```c
for(int i = 5; i > 3; i++)
{
///code...
}
```
> Скомпилировав это под `gcc` с оптимизациями можно легко получить бесконечный цикл!
---
Беззнаковое переполнение в стандарте четко определенно:
```c
unsigned int a = -1; //эквивалент = (-1)%(2^32), по математическому модулю
```
> в дополнение до 2x вообще не будет проблем, там просто присвоятся все биты
---
Кроме деления на 0 есть ещё одни грабли, `Int.MIN / -1` - это [Undefined Behavior](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D1%91%D0%BD%D0%BD%D0%BE%D0%B5_%D0%BF%D0%BE%D0%B2%D0%B5%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5). Происходит, тк диапазон значений (в дополнение до 2х) больше на 1 в отрицательных значениях.\
`-Int.MIN == Int.MIN`
**Модуль int**
```c
if (a < 0)
a = -a; // a может быть Int.MIN
```
(В стандарте знаковое переполнение не определенно! `a = -a;` - эквивалент инверт биты + 1)
```c
-128 == 1000 0000
//инверт биты
0111 1111
// + 1
1000 0000 == -128
```
```c
0 == 0000 0000
//инверт биты
1111 1111
// + 1
0000 0000 == 0
```
```c
-127 == 1000 0001
//инверт биты
0111 1110
// + 1
0111 1111 == 127
```
**Правильно**
```c
int a;
unsigned int b;
if (a < 0)
{
b = a;
b = -b; //эквивалент инверт биты и +1,
//только без Undefined Behavior
//(случался из-за переполнения)
}
else
{
b = a;
}
```
```c
-128 == 1000 0000 //в форме доп до 2х
//инверт биты
0111 1111
// + 1
1000 0000 == 128 //напрямую
```
## Ввод-вывод
Кодовая страница - обычно один байт, от 0 до 128 - ASCII, вторая половина (-128 до 0 или 128 до 255) - набор символов по текущей кодовой странице (обычно второй язык) (cp-866/cp-1256/ - русские, cp-65001 - UTF-8)
`\n` - символ перевода строки\
`\t` - символ табуляции\
`\` - спецсимвол строк (`char *`) си, который используется для спец символов вроде перевода строки, чтобы написать просто символ обратного слеша - `\\`
Если писать пути в коде программы - нужно удваивать `\\`
Путь к файлу `C:\Users\My\Desktop` - выдаст ошибку при компиляции (непонятно что такое `\U`, `\M` и `\D`)
Путь к файлу на си `C:\\Users\\My\\Desktop`
либо `C:/Users/My/Desktop`
```c=
// char path[] = "C:\the_programm\no_response.exe"; //НЕправильно
// не выдаст ошибку при компиляции
// тк \t - символ табуляции, \n - символ перевода строки
char path[] = "C:\\the_programm\\no_response.exe"; // правильно
char path[] = "C:/the_programm/no_response.exe"; // или так
```
> Пути можно писать через `/`, и их НЕ нужно удваивать, это не спец. символ. (в линуксе только `/` в путях), рекомендуется так писать в `include`
> `\` спец символ везде, не только в `printf/scanf`, если поставить `\` перед концом строки - значит перевода строки не существует
> `printf("a = ", a);` - что выведется на экран?
>
> -выведется `"a = "`
Чтобы вывести значение какой-либо перемменной через `printf` есть специальный спецсимвол:
`%` - спецсимвол `printf` (есть и другие спец. сиволы чтобы печатать)
> если `\` спецсимвол во всех строках в си, то `%` спец символ только в семействе функций `scanf/printf`
Чтобы напечать символ `\` нужно его удвоить - `printf("\\");`\
Чтобы напечать символ `%` нужно его удвоить - `printf("%%");`
> В чем различие с `std::cin` и `std::cout`?
>
> 1) если случается что-то сложное, вывести это через плюсовые функции будет гораздо сложнее, то есть порог входа в плюсовые ниже, но если освоить сишные - будет потом гораздо проще
> 2) Плюсовые функции ооочень много жрут времени, функции вывода и так очень медлянные, а под плюсовыми скрывается ещё больший ужас
`cin`, `cout` самые медленные\
`scanf`, `printf` сильно быстрее\
`ios_base cin.tie(0); cout.tie(0);` ещё быстрее\
двоичный ввод/вывод - самое быстрое
В `printf/scanf` четко указывается тип значения/переменной которая будет печататься/считываться (через `%`), если написать переменную другого типа - она будет интерпретированна как переменная типа которого вы указали через `%`.
```c
int a = 5, b = 4, c;
c = a + b;
printf("a + b = %i", c); //выводит переменную типа int
```
> Чем `%i` и `%d` отличается от друг от друга?
>
> В `printf` - ничем, выводит `int`, в `scanf` `%i` может считать ещё значения в 8/16-ричной системе счисления, надо поставить `0` или `0x` перед числом соответственно (`%d` - считывает только десятичные числа) (`0123` - восмиричное число `83`)
Сишные функции - это функции с переменном числом аргументов, так называемые `vararg` функции.
`scanf` возвращает кол-во прочитанных переменных (прочитанных `%`).\
`printf` возвращает кол-во фактически напечатанных **символов**.\
К вопросу о том, как проверить считалось/напечаталось ли что-то на самом деле.
`scanf` пропускает пробелы при считывании (всё что считается пробельным символов в си - `' ','\n', '\t'`)
# Оглавление
Full - C/C++, 2 лекция, 19.02.2022, Скаков
> [TOC]