# 04 лекция - C, подробно
> [TOC]
## Типы с плавующей точкой
> Много вычислений в числах с плавающей точкой - накапливают большую погрешность, немного разные реализации могут давать существо разную точность
Сравнивать можно только через `eps`
```c
if (a==b) // неправильно
if (fabs(a-b) < eps) // правильно
```
## Структуры
Массивы - способ обьединения однородных данных (одного типа)\
Структуры - способ обьединения разнородных данных (разных типов)
**Структуры - это новый тип данных, его описание**
### Создание
Обычное использование:
```c
struct A
{
int x; // один int
float y; // один float
char z[4]; // массив 4х char
};
struct A a; //"struct A" - название нового типа
//"a" - название переменной типа "struct A"
```
> Внутри структур могут быть массивы, другие структуры. Можно делать массивы структур и т.д.
**Здесь не создается никакой обьект**, как с `typedef` (создавал новый тип), создаем новый тип, который можно использовать для создания новых переменных. Такой тип использовать немного неудобно, тк нужно всегда указывать `struct` (только в си) перед именем структуры.
Обратим внимание на `;` после структуры, она тут нужна потому что после структуры можно сразу содать обьекты типа нашей структуры.
```c
struct // Структуру можно даже не называть
// (не рекомендуется)
{
int x; // один int
float y; // один float
char z[4]; // массив 4х char
} a; //можем сразу создать обьект такого типа
```
> Структуру можно даже не называть, не рекомендуется тк есть спец эффекты, например при отладке имя структуры не покажется
Часто можно встретить: (Чтобы не писать `struct`)
```c
typedef struct A // 'A' можно не писать
{
int x;
float y;
char z[4];
} B;
B b;
```
ИЛИ
```c
struct A // 'A' нужно писать
{
int x;
float y;
char z[4];
};
typedef struct A B;
B b;
```
> У структур нет модификаторов доступа, в си такого нет, появляется только в си++
### Обращение к полям структур
```c
A a, b = {1, 2}; //в порядке сверху вниз (в самой структуре)
a.x; //доступ к переменной x
a.z[1]; //доступ к 1 элементу массива z
a = b; // можно скопировать поэлементно
```
Структуры можно скопировать поэлементно, что существенно отличает структуры от массивов. Если мы напишем так про массивы (`a = b`, где `a` и `b` массивы) - то ничего хорошего не произойдет, массивы будут интерпретированны как указатели и при компиляции нам скажут "что за ужас здесь творится". Чтобы скопировать массивы надо либо писать цикл либо вызвать специальную функцию копирования памяти (из одного места в другое). (либо же обернуть их в структуры)
**Массивы внутри структур ведут себя как структуры** (копируются полностью)
Сразу ничем нельзя инициализировать, тк структура это не создание обьекта, **описание структуры - описание формата данных в памяти**. Описание расположения ничего не говорит о содержимом.
Можно иниц обьект при создании:
```c
typedef struct
{
int x;
int y;
} A;
A a = {.y=2, .x=1}; //a.x == 1 a.y == 2
//чтобы иниц. не по очереди
A b = {1, 2}; //b.x == 1 b.y == 2
A c = {1}; //c.x == 1 c.y == 0
A d = {}; //d.x == 0 d.y == 0
//не все компиляторы понимают
A e; // Мусор и в 'e.x' и в 'e.y'
```
`A a = {.y=2, .x=1};` Начиная с C99, нет в C++
`A d = {};` - компилятор инициализирует всё нулям, некоторые компиляторы не любят пустые `{}`
### Как хранится в памяти
Элементы структуры в памяти хранятся **ровно** в том порядке в котором их описали в структуре. Переставлять их компилятор не имеет права. Но это не означает что они лежат в памяти плотно.
```c
struct A
{
// первая переменная можно считать начитаеся с адреса 0
int x; // 4 байта
// дырка 4 байта
double y; // 8 байт
char z[4]; // 4 байта
// дырка 4 байт - для выравнивания при создании массива структур
// всего 24 байта
};
```
Это так называемое **Выравнивание на родную границу**. Железкам **очень** нравится когда данные храняться по адресу кратному размеру данных
Для x86 архитектуры железок (x64 частный случай x86) практически не важно есть выразвнивание или нет (иногда небольшое замедление). В других железках программа может даже упасть, тк железо может не уметь читать невыравненные данные, либо может быть дикое замедление.
В си **по умолчанию все данные выравниваются на родную границу**, для этого копилятору может прийтись напихать дырок.
Первый `int` начаниется в начале структуры и начало структуры достаточно хорошо выразвненно для ВСЕХ элементов (можно считать что начинается с 0 адреса)
Чтобы **избежать** дырок в структуре - рекомендуется описывать структуры в порядке уменьшения рамера базового типа (`double -> int -> char[4]`), но можно обойтись и без этого
**Очень часто есть хвостовое выравнивание для массивов из структур**
Чтобы избежать свостового выравнивания при создании массива струкр - можно передалать логику работы алгоритма на **структуру массивов**! (вместо массива структур)
### Как отключить выравнивание
Зачем - например, для работы с файлами можно описать структуру чтобы её внутреннее устройство соответствовало один в один расположению данных в файлике. Читать так файли можно не поэлементно а одной операйией чтения. (Расположение данных должно точно совпадать) (Быстрее обычного способа)
В таких случаях вырвнивание можно отключить с помощью флагов компиляции (компиляторозависимо) (код может начать работать существенно медленее)
Для VS (Microsoft)
```c
#programa pack (push, 1) //без выравнивания
// (Пушим старое выравнивание на стек)
//code - описываем структуры
// которое хотим чтобы плотно упаковывались
#programa pack (pop) //выравнивание структур на дефолтное
```
Для Clang/GCC
```c
struct A __attribute__ ((packed))
{
//code
};
```
## Про размер в си
Размер какого-то обьекта - типа, переменной, массива в C/C++ можно узнать с помощью **оператора** `sizeof`, например: (в байтах)
```c
int a;
sizeof a == 4;
sizeof(unsigned int) == 4;
int arr[10];
sizeof(arr) == 40;
```
Скобки можно иногда не писать, это не функция!
Полученное число - это размер **типа выражения** в скобках в **байтах**!
---
К слову:
```c
struct A
{
int x;
double y;
char z[4];
} a[10];
struct B
{
double y;
int x;
char z[4];
} b[10];
struct C
{
int x[10];
double y[10];
char z[40];
} c;
sizeof(a) == 240; //байт
sizeof(b) == 160; //байт
sizeof(c) == 160; //байт
```
---
Тип выражения - `typeof`, есть в некоторых компиляторах, предлагают добавить в стандарт C23. По сути `typeof` уже существует, тк до того как посчитать размер в памяти нужно почитать тип выражения.
---
## Union
Синтаксически - тоже самое, что и структура
**Разница только в одном** - в том, как он **хранится в памяти**.


