Abbiamo discusso l'idioma RAII. Alcuni usi linguistici e linguaggi di programmazione in C ++ potrebbero sembrare estranei o inutili a prima vista, ma hanno uno scopo. In questo capitolo, esploreremo alcuni di questi strani usi e idiomi per capire da dove vengono e perché vengono utilizzati.
Vedrete comunemente C ++ incrementare un intero usando la sintassi ++ i invece di i ++. La ragione di ciò è in parte storica, in parte utile e in parte una sorta di stretta di mano segreta. Uno dei luoghi comuni che vedrai è in un ciclo for (ad es., per (int i = 0; i < someNumber; ++i) …
). Perché i programmatori C ++ usano ++ i piuttosto che i ++? Consideriamo cosa significano questi due operatori.
int i = 0; int x = ++ i; int y = i ++;
Nel codice precedente, quando tutte e tre le istruzioni finiscono di essere eseguite, sarò uguale a 2. Ma cosa saranno xey? Entrambi equivale a 1. Questo perché l'operatore di pre-incremento nell'istruzione, ++ i, significa "incrementa i e assegna il nuovo valore di i come risultato". Quindi quando assegni x il suo valore, io vado da 0 a 1, e il nuovo valore di i, 1, è assegnato a x. L'operatore post-incremento nella dichiarazione i ++ significa "incrementa i e assegna il valore originale di i come risultato." Quindi quando assegni il suo valore, vado da 1 a 2 e il valore originale di i, 1, è assegnato a y.
Se dovessimo scomporre la sequenza di istruzioni passo dopo passo come scritto, eliminando gli operatori di pre-incremento e post-incremento e sostituendole con aggiunte regolari, ci renderemo conto che per eseguire l'assegnazione a y, abbiamo bisogno di una variabile extra per mantenere il valore originale di i. Il risultato sarebbe qualcosa di simile a questo:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int magicTemp = i; i = i + 1; int y = magicTemp;
I primi compilatori, infatti, facevano cose del genere. I moderni compilatori ora stabiliscono che non ci sono effetti secondari osservabili da assegnare prima a y, quindi il codice assembly che generano, anche senza ottimizzazione, apparirà in genere equivalente al linguaggio assembly di questo codice C ++:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int y = i; i = i + 1;
In qualche modo, la sintassi ++ i (specialmente all'interno di un ciclo for) è un residuo dei primi tempi di C ++ e anche di C prima di esso. Sapendo che altri programmatori di C ++ lo usano, impiegandolo da solo, consente agli altri di sapere che hai almeno una certa dimestichezza con gli usi e lo stile del C ++, la segreta stretta di mano. La parte utile è che puoi scrivere una singola riga di codice, int x = ++ i;
, e ottenere il risultato desiderato piuttosto che scrivere due righe di codice: i ++;
seguito da int x = i;
.
Mancia: Mentre è possibile salvare una riga di codice qua e là con trucchi come catturare il risultato dell'operatore di pre-incremento, in genere è meglio evitare di combinare un gruppo di operazioni su una singola riga. Il compilatore non genererà un codice migliore, poiché decomporrà semplicemente quella linea nelle sue parti componenti (come se avessi scritto più righe). Quindi, il compilatore genererà codice macchina che esegue ciascuna operazione in modo efficiente, rispettando l'ordine delle operazioni e altri vincoli linguistici. Tutto quello che farai è confondere le altre persone che devono guardare il tuo codice. Introdurrai anche una situazione perfetta per i bug, sia perché hai abusato di qualcosa o perché qualcuno ha apportato una modifica senza capire il codice. Aumenterai anche la probabilità che tu stesso non capisca il codice se torni a farlo sei mesi dopo.
nullptr
All'inizio della sua vita, il C ++ ha adottato molte cose da C, incluso l'uso dello zero binario come rappresentazione di un valore nullo. Questo ha creato innumerevoli bug nel corso degli anni. Non sto incolpando Kernighan, Ritchie, Stroustrup, o chiunque altro per questo; è incredibile quanto hanno realizzato durante la creazione di questi linguaggi, visti i computer disponibili negli anni '70 e nei primi anni '80. Cercare di capire quali saranno i problemi durante la creazione di un linguaggio informatico è un compito estremamente difficile.
Ciononostante, all'inizio, i programmatori si resero conto che l'uso di uno 0 letterale nel loro codice poteva generare confusione in alcuni casi. Ad esempio, immagina di aver scritto:
int * p_x = p_d; // Più codice qui ... p_x = 0;
Intendevi impostare il puntatore su null come scritto (ad es. P_x = 0;) o intendevi impostare il valore puntato a 0 (ad esempio * p_x = 0;)? Anche con codice di ragionevole complessità, il debugger potrebbe impiegare molto tempo per diagnosticare tali errori.
Il risultato di questa realizzazione è stata l'adozione della macro del preprocessore NULL: #define NULL 0
. Ciò contribuirebbe a ridurre gli errori, se hai visto * p_x = NULL;
o p_x = 0;
quindi, supponendo che tu e gli altri programmatori stiate usando la macro NULL in modo coerente, l'errore sarebbe più facile da individuare, correggere e la correzione sarebbe più semplice da verificare.
Ma poiché la macro NULL è una definizione del preprocessore, il compilatore non vedrebbe mai nulla di diverso da 0 a causa della sostituzione testuale; non potrebbe avvisarti di codice potenzialmente errato. Se qualcuno ha ridefinito la macro NULL su un altro valore, potrebbero verificarsi tutti i tipi di problemi aggiuntivi. Ridefinire NULL è una cosa molto brutta da fare, ma a volte i programmatori fanno cose cattive.
C ++ 11 ha aggiunto una nuova parola chiave, nullptr, che può e dovrebbe essere usata al posto di 0, NULL e qualsiasi altra cosa quando è necessario assegnare un valore nullo a un puntatore o controllare se un puntatore è nullo. Ci sono diversi buoni motivi per usarlo.
La parola chiave nullptr è una parola chiave della lingua; non è eliminato dal preprocessore. Dal momento che passa attraverso il compilatore, il compilatore può rilevare errori e generare avvisi di utilizzo che non è in grado di rilevare o generare con lo zero letterale o qualsiasi macro.
Inoltre, non può essere ridefinito accidentalmente o intenzionalmente, a differenza di una macro come NULL. Ciò elimina tutti gli errori che possono essere introdotti dalle macro.
Infine, fornisce prove future. Avere zero binario come il valore nullo era una decisione pratica quando è stato fatto, ma era comunque arbitrario. Un'altra scelta ragionevole potrebbe essere quella di avere null come valore massimo di un intero nativo senza segno. Ci sono elementi positivi e negativi a tale valore, ma non c'è nulla che io sappia che l'avrebbe reso inutilizzabile.
Con nullptr, diventa improvvisamente possibile modificare ciò che è nullo per un particolare ambiente operativo senza apportare modifiche a qualsiasi codice C ++ che abbia completamente adottato nullptr. Il compilatore può eseguire un confronto con nullptr o l'assegnazione di nullptr a una variabile puntatore e generare qualsiasi codice macchina richiesto dall'ambiente di destinazione. Cercare di fare lo stesso con un binario 0 sarebbe molto difficile, se non impossibile. Se in futuro qualcuno decide di progettare un'architettura e un sistema operativo che aggiunga un bit di null flag per tutti gli indirizzi di memoria per designare null, il C ++ moderno potrebbe supportare quello a causa di nullptr.
Vedrete comunemente persone scrivere codice come if (nullptr == p_a) ...
. Non ho seguito questo stile nei campioni perché mi sembra semplicemente sbagliato. Nei 18 anni in cui ho scritto programmi in C e C ++, non ho mai avuto problemi con il problema che questo stile evita. Tuttavia, altre persone hanno avuto tali problemi. Questo stile potrebbe essere parte delle regole di stile che devi seguire; quindi, vale la pena discutere.
Se hai scritto if (p_a = nullptr) ...
invece di if (p_a == nullptr) ...
, allora il tuo programma assegnerebbe il valore nullo a p_a e l'istruzione if valuterà sempre a false. C ++, grazie alla sua eredità C, ti permette di avere un'espressione che valuta qualsiasi tipo di integrale all'interno delle parentesi di una dichiarazione di controllo, come se. C # richiede che il risultato di tale espressione sia un valore booleano. Dato che non puoi assegnare un valore a qualcosa come nullptr oa valori costanti, come 3 e 0.0F, se metti quel valore R sul lato sinistro di un controllo di uguaglianza, il compilatore ti avviserà dell'errore. Questo perché assegneresti un valore a qualcosa a cui non è possibile assegnare un valore.
Per questo motivo, alcuni sviluppatori hanno iniziato a scrivere i loro controlli di uguaglianza in questo modo. La parte importante non è lo stile che scegli, ma sei consapevole che un compito all'interno di qualcosa come un'espressione if è valido in C ++. In questo modo, sai di stare attento a questi problemi.
Qualunque cosa tu faccia, non scrivere intenzionalmente affermazioni come if (x = 3) ...
. Questo è uno stile pessimo, il che rende il tuo codice più difficile da capire e più incline allo sviluppo di bug.
gettare()
e noxcept (espressione bool)
Nota: A partire da Visual Studio 2012 RC, il compilatore Visual C ++ accetta ma non implementa le specifiche delle eccezioni. Tuttavia, se si include una specifica di eccezione throw (), il compilatore probabilmente ottimizzerà qualsiasi codice che altrimenti genererebbe per supportare lo srotolamento quando viene lanciata un'eccezione. Il tuo programma potrebbe non funzionare correttamente se un'eccezione viene lanciata da una funzione contrassegnata da throw (). Altri compilatori che implementano le specifiche di lancio si aspettano che vengano contrassegnati correttamente, quindi è necessario implementare specifiche di eccezione appropriate se il codice deve essere compilato con un altro compilatore.
Nota: Le specifiche di eccezione che utilizzano la sintassi throw () (chiamate specifiche di eccezione dinamica) sono deprecate dal C ++ 11. In quanto tali, potrebbero essere rimossi dalla lingua in futuro. Le specifiche e l'operatore noexcept sono sostituzioni per questa funzionalità linguistica, ma non sono implementate in Visual C ++ a partire da Visual Studio 2012 RC.
Le funzioni C ++ possono specificare tramite la parola chiave specifica dell'eccezione throw () se generare o meno eccezioni e, in tal caso, che tipo lanciare.
Per esempio, int AddTwoNumbers (int, int) throw ();
dichiara una funzione che, a causa delle parentesi vuote, afferma che non genera eccezioni, escludendo quelle che intercetta internamente e non rilancia. Al contrario, int AddTwoNumbers (int, int) throw (std :: logic_error);
dichiara una funzione che afferma che può generare un'eccezione di tipo std :: logic_error
, o qualsiasi tipo derivato da quello.
La dichiarazione di funzione int AddTwoNumber (int, int) throw (...);
dichiara che può generare un'eccezione di qualsiasi tipo. Questa sintassi è specifica per Microsoft, quindi dovresti evitarlo per codice che potrebbe dover essere compilato con qualcosa di diverso dal compilatore Visual C ++.
Se non appare alcun identificatore, come in int AddTwoNumbers (int, int);
, quindi la funzione può lanciare qualsiasi tipo di eccezione. È l'equivalente di avere il gettare(… )
specificatore.
C ++ 11 ha aggiunto la nuova specifica e l'operatore noexcept (bool expression). Visual C ++ non supporta questi a partire da Visual Studio 2012 RC, ma li discuteremo brevemente poiché verranno aggiunti senza dubbio in futuro.
Lo specificatore noexcept (false)
è l'equivalente di entrambi gettare(… )
e di una funzione senza specificatore di lancio. Per esempio, int AddTwoNumbers (int, int) noexcept (false);
è l'equivalente di entrambi int AddTwoNumber (int, int) throw (...);
e int AddTwoNumbers (int, int);
.
Gli specificatori noexcept (vero)
e noexcept sono l'equivalente di gettare()
. In altre parole, tutti specificano che la funzione non consente ad alcuna eccezione di uscire da essa.
Quando si esegue l'override di una funzione membro virtuale, la specifica dell'eccezione della funzione di sostituzione nella classe derivata non può specificare eccezioni oltre a quelle dichiarate per il tipo che esegue l'override. Diamo un'occhiata a un esempio.
#includere#includere class A public: A (void) throw (...); virtual ~ A (void) throw (); virtuale int Aggiungi (int, int) throw (std :: overflow_error); virtual float Aggiungi (float, float) throw (); doppio virtuale Aggiungi (doppio, doppio) lancio (int); ; classe B: pubblico A pubblico: B (vuoto); // Bene, dato che non avere un lancio è lo stesso del lancio (...). virtual ~ B (void) throw (); // Bene visto che corrisponde a ~ A. // L'add. Override va bene dato che puoi sempre lanciare meno in // un override di quanto la base dice che può lanciare. virtuale int Aggiungi (int, int) throw () override; // L'aggancio del float qui non è valido perché la versione A dice // che non getterà, ma questa sovrascrittura dice che può lanciare un'eccezione // std ::. virtual float Aggiungi (float, float) throw (std :: exception) override; // Il doppio Aggiungi override qui non è valido perché la versione A dice // può lanciare un int, ma questo override dice che può lanciare un double, // che la versione A non specifica. virtuale doppio Aggiungi (doppio, doppio) lancio (doppio) override; ;
Poiché la sintassi della specifica dell'eccezione throw è deprecata, è necessario utilizzare solo la forma parentesi vuota di essa, throw (), per specificare che una particolare funzione non genera eccezioni; altrimenti, basta lasciarlo fuori. Se vuoi far sapere agli altri quali eccezioni possono essere lanciate dalle tue funzioni, prendi in considerazione l'uso di commenti nei tuoi file di intestazione o in altra documentazione, assicurandoti di tenerli aggiornati.
noxcept (espressione bool)
è anche un operatore. Quando viene utilizzato come operatore, accetta un'espressione che verrà valutata true se non può generare un'eccezione o false se può generare un'eccezione. Si noti che il risultato è una semplice valutazione; controlla per vedere se tutte le funzioni chiamate sono noexcept (vero)
, e se ci sono delle dichiarazioni di tiro nell'espressione. Se trova qualche istruzione di lancio, anche quelli che conosci sono irraggiungibili (es., se (x% 2 < 0) throw "This computer is broken";
), tuttavia, può essere valutato come falso poiché non è richiesto al compilatore di eseguire un'analisi approfondita.
L'idioma pointer-to-implementation è una tecnica precedente che ha ricevuto molta attenzione in C ++. Questo è buono, perché è abbastanza utile. L'essenza della tecnica è che nel tuo file di intestazione definisci l'interfaccia pubblica della tua classe. L'unico membro di dati che hai è un puntatore privato a una classe o struttura dichiarata in avanti (racchiusa in a std :: unique_ptr
per la gestione della memoria eccezionalmente sicura), che fungerà da implementazione effettiva.
Nel file del codice sorgente, definisci questa classe di implementazione e tutte le sue funzioni membro e i dati dei membri. Le funzioni pubbliche dalla chiamata all'interfaccia nella classe di implementazione per la sua funzionalità. Il risultato è che una volta stabilita l'interfaccia pubblica per la classe, il file di intestazione non cambia mai. Pertanto, i file del codice sorgente che includono l'intestazione non dovranno essere ricompilati a causa di modifiche all'implementazione che non influiscono sull'interfaccia pubblica.
Ogni volta che si desidera apportare modifiche all'implementazione, l'unica cosa che deve essere ricompilata è il file del codice sorgente in cui esiste la classe di implementazione, piuttosto che ogni file di codice sorgente che include il file di intestazione della classe.
Ecco un semplice esempio.
Esempio: PimplSample \ Sandwich.h
#pragma once #includeclass SandwichImpl; class Sandwich public: Sandwich (void); ~ Sandwich (void); vuoto AddIngredient (const wchar_t * ingrediente); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privato: std :: unique_ptr m_pImpl; ;
Esempio: PimplSample \ Sandwich.cpp
#include "Sandwich.h" # include#includere #includere usando lo spazio dei nomi std; // Possiamo apportare qualsiasi modifica alla classe di implementazione senza // attivando una ricompilazione di altri file di origine che includono Sandwich.h poiché // SandwichImpl è solo definito in questo file sorgente. Quindi, solo questo file sorgente // deve essere ricompilato se apportiamo modifiche a SandwichImpl. class SandwichImpl public: SandwichImpl (); ~ SandwichImpl (); vuoto AddIngredient (const wchar_t * ingrediente); void RemoveIngredient (const wchar_t * ingredient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privato: vettore m_ingredients; wstring m_breadType; wstring m_description; ; SandwichImpl :: SandwichImpl () SandwichImpl :: ~ SandwichImpl () void SandwichImpl :: AddIngredient (const wchar_t * ingredient) m_ingredients.emplace_back (ingrediente); void SandwichImpl :: RemoveIngredient (const wchar_t * ingredient) auto it = find_if (m_ingredients.begin (), m_ingredients.end (), [=] (oggetto wstring) -> 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"); per (ingrediente automatico: m_ingredients) m_description.append (ingrediente); m_description.append (L ","); m_description.erase (m_description.end () - 2, m_description.end ()); m_description.append (L "on"); m_description.append (m_breadType); m_description.append (L ""); return m_description.c_str (); Sandwich :: Sandwich (void): m_pImpl (new SandwichImpl ()) Sandwich :: ~ Sandwich (void) void Sandwich :: AddIngredient (const wchar_t * ingredient) m_pImpl-> AddIngredient (ingrediente); void Sandwich :: RemoveIngredient (const wchar_t * ingredient) m_pImpl-> RemoveIngredient (ingrediente); void Sandwich :: SetBreadType (const wchar_t * breadType) m_pImpl-> SetBreadType (breadType); const wchar_t * Sandwich :: GetSandwich (void) return m_pImpl-> GetSandwich ();
Esempio: PimplSample \ PimplSample.cpp
#includere#includere #include "Sandwich.h" #include "... /pchar.h" usando namespace std; int _pmain (int / * argc * /, _pchar * / * argv * / []) Sandwich s; s.AddIngredient (L "Turchia"); s.AddIngredient (L "Cheddar"); s.AddIngredient (L "Lettuce"); s.AddIngredient (L "Tomato"); s.AddIngredient (L "Mayo"); s.RemoveIngredient (L "Cheddar"); s.SetBreadType (L "a Roll"); wcout << s.GetSandwich() << endl; return 0;
Le migliori pratiche e le espressioni idiomatiche sono essenziali per qualsiasi lingua o piattaforma, quindi rivisita questo articolo per comprendere veramente ciò che abbiamo trattato qui. Di seguito sono disponibili i modelli, una funzione della lingua che consente di riutilizzare il codice.
Questa lezione rappresenta un capitolo di C ++, un eBook gratuito del team di Syncfusion.