Основы управления ресурсами в C

от автора

Привет, Хабр!

Управлении ресурсами включает в себя распределение, использование и освобождение различных типов ресурсов. В языке программирования C автоматическое управление памятью отсутствует, эта задача ложится на плечи разработчиков.

В этой статьи рассмотрим основные возможности для работы с ресурсами в C.

Начнем с динамической памяти.

Работа с динамической памятью

malloc предоставляет сырую память без инициализации. Подходит для тех моментов, когда нужен контроль и гибкость. Но здесь нужно не забывать о инициализации выделенной памяти, чтобы избежать неопределенного поведения:

int *numbers = malloc(sizeof(int) * 10); if (numbers == NULL) {     // обработка ошибки выделения памяти }

calloc подходит, когда нужно работать с массивами или структурами данных, где начальное нулевое состояние предпочтительно.

int *matrix = calloc(10, sizeof(int)); if (matrix == NULL) {     // обработка ошибки выделения памяти }

С realloc можно перевыделить уже выделенную память, изменяя ее размер:

numbers = realloc(numbers, sizeof(int) * 20); if (numbers == NULL) {     // обработка ошибки выделения памяти }

free помогает вернуть выделенную память обратно в систему. После вызова free любые операции с этим указателем становятся невозможными:

free(numbers); numbers = NULL; // обнуляем указатель для безопасности

Функциональные и двойные указатели

Функциональные указатели позволяют хранить адреса функций и вызывать их по этим адресам.

Пример кода:

void hello() {   printf("Привет, Хабр!\n"); }  void world() {   printf("Мир C!\n"); }  int main() {   void (*funcPtr)();  // объявление функционального указателя   funcPtr = &hello;   // присвоение адреса функции hello   funcPtr();          // вызов функции hello   funcPtr = &world;   // смена указателя на функцию world   funcPtr();          // вызов функции world   return 0; }

Двойные указатели используются для управления памятью и структурами данных, такими как динамические массивы массивов. Они также юзабельны при передаче адреса указателя в функцию для его изменения. Пример работы с двойными указателями:

#include <stdio.h> #include <stdlib.h>  void allocateArray(int **arr, int size, int value) {   *arr = (int*)malloc(size * sizeof(int));   for(int i = 0; i < size; i++) {     *(*arr + i) = value;   } }  int main() {   int *array = NULL;   allocateArray(&array, 5, 45);  // выделяем память и инициализируем массив   for(int i = 0; i < 5; i++) {     printf("%d ", array[i]);   }   free(array);  // не забываем освободить память   return 0; }

Valgrind и санитайзеры

Valgrind ищет неинициализированную память, ошибки работы с динамической памятью и многое другое, а после поиска сообщает об ошибках.

Например:

#include <stdlib.h>  int main() {     int *a = malloc(sizeof(int) * 10); // здесь мы выделили память...     // ...и забыли ее освободить. Опечатка или судьба?     return 0; }

Запустив Valgrind с этим кодом, получим нечто вроде:

==12345== LEAK SUMMARY: ==12345==    definitely lost: 40 bytes in 1 blocks

Санитайзеры также помогают нам обнаружить утечки памяти, не выходя из IDE. Например, AddressSanitizer, один из таких инструментов, может быть использован следующим образом:

gcc -fsanitize=address -g your_program.c

Это не позволит пройти незаметно утечкам памяти.

Можно использовать фичи из C++ (что?)

Как вы, возможно, знаете, RAII — это паттерн из C++, где при создании объекта ресурс захватывается, а при уничтожении объекта — освобождается. «Но стоп,» — скажете вы, — «в C нет классов и деструкторов!». Однако попробуем адаптировать RAII для C, используя структуры и функции очистки.

Представим, что есть структура для управления динамической памятью:

#include <stdlib.h>  typedef struct {     int* array;     size_t size; } IntArray;  IntArray* IntArray_create(size_t size) {     IntArray* ia = malloc(sizeof(IntArray));     if (ia) {         ia->array = malloc(size * sizeof(int));         ia->size = size;     }     return ia; }  void IntArray_destroy(IntArray* ia) {     if (ia) {         free(ia->array);         free(ia);     } }

IntArray_create и IntArray_destroy играют роли конструктора и деструктора.

Еще один интересный паттерн — фабричные функции. Они позволяют абстрагироваться от процесса создания объектов, скрывая детали инициализации. В C это могут быть функции, возвращающие указатели на различные структуры, представляющие ресурсы:

typedef struct {     FILE* file; } FileResource;  FileResource* FileResource_open(const char* filename, const char* mode) {     FileResource* fr = malloc(sizeof(FileResource));     if (fr) {         fr->file = fopen(filename, mode);     }     return fr; }  void FileResource_close(FileResource* fr) {     if (fr) {         fclose(fr->file);         free(fr);     } }

Параллелизм и многопоточность

Используя pthreads, можно разделить задачи на несколько потоков, которые выполняются параллельно. Создадим поток для выполнения простой функции, которая выводит сообщение на экран:

#include <pthread.h> #include <stdio.h> #include <stdlib.h>  // функция, которая будет выполнена в потоке void* thread_function(void* arg) {     printf("Привет из потока! Аргумент функции: %s\n", (char*)arg);     return NULL; }  int main() {     pthread_t thread_id;     char* message = "Thread's Message";      // создаем поток     if(pthread_create(&thread_id, NULL, thread_function, (void*)message)) {         fprintf(stderr, "Ошибка при создании потока\n");         return 1;     }      // ожидаем завершения потока     if(pthread_join(thread_id, NULL)) {         fprintf(stderr, "Ошибка при ожидании потока\n");         return 2;     }      printf("Поток завершил работу\n");     return 0; }

pthread_create запускает новый поток, который выполняет thread_function, в то время как pthread_join ожидает завершения этого потока.

Разделив массив на части и обработав каждую часть в отдельном потоке, мы можем значительно ускорить процесс. Суммируем элементы большого массива, используя многопоточность:

#include <pthread.h> #include <stdio.h> #include <stdlib.h>  #define SIZE 1000000 #define THREADS 4  int array[SIZE]; long long sum[THREADS] = {0};  void* sum_function(void* arg) {     int thread_part = (int)arg;     int start = thread_part * (SIZE / THREADS);     int end = (thread_part + 1) * (SIZE / THREADS);      for(int i = start; i < end; i++) {         sum[thread_part] += array[i];     }      return NULL; }  int main() {     pthread_t threads[THREADS];      // инициализация массива     for(int i = 0; i < SIZE; i++) {         array[i] = i + 1;     }      // создание потоков для суммирования частей массива     for(int i = 0; i < THREADS; i++) {         pthread_create(&threads[i], NULL, sum_function, (void*)i);     }      // ожидание завершения всех потоков     for(int i = 0; i < THREADS; i++) {         pthread_join(threads[i], NULL);     }      // суммирование результатов     long long total_sum = 0;     for(int i = 0; i < THREADS; i++) {         total_sum += sum[i];     }      printf("Общая сумма: %lld\n", total_sum);     return 0; }

Код делит массив на четыре части и вычисляет сумму каждой части в отдельном потоке, после чего суммирует полученные результаты.

Атомарные операции

#include <stdatomic.h> #include <stdio.h> #include <pthread.h> atomic_int counter = ATOMIC_VAR_INIT(0); void* increment(void* arg) { for (int i = 0; i < 100000; ++i) { atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed); } return NULL; } int main() { pthread_t t1, t2; if(pthread_create(&t1, NULL, increment, NULL)) { return 1; } if(pthread_create(&t2, NULL, increment, NULL)) { return 1; } pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Значение счетчика: %d\n", atomic_load(&counter)); return 0; }

atomic_int используется для объявления счетчика, а atomic_fetch_add_explicit — для атомарного увеличения его значения. memory_order_relaxed указывает, что операция может быть реорганизована по отношению к другим операциям, но гарантируется атомарность самой операции увеличения.

GCC предлагает расширения для атомарных операций, которые можно использовать в версиях C, предшествующих C11, или в случаях, когда поддержка <stdatomic.h> недоступна или нежелательна.

Реализуем атомарный обмен значений:

#include <stdio.h> #include <pthread.h>  int counter = 0;  void* increment(void* arg) {     for (int i = 0; i < 100000; ++i) {         __sync_fetch_and_add(&counter, 1);     }     return NULL; }  int main() {     pthread_t t1, t2;      if(pthread_create(&t1, NULL, increment, NULL)) {         return 1;     }     if(pthread_create(&t2, NULL, increment, NULL)) {         return 1;     }      pthread_join(t1, NULL);     pthread_join(t2, NULL);      printf("Значение счетчика: %d\n", counter);     return 0; }

Фнкция __sync_fetch_and_add из расширений GCC используетсядля атомарного увеличения значения счетчика.


В С есть максимальная свобода и контроль над своим кодом, но вместе с этим приходит и ответственность. Используйте эту свободу с умом, осознавая последствия своих решений.

По ссылке вы сможете зарегистрироваться на бесплатный вебинар курса «Программист С» про реализацию динамических структур данных на С и Python.


ссылка на оригинал статьи https://habr.com/ru/articles/800381/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *