# 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 Синтаксически - тоже самое, что и структура **Разница только в одном** - в том, как он **хранится в памяти**. ![](https://i.imgur.com/I9MWHA4.png) ![](https://i.imgur.com/MSEpdjQ.png) Это структура в которой всё элементы побайтово лежат поверх друг друга. Размер 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) (очень быстро) ![](https://i.imgur.com/mrTCgJo.jpg) 2) **Двумерный массив** (`int m[h][w];`) * храниться в памяти подряд * строки единого размера * одно обращение в память (быстрее) * строки можно поменять местами минимум за O(w) (довольно медленно) ![](https://i.imgur.com/p5d8DSN.jpg) Разницу нужно понимать, тк обращение к этим двум обьектам выглядит идентично - `m[i][j]` > В двумерном массиве нужный указатель вычисляется с помощью умножение, фактически, двумерный массив - это одномерный массив ### Массив массивов Массив массивов может выделяться 3мя разными способами: --- 1) ![](https://i.imgur.com/3Z8Um1x.jpg) --- 2) ![](https://i.imgur.com/AY6m7Ot.jpg) --- 3) ![](https://i.imgur.com/0GOp0eH.jpg) --- ### Двумерный массив Двумерный массив выделяется так: ```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]