A dinamikus memóriafoglalás nem egyszerűen „hely kérése a heapen”. A C program szempontjából új tárolási terület keletkezik, amelynek méretét, használatát és felszabadítását a programnak kell helyesen kezelnie. A sikeres foglalás önmagában nem jelenti azt, hogy a tároló már érvényes alkalmazásszintű adatokat tartalmaz.
Ebben az egységben négy külön kérdést követünk: mekkora tároló szükséges, sikerült-e lefoglalni, mely részei inicializáltak, és ki köteles felszabadítani. Az egészaritmetikai előfeltételeket a Promóciók, signedness és túlcsordulás, a típusos hozzáférés szabályait az Effective type, aliasing és objektumreprezentáció tárgyalta.
A példák ISO C17-et használnak. A konkrét laborprofil Linux, x86-64 System V ABI, LP64, 8 bites C bájt, GCC 14.2.0 és glibc 2.40. A fordítási parancs reprodukciós útmutató; a példához itt nem tartozik végrehajtott fordítás vagy mérés.
ISO C17. A <stdlib.h> fejlécben
deklarált malloc(size) egy size C bájt méretű
területet foglal. A C bájt a sizeof egysége; bitszáma
CHAR_BIT, nem általánosan nyolc.
Pozitív méret esetén a visszatérés:
NULL-lal hasonlítunk össze.A malloc nem inicializálja az adatokat. A tárolt értékek
indeterminate állapotúak: nem tekinthetők használható kezdőértéknek, és
típusos olvasásuk nem definiált viselkedéshez is vezethet.
A calloc(count, element_size) count darab,
egyenként element_size C bájt méretű elemhez foglal
tárolót, majd annak minden bitjét nullára állítja. Pozitív paraméterek
mellett itt is ellenőrizni kell a null pointer visszatérést.
A malloc és a calloc eredménye
void *, amely C-ben megfelelő objektumpointerré implicit
módon konvertálható. Ezért a helyes idiomatikus alak:
/* ISO C17; részlet, <stdlib.h> szükséges.
Előfeltétel: count pozitív, a szorzás nem csordul túl. */
int *p = malloc(count * sizeof *p);Nem szükséges (int *) cast. A fejléc viszont kötelező: a
cast nem pótolja a függvény megfelelő deklarációját.
A sizeof *p a célobjektum méretét fejezi ki. Ha később
p típusa változik, a méretkifejezés követi. Ebben a
példában az operandus nem változó hosszúságú tömbtípus, ezért a
sizeof nem értékeli ki a *p kifejezést: nem
történik dereferencia.
A szokásos foglalófüggvények megfelelő igazítást biztosítanak a
fundamentális igazítási követelményű típusokhoz, például az itt használt
int típushoz. Ez nem korlátlan ígéret minden kiterjesztett
igazításra; azt az Alignment és speciális
allokációk rész tárgyalja.
Operációs rendszer szintje. A sikeres foglalásból
nem következik, hogy ugyanennyi fizikai memória azonnal rezidenssé vált.
Hasonlóan, a free sem ígéri a folyamat RSS-ének azonnali
csökkenését. Ezeket ne keverjük össze a C nyelvi
élettartam-szerződésével.
Legyen
az elemszám,
az elemméret C bájtban,
pedig a size_t legnagyobb értéke. A szükséges méret:
Mivel
,
a szorzat pontosan akkor reprezentálható size_t értékként,
ha
Bizonyításvázlat. A reprezentálhatóság feltétele . Pozitív mellett oszthatunk vele; az egész felső korlátját a hányados egészrésze adja. Emiatt az ellenőrzést szorzás nélkül is elvégezhetjük:
/* ISO C17; részlet, <stdint.h> szükséges a SIZE_MAX makróhoz. */
if (count > SIZE_MAX / sizeof *p) {
/* A kért méret nem reprezentálható. */
}A malloc(count * sizeof *p) hívás önmagában nem védi ki
a hibát. A szorzás már a függvény meghívása előtt megtörténik. A
size_t előjel nélküli aritmetikájának körbefordulása
definiált, de az így kapott túl kicsi foglalás későbbi túlindexeléshez
vezethet.
A megadott x86-64 laborprofilban a size_t 64 bites, az
int mérete négy C bájt. Ekkor:
A elemszámhoz tartozó szorzat már lenne, amely ebben az előjel nélküli típusban nullára fordul körbe. A számítás helyessége természetesen nem garantálja, hogy egy kisebb, reprezentálható kérés teljesíthető.
Ha az elemszám előjeles külső adatból származik, a negatív értéket
a size_t-ra konvertálás előtt kell
elutasítani.
ISO C17. Nulla méretű foglalásnál implementáció által meghatározott, hogy null pointert vagy más eredményt kapunk-e. A visszakapott nem null pointert sem használhatjuk objektum elérésére; megfelelően felszabadítani viszont lehet.
Egyszerűbb saját szerződést választani:
Nulla elemszám esetén a művelet sikeres, az eredmény
NULL, és nem történik allokáció.
Ez megkülönbözteti az üres eredményt az allokációs hibától, ezért a
függvény külön státuszkódot ad vissza. A reprezentációhoz ne képezzünk
automatikusan NULL + 0 kifejezést sem; üres tartománynál
nincs szükség pointeraritmetikára.
A calloc(count, sizeof *p) előnye, hogy nem a hívó
szorozza össze előre az argumentumokat. A saját méretellenőrzés mégis
hasznos: elkülöníti a reprezentálhatatlan méretet az erőforráshiánytól,
és egyértelmű API-szerződést ad. A
calloc(1, count * sizeof *p) alak ezt az előnyt
elveszíti.
Az alábbi függvény initial_value értékekkel feltöltött
tömböt készít. Az out egy kölcsönzött hozzáférés a hívó
pointerobjektumához.
Előfeltétele, hogy out élő, írható int *
objektumra mutasson, amelynek kezdeti értéke NULL. Ez
kizárja egy korábban birtokolt foglalás csendes felülírását.
Sikertelenségkor az objektum változatlan marad; siker esetén a hívó
kapja meg a felszabadítás felelősségét.
Az allocation.c teljes tartalma:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
enum array_status {
ARRAY_OK,
ARRAY_TOO_LARGE,
ARRAY_NO_MEMORY
};
static enum array_status
int_array_create(size_t count, int initial_value, int **out)
{
int *p = NULL;
if (count == 0) {
return ARRAY_OK;
}
if (count > SIZE_MAX / sizeof *p) {
return ARRAY_TOO_LARGE;
}
p = malloc(count * sizeof *p);
if (p == NULL) {
return ARRAY_NO_MEMORY;
}
for (size_t i = 0; i < count; ++i) {
p[i] = initial_value;
}
*out = p;
return ARRAY_OK;
}
int main(void)
{
const size_t count = 4;
int *values = NULL;
enum array_status status = int_array_create(count, 7, &values);
if (status != ARRAY_OK) {
const char *message =
status == ARRAY_TOO_LARGE
? "A kert meret nem abrazolhato."
: "A memoriafoglalas sikertelen.";
fprintf(stderr, "%s\n", message);
return EXIT_FAILURE;
}
for (size_t i = 0; i < count; ++i) {
printf("%d\n", values[i]);
}
free(values);
values = NULL;
return EXIT_SUCCESS;
}Fordítás a megadott profilban:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow \
-O1 -g allocation.c -o allocationSikeres foglalás és zavartalan kimeneti I/O esetén a várt kimenet
négy sor, mindegyikben 7. Ez várt viselkedés, nem közölt
futtatási eredmény.
A műveletek sorrendje lényeges. Először ellenőrizzük a méretet, majd
tárolót foglalunk. A típusos értékadás minden elembe érvényes
int értéket ír. A kimeneti pointert csak a teljes
inicializálás után állítjuk be.
ISO C17. A dinamikusan foglalt tárolóhoz nem
tartozik deklarált típus. Az int * pointer deklarálása vagy
a foglalási eredmény hozzárendelése önmagában nem inicializálja a
tárolót. A példában az int lvalue-kon keresztüli írás
határozza meg az érintett objektumok effective type-ját az írás és a
megfelelő későbbi hozzáférések számára.
Az effective type teljes szabályrendszere ennél részletesebb.
Különösen a memcpy nem azonos egy tetszőleges bájtonkénti
feltöltéssel; ezt az Effective type, aliasing és
objektumreprezentáció, illetve a Memóriamásolás,
összehasonlítás és byte view kapcsolja össze.
A tulajdonlási és élettartam-szerződés:
| Résztvevő | Szerep | Érvényesség és megszűnés |
|---|---|---|
Lokális p |
Sikeres foglalás után ideiglenes tulajdonos | A függvény visszatéréséig él; a felelősséget előtte átadja |
| Allokált tároló | Az elemek tárolási területe | Sikeres foglalástól a free-ig él |
values |
Sikeres visszatérés után tulajdonos | A main blokkban él; egyszer felszabadítja a
tárolót |
out |
Kölcsönzött hozzáférés a values pointerobjektumhoz |
Csak a függvényhívás alatt használjuk, nem tároljuk el |
Például &values[1] |
Lehetséges kölcsönzött elempointer | Csak elegendő elemszám és élő tároló mellett érvényes; a
free érvényteleníti |
Az out előfeltételének megsértésére a függvény nem ad
külön hibakódot: az szerződésszegés, amely nem definiált viselkedéshez
vezethet. A mérethiba és az allokációs hiba viszont szabályosan
jelentett hibakimenet.
A minden bitjén nulla reprezentáció az egész típusoknál a nulla értéket adja. Nem általános viszont, hogy ugyanez null pointert vagy lebegőpontos nullát jelent.
A következő, ISO C17-es függvényrészlet az előző program fejléceit és
enum array_status definícióját használja. Ugyanaz az
out-szerződése, de kizárólag nulla értékű int
elemeket állít elő:
static enum array_status
zero_int_array_create(size_t count, int **out)
{
int *p = NULL;
if (count == 0) {
return ARRAY_OK;
}
if (count > SIZE_MAX / sizeof *p) {
return ARRAY_TOO_LARGE;
}
p = calloc(count, sizeof *p);
if (p == NULL) {
return ARRAY_NO_MEMORY;
}
*out = p;
return ARRAY_OK;
}Itt a nullázott reprezentáció elegendő a kívánt egész
kezdőértékekhez. Más alkalmazásszerződésben nem feltétlenül: ha egy
int tömb érvénytelen file descriptorait -1
jelöli, a calloc eredménye logikailag hibás kezdőállapot,
mert a nulla érvényes descriptorérték lehet.
A nullázás továbbá nem helyettesíti az elemszám, a kapacitás vagy más
invariáns nyilvántartását. Ha minden elemet rögtön más értékkel írunk
felül, a malloc általában világosabban fejezi ki a
szándékot. Teljesítménykülönbséget mérés nélkül ne állítsunk.
ISO C17. A free argumentuma lehet null
pointer; a free(NULL) nem végez műveletet. Egyébként olyan
pointerérték szükséges, amely megfelelő memóriafoglaló függvényből
származik, és az általa jelölt foglalást még nem szabadították fel. A
jelen egységben ezek a malloc és a calloc.
Nem szükséges ugyanazt a pointerváltozót használni, amelybe az eredmény eredetileg került: egy szabályosan másolt kezdőpointer is megfelelhet. A felszabadítás felelősségének azonban egyértelműnek kell maradnia. A C típusrendszere ezt az egyszeri tulajdonlást nem kényszeríti ki.
Nem adható át:
Az alábbiak egymástól független, hibás ISO
C17-részletek. Kezdetben p legalább két
inicializált int elem sikeres foglalásának tulajdonosa.
Egyikhez sem tartozik garantált kimenet.
/* Interior pointer free: nem a foglalás kezdete. */
free(p + 1);/* Double free: ugyanazt a foglalást másodszor szabadítaná fel. */
free(p);
free(p);/* Use-after-free: az objektum élettartama már véget ért. */
free(p);
int value = p[0];A free nem nullázza a hívó pointerváltozóját. A megszűnt
objektumra hivatkozó pointerértéket nem szabad további hozzáféréshez
használni; hordozható C17 kódban annak kiolvasására vagy
összehasonlítására se építsünk.
A még élő pointerobjektumnak új érték adható:
/* ISO C17; részlet. p élő lokális pointerobjektum. */
free(p);
p = NULL;Ez nem javítja meg a többi aliast. A tárolóra korábban kapott minden kölcsönzött hozzáférés érvénytelenné válik.
A másik gyakori hiba a kezdőpointer elvesztése: ha a tulajdonos változót felülírjuk, és másutt nem őrizzük a kezdőcímet, a tároló felszabadíthatatlanná válhat. Ez memóriaszivárgás. A megoldás nem egy utólagos cast, hanem a tulajdonosi felelősség megőrzése.
Mi a különbség a malloc sikeres visszatérése, a
calloc nullázása és az alkalmazásszintű inicializálás
között? Miért kell külön státusz a nulla elemszámú tömbhöz, ha annak
reprezentációja NULL?
Írj
byte_buffer_create(size_t payload, unsigned char **out)
függvényt az előző státuszkódokkal. Foglaljon payload + 1 C
bájtot, és az utolsóba írjon nullát. A payload többi bájtját ne
inicializálja.
Az out kezdeti értékére ugyanaz a szerződés vonatkozzon,
mint korábban. Nulla payload esetén is kell egyetlen bájt.
Független, hibás ISO C17-részletben base két
inicializált int elem foglalásának tulajdonosa:
int *cursor = base + 1;
free(base);
base = NULL;
printf("%d\n", *cursor);
free(cursor);Azonosítsd a hibákat, és adj javított műveleti sorrendet. A
cursor eredetileg kölcsönzött pointer, nem tulajdonos.
A malloc tárolót biztosít, de használható kezdőértékeket
nem. A calloc a reprezentáció bitjeit nullázza. A logikai
inicializálás az alkalmazás által megkövetelt érvényes állapot
létrehozása; ez adott esetben lehet nulla, máskor például
-1.
A NULL önmagában nem különbözteti meg az üres sikeres
eredményt az allokációs hibától.
Ellenőrzési feltétel: a nulla elemszámú hívás
eredménye ARRAY_OK és NULL; az erőforráshiányé
ARRAY_NO_MEMORY.
ISO C17-részlet, az előző program fejléceivel és státusztípusával:
static enum array_status
byte_buffer_create(size_t payload, unsigned char **out)
{
if (payload == SIZE_MAX) {
return ARRAY_TOO_LARGE;
}
size_t count = payload + 1;
unsigned char *p = malloc(count * sizeof *p);
if (p == NULL) {
return ARRAY_NO_MEMORY;
}
p[payload] = 0;
*out = p;
return ARRAY_OK;
}A sizeof(unsigned char) definíció szerint egy C bájt,
ezért itt az összeadás jelenti a túlcsordulási kockázatot. A payload
tartalmát a hívónak kell meghatároznia használat előtt. Siker esetén a
hívó a tulajdonos; a free minden ide mutató kölcsönzött
pointert érvénytelenít.
Ellenőrzési feltétel: payload == 0
esetén egy nulla bájt keletkezik; payload == SIZE_MAX
esetén nincs foglalás, a kimenet változatlan. Sikernél a hívó egyszer
felszabadítja az eredményt.
A free(base) megszünteti az allokált objektumok
élettartamát. A base = NULL csak a base
pointerobjektumot módosítja; a cursor nem válik
biztonságossá. Dereferálása use-after-free, felszabadítása pedig szintén
hibás. Élő foglalás esetén sem lenne felszabadítható, mert belső
pointer.
Javított sorrend, azonos előfeltételekkel:
/* ISO C17; részlet, <stdio.h> és <stdlib.h> szükséges. */
int *cursor = base + 1;
printf("%d\n", *cursor);
cursor = NULL;
free(base);
base = NULL;Ellenőrzési feltétel: minden elemhozzáférés a
felszabadítás előtt történik, és pontosan egy free kapja
meg a foglalás kezdőpointerét.
A biztonságos dinamikus tömbkezelés sorrendje: méretszerződés,
túlcsordulás-ellenőrzés, foglalás, hibaellenőrzés, inicializálás, majd
egyértelmű tulajdonoshoz rendelés. A sizeof *p összeköti a
méretszámítást a cél típusával; a calloc nullázása nem
általános logikai inicializálás.
A free null pointerrel megengedett, egyébként élő
foglalás kezdőpointerét igényli. A tároló felszabadítása minden hozzá
tartozó kölcsönzött hozzáférést érvénytelenít, függetlenül attól, hány
pointerváltozó őrzi még a korábbi címet.