Domanda

Così ho terminato il mio primo compito di programmazione C++ e ho ricevuto il mio voto.Ma secondo la valutazione ho perso i voti including cpp files instead of compiling and linking them.Non mi è molto chiaro cosa significhi.

Dando uno sguardo al mio codice, ho scelto di non creare file header per le mie classi, ma ho fatto tutto nei file cpp (sembrava funzionare bene senza file header...).Immagino che il selezionatore intendesse che ho scritto "#include" mycppfile.cpp ";" In alcuni dei miei file.

Il mio ragionamento per #includel'inserimento dei file cpp era:- Tutto ciò che doveva entrare nel file di intestazione era nel mio file CPP, quindi ho fatto finta che fosse come un file di intestazione- In Monkey-See-Monkey, ho visto che altri file di intestazione erano #includenei file, quindi ho fatto lo stesso per il mio file cpp.

Quindi cosa ho fatto esattamente di sbagliato e perché è un male?

È stato utile?

Soluzione

Per quanto a mia conoscenza, lo standard C ++ conosce alcuna differenza tra i file di intestazione e file di origine. Per quanto riguarda la lingua è interessato, qualsiasi file di testo con il codice giuridico è la stessa di ogni altro. Tuttavia, anche se non è illegale, inclusi i file di origine nel vostro programma eliminerà praticamente alcun vantaggio avresti ottenuto da separare i file di origine in primo luogo.

In sostanza, ciò che #include fa è dire al preprocessore per prendere l'intero file che hai specificato, e copiarlo nella tua file attivo prima che il compilatore ottiene le mani su esso. Così, quando si includono tutti i file di origine nel progetto insieme, non v'è fondamentalmente alcuna differenza tra quello che hai fatto, e solo facendo un enorme file di origine senza alcuna separazione a tutti.

"Oh, questo è un grosso problema. Se funziona, va bene," ti sento piangere. E in un certo senso, si sarebbe corretto. Ma in questo momento hai a che fare con un piccolo programmino molto piccolo, e una CPU piacevole e relativamente sgombra per compilarlo per voi. Non sarà sempre così fortunato.

Se mai approfondire i regni di seria programmazione di computer, potrai vedere i progetti con i conteggi di linea che possono raggiungere milioni di persone, piuttosto che decine. Questo è un sacco di linee. E se si tenta di compilare uno di questi su un moderno computer desktop, si può prendere una questione di ore invece di secondi.

"Oh no! Che suona orribile! Comunque posso evitare questo destino terribile?" Purtroppo, non c'è molto che si può fare al riguardo. Se ci vogliono ore per compilare, ci vogliono ore per compilare. Ma che solo conta davvero per la prima volta -. Una volta compilato una volta, non c'è motivo per compilarlo di nuovo

A meno che non si cambia qualcosa.

Ora, se tu avessi due milioni di linee di codice fuse insieme in un unico colosso gigante, e la necessità di fare una semplice correzione di bug, come, ad esempio, x = y + 1, che significa che è necessario compilare tutti i due milioni di linee di nuovo al fine di testare Questo. E se si scopre che si intende fare un x = y - 1 invece, ancora una volta, due milioni di linee di compilazione vi aspettano. Ecco molte ore di tempo sprecato che potrebbe essere speso meglio fare qualsiasi altra cosa.

"Ma io odio essere improduttivo! Se solo ci fosse un modo per di compilazione parti distinte della mia base di codice individuale, e in qualche modo collegamento insieme dopo!" un'idea eccellente, in teoria. Ma cosa succede se il programma ha bisogno di sapere cosa sta succedendo in un file diverso? E 'impossibile separare completamente la vostra base di codice a meno che non si desidera eseguire un gruppo di piccoli piccoli file .exe invece.

"Ma di sicuro deve essere possibile! Programmazione suona come pura tortura altrimenti! Che cosa succede se ho trovato un modo per separare interfaccia dall'implementazione ? Dire prendendo quel tanto che basta informazioni da questi codice distinta segmenti la loro identificazione con il resto del programma, e la loro messa in una sorta di intestazione file invece? e in questo modo, posso usare il #include preprocessore direttiva per portare a solo le informazioni necessarie per compilare! "

Hmm. Si potrebbe essere a qualcosa lì. Fammi sapere come funziona per voi.

Altri suggerimenti

Questa è probabilmente una risposta più dettagliata di quanto si voleva, ma credo che una spiegazione decente è giustificata.

