Clases y objetos: ¿cuántos y qué tipos de archivos necesito realmente para usarlos?


20

No tengo experiencia previa con C ++ o C, pero sé cómo programar C # y estoy aprendiendo Arduino. Solo quiero organizar mis bocetos y estoy bastante cómodo con el lenguaje Arduino incluso con sus limitaciones, pero realmente me gustaría tener un enfoque orientado a objetos para mi programación Arduino.

Entonces, he visto que puede tener las siguientes formas (no una lista exhaustiva) para organizar el código:

  1. Un solo archivo .ino;
  2. Múltiples archivos .ino en la misma carpeta (lo que el IDE llama y muestra como "pestañas");
  3. Un archivo .ino con un archivo .h y .cpp incluido en la misma carpeta;
  4. Igual que el anterior, pero los archivos son una biblioteca instalada dentro de la carpeta del programa Arduino.

También he oído hablar de las siguientes formas, pero todavía no las tengo funcionando:

  • Declarar una clase de estilo C ++ en el mismo archivo .ino único (he oído hablar de él, pero nunca lo he visto funcionar, ¿es eso posible?);
  • [enfoque preferido] Incluyendo un archivo .cpp donde se declara una clase, pero sin usar un archivo .h (quisiera este enfoque, ¿debería funcionar?);

Tenga en cuenta que solo quiero usar clases para que el código esté más particionado, mis aplicaciones deben ser muy simples, solo con botones, leds y zumbadores en su mayoría.


Para aquellos interesados, hay una discusión interesante sobre las definiciones de clase sin encabezado (solo cpp) aquí: programmers.stackexchange.com/a/35391/35959
heltonbiker

Respuestas:


31

Cómo organiza las cosas el IDE

Primero, así es como el IDE organiza su "boceto":

  • El .inoarchivo principal tiene el mismo nombre que la carpeta en la que se encuentra. Por lo tanto, foobar.inoen la foobarcarpeta, el archivo principal es foobar.ino.
  • Todos los demás .inoarchivos de esa carpeta se concatenan juntos, en orden alfabético, al final del archivo principal (independientemente de dónde esté el archivo principal, alfabéticamente).
  • Este archivo concatenado se convierte en un .cpparchivo (p. Ej. foobar.cpp), Se coloca en una carpeta de compilación temporal.
  • El preprocesador "útilmente" genera prototipos de funciones para las funciones que encuentra en ese archivo.
  • El archivo principal se analiza en busca de #include <libraryname>directivas. Esto activa el IDE para copiar también todos los archivos relevantes de cada biblioteca (mencionada) en la carpeta temporal y generar instrucciones para compilarlos.
  • Cualquiera .c, .cppo los .asmarchivos en la carpeta de croquis se agregan al proceso de compilación como unidades de compilación separadas (es decir, se compilan de la manera habitual como archivos separados)
  • Todos los .harchivos también se copian en la carpeta de compilación temporal, por lo que sus archivos .c o .cpp pueden consultarlos.
  • El compilador agrega al proceso de compilación archivos estándar (como main.cpp)
  • El proceso de compilación luego compila todos los archivos anteriores en archivos de objetos.
  • Si la fase de compilación tiene éxito, se vinculan entre sí junto con las bibliotecas estándar de AVR (p. Ej., Ofreciéndole, strcpyetc.)

Un efecto secundario de todo esto es que puede considerar que el boceto principal (los archivos .ino) es C ++ a todos los efectos. Sin embargo, la generación del prototipo de la función puede generar mensajes de error oscuros si no tiene cuidado.


Evitar las peculiaridades del preprocesador

La forma más sencilla de evitar estas idiosincrasias es dejar su boceto principal en blanco (y no usar ningún otro .inoarchivo). Luego crea otra pestaña (un .cpparchivo) y coloca tus cosas en ella de esta manera:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Tenga en cuenta que debe incluir Arduino.h. El IDE lo hace automáticamente para el boceto principal, pero para otras unidades de compilación, debe hacerlo. De lo contrario, no sabrá cosas como String, los registros de hardware, etc.


Evitar la configuración / paradigma principal

No tiene que ejecutar con el concepto de configuración / bucle. Por ejemplo, su archivo .cpp puede ser:

