Classes e objetos: quantos e quais tipos de arquivo eu realmente preciso para usá-los?

20

Não tenho experiência anterior com C ++ ou C, mas sei como programar C # e estou aprendendo Arduino. Eu só quero organizar meus esboços e me sinto bastante à vontade com a linguagem Arduino, mesmo com suas limitações, mas eu realmente gostaria de ter uma abordagem orientada a objetos para a minha programação Arduino.

Então, eu vi que você pode ter as seguintes maneiras (lista não exaustiva) para organizar o código:

  1. Um único arquivo .ino;
  2. Vários arquivos .ino na mesma pasta (o que o IDE chama e exibe como "guias");
  3. Um arquivo .ino com um arquivo .h e .cpp incluído na mesma pasta;
  4. O mesmo que acima, mas os arquivos são uma biblioteca instalada dentro da pasta do programa Arduino.

Também ouvi falar das seguintes maneiras, mas ainda não as consegui trabalhar:

  • Declarar uma classe de estilo C ++ no mesmo arquivo .ino único (já ouviu falar, mas nunca viu funcionar - isso é possível?);
  • [abordagem preferencial] Incluindo um arquivo .cpp em que uma classe é declarada, mas sem o uso de um arquivo .h (gostaria dessa abordagem, isso funcionaria?);

Observe que eu só quero usar classes para que o código seja mais particionado, meus aplicativos devem ser muito simples, envolvendo apenas botões, leds e campainhas.

heltonbiker
fonte
Para os interessados, há uma discussão interessante sobre sem cabeçalho (CPP apenas) definições de classe aqui: programmers.stackexchange.com/a/35391/35959
heltonbiker

Respostas:

31

Como o IDE organiza as coisas

Primeiro, é assim que o IDE organiza seu "esboço":

  • O principal .inoarquivo é a do mesmo nome que a pasta está em Então, por. foobar.inoNa foobarpasta - o arquivo principal é foobar.ino.
  • Quaisquer outros .inoarquivos nessa pasta são concatenados juntos, em ordem alfabética, no final do arquivo principal (independentemente de onde o arquivo principal está, em ordem alfabética).
  • Este arquivo concatenado se torna um .cpparquivo (por exemplo foobar.cpp) . É colocado em uma pasta de compilação temporária.
  • O pré-processador "útil" gera protótipos de funções para as funções encontradas nesse arquivo.
  • O arquivo principal é verificado quanto a #include <libraryname>diretivas. Isso aciona o IDE para copiar também todos os arquivos relevantes de cada biblioteca (mencionada) na pasta temporária e gerar instruções para compilá-los.
  • Nenhum .c, .cppou .asmarquivos na pasta esboço são adicionados ao processo de construção de unidades de compilação separadas (isto é, eles são compilados na forma usual como arquivos separados)
  • Todos os .harquivos também são copiados para a pasta de compilação temporária, para que possam ser referidos pelos arquivos .c ou .cpp.
  • O compilador adiciona aos arquivos padrão do processo de compilação (como main.cpp)
  • O processo de compilação compila todos os arquivos acima em arquivos de objetos.
  • Se a fase de compilação for bem-sucedida, eles serão vinculados juntamente com as bibliotecas padrão do AVR (por exemplo, fornecendo a você strcpyetc.)

Um efeito colateral de tudo isso é que você pode considerar o esboço principal (os arquivos .ino) em C ++ para todos os efeitos. A geração do protótipo da função, no entanto, pode levar a mensagens de erro obscuras, se você não tomar cuidado.


Evitando as peculiaridades do pré-processador

A maneira mais simples de evitar essas idiossincrasias é deixar o esboço principal em branco (e não usar outros .inoarquivos). Em seguida, crie outra guia (um .cpparquivo) e coloque suas coisas assim:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

Observe que você precisa incluir Arduino.h. O IDE faz isso automaticamente para o esboço principal, mas para outras unidades de compilação, você precisa fazer isso. Caso contrário, ele não saberá sobre coisas como String, os registros de hardware, etc.


Evitando o paradigma de configuração / principal

Você não precisa executar o conceito de configuração / loop. Por exemplo, seu arquivo .cpp pode 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

Forçar a inclusão da biblioteca

