
Привет, Хабр!
Управлении ресурсами включает в себя распределение, использование и освобождение различных типов ресурсов. В языке программирования 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; }
GCC предлагает расширения для атомарных операций, которые можно использовать в версиях C, предшествующих C11, или в случаях, когда поддержка Реализуем атомарный обмен значений: Фнкция В С есть максимальная свобода и контроль над своим кодом, но вместе с этим приходит и ответственность. Используйте эту свободу с умом, осознавая последствия своих решений. По ссылке вы сможете зарегистрироваться на бесплатный вебинар курса «Программист С» про реализацию динамических структур данных на С и Python.atomic_int используется для объявления счетчика, а atomic_fetch_add_explicit — для атомарного увеличения его значения. memory_order_relaxed указывает, что операция может быть реорганизована по отношению к другим операциям, но гарантируется атомарность самой операции увеличения.<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 используетсядля атомарного увеличения значения счетчика.
ссылка на оригинал статьи https://habr.com/ru/articles/800381/
Добавить комментарий