Kodėl mano advokatai neleidžia išvengti rekursinio įtraukimo ir kelių simbolių apibrėžimų?

Du bendri klausimai yra apsaugos darbuotojai :

  • PIRMASIS KLAUSIMAS:

    Kodėl gi ne įtraukti mano antraštės failų gynėjus iš abipusės rekursinės įtraukties ? Aš nuolat gaunu klaidų apie neegzistuojančius simbolius, kurie akivaizdžiai egzistuoja, ar net svetimų sintaksės klaidų kiekvieną kartą, kai rašau kažką panašaus:

    "hijra"

     #ifndef A_H #define A_H #include "bh" ... #endif // A_H 

    "bh"

     #ifndef B_H #define B_H #include "ah" ... #endif // B_H 

    "main.cpp"

     #include "ah" int main() { ... } 

    Kodėl įvyksta kompiliavimo klaida "main.cpp"? Ką man reikia padaryti, kad išspręčiau savo problemą?


  1. ANTRASIS KLAUSIMAS:

    Kodėl ne gynėjai neleidžia įtraukti kelių apibrėžimų ? Pavyzdžiui, kai mano projekte yra du failai, kuriuose yra ta pati antraštė, kartais linkeris skundžiasi, kad simbolis yra apibrėžtas kelis kartus. Pavyzdžiui:

    "header.h"

     #ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H 

    "source1.cpp"

     #include "header.h" ... 

    "source2.cpp"

     #include "header.h" ... 

    Kodėl taip vyksta? Ką man reikia padaryti, kad išspręčiau savo problemą?

64
16 февр. Andy Prowl nustatė 16 vasario mėn. 2013-02-16 14:55 '13, 14:55, 2013-02-16 14:55
ответ 1 atsakymas

PIRMASIS KLAUSIMAS:

Kodėl gi ne įtraukti mano antraštės failų gynėjus iš abipusės rekursinės įtraukties ?

Jie yra .

Tai, ko jie nepadeda, yra duomenų struktūros apibrėžčių tarpusavio priklausomybė tarpusavyje įtraukiant antraštes. Norėdami suprasti, ką tai reiškia, pradėkime nuo pagrindinio scenarijaus ir pamatysime, kodėl įtraukiame gynėjus tarpusavyje.

Tarkime, kad jūsų abipusiai įtraukiantys antraštės failai ah ir bh turi trivialų turinį, t. Y. Klausimo teksto sekcijų elipsės pakeičiamos tuščia eilute. Esant tokiai situacijai, jūsų main.cpp mielai susisieks. Ir tai tik dėl jūsų apsaugos!

Jei nesate tikri, pabandykite juos pašalinti:

 //================================================ // ah #include "bh" //================================================ // bh #include "ah" //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

Pastebėsite, kad kompiliatorius praneš apie klaidą, kai pasiekia gylio ribą. Ši riba yra konkrečiai įgyvendinama. Pagal C ++ 11 standarto 16.2 / 6 punktą:

Išankstinio priskyrimo #include direktyva gali būti rodoma šaltinio faile, kuris buvo perskaitytas dėl #include direktyvos kitame faile, iki tam tikro priedo įgyvendinimo ribos .

Taigi, kas atsitiks ?

  • Analizuojant main.cpp išankstinio apdorojimo įrenginys laikysis direktyvos #include "ah" . Ši direktyva nurodo pirminio apdorojimo procesoriui apdoroti antraštės failą ah , sutinka su šio apdorojimo rezultatu ir pakeisti rezultatą #include "ah" ;
  • Apdorojant ah išankstinis apdorojimo įrenginys laikysis „ #include "bh" direktyvos ir bus naudojamas tas pats mechanizmas: išankstinio apdorojimo procesorius apdoroja bh antraštės failą, priima apdorojimo rezultatus ir pakeičia #include direktyvą su šiuo rezultatu;
  • Apdorojant bh #include "ah" direktyva nurodo procesoriui apdoroti ah ir pakeičia šią direktyvą rezultatu;
  • Išankstinis procesorius vėl pradės analizuoti „ ah , vėl atitiks „ #include "bh" direktyvą, ir tai sukurs potencialiai begalinį rekursinį procesą. Pasiekęs kritinį lizdų lygį, kompiliatorius praneš apie klaidą.