In C e C ++, un file sorgente è definito come uno unità di traduzione . Per convenzione, i file header contengono dichiarazioni di funzioni, definizioni di tipo e definizioni di classe. Le implementazioni delle funzioni effettive risiedono in unità di traduzione, cioè .cpp file.

L'idea alla base di questo è che le funzioni e le funzioni di classe / struct membri sono compilati e assemblate una volta, poi altre funzioni possono chiamare che il codice da un luogo senza fare i duplicati. Le funzioni sono dichiarati come "extern" implicitamente.

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

Se si desidera una funzione sia locale per un unità di traduzione, si definisce come 'statica'. Cosa significa questo? Ciò significa che se si includono i file di origine con funzioni extern, si otterrà errori ridefinizione, perché il compilatore incontra la stessa implementazione più di una volta. Quindi, si desidera che tutti i vostri unità di traduzione di vedere il dichiarazione di funzione , ma non il funzione del corpo .

Quindi, come fa il tutto ottiene schiacciato insieme alla fine? Questo è il compito del linker. Un linker legge tutti i file oggetto, che viene generato dalla fase di assemblatore e risolve i simboli. Come ho detto in precedenza, un simbolo è solo un nome. Ad esempio, il nome di una variabile o una funzione. Quando le unità di traduzione che chiamano le funzioni o dichiarano i tipi non si conosce l'implementazione per quelle funzioni o tipi, quei simboli si dice che sono irrisolti. Il linker risolve il simbolo non risolto collegando l'unità di traduzione che contiene il simbolo indefinito insieme con quello che contiene l'implementazione. Uff. Questo è vero per tutti i simboli visibili esternamente, siano essi sono implementati nel codice, o forniti da una libreria aggiuntiva. Una libreria è in realtà solo un archivio con il codice riutilizzabile.

Ci sono due eccezioni degne di nota. In primo luogo, se si dispone di una piccola funzione, si può fare in linea. Ciò significa che il codice macchina generato non genera una chiamata di funzione extern, ma è letteralmente concatenato sul posto. Dal momento che di solito sono piccole, l'overhead dimensione non importa. Si può immaginare che siano statici nel loro modo di lavorare. Così è sicuro di implementare le funzioni inline nelle intestazioni. implementazioni funzione all'interno di una classe o struct definizione sono spesso inline automaticamente dal compilatore.

L'altra eccezione è modelli. Poiché il compilatore deve vedere l'intera definizione del tipo di modello quando un'istanza loro, non è possibile disaccoppiare l'applicazione dalla definizione come con funzioni indipendenti o classi normali. Ecco, forse questo è possibile, ma ottenere il supporto del compilatore diffuso per la parola chiave "esportazione" ha preso un lungo, lungo tempo. Così, senza il supporto per 'esportare', unità di traduzione ottengono le proprie copie locali dei tipi templated istanziati e funzioni, in modo simile a come funzionano le funzioni inline. Grazie al supporto di 'esportazione', questo non è il caso.

Per i due eccezioni, alcune persone trovano "più bello" per mettere le implementazioni di funzioni inline, funzioni templated e tipi su modelli in file cpp, e poi # include il file cpp. Se questo è un colpo di testa o un file di origine non ha molta importanza; il preprocessore non si preoccupa ed è solo una convenzione.

Un breve riassunto di tutto il processo dal codice C ++ (più file) e ad un eseguibile finale:

  • Il preprocessore è gestito, che analizza tutte le direttive che inizia con un '#'. La direttiva #include concatena il file incluso con inferiore, per esempio. Lo fa anche macro-sostituzione e token-incolla.
  • L'attuale compilatore viene eseguito su file di testo intermedio dopo la fase di preprocessore, ed emette codice assembler.
  • Il assembler viene eseguito sul file di assieme ed emette codice macchina, questo di solito è chiamato un objefile di ct e segue il formato binario eseguibile del sistema operativo in questione. Ad esempio, Windows utilizza il PE (formato Portable Executable), mentre Linux utilizza il formato Unix System V ELF, con estensioni GNU. In questa fase, i simboli sono ancora contrassegnati come non definito.
  • Infine, il linker viene eseguito. Tutte le fasi precedenti sono stati eseguiti su ciascuna unità di traduzione in ordine. Tuttavia, la fase di linker funziona su tutti i file oggetto generati che sono stati generati dal assembler. Il linker risolve simboli e fa molta magia come la creazione di sezioni e segmenti, che dipende dalla piattaforma di destinazione e il formato binario. I programmatori non sono tenuti a sapere questo, in generale, ma aiuta sicuramente in alcuni casi.

