Classi e oggetti: quanti e quali tipi di file sono effettivamente necessari per usarli?


20

Non ho precedenti esperienze con C ++ o C, ma so programmare C # e sto imparando Arduino. Voglio solo organizzare i miei schizzi e sono abbastanza a mio agio con il linguaggio Arduino anche con i suoi limiti, ma mi piacerebbe davvero avere un approccio orientato agli oggetti per la mia programmazione Arduino.

Quindi ho visto che puoi organizzare i seguenti modi (non elenco esaustivo) per organizzare il codice:

  1. Un singolo file .ino;
  2. Più file .ino nella stessa cartella (ciò che l'IDE chiama e visualizza come "tab");
  3. Un file .ino con un file .h e .cpp incluso nella stessa cartella;
  4. Come sopra, ma i file sono una libreria installata nella cartella del programma Arduino.

Ho anche sentito parlare dei seguenti modi, ma non li ho ancora fatti funzionare:

  • Dichiarare una classe in stile C ++ nello stesso singolo file .ino (ne hai sentito parlare, ma non l'hai mai visto funzionare - è persino possibile?);
  • [approccio preferito] Incluso un file .cpp in cui viene dichiarata una classe, ma senza utilizzare un file .h (piacerebbe questo approccio, dovrebbe funzionare?);

Nota che voglio solo usare le classi in modo che il codice sia più partizionato, le mie applicazioni dovrebbero essere molto semplici, coinvolgendo principalmente solo pulsanti, led e cicalini.


Per coloro che sono interessati, c'è una discussione interessante sulle definizioni delle classi headerless (solo cpp) qui: programmers.stackexchange.com/a/35391/35959
heltonbiker

Risposte:


31

Come l'IDE organizza le cose

Per prima cosa, ecco come l'IDE organizza il tuo "schizzo":

  • Il .inofile principale è lo stesso nome della cartella in cui si trova. Quindi, per foobar.inonella foobarcartella - il file principale è foobar.ino.
  • Tutti gli altri .inofile in quella cartella vengono concatenati insieme, in ordine alfabetico, alla fine del file principale (indipendentemente da dove si trova il file principale, in ordine alfabetico).
  • Questo file concatenato diventa un .cppfile (ad es. foobar.cpp) - viene inserito in una cartella di compilazione temporanea.
  • Il preprocessore "utile" genera prototipi di funzioni per le funzioni che trova in quel file.
  • Il file principale è sottoposto a scansione per le #include <libraryname>direttive. Questo innesca l'IDE per copiare anche tutti i file rilevanti da ciascuna libreria (menzionata) nella cartella temporanea e generare istruzioni per compilarli.
  • Eventuali .c, .cppo .asmfile nella cartella bozza vengono aggiunti al processo di compilazione come unità di compilazione distinte (che è, sono compilati nel modo usuale come file separati)
  • Tutti i .hfile vengono anche copiati nella cartella di compilazione temporanea, quindi possono essere indicati dai file .c o .cpp.
  • Il compilatore aggiunge ai file standard del processo di compilazione (come main.cpp)
  • Il processo di compilazione quindi compila tutti i file sopra in file oggetto.
  • Se la fase di compilazione ha esito positivo, vengono collegati tra loro con le librerie standard AVR (ad es. Dandoti strcpyecc.)

Un effetto collaterale di tutto ciò è che puoi considerare lo schizzo principale (i file .ino) come C ++ a tutti gli effetti. La generazione del prototipo di funzione, tuttavia, può portare a oscuri messaggi di errore se non si presta attenzione.


Evitare le stranezze del pre-processore

Il modo più semplice per evitare queste idiosincrasie è lasciare vuoto lo schizzo principale (e non utilizzare altri .inofile). Quindi crea un'altra scheda (un .cppfile) e inserisci le tue cose in questo modo:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Si noti che è necessario includere Arduino.h. L'IDE lo fa automaticamente per lo schizzo principale, ma per altre unità di compilazione, devi farlo. Altrimenti non conoscerà cose come String, i registri hardware, ecc.


Evitare il paradigma di installazione / principale

Non devi correre con il concetto di setup / loop. Ad esempio, il tuo file .cpp può essere:

#include <Arduino.h>

int main ()
  {
  init ();  // initialize timers
  Serial.begin (115200);
  Serial.println ("Hello, world");
  Serial.flush (); // let serial printing finish
  }  // end of main

Forza l'inclusione della libreria

Se si esegue con il concetto di "schizzo vuoto" è comunque necessario includere le librerie utilizzate altrove nel progetto, ad esempio nel .inofile principale :

#include <Wire.h>
#include <SPI.h>
#include <EEPROM.h>

Questo perché l'IDE esegue la scansione del file principale solo per l'utilizzo della libreria. In effetti è possibile considerare il file principale come un file di "progetto" che nomina quali librerie esterne sono in uso.


Problemi di denominazione

  • Non nominare il tuo schizzo principale "main.cpp" - l'IDE include il proprio main.cpp, quindi avrai un duplicato se lo fai.

  • Non nominare il tuo file .cpp con lo stesso nome del tuo file .ino principale. Poiché il file .ino diventa effettivamente un file .cpp, anche questo ti darebbe uno scontro di nomi.


Dichiarare una classe in stile C ++ nello stesso singolo file .ino (ne hai sentito parlare, ma non l'hai mai visto funzionare - è persino possibile?);

Sì, questo viene compilato OK:

class foo {
  public:
};

foo bar;

void setup () { }
void loop () { }

Tuttavia, probabilmente è meglio seguire la normale pratica: inserire le dichiarazioni in .hfile e le definizioni (implementazioni) in .cpp(o .c) file.

Perché "probabilmente"?

Come mostra il mio esempio, puoi mettere tutto in un unico file. Per progetti più grandi è meglio essere più organizzati. Alla fine si sale sul palco in un progetto di medie e grandi dimensioni in cui si desidera separare le cose in "scatole nere", ovvero una classe che fa una cosa, la fa bene, viene testata ed è indipendente ( per quanto possibile).

Se questa classe viene quindi utilizzata in più altri file nel progetto, è qui .hche .cppentrano in gioco i file separati e i file.

  • Il .hfile dichiara la classe, ovvero fornisce dettagli sufficienti per consentire ad altri file di sapere cosa fa, quali funzioni ha e come vengono chiamati.

  • Il .cppfile definisce (implementa) la classe, ovvero fornisce effettivamente le funzioni e i membri statici della classe che fanno sì che la classe faccia il suo lavoro. Dal momento che si desidera implementarlo solo una volta, questo è in un file separato.

  • Il .hfile è ciò che viene incluso in altri file. Il .cppfile viene compilato una volta dall'IDE per implementare le funzioni di classe.

biblioteche

Se segui questo paradigma, allora sei pronto a spostare l'intera classe (i file .he .cpp) in una libreria molto facilmente. Quindi può essere condiviso tra più progetti. Tutto ciò che serve è creare una cartella (ad es. myLibrary) E inserire i file .he .cpp(ad es. myLibrary.hE myLibrary.cpp) e quindi inserire questa cartella nella librariescartella nella cartella in cui sono conservati gli schizzi (la cartella dello sketchbook).

Riavvia l'IDE e ora conosce questa libreria. Questo è davvero banalmente semplice e ora puoi condividere questa libreria su più progetti. Lo faccio molto.


Un po 'più di dettagli qui .


Bella risposta. Un argomento molto importante, tuttavia, non mi è ancora stato chiarito: perché tutti dicono " probabilmente è meglio seguire la pratica normale: .h + .cpp"? Perché è meglio? Perché probabilmente la parte? E, cosa più importante: come posso non farlo, cioè avere sia l'interfaccia che l' implementazione (cioè, l'intero codice di classe) nello stesso singolo file .cpp? Grazie mille per ora! : o)
heltonbiker,

Aggiunti altri due paragrafi per rispondere al motivo per cui "probabilmente" dovresti avere file separati.
Nick Gammon

1
Come si fa a non farlo? Mettili tutti insieme come illustrato nella mia risposta, tuttavia potresti scoprire che il preprocessore lavora contro di te. Alcune definizioni di classe C ++ perfettamente valide non riescono se vengono inserite nel file .ino principale.
Nick Gammon

Inoltre falliranno se includi un file .H in due dei tuoi file .cpp e quel file .h contiene codice, che è un'abitudine comune di alcuni. È open source, riparalo da solo. Se non ti senti a tuo agio nel farlo, probabilmente non dovresti utilizzare l'open source. Bella spiegazione @ Nick Gammon, meglio di qualsiasi cosa abbia visto finora.

@ Spiked3 Non è tanto una questione di scegliere ciò con cui mi sento più a mio agio, per ora, è una questione di sapere cosa è disponibile per me da scegliere in primo luogo. Come potrei fare una scelta sensata se non so nemmeno quali sono le mie opzioni e perché ogni opzione è come è? Come ho detto, non ho precedenti esperienze con il C ++ e sembra che il C ++ in Arduino potrebbe richiedere un'attenzione particolare, come mostrato in questa risposta. Ma sono sicuro che alla fine ne avrò
un'idea

6

Il mio consiglio è di attenersi al tipico modo C ++ di fare le cose: interfaccia separata e implementazione in file .h e .cpp per ogni classe.

Ci sono alcune catture:

  • hai bisogno di almeno un file .ino - Uso un link simbolico al file .cpp dove istanzio le classi.
  • è necessario fornire i callback previsti dall'ambiente Arduino (setu, loop, ecc.)
  • in alcuni casi rimarrai sorpreso dalle cose strane non standard che differenziano l'IDE di Arduino da uno normale, come l'inclusione automatica di alcune librerie, ma non di altre.

Oppure, potresti abbandonare l'IDE di Arduino e provare con Eclipse . Come ho già detto, alcune delle cose che dovrebbero aiutare i principianti, tendono a ostacolare gli sviluppatori più esperti.


Mentre sento che separare uno schizzo in più file (schede o include) aiuta tutto a trovarsi al suo posto, mi sento come avere bisogno di avere due file per occuparsi della stessa cosa (.h e .cpp) è una sorta di ridondanza / duplicazione non necessaria. Sembra che la classe venga definita due volte e ogni volta che devo cambiare un posto, devo cambiare l'altro. Nota che questo vale solo per casi semplici come il mio, dove ci sarà una sola implementazione di una data intestazione, e saranno usati solo una volta (in un singolo schizzo).
Heltonbiker,

Semplifica il lavoro del compilatore / linker e ti permette di avere nei file .cpp elementi che non fanno parte della classe, ma sono usati in qualche metodo. E nel caso in cui la classe abbia memer statici, non è possibile inserirli nel file .h.
Igor Stoppa,

La separazione dei file .h e .cpp è stata a lungo riconosciuta come non necessaria. Java, C #, JS non richiedono file di intestazione e persino gli standard ISO cpp stanno cercando di allontanarsi da essi. Il problema è che c'è troppo codice legacy che potrebbe rompersi con un cambiamento così radicale. Questo è il motivo per cui abbiamo CPP dopo C, e non solo un C. espanso. Mi aspetto che lo stesso accada di nuovo, CPX dopo CPP?

Certo, se la prossima revisione viene fuori con un modo per eseguire gli stessi compiti che vengono eseguiti dalle intestazioni, senza intestazioni ... ma nel frattempo ci sono molte cose che non possono essere fatte senza intestazioni: voglio vedere come la compilazione distribuita potrebbe accadere senza intestazioni e senza incorrere in grandi spese generali.
Igor Stoppa,

6

Sto postando una risposta solo per completezza, dopo aver scoperto e testato un modo per dichiarare e implementare una classe nello stesso file .cpp, senza usare un'intestazione. Quindi, per quanto riguarda l'esatta frase della mia domanda "di quanti tipi di file ho bisogno per usare le classi", la presente risposta usa due file: uno .ino con include, setup e loop e il .cpp che contiene il tutto (piuttosto minimalista ) classe, che rappresentano i segnali di svolta di un veicolo giocattolo.

Blinker.ino

#include <TurnSignals.cpp>

TurnSignals turnSignals(2, 4, 8);

void setup() { }

void loop() {
  turnSignals.run();
}

TurnSignals.cpp

#include "Arduino.h"

class TurnSignals
{
    int 
        _left, 
        _right, 
        _buzzer;

    const int 
        amberPeriod = 300,

        beepInFrequency = 600,
        beepOutFrequency = 500,
        beepDuration = 20;    

    boolean
        lightsOn = false;

    public : TurnSignals(int leftPin, int rightPin, int buzzerPin)
    {
        _left = leftPin;
        _right = rightPin;
        _buzzer = buzzerPin;

        pinMode(_left, OUTPUT);
        pinMode(_right, OUTPUT);
        pinMode(_buzzer, OUTPUT);            
    }

    public : void run() 
    {        
        blinkAll();
    }

    void blinkAll() 
    {
        static long lastMillis = 0;
        long currentMillis = millis();
        long elapsed = currentMillis - lastMillis;
        if (elapsed > amberPeriod) {
            if (lightsOn)
                turnLightsOff();   
            else
                turnLightsOn();
            lastMillis = currentMillis;
        }
    }

    void turnLightsOn()
    {
        tone(_buzzer, beepInFrequency, beepDuration);
        digitalWrite(_left, HIGH);
        digitalWrite(_right, HIGH);
        lightsOn = true;
    }

    void turnLightsOff()
    {
        tone(_buzzer, beepOutFrequency, beepDuration);
        digitalWrite(_left, LOW);
        digitalWrite(_right, LOW);
        lightsOn = false;
    }
};

1
È simile a java e schiaffeggia l'implementazione dei metodi nella dichiarazione della classe. A parte la ridotta leggibilità - l'intestazione ti dà la dichiarazione dei metodi in una forma concisa - Mi chiedo se le dichiarazioni di classe più insolite (come con statica, amici ecc.) Funzionerebbero ancora. Ma la maggior parte di questo esempio non è davvero buona, perché include il file solo una volta che un'inclusione si concatena semplicemente. I problemi reali iniziano quando si include lo stesso file in più posizioni e si iniziano a ottenere dichiarazioni di oggetti in conflitto dal linker.
Igor Stoppa,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.