Jei įtraukiate apsaugos požymius , 4 žingsnyje nebus sukurta begalinė rekursija. Pažiūrėkime, kodėl:

  • (panašus į ankstesnį) Analizuojant main.cpp išankstinis apdorotojas laikysis direktyvos #include "ah" . Tai nurodo išankstinio apdorojimo procesoriui apdoroti „ ah antraštės failą, atlikite šio apdorojimo rezultatus ir pakeiskite #include "ah" su šiuo rezultatu;
  • Apdorojant ah išankstinis apdorojimas laikysis direktyvos #ifndef A_H . Kadangi makro A_H dar nėra apibrėžta, jis toliau apdoros šį tekstą. Ši direktyva ( #defines A_H ) apibrėžia makrokomandą A_H . Tada išankstinis apdorotojas laikysis direktyvos #include "bh" : išankstinis apdorojimas turi apdoroti antraštės failą bh , atlikti jo apdorojimo rezultatą ir pakeisti šią #include rezultatą;
  • Apdorojant bh išankstinis #ifndef B_H direktyvos #ifndef B_H . Kadangi B_H makro dar nėra apibrėžtas, jis apdoros šį tekstą. Ši direktyva ( #defines B_H ) apibrėžia makrokomandą B_H . Tada direktyvoje #include "ah" papasakos išankstiniam procesoriui apdoroti ah ir pakeiskite #include direktyvą bh išankstinio apdorojimo rezultatu ah ;
  • Kompiliatorius vėl pradės apdoroti ah ir vėl atitiks #ifndef A_H direktyvą. Tačiau ankstesnio išankstinio apdorojimo metu buvo apibrėžta makro A_H . Todėl šį kartą kompiliatorius praleis šį tekstą, kol bus nustatyta atitinkama direktyva #endif , o šio apdorojimo rezultatas bus tuščias eilutė (nebent, žinoma, niekas nesilaikys direktyvos #endif ). Todėl išankstinis apdorojimas pakeičia #include "ah" direktyvą bh tuščia eilute ir seka vykdymą, kol ji pakeis pradinę #include direktyvą main.cpp .

Taigi įtraukti gynėjus apsaugoti nuo abipusės įtraukties . Tačiau jie negali padėti priklausomai nuo jūsų klasių apibrėžimų abipusiai įtraukiančiuose failuose:

 //================================================ // ah #ifndef A_H #define A_H #include "bh" struct A { }; #endif // A_H //================================================ // bh #ifndef B_H #define B_H #include "ah" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

Atsižvelgiant į pirmiau nurodytas antraštes, main.cpp nesudarys.

Kodėl taip vyksta?

Norėdami sužinoti, kas vyksta, pakanka pakartoti 1-4 veiksmus.

Tai lengva matyti, kad pirmuosius tris žingsnius ir didžiąją dalį ketvirtojo etapo šis pokytis nedaro (tiesiog perskaitykite juos, kad įsitikintumėte). Tačiau 4-ojo etapo pabaigoje vyksta kažkas kita: pakeitus #include "ah" direktyvą bh tuščia eilute, išankstinis apdorojimas pradės analizuoti bh turinį ir ypač B apibrėžtį, deja, B apibrėžimas reiškia A klasę, kuri niekada Tai buvo atlikta anksčiau dėl apsaugos nuo įtraukimo!

Neiš anksto deklaruoto tipo kintamojo deklaravimas, žinoma, yra klaida, ir kompiliatorius tai mandagiai nurodo.

Ką man reikia padaryti, kad išspręčiau savo problemą?

Jums reikia išplėstinių skelbimų .

Iš tiesų A klasės apibrėžimas nėra būtinas B klasės apibrėžimui, nes žymeklis į A deklaruojamas kaip nario kintamasis, o ne A tipo objektas. Kadangi rodyklės yra fiksuoto dydžio, kompiliatoriui nereikia žinoti tikslios A vietos, o ne apskaičiuoti jo dydį, kad būtų galima teisingai nustatyti B klasę B Todėl pakanka persiųsti A klasės bh ir pranešti kompiliatoriui apie jo egzistavimą:

 //================================================ // bh #ifndef B_H #define B_H // Forward declaration of A: no need to #include "ah" struct A; struct B { A* pA; }; #endif // B_H 

