Come posso utilizzare efficacemente un file per classe in C ++?

6

Anche se ho un discreto livello di esperienza professionale con programmazione orientata agli oggetti in Java e una familiarità di base con C, mi sono imbattuto in un blocco mentale con C ++ che speravo che altri sviluppatori potessero aiutarmi a brillare luce accesa.

In Java, quasi ogni volta che definisco una classe, creo naturalmente un nuovo file .java. Ovviamente ci sono le eccezioni delle classi interne e anonime, ma nel complesso una nuova classe indica un nuovo file. Con le nuove classi vengono i pacchetti per organizzarli e così via, e come ho iniziato a dilettarmi in C ++ ho preso lo stesso approccio di un file .java per classe e l'ho esteso a un file .hpp e un file .cpp per classe . Tuttavia, sembra che ci siano molti problemi con questo approccio. Quello che ho pensato ultimamente è così:

Diciamo che ho qualche tipo di gioco in cui c'è una classe che rappresenta il mondo di gioco e una classe per rappresentare le unità. Se il gioco di classe mondiale ha un metodo per ottenere l'unità in una posizione, va benissimo. Tuttavia, se voglio avere un metodo per recuperare il mondo che contiene dall'unità, le direttive di inclusione non si risolveranno nel modo in cui sono abituato a costruire progetti poiché ogni intestazione include l'altro. So che ovviamente deve esserci una soluzione pulita e voglio sapere se alcuni sviluppatori più esperti possono avere idee sulle risorse relative all'organizzazione del progetto C ++ o sulla gestione del costrutto specifico che ho citato.

    
posta SaxSalute 01.07.2016 - 04:25
fonte

4 risposte

6

In C ++ hai piena flessibilità su come vuoi organizzare i tuoi file. Ma devi abituarti a questa libertà per fare le buone scelte:

  • Una prima pratica è quella di includere protezioni nelle intestazioni, per evitare ciò a causa di dipendenze condivise , la stessa intestazione viene inclusa più volte.

  • Un secondo è la disciplina: crea intestazioni autosufficienti e includi sempre in esse tutte le dipendenze dell'intestazione richieste . Rendi no ipotesi su ciò che il codice incluso dovrebbe aver incluso. Non preoccuparti della dipendenza reciproca: di solito viene gestita tramite forward dichiarazioni e separando dichiarazione dall'implementazione

  • Un terzo è decidere e utilizzare in modo coerente la granularità desiderata per le intestazioni: vuoi un'intestazione per classe (se ci sei abituato, perché non continuare)? O preferisci un'intestazione per unità logiche superiori (ad esempio classe e suoi ausiliari)?

  • Poi viene la domanda directory e spazi dei nomi (forse qualcosa che si chiude alla nozione di" pacchetti "java per il momento): si conservano file di namespace diversi in directory diverse o no? Si noti che questa riflessione può anche essere strettamente correlata alla questione se realizzare un sottoprogetto di libreria o meno.

  • Non dimenticare che l'ambiente di sviluppo potrebbe anche influenzare la struttura della directory scelta ( esempio o esempio ).

Per il futuro, potrebbe essere interessante tenere d'occhio il concetto di moduli C ++ . Non è ancora standard, quindi non dovresti costruire il tuo attuale approccio su di esso ora. Eppure è interessante, poiché intende semplificare la componentizzazione del codice usando un pacchetto java come la moda, che gradualmente eliminerebbe le intestazioni. Ma ti lascerà comunque una grande libertà per l'organizzazione dei file.

    
risposta data 01.07.2016 - 15:54
fonte
5

Say I have some kind of game where there is a class representing the game world and a class to represent the units. If the game world class has a method to get the unit at a location, that's all well and good.

Immagino tu intenda qualcosa del genere

// Gamworld.hpp
#ifndef GAMEWORLD_HPP
#define GAMEWORLD_HPP
#endif

#include "Unit.hpp"

class Gameworld
{
     public:
       Unit GetUnit();
};

However, if I want to have a method for retrieving the containing world from the unit, the include directives won't resolve in the way that I'm used to building projects since each header includes the other.

No, non è necessario includere Gameworld.hpp in Unit.hpp se si utilizza una dichiarazione diretta e si utilizzano solo puntatori, riferimenti o punti di riferimento condivisi:

// Unit.hpp
#ifndef UNIT_HPP
#define UNIT_HPP
#endif

class Gameworld;

class Unit
{  
      Gameworld *world;

   public:
       Gameworld *GetWorld();
}

Includere solo Gameworld.hpp nell'Unità. cpp :

// Unit.cpp
#include "Gameworld.hpp"

Gameworld *Unit::GetWorld()
{
    return world;
}

