Kas yra trijų taisyklių taisyklė?

  • Ką reiškia objekto kopijavimas?
  • Kas yra kopijavimo kūrėjas ir kopijos priskyrimo ataskaita?
  • Kada turiu juos paskelbti?
  • Kaip galiu išvengti savo objektų kopijavimo?
1911 m
13 нояб. lapkričio 13 d 2010-11-13 16:27 '10, 16:27, 2010-11-13 16:27
@ 8 atsakymai

Įvadas

C ++ apdoroja pasirinktinius kintamuosius su semantinėmis reikšmėmis. Tai reiškia, kad objektai yra netiesiogiai nukopijuoti skirtinguose kontekstuose, ir mes turime suprasti, ką reiškia „objekto kopijavimas“.

Apsvarstykite paprastą pavyzdį:

 class person { std::string name; int age; public: person(const std::string name, int age) : name(name), age(age) { } }; int main() { person a("Bjarne Stroustrup", 60); person b(a); // What happens here? b = a; // And here? } 

(Jei nesuprantate name(name), age(age) , tai vadinama narių iniciatorių sąrašu .)

Specialios narių funkcijos

Ką reiškia person kopijavimas? main funkcija rodo du skirtingus kopijavimo scenarijus. Inicializavimo person b(a); atlieka kopijos konstruktorius. Jo užduotis yra sukurti naują objektą pagal esamo objekto būklę. Užduotį b = a atlieka kopijavimo priskyrimo operatorius. Jo darbas paprastai yra šiek tiek sunkesnis, nes tikslinis objektas jau yra priimtinoje būsenoje, kurią reikia spręsti.

Kadangi mes ne deklaravome nei kopijavimo konstruktorių, nei paskyrimo operatorių (ar destruktorių), jie yra netiesiogiai apibrėžti mums. Citata iš standarto:

Kopijavimo konstruktorius [...] ir kopijavimo priskyrimo operatorius [...] ir destruktorius yra specialios narių funkcijos. [Pastaba: įgyvendinimas netiesiogiai paskelbs šias nario funkcijas tam tikrų tipų klasėms, kai programa jų aiškiai nenurodo. Įgyvendinimas juos netiesiogiai apibrėžia, jei jie bus naudojami. [...] galutinė pastaba] [n3126.pdf 12 skirsnio 1 dalis]

Pagal nutylėjimą objekto kopijavimas reiškia jo elementų kopijavimą:

Netiesiogiai apibrėžtas kopijavimo konstruktorius ne vieneto X klasei atlieka pakopinių savo sub-objektų kopiją. [n3126.pdf 12.8 §16]

Netiesiogiai priskirtas X priskyrimo operacijų vykdytojas atlieka pakopinį sub-objektų kopijos priskyrimą. [n3126.pdf 12.8 skirsnis30]

Netiesioginės apibrėžtys

person netiesiogiai apibrėžtos specialios narių funkcijos:

 // 1. copy constructor person(const person that) : name(that.name), age(that.age) { } // 2. copy assignment operator person operator=(const person that) { name = that.name; age = that.age; return *this; } // 3. destructor ~person() { } 

Tokiu atveju norime nukopijuoti tai, ko norime: name ir age nukopijuojami, todėl gauname savarankišką, nepriklausomo person objektą. Netiesiogiai apibrėžtas destruktorius visada yra tuščias. Tai taip pat labai tinka šiuo atveju, nes mes neturėjome jokių išteklių konstruktoriuje. Nario destruktoriai yra netiesiogiai vadinami po to, kai person baigia naikintuvą:

Atlikus destruktoriaus kūną ir sunaikinus bet kokius kūno paskirtus automatinius objektus, X klasės naikintuvas sukelia X tiesioginių narių destruktorius [n3126.pdf 12.4 §6]

Išteklių valdymas

Taigi, kada turėtume aiškiai nurodyti šiuos specialius narius? Kai mūsų klasė valdo išteklius, ty kai už objektą atsako klasės objektas. Tai paprastai reiškia, kad ištekliai yra įgyti konstruktoriuje (arba perduodami konstruktoriui) ir išleidžiami į destruktorių.

Grįžkime prie preliminaraus standarto C ++. std::string , ir programuotojai įsimylėjo. Asmenų klasė gali atrodyti taip:

 class person { char* name; int age; public: // the constructor acquires a resource: // in this case, dynamic memory obtained via new[] person(const char* the_name, int the_age) { name = new char[strlen(the_name) + 1]; strcpy(name, the_name); age = the_age; } // the destructor must release this resource via delete[] ~person() { delete[] name; } }; 

Net ir šiandien žmonės vis dar rašo klases šitame stiliuje ir patenka į bėdą: „Aš stumdavau asmenį į vektorių, o dabar gaunu beprotiškų atminties klaidų!“. Atminkite, kad pagal nutylėjimą objekto kopijavimas reiškia jo elementų kopijavimą, bet kopijuoti name narį paprasčiausiai nukopijuoja žymiklį, o ne simbolių masyvą, kurį jis nurodo! Tai turi keletą nemalonių efektų:

  • Pokyčius per a galima stebėti b .
  • Kai b sunaikinta, a.name yra a.name žymeklis.
  • Jei a sunaikinta, nutraukus žymeklį, pašalinamas neapibrėžtas elgesys .
  • Kadangi priskyrime neatsižvelgiama į tai, ką prieš užduotį nurodytas name , anksčiau ar vėliau visur atsiras atminties nutekėjimas.

Aiškios apibrėžtys

Kadangi kopijavimas tvarkoje neturi reikiamo efekto, privalome aiškiai apibrėžti kopijavimo konstruktorių ir kopijavimo priskyrimo operatorių, kad sukurtume gilias simbolių masyvo kopijas:

 // 1. copy constructor person(const person that) { name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } // 2. copy assignment operator person operator=(const person that) { if (this !=  { delete[] name; // This is a dangerous point in the flow of execution! // We have temporarily invalidated the class invariants, // and the next statement might throw an exception, // leaving the object in an invalid state :( name = new char[strlen(that.name) + 1]; strcpy(name, that.name); age = that.age; } return *this; } 

Atkreipkite dėmesį į skirtumą tarp inicijavimo ir priskyrimo: prieš priskiriant name kad būtų išvengta atminties, privalome nuleisti senąją būseną. Be to, privalome apsaugoti formą x = x nuo savimonės. Be šio patikrinimo, delete[] name bus ištrintas masyvas, kuriame yra šaltinio eilutė, nes rašant x = x , tiek šis, tiek ir this->name .

Išimties saugumas

Deja, šis sprendimas bus nesėkmingas, jei new char[...] išskirs dėl atminties išnaudojimo. Vienas iš galimų sprendimų yra įdiegti vietinį kintamąjį ir keisti operatorių tvarką:

 // 2. copy assignment operator person operator=(const person that) { char* local_name = new char[strlen(that.name) + 1]; // If the above statement throws, // the object is still in the same state as before. // None of the following statements will throw an exception :) strcpy(local_name, that.name); delete[] name; name = local_name; age = that.age; return *this; } 

Ji taip pat suteikia savarankišką paskyrimą be aiškaus patikrinimo. Dar patikimesnis šios problemos sprendimas yra kopijavimo ir keitimo idėja , tačiau aš nesiruošsiu pateikti informacijos apie saugumo išimtis. Aš minėjau tik išimtis, kad atliktumėte šiuos veiksmus: Sunku valdyti išteklius valdančias klases.

Nepakankami ištekliai

Kai kurie ištekliai negali būti kopijuojami, pvz., Failų aprašai arba „mutexes“. Tokiu atveju tiesiog deklaruokite kopijos konstruktorių ir kopijavimo paskirties operatorių kaip private , nenurodydami apibrėžimo:

 private: person(const person that); person operator=(const person that); 

Arba galite paveldėti iš boost::noncopyable arba juos paskelbti kaip ištrintus (C ++ 0x):

 person(const person that) = delete; person operator=(const person that) = delete; 

Trijų taisyklė

Kartais reikia įdiegti klasę, kuri valdo išteklius. (Niekada nekeiskite kelių išteklių toje pačioje klasėje, tai sukels tik skausmą.) Tokiu atveju prisiminkite trijų taisyklių taisyklę:

Jei reikia aiškiai paskelbti sunaikintuvą, nukopijuokite konstruktoriaus arba kopijos priskyrimo pareiškimą patys, jums tikriausiai reikia aiškiai paskelbti visus tris.

(Deja, ši „taisyklė“ netaikoma C ++ standarto ar kompiliatoriaus, kurį aš žinau.)

Valdyba

Daugeliu atvejų jums nereikia valdyti išteklių, nes esama klasė, pvz., std::string , jau tai daro už jus. Tiesiog palyginkite paprastą kodą su std::string nariu į klaidinančią ir klaidinančią alternatyvą, naudodami char* , ir jūs turėtumėte būti tikri. Tol, kol liksite nuošalyje nuo žaliavų, trečioji taisyklė vargu ar taikoma jūsų kodui.

1561
13 нояб. atsakymas pateikiamas fredoverflow lapkričio 13 d 2010-11-13 16:27 '10, 16:27, 2010-11-13 16:27

Trečioji taisyklė yra „C ++“ nykščio taisyklė, paprastai kalbant

Jei jūsų klasėje reikia

  • kopijavimo konstruktorius ,
  • priskyrimo operatorius,
  • arba destruktorius ,

aiškiai nustatoma, tada tikriausiai bus reikalingi visi trys .

To priežastis yra ta, kad visi trys iš jų dažniausiai naudojami ištekliams valdyti, o jei jūsų klasė valdo išteklius, ji paprastai turi valdyti kopijavimą ir jį nemokamai.

border=0

Jei nėra geros semantikos, kad būtų galima kopijuoti savo klasės valdomus išteklius, apsvarstykite galimybę uždrausti kopijuoti, deklaruodami (nenustatydami) kopijavimo konstruktorių ir priskyrimo operatorių kaip private .

(Atkreipkite dėmesį, kad artėjanti nauja C ++ standarto versija (kuri yra C ++ 11) prideda judėjimo semantiką C + +, kuri greičiausiai pakeis trejų taisyklių taisyklę. +11 skyrius dėl trijų taisyklių.)

467
13 нояб. Atsakymą pateikė sbi lapkričio 13 d. 2010-11-13 17:22 '10, 17:22, 2010-11-13 17:22

Didžiųjų trijų įstatymų įstatymas yra toks, kaip nurodyta pirmiau.

Paprastas anglų kalbos pavyzdys dėl problemos, kurią jis sprendžia:

Custom Destructor

Savo konstruktoriuje jūs priskyrėte atmintį, ir reikia ją pašalinti. Priešingu atveju atsiras atmintis.

Galbūt manote, kad tai yra darbas.

Problema bus, jei kopija bus pagaminta iš jūsų objekto, tada kopija bus nukreipta į tą pačią atmintį kaip ir originalus objektas.

Kai tik vienas iš jų pašalins atmintį į savo destruktorių, kitas turės žymeklį į negaliojančią atmintį (tai vadinama kabančiu rodikliu), kai jis bando jį naudoti, viskas atrodys plaukuota.

Todėl rašote kopijavimo konstruktorių, kad jis priskirtų naujus objektus, kad sunaikintų savo atminties fragmentus.

Priskyrimo operatorius ir kopijavimo konstruktorius

Savo konstruktoriaus atmintį nukreipėte į savo klasės narį. Kai nukopijuojate šios klasės objektą, numatytasis priskyrimo pareiškimas ir kopijavimo konstruktorius nukopijuos šio rodyklės elemento vertę į naują objektą.

Tai reiškia, kad naujasis objektas ir senas objektas nukreips į tą pačią atminties dalį, taigi, pakeisdami jį viename objekte, jis bus pakeistas kitam objekto objektui. Jei vienas objektas pašalina šią atmintį, kitas ir toliau bandys jį naudoti.

Norėdami išspręsti šią problemą, parašykite savo kopijos konstruktoriaus ir priskyrimo operatoriaus versiją. Jūsų versijos skiria atskirą atmintį naujiems objektams ir nukopijuoja pirmojo rodyklės nurodytas vertes, o ne jos adresą.

143
14 мая '12 в 17:22 2012-05-14 17:22 atsakė Stefanui gegužės 12 d., 12 val. 2012-05-14 17:22

Iš esmės, jei turite destruktorių (o ne numatytąjį destruktorių), tai reiškia, kad jūsų apibrėžta klasė turi šiek tiek atminties. Tarkime, kad klasė yra naudojama ne tam tikram kliento kodui ar jums.

  MyClass x(a, b); MyClass y(c, d); x = y; // This is a shallow copy if assignment operator is not provided 

Jei „MyClass“ turi tik kai kuriuos primityvius įvestus narius, bus atlikta numatytoji priskyrimo ataskaita, bet jei ji turi tam tikrus rodyklės elementus ir objektus, neturinčius priskyrimo ataskaitų, rezultatas bus nenuspėjamas. Todėl galime pasakyti, kad jei yra kažkas ištrinti klasėje destruktoriuje, mums gali prireikti gilaus kopijavimo operatoriaus, o tai reiškia, kad turime pateikti kopijavimo konstruktorių ir priskyrimo operatorių.

39
31 дек. atsakymas fatma.ekici 31 d. 2012-12-31 22:29 '13, 22:29, 2012-12-31 22:29

Ką reiškia objekto kopijavimas? Yra keli būdai, kaip nukopijuoti objektus - papasakokite apie du tipus, kuriuos greičiausiai nurodote: gilų kopiją ir seklią kopiją.

Kadangi mes esame objekto kalba (arba bent jau manome, kad), tarkime, kad turite tam skirtą atmintį. Kadangi tai yra OO kalba, mes galime lengvai remtis mūsų skiriamomis atminties dalimis, nes jie paprastai yra primityvūs kintamieji (ints, chars, baitai) arba klasės, kuriuos mes sukūrėme iš savo tipų ir primityvių. Taigi, tarkime, kad mes turime automobilių klasę taip:

 class Car //A very simple class just to demonstrate what these definitions mean. //It pseudocode C++/Javaish, I assume strings do not need to be allocated. { private String sPrintColor; private String sModel; private String sMake; public changePaint(String newColor) { this.sPrintColor = newColor; } public Car(String model, String make, String color) //Constructor { this.sPrintColor = color; this.sModel = model; this.sMake = make; } public ~Car() //Destructor { //Because we did not create any custom types, we aren't adding more code. //Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors. //Since we did not use anything but strings, we have nothing additional to handle. //The assumption is being made that the 3 strings will be handled by string destructor and that it is being called automatically--if this were not the case you would need to do it here. } public Car(const Car  // Copy Constructor { this.sPrintColor = other.sPrintColor; this.sModel = other.sModel; this.sMake = other.sMake; } public Car  =(const Car  // Assignment Operator { if(this !=  { this.sPrintColor = other.sPrintColor; this.sModel = other.sModel; this.sMake = other.sMake; } return *this; } } 

Gilus egzempliorius yra, jei deklaruojame objektą ir tada sukuriame visiškai atskirą objekto kopiją ... mes galime turėti du objektus dviejuose pilno atminties rinkiniuose.

 Car car1 = new Car("mustang", "ford", "red"); Car car2 = car1; //Call the copy constructor car2.changePaint("green"); //car2 is now green but car1 is still red. 

Dabar darykime kažką keisto. Leiskite pasakyti, kad automobilis2 yra užprogramuotas neteisingai arba tyčia, skirtas keistis faktine atmintimi, iš kurios gaminamas automobilis1. (Tai paprastai yra klaida, o klasėse paprastai paminėta antklodė.) Įsivaizduokite, kad bet kuriuo metu, kai klausiate apie automobilį2, iš tikrųjų nuspręsite nukreipti į automobilio atminties vietą1, kuri yra daugiau ar mažiau maža yra kopija.

 //Shallow copy example //Assume we're in C++ because it standard behavior is to shallow copy objects if you do not have a constructor written for an operation. //Now let assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default. Car car1 = new Car("ford", "mustang", "red"); Car car2 = car1; car2.changePaint("green");//car1 is also now green delete car2; car1.changePaint("red"); 

Todėl, nesvarbu, kokia kalba rašote, būkite labai atsargūs, ką reiškia, kai kopijuojate objektus, nes didžiąją laiko dalį norite gauti gilias kopijas.

Kas yra kopijavimo kūrėjas ir kopijos priskyrimo ataskaita? Jau anksčiau juos panaudojau. Kopijavimo konstruktorius kviečiamas įvedant kodą, pvz., Car car2 = car1; Iš esmės, jei skelbiate kintamąjį ir priskiriate jį vienai eilutei, tada, kai skambinate kopijos konstruktoriui. car2 = car1; operatorius yra tai, kas atsitinka, kai naudojate lygų ženklą - car2 = car1; . car2 pačiame pareiškime nėra pranešama apie pranešimą 2. Du kodai, kuriuos rašote šioms operacijoms, greičiausiai yra labai panašūs. Tiesą sakant, tipiškame dizaine yra dar viena funkcija, kurią skambinate, kad nustatytumėte viską, kai esate patenkinti pradine kopija / užduotimi, yra teisėtas - jei pažvelgsite į kodą, kurį parašiau, funkcijos yra beveik identiškos.

Kada turiu juos paskelbti? Jei nenorite rašyti kodo, kurį reikia bendrinti ar gaminti bet kokiu būdu, jūs tikrai turite tik juos deklaruoti, kai juos reikia. Turite žinoti, ką daro jūsų programavimo kalba, jei nuspręsite jį naudoti „atsitiktinai“ ir to nepadarėte - tai yra jūs gaunate numatytąjį kompiliatorių. Pvz., Retai naudojasi kopijavimo konstruktoriais, tačiau svarbiausi priskyrimo operatoriai yra labai dažni. Ar žinote, kad galite iš naujo apibrėžti, ką reiškia pridėti, atimti ir tt

Kaip galiu išvengti savo objektų kopijavimo? Protingas paleidimas yra nepaisyti visų būdų, kuriais galite priskirti savo objekto atmintį naudodami privačią funkciją. Jei tikrai nenorite, kad žmonės juos nukopijuotų, galite padaryti jį viešai prieinamą ir įspėti programuotoją išmesti išimtį ir taip pat ne kopijuoti objektą.

31
17 окт. atsakymas suteiktas user1701047 17 okt. 2012-10-17 19:37 '12 at 7:37 pm 2012-10-17 19:37

Kada turiu juos paskelbti?

Trijų valstybių taisyklėje teigiama, kad jei paskelbiate bet kurį iš jų

  • kopijavimo konstruktoriaus paskyrimo ataskaita
  • destruktorius

tada jūs turite paskelbti visus tris. Iš to, kad būtinybė naudoti kopijavimo operacijos reikšmę beveik visada kyla iš klasės, kuri atlieka tam tikrą išteklių valdymą, ir kad beveik visada tai reiškia, kad

  • bet koks išteklių valdymas buvo atliktas vienoje kopijavimo operacijoje, gali tekti atlikti kopijavimo operacijas kitoje, ir

  • klasės destruktorius taip pat dalyvaus išteklių valdyme (paprastai jį atlaisvina). Klasikinis valdymo šaltinis buvo atmintis, todėl visos standartinės bibliotekos klasės, kurios (pavyzdžiui, STL konteineriai, atliekantys dinaminį atminties valdymą), deklaruoja „didelius tris“: tiek kopijavimo operacijas, tiek destruktorių.

Trečios taisyklės pasekmė yra ta, kad vartotojo deklaruotas destruktorius rodo, kad paprastas nario egzempliorius netinka kopijavimo operacijoms klasėje. Tai, savo ruožtu, rodo, kad jei klasė deklaruoja destruktorių, kopijavimo operacijos tikriausiai neturėtų būti generuojamos automatiškai, nes jos nedarys teisingo dalyko. Tuo metu, kai buvo priimtas C ++ 98, šios argumentavimo linijos reikšmė nebuvo visiškai įvertinta, todėl C ++ 98 naudotojo deklaruoto destruktoriaus buvimas neturėjo įtakos kompiliatorių pasirengimui generuoti kopijavimo operacijas. Tai vis dar yra C ++ 11 atveju, tačiau tik todėl, kad apribojant sąlygas, kuriomis atliekamos kopijavimo operacijos, bus per daug pasenęs kodas.

Kaip galiu išvengti savo objektų kopijavimo?

Pripažinkite kopijos konstruktorių ir kopijos priskyrimo ataskaitą kaip privataus prieigos specifiką.

 class MemoryBlock { public: //code here private: MemoryBlock(const MemoryBlock other) { cout<<"copy constructor"<<endl; } // Copy assignment operator. MemoryBlock operator=(const MemoryBlock other) { return *this; } }; int main() { MemoryBlock a; MemoryBlock b(a); } 

C ++ 11 taip pat galite deklaruoti, kad kopijavimo konstruktorius ir priskyrimo operatorius yra pašalinami.

 class MemoryBlock { public: MemoryBlock(const MemoryBlock other) = delete // Copy assignment operator. MemoryBlock operator=(const MemoryBlock other) =delete }; int main() { MemoryBlock a; MemoryBlock b(a); } 
21
12 янв. Atsakymą pateikė Ajay yadav 12 sausis 2016-01-12 12:54 '16 at 12:54 2016-01-12 12:54

Daugelis esamų atsakymų jau yra susiję su kopijavimo konstruktoriumi, priskyrimo operatoriumi ir destruktoriumi. Tačiau po C ++ 11 judėjimo semantikos įvedimas gali išplėsti šią vertę virš 3.

Neseniai Michael Clyce kalbėjo apie šią temą: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

12
07 янв. atsakymas pateikiamas 07 jan. 2015-01-07 08:38 '15 at 8:38 AM 2015-01-07 08:38

Trijų taisyklių taisyklė C ++ yra pagrindinis trijų reikalavimų kūrimo ir tobulinimo principas, kuris, jei vienoje iš šių funkcijų yra aiški apibrėžtis, programuotojas turi apibrėžti dvi kitas nario funkcijas kartu. А именно, необходимы следующие три функции-члена: деструктор, конструктор копирования, оператор присваивания копии.