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