Classes et objets: combien et de quels types de fichiers ai-je réellement besoin pour les utiliser?

20

Je n'ai aucune expérience avec C ++ ou C, mais je sais programmer C # et j'apprends Arduino. Je veux juste organiser mes croquis et je suis assez à l'aise avec le langage Arduino même avec ses limites, mais j'aimerais vraiment avoir une approche orientée objet de ma programmation Arduino.

J'ai donc vu que vous pouvez avoir les moyens suivants (liste non exhaustive) d'organiser le code:

  1. Un seul fichier .ino;
  2. Plusieurs fichiers .ino dans le même dossier (ce que l'EDI appelle et affiche comme des "onglets");
  3. Un fichier .ino avec un fichier .h et .cpp inclus dans le même dossier;
  4. Comme ci-dessus, mais les fichiers sont une bibliothèque installée dans le dossier du programme Arduino.

J'ai également entendu parler des moyens suivants, mais je ne les ai pas encore fait fonctionner:

  • Déclarer une classe de style C ++ dans le même fichier .ino unique (a entendu parler, mais n'a jamais vu de travail - est-ce même possible?);
  • [approche préférée] Inclure un fichier .cpp dans lequel une classe est déclarée, mais sans utiliser de fichier .h (aimerait cette approche, cela devrait-il fonctionner?);

Notez que je veux uniquement utiliser des classes pour que le code soit plus partitionné, mes applications devraient être très simples, impliquant uniquement des boutons, des leds et des buzzers.

heltonbiker
la source
Pour ceux qui sont intéressés, il y a une discussion intéressante sur les définitions de classe sans en-tête (cpp uniquement) ici: programmers.stackexchange.com/a/35391/35959
heltonbiker

Réponses:

31

Comment l'IDE organise les choses

Tout d'abord, voici comment l'IDE organise votre "sketch":

  • Le .inofichier principal est celui du même nom que le dossier dans lequel il se trouve. Donc, pour foobar.inodans le foobardossier - le fichier principal est foobar.ino.
  • Tous les autres .inofichiers de ce dossier sont concaténés ensemble, par ordre alphabétique, à la fin du fichier principal (indépendamment de l'endroit où se trouve le fichier principal, par ordre alphabétique).
  • Ce fichier concaténé devient un .cppfichier (par exemple. foobar.cpp) - il est placé dans un dossier de compilation temporaire.
  • Le préprocesseur génère "utilement" des prototypes de fonction pour les fonctions qu'il trouve dans ce fichier.
  • Le fichier principal est analysé pour les #include <libraryname>directives. Cela déclenche l'IDE pour copier également tous les fichiers pertinents de chaque bibliothèque (mentionnée) dans le dossier temporaire et générer des instructions pour les compiler.
  • Tous les .c, .cppou les .asmfichiers dans le dossier d' esquisse sont ajoutés au processus de construction d'unités de compilation séparée (qui est, ils sont compilés de la manière habituelle sous forme de fichiers séparés)
  • Tous les .hfichiers sont également copiés dans le dossier de compilation temporaire, afin qu'ils puissent être référencés par vos fichiers .c ou .cpp.
  • Le compilateur ajoute dans le processus de construction des fichiers standard (comme main.cpp)
  • Le processus de génération compile ensuite tous les fichiers ci-dessus dans des fichiers objets.
  • Si la phase de compilation réussit, ils sont liés entre eux avec les bibliothèques standard AVR (par exemple, vous donner, strcpyetc.)

Un effet secondaire de tout cela est que vous pouvez considérer l'esquisse principale (les fichiers .ino) comme C ++ à toutes fins utiles. La génération du prototype de fonction peut cependant conduire à des messages d'erreur obscurs si vous ne faites pas attention.


Éviter les bizarreries du pré-processeur

Le moyen le plus simple d'éviter ces idiosyncrasies est de laisser votre esquisse principale vierge (et de ne pas utiliser d'autres .inofichiers). Ensuite, créez un autre onglet (un .cppfichier) et mettez-y vos trucs comme ceci:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Notez que vous devez inclure Arduino.h. L'IDE le fait automatiquement pour l'esquisse principale, mais pour les autres unités de compilation, vous devez le faire. Sinon, il ne connaîtra pas des choses comme String, les registres matériels, etc.


Éviter le paradigme de configuration / principal

Vous n'avez pas à exécuter le concept de configuration / boucle. Par exemple, votre fichier .cpp peut être:

#include <Arduino.h>

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

Forcer l'inclusion de bibliothèque

Si vous utilisez le concept de "croquis vide", vous devez toujours inclure les bibliothèques utilisées ailleurs dans le projet, par exemple dans votre .inofichier principal :

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

En effet, l'EDI analyse uniquement le fichier principal pour l'utilisation de la bibliothèque. En effet, vous pouvez considérer le fichier principal comme un fichier "projet" qui désigne les bibliothèques externes utilisées.


Problèmes de dénomination

  • Ne nommez pas votre croquis principal "main.cpp" - l'EDI inclut son propre main.cpp, vous en aurez donc un double si vous le faites.

  • Ne nommez pas votre fichier .cpp avec le même nom que votre fichier .ino principal. Étant donné que le fichier .ino devient effectivement un fichier .cpp, cela vous donnera également un conflit de noms.


Déclarer une classe de style C ++ dans le même fichier .ino unique (a entendu parler, mais n'a jamais vu de travail - est-ce même possible?);

Oui, cela compile OK:

class foo {
  public:
};

foo bar;

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

Cependant, il est probablement préférable de suivre la pratique normale: Mettez vos déclarations dans des .hfichiers et vos définitions (implémentations) dans des fichiers .cpp(ou .c).

Pourquoi "probablement"?

Comme mon exemple le montre, vous pouvez tout rassembler dans un seul fichier. Pour les grands projets, il vaut mieux être plus organisé. Finalement, vous arrivez sur scène dans un projet de taille moyenne à grande où vous voulez séparer les choses en "boîtes noires" - c'est-à-dire une classe qui fait une chose, le fait bien, est testée et est autonome ( le plus loin possible).

Si cette classe est ensuite utilisée dans plusieurs autres fichiers de votre projet, c'est là que les fichiers séparés .het .cppentrent en jeu.

  • Le .hfichier déclare la classe - c'est-à-dire qu'il fournit suffisamment de détails pour que les autres fichiers sachent ce qu'il fait, quelles fonctions il a et comment ils sont appelés.

  • Le .cppfichier définit (implémente) la classe - c'est-à-dire qu'il fournit en fait les fonctions et les membres statiques de la classe qui font que la classe fait son travail. Comme vous ne voulez l'implémenter qu'une seule fois, cela se trouve dans un fichier séparé.

  • Le .hfichier est ce qui est inclus dans les autres fichiers. Le .cppfichier est compilé une fois par l'IDE pour implémenter les fonctions de classe.

Bibliothèques

Si vous suivez ce paradigme, vous êtes prêt à déplacer très facilement toute la classe (les fichiers .het .cpp) dans une bibliothèque. Il peut ensuite être partagé entre plusieurs projets. Tout ce qui est nécessaire est de créer un dossier (par exemple. myLibrary) Et d'y placer les fichiers .het .cpp(par exemple. myLibrary.hEt myLibrary.cpp), puis de placer ce dossier dans votre librariesdossier dans le dossier où sont conservés vos croquis (le dossier du carnet de croquis).

Redémarrez l'IDE et il connaît maintenant cette bibliothèque. C'est vraiment très simple, et maintenant vous pouvez partager cette bibliothèque sur plusieurs projets. Je le fais beaucoup.


Un peu plus de détails ici .

Nick Gammon
la source
Bonne réponse. Un sujet le plus important, cependant, ne m'est pas encore devenu clair: pourquoi tout le monde dit "vous êtes probablement mieux de suivre la pratique normale: .h + .cpp"? Pourquoi est-ce mieux? Pourquoi la partie probablement ? Et le plus important: comment puis-je ne pas le faire, c'est-à-dire avoir à la fois l'interface et l' implémentation (c'est-à-dire le code de classe entier) dans le même fichier .cpp unique? Merci beaucoup pour l'instant! : o)
heltonbiker
Ajout de quelques paragraphes pour expliquer pourquoi "probablement" vous devriez avoir des fichiers séparés.
Nick Gammon
1
Comment ne le faites-vous pas ? Mettez-les tous ensemble comme illustré dans ma réponse, mais vous pouvez constater que le préprocesseur fonctionne contre vous. Certaines définitions de classe C ++ parfaitement valides échouent si elles sont placées dans le fichier .ino principal.
Nick Gammon
Ils échoueront également si vous incluez un fichier .H dans deux de vos fichiers .cpp et que ce fichier .h contient du code, ce qui est une habitude courante pour certains. Son open source, corrigez-le vous-même. Si vous n'êtes pas à l'aise avec cela, vous ne devriez probablement pas utiliser l'open source. Belle explication @ Nick Gammon, mieux que tout ce que j'ai vu jusqu'à présent.
@ Spiked3 Il ne s'agit pas tant de choisir avec quoi je suis le plus à l'aise, pour l'instant, il s'agit de savoir ce que je peux choisir en premier lieu. Comment pourrais-je faire un choix judicieux si je ne sais même pas quelles sont mes options, et pourquoi chaque option est comme elle est? Comme je l'ai dit, je n'ai aucune expérience antérieure avec C ++, et il semble que C ++ dans Arduino puisse nécessiter des précautions supplémentaires, comme le montre cette même réponse. Mais je suis sûr qu'en fin de compte, je m'en
saisis
6

Mon conseil est de s'en tenir à la manière typique de faire C ++: interface séparée et implémentation dans des fichiers .h et .cpp pour chaque classe.

Il y a quelques captures:

  • vous avez besoin d'au moins un fichier .ino - j'utilise un lien symbolique vers le fichier .cpp où j'instancie les classes.
  • vous devez fournir les rappels que l'environnement Arduino attend (setu, boucle, etc.)
  • dans certains cas, vous serez surpris par les choses étranges non standard qui différencient l'IDE Arduino d'un IDE normal, comme l'inclusion automatique de certaines bibliothèques, mais pas d'autres.

Ou, vous pouvez abandonner l'IDE Arduino et essayer avec Eclipse . Comme je l'ai mentionné, certaines des choses qui sont censées aider les débutants ont tendance à gêner les développeurs plus expérimentés.

Igor Stoppa
la source
Bien que je pense que séparer une esquisse en plusieurs fichiers (onglets ou inclus) aide tout à être à sa place, j'ai l'impression d'avoir besoin d'avoir deux fichiers pour prendre soin de la même chose (.h et .cpp) est une sorte de redondance / duplication inutile. J'ai l'impression que la classe est définie deux fois, et chaque fois que je dois changer un endroit, je dois changer l'autre. Notez que cela ne s'applique qu'aux cas simples comme le mien, où il n'y aura qu'une seule implémentation d'un en-tête donné, et ils ne seront utilisés qu'une seule fois (dans une seule esquisse).
heltonbiker
Il simplifie le travail du compilateur / éditeur de liens et vous permet d'avoir dans les fichiers .cpp des éléments qui ne font pas partie de la classe, mais qui sont utilisés dans une certaine méthode. Et dans le cas où la classe a des memers statiques, vous ne pouvez pas les placer dans le fichier .h.
Igor Stoppa
La séparation des fichiers .h et .cpp est depuis longtemps reconnue comme inutile. Java, C #, JS ne nécessitent aucun fichier d'en-tête, et même les normes iso cpp essaient de s'en éloigner. Le problème est qu'il y a trop de code hérité qui pourrait rompre avec un changement aussi radical. C'est la raison pour laquelle nous avons CPP après C, et pas seulement un C. élargi. Je m'attends à ce que la même chose se reproduise, CPX après CPP?
Bien sûr, si la prochaine révision propose un moyen d'effectuer les mêmes tâches que celles effectuées par les en-têtes, sans en-têtes ... mais en attendant, il y a beaucoup de choses qui ne peuvent pas être faites sans en-têtes: je veux voir comment la compilation distribuée pourrait se produire sans en-têtes et sans entraîner de frais généraux importants.
Igor Stoppa
6

Je poste une réponse juste pour être complet, après avoir découvert et testé un moyen de déclarer et d' implémenter une classe dans le même fichier .cpp, sans utiliser d'en-tête. Donc, en ce qui concerne la formulation exacte de ma question "combien de types de fichiers dois-je utiliser pour les classes", la réponse actuelle utilise deux fichiers: un .ino avec un include, une configuration et une boucle, et le .cpp contenant l'ensemble (plutôt minimaliste) ), représentant les clignotants d'un véhicule-jouet.

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;
    }
};
heltonbiker
la source
1
C'est semblable à Java et gifle l'implémentation des méthodes dans la déclaration de la classe. Outre la lisibilité réduite - l'en-tête vous donne la déclaration des méthodes sous une forme concise - je me demande si des déclarations de classe plus inhabituelles (comme avec la statique, les amis, etc.) fonctionneraient toujours. Mais la plupart de cet exemple n'est pas vraiment bon, car il n'inclut le fichier qu'une fois qu'une inclusion concatène simplement. Les vrais problèmes commencent lorsque vous incluez le même fichier à plusieurs endroits et que vous commencez à obtenir des déclarations d'objet conflictuelles de l'éditeur de liens.
Igor Stoppa