Se você executar com o conceito "esboço vazio", ainda precisará incluir bibliotecas usadas em outras partes do projeto, por exemplo, no .inoarquivo principal :

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

Isso ocorre porque o IDE verifica apenas o arquivo principal quanto ao uso da biblioteca. Efetivamente, você pode considerar o arquivo principal como um arquivo de "projeto" que indica quais bibliotecas externas estão em uso.


Problemas de nomeação

  • Não nomeie seu esboço principal como "main.cpp" - o IDE inclui seu próprio main.cpp, assim você terá uma duplicata se fizer isso.

  • Não nomeie seu arquivo .cpp com o mesmo nome que o arquivo .ino principal. Como o arquivo .ino se torna efetivamente um arquivo .cpp, isso também gera um conflito de nome.


Declarar uma classe de estilo C ++ no mesmo arquivo .ino único (já ouviu falar, mas nunca viu funcionar - isso é possível?);

Sim, isso compila OK:

class foo {
  public:
};

foo bar;

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

No entanto, é melhor você seguir a prática normal: Coloque suas declarações em .harquivos e suas definições (implementações) em .cpp(ou .c) arquivos.

Por que "provavelmente"?

Como meu exemplo mostra, você pode juntar tudo em um arquivo. Para projetos maiores, é melhor ser mais organizado. Eventualmente, você chega ao estágio em um projeto de médio a grande porte, no qual deseja separar as coisas em "caixas pretas" - ou seja, uma classe que faz uma coisa, faz bem, é testada e é independente ( o mais longe possível).

Se essa classe for usada em vários outros arquivos no seu projeto, é nesse ponto que os arquivos .he os .cpparquivos separados entram em cena.

  • O .harquivo declara a classe - ou seja, fornece detalhes suficientes para outros arquivos saberem o que fazem, quais funções têm e como são chamados.

  • O .cpparquivo define (implementa) a classe - ou seja, na verdade fornece as funções e os membros estáticos da classe que fazem com que a classe faça seu trabalho. Como você deseja implementá-lo apenas uma vez, isso está em um arquivo separado.

  • O .harquivo é o que é incluído em outros arquivos. O .cpparquivo é compilado uma vez pelo IDE para implementar as funções da classe.

Bibliotecas

Se você seguir esse paradigma, estará pronto para mover a classe inteira (os arquivos .he .cpp) para uma biblioteca com muita facilidade. Em seguida, ele pode ser compartilhado entre vários projetos. Tudo o que é necessário é criar uma pasta (por exemplo, myLibrary) e colocar os arquivos .he .cpp(por exemplo, myLibrary.he myLibrary.cpp) e, em seguida, colocar essa pasta dentro da sua librariespasta na pasta em que seus esboços são mantidos (a pasta do caderno de rascunhos).

Reinicie o IDE e agora ele conhece essa biblioteca. Isso é realmente trivialmente simples e agora você pode compartilhar esta biblioteca em vários projetos. Eu faço muito isso.


Um pouco mais detalhadamente aqui .

Nick Gammon
fonte
Boa resposta. Um tópico mais importante, porém, não ficou claro para mim ainda: por que todo mundo diz "você é provavelmente melhor fora de seguir a prática normal: .h + .cpp"? Porque é melhor? Por que a parte provavelmente ? E o mais importante: como não posso fazer isso, ou seja, ter interface e implementação (ou seja, todo o código da classe) no mesmo arquivo .cpp único? Muito obrigado por enquanto! : o)
heltonbiker
Foram adicionados mais alguns parágrafos para responder por que "provavelmente" você deveria ter arquivos separados.
Nick Gammon
11
Como você não faz isso? Apenas junte todos eles como ilustrado na minha resposta, no entanto, você pode achar que o pré-processador funciona contra você. Algumas definições de classe C ++ perfeitamente válidas falham se forem colocadas no arquivo .ino principal.
Nick Gammon
Eles também falharão se você incluir um arquivo .H em dois dos seus arquivos .cpp e esse arquivo .h contiver código, que é um hábito comum de alguns. Seu código aberto, conserte você mesmo. Se você não se sente à vontade para fazer isso, provavelmente não deve usar o código aberto. Linda explicação @Nick Gammon, melhor do que qualquer coisa que eu tenha visto até agora.
@ Spiked3 Não é tanto uma questão de escolher com o que me sinto mais confortável, por enquanto, é uma questão de saber o que está disponível para eu escolher em primeiro lugar. Como eu poderia fazer uma escolha sensata se eu nem sabia quais são minhas opções e por que cada opção é do jeito que é? Como eu disse, não tenho experiência anterior com C ++, e parece que o C ++ no Arduino pode exigir cuidados extras, como mostrado nesta mesma resposta. Mas tenho certeza que, eventualmente, eu estou recebendo um aperto dele e pegar minhas coisas feito sem reinventar a roda (pelo menos eu espero que sim) :)
heltonbiker
6

