Programowanie w języku C/C++ » Artykuły

Bezpieczny malloc w Ansi C

Funkcja malloc jest przyczyną wielu ciężkich do wykrycia błędów. Wystarczy zapomnieć o pomnożeniu przez rozmiar elementu i bug gotowy. Dodatkowo w starszych kompilatorach wartość zwracaną przez malloc trzeba rzutować do określonego typu co na dłuższą metę jest bardzo pracochłonne i niewygodne. Tak mniej więcej wygląda standardowe wywołanie funkcji:
unsigned long int **p = (unsigned long int**)malloc(10*sizeof(unsigned long int*));

Oczywiście zamiast sizeof(unsigned long int*) można wpisać 4 ale zmniejsza to przenośność kodu - w końcu niekoniecznie na wszystkich komputerach i nie na wszystkich systemach wskaźniki muszą mieć rozmiar 4 bajtów.
W powyższym zapisie bardzo łatwo o pomyłkę a i czytelność jest znikoma. Jednak jest sposób aby unknąć wszystkich wyżej wymienionych problemów z malloc: z pomocą przychodzi nam preprocesor języka C. Bardzo prosto napisać makro, które wykona za nas rzutowanie oraz mnożenie przez rozmiar elementu. Poniższe makro jako argumenty przyjmuje typ elementów oraz ilość elementów do przydzielenia

#define MALLOC(typ, ilosc) (typ*)malloc((ilosc)*sizeof(typ))


Makra uzywa się w ten sposób:

unsigned long int **p = MALLOC(unsigned long int*, 12);


Prawda, że czytelniejsze? ;>

Kolejnym błędem podczas przydziału pamięci jest nie sprawdzanie czy pamięć została przydzielona (komu się chce za każdym razem pisać
if(p == NULL) {fprintf(stderr, "malloc error"); exit(0);}
). Na to również znajdzie się sposób: wystarczy zdefiniować funkcję, która będzie to sprawdzała:

void* _new(size_t rozmiar)
{
     void* p = malloc(rozmiar);
     if(p == NULL)
     {
           fprintf(stderr, "Out of memory!");
           exit(EXIT_FAILURE);
     }
      return p;
}


oraz troche przerobić nasze makro:

#define MALLOC(typ, ilosc) (typ*)_new((ilosc)*sizeof(typ))


Jeszcze jednym popełnianym błędem może być używanie malloc z argumentami naszego MALLOC (np. z przyzwyczajenia)
można temu tak zaradzić:
/*---plik my_alloc.h---*/
#define MALLOC(typ, ilosc) (typ*)_new((ilosc)*sizeof(typ))
#define malloc NIE_UŻYWAJ_BEZPOŚREDNIO_malloc!
 
extern void* _new(size_t);
/*---EOF---*/
 
/*---plik my_alloc.c---*/
#include <stdlib.h>
#include <stdio.h>
#include "my_alloc.h"
#undef malloc
 
void* _new(size_t rozmiar)
{
     void* p = malloc(rozmiar);
     if(p == NULL)
     {
           fprintf(stderr, "Out of memory!");
           exit(EXIT_FAILURE);
     }
      return p;
}
/*---EOF---*/
 
/*---plik main.c---*/
#include "my_alloc.h"
 
int main(int argc, char *argv[])
{
    unsigned long int **p = MALLOC(unsigned long int*, 12);
    int *b = malloc(12); /*---Błąd!---*/
    return 0;
}
/*---EOF---*/


Całkiem niewielkim nakładem pracy mamy bezpieczny i wygodny system przydziału pamięci podobny do new z C++.

Jeżeli artykuł Wam się spodobał to następnym razem napisze coś o programowaniu obiektowym w C.

3 komentarze

skiter 2007-01-19 05:15

Chym a moze kozystac z perror i jemu podobnych chym? ...

Gynvael Coldwind 2006-07-26 18:40

fprintf(stderr, "Out of memory!");
exit(EXIT_FAILURE);

Imho to zły pomysł. Nie wszystkie OS'y czyszczą heap po procesie gdy on wychodzi (np win9x nie czyści). A "exit" tam uniemożliwia programiście wyczyszczenie pamięci przed wyjściem z błędem.
Co innego gdyby była jakaś globalna lista (drzewo binarne najlepiej) zaalokowanych fragmentów pamięci, do której _new by wrzucał po prostu wskaźniki przy alokacji, a jakiś _delete by usuwał je. Można by wtedo "atexit" poprosić o wywołanie funkcji dealokującej pamięć (mamy wkońcu wszystko zaalokowane na liście/drzewie), więc to "exit" tam by było całkiem OK ;>