Understanding pointers to pointers in C

0
6

Zní to jako noční můra rekurze, ale ukazatele na ukazatele jsou nedílnou součástí programování systémů. V podstatě ukazujete na adresu paměti, která obsahuje adresu vašich skutečných dat. Toto dvojité nepřímé adresování je vývojáři nazýváno handle. This is not just a trick in the code. Je to nezbytný mechanismus, který operační systémy používají k efektivní správě paměti haldy.

Consider the heap as a crowded room. Někdy potřebuje operační systém přesunout lidi, aby uvolnil místo. Pokud na někoho držíte přímý ukazatel, tato osoba se nemůže pohnout, aniž by přerušila váš odkaz. Pokud však podržíte ukazatel na seznam jmen, operační systém může změnit umístění jakékoli osoby v tomto seznamu, aniž by bylo nutné aktualizovat vaši původní referenci. This is the power of the pen.

Zde je návod, jak se tato logika převádí do surového kódu C. You declare p as a pointer to a pointer. Then q becomes the standard pointer. Alokujete paměť pro p a pak alokujete paměť pro cokoli, na co ukazuje p. Nakonec dvakrát dereferencujete ukazatel, abyste přiřadili hodnotu 12.

Windows a macOS spoléhají na tuto strukturu při komprimaci paměti. The distinction is important here. Vy jako programátor ovládáte externí ukazatel p. Operační systém spravuje interní ukazatel *p. Protože operační systém ovládá *p, může přesunout aktuální datový blok (**p) kamkoli na hromadu. Jednoduše aktualizuje *p, aby odrážel novou adresu. Your code continues to use p without problems.

Kromě správy paměti OS je tento vzor potřebný k předávání ukazatelů na funkce. Pokud potřebujete, aby funkce upravila samotný ukazatel, předáte ukazatel na tento ukazatel. Toto je jediný způsob, jak znovu přiřadit odkaz z jiného oboru.

Správa ukazatelů na struktury

Complexity is not limited to simple integers. Tuto logiku můžete vložit do struktur. To je běžné při zpracování dat s proměnnou délkou, jako jsou řetězce.

Zvažte strukturu Addr. Obsahuje pole pevné velikosti pro jména, města a telefonní čísla. Ale komentáře mají různou délku. Proto je „komentář“ definován jako ukazatel na znak (znak). Když alokujete paměť pro samotnou strukturu, rezervujete prostor pro ukazatele a pevná pole. Zatím nemáte vyhrazené místo pro samotný text komentáře.

Nejprve zvýrazněte „s“. Poté načtete vstup uživatele do pevných vyrovnávacích pamětí. Poté si dočasně přečtete komentář

Pokud ignorujete vnoření ukazatelů, plýtváte pamětí.

Zvažte ukazatel s, který ukazuje na strukturu. Tato struktura obsahuje další ukazatel. Tento druhý ukazatel ukazuje na skutečný řetězec v paměti. Dvě úrovně nepřímého adresování. Jedna alokace paměti na strukturu. Jedna alokace paměti na řetězec.

Vše je jednoduché, dokud se nepokusíte uvolnit paměť.

To je místo, kde většina vývojářů klopýtá. Vidíte free(s). Myslíte si, že je vše připraveno. Mýlíte se.

Podívejte se na tento fragment kódu:

Proměnná s ukazuje na strukturu Addr. Uvnitř této struktury je pole „komentář“. komentář ukazuje na blok paměti na haldě přidělené pro data řádku.

Když zavoláte free(s), uvolníte paměť obsazenou samotnou strukturou. Struktura Addr zmizí. Paměť se vrátí do systému.

Ale co se stane s s->komentářem?

Mizí v zapomnění.

Ukazatel na data řetězce byl uložen uvnitř struktury, kterou jste právě uvolnili. Přístup k němu již není možný. V řetězci jste nezavolali free(). Paměť zůstává přidělena, ale není přístupná.

Toto je únik paměti.

To není krach. Toto není chybová zpráva. Program funguje dobře. Jen pomalu spotřebovává více a více RAM, dokud systém nezačne používat stránkovací soubor nebo se nezhroutí.

Jak opravit netěsnosti kvůli dvojitým ukazatelům

Nejprve musíte uvolnit vnitřní ukazatel. Nebo jej před uvolněním externí struktury uložte do dočasné proměnné.

Nyní jsou tyto linky uvolněny. Poté se struktura uvolní. Nejsou žádné ztracené paměťové bloky.