Это структура в которой всё элементы побайтово лежат поверх друг друга. Размер union - sizeof максимального элемента. Может использоваться для создания переменных с разными типами. (обычно вместе со структорой, которая хранит активный тип) (ещё можно прочитать битики floata, напрмер))
Все элементы начинаются с адреса 0, массив считается одним элементом, структура тоже будет считаться одним элементом, со всеми своими дырками
Нужны не очень часто, но там где их нет, но они нужны - уровень извращений зашкаливает, и он будет гораздо выше чем здесь.
## Указатели
В интернете есть много картинок про сравнение перехода с C на Python (например, и наоборот). Во многом легкость изучения других языков после C/С++ обуславливается тем, что здесь приходится вручную управлять памятью (=> существуют указатели).
Когда говорят что на Java можно писать эффективный код - имею ввиду, что для этого программист должен обладать высшей калификацией, так как чтобы писать эффектывный код нужно понимать что язык прячет от вас и как он это длает, C, меньше С++ прячут значительно меньше чем Java => присать быстрый код на них проще (несмотря на то, что писать просто код на них сложнее)
В последнее время почему-то чем быстрее удаётся накодить программу - тем лучше (лучше выпустить на неделю раньше, чем исправить пару багов), такой подход как раз и провоцирует использование простых, однако неэффективных языков, например Python, Java
**Указатели** - идейно они довольно похожи на ярлыки в windows, если создать кучу ярлыков и один удалить, данные никак не поменяются. Ярлыки не содержат данные, только информацию как эти данные найти. На си тоже самое, только указатели строго типизированны, в указателе зашито то, на что он указывает. Указатель на `int` может указывать только на `int`. **Сами обьекты не содержат информации о типе обьекта**, `int` - это просто 4 байта в памяти. Информация о типе есть только в момент компиляции. По этому в C/C++ в момент компиляции указатель знает информация о типе.
**Занимают в памяти столько, какая разрядность программы** (32 или 64 бита)
Система обычно позволяет запускать меньшей и равной битности программы (иногда строго равной)
```c
int x; // int
int *p, a; // указатель на инт, и обычный инт
```
Есть религиозный момент, где ставить `*` рядом с `int` или рядом с именем переменной. Тк при создании переменной указатель относиться только к этой пеменной, а не типу, то следует её ставить рядом с переменной.
`*` может относиться к типу, но это редкость (в основном при создании указателя на функцию)
От того, где в C/C++ стоят пробелы - ничего не меняется!
```c
p = &x; // взять адрес в памяти переменной x
//и присвоить указателю p
// (унарный контекст &)
```
Указатель - чтобы работать с данными, другой смысл `*`: (унарный)
```c
int y = *p; // обращение сквозь указатель
//(разьименовка указателя), y == x
//чтение сквозь указатель
*p = 2; // x == 2
//запись сквозб указатель
int **q=&p; // указатель на указатель
**q = 3; // x == 3, *q на p, **q на x
```
Тип указателя позволяет их коректно разименовывать
В си ссылок нет! (`int &a;`) (есть в си++)
Пример пользы указателей - `scanf`, мы даем адрес переменной, а функция туда записывает данные.
## Про память
Обычные переменные, аргументы функции и т.д. создаются **на стеке** (храняться на стеке)\
Выделение памяти - проиходит **на куче**.
* стек - +- 2 мегабайта на процесс
* куча - сколько есть операт пам
Если привысить стек - ***программа упадет***, возможно молча, тк для вывода ошибки тоже нужно место на стеке.
### Глобальные переменные
Опишем переменную вне функции:
```c
int g = 2;
int f(int a)
{
return a;
}
int main(void)
{
f(8);
return 0;
}
```
`g` будет глобальной переменной. Такие переменные глобальные:
* **Одни на всю программу всегда**
* **Видно из всех функций**
* **Время жизни - глобальное**
* **По умолчанию - видимость только в файле**
+ обьявить в другом файле (строго без иниц):
+ `int g;` - C
+ `extern int g;` - C++,
+ `::g` - обратиться в С++ (когда имя скрыто локальной переменной)
* Создается до запуска main
* Уничтожается после окончания main
* Создается в секции данных (на куче)
* Размер должен быть известен в момент компиляции
Минусы глобальных переменных:
* Ломают многопоточность
* рекурсию
* очень сложно отлаживать (могут измениться в неожиданном месте)
> Глобальные переменные лучше не использовать без **Острой** необходимости
> Константы **рекомендуется** размещать в глобальной области
`static` по глобальной переменной **ограничивает видимость** текущим файлом.
### Static локальные переменные
Выглядит как локальная:
* видимость - локальная
* время жизни - глобальное
```c
int f(void)
{
static int a = 5; // будет выполненно один раз (первый)
a++; // будет выполненно Каждый раз
return a; // будет выполненно Каждый раз
}
int main(void)
{
//здесь a не видно
f(); // == 6
f(); // == 7
f(); // == 8
return 0;
}
```
Тоже самое, что и глобальная переменная.
Из минусов осталось:
* Ломает многопоточность
### Секции
Три секции:
1) секция кода - код
2) секция данных - глобальные переменные (`static` локальные тоже)
3) секция данных на чтение - глобальные константы
---
4) Stack - локальные переменные
5) Free RAM (Heap) - куда происходит выделение памяти (обычно называют кучей)
И всё это лежит в оперативной памяти.
[ПОДРОБНЕЕ](https://stackoverflow.com/questions/53942282/where-will-the-initialized-data-segment-values-are-stored-before-run-time)
[ПОДРОБНЕЕ 2](https://www.geeksforgeeks.org/memory-layout-of-c-program)
### Выделение памяти
Для выделения памяти нам и нужны указатели:
```c=
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int w = 1000;
//int a[1000]; - создание обычного статического массива
// так не надо при больших количествах информации
// тк локальные масивы храняться на стеке (+- 2мб на программу)
int *a = malloc(sizeof(int) * w); //создание массива с выделением памяти
if(a == NULL) // проверять рекомендуется при выделении большого размера
{
printf("Memory was not allocated!\n");
return 1;
}
a[999]; // доступ такой же
free(a); // освободить память
// функции free можно отдавать NULL
}
```
`malloc`, возвращает `void *`, принимает количество байт которые нужно *попросить* выделить систему
Религиозный вопрос про проверку. Проверять рекомендуется при выделении большого размера в памяти, тк при выделении маленького - есть огромная веротность что уже всё остальное тоже сломалось.
В си **НЕ** нужно кастовать указатель из `void *` к `int *` (*в ++ нужно, по религиозным причинам*)
Освобождать память нужно точно **таким же способом** (семейство функций), каким и выделили (на будущее). Так проиходит из-за того, что у разных функций выделения - разные вспомогательные данные, из-за этого ещё если мы попросим выделить 0 байт, то место в оперативной памяти такая штука займет больше 0.
> В системе существует много разных способов (функций) выделить память, для разных нужд:
> * просто выделение
> * выделить очень много
> * чтобы была возможность отдать другому процессу
> * и т.д.
#### Доступ
Доступ к выделенной памяти такой же как и у одномерных массивов, тк синтаксис `[]` это ни что иное, как разьменование и перемещение указателя. Обычные одномерные массивы почти тоже самое, что указатель, за исключением того, что:
1) указатель не знает рамер массива
2) массив нельзя переопределить (указатель можно заставить указывать на другое место в памяти)
3) нельзя создать указатель на массив, он будет тем же самым указателем
### Передача массива в функцию
```c
int sum(int x[3]) // -> (int *x) автоматически будет преобразованно
{
return x[0] + x[1] + x[2];
}
int main(void)
{
int a[3] = {1, 2}; //1, 2, 0
return sum(a);
}
```
Массивы в аргументах функций **никогда** не бывают массивами, они автоматически преобразуются до указателя. (альтернативная запись указателя - `int x[]`)
=> массивы никогда не **копируются** в функцию, любое изменение которые мы сдлаем с массивом внутри функции будет видно там, где этот массив создавали.
### Разница массива массивов и двумерного массива
1) **Массив массивов** (`int **m;`)
* хранится массив указатей на строки, а затем разбросанно по памяти строки
* строки могут быть разного размера
* два обращения в память (медленнее)
* строки могут поменены местами за O(1) (очень быстро)