Spero che tu abbia un'idea di base. Anche se un po 'obsoleto, consiglio vivamente di ottenere una copia del libro di Lakos "Progettazione software C ++ su larga scala" , spiegherà tutti i dettagli e le strategie cruente sul layout fisico del file C ++ che è necessario conoscere.

    
risposta data 01.07.2016 - 16:54
fonte
5

Le opzioni per la risoluzione delle dipendenze circolari sono:

  1. forward forward

    Sì, questo è comunemente usato e non è considerato un hack.

    Nota che solo le interfacce pubbliche dei tuoi due tipi interdipendenti hanno bisogno della dichiarazione forward: se l'implementazione è out-of-line, è un'unità di traduzione separata e non c'è alcun problema con ciascuna di esse l'intestazione dell'altro.

    Si noti inoltre che anche se si inserisce tutto in una singola intestazione, come suggerisce Basile, è del tutto normale utilizzare la forward-declaration anche quando la dichiarazione vera segue più tardi nella stessa intestazione.

  2. tipo di cancellazione

    Assegna all'unità un puntatore o un riferimento a una classe base astratta della classe di gioco reale, che di per sé non ha alcuna dipendenza dal tipo di unità. Questo però obbliga il chiamante a trasmettere,

    Potresti usare void* invece di introdurre un ABC, ma è ancora più brutto.

L'opzione migliore, tuttavia, è quella di interrompere completamente la dipendenza se possibile.

Quando hai effettivamente un oggetto unitario, ma non il contesto di gioco? Non puoi semplicemente passarlo invece di rendere ogni oggetto unitario più grande per contenere un riferimento allo stato che il chiamante aveva già?

    
risposta data 01.07.2016 - 14:25
fonte
3

In C ++ 11 (e in altri dialetti di C ++ e in C) puoi avere diverse unità di traduzione per un dato programma. Quindi il tuo programma potrebbe essere composto da foo.cc , bar.cc , gee.cc file sorgente C ++ compilati separatamente e collegato insieme. Se utilizzi GCC (ad es. Su Linux), dovrai compilare il tuo programma con ad esempio

 g++ -Wall -g -c foo.cc
 g++ -Wall -g -c bar.cc
 g++ -Wall -g -c gee.cc

Sopra: -Wall chiede tutti gli avvertimenti, -g chiede informazioni di debug, -c chiede solo la compilazione senza collegamento.

(così foo.cc viene compilato nel file foo.o oggetto , bar.cc in bar.o , e così via) e successivamente collegarlo (in un progbin eseguibile ) con

 g++ -g foo.o bar.o gee.o -o progbin

In pratica non dovrai digitare i comandi ogni volta, ma dovresti usare un builder come fare per guidare ed eseguire i comandi precedenti (vedi questo e che esempi di Makefile -s ). I comandi sopra riportati sono per un caso semplice, leggi la documentazione su Invocazione di GCC per ulteriori (e ordine di argomenti su g++ conta molto!). Spesso si desidera aggiungere -I dir opzioni per la compilazione con alcuni include directory dir e -L dir & -l nome opzioni per il collegamento (aggiungi una directory della libreria dir , e usa una data libreria nome ).

Vuoi evitare di scrivere (e copiare / incollare) le stesse dichiarazioni più volte in ogni file sorgente. Puoi (e talvolta devi) avere inoltrare dichiarazioni e tipi di puntatori opachi (si desidera utilizzare ampiamente puntatori intelligenti ), leggi linguaggio PIMPL . Pertanto, avrai un file di intestazione myprog.h comune (o diversi file di intestazione) che sarebbe #include -d da ogni file sorgente foo.cc , bar.cc ecc ....).

Non ci sono obblighi per i file di intestazione e di origine. In particolare (contrariamente a Java) puoi (e spesso vuoi) avere diverse dichiarazioni in un file di intestazione e diverse definizioni di classi e funzioni (& file sorgente.

Quindi per un progetto piccolo (meno di 50KLOC), consiglierei di avere un singolo file di intestazione comune (con tutte le dichiarazioni) che potrebbe essere precompilato e alcuni file sorgente. Ma è soprattutto una questione di opinione: mi piace avere un singolo file di intestazione (di qualche migliaio di righe) e pochi file di origine (di qualche migliaio di righe) ma ad alcune persone piace avere molti piccoli file di intestazione e piccoli file sorgente (che rende più lento il tempo di compilazione).

Una volta eseguito il debug del tuo programma (con l'aiuto di gdb e valgrind , forse usando alcuni opzioni di strumentazione compilando e collegando con -fsanitize=address o -fsanitize=undefined o altre opzioni -fsanitize= ), potresti voler profilo o benchmark. Per il profiling, compila con -pg dopo -g e usa gprof e / o oprofile . Per il benchmarking, vuoi chiedere al compilatore di ottimizzare con -O2 .

In pratica, controlla il codice esistente software gratuito (ad esempio su github ) per esempi concreti.

    
risposta data 01.07.2016 - 06:52
fonte

Leggi altre domande sui tag