Руководство по работе с памятью Си

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Всем привет, это снова 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/


Интересные статьи

Интересные статьи

Руководство по парольной политике. Часть 1Перевод чрезвычайно полезного документа от большого коллектива авторов. Содержит конкретные рекомендаци и объединяет появившиеся в последнее время руководства...
Вот уже несколько лет React Native является горячей темой в мире мобильной разработки. Неудивительно – он взял мир технологий штурмом, предложив способ разработки мобильных приложений как дл...
Хочу поделиться опытом автоматизации экспорта заказов из Aliexpress в несколько CRM. Приведенные примеры написаны на PHP, но библиотеки для работы с Aliexpress есть и для...
Переезд за границу — звучит интересно, но никогда не угадаешь, какие сложности будут ждать именно тебя. Мы в редакции Нетологии поговорили с разработчиками, которые решились на смену стра...
Данная статья – это мини-справочник и руководство по Scrum, созданные в результате прочтения книги Сазерленда и статей из интернета. Надо различать Agile и Scrum. Agile – это методология (наук...