Kodėl C ++ programuotojai sumažina „naujo“ naudojimą?

Aš atėjau į klausimą "kamino perpildymas". Atminties nutekėjimas su std :: string naudojant std :: list <std :: string> ir vienas komentaras sako:

Nenaudokite new tiek daug. Nematau jokios priežasties, kodėl jūs naudojote naują, kad ir kur esate. Galite sukurti objektus pagal vertę C ++ ir tai yra vienas iš svarbiausių kalbos naudojimo privalumų. Jums nereikia viską paskirstyti krūvoje. Nustokite galvoti kaip „Java“ programuotojas.

Aš nesu tikras, ką jis reiškia. Kodėl objektai turėtų būti sukuriami pagal vertę C + +, kaip įmanoma dažniau, ir kokį skirtumą ji daro viduje? Aš neteisingai supratau atsakymą?

757
28 июня '11 в 3:08 2011-06-28 03:08 bitgarden yra nustatytas birželio 28 d. 11 val. 03:08 2011-06-28 03:08
@ 17 atsakymų

Yra du plačiai naudojami atminties paskirstymo metodai: automatinis paskirstymas ir dinaminis paskirstymas. Paprastai kiekvienoje atminties srityje yra atitinkama atminties sritis: kamino ir krūvos.

Stack

Stack visada nuosekliai skiria atmintį. Jis gali tai padaryti, nes jis turi atlaisvinti atmintį atvirkštine tvarka („First-In“, „Last-Out: FILO“). Tai atminties paskirstymo metodas vietiniams kintamiesiems daugelyje programavimo kalbų. Tai labai, labai greitai, nes tam reikalingos minimalios finansinės ataskaitos, o šis paskirstymo adresas yra numanomas.

„C ++“ tai vadinama automatiniu saugojimu, nes saugojimas automatiškai paskelbiamas taikymo srities pabaigoje. Kai tik bus įvykdytas dabartinis kodo blokas (su separatoriumi naudojant {} ), visų šio bloko kintamųjų atmintis bus automatiškai surenkama. Tai yra ir momentas, kai destruktoriai kviečiami išvalyti išteklius.

Krūva

„Heap“ leidžia naudoti lankstesnį atminties paskirstymo režimą. Buhalterinė apskaita yra sudėtingesnė, o platinimas yra lėtesnis. Kadangi nėra implicitinio išleidimo taško, turite rankiniu būdu atlaisvinti atmintį naudodami delete arba delete[] ( free C). Tačiau netiesioginio išleidimo taško trūkumas yra raktas į lankstumą.

Dinaminio paskirstymo priežastys

Net jei krūvos naudojimas yra lėtesnis ir galbūt sukelia atminties nutekėjimą ar atminties susiskaidymą, dinamiškam paskirstymui naudojami labai gerai, nes jie yra mažiau riboti.

Dvi pagrindinės dinaminės paskirstymo priežastys yra šios:

  • Jūs nežinote, kiek atminties reikia kompiliavimo metu. Pvz., Skaitant tekstinį failą į eilutę, paprastai nežinote, kokio dydžio failas yra, todėl negalite nuspręsti, kiek atminties bus skiriama prieš pradedant programą.

  • Norite priskirti atmintį, kuri bus išsaugota išeinant iš dabartinio bloko. Pvz., Galite parašyti funkcijų string readfile(string path) failą string readfile(string path) , kuris grąžina failo turinį. Tokiu atveju, net jei kamino sudėtyje gali būti visas failo turinys, negalite grįžti iš funkcijos ir išsaugoti priskirtą atminties bloką.

Kodėl dažnai nereikia dinaminio paskyrimo

C ++ yra tvarkingas statinys, vadinamas destruktoriumi. Šis mechanizmas leidžia valdyti išteklius, suderinant išteklių išteklius su kintamojo gyvavimo trukme. Šis metodas vadinamas RAII ir yra C + + ženklas. Jis „apgaubia“ išteklius objektams. std::string yra puikus pavyzdys. Šis fragmentas yra:

 int main ( int argc, char* argv[] ) { std::string program(argv[0]); } 

iš tikrųjų skiria kintamą kiekį atminties. std::string objektas skiria atmintį, naudodamas krūvą, ir atlaisvina jį savo destruktoriuje. Tokiu atveju nereikėjo rankiniu būdu valdyti jokių išteklių ir pasinaudoti dinaminio atminties paskirstymo privalumais.