2) **Двумерный массив** (`int m[h][w];`)
* храниться в памяти подряд
* строки единого размера
* одно обращение в память (быстрее)
* строки можно поменять местами минимум за O(w) (довольно медленно)

Разницу нужно понимать, тк обращение к этим двум обьектам выглядит идентично - `m[i][j]`
> В двумерном массиве нужный указатель вычисляется с помощью умножение, фактически, двумерный массив - это одномерный массив
### Массив массивов
Массив массивов может выделяться 3мя разными способами:
---
1) 
---
2) 
---
3) 
---
### Двумерный массив
Двумерный массив выделяется так:
```c=
int g[5][5]; //В секции данных, фикс размер
int main(void)
{
int h = 8, w = 10; //Могут быть введены пользователем
int a[5][5]; //На стеке, мало памяти и фикс размер
int *m = malloc(sizeof(int) * h * w); //h - высота, w - ширина
g[2][3];
a[2][3];
m[2*w + 3]; //неудобный доступ к элементам
free(m);
return 0;
}
```
### VLA
**Есть ещё один способ** сделать двумерный массив ***ТОЛЬКО В СИ***, выделить на куче, и с нормальным доступом: (трудный для понимания):
```c
int main(void)
{
int h = 8, w = 10; //Могут быть введены пользователем
//h - высота, w - ширина
int (*m)[w] = malloc(sizeof(int) * h * w); //VLA указатель
m[2][3];
free(m);
return 0;
}
```
В этом примере мы использовали понятие массива с неконстантной длинной, VLA - variable length array Не все компиляторы это умеют. Дело в том, что добавили это только в стандарт C99, но убрали из обязательной части стандарта в оптиональную в C11, те компиляторы, которые не поддерживают стандарт C99, не умеют в VLA (например microsoft), даже в VLA указатели (данный пример)
VLA массивом называют локальный или глобальный массив с неконстантным размером. Многие утверждаю, что это зло - **так и есть**, потому что такие массивы
1) Не имеют ограничей по длинне (может забить стек)
2) При попадании такого в цикл, компилятор обязан хитро уметь в реальном времени освобождать и выделять место на стеке прям посреди функции (обычно делается при входе в функцию)
VLA называю злом из-за того, что *"Тупые хомячки"* используют его для создания динамического массива на стеке (VLA массив), вместо создания хитрого типа переменного размера (VLA указатель). На самом деле идея очень хорошая, но только в качестве VLA указателей.
В 23 стандарте хотят вернуть часть VLA (указатели) в обязательную часть стандарта
---
#### Как выглядит VLA массив (зло)
**Как НЕ надо писать**
```c
int w = 5;
const int w_const = 5;
int g[5]; //НЕ VLA array - Так НАДО
int g_vla[w]; //VLA array
int g_also_vla[w_const]; //VLA array
int main (void)
{
int w_in_main = 5;
const int w_in_main_const = 5;
int a[5]; //НЕ VLA array - Так НАДО
int a_vla[w]; //VLA array
int a_also_vla[w_const]; //VLA array
}
```
---
#### Передача VLA указателя в функцию
Нужно сначала передать в функцию размер массива:
```c
void f(int h, int w, int a[h][w]) // первая размерность всегда превратиться в указатель
{
}
//или
void f(int w, int a[][w]) // первая размерность всегда превратиться в указатель
{
}
```
# Оглавление
Full - C/C++, 4 лекция, 05.03.2022, Скаков
> [TOC]