Ancora una volta, questo è stato sicuramente più di quanto chiesto, ma spero che i dettagli essenziali aiuta a vedere il quadro più ampio.

La soluzione tipica è quella di utilizzare i file .h solo per le dichiarazioni ei file .cpp di attuazione. Se avete bisogno di riutilizzare l'attuazione di includere il file .h corrispondente all'interno del file .cpp in cui la classe / funzione necessaria / qualunque cosa viene utilizzato e Link contro un file .cpp già compilato (sia un file .obj - di solito utilizzati entro un progetto - o lib di file - di solito utilizzato per il riutilizzo da più progetti). In questo modo non è necessario ricompilare tutto, se solo le modifiche di implementazione.

Si pensi file cpp come una scatola nera ei file .h come le guide su come utilizzare quelle scatole nere.

I file cpp possono essere compilati prima del tempo. Questo non funziona in esse si #include, come ha bisogno di vera e propria "include" il codice nel vostro programma ogni volta che lo compila. Se hai appena includere l'intestazione, si può semplicemente utilizzare il file di intestazione per determinare come utilizzare il file cpp precompilato.

Anche se questo non farà molta differenza per il vostro primo progetto, se si inizia a scrivere programmi di grandi dimensioni cpp, la gente va a odiare voi, perché i tempi di compilazione stanno per esplodere.

Inoltre avere una lettura di questo: File di intestazione include modelli

file

intestazione di solito contengono dichiarazioni di funzioni / classi, mentre i file cpp contengono le implementazioni attuali. Al momento della compilazione, ogni file cpp viene compilato in un file oggetto (di solito estensione .o), e il linker combina i vari file oggetto nel eseguibile finale. Il processo di collegamento è in genere molto più veloce di compilazione.

I vantaggi di questa separazione: se si sta ricompilando uno dei file cpp nel progetto, non c'è bisogno di ricompilare tutti gli altri. Basta creare il nuovo file oggetto per quel particolare file cpp. Il compilatore non ha bisogno di guardare le altre file cpp. Tuttavia, se si desidera chiamare le funzioni nel file cpp corrente che sono state attuate negli altri file cpp, si deve dire al compilatore quali argomenti prendono; questo è lo scopo di includere i file di intestazione.

Svantaggi: Quando si compila un dato file cpp, il compilatore non può 'vedere' ciò che è dentro gli altri file cpp. Quindi non sa come le funzioni non sono implementate, e di conseguenza non è in grado di ottimizzare in modo aggressivo. Ma penso che non c'è bisogno di preoccuparti di quello appena ancora (:

