On monia sudenkuoppia, jotka a C ++ -kehittäjä voi kohdata. Tämä voi tehdä laadukkaasta ohjelmoinnista erittäin vaikeaa ja ylläpidosta erittäin kallista. Kielen syntaksin oppiminen ja hyvät ohjelmointitaidot samankaltaisilla kielillä, kuten C # ja Java, eivät vain riitä C ++: n koko potentiaalin hyödyntämiseen. Se vaatii vuosien kokemusta ja hyvää kurinalaisuutta virheiden välttämiseksi C ++: ssa. Tässä artikkelissa aiomme tarkastella joitain yleisiä virheitä, joita kaikkien tasojen kehittäjät tekevät, jos he eivät ole tarpeeksi varovaisia C ++ -kehityksen kanssa.
Riippumatta siitä kuinka paljon yritämme, kaiken dynaamisesti varatun muistin vapauttaminen on erittäin vaikeaa. Vaikka voimme tehdä sen, se ei ole usein turvallista poikkeuksilta. Katsotaanpa yksinkertaista esimerkkiä:
void SomeMethod() { ClassA *a = new ClassA; SomeOtherMethod(); // it can throw an exception delete a; }
Jos heitetään poikkeus, a-objektia ei koskaan poisteta. Seuraava esimerkki osoittaa turvallisemman ja lyhyemmän tavan tehdä se. Se käyttää auto_ptr: tä, joka on vanhentunut C ++ 11: ssä, mutta vanhaa standardia käytetään edelleen laajalti. Se voidaan korvata Boostin C ++ 11 unique_ptr: llä tai scoped_ptr: llä, jos mahdollista.
void SomeMethod() { std::auto_ptr a(new ClassA); // deprecated, please check the text SomeOtherMethod(); // it can throw an exception }
Riippumatta siitä, mitä tapahtuu, 'a' -objektin luomisen jälkeen se poistetaan heti, kun ohjelman toteutus poistuu laajuudesta.
Tämä oli kuitenkin vain yksinkertaisin esimerkki tästä C ++ -ongelmasta. On monia esimerkkejä, kun poistaminen tulisi tehdä jossakin muussa paikassa, ehkä ulkoisessa toiminnossa tai toisessa säikeessä. Siksi uuden / poiston käyttöä pareittain tulisi välttää kokonaan ja sen sijaan tulisi käyttää sopivia älykkäitä osoittimia.
Tämä on yksi yleisimmistä virheistä, joka johtaa muistivuotoihin johdettujen luokkien sisällä, jos niiden sisällä on varattu dynaamista muistia. Joissakin tapauksissa virtuaalinen tuhoaja ei ole toivottavaa, ts. Kun luokkaa ei ole tarkoitettu perinnöksi ja sen koko ja suorituskyky ovat ratkaisevan tärkeitä. Virtuaalituhooja tai mikä tahansa muu virtuaalinen toiminto tuo lisätietoa luokan rakenteeseen, ts. Osoitin virtuaaliseen taulukkoon, joka tekee minkä tahansa luokan esiintymän koosta suuremman.
Useimmissa tapauksissa luokat voidaan periä, vaikka sitä ei olekaan alun perin tarkoitettu. Joten on erittäin hyvä käytäntö lisätä virtuaalinen tuhoaja, kun luokka ilmoitetaan. Muussa tapauksessa, jos luokka ei saa sisältää virtuaalisia toimintoja suorituskykysyistä, on hyvä käytäntö laittaa kommentti luokkatiedostoon sisältäen, että luokkaa ei pitäisi periä. Yksi parhaista vaihtoehdoista tämän ongelman välttämiseksi on käyttää IDE: tä, joka tukee virtuaalisen tuhoajan luomista luokan luomisen aikana.
Yksi lisäkohta aiheeseen ovat luokat / mallit vakiokirjastosta. Niitä ei ole tarkoitettu perinnöksi, eikä niillä ole virtuaalista tuhoajaa. Jos esimerkiksi luomme uuden parannetun merkkijonoluokan, joka perii julkisesti std :: stringistä, on mahdollista, että joku käyttää sitä väärin osoittimen tai viitteen kanssa std :: merkkijonoon ja aiheuttaa muistivuodon.
class MyString : public std::string { ~MyString() { // ... } }; int main() { std::string *s = new MyString(); delete s; // May not invoke the destructor defined in MyString }
Tällaisten C ++ -ongelmien välttämiseksi turvallisempi tapa käyttää luokan / mallin vakiokirjastosta on käyttää yksityistä perintöä tai sommittelua.
Dynaamisen kokoisten väliaikaisten taulukoiden luominen on usein tarpeen. Kun niitä ei enää tarvita, on tärkeää vapauttaa varattu muisti. Suuri ongelma tässä on se, että C ++ vaatii erityisen poisto-operaattorin [] suluilla, mikä unohdetaan helposti. Delete [] -operaattori ei vain poista taulukkoon varattua muistia, mutta se kutsuu ensin kaikki objektin destruktorit taulukosta. On myös väärin käyttää poisto-operaattoria ilman hakasulkeita alkukantatyypeille, vaikka näille tyypeille ei ole hävittäjää. Jokaiselle kääntäjälle ei ole takeita siitä, että taulukon osoitin osoittaa matriisin ensimmäiseen elementtiin, joten poista ilman [] -sulkeita käyttäminen voi johtaa myös määrittelemättömään toimintaan.
Älykkäiden osoittimien, kuten auto_ptr, ainutlaatuinen_ptr, jaettu_ptr, käyttö taulukoiden kanssa on myös väärin. Kun tällainen älykäs osoitin poistuu alueelta, se kutsuu poisto-operaattoria ilman sulkeita, jotka johtavat samoihin edellä kuvattuihin ongelmiin. Jos matriisiin vaaditaan älykkään osoittimen käyttö, on mahdollista käyttää Boostin scoped_array- tai shared_array- tai unique_ptr-erikoistumista.
Jos referenssilaskennan toiminnallisuutta ei vaadita, mikä pätee enimmäkseen matriisien tapauksessa, tyylikkäin tapa on käyttää STL-vektoreita. He eivät vain huolehdi muistin vapauttamisesta, vaan tarjoavat myös lisätoimintoja.
Tämä on enimmäkseen aloittelijan virhe, mutta se on syytä mainita, koska tästä ongelmasta kärsii paljon vanhoja koodeja. Katsotaanpa seuraavaa koodia, jossa ohjelmoija halusi tehdä jonkinlaisen optimoinnin välttämällä tarpeetonta kopiointia:
Complex& SumComplex(const Complex& a, const Complex& b) { Complex result; ….. return result; } Complex& sum = SumComplex(a, b);
Objekti 'summa' osoittaa nyt paikalliseen kohteeseen 'tulos'. Mutta missä kohde 'tulos' sijaitsee SumComplex-toiminnon suorittamisen jälkeen? Ei mihinkään. Se sijaitsi pinossa, mutta toiminnon palauttamisen jälkeen pino purettiin ja kaikki toiminnon paikalliset objektit tuhoutuivat. Tämä johtaa lopulta määrittelemättömään käyttäytymiseen, jopa primitiivisille tyyppeille. Suorituskykyongelmien välttämiseksi on joskus mahdollista käyttää palautusarvon optimointia:
Complex SumComplex(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imaginar + b.imaginar); } Complex sum = SumComplex(a, b);
Suurimmalle osalle tämän päivän kääntäjistä, jos paluulinja sisältää objektin rakentajan, koodi optimoidaan tarpeettoman kopioinnin välttämiseksi - konstruktori suoritetaan suoraan 'summa' -objektille.
Näitä C ++ -ongelmia esiintyy useammin kuin luulet, ja ne näkyvät yleensä monisäikeisissä sovelluksissa. Tarkastellaan seuraavaa koodia:
Kierre 1:
Connection& connection= connections.GetConnection(connectionId); // ...
Kierre 2:
connections.DeleteConnection(connectionId); // …
Kierre 1:
connection.send(data);
Tässä esimerkissä, jos molemmat säikeet käyttivät samaa yhteystunnusta, tämä johtaa määrittelemättömään toimintaan. Käyttöoikeusloukkausvirheitä on usein vaikea löytää.
Näissä tapauksissa, kun useampia kuin yksi ketju käyttää samaa resurssia, on erittäin vaarallista pitää viitteitä tai viitteitä resursseihin, koska jokin muu ketju voi poistaa sen. On paljon turvallisempaa käyttää älykkäitä osoittimia viitteiden laskennalla, esimerkiksi jaettu_ptr Boostilta. Se käyttää atomioperaatioita vertailulaskurin kasvattamiseen / pienentämiseen, joten se on säiettä turvallinen.
Ei ole usein tarpeen heittää poikkeusta tuhoajasta. Silloinkin on olemassa parempi tapa tehdä se. Poikkeuksia ei kuitenkaan useinkaan heitetä hävittäjiltä nimenomaisesti. Voi tapahtua, että yksinkertainen komento objektin tuhon kirjaamiseksi aiheuttaa poikkeuksen heiton. Tarkastellaan seuraavaa koodia:
class A { public: A(){} ~A() { writeToLog(); // could cause an exception to be thrown } }; // … try { A a1; A a2; } catch (std::exception& e) { std::cout << 'exception caught'; }
Yllä olevassa koodissa, jos poikkeusta esiintyy kahdesti, esimerkiksi molempien objektien tuhoutumisen aikana, saalislauseketta ei koskaan suoriteta. Koska on olemassa kaksi rinnakkaista poikkeusta, riippumatta siitä, ovatko ne samantyyppisiä vai erityyppisiä, C ++ -ajoympäristö ei tiedä miten sitä käsitellään ja kutsuu lopetustoimintoa, joka johtaa ohjelman suorituksen lopettamiseen.
Joten yleinen sääntö on: Älä koskaan anna poikkeusten jättää tuhoajia. Vaikka se olisi ruma, mahdollinen poikkeus on suojattava näin:
try { writeToLog(); // could cause an exception to be thrown } catch (...) {}
Auto_ptr-malli on poistettu luokasta C ++ 11 useista syistä. Sitä käytetään edelleen laajalti, koska suurinta osaa hankkeista kehitetään edelleen C ++ 98 -järjestelmässä. Sillä on tietty ominaisuus, joka ei todennäköisesti tunnu kaikille C ++ -kehittäjille, ja se voi aiheuttaa vakavia ongelmia jollekulle, joka ei ole varovainen. Auto_ptr-objektin kopiointi siirtää omistajuuden objektista toiseen. Esimerkiksi seuraava koodi:
auto_ptr a(new ClassA); // deprecated, please check the text auto_ptr b = a; a->SomeMethod(); // will result in access violation error
… Johtaa käyttöoikeusrikkovirheeseen. Ainoa objekti “b” sisältää osoittimen luokan A objektille, kun taas ”a” on tyhjä. Yritetään käyttää objektin ”a” luokan jäsentä, mikä johtaa käyttöoikeusrikkovirheeseen. On olemassa monia tapoja käyttää auto_ptr väärin. Neljä hyvin kriittistä asiaa, jotka on muistettava niistä:
Älä koskaan käytä auto_ptr STL-säiliöiden sisällä. Konttien kopiointi jättää lähdekontit virheellisiksi. Jotkut STL-algoritmit voivat myös johtaa “auto_ptr”: n kumoamiseen.
Älä koskaan käytä auto_ptr: tä funktion argumenttina, koska se johtaa kopiointiin ja jätä argumentille välitetty arvo virheelliseksi funktion kutsun jälkeen.
Jos auto_ptr: tä käytetään luokan datajäsenille, muista tehdä oikea kopio kopionrakentajasta ja tehtäväoperaattorista tai kieltää nämä toiminnot tekemällä niistä yksityisiä.
Aina kun mahdollista, käytä jotain muuta nykyaikaista älyosoitinta auto_ptr: n sijaan.
Olisi mahdollista kirjoittaa koko kirja tästä aiheesta. Jokaisella STL-säilöllä on joitain erityisehtoja, joissa se mitätöi iteraattorit ja viitteet. On tärkeää olla tietoinen näistä yksityiskohdista käyttäessäsi mitään toimintoa. Aivan kuten edellinen C ++ -ongelma, tätä voi esiintyä myös hyvin usein monisäikeisissä ympäristöissä, joten sen välttämiseksi on käytettävä synkronointimekanismeja. Katsotaanpa seuraava järjestyskoodi esimerkkinä:
vector v; v.push_back(“string1”); string& s1 = v[0]; // assign a reference to the 1st element vector::iterator iter = v.begin(); // assign an iterator to the 1st element v.push_back(“string2”); cout << s1; // access to a reference of the 1st element cout << *iter; // access to an iterator of the 1st element
Loogisesta näkökulmasta koodi näyttää täysin hyvältä. Toisen elementin lisääminen vektoriin voi kuitenkin johtaa vektorin muistin uudelleenjakautumiseen, mikä tekee sekä iteraattorin että viitteen virheelliseksi ja johtaa pääsyrikkovirheeseen, kun yritetään käyttää niitä kahdella viimeisellä rivillä.
Luultavasti tiedät, että on huono idea siirtää esineitä arvojen perusteella niiden suorituskykyvaikutusten vuoksi. Monet jättävät sen niin välttääkseen ylimääräisten merkkien kirjoittamisen tai luultavasti ajattelevat palata myöhemmin tekemään optimoinnin. Se ei yleensä koskaan onnistu, ja sen seurauksena johtaa vähemmän suorituskykyiseen koodiin ja odottamattomaan käyttäytymiseen alttiiseen koodiin:
class A { public: virtual std::string GetName() const {return 'A';} … }; class B: public A { public: virtual std::string GetName() const {return 'B';} ... }; void func1(A a) { std::string name = a.GetName(); ... } B b; func1(b);
Tämä koodi kootaan. Funktion “func1” kutsuminen luo osittaisen kopion objektista ”b”, ts. Se kopioi vain luokan “A” osan objektista ”b” objektiksi ”a” (”viipalointiongelma”). Joten funktion sisällä se kutsuu myös luokan 'A' menetelmää luokan 'B' menetelmän sijaan, mikä todennäköisesti ei ole sitä, mitä joku, joka kutsuu toimintoa.
Samanlaisia ongelmia esiintyy yritettäessä saada poikkeuksia. Esimerkiksi:
class ExceptionA: public std::exception; class ExceptionB: public ExceptionA; try { func2(); // can throw an ExceptionB exception } catch (ExceptionA ex) { writeToLog(ex.GetDescription()); throw; }
Kun poikkeuksen tyyppi poikkeusB heitetään funktiosta 'func2', se jää kiinni lukituslohkoon, mutta viipalointiongelman vuoksi kopioidaan vain osa ExceptionA-luokasta, väärä menetelmä kutsutaan ja myös heitetään uudelleen heittää virheellisen poikkeuksen ulkopuoliseen try-catch-lohkoon.
Yhteenvetona voidaan todeta, että välitä objektit aina viitteellä, ei arvolla.
Jopa käyttäjän määrittämät konversiot ovat joskus erittäin hyödyllisiä, mutta ne voivat johtaa ennalta arvaamattomiin konversioihin, joita on erittäin vaikea löytää. Oletetaan, että joku loi kirjaston, jolla on merkkijono-luokka:
class String { public: String(int n); String(const char *s); …. }
Ensimmäisen menetelmän on tarkoitus luoda merkkijono, jonka pituus on n, ja toisen on tarkoitus luoda merkkijono, joka sisältää annetut merkit. Mutta ongelma alkaa heti, kun sinulla on jotain tällaista:
String s1 = 123; String s2 = ‘abc’;
Yllä olevassa esimerkissä s1: stä tulee merkkijono, jonka koko on 123, ei merkkijono, joka sisältää merkkejä ”123”. Toinen esimerkki sisältää lainausmerkit kaksoislainausten sijasta (mikä voi tapahtua vahingossa), mikä johtaa myös ensimmäisen konstruktorin kutsumiseen ja erittäin suuren kokoisen merkkijonon luomiseen. Nämä ovat todella yksinkertaisia esimerkkejä, ja on monia monimutkaisempia tapauksia, jotka johtavat sekaannukseen ja ennalta arvaamattomiin muutoksiin, joita on erittäin vaikea löytää. On 2 yleistä sääntöä tällaisten ongelmien välttämiseksi:
Määritä rakentaja, jolla on eksplisiittinen avainsana, jotta estetään implisiittiset konversiot.
Muunnosoperaattoreiden sijaan käytä nimenomaisia keskustelutapoja. Se vaatii hieman enemmän kirjoittamista, mutta se on paljon puhtaampaa lukea ja voi auttaa välttämään arvaamattomia tuloksia.
C ++ on tehokas kieli. Itse asiassa monet sovellukset, joita käytät päivittäin tietokoneellasi ja joista olet rakastunut, luultavasti rakennetaan C ++: lla. Kielenä C ++ antaa a valtava määrä joustavuutta kehittäjälle eräiden hienostuneimpien ominaisuuksien kautta, jotka näkyvät olio-ohjelmointikielillä. Nämä hienostuneet ominaisuudet tai joustavuudet voivat kuitenkin usein aiheuttaa hämmennystä ja turhautumista monille kehittäjille, ellei niitä käytetä vastuullisesti. Toivottavasti tämä luettelo auttaa sinua ymmärtämään, miten jotkut näistä yleisimmistä virheistä vaikuttavat siihen, mitä voit saavuttaa C ++: lla.
Liittyvät: Kuinka oppia C- ja C ++ -kielet: Lopullinen luettelo