4 uobičajene C programske pogreške - i 5 savjeta kako ih izbjeći

Nekoliko programskih jezika može se podudarati s C zbog puke brzine i snage na razini stroja. Ova je izjava bila istinita prije 50 godina, a vrijedi i danas. Međutim, postoji razlog zbog kojeg su programeri smislili izraz "puška" da bi opisali C-ovu snagu. Ako niste oprezni, C vam može otpuhati nožne prste - ili tuđe.

Evo četiri najčešće pogreške koje možete napraviti s C i pet koraka koje možete poduzeti kako biste ih spriječili.

Uobičajena pogreška C: Ne oslobađanje mallocmemorije (ili oslobađanje više puta)

Ovo je jedna od velikih pogrešaka u C-u, od kojih mnoge uključuju upravljanje memorijom. Dodijeljena memorija (učinjena pomoću malloc funkcije) ne uklanja se automatski u C. Zadatak programera je raspolagati tom memorijom kada se više ne koristi. Ne uspijevaju se osloboditi ponovljeni zahtjevi za memorijom i na kraju ćete doći do curenja memorije. Pokušajte upotrijebiti područje memorije koje je već oslobođeno i vaš će se program srušiti - ili, još gore, šepati će i postati ranjiv na napad pomoću tog mehanizma.

Imajte na umu da curenje memorije treba opisivati ​​samo situacije u kojima bi memorija trebala biti oslobođena, ali nije. Ako program nastavi dodjeljivati ​​memoriju jer je memorija stvarno potrebna i koristi se za rad, tada njegova upotreba memorije može biti  neučinkovita , ali strogo govoreći, to nije curenje.

Uobičajena pogreška C: Čitanje niza izvan granica

Ovdje imamo još jednu od najčešćih i najopasnijih pogrešaka u C. Čitanje nakon kraja niza može vratiti podatke o smeću. Pisanje preko granica niza može oštetiti stanje programa ili ga potpuno srušiti ili, što je najgore od svega, postati vektor napada za zlonamjerni softver.

Pa zašto je teret provjere granica niza prepušten programeru? U službenoj C specifikaciji čitanje ili pisanje niza izvan njegovih granica je "nedefinirano ponašanje", što znači da specifikacija nema pravo glasa u onome što bi se trebalo dogoditi. Prevoditelj se čak ni ne mora žaliti na to.

C odavno preferira davanje programera čak i na vlastiti rizik. Kompajler obično ne zarobi izvangranična čitanja ili pisanja, osim ako posebno ne omogućite mogućnosti kompajlera da se zaštite od toga. Štoviše, možda je moguće prekoračiti granicu niza u vrijeme izvođenja na način da se ne može zaštititi ni provjera kompajlera.

Uobičajena pogreška C: Neprovjeravanje rezultata malloc

malloc i calloc (za memoriju pred nulom) su funkcije knjižnice C koje dobivaju memoriju dodijeljenu hrpom iz sustava. Ako ne mogu dodijeliti memoriju, generiraju pogrešku. U doba kada su računala imala relativno malo memorije, postojala je velika šansa da poziv mallocmožda neće biti uspješan.

Iako računala danas imaju gigabajte RAM-a za bacanje, i dalje uvijek postoji šansa mallocda ne uspiju, pogotovo pod velikim pritiskom memorije ili pri dodjeli velikih ploča memorije odjednom. To se posebno odnosi na C programe koji prvo “dodijele” veliki blok memorije iz OS-a, a zatim ga podijele za vlastitu upotrebu. Ako prva dodjela ne uspije jer je prevelika, možda ćete moći odbiti to odbijanje, smanjiti dodjelu i u skladu s tim podesiti heuristiku upotrebe memorije programa. Ali ako dodjela memorije ne uspije neiskorištena, cijeli bi program mogao trbuhom za kruhom.

Uobičajena pogreška C: Upotreba void*za generičke pokazivače na memoriju

Korištenje  void* za usmjeravanje na sjećanje stara je i loša navika. Upućuje na memoriju uvijek treba biti char*, unsigned char*ili  uintptr_t*. Suvremeni C prevodilački programi trebali bi uintptr_tbiti dio stdint.h

Kad je označen na jedan od ovih načina, jasno je da se pokazivač odnosi na memorijsko mjesto u sažetku umjesto na neki nedefinirani tip objekta. To je dvostruko važno ako izvodite matematiku pokazivača. Sa  uintptr_t*i slično, element veličine na koji se ukazuje i kako će se koristiti, jednoznačni su. Sa void*, ne toliko.

Izbjegavanje uobičajenih C pogrešaka - 5 savjeta