Meu conselho é seguir a maneira típica de C ++ de fazer as coisas: interface e implementação separadas em arquivos .h e .cpp para cada classe.

Existem algumas capturas:

  • você precisa de pelo menos um arquivo .ino - eu uso um link simbólico para o arquivo .cpp onde instancia as classes.
  • você deve fornecer os retornos de chamada que o ambiente do Arduino espera (setu, loop, etc.)
  • em alguns casos, você ficará surpreso com as coisas estranhas não-padrão que diferenciam o IDE do Arduino de um normal, como a inclusão automática de determinadas bibliotecas, mas não outras.

Ou, você pode abandonar o IDE do Arduino e tentar com o Eclipse . Como mencionei, algumas das coisas que devem ajudar iniciantes tendem a atrapalhar desenvolvedores mais experientes.

Igor Stoppa
fonte
Embora eu ache que separar um esboço em vários arquivos (guias ou inclusões) ajude tudo a ficar em seu próprio lugar, sinto que precisar de dois arquivos para cuidar da mesma coisa (.h e .cpp) é uma espécie de redundância / duplicação desnecessárias. Parece que a turma está sendo definida duas vezes, e toda vez que preciso mudar um lugar, preciso mudar o outro. Observe que isso se aplica apenas a casos simples como o meu, onde haverá apenas uma implementação de um determinado cabeçalho e eles serão usados ​​apenas uma vez (em um único esboço).
Heltonbiker
Ele simplifica o trabalho do compilador / vinculador e permite que você tenha nos arquivos .cpp elementos que não fazem parte da classe, mas são usados ​​em algum método. E caso a classe possua memers estáticos, você não poderá colocá-los no arquivo .h.
Igor Stoppa
A separação dos arquivos. He .cpp é reconhecida há muito tempo como desnecessária. Java, C #, JS nenhum requer arquivos de cabeçalho, e até os padrões iso do cpp estão tentando se afastar deles. O problema é que há muito código legado que poderia romper com uma mudança tão radical. Essa é a razão pela qual temos CPP após C, e não apenas um C. expandido. Espero que o mesmo aconteça novamente, CPX após CPP?
Claro, se a próxima revisão vier com uma maneira de executar as mesmas tarefas que são feitas por cabeçalhos, sem cabeçalhos ... mas, enquanto isso, existem muitas coisas que não podem ser feitas sem cabeçalhos: quero ver como a compilação distribuída pode acontecer sem cabeçalhos e sem incorrer em grandes despesas gerais.
Igor Stoppa 07/07
6

Estou postando uma resposta apenas para completar, depois de descobrir e testar uma maneira de declarar e implementar uma classe no mesmo arquivo .cpp, sem usar um cabeçalho. Portanto, em relação à formulação exata da minha pergunta "quantos tipos de arquivo eu preciso para usar classes", a resposta atual usa dois arquivos: um .ino com um include, setup e loop e o .cpp que contém o todo (bastante minimalista ), representando os sinais de mudança de direção de um veículo de brinquedo.

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
fonte
11
É semelhante a java e coloca a implementação de métodos na declaração da classe. Além da legibilidade reduzida - o cabeçalho fornece a declaração dos métodos de forma concisa - eu me pergunto se declarações de classe mais incomuns (como estática, amigos etc.) ainda funcionariam. Mas a maior parte deste exemplo não é realmente boa, porque inclui o arquivo apenas quando uma inclusão simplesmente concatena. Os problemas reais começam quando você inclui o mesmo arquivo em vários locais e começa a obter declarações de objetos conflitantes do vinculador.
Igor Stoppa 24/03