L'idea di base che le intestazioni solo sono inclusi e file cpp vengono compilati solo. Questo diventerà più utile una volta che si dispone di molti file cpp, e ricompilare l'intera applicazione quando si modifica solo uno di loro sarà troppo lento. O quando le funzioni nei file inizieranno a seconda vicenda. Quindi, si dovrebbe separare dichiarazioni di classe nei file di intestazione, lasciare implementazione nei file cpp e scrivere un Makefile (o qualcosa d'altro, a seconda di quali strumenti si sta utilizzando) per compilare i file cpp e collegare i file oggetto risultanti in un programma.

Se si #include un file cpp in diversi altri file nel programma, il compilatore cercherà di compilare il file cpp più volte, e genererà un errore come ci saranno più implementazioni degli stessi metodi.

compilazione richiederà più tempo (che diventa un problema su progetti di grandi dimensioni), se si apportano modifiche nel file cpp #included, che poi forzare la ricompilazione di tutti i file li #including.

Basta mettere le dichiarazioni in file di intestazione e comprendono (come in realtà non generano codice di per sé), e il linker collegare le dichiarazioni con il codice cpp corrispondente (che solo allora viene compilato una volta).

Mentre è certamente possibile fare come hai fatto, la prassi è quella di mettere le dichiarazioni condivise in file di intestazione (h), e le definizioni di funzioni e variabili - implementazione -. In file di origine (cpp)

Per convenzione, questo aiuta a chiarire dove tutto è, e fa una chiara distinzione tra interfaccia e implementazione dei moduli. Significa anche che non si controlla per vedere se un file cpp è incluso in un altro, prima di aggiungere qualcosa ad esso che potrebbe rompere se è stato definito in diverse unità diverse.

riutilizzabilità, l'architettura e l'incapsulamento dei dati

Ecco un esempio:

dici si crea un file cpp che contiene una semplice forma di routine stringa tutto in un mystring di classe, si posiziona il decl classe per questo in un MyString.h compilazione MyString.cpp in un file obj

ora nel tuo programma principale (ad esempio main.cpp) si include intestazione e legame con il mystring.obj. utilizzare mystring nel programma non si cura dei dettagli Come mystring è implementato in quanto l'intestazione dice quanto Si può fare

Ora, se un compagno vuole utilizzare la classe mystring gli dai MyString.h e la mystring.obj, anche lui non ha necessariamente bisogno di sapere come funziona finché funziona.

in seguito, se si dispone di più come obj file si possono combinare in un file LIB e link a quella invece.

si può anche decidere di cambiare il file MyString.cpp e implementare in modo più efficace, questo non influenzerà il vostro main.cpp o il vostro programma di amici.

Se funziona per voi allora non c'è niente di sbagliato con esso -., Tranne che sarà arruffare le penne di persone che pensano che c'è un solo modo per fare le cose

Molte delle risposte qui riportati ottimizzazioni di indirizzo per progetti software di grandi dimensioni. Queste sono cose buone da sapere su, ma non c'è nessun punto per ottimizzare un piccolo progetto come se fosse un grande progetto - che è ciò che è noto come "ottimizzazione prematura". A seconda dell'ambiente di sviluppo, ci possono essere significative ulteriore complessità coinvolti nella creazione di una configurazione di generazione di supportare più file di origine per programma.

Se, nel corso del tempo, il progetto si evolve e si scopre che il processo di compilazione richiede troppo tempo, poi puoi refactoring il codice per utilizzare più file di origine per incrementale veloce costruisce.

Molte delle risposte discutere separando l'interfaccia dall'implementazione. Tuttavia, questa non è una caratteristica intrinseca di includere i file, ed è abbastanza comune # include file "intestazione" che incorporano direttamente la loro attuazione (anche la libreria standard C ++ fa questo in misura significativa).

L'unica cosa veramente "non convenzionale" di quello che avete fatto è stato di denominazione dei file inclusi "cpp" invece di ".h" o ".hpp".

Quando si compila e collegare un programma il compilatore compila prima i singoli file cpp e poi Link (collegamento) di loro. Le intestazioni non potrà mai ottenere compilato, se non compresi in un file cpp prima.

In genere le intestazioni sono le dichiarazioni e CPP sono file di implementazione. Nelle intestazioni si definisce un'interfaccia per una classe o di una funzione, ma si lascia fuori come effettivamente implementare i dettagli. In questo modo non c'è bisogno di ricompilare ogni file cpp se si apporta una modifica in uno.

Io suggerisco di passare attraverso Large Scale C ++ Software Design by Giovanni Lakos . Nel collegio, scriviamo solitamente piccoli progetti dove non si incontrano questi problemi. Il libro mette in evidenza l'importanza di separare interfacce e le implementazioni.

file

intestazione di solito hanno interfacce che si suppone non essere cambiato così spesso. Allo stesso modo uno sguardo in modelli come virtuale Constructor idioma vi aiuterà a cogliere ulteriormente il concetto.

Sto ancora imparando come te:)

È come scrivere un libro, vuoi stampare i capitoli finiti solo una volta

Diciamo che stai scrivendo un libro.Se inserisci i capitoli in file separati, dovrai stampare un capitolo solo se lo hai modificato.Lavorare su un capitolo non cambia nessuno degli altri.

Ma includere i file cpp è, dal punto di vista del compilatore, come modificare tutti i capitoli del libro in un unico file.Quindi se lo cambi devi stampare tutte le pagine dell'intero libro per poter stampare il capitolo rivisto.Non esiste l'opzione "stampa pagine selezionate" nella generazione del codice oggetto.

Torniamo al software:Ho Linux e Ruby src in giro.Una misura approssimativa di righe di codice...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, ruby top level dir)
10,000,000    200,000   everything 

Ognuna di queste quattro categorie ha molto codice, da qui la necessità di modularità.Questo tipo di base di codice è sorprendentemente tipico dei sistemi del mondo reale.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top