Kako izbjeći ove previše uobičajene pogreške pri radu s memorijom, nizovima i pokazivačima u C-u? Imajte na umu ovih pet savjeta. 

Struktura programa C tako da vlasništvo nad memorijom ostane čisto

Ako tek pokrećete aplikaciju C, vrijedi razmisliti o načinu na koji se memorija dodjeljuje i oslobađa kao jedno od organizacijskih načela programa. Ako je nejasno gdje se oslobađa određena dodjela memorije ili pod kojim okolnostima, tražite probleme. Uložite dodatne napore kako biste vlasništvo nad memorijom učinili što jasnijim. Učinit ćete uslugu sebi (i budućim programerima).

To je filozofija iza jezika poput Rust-a. Rust onemogućava pisanje programa koji se pravilno kompajlira ako jasno ne izrazite kako se memorija posjeduje i prenosi. C nema takvih ograničenja, ali pametno je tu filozofiju usvojiti kao svjetlo vodilja kad god je to moguće.

Upotrijebite opcije kompajlera C koje štite od problema s memorijom

Mnogi problemi opisani u prvoj polovici ovog članka mogu se označiti korištenjem strogih opcija kompajlera. Na gccprimjer, nedavna izdanja nude alate poput AddressSanitizer ("ASAN") kao opciju kompilacije za provjeru protiv uobičajenih pogrešaka u upravljanju memorijom.

Upozorite, ovi alati ne zahvaćaju baš sve. Oni su zaštitne ograde; ne hvataju se za volan ako idete van ceste. Također, neki od ovih alata, poput ASAN-a, nameću troškove sastavljanja i vremena izvođenja, pa ih treba izbjegavati u verzijama izdanja.

Koristite Cppcheck ili Valgrind za analizu C koda na curenje memorije

Tamo gdje sami prevoditelji ne uspijevaju, drugi alati ulaze kako bi popunili prazninu - posebno kada je riječ o analizi ponašanja programa u vrijeme izvođenja.

Cppcheck izvodi statičku analizu izvornog koda C kako bi potražio uobičajene pogreške u upravljanju memorijom i nedefiniranim ponašanjem (između ostalog).

Valgrind nudi predmemoriju alata za otkrivanje pogrešaka memorije i niti u pokrenutim C programima. Ovo je daleko moćnije od korištenja analize vremena kompajliranja, jer možete izvući informacije o ponašanju programa kada je zapravo uživo. Loša strana je što se program izvršava djelićem njegove normalne brzine. Ali ovo je općenito u redu za testiranje.

Ovi alati nisu srebrni meci i neće uhvatiti sve. Ali oni djeluju kao dio opće obrambene strategije protiv lošeg upravljanja pamćenjem u C.

Automatizirajte upravljanje memorijom C pomoću sakupljača smeća

Budući da su pogreške u memoriji vidljiv izvor C problema, evo jednog jednostavnog rješenja: Nemojte ručno upravljati memorijom u C-u. Koristite sakupljač smeća. 

Da, to je moguće u C. Možete koristiti nešto poput sakupljača smeća Boehm-Demers-Weiser da biste dodali automatsko upravljanje memorijom u programe C. Za neke programe upotreba Boehmovog kolektora može čak i ubrzati stvari. Može se čak koristiti i kao mehanizam za otkrivanje curenja.

Glavni nedostatak Boehmovog sakupljača smeća je taj što ne može skenirati ili osloboditi memoriju koja koristi zadanu postavku malloc. Koristi vlastitu funkciju dodjele i radi samo na memoriji koju ste dodijelili s njom.

Ne upotrebljavajte C kad to može učiniti drugi jezik

Neki ljudi pišu na C jeziku jer istinski uživaju i smatraju plodnim. U cjelini, međutim, najbolje je upotrebljavati C samo kad morate, i to vrlo rijetko, za nekoliko situacija u kojima je to zaista idealan izbor.

Ako imate projekt u kojem će izvedbu ograničavati uglavnom I / O ili pristup disku, pisanje u C vjerojatno ga neće učiniti bržim na najvažnije načine, a vjerojatno će ga samo učiniti sklonijim greškama i teškim za održavati. Isti bi program mogao biti napisan na jeziku Go ili Python.

Drugi je pristup upotreba C-a samo za dijelove aplikacije koji zahtijevaju izvedbu , a pouzdaniji, iako sporiji jezik za ostale dijelove. Opet, Python se može koristiti za umotavanje C biblioteka ili prilagođenog C koda, što ga čini dobrim izborom za više komponenata kao što je upravljanje opcijama naredbenog retka.