Visų pirma tai reiškia, kad šiame fragmente:

 int main ( int argc, char* argv[] ) { std::string * program = new std::string(argv[0]); // Bad! delete program; } 

yra nereikalingas dinaminės atminties paskirstymas. Programai reikia daugiau įvesties (!) Ir įvedama rizika užmiršti atlaisvinti atmintį. Jis tai daro be jokios akivaizdžios naudos.

Kodėl turėtumėte naudoti kuo dažniau automatinį saugojimą

Iš esmės paskutinė pastraipa apibendrinama. Naudojant automatinį saugojimą, kaip įmanoma dažniau, jūsų programos:

  • greitesnis spausdinimas;
  • greičiau paleidžiant;
  • mažiau linkę atminties / išteklių nutekėjimui.

Premijos taškai

Šiuo klausimu kyla papildomų problemų. Visų pirma ši klasė:

 class Line { public: Line(); ~Line(); std::string* mString; }; Line::Line() { mString = new std::string("foo_bar"); } Line::~Line() { delete mString; } 

Iš tikrųjų yra daug rizikingiau naudoti nei:

 class Line { public: Line(); std::string mString; }; Line::Line() { mString = "foo_bar"; // note: there is a cleaner way to write this. } 

Taip yra todėl, kad std::string teisingai apibrėžia kopijavimo konstruktorių. Apsvarstykite šią programą:

 int main () { Line l1; Line l2 = l1; } 

Naudojant pradinę versiją, ši programa greičiausiai nepavyks, nes ji du kartus delete vieną eilutę. Naudojant modifikuotą versiją, kiekviena Line instancija turės savo eilutės pavyzdį, kiekviena turės savo atmintį, ir abi programos bus paleistos programos pabaigoje.

Kitos pastabos

Platus RAII naudojimas yra laikomas geriausia C ++ patirtimi dėl visų minėtų priežasčių. Tačiau yra papildomas pranašumas, kuris iš karto nepasireiškia. Iš esmės tai yra geriau nei jos dalių suma. Sudarė visą mechanizmą. Jis matuoja.

Jei naudojate Line klasę kaip statybinį bloką:

  class Table { Line borders[4]; }; 

Tada

  int main () { Table table; } 

skiria keturis std::string , keturius Line , vieną Table egzempliorių ir visą eilutės turinį, ir viskas automatiškai išlaisvinama.

907
28 июня '11 в 3:47 2011-06-28 03:47 atsakymą pateikė André Caron birželio 28 d. 11 val. 03:47 2011-06-28 03:47

Kadangi kaminas yra greitas ir patikimas

border=0

C ++, kiekvienai vietinei sričiai tam tikroje funkcijoje, reikalinga tik viena komanda, skirta priskirti erdvę - ant kamino, ir bet kuri iš šios atminties negali būti nutekėjusi. Šiame komentare siūloma (arba turėjo būti) pasakyti kažką panašaus į „naudoti kamino, o ne krūva“.

155
28 июня '11 в 3:14 2011-06-28 03:14 atsakymą pateikė „ DigitalRoss “ birželio 28 d. 11 d. 3:14 2011-06-28 03:14

Sunku.

Pirma, „C ++“ nerenka šiukšlių. Todėl kiekvienam naujam turi būti atitinkamas pašalinimas. Jei nepavyks įvesti šio ištrynimo, turėsite atminties. Dabar paprastu atveju:

 std::string *someString = new std::string(...); //Do stuff delete someString; 

Tai paprasta. Bet kas atsitiks, jei „Do stuff“ išmeta išimtį? Oi: atminties nutekėjimas. Kas atsitiks, jei „Do things“ return anksčiau? Atsiprašome, atminties nutekėjimas.

Ir tai yra paprasčiausias atvejis. Jei sugebėsite grąžinti šią eilutę kitam asmeniui, jie turi ištrinti. Ir jei jie perduoda jį kaip argumentą, ar jį gaunantis asmuo jį ištrins? Kada jie turėtų pašalinti?

Arba galite tai padaryti:

 std::string someString(...); //Do stuff 

Nėra delete . Objektas buvo sukurtas ant kamino, ir jis bus sunaikintas po jo. Jūs netgi galite grąžinti objektą, perduodant jo turinį į skambinimo funkciją. Objektą galite perkelti į funkciją (kaip taisyklę, kaip nuorodą arba const-link: void SomeFunc(std::string const std::string > ir tt

Visi be new ir delete . Nėra abejonių, kas turi atmintį arba kas yra atsakingas už jo pašalinimą. Jei paleisite:

 std::string someString(...); std::string otherString; otherString = someString; 

Akivaizdu, kad „ otherString turi tam tikros informacijos someString . Tai nėra rodyklė; Tai yra atskiras objektas. Jie gali turėti tą patį turinį, tačiau galite jį pakeisti nepaveikdami kitų:

 someString += "More text."; if(otherString == someString) {  } 

Žr. Idėją?

95
28 июня '11 в 3:17 2011-06-28 03:17 atsakymą davė Nicol Bolas birželio 28 d. 11 d. 03:17 2011-06-28 03:17

new objektų sukurti daiktai galiausiai turi delete d, kad jie nebūtų nutekėję. Sunaikintojas nebus vadinamas, atmintis nebus išlaisvinta, visa bitė. Kadangi C ++ nėra šiukšlių surinkimo, tai yra problema.

Objektai, sukurti pagal vertę (ty ant kamino), automatiškai miršta, kai jie viršija. Kompiliatorius įterpia destruktyvų skambutį, o atmintis automatiškai atleidžiama po funkcijos grąžinimo.

Išmanieji rodikliai, pvz., auto_ptr , shared_ptr , išsprendžia problemą, susijusią su kabančiu ryšiu, tačiau jiems reikia kodavimo disciplinos ir yra ir kitų problemų (kopijavimo, nuorodų ciklų ir tt).

Be to, esant labai daugiamandatėms scenarijoms, new dalykas yra gija tarp sričių; gali pasireikšti poveikis pernelyg new naudojimui. Sukuriant kamino objektą, pagal sąvoką yra vietinė vietovė, nes kiekvienas siūlelis turi savo kamino.

Vertybinių objektų trūkumas yra tas, kad jie miršta po to, kai grįžta į priimančiąją funkciją - jūs negalite siųsti nuorodos tiems, kurie grįžta į skambintoją, tiesiog kopijuodami ar grąžindami pagal vertę.

66
28 июня '11 в 3:11 2011-06-28 03:11 atsakymą Seva Aleksejevas pateikė birželio 28 d. 11 d. 3:11 2011-06-28 03:11
  • C ++ nenaudoja pačios atminties tvarkyklės. Kitos kalbos, pvz., „C #“, „Java“, turi šiukšlių surinktuvą atminties tvarkymui.
  • C + +, naudojant operacinės sistemos rutinus, skiriant atmintį ir per daug naujų / ištrinti gali fragmentuoti turimą atmintį
  • Bet kokioje taikomojoje programoje, jei atmintis naudojama dažnai, rekomenduojama jį pirmiausia skirti ir atleisti, kai to nereikia.
  • Netinkamas atminties valdymas gali sukelti atminties nutekėjimą ir labai sunku stebėti. Tokiu būdu, naudojant stack objektus, funkcija yra įrodyta technika.
  • Stekų objektų panaudojimo trūkumas yra tai, kad grįžtamosios, perkėlimo funkcijos ir pan. Tačiau intelektualūs kompiliatoriai gerai žino apie šias situacijas ir yra gerai optimizuoti našumui.
  • Tai tikrai varginantis C ++, jei atmintis skiriama ir išleista dviejose skirtingose ​​vietose. Atsakomybė už išleidimą visada yra problema, ir mes daugiausia pasikliaujame tam tikrais viešais rodikliais, stekų objektais (didžiausiais įmanomais) ir metodais, pvz., Auto_ptr (RAII objektai)
  • Geriausia yra tai, kad valdote atmintį, o blogiausia yra tai, kad neturėsite jokios atminties kontrolės, jei taikysime klaidingą atminties valdymą. Atminties gedimai yra labiausiai bjaurus ir sunku stebėti.
27
28 июня '11 в 5:59 2011-06-28 05:59 atsakymą pateikė Saratas birželio 28 d. 11 d. 5:59 2011-06-28 05:59

Matau, kad praleidžiama keletas svarbių priežasčių, kodėl reikia kuo mažiau naujų:

new operatorius turi nenuoseklią vykdymo laiką.

new skambinimas gali arba nesukelia OS priskirti savo procesui naują fizinį puslapį, jis gali būti gana lėtas, jei tai darote dažnai. Arba jis jau gali turėti tinkamą vietą atminties, mes nežinome. Jei jūsų programoje turi būti nuoseklus ir nuspėjamas vykdymo laikas (pvz., Realiu laiku arba žaidimo / fizinio modeliavimo metu), kritiniuose laiko cikluose reikia vengti new .

new operatorius yra netiesioginis siūlų sinchronizavimas.

Taip, jūs girdėjote mane, jūsų OS turėtų įsitikinti, kad jūsų puslapių lentelės yra nuoseklios, ir todėl, kad toks skambutis į new jūsų siūlą sukurs netiesioginį užrakto užraktą. Jei nuosekliai vadinate new iš daugelio sriegių, jūs iš tikrųjų serializuojate temas (aš tai padariau su 32 procesoriais, kurių kiekvienas gavo new kad gautumėte kelis šimtus baitų kiekvienas, ty! Tai buvo karališkoji pita derinimui)

Likusi dalis, pvz., Lėtas, susiskaidymas, klaidų polinkis ir tt, jau buvo paminėti kituose atsakymuose.

16
12 февр. Atsakymą pateikė Emily L. vasario 12 d. 2014-02-12 20:57 '14 at 20:57 2014-02-12 20:57

Daugeliu atvejų, kuris kelia trūkumus bendroje taisyklėje. Nėra nieko blogo kuriant objektus su new operatoriumi. Kai kuriems yra argumentas, kad jūs turite tai padaryti su tam tikra disciplina: jei kuriate objektą, turite įsitikinti, kad jis bus sunaikintas.

Paprasčiausias būdas tai padaryti yra sukurti objektą automatinėje saugykloje, todėl „C ++“ žino, kaip ją sunaikinti, kai jis išeina iš taikymo srities:

  { File foo = File("foo.dat"); // do things } 

Atkreipkite dėmesį, kad po to, kai baigsite šį bloką po galutinio laikiklio, foo viršija. „C + +“ automatiškai paskambins jūsų dtoriui. Skirtingai nei „Java“, nereikia laukti, kol GC jį suras.

Jei parašėte

  { File * foo = new File("foo.dat"); 

norite aiškiai suderinti

  delete foo; } 

arba dar geriau, paryškinkite File * kaip protingą rodyklę. Jei nesate atsargūs, tai gali sukelti nuotėkį.

Atsakymas pats klaidingai numato, kad jei jūs nenaudojate new , nesuteikiate krūva; iš tiesų, C + + jūs to nežinote. Geriausiu atveju, jūs žinote, kad mažai atminties kiekio, tarkim, vienas žymeklis, žinoma, skiriamas kaminai. Tačiau apsvarstykite, ar failo įgyvendinimas yra panašus

  class File { private: FileImpl * fd; public: File(String fn){ fd = new FileImpl(fn);} 

tada „ FileImpl “ vis dar bus priskirtas kaminai.

Ir taip, jūs turite būti tikri

  ~File(){ delete fd ; } 

klasėje; be jo, jūs atminsite nuotėkį iš krūvos, net jei akivaizdžiai nepadarėte krūvos.

16
28 июня '11 в 3:11 2011-06-28 03:11 atsakymą pateikė Charlie Martin birželio 28 d. 11 d. 3:11 2011-06-28 03:11

Kai naudojate naują, objektai priskiriami krūvai. Paprastai jis naudojamas, kai laukiate pratęsimo. Pareiškus objektą, pvz.,

 Class var; 

jis yra stumiamas ant kamino.

Jūs visada turite skambinti sunaikinti ant objekto, kurį įdėjote į krūvą su nauju. Tai atveria atminties nutekėjimo galimybę. Objektai, stumiami ant kamino, nėra jautrūs atminties nuotėkiui!

13
28 июня '11 в 3:10 2011-06-28 03:10 atsakymą pateikė Tim birželio 28 d. 11 val. 03:10 2011-06-28 03:10

Pre-C ++ 17:

Nes jis yra linkęs ploniems nuotėkiams, net jei rezultatą suvyniote su protingu žymikliu.

Apsvarstykite „atsargų“ naudotoją, kuris prisimena apvynioti objektus į protingus žymenis:

 foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2())); 

Šis kodas yra pavojingas, nes nėra jokių garantijų, kad shared_ptr bus sukurtas prieš T1 arba T2 . Todėl, jei vienas iš new T1() arba new T2() nepavyksta po to, kai pavyks, tada pirmasis objektas nutekės, nes shared_ptr egzistuoja, kad jį sunaikintų ir išlaisvintų.

Sprendimas: naudokite make_shared .

Post-C ++ 17:

Tai nebėra problema: C ++ 17 nustato šių operacijų tvarką, šiuo atveju užtikrindamas, kad kiekvienas skambutis į new() turi būti nedelsiant pridedamas prie atitinkamo sumanaus žymeklio statybos be jokios kitos operacijos. Tai reiškia, kad iki to laiko, kai new() iškviestas antrasis new() , garantuojama, kad pirmasis objektas jau buvo supakuotas į savo protingą žymiklį, kuris neleis nutekėti išimties atveju.

Barry pateikė išsamesnį C ++ 17 pateiktos naujos vertinimo tvarkos paaiškinimą.

13
15 авг. atsakymą pateikė Mehrdad 15 rug. 2013-08-15 00:22 '13 - 0:22 2013-08-15 00:22

new() neturėtų būti naudojamas kuo mažiau. Jis turėtų būti naudojamas kuo atidžiau. Ir jis turėtų būti naudojamas taip dažnai, kaip reikia, kaip diktuoja pragmatizmas.

Objektų pasiskirstymas kaminai, remiantis jų numanomu sunaikinimu, yra paprastas modelis. Jei reikiamo objekto apimtis tinka šiam modeliui, tuomet nereikia naudoti new() , su delete() įpareigojimu ir NULL rodyklių tikrinimu. Tuo atveju, kai turite daug trumpalaikių objektų, skirstymas į kaminą turėtų sumažinti krūvos fragmentacijos problemas.

Tačiau, jei jūsų objekto tarnavimo laikas turi viršyti dabartinę sritį, tada new() yra teisingas atsakymas. Tiesiog įsitikinkite, kad atkreipiate dėmesį į tai, kada ir kaip skambinate delete() , ir į NULL rodyklių galimybes, naudodami nuotolinius objektus ir visas kitas klaidas, susijusias su nuorodų naudojimu.

13
28 июня '11 в 3:38 2011-06-28 03:38 atsakymą pateikė Andrew Edgecombe , birželio 28 d. 11, 03:38 2011-06-28 03:38

Aš linkęs nesutikti su idėja naudoti naują „per daug“. Nors originalus naujas plakatas su sistemos klasėmis yra šiek tiek juokingas. ( int *i; i = new int[9999]; tikrai? int i[9999]; daug aiškiau.) Manau, kad tai buvo Cocker komentatorius.

Dirbdami su sistemos objektais, labai retai jums reikės daugiau nei vienos nuorodos į tą patį objektą. Tol, kol reikšmė yra tokia pati, viskas svarbu. Ir sistemos objektai paprastai neatima daug vietos atmintyje. (vienas baitas už simbolį, už eilutę). Ir jei jie tai padarys, bibliotekos turėtų būti suprojektuotos taip, kad atsiskaitytų už šį atminties valdymą (jei jie gerai parašyti). Tokiais atvejais (viskas, išskyrus vieną ar dvi naujienas jo kode) yra beveik beprasmiška ir skirta tik supainioti ir klaidoms.

Tačiau dirbdami su savo klasėmis / objektais (pvz., Pradine „Line“ plakatų klase), turėtumėte pradėti galvoti apie tokias problemas kaip atmintis, duomenų išlikimas ir kt. savarankiškai. Šiuo metu neįkainojama naudoti kelias nuorodas į tą pačią vertę - tai leidžia sukurti tokias struktūras kaip susieti sąrašai, žodynai ir grafikai, kai keli kintamieji turi turėti ne tik tą pačią vertę, bet ir tą patį objektą atmintyje. Tačiau linijos klasė neturi jokių šių reikalavimų. Taigi plakato šaltinis yra visiškai nereikalingas new .

10
28 июня '11 в 8:10 2011-06-28 08:10 atsakymą pateikė Chrisas Hayesas birželio 11 d. 11 val. 08:10 2011-06-28 08:10

Manau, kad plakatas reiškė, kad You do not have to allocate everything on the heap , o ne stack .

Apskritai daiktai priskiriami kaminai (jei, žinoma, objekto dydis leidžia) dėl pigios stekų paskirstymo kainos, o ne krūvos paskirstymo, kuriam reikalingas didelis paskirstytojo darbas, ir pridedamas verbiškumas, nes tuomet turite valdyti duomenis, skiriamus krūva.

10
28 июня '11 в 3:14 2011-06-28 03:14 atsakymą pateikė Khaled Nassar, birželio 28 d., 11 d., 03:14 2011-06-28 03:14

Viena iš svarbiausių priežasčių išvengti pernelyg didelio krūvio panaudojimo, visų pirma yra C ++ naudojamo numatytojo atminties valdymo mechanizmo naudojimas. Хотя распределение может быть довольно быстрым в тривиальном случае, выполнение большого количества new и delete на объектах неравномерного размера без строгого порядка приводит не только к фрагментации памяти, но также усложняет алгоритм распределения и может полностью уничтожить производительность в определенных случаях.

Проблема с тем, что пулы памяти , которые созданы для решения, позволяя смягчить присущие ему недостатки традиционных реализаций кучи, в то же время позволяя вам используйте кучу по мере необходимости.