We hebben het RAII-idioom besproken. Sommige taalgebruiken en programmeertaal in C ++ kunnen op het eerste gezicht vreemd of zinloos lijken, maar ze hebben wel een doel. In dit hoofdstuk zullen we enkele van deze vreemde gebruiken en idiomen verkennen om te begrijpen waar ze vandaan komen en waarom ze worden gebruikt.
U ziet gewoonlijk dat C ++ een geheel getal verhoogt met behulp van de syntaxis ++ i in plaats van i ++. De reden hiervoor is deels historisch, deels nuttig en deels een soort geheime handdruk. Een van de meest voorkomende plaatsen die u zult zien, is in een for-lus (bijv., voor (int i = 0; i < someNumber; ++i) …
). Waarom gebruiken C ++ -programmeurs ++ i in plaats van i ++? Laten we eens kijken wat deze twee operatoren bedoelen.
int i = 0; int x = ++ i; int y = i ++;
In de vorige code, wanneer alle drie de instructies zijn voltooid, ben ik gelijk aan 2. Maar wat is x en y gelijk? Ze zullen allebei gelijk zijn aan 1. Dit komt omdat de pre-increment operator in de instructie, ++ i, "increment i betekent en de nieuwe waarde van i als het resultaat geeft." Dus bij het toekennen van x de waarde, ga ik van 0 naar 1, en de nieuwe waarde van i, 1 is toegewezen aan x. De post-increment operator in de statement i ++ betekent "increment i en geef de oorspronkelijke waarde van i als resultaat." Dus wanneer je y zijn waarde toewijst, ga ik van 1 naar 2 en wordt de oorspronkelijke waarde van i, 1 toegewezen tot y.
Als we die reeks instructies stap voor stap zouden opsplitsen zoals geschreven, waardoor de operatoren voor en na de incrementering zouden worden geëlimineerd en vervangen door regelmatige toevoegingen, zouden we ons realiseren dat om de toewijzing aan y uit te voeren, we een extra variabele nodig hebben om de oorspronkelijke waarde van i te behouden. Het resultaat zou ongeveer zo zijn:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int magicTemp = i; i = i + 1; int y = magicTemp;
Vroege compilers, in feite, gebruikt om dat soort dingen te doen. Moderne compilers bepalen nu dat er geen waarneembare neveneffecten zijn die eerst aan y worden toegewezen, dus de assemblagecode die ze genereren, zelfs zonder optimalisatie, zal er doorgaans uitzien als het assembly-taalequivalent van deze C ++ -code:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int y = i; i = i + 1;
In sommige opzichten is de syntaxis ++ i (vooral binnen een for-lus) een overblijfsel uit de begintijd van C ++ en zelfs C ervoor. Wetende dat andere C ++ -programmeurs het gebruiken, laat het anderen zelf weten dat je op zijn minst enige bekendheid hebt met C ++ -gebruiken en -stijlen - de geheime handdruk. Het handige deel is dat je een enkele regel code kunt schrijven, int x = ++ i;
, en krijg het gewenste resultaat in plaats van twee regels code te schrijven: i ++;
gevolgd door int x = i;
.
Tip: Hoewel u hier en daar een regel code kunt opslaan met trucs, zoals het vastleggen van het resultaat van de pre-increment operator, is het over het algemeen het beste om een reeks bewerkingen op één regel te combineren. De compiler genereert geen betere code, omdat deze alleen die lijn in zijn samenstellende delen zal ontbinden (hetzelfde als wanneer je meerdere regels had geschreven). Vandaar dat de complier machinecode genereert die elke bewerking op een efficiënte manier uitvoert, waarbij de volgorde van bewerkingen en andere taalbeperkingen worden gerespecteerd. Het enige dat u hoeft te doen is andere mensen verwarren die naar uw code moeten kijken. Je introduceert ook een perfecte situatie voor bugs, ofwel omdat je iets verkeerd hebt gebruikt of omdat iemand een verandering heeft aangebracht zonder de code te begrijpen. Je vergroot ook de kans dat je de code niet begrijpt als je er zes maanden later nog eens naar teruggaat.
nullptr
Aan het begin van zijn levensduur heeft C ++ veel dingen overgenomen van C, inclusief het gebruik van binaire nul als de weergave van een nulwaarde. Dit heeft door de jaren heen talloze bugs veroorzaakt. Ik geef Kernighan, Ritchie, Stroustrup of iemand anders hiervoor geen schuld; het is verbazingwekkend hoeveel ze hebben bereikt bij het maken van deze talen, gezien de computers die beschikbaar waren in de jaren '70 en '80. Proberen te bedenken welke dingen problemen zullen zijn bij het maken van een computertaal is een uiterst moeilijke taak.
Desalniettemin realiseerden programmeurs zich al snel dat het gebruik van een letterlijke 0 in hun code in sommige gevallen tot verwarring kon leiden. Stel je bijvoorbeeld voor dat je schreef:
int * p_x = p_d; // Meer code hier ... p_x = 0;
Bedoelde u het om de wijzer naar nul in te stellen zoals geschreven (d.w.z. p_x = 0;) of wilde u de punt-tot-waarde op 0 zetten (d.w.z. * p_x = 0;)? Zelfs met een code van redelijke complexiteit, zou de debugger veel tijd kunnen nemen om dergelijke fouten te diagnosticeren.
Het resultaat van deze realisatie was de goedkeuring van de macro NULL preprocessor: #define NULL 0
. Dit zou helpen om fouten te verminderen, als je het zag * p_x = NULL;
of p_x = 0;
vervolgens, ervan uitgaande dat jij en de andere programmeurs de macro NULL consequent gebruikten, zou de fout gemakkelijker te herkennen, op te lossen zijn en zou de fix gemakkelijker te verifiëren zijn.
Maar omdat de NULL-macro een preprocessor-definitie is, ziet de compiler nooit iets anders dan 0 vanwege tekstuele vervanging; het kan u niet waarschuwen voor mogelijk foutieve code. Als iemand de NULL-macro opnieuw heeft gedefinieerd naar een andere waarde, kunnen allerlei extra problemen het gevolg zijn. NULL herdefiniëren is een heel slechte zaak, maar soms doen programmeurs slechte dingen.
C ++ 11 heeft een nieuw zoekwoord toegevoegd, nullptr, dat kan en moet worden gebruikt in plaats van 0, NULL en al het andere wanneer u een nulwaarde aan een pointer moet toewijzen of moet controleren of een aanwijzer null is. Er zijn verschillende goede redenen om het te gebruiken.
Het nullptr-sleutelwoord is een taalsleutelwoord; het wordt niet geëlimineerd door de preprocessor. Omdat het doorwerkt naar de compiler, kan de compiler fouten detecteren en gebruikswaarschuwingen genereren die het niet kon detecteren of genereren met de letterlijke 0 of macro's.
Het kan ook niet per ongeluk of opzettelijk worden geherdefinieerd, in tegenstelling tot een macro zoals NULL. Dit elimineert alle fouten die macro's kunnen introduceren.
Ten slotte biedt het toekomstvastheid. Het hebben van binaire nul als nulwaarde was een praktische beslissing toen het werd gemaakt, maar het was desalniettemin willekeurig. Een andere redelijke keuze zou kunnen zijn om nul de maximale waarde van een niet-ondertekend native geheel getal te hebben. Er zijn positieven en negatieven tot zo'n waarde, maar er is niets dat ik weet dat het onbruikbaar zou hebben gemaakt.
Met nullptr wordt het opeens mogelijk om te veranderen wat nul is voor een bepaalde besturingsomgeving zonder wijzigingen aan te brengen in een C ++ -code die volledig is overgenomen van nullptr. De compiler kan een vergelijking maken met nullptr, of de toewijzing van nullptr aan een pointervariabele en elke machinecode genereren die de doelomgeving daar van nodig heeft. Proberen hetzelfde te doen met een binaire 0 zou heel moeilijk, zo niet onmogelijk zijn. Als iemand in de toekomst besluit om een computerarchitectuur en een besturingssysteem te ontwerpen dat een nul-vlagbit toevoegt voor alle geheugenadressen om nul aan te duiden, zou moderne C ++ dat kunnen ondersteunen vanwege nullptr.
Je zult mensen vaak code zien schrijven zoals if (nullptr == p_a) ...
. Ik heb die stijl niet in de voorbeelden gevolgd omdat het er gewoon verkeerd uitziet. In de 18 jaar dat ik programma's schrijf in C en C ++, heb ik nooit problemen gehad met het probleem dat deze stijl vermijdt. Niettemin hebben andere mensen dergelijke problemen gehad. Deze stijl kan mogelijk onderdeel zijn van de stijlregels die u moet volgen; daarom is het de moeite van het bespreken waard.
Als je schreef if (p_a = nullptr) ...
in plaats van if (p_a == nullptr) ...
, dan zou jouw programma de nulwaarde toekennen aan p_a en de if-statement zou altijd evalueren naar false. C ++, dankzij zijn C-erfenis, kunt u een uitdrukking hebben die evalueert naar elk integraal type binnen de haakjes van een besturingsinstructie, zoals if. C # vereist dat het resultaat van een dergelijke expressie een Booleaanse waarde is. Omdat je geen waarde kunt toewijzen aan iets als nullptr of aan constante waarden, zoals 3 en 0.0F, als je die R-waarde aan de linkerkant van een gelijkheidscontrole plaatst, zal de compiler je waarschuwen voor de fout. Dit komt omdat je een waarde toewijst aan iets waaraan geen waarde kan worden toegewezen.
Om deze reden zijn sommige ontwikkelaars begonnen met het schrijven van hun gelijkheidscontroles op deze manier. Het belangrijkste is niet welke stijl je kiest, maar dat je je ervan bewust bent dat een opdracht in iets als een if-expressie geldig is in C ++. Op die manier weet je dat je op zulke problemen moet letten.
Wat je ook doet, schrijf niet opzettelijk verklaringen zoals if (x = 3) ...
. Dat is een erg slechte stijl, waardoor je code moeilijker te begrijpen is en meer vatbaar voor bugs.
gooien()
en noexcept (bool-expressie)
Notitie: Vanaf Visual Studio 2012 RC accepteert de Visual C ++-compiler uitzonderingsspecificaties maar worden deze niet geïmplementeerd. Als u echter een throw () -uitzonderingsspecificatie opneemt, zal de compiler waarschijnlijk alle code wegschrijven die deze anders zou genereren om afwikkeling te ondersteunen wanneer een uitzondering wordt gegenereerd. Uw programma wordt mogelijk niet correct uitgevoerd als een uitzondering wordt gegenereerd door een functie die is gemarkeerd met throw (). Andere compilers die werpspecificaties implementeren, zullen verwachten dat ze goed worden gemarkeerd, dus u moet de juiste uitzonderingsspecificaties implementeren als uw code moet worden gecompileerd met een andere compiler.
Notitie: Uitzonderingsspecificaties met de throw () -syntaxis (dynamische-uitzonderingsspecificaties genoemd) worden vanaf C ++ 11 gedeprecieerd. Als zodanig kunnen ze in de toekomst uit de taal worden verwijderd. De niet-toegestane specificatie en operator zijn vervangingen voor deze taalkenmerk maar zijn niet geïmplementeerd in Visual C ++ vanaf Visual Studio 2012 RC.
C ++ -functies kunnen via het throw () -sleutel voor uitzonderingsspecificaties bepalen of uitzonderingen al dan niet worden gegooid en, zo ja, wat voor soort te gooien.
Bijvoorbeeld, int AddTwoNumbers (int, int) throw ();
declareert een functie die, vanwege de lege haakjes, aangeeft dat er geen uitzonderingen zijn, met uitzondering van die die deze intern vangst en niet opnieuw gooit. Daarentegen, int AddTwoNumbers (int, int) throw (std: logic_error);
verklaart een functie die aangeeft dat deze een uitzondering van het type kan genereren std :: logic_error
, of elk daarvan afgeleid type.
De functieverklaring int AddTwoNumber (int, int) throw (...);
verklaart dat het een uitzondering van welk type dan ook kan gooien. Deze syntaxis is Microsoft-specifiek, dus u moet deze vermijden voor code die mogelijk moet worden gecompileerd met iets anders dan de Visual C ++-compiler.
Als er geen specificatie verschijnt, zoals in int AddTwoNumbers (int, int);
, dan kan de functie elk uitzonderingstype gebruiken. Het is het equivalent van het hebben van de gooien(… )
specifier.
C ++ 11 heeft de nieuwe noexcept (bool-expressie) -specificatie en -operator toegevoegd. Visual C ++ ondersteunt deze niet vanaf Visual Studio 2012 RC, maar we zullen ze kort bespreken omdat ze ongetwijfeld in de toekomst zullen worden toegevoegd.
De specifier noexcept (false)
is het equivalent van beide gooien(… )
en van een functie zonder specificatie voor gooien. Bijvoorbeeld, int AddTwoNumbers (int, int) noexcept (false);
is het equivalent van beide int AddTwoNumber (int, int) throw (...);
en int AddTwoNumbers (int, int);
.
De specifiers noexcept (true)
en noexcept zijn het equivalent van gooien()
. Met andere woorden, ze specificeren allemaal dat de functie geen uitzonderingen toelaat om eraan te ontsnappen.
Wanneer een virtuele lidfunctie wordt vervangen, kan de uitzonderingsspecificatie van de negeringsfunctie in de afgeleide klasse geen uitzonderingen specificeren die worden opgegeven voor het type dat door de klasse wordt overschreven. Laten we een voorbeeld bekijken.
#include#include klasse A public: A (void) throw (...); virtuele ~ A (ongeldige) worp (); virtual int Toevoegen (int, int) throw (std :: overflow_error); virtuele zwevend Voeg (zwevend, zwevend) worp toe (); virtueel dubbel Toevoegen (dubbel, dubbel) gooien (int); ; klasse B: public A public: B (void); // Fijn, omdat het niet gooien gelijk is aan gooien (...). virtuele ~ B (void) throw (); // Fijn omdat het overeenkomt met ~ A. // De int Override toevoegen is prima, omdat je altijd minder kunt // een overschrijving dan de base zegt dat het kan gooien. virtueel int Toevoegen (int, int) throw () override; // De zwevende add-over hier is ongeldig omdat de A-versie zegt // het zal niet werpen, maar deze overschrijving zegt dat het een // std: uitzondering kan gooien. virtuele float Voeg (float, float) throw (std: exception) override toe; // De dubbele Add-override hier is ongeldig omdat de A-versie zegt // het kan een int gooien, maar deze overschrijving zegt dat het een double kan gooien, // wat de A-versie niet specificeert. virtueel dubbel Toevoegen (dubbel, dubbel) gooien (dubbel) overbruggen; ;
Omdat de syntaxis voor de uitzondering van de throw-uitzondering is verouderd, moet u alleen de lege ronde-haakjes-vorm ervan, throw (), gebruiken om aan te geven dat een bepaalde functie geen uitzonderingen genereert; anders laat u het gewoon staan. Als u anderen wilt laten weten welke uitzonderingen uw functies kunnen opleveren, overweeg dan om opmerkingen in uw headerbestanden of in andere documentatie te gebruiken, en zorg ervoor dat ze up-to-date blijven.
noexcept (bool-expressie)
is ook een operator. Wanneer het wordt gebruikt als een operator, is er een expressie nodig die evalueert naar true als deze geen uitzondering kan genereren of als deze een uitzondering kan opleveren. Merk op dat het resultaat een eenvoudige evaluatie is; het controleert om te zien of alle geroepen functies zijn noexcept (true)
, en of er uitdrukkingen zijn in de expressie. Als het worpinstructies vindt, zijn zelfs die waarvan je weet dat ze onbereikbaar zijn (bijv., if (x% 2 < 0) throw "This computer is broken";
) het kan echter evalueren naar false omdat de compiler niet verplicht is om een analyse op diep niveau uit te voeren.
Het pointer-naar-implementatie idioom is een oudere techniek die veel aandacht heeft gekregen in C ++. Dit is goed, want het is best handig. De essentie van de techniek is dat je in je headerbestand de openbare interface van je klas definieert. Het enige gegevenslid dat u hebt, is een persoonlijke verwijzing naar een klasse of structuur die is doorverwezen (verpakt in een std :: unique_ptr
voor exception-safe memory handling), die als de daadwerkelijke implementatie zal dienen.
In uw broncodebestand definieert u deze implementatieklasse en alle bijbehorende lidfuncties en lidgegevens. De openbare functies van de interface maken gebruik van de implementatieklasse voor de functionaliteit. Het resultaat is dat als je eenmaal hebt afgerekend op de openbare interface voor je klas, het headerbestand nooit verandert. De broncodebestanden die de header bevatten, hoeven dus niet opnieuw te worden gecompileerd vanwege implementatiewijzigingen die de openbare interface niet beïnvloeden.
Wanneer u wijzigingen wilt aanbrengen in de implementatie, is het enige dat moet worden gecompileerd het broncodebestand waarin die implementatieklasse bestaat in plaats van elk broncodebestand dat het klassenheaderbestand bevat..
Hier is een eenvoudig voorbeeld.
Voorbeeld: PimplSample \ Sandwich.h
#pragma één keer # opnemenklasse SandwichImpl; class Sandwich public: Sandwich (void); ~ Sandwich (void); void AddIngredient (const wchar_t * ingredient); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privé: std :: unique_ptr m_pImpl; ;
Voorbeeld: PimplSample \ Sandwich.cpp
# include "Sandwich.h" # include#include #include namespace std; gebruiken; // We kunnen alle wijzigingen aanbrengen die we in de implementatieklasse willen zonder // die een hercompilatie van andere bronbestanden met Sandwich.h veroorzaakt, omdat // SandwichImpl alleen in dit bronbestand is gedefinieerd. Dus alleen dit bron // -bestand moet opnieuw worden gecompileerd als we wijzigingen aanbrengen in SandwichImpl. class SandwichImpl public: SandwichImpl (); ~ SandwichImpl (); void AddIngredient (const wchar_t * ingredient); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privé: vector m_ingredients; wstring m_breadType; wstring m_description; ; SandwichImpl :: SandwichImpl () SandwichImpl :: ~ SandwichImpl () void SandwichImpl :: AddIngredient (const wchar_t * ingredient) m_ingredients.emplace_back (ingredient); ongeldig SandwichImpl :: RemoveIngredient (const wchar_t * ingrediënt) auto it = find_if (m_ingredients.begin (), m_ingredients.end (), [=] (wstring item) -> bool return (item.compare (ingredient) = = 0);); if (it! = m_ingredients.end ()) m_ingredients.erase (it); void SandwichImpl :: SetBreadType (const wchar_t * breadType) m_breadType = breadType; const wchar_t * SandwichImpl :: GetSandwich (void) m_description.clear (); m_description.append (L "A"); for (auto ingredient: m_ingredients) m_description.append (ingrediënt); m_description.append (L ","); m_description.erase (m_description.end () - 2, m_description.end ()); m_description.append (L "aan"); m_description.append (m_breadType); m_description.append (L ""); retourneer m_description.c_str (); Sandwich :: Sandwich (void): m_pImpl (nieuwe SandwichImpl ()) Sandwich :: ~ Sandwich (void) void Sandwich :: AddIngredient (const wchar_t * ingredient) m_pImpl-> AddIngredient (ingrediënt); ongeldig Sandwich :: RemoveIngredient (const wchar_t * ingredient) m_pImpl-> RemoveIngredient (ingrediënt); void Sandwich :: SetBreadType (const wchar_t * breadType) m_pImpl-> SetBreadType (breadType); const wchar_t * Sandwich :: GetSandwich (void) return m_pImpl-> GetSandwich ();
Voorbeeld: PimplSample \ PimplSample.cpp
#include#include # include "Sandwich.h" # include "... /pchar.h" met behulp van namespace std; int _pmain (int / * argc * /, _pchar * / * argv * / []) Sandwich s; s.AddIngredient (L "Turkije"); s.AddIngredient (L "Cheddar"); s.AddIngredient (L "Sla"); s.AddIngredient (L "Tomaat"); s.AddIngredient (L "Mayo"); s.RemoveIngredient (L "Cheddar"); s.SetBreadType (L "a Roll"); wcout << s.GetSandwich() << endl; return 0;
Beste werkwijzen en idiomen zijn essentieel voor elke taal of platform, dus bekijk dit artikel opnieuw om echt te begrijpen wat we hier behandeld hebben. De volgende zijn sjablonen, een taalfunctie waarmee u uw code opnieuw kunt gebruiken.
Deze les staat voor een hoofdstuk uit C ++ Kort gezegd, een gratis eBoek van het team van Syncfusion.