# 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]