#include <Arduino.h>

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

Forzar inclusión de biblioteca

Si ejecuta con el concepto de "boceto vacío" aún necesita incluir bibliotecas utilizadas en otros lugares del proyecto, por ejemplo, en su .inoarchivo principal :

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

Esto se debe a que el IDE solo escanea el archivo principal para el uso de la biblioteca. Efectivamente, puede considerar el archivo principal como un archivo de "proyecto" que designa qué bibliotecas externas están en uso.


Problemas de nombres

  • No nombre su boceto principal "main.cpp": el IDE incluye su propio main.cpp, por lo que tendrá un duplicado si lo hace.

  • No asigne un nombre a su archivo .cpp con el mismo nombre que su archivo .ino principal. Dado que el archivo .ino se convierte efectivamente en un archivo .cpp, esto también le daría un choque de nombres.


Declarar una clase de estilo C ++ en el mismo archivo .ino único (he oído hablar de él, pero nunca lo he visto funcionar, ¿es eso posible?);

Sí, esto compila OK:

class foo {
  public:
};

foo bar;

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

Sin embargo, probablemente sea mejor que siga la práctica normal: coloque sus declaraciones en .harchivos y sus definiciones (implementaciones) en .cpp(o .c) archivos.

¿Por qué "probablemente"?

Como muestra mi ejemplo, puedes juntar todo en un solo archivo. Para proyectos más grandes es mejor estar más organizado. Finalmente, llega al escenario en un proyecto de tamaño mediano a grande donde desea separar las cosas en "cajas negras", es decir, una clase que hace una cosa, lo hace bien, se prueba y es autónoma ( tan lejos como sea posible).

Si esta clase se usa en varios otros archivos en su proyecto, aquí es donde entran en juego los archivos separados .hy .cpp.

  • El .harchivo declara la clase, es decir, proporciona detalles suficientes para que otros archivos sepan qué hace, qué funciones tiene y cómo se llaman.

  • El .cpparchivo define (implementa) la clase, es decir, en realidad proporciona las funciones y los miembros de clase estáticos que hacen que la clase haga lo suyo. Como solo desea implementarlo una vez, esto está en un archivo separado.

  • El .harchivo es lo que se incluye en otros archivos. El .cppIDE compila el archivo una vez para implementar las funciones de clase.

Bibliotecas

Si sigue este paradigma, entonces está listo para mover toda la clase (los archivos .hy .cpp) a una biblioteca muy fácilmente. Luego se puede compartir entre múltiples proyectos. Todo lo que se requiere es hacer una carpeta (p. Ej. myLibrary) Y poner los archivos .hy .cppen ella (p. Ej. myLibrary.hY myLibrary.cpp) y luego poner esta carpeta dentro de su librariescarpeta en la carpeta donde se guardan sus bocetos (la carpeta del cuaderno de bocetos).

Reinicie el IDE y ahora sabe sobre esta biblioteca. Esto es realmente trivialmente simple, y ahora puede compartir esta biblioteca en múltiples proyectos. Lo hago mucho.


Un poco más de detalle aquí .


Buena respuesta. Sin embargo, el tema más importante todavía no me quedó claro: ¿por qué todos dicen " probablemente sea mejor que sigas la práctica normal: .h + .cpp"? ¿Por qué es mejor? ¿Por qué la parte probablemente ? Y lo más importante: ¿cómo puedo no hacerlo, es decir, tener tanto la interfaz como la implementación (es decir, todo el código de clase) en el mismo archivo único .cpp? Muchas gracias por ahora! : o)
heltonbiker

Se agregaron otros dos párrafos para responder por qué "probablemente" debería tener archivos separados.
Nick Gammon

1
¿Cómo no lo hace? Simplemente póngalos todos juntos como se ilustra en mi respuesta, sin embargo, puede encontrar que el preprocesador funciona en su contra. Algunas definiciones de clase C ++ perfectamente válidas fallan si se colocan en el archivo .ino principal.
Nick Gammon

También fallarán si incluye un archivo .H en dos de sus archivos .cpp y ese archivo .h contiene código, que es un hábito común de algunos. Es de código abierto, solo arréglalo tú mismo. Si no se siente cómodo haciendo eso, probablemente no debería estar usando código abierto. Hermosa explicación @Nick Gammon, mejor que cualquier cosa que haya visto hasta ahora.

