Jeden argument free(void *)
(wprowadzony w Unix V7) ma jeszcze jedną dużą przewagę nad wcześniejszymi dwoma argumentami, o mfree(void *, size_t)
których tu nie wspomniałem: jeden argument free
radykalnie upraszcza każde inne API, które działa z pamięcią sterty. Na przykład, w razie free
potrzeby, rozmiar bloku pamięci, strdup
musiałby w jakiś sposób zwrócić dwie wartości (wskaźnik + rozmiar) zamiast jednej (wskaźnik), a C sprawia, że zwroty wielu wartości są znacznie bardziej uciążliwe niż zwroty jednowartościowe. Zamiast tego char *strdup(char *)
musielibyśmy pisać char *strdup(char *, size_t *)
albo inaczej struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
. (Obecnie ta druga opcja wygląda dość kusząco, ponieważ wiemy, że ciągi zakończone znakiem NUL są "najbardziej katastrofalnym błędem projektowym w historii komputerów", ale to z perspektywy czasu. Jeszcze w latach 70-tych zdolność C do obsługi łańcuchów jako prostego char *
traktowania była faktycznie uważana za decydującą przewagę nad konkurentami, takimi jak Pascal i Algol .) Poza tym nie tylko strdup
cierpi na ten problem - wpływa na każdy system lub zdefiniowany przez użytkownika funkcja, która przydziela pamięć sterty.
Wcześni projektanci Uniksa byli bardzo sprytnymi ludźmi i jest wiele powodów, dla których free
jest lepszy niż mfree
taki, w zasadzie myślę, że odpowiedź na pytanie jest taka, że zauważyli to i odpowiednio zaprojektowali swój system. Wątpię, czy znajdziesz jakikolwiek bezpośredni zapis tego, co działo się w ich głowach w momencie, gdy podjęli tę decyzję. Ale możemy sobie wyobrazić.
Udawaj, że piszesz aplikacje w C, aby działały na V6 Unix, z jego dwoma argumentami mfree
. Jak dotąd radziłeś sobie dobrze, ale śledzenie tych rozmiarów wskaźników staje się coraz bardziej kłopotliwe, ponieważ twoje programy stają się coraz bardziej ambitne i wymagają coraz częstszego używania zmiennych alokowanych na stercie. Ale masz genialny pomysł: zamiast ciągłego ich kopiowania size_t
, możesz po prostu napisać kilka funkcji narzędziowych, które przechowują rozmiar bezpośrednio w przydzielonej pamięci:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
Im więcej kodu napiszesz przy użyciu tych nowych funkcji, tym bardziej będą one niesamowite. Nie tylko uczynić kod łatwiej napisać, ale również uczynić swój kod szybciej - dwie rzeczy, które często nie idą w parze! Wcześniej przekazywałeś je size_t
wszędzie, co powodowało dodatkowe obciążenie procesora związane z kopiowaniem i oznaczało, że trzeba było częściej rozlewać rejestry (szczególnie w przypadku argumentów funkcji dodatkowych) i marnować pamięć (ponieważ wywołania funkcji zagnieżdżonych często powodują w wielu kopiach size_t
przechowywanych w różnych ramkach stosu). W nowym systemie nadal musisz wydać pamięć na przechowywanie plikówsize_t
, ale tylko raz i nigdzie nie jest kopiowany. Może się to wydawać niewielkimi korzyściami, ale pamiętaj, że mówimy o maszynach z wyższej półki z 256 KB pamięci RAM.
To cię uszczęśliwia! Więc dzielisz się swoją fajną sztuczką z brodatymi mężczyznami, którzy pracują nad następną wersją Uniksa, ale to ich nie uszczęśliwia, tylko zasmuca. Widzisz, byli właśnie w trakcie dodawania kilku nowych funkcji narzędziowych, takich jak strdup
, i zdają sobie sprawę, że ludzie używający twojej fajnej sztuczki nie będą w stanie używać swoich nowych funkcji, ponieważ wszystkie ich nowe funkcje używają kłopotliwego wskaźnika + rozmiar API. I to też sprawia, że jesteś smutny, ponieważ zdajesz sobie sprawę, że będziesz musiał sam przepisać dobrą strdup(char *)
funkcję w każdym programie, który napiszesz, zamiast korzystać z wersji systemu.
Ale poczekaj! Jest rok 1977, a kompatybilność wsteczna nie zostanie wynaleziona przez kolejne 5 lat! A poza tym nikt poważny nie używa tej niejasnej „uniksowej” rzeczy z jej niekolorową nazwą. Pierwsza edycja K&R jest już w drodze do wydawcy, ale to żaden problem - na pierwszej stronie jest napisane, że „C nie udostępnia żadnych operacji bezpośrednio zajmujących się obiektami złożonymi, takimi jak ciągi znaków… nie ma sterty … ”. W tym momencie w historii string.h
i malloc
są to rozszerzenia dostawców (!). Tak więc, sugeruje Bearded Man # 1, możemy je zmieniać, jak chcemy; dlaczego po prostu nie zadeklarujemy, że Twój trudny dzielnik jest oficjalnym dystrybutorem?
Kilka dni później Bearded Man # 2 widzi nowe API i mówi hej, czekaj, to jest lepsze niż wcześniej, ale nadal spędza całe słowo na alokację, przechowując rozmiar. Uważa to za kolejną rzecz do bluźnierstwa. Wszyscy inni patrzą na niego, jakby był szalony, bo co innego możesz zrobić? Tej nocy zostaje do późna i wymyśla nowy alokator, który w ogóle nie przechowuje rozmiaru, ale zamiast tego wnioskuje o nim w locie, wykonując czarną magię przesunięcia bitów na wartości wskaźnika i zamienia go, utrzymując nowe API na miejscu. Nowe API oznacza, że nikt nie zauważa przełączenia, ale zauważa, że następnego ranka kompilator zużywa o 10% mniej pamięci RAM.
A teraz wszyscy są szczęśliwi: otrzymujesz łatwiejszy do napisania i szybszy kod, Brodaty # 1 może napisać fajny prosty, strdup
którego ludzie będą naprawdę używać, a Brodaty # 2 - pewny, że zasłużył na trochę na utrzymanie - - wraca do zabawy z quinami . Wyślij to!
A przynajmniej tak mogło się stać.