Dabar jūsų main.cpp sudarys. Keletas pastabų:

  • Nepavyko visiškai panaikinti abipusės įtraukties, pakeičiant „ #include direktyvą su išankstine deklaracija bh , siekiant veiksmingai išreikšti „ B “ priklausomybę nuo A : pažangių deklaracijų naudojimas, kai įmanoma / praktiškas, taip pat laikomas gera programavimo praktika, nes tai padeda išvengti nereikalingų intarpų. sumažinant bendrą kompiliavimo laiką. Tačiau, po abipusės įtraukties pašalinimo, main.cpp turės būti pakeistas į #include ah ir bh (jei pastarasis yra būtinas), nes bh netiesiogiai nėra #include į ah ;
  • Nors A klasės priekinė deklaracija A pakankama, kad kompiliatorius galėtų nukreipti į šios klasės rodykles (arba naudoti jį bet kuriame kitame kontekste, kur leidžiami daliniai tipai), nukreipiantys nurodymus į A (pvz., Norėdami paskambinti nario funkcijai) arba apskaičiuoti jo dydį yra neteisėtos operacijos nebaigtų tipų atveju: jei reikia, kompiliatoriui turi būti suteikta visa A kompiliavimo apibrėžtis, o tai reiškia, kad turi būti nurodyta antraštės byla, kuri ją apibrėžia. Štai kodėl klasių apibrėžimai ir jų narių funkcijų įgyvendinimas paprastai yra suskirstyti į antraštės failą ir šios klasės įgyvendinimo failą (klasės šablonai yra šios taisyklės išimtis): įgyvendinimo failai, kurie niekada nėra įtraukti į kitus projekto failus, gali būti #include reikalingos antraštės, kad apibrėžimai būtų matomi. Kita vertus, antraštės failai nebus įtraukti į kitus antraštės failus, išskyrus tuos atvejus, kai jiems jų reikia (pvz., Kad pamatinės klasės apibrėžimas būtų matomas) ir, jei įmanoma, prireikus naudos išankstines deklaracijas.

ANTRASIS KLAUSIMAS:

Kodėl ne gynėjai neleidžia įtraukti kelių apibrėžimų ?

Jie yra .

Tai, kad jie nesaugo jūsų, yra keli apibrėžimai atskiruose vertimo vienetuose. Tai taip pat paaiškinta šiame „Q A“ puslapyje „StackOverflow“.

Tai pasakius, pabandykite pašalinti įtrauktus saugos įrenginius ir surinkti šią modifikuotą source1.cpp versiją (arba source2.cpp , kuris yra jam svarbus):

 //================================================ // source1.cpp // // Good luck getting this to compile... #include "header.h" #include "header.h" int main() { ... } 

