Всем привет, это снова stalker320, и я решил поделиться некоторой информацией, которую долго не переварить и только в конце 2022 года я смог понять управление памятью и как работают указатели. Если есть какие-то поправки, можете написать в комментарии, так как я и сам хотел бы лучше понять эту часть.
Размерность в Си
Итак, перед тем, как рассказывать про байты, я приведу один точный пример, чтобы разубедить всех, что программирование работает с битами.
Итак, если размер int
= 4 байта, short int
= 2 байта, сhar
= 1 байт, то какой размер у булевой переменной? Ответ: булевой переменной в принципе не существует без подключения стандартных библиотек. А при подключении библиотеки stdbool.h
, размер bool
= 1 байт.
Для проверки этого можно использовать следующий код:
#include <stdbool.h> #include <stdio.h> int main(int argc, char** argv) { printf("%llu\n", sizeof(bool)); // sizeof возвращает размер в байтах. А в библиотеке // stdint существует соответсвующий символу тип, "uint8_t", где 8 - это количество бит. Но размер возвращает 1 байт. // %llu для вывода long long unsigned int, или, коротко, size_t. return 0; } // Вывод: // sizeof bool: 1
Пояснение: булевая переменная — это число, которое принимает либо 0(false), либо 1(true).
Если говорить о размерах, то лучше сразу вывести размеры различных типов:
#include <stdio.h> #include <stdint.h> int main(int argc, char** argv) { // вот размеры разных типов // Полные названия типов printf("sizeof int8_t: %llu\n", sizeof(int8_t)); // char printf("sizeof uint8_t: %llu\n", sizeof(uint8_t); // unsigned char printf("sizeof int16_t: %llu\n", sizeof(int16_t); // short int printf("sizeof uint16_t: %llu\n", sizeof(uint16_t); // unsigned short int printf("sizeof int32_t: %llu\n", sizeof(int32_t); // int printf("sizeof uint32_t: %llu\n", sizeof(uint32_t); // unsigned int printf("sizeof int64_t: %llu\n", sizeof(int64_t); // long long int printf("sizeof uint64_t: %llu\n", sizeof(uint64_t); // unsigned long long int // Но это не всё. Сразу отмечу ещё один момент, который меняет ВСЁ. printf("sizeof uint8_t*: %llu\n", sizeof(uint8_t*); // unsigned char* printf("sizeof uint16_t*: %llu\n", sizeof(uint16_t*); // unsigned short int* printf("sizeof uint32_t*: %llu\n", sizeof(uint32_t*); // unsigned int* printf("sizeof uint64_t*: %llu\n", sizeof(uint64_t*); // unsigned long long int* printf("sizeof void*: %llu\n", sizeof(void*); // void* // В этом блоке размеры РАВНЫ. // Это потому что размер указателя диктуется РАЗРЯДНОСТЬЮ ПРОЦЕССОРА, // где 64-битные процессоры соответсвуют 8-байтным указателям, // в следствие чего меняется и максимальный размер оперативной памяти. // И раз зашла речь о типах данных, то в библиотеке stdint.h // содержится ещё несколько интересных типов printf("sizeof uintptr_t: %llu\n", sizeof(uintptr_t)); printf("sizeof intptr_t: %llu\n", sizeof(uintptr_t)); return 0; } // Вывод: // sizeof int8_t: 1 // sizeof uint8_t: 1 // sizeof int16_t: 2 // sizeof uint16_t: 2 // sizeof int32_t: 4 // sizeof uint32_t: 4 // sizeof int64_t: 8 // sizeof uint64_t: 8 // sizeof uint8_t*: 8 // Размеры указателей верны для 8-байтного процессора. // sizeof uint16_t*: 8 // sizeof uint32_t*: 8 // sizeof uint64_t*: 8 // sizeof void*: 8 // sizeof uintptr_t: 8 // Этот два типа представляет собой указатель, как число. // sizeof intptr_t: 8 // Да, В него можно преобразовать указатель. В этом даже фишка
Если у вас ничего не щёлкнуло в голове, то поясню — все указатели имеют одинаковый размер, а значит их можно безболезненно преобразовывать между собой. void* void_ptr = (void*) int_ptr;
Использовать аккуратно.
Структуры(struct), объединения(union) и немного enum.
Итак, начнём со структур, потому что без них нельзя объяснить смысл и удобство объединений.
Структура — из того, что я увидел — это последовательность данных записанных по порядку. Размер же структуры — это сумма размеров её полей, выравненных по байтам. Это выглядит так:
#include <stdio.h> struct s_data { unsigned char type; // sizeof(char) = 1; int x, y, z; // (sizeof(int) = 4) + (sizeof(int) = 4) + (sizeof(int) = 4); }; // суммарно 13 struct s_data_arr { unsigned char type; // sizeof(char) = 1; int values[3]; // sizeof(int) * 3 = 4 * 3; }; // Суммарно 13 int main(int argc, char** argv) { printf("sizeof struct s_data: %llu\n", sizeof(struct s_data)); printf("sizeof struct s_data_arr: %llu\n", sizeof(struct s_data_arr)); printf("sizeof struct s_data*: %llu\n", sizeof(struct s_data*)); return 0; } // Вывод // sizeof struct s_data: 16 //(Всё дело в выравнивании по байтам. // sizeof struct s_data_arr: 16 // Это будет представлено // sizeof struct s_data*: 8 // так: // char, NULL_byte, NULL_byte, NULL_byte, int, int, int) // Выравнивание по n sizeof(type), где type - тип, // а n - положение на линейке оперативной памяти // Ссылку на более подробное описание добавлю в конце // статьи, так как сам только недавно прочитал. // Пы. Сы. Зато в эти 3 байта можно вписать ещё переменных. Вроде такого: struct s_data__ { unsigned char type; // sizeof(char) = 1; unsigned char chr; // sizeof(char) = 1; unsigned short int count; // sizeof(unsigned short int) = 2; int x, y, z; // (sizeof(int) = 4) + (sizeof(int) = 4) + (sizeof(int) = 4); }; // суммарно 16. sizeof(s_data__) = 16
Если поля структуры размещаются последовательно, то поля объединений начинаются из одной точки и имеют размер наибольшего элемента. И если привести одну структуру к другой, даже если они имеют одинаковый размер, невозможно, то union позволяет сотворить чудо. Меньше трёпа, больше кода:
#include <stdio.h> struct s_data_xyz { unsigned char type; // sizeof(char) = 1; int x, y, z; // sizeof(int) = 4; }; // суммарно 13, но 16, хотя это для нас не важно, доверимся компилятору. struct s_data_arr { unsigned char type; // sizeof(char) = 1 int values[3]; // sizeof(int) * 3 = 4 * 3 }; // Суммарно 13, но 16 union pos { unsigned char type; struct s_data_xyz as_xyz; struct s_data_arr as_arr; }; int main(int argc, char** argv) { union pos p; printf("sizeof union pos: %llu\n", sizeof(union p)); p.type = 0; p.as_xyz.x = 12; p.as_xyz.y = 3; p.as_xyz.z = 7; printf("p.type: %u\n", p.type); printf("p.as_xyz.type: %u\n", p.as_xyz.type); printf("p.as_arr.type: %u\n", p.as_arr.type); printf("arr elems:\n"); for (int i = 0; i < 3; i++) { printf("%d: %d\n", i, p.as_arr.values[i]); } return 0; } // Вывод // sizeof union pos: 16 // p.type: 0 // Указатели на один и тот же байт без указателей. Всё это. // p.as_xyz.type: 0 // p.as_arr.type: 0 // arr elems: // 0: 12 // 1: 3 // 2: 7
Рассказывать о перечислениях(enum) Нечего, потому что это массив чисел, который компилятор удобно подписал ключевыми словами. Не хуже справляется команда препроцессора #define
.
В любом случае покажу на примере:
enum { ELEM_1, ELEM_2, ELEM_3, ELEM_MAX }; // Всё int enum Elems { ELEM_1, ELEM_2, ELEM_3, ELEM_MAX }; // Всё Elems, который typedef int Elems; // Работает только с int typedef unsigned char Elems; #define ELEM_1 ((Elems) 0x00) // Не уверен в том, что это не будет воспринято как препроцессорный метод #define ELEM_2 ((Elems) 0x01) #define ELEM_3 ((Elems) 0x02) #define ELEM_MAX ((Elems) 0x03) void fn(Elems a); // Так используется в объявлении функций.
Указатели
Итак, дорогие читатели-потенциальные Си программисты, вы должны были понять, что размер указателя одинаковый в пределах программы на одном компьютере. Однако как их использовать? Некоторые даже видели запись, когда изучали какой-то код вроде *x++
. Что тут происходит? Как такое возможно? Указатель — это число с размером разрядности компьютера. Но даже если размер не совпадает с разрядностью, размер указателя точно будет совпадать с размером uintptr_t
.
В любом случае, нам потребуется подключить stdlib.h
, стандартную библиотеку.
При этом мы не сами выделяем память, а просим операционную систему выделить память нам нужное количество байтов.
Для примера напишем функцию, которая принимает указатель и увеличивает его значение на 1 (В ней мы ещё не выделяем и не освобождаем память):
#include <stdlib.h> #include <stdio.h> // Объявления void increace_value(int* pvalue); int main(int argc, char** argv); // Реализации int main(int argc, char** argv) { int a = 2; printf("start_\ta: %d\n", a); increace_value( // Вызываем функцию &a // Передаём АДРЕС переменной в функцию ( Увеличивает количество звёзд после типа на 1) ); // тип &a = int*, &&a = int**, &&&a = int*** и так далее printf("inc_\ta: %d\n", a); return 0; } void increace_value( int* pvalue // Прибавляем префикс p столько же раз, сколько есть звёздочка. // Так удобно воспринимать аргументы ) { *pvalue += 1; // обращаемся к значению и увеличиваем на один. // *(px++) (Или *px++) - это получить значение и сместить указатель на байт. // Именно в этом порядке. // * - Уменьшает количество звёзд после типа на 1: // тип *pvalue = int } // Вывод: // start_ a: 2 // inc_ a: 3
А теперь перепишем его так, чтобы a
был изначально указателем:
#include <stdlib.h> #include <stdio.h> // Объявления. Можно вынести в main.h void increace_value(int* pvalue); int main(int argc, char** argv); // Реализации int main(int argc, char** argv) { int* a = (int*) malloc(sizeof(int)); // Выделяем байты по размеру числа. *a = 5; // задаём значение переменной по адресу, как в функции. printf("start_\ta: %d\n", *a); increace_value(a); // Передаём указатель printf("inc_\ta: %d\n", *a); free(a); // очищаем память return 0; } void increace_value( int* pvalue // Прибавляем префикс p столько же раз, сколько есть звёздочка. // Так удобно воспринимать аргументы ) { *pvalue += 1; // обращаемся к значению и увеличиваем на один. } // Вывод: // start_ a: 5 // inc_ a: 6
Массивы в Си
Массивы в Си — это переменные, которым выделено N размеров типа данных. Массив указателей можно назвать списком из python. Правда отсутствие записи информации о типах данных различных элементов делает его сложным для обработки, хотя это решается структурами. В любом случае вернёмся к массивам и коду:
#include <stdlib.h> #include <stdio.h> int main(int argc, char** argv) { int* iarr = (int*) malloc(sizeof(int) * 4)); // выделяем память для 4 элементов типа int for (int i = 0; i < 4; i++) { iarr[i] = i * 2; // записываем значения. } for (int i = 0; i < 4; i++) { printf("%d: %d\n", i, iarr[i]); // вывод } free(iarr); // очищаем память return 0; }
Однако для создания массива лучше подойдёт функция calloc
, которая принимает количество элементов и размер одного элемента:
#include <stdlib.h> #include <stdio.h> int main(int argc, char** argv) { /* (тип*)calloc(кол-во элементов, размер одного элемента.);*/ int* iarr = (int*) calloc(4, sizeof(int))); // выделяем память для 4 элементов типа int for (int i = 0; i < 4; i++) { iarr[i] = i * 2; // записываем значения. } for (int i = 0; i < 4; i++) { printf("%d: %d\n", i, iarr[i]); // вывод } free(iarr); // очищаем память return 0; }
Вот и вся суть массива. Не злоупотребляйте, либо злоупотребляйте хотя бы в меру. Иначе вылетит stack trace.
Заключение
Управление памятью одновременно невероятно сложно и невероятно просто. Одни могут и не понять его, другие поймут с ходу. Всё зависит от представления об оперативной памяти. Надеюсь мои объяснения смогли не понимающим дать это самое понимание, а понимающим укрепить свои познания. Благодарю за прочтение.
ссылка на оригинал статьи https://habr.com/ru/post/711458/
Добавить комментарий