Proč se to stává tak často?

Lidé považují ukazatele za hodnoty. Zapomínají, že ukazatele jsou odkazy na zdroje.

Když struktura obsahuje ukazatel, struktura není samostatná. Záleží na externí paměti. Uvolnění kontejneru neuvolní jeho obsah.

S hlubším hnízděním se situace zhoršuje. Ukazatel na ukazatel na ukazatel. Tři úrovně. Potřebujete tři volání free(). Ve správném pořadí.

Pokud jeden zapomenete, dojde k úniku.

Problém s get()

Příklad používá gets(). Tato funkce je nebezpečná. Byl odstraněn ze standardu C11, protože umožňuje přetečení vyrovnávací paměti. Logika práce s pamětí ale zůstává stejná.

Ať už používáte gets(), fgets() nebo scanf(), strategie alokace paměti je úzkým hrdlem vedoucím k únikům.

Přidělujete paměť pro „s“.
Přidělujete paměť pro s->komentář.

Pokud uvolníte pouze s, zanecháte s->komentář.

Klíčové věci

  • Dvojité ukazatele vyžadují dvojité uvolnění.
  • Zkontrolujte každý malloc, zda neobsahuje odpovídající volný.
  • Pokud struktura obsahuje ukazatel na dynamickou paměť, musí být tato paměť uvolněna, než bude možné uvolnit samotnou strukturu.
  • free() neuvolňuje členy struktury rekurzivně.

Je snadné minout. Kód se zkompiluje. Funguje to. K úniku dochází nepozorovaně.

Dokud není pozdě.

Vytvářejte související seznamy

Uvolnění kontejneru před daty, na která odkazuje, může mít za následek katastrofu paměti. Struktura obsahující ukazatel bude vymazána, ale blok řetězce zůstane. Stane se ztraceným blokem. K tomu dochází, když je pořadí uvolnění nesprávné.

Propojení

Struktury mohou ukazovat samy na sebe. To vám umožní spojit identické záznamy do řetězce. Výsledkem je propojený seznam. Toto je standardní způsob, jak organizovat data v C.

Definuje se takto:

Pole „další“ obsahuje adresu dalšího záznamu. Ke spuštění řetězce se používá proměnná s jedním ukazatelem.

Náklady na flexibilitu

Kompilátor umožňuje porušit pravidla, která se na první pohled mohou zdát neintuitivní. S dostatkem zkušeností můžete vytvořit struktury podobné té, která je zobrazena výše. Tohle je ukázka síly.

Ale neobejde se to bez rizik. Jdete po tenké hranici mezi chytrým kódem a neudržitelným kódem špaget.

Proč je to důležité?

Není to jen o syntaxi. Jde o to, co vám jazyk umožňuje, jakmile překonáte jeho omezení.

  • Ovládání : Získáte podrobnou kontrolu nad umístěním dat v paměti.
  • Interoperabilita : S knihovnami C můžete pracovat snadněji.
  • Výkon : Někdy obcházení bezpečnostních kontrol šetří cykly CPU.

Ale je tu nuance: kompilátor vás nezachrání před sebou samým. Umožní vám zkompilovat kód, který se za běhu zhroutí.

Jak jej bezpečně používat

Pokud to budete dělat, udělejte to vědomě.

  1. Isolate : Nerozšiřujte tento vzor ve své kódové základně. Uchovávejte jej v malém, dobře otestovaném modulu.
  2. Dokumentujte vše : Budoucnost, za kterou poděkujete, přítomnost vy. Nebo to bude nenávidět. S největší pravděpodobností to bude nenávidět.
  3. Používejte abstrakce : Zabalte nezpracované ukazatele nebo nebezpečné bloky do čistého rozhraní. Skryjte nepořádek.

Kontrola reality

Většina vývojářů to nepotřebuje. Budou používat standardní struktury. A budou za to šťastnější.

Když ale narazíte na zeď, kam se standardní knihovna nevejde, budete rádi, že vás kompilátor nezastavil.

Otázkou je, zda jste ochotni zaplatit cenu za podporu takového kódu.

Je to kompromis. Rychlost výměnou za bezpečnost. Síla pro přehlednost.

Volba je na vás.

попередня статтяUnix vs Linux: Proč je tento rozdíl důležitý pro vývojáře
наступна статтяJak syntezátory změnily hudbu porušením pravidel formy