Rust - язык без стандарта, свой компилятор языка без стандарта создать невозможно, тк то, как работает компилятор разработчика языка и является фактически стандартом.
int a, b = 5; // переменная a не инициализируется
Инициализуется только переменные те у которых стоит
=
Локальные переменные без инициализации содержат мусор (рандомное значение), тк под локальные переменные место на стеке выделяется, но ничем не заполняется, может случиться 0, может не 0, из разных запусков - может случиться разное, из разных компиляций (release/debug) может случиться разное. Вообще, ошибки обращения к неиницилизированным переменным - это очень частые ошибки.
Как искать обращение к неинициализированным переменным
//
и///
- это комментарий, начиная от этого места до конца строки (до перевода строки), компилятор игнорирует, можно писать что угодно
если пишите комментарии на русском, надо посмотреть, чтобы кодировка файла в среде (текстовом редакторе) была
UTF-8
-многие вещи в C/C++ имеют результат, некое значение, например операция =
(присваивания) тоже имеет результат, почему можно писать a = b = c
? (что это значит?)
то есть операция
=
вместо того, чтобы просто присваивать ещё и отдает значение которое она присвоила.
a = b;
, a
, слево от =
- то куда присваиваем, b
, справо от =
- значение, которое мы присваиваем, операция присваивания всегда левостороняя.
a = b = c
, тоже самое a = (b = c);
- всегда читается справа налево!
В значение нельзя записать значение, можно только в обьект (переменную) Разница между объектом и значением видна только еcли мы туда пишем, если нет - нет
В си++ - в результате операции присваивания =
отдается обьект, то есть (a = b) = c;
будет эквивалентом a = b; a = c;
, но a = b = c
всё равно юудет читаться как в си, справа налево!
Кажется, что это довольно глупая операция, ведь значение
b
сразу затрётся, но это еслиa
иb
простые типы, а в C++ всё может быть гораздо хитрее. Можно переобозначить операцию=
, и он может выполнять совсем нетривиальные вещи.
Надо поподробнее остановиться на a++
(постфиксный инкремент) и ++a
(префиксный инкремент)
С точки зрения
a
разницы никакой (в си), а вот отдают они разное значение.
a++;
результат - значение a
до увеличения
++a;
результат - значение a
после увеличения
Класс - обьединение переменных и функций, как структуры в си, только ещё со своими функциями, методами.
В C++ если обьекты обычные (простые типы) - разници нет никакой, если это какие-то классы, то для них могут быть переопредленны операторы
a++
или++a
, и они могут означать разное. В плюсахa
может оказаться классом, а если так -a++
будет создавать временный обьект, копию класса, что может сильно замедлять программу, например цикл. В двух словах: для сложных объектов в C++ вполне возможно, что будет оверхед (i++
может создать временный объект), для простых int'ов – нет, для отдельно стоящегоi++
точно такой же код, как и++i
.
+, =, ++ и прочие операторы в C++ можно перопределить!
(нельзя переопределить .
и :
)
Один и тот же символ может означать разные операции, в зависимости от контекста, например -
, может быть вычитанием, когда он используется в бинарном контексте, и может быть инвертированием знака, когда используется в унарном контексте.
У *
вообще три разных контекста, бинарный, унарный и при создании переменной.
Хитрых операций много, а вот символов нет, в те времена (написания си) даже большие/маленькие буквы не везде различали
У операций есть приоритет! При одинаковом приоритете операции выполняются в нужном порядке, например для +-
это слева направо, для =
справа налево. То есть порядок зависит ещё и от контекста. (правила - Operator Precedence
-
a+++++a
что это означает в си?
(a++)+(++a)
, НЕ((a++)++) + a
потому что в си операция++
отдает значение, а не обьект!
В C++ i++
и ++i
могут вернут объект
Не надо писать такc = (a++)+(++a);
Нельзя в одной строке менять 2 раза переменную, выдаст неопределенное значение (зависит от порядка в моменте), тк порядок выполнения в си неопределён!
Никогда не меняйте в выражение дважды одну переменную!
Для большинства бинарных операций порядок выполнения не определен
Это справедливо и вызова функции нескольких аргументов: для аргументов функции, никто не обещает в каком порядке посчитаются аргументы функции, гарантируют, что они посчитаются перед непосредственном вызовом функции
c = f() + g();
никто не обещает какая функция будет выполнена первой!
В си все операции происходят без изменения типов (делим целое на целое - получаем целое)
При делении целового на дробное - целое будет приведено к дробному, рузультат дробный
Округление к 0 (в отличие от Python)
Правило: a/b * b + a%b == a
Проверка на нечетность
a%2 == 1
только для полож
a%2 != 0
для любых
При деление на 0 - Undefined Behavior (Может быть всё, что угодно)
Бинарная
a & b
a = 12 & 6; // a == 4
Таблица истинности:
Бинарная
a | b
a = 12 | 6; // a == 14
Таблица истинности:
Унарная
~a
a = ~12; // a == 3
Таблица истинности:
Бинарная
a ^ b
a = 12 ^ 6; // a == 6
Таблица истинности:
Бинарная
`a = ~(12 & 6); //a == 3
Таблица истинности:
Бинарная
`a = ~(12 | 6); //a == 1
Таблица истинности:
Подробнее про побитывые операции
Проверка на нечетность
(a&1) == 1
(если без "()" будет a&(1 == 1))
К сожалению в стандарте приоритеты побитывых операций не те - которые хотелось бы, по этому можно часто встретить скобки рядом с такими операциями
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
Подробнее про логические операции
!!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
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
в си имеет смысл только тут)
signed
можно применять к любым целочисленным переменным (но все кроме char
по умолчанию - signed
)_Bool
занимает 1 БАЙТ (минимально адресуемая ячейка памяти), можно написать bool
подключив заголовочный файл #include <stdbool.h>
(также получите true
и false
)
В стандарте Си нет ничего про
true
иfalse
, только про (1
и0
)
(создание синонимов типов)
отличается от
define
тем, что это не простая текстовая замена, а создание нового типа, например:
К
typedef
новому типу (синониму) нельзя применить никакие модификаторы (unsigned
,long
и др.)
Применяется только по целочисленным переменным
Модулярная арифметика (целочисленная) по стандарту определенна только в беззнаковых числах
Сдвиг налево <<
все битики сдвинуть на один влево, старший (самый левый) стереть, младший (самый правый) в 0, - эквивалентен умножению на 2
Сдвиг направо >>
все битики сдвинуть на один вправо, младший (самый правый) стереть, сдвигов >>
(направо) есть 2:
Так происходит из-за представления чисел в компьютере
То есть если мы напишем
int a = -140; a /= 8;
- компилятор не имеет права съоптимизировать это доa >>= 3;
, тк округление у>>
идет вниз!
В стандарте операция побитового сдвига направо для отрицательных чисел не определенна! (в 23 стандарте скорее всего поправять, тк признают числа - в качестве формы дополнение до 2х
), то есть компилятор может посмотреть что здесь есть Undefined Behavior и просто выкинуть эту строчку.
Знаковые переполнение в стандарте неопределенно:
Скомпилировав это под
gcc
с оптимизациями можно легко получить бесконечный цикл!
Беззнаковое переполнение в стандарте четко определенно:
в дополнение до 2x вообще не будет проблем, там просто присвоятся все биты
Кроме деления на 0 есть ещё одни грабли, Int.MIN / -1
- это Undefined Behavior. Происходит, тк диапазон значений (в дополнение до 2х) больше на 1 в отрицательных значениях.
-Int.MIN == Int.MIN
Модуль int
(В стандарте знаковое переполнение не определенно! a = -a;
- эквивалент инверт биты + 1)
Правильно
Кодовая страница - обычно один байт, от 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
Пути можно писать через
/
, и их НЕ нужно удваивать, это не спец. символ. (в линуксе только/
в путях), рекомендуется так писать вinclude
\
спец символ везде, не только вprintf/scanf
, если поставить\
перед концом строки - значит перевода строки не существует
printf("a = ", a);
- что выведется на экран?-выведется
"a = "
Чтобы вывести значение какой-либо перемменной через printf
есть специальный спецсимвол:
%
- спецсимвол printf
(есть и другие спец. сиволы чтобы печатать)
если
\
спецсимвол во всех строках в си, то%
спец символ только в семействе функцийscanf/printf
Чтобы напечать символ \
нужно его удвоить - printf("\\");
Чтобы напечать символ %
нужно его удвоить - printf("%%");
В чем различие с
std::cin
иstd::cout
?
- если случается что-то сложное, вывести это через плюсовые функции будет гораздо сложнее, то есть порог входа в плюсовые ниже, но если освоить сишные - будет потом гораздо проще
- Плюсовые функции ооочень много жрут времени, функции вывода и так очень медлянные, а под плюсовыми скрывается ещё больший ужас
cin
, cout
самые медленные
scanf
, printf
сильно быстрее
ios_base cin.tie(0); cout.tie(0);
ещё быстрее
двоичный ввод/вывод - самое быстрое
В printf/scanf
четко указывается тип значения/переменной которая будет печататься/считываться (через %
), если написать переменную другого типа - она будет интерпретированна как переменная типа которого вы указали через %
.
Чем
%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, Скаков