Kompiliatorius tikrai skundžiasi dėl naujo ( f() . Tai akivaizdu: jos apibrėžimas įtrauktas du kartus! Tačiau aukščiau source1.cpp bus sukompiliuota be problemų, jei header.h turi atitinkamus atributus . Tai tikimasi.

Tačiau, net jei yra apsaugos įrenginių ir kompiliatorius nustoja jus su klaidos pranešimu, source1.cpp primygtinai reikalauja, kad derinant objekto kodą, gautą sudarant source1.cpp ir source2.cpp ir atsisakyti generuoti vykdomąjį failą.

Kodėl taip vyksta?

Iš esmės kiekvienas .cpp failas (techninis terminas šiame kontekste yra vertimo vienetas) jūsų projekte yra sudaromas atskirai ir nepriklausomai. Analizuojant .cpp failą, išankstinio apdorojimo procesorius apdoroja visas #include direktyvas ir plečia visas jam .cpp makrokomandas, o šio grynojo teksto apdorojimo išvestis bus pateikta įvesties signale kompiliatoriui, kad jis būtų išverstas į objekto kodą. Sukūrus kompiliatorių sukuriant objekto kodą vienam vertimo vienetui, jis tęs kitą, o visi makrokomandos, su kuriomis susidurta apdorojant ankstesnį vertimo vienetą, bus pamiršti.

Tiesą sakant, projekto sudarymas su n vertimo vienetais ( .cpp failais) yra panašus į tą pačią programą (kompiliatorių) n kartus, kiekvieną kartą su kitokiu įvedimu: skirtingos to paties programos vykdymo operacijos nesidalins ankstesnės programos vykdymo būsena . Taigi, kiekvienas vertimas atliekamas atskirai, o rengiant kitus vertimo vienetus, nepamirštama išankstinio apdorojimo simbolių, su kuriais susiduriama rengiant vieną vertimo vienetą (jei jūs apie tai galvojate, jūs lengvai suprasite, kad tai tikrai norimas elgesys).

Todėl, nors advokatų įtraukimas padeda užkirsti kelią rekursiniams abipusiams įtraukimams ir nereikalingiems to paties pozicijos įtraukimams į vieną vertimo skyrių, jie negali nustatyti, ar tas pats apibrėžimas yra įtrauktas į kitą vertimo skyrių.

Tačiau, derinant objekto kodą, sukurtą sudarant visus jūsų projekto „ .cpp failus, nuoroda matys, kad tas pats simbolis yra apibrėžtas daugiau nei vieną kartą, ir todėl, kad jis pažeidžia vieną apibrėžimo taisyklę . C ++ 11 standarto 3.2 / 3 punkte:

Kiekvienoje programoje turi būti tiksliai apibrėžta kiekviena ne inline funkcija, kuri naudojama šioje programoje; nereikia diagnozės. Apibrėžtis gali būti aiškiai parodyta programoje, ją galima rasti standartinėje arba vartotojo apibrėžtoje bibliotekoje arba, jei reikia, ji yra netiesiogiai apibrėžta (žr. 12.1, 12.4 ir 12.8). Integruota funkcija turi būti apibrėžta kiekviename vertimo vienete, kuriame ji naudojama .

Todėl nuorodą pateikia klaida ir atsisako generuoti jūsų programos vykdomąjį failą.

Ką man reikia padaryti, kad išspręčiau savo problemą?

Jei norite išsaugoti funkcijų apibrėžtį „ #include d“ antraštės faile su keliais vertimo vienetais (atkreipkite dėmesį, kad problema neįvyksta, jei jūsų #include d antraštė yra tik vienas vertimo vienetas), turite naudoti inline .

Priešingu atveju, jums reikia išsaugoti savo funkcijos deklaraciją header.h , header.h jo apibrėžimą (kūną) tik į vieną atskirą .cpp failą (tai yra klasikinis metodas).

inline yra neprivalomas kompiliatoriaus prašymas įterpti funkcijos kūną tiesiai į skambučių svetainę, o ne nustatyti rėmelio rėmelį įprastam funkcijų skambučiui. Nors kompiliatorius neprivalo įvykdyti jūsų užklausos, inline gali pasakyti saitui, kad jis leistų naudoti kelis simbolių apibrėžimus. Pagal C ++ 11 standarto 3.2 / 5 punktą:

Gali būti daugiau nei vienos klasės tipo apibrėžimas (9 skirsnis), skaičiavimo tipas (7.2), įmontuota funkcija su išorine nuoroda (7.1.2), klasės šablonas (14 skirsnis), ne statinis funkcijų šablonas (14.5.6), klasikinio šablono duomenų elementas (14.5.1.3), klasės šablono (14.5.1.1) nario funkcija arba specializuota šablono specializacija, kuriai programoje nenurodyti jokie šablono parametrai (14.7, 14.5.5), su sąlyga, kad kiekvienas apibrėžimas priskiriamas kitam vienetui vertimai ir su sąlyga, kad apibrėžimai atitinka šiuos reikalavimus [...]

Pirmiau pateiktoje pastraipoje iš esmės išvardijamos visos apibrėžtys, kurios paprastai pateikiamos antraštės rinkmenose , nes jos gali būti saugiai įtrauktos į keletą vertimo vienetų. Visos kitos apibrėžtys su išorine nuoroda priklauso šaltinio failams.

Naudojant static raktinį žodį vietoj inline taip pat susilpnėja nuorodų klaidos, suteikiant jūsų funkcijai vidinį ryšį , todėl kiekvienas vertimo vienetas turi privačią šios funkcijos kopiją (ir jos vietinius statinius kintamuosius). Tačiau tai galiausiai lemia didesnį vykdomąjį failą, o bendrojo naudojimo turėtų būti teikiama pirmenybė.

Alternatyvus būdas pasiekti tą patį rezultatą kaip ir su static yra funkcija f() be vardo vardų. C ++ 11 standarto 3.5 / 4 dalyje:

Nepažymėtai vardų sričiai arba vardų sričiai, deklaruotai tiesiogiai ar netiesiogiai nenurodytoje vardų erdvėje, yra vidinis ryšys. Visos kitos vardų erdvės turi išorinę nuorodą. Vardas su vardų sritimi, kuri nebuvo pateikta aukščiau pateiktoje vidaus nuorodoje, turi tą pačią nuorodą, kaip ir pavadinimo sritis, jei šis pavadinimas:

- kintamasis; arba

- funkcija ; arba

- pavadintą klasę (9 punktas) arba nenurodytą klasę, apibrėžtą tipedef deklaracijoje, kurioje klasė priskiriama tipedefi privalomojo pobūdžio tikslais (7.1.3); arba

- pavadinimą (7.2) arba nenurodytą enum, apibrėžtą tipedef deklaracijoje, kurioje enum yra pavadintas typedef privalomiesiems tikslams (7.1.3); arba

- sąrašo sudarytojas, susijęs su įrašu, privalomas; arba

- šablonas.

Dėl tos pačios priežasties, kuri buvo nurodyta pirmiau, pirmenybė turėtų būti teikiama inline .

121
16 февр. Atsakymą pateikė Andy Prowl 16 vasaris. 2013-02-16 14:55 '13, 14:55, 2013-02-16 14:55