# 05 лекция - C, подробно > [TOC] ## Массивы ### Различия массива массивов и матриц - Матрица - `int m[5][10]` - доступ к элементу `m[2][9]`, расшифровывается как `m[2*10 + 9]` - Массив массивов `int **a` - доступ к элементу `a[1][5]`, расшифровывается как: в 1й ячейке массива лежит указатель на 1й строку, от него берется 5й элемент (2 обращения в память) > При одинаковом синтаксисе доступа к элементу происходят совершенно разные вещи Под малое количество данных лучше использовать статический массив (на стеке), под большое - динамический (на куче). Если размер массива заранее неизвестен, то лучше всего завести маленький статический массив и если его не хватит - выделить память динамически: - При малом размере данных - нет обращения в операционную систему - При большом обьеме данных - маленький статический массив погрешность > Место на стеке относительно бесплатное, по сравнению с кучей ## Указатели ```c int arr[10]; // статический массив на 10 элементов //Можно узнать размер этого СТАТИЧЕСКОГО массива sizeof(arr)/sizeof(arr[0]); // размер arr: 40/4 = 10 ``` > `arr` - это указатель на начало статического массива в памяти ```c arr + 1; //это указатель сдвинутый на один элемент //(сдвинется на размер типа указателя в байтах) arr[3]; //одно и тоже *(arr + 3); //одно и тоже ``` --- ```c int *a, *b; int c, d[10]; a = &c; //указатель на переменную c b = &d[3]; //указатель на 3й элемент массива d b = d + 3; //тоже самое, на 3й элемент массива d *b = 2; //d[3] = 2 *(b+1) = 5; //d[3 + 1] = 5 ``` Двигать указатели имеет смысл только в случает, когда в памяти лежат подряд много однотипных данных (например массив) Проход по массиву: (Указатель за границы массива - это НЕ всегда плохо) ```c a = &d[9]+1; //указатель на следующий байт после d // не обращаемся сквозь него for(;b < a; b++) //обрабатывать массив до его конца { //*b - доступ к элементу } ``` Сранение указателей имеет смысл только если они сделанны от одного обьекта в памяти (например массив) (тк разные обьекты могут быть разбросанны хрен знает как по памяти) --- ```c int arr[10]; int *a = &arr[1], *b = &arr[5]; a - b; // 4 - расстояние между указателями a + b; // что-то бредовое a * 2 // что-то бредовое ``` --- ```c void *v; // указатель на неопределенный тип ``` > Такой указатель (`void *`) например возвращает `malloc`. Свойства - Такой указатель **нельзя ** сдвинуть или разьменовать! Потому что не понятно на сколько байт нужно сдвигать и как разьемновывать тк `void` не существует. Можно сконвертировать и затем двигать и разьменовывать. - Такие указатели можно сравнивать. - Указатель на любой другой тип можно присвоить в указатель на `void` - Указатель на `void` можно присвоить в любой другой указатель (ТОЛЬКО В СИ, из-за более сильной статической типиззации в языке си++, можно только с кастом) - В си++ `new/delete` является типизированным выделением памяти, так что к нему не нужно применять каст, однако `new/delete` это **не функции**, это **операторы **- часть языка! --- > Кажется что более сильная статическая типизация противоречит концепциям ООП, действительно, *так и есть*, НО в си++ динамическая типизация обьектов (или **Полиморфизм**) достигается с помощью других инструментов, в том числе классов и наследования --- Нулевой адрес обычно никуда не сопостовляют. (`NULL`) Обычные функции выделения памяти никогда не выделяют обьекты по адресу `0`, и очень часто возвращают `0` (`NULL`) в случае ошибки Ещё иногда в параметры функции можно отдать `NULL` или адрес обьекта. В случае `NULL` функция поймет, что в этой обьект не нужно писать информацию Это удобно, тк есть значение, которое означает ошибку (указатель на несущетвующий, отсутвующий обьект) > `#define NULL (0)` - это `NULL`. Сделали так для удобства чтения прграммы > В си++ есть `nullptr`, тоже ассоциирован с 0, но хитрее (несовместим с `int`). Существует для извращенных перегрузок ```c int *ptr = 0; //Можно, 0 == NULL //int *ptr = 1; //Не скомпилируется ``` `int` **нельзя ** присвоить в `int *` Указатель НЕ совместимы между собой. Даже указатель на `int` не совместим с указателем на `unsigned int`. То есть указатель в момент компиляции тесно связан с **полным типом** (*не только размером типа*). Несмотря на то, что в памяти храниться только адрес. --- ### Про память - Если **создаем **указатель за границы массива, то **ничего не происходит**: ```c int arr[10]; int *ptr = arr + 1000; ``` - Если мы **читаем** сквозь него `int b = *ptr;` , то в случае **когда там ЕСТЬ память** - **мы получаем мусор** - Если мы **пишем** сквозь него `*ptr = 5;`, или там **нет памяти**, например `NULL` (см прошлый пункт) - **мы получаем** [Undefined behavior](https://en.cppreference.com/w/cpp/language/ub), полностью **неопределенное** поведение! (Программа может упасть или магическим образом измениться) Сквозь `NULL` нельзя ни читать ни писать, тк там нет памяти. > При обращении по адресу где нет памяти у процессора случается исключение, он переходит на обработчик исключений, его настраивает операционная система. Обычно самое простое и понятное решение для операционной системы - прибить процесс где вызвалось исключение. > В современных операционных системах работает механизм виртуальной памяти, где у каждого процесса своё адресное пространство в памяти. Сопоставлением логических адресов (указателей) с физичекой памятью занимается OC и оно своё у каждого процесса. > Указатели в си - это не физические адресса в памяти! > Сваливаемся мы тогда, когда мы обращаемся к той части памяти, для которого адреса не сопоставленно никакое место в физической оперативке (в табличке трансляции адресов). Данное сопоставление выборочное, то есть сопоставленны далеко не все 2^64 адреса (при 64 битной программе) > При динамическом выделении памяти операционаая система: > > - Выделяет нам адреса > > - Сопостовляет этим адресам место в физической оперативке > > Иногда укзатели шифруют для защиты. Смысл таких защит в том, чтобы если атакующий всё же нашел дырку в программе и переполнил стек - чтобы программа просто упала, а сделала то действие, которые хочет атакующий --- ### Про массивы Одномерные массивы и указатели - штуки довольно взаимозаменяемые: ```c int arr[10]; int *ptr = &arr[0]; //или int *ptr = arr; ``` Теперь `ptr` и `arr` ПОЧТИ одно и тоже: Одинаковое поведение: - доступ одинаковый `arr[4] == ptr[4]` - значение одинаковое `arr == ptr` --- Разное поведение: - разный смысл: - Адрес `ptr` будет указывать на ячейку в памяти (`&ptr != ptr`) (где храниться наш указатель) - Адрес `arr` будет равен адресу массива `arr` (`&arr == arr`), но у них будут разные типы: `arr` - тип массив, `&arr` тип адрес массива - Разные при создании - указателя он указывает на случайное место в памяти (в нём мусор) - У массива память сразу выделена - Менять - указатель можно (например `ptr = NULL;`) - массив НЕЛЬЗЯ (так не скомпилируется `//arr = NULL;`) - Разный размер - `sizeof(ptr)` - даст 32 или 64 размер указателя (в зависимости от битности программы) - `sizeof(arr)` - даст 40, размер массива в байтах **Указатель - это отдельная ячейка памяти, которая хранит адрес** **Массив - неявный неизменяемый указатель на начало области в памяти, которая сама выделяется и освобождается** (происходит это на стеке) --- Передать массив в функцию (через аргументы) **нельзя**! При передаче он превратиться в **обычный** указатель. Для честного копирования (передачи) массива в функцию его нужно обернуть в структуру, но такой метод всё равно займет `O(n)` времени и памяти ## Циклы ### goto `break` к сожалению не имеет аргументов и выходит **всегда** на один уровень выше. => Существует реальная проблема выхода из тройного цикла, есть 2 решения: - Покрыть каждый уровень `if`'ами (очевидно не удобно) - Использовать `goto` (есть много нареканий со стороны других программистов) На самом деле нарикания по поводу использования `goto` относятся к совершенно другой эпохе, к похе раннего программирования и там *были* причины не использовать `goto` лишний раз. Программы в то время были плотно покрыты `goto`. Раньше: - после `if` нельзя было поставить блок, можно было указать `goto` в какое-то место (В ранних версиях языка *Basic*) - `goto` прыгал не до метки, а до какой-то строки нашей программы (нумеровалиь с шагом 10, чтобы было можно что-то незначительное добавить сверху) (что очевидно крайне неудобно) Про это и была статья Дейкстры `goto is cansidled hardfull` (и то такое название сделанно для громкого названия) `goto` ещё полезно для обработки ошибок, проще всего сделать освобождение памяти и закрытие файлов в конце функции, поставить метку до и прыгать на неё в случае ошибки, это позволит не городить кучу кода в `if` (по освобождению памяти и закрытию файлов) Вообще с помощью `goto` можно перепрыгныть через создание переменной, но, по моему это вещь НЕстандартная, и об этом точно предупредит компилятор (не понятно чтотогда ожидать от использования такой переменной) Использовать для увеличения читаемости программы ### for В цикле `for` могут отсутвовать все три условия: ```c for(;;); ``` Такая конструкция законна, означает бесконечный цикл без тела. Бесконечные циклы можно использовать когда условия выхода сложное и его вычисление не хочеться ставить в условие ## Препроцессор Это дополнительный язык, не си. Выполняется (во время компилируется) до си. К нему относятся команды которые начинаются с `#` (первым непробельным символом). Команды закончиваются в конце строки и занимают ВСЮ строку (в отличие от си, в си например можно написать весь `main` в одну строку). ### include Многие команды препроцессора на самом деле текстовые замены. Например команда `#include <stdio.h>` работает просто с текстом, и вставляет всё содержимое файла `stdio.h` в эту строчку. Это не магическое подключение библиотеки. В таких `.h` файлах содержатся все нужные прототипы и обьявления функций. --- В `.h` очень часто находится так называемый *include guard* (или защита от повторного включения), выглядит так: ```c #ifndef FILE_NAME_H #define FILE_NAME_H //принято именовать этот define названием файла // вместо точки ставиться '_' //обьявления #endif //FILE_NAME_H ``` Или (по новому) ```c #progma once //обьявления ``` Нужна она для того, чтобы случайно не подключить в *один* исходный файл (`.c` или `.cpp`) несколько раз один и тот же `.h`, тк его подключение может быть в том числе в другом `.h` файле. Если он подключиться дважды то у компилятора (до линковки) случится ошибка, что функция обьявленна дважды. > файлы `.h` вообще не компилируются, только внутри `.c` или `.cpp` после вставки с помощью `include` --- Если же мы реализуем функцию в каком-то `.h` файле и подключим его хотя бы в 2 исходных файла (`.c` или `.cpp`) то такая программа упадет на этапе линковки, тк у линковщика будет несколько реализации одной и той же функции, и он не сможет выбрать одну из реализаций. **Include Guard от этого не спасет!** (тк include guard про защиту от повторного включения в ОДИН исходный файл, а не в сесь проект, он избаляет от ошибки на этапе компиляции, а не линковки) --- Параметр у инклуда также можно указать через двойные кавычки - `#include "my.h"`. Правило: - `<...>` - ищет файл только в стандартных путях компилятора (этот вариант для файлов стандартной библиотеки) - `"..."` - ищет файл только в рабочей папке (при компиляции), если не нашел, то в стандартных путях компилятора (этот вариант для остальных библиотек или своих `.h` файлов) --- > Вообще инклуды это способ избавиться от копипасты простотипов функции в разные исполняемые файлы > Описание функций не генерирует никакого машинного кода! ### define Так называемое создание макроса. (на самом деле тоже обычная текстовая замена) Бывает в двух формах: - простой: `#define A B`. `A` - слово (состоящее из букв и символов подчеркивания '_'), без пробелов, точек, скобочек и т.д. Означает все экземпляры слова `A` заменить на `B`. (`B` до конца строки) (`#define "A" B` - так не замениться `A`) > Препроцессор ничего не знает о си, о типах данных, переменных, функциях и т.д. Препроцессор работает с обычным текстом, он видит слово и заменяет его на другой текст. - С аргуменами: `#define A(x, y) B`. Это не вызов функции, это до сих пор тупая текстовая замена, только с аргументами. Например: ```c #define A(f, s) f + s int a = A(4, 5)2; //x == 56, 4 + 52 #define B(f, s) ((f) + (s)) //правильно int b = B(4, 5)2; //b == 14, ((4)) + (5))2 ``` > По этому многие любят обносить дефайны скобками Минус и плюс - то, что это не функция. Пример минуса выше, пример плюса в том, что дефайн ничего не знает о типах. С его помощью, например, можно легко сделать `min` и `max`: ```c #define MAX(x, y) (x > y ? (x) : (y)) #define MIN(x, y) (x < y ? (x) : (y)) int main(void) { int a = 5, b = 6; MIN(a, b); //5 } ``` Здесь используется так называемый **тернарный оператор выбора** > Так как в си больше нет тернарных операторов (три аргумента), то *оператор выбора* просто называют *тернарный оператор* Тернарный оператор выбора: `a ? b : c`, если `a ==` - `1` (True), то `b` - `0` (False, то `c` --- **Главное** при использовании такого дефайна **не забывать про типы!** ```c #define MAX(x, y) (x > y ? (x) : (y)) signed int a = -1; unsigned int b; //любой MAX(a, b) == UINT_MAX; //порядка 4 миллиарда, при любом b ``` ```c #define MAX(x, y) (x > y ? (x) : (y)) int a=2; int b=4; int c = MAX(a--, b++); //c = 5, a = 1, b = 6 //int c = ((a--)>(b++)?(a--):(b++)) ``` --- ### Дерективы условной компиляции `#if`, `#elif`, `#else`, `#endif` - предпроцессорные конструкции ветвления, аналог `if` в си, отличия: - Происходит во время компиляции - Нужно закрывать командой `#endif` Условие у него может быть любое, какое можно посчитать во время компиляции --- Пример: ```c void f(int x #ifdef __llvm__ //if define __llvm__ , float y #endif ) { ... } ``` При компиляции `llvm` эта функция будет принимать 2 аргумента > `#if define A` очень часто сокращают до `#ifdef A`. > > Eсть ещё `#ifndef A` - когда не задефайнен `A` Пример 2: ```c #if A == 5 //A - дефайн //code #endif ``` --- Предпроцессорные `#if` имеют вложенность: ```c #ifdef DEBUG #if DEBUG == 1 printf("OK\n"); #endif #endif ``` В отличие от многострочных комментариев (`/* */`): ```c /* комментарий /* до сих пор комментарий */ // - закрыли комментарий */ // - ошибка компиляции, неоткрытый комментарий ``` --- Предпроцессорным `#if` очень удобно переключать 2 варианта кода: ```c #define DEBUG 1 void f(void) #if DEBUG == 1 { printf("debug\n"); } #else { printf("realese\n"); } #endif ``` --- Комментарии тоже обрабатываются препроцессором, не начинаются с `#`, просто выкидывается из программы во время компиляции ## Как не надо --- ### exit `exit(code)` - это команда которая завершает процесс с кодом `code`. Это зло, тк оно не освобождает от закрытия файлов и освобождения памяти. Программа благодаря `exit` может не падать с ошибкой, но это не значит, что её не происходит. --- ### Нельзя закрыть файл, который не открылся --- ### Нельзя закрыть файл, если переменную не инициализировали --- # Оглавление Full - C/C++, 5 лекция, 12.03.2022, Скаков > [TOC]