@ Spiked3 No es tanto una cuestión de elegir con qué me siento más cómodo, por ahora, se trata de saber qué hay disponible para que yo elija en primer lugar. ¿Cómo podría hacer una elección sensata si ni siquiera sé cuáles son mis opciones y por qué cada opción es como es? Como dije, no tengo experiencia previa con C ++, y parece que C ++ en Arduino podría requerir un cuidado adicional, como se muestra en esta misma respuesta. Pero estoy seguro de que eventualmente lo estoy entendiendo y haciendo mis cosas sin reinventar la rueda (al menos eso espero) :)
heltonbiker

6

Mi consejo es mantener la forma típica de hacer las cosas en C ++: interfaz separada e implementación en archivos .h y .cpp para cada clase.

Hay algunas capturas:

  • necesita al menos un archivo .ino. Utilizo un enlace simbólico al archivo .cpp donde instancia las clases.
  • debe proporcionar las devoluciones de llamada que el entorno Arduino espera (setu, loop, etc.)
  • en algunos casos, se sorprenderá de las cosas extrañas no estándar que diferencian el IDE de Arduino de uno normal, como la inclusión automática de ciertas bibliotecas, pero no otras.

O bien, puede deshacerse del Arduino IDE e intentar con Eclipse . Como mencioné, algunas de las cosas que se supone que ayudan a los principiantes, tienden a interponerse en el camino de los desarrolladores más experimentados.


Si bien siento que separar un boceto en varios archivos (pestañas o incluye) ayuda a que todo esté en su propio lugar, siento que la necesidad de tener dos archivos para encargarse de lo mismo (.h y .cpp) es una especie de redundancia / duplicación innecesaria. Parece que la clase se está definiendo dos veces, y cada vez que necesito cambiar un lugar, necesito cambiar el otro. Tenga en cuenta que esto se aplica solo a casos simples como el mío, donde solo habrá una implementación de un encabezado dado, y se usarán solo una vez (en un solo boceto).
heltonbiker

Simplifica el trabajo del compilador / enlazador y le permite tener en los archivos .cpp elementos que no son parte de la clase, pero que se utilizan en algún método. Y en caso de que la clase tenga memers estáticos, no puede colocarlos en el archivo .h.
Igor Stoppa

Separar archivos .h y .cpp ha sido reconocido por mucho tiempo como innecesario. Java, C #, JS ninguno requiere archivos de encabezado, e incluso los estándares iso de cpp están tratando de alejarse de ellos. El problema es que hay demasiado código heredado que podría romper con un cambio tan radical. Esa es la razón por la que tenemos CPP después de C, y no solo una C. expandida. Espero que vuelva a ocurrir lo mismo, ¿CPX después de CPP?

Claro, si la próxima revisión presenta una forma de realizar las mismas tareas que realizan los encabezados, sin encabezados ... pero mientras tanto hay muchas cosas que no se pueden hacer sin encabezados: quiero ver cómo se distribuye la compilación podría suceder sin encabezados y sin incurrir en gastos generales importantes.
Igor Stoppa

6

Estoy publicando una respuesta solo para completar, después de descubrir y probar una forma de declarar e implementar una clase en el mismo archivo .cpp, sin usar un encabezado. Entonces, con respecto a la formulación exacta de mi pregunta "cuántos tipos de archivos necesito para usar las clases", la respuesta actual usa dos archivos: uno .ino con un archivo include, setup y loop, y el .cpp que contiene el conjunto (bastante minimalista ) clase, que representa las señales de giro de un vehículo de juguete.

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
Esto es similar a Java y aplica la implementación de métodos en la declaración de la clase. Además de la legibilidad reducida (el encabezado le da la declaración de los métodos en forma concisa), me pregunto si las declaraciones de clase más inusuales (como estadísticas, amigos, etc.) seguirían funcionando. Pero la mayoría de este ejemplo no es realmente bueno, porque incluye el archivo solo una vez que una inclusión simplemente se concatena. Los problemas reales comienzan cuando incluye el mismo archivo en varios lugares y comienza a obtener declaraciones de objetos en conflicto desde el vinculador.
Igor Stoppa
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.