类和对象:使用它们实际上需要多少个文件类型?


20

我以前没有使用C ++或C的经验,但是知道如何编写C#并正在学习Arduino。我只是想整理我的草图,即使有其局限性,我也对Arduino语言非常满意,但是我真的很想在Arduino编程中采用一种面向对象的方法。

因此,我看到您可以通过以下方式(并非详尽的列表)来组织代码:

  1. 单个.ino文件;
  2. 同一文件夹中的多个.ino文件(IDE调用并显示类似“选项卡”的内容);
  3. .ino文件,在同一文件夹中包含.h和.cpp文件;
  4. 与上面相同,但是文件是Arduino程序文件夹中的已安装库。

我也听说过以下方法,但尚未使它们起作用:

  • 在相同的单个.ino文件中声明C ++风格的类(听说过,但从未见过工作-可能吗?);
  • [首选方法]包括一个在其中声明一个类的.cpp文件,但使用.h文件(这种方法会行得通吗?);

请注意,我只想使用类以使代码更加分区,我的应用程序应该非常简单,仅涉及按钮,LED和蜂鸣器。


对于那些感兴趣的人,这里有一个关于无头(仅cpp)类定义的有趣讨论:programmers.stackexchange.com/a/35391/35959
heltonbiker

Answers:


31

IDE如何组织事物

首先,这是IDE如何组织您的“草图”:

  • .ino文件与其所在文件夹的名称相同。因此,对于foobar.inoin foobar文件夹-主文件为foobar.ino。
  • .ino该文件夹中的所有其他文件在主文件的末尾按字母顺序串联在一起(无论主文件在何处按字母顺序)。
  • 此级联文件成为.cpp文件(例如foobar.cpp)-放置在临时编译文件夹中。
  • 预处理器“有帮助地”为其在该文件中找到的函数生成函数原型。
  • 扫描主文件中的#include <libraryname>指令。这会触发IDE还将所有(提到的)库中的所有相关文件复制到临时文件夹中,并生成编译它们的指令。
  • 任何.c.cpp.asm在草图夹中的文件被添加到构建过程作为单独的编译单元(即,它们以常规方式作为单独的文件编译)
  • 所有.h文件也都被复制到临时编译文件夹中,因此您的.c或.cpp文件可以引用它们。
  • 编译器将标准文件添加到构建过程中(如main.cpp
  • 然后,构建过程将上述所有文件编译为目标文件。
  • 如果编译阶段成功,它们将与AVR标准库链接在一起(例如,提供给您strcpy等)。

所有这些的副作用是,可以将主草图(.ino文件)视为所有目的和用途的C ++。但是,如果不小心,函数原型的生成会导致模糊的错误消息。


避免预处理程序怪癖

避免这些特性的最简单方法是将主草图保留为空白(而不使用任何其他.ino文件)。然后制作另一个标签(.cpp文件),然后将您的内容放入其中:

#include <Arduino.h>

// put your sketch here ...

void setup ()
  {

  }  // end of setup

void loop ()
  {

  }  // end of loop

请注意,您需要添加Arduino.h。IDE将为主草图自动执行此操作,但对于其他编译单元,则必须执行此操作。否则,它将不了解诸如String,硬件寄存器等之类的信息。


避免设置/主范式

您不必运行设置/循环概念。例如,您的.cpp文件可以是:

#include <Arduino.h>

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

强制库包含

如果您使用“空草图”概念运行,则仍需要包括项目其他地方使用的库,例如在主.ino文件中:

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

这是因为IDE仅扫描主文件以查找库使用情况。实际上,您可以将主文件视为“项目”文件,该文件指定使用了哪些外部库。


命名问题

  • 不要将主草图命名为“ main.cpp”-IDE包含自己的main.cpp,因此如果这样做,您将拥有一个副本。

  • 不要使用与主.ino文件相同的名称来命名.cpp文件。由于.ino文件实际上变成了.cpp文件,因此这也会给您带来名称冲突。


在相同的单个.ino文件中声明C ++风格的类(听说过,但从未见过工作-可能吗?);

是的,这样编译就可以了:

class foo {
  public:
};

foo bar;

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

但是,最好遵循常规做法:将声明放入.h文件中,并将定义(实现)放入.cpp(或.c)文件中。

为什么“可能”?

如我的示例所示,您可以将所有内容放到一个文件中。对于较大的项目,最好有条理。最终,您进入了一个大中型项目的阶段,在这里您想将事物分成“黑匣子”-也就是说,一类要做一件事,做得很好,经过测试并且是独立的(越远越好)。

如果该类随后在项目中的多个其他文件中使用,则这是单独文件.h.cpp文件起作用的地方。

  • .h文件声明了该类-也就是说,它为其他文件提供了足够的详细信息,以使其了解其功能,具有的功能以及如何调用它们。

  • .cpp文件定义(实现)了类-也就是说,它实际上提供了使类执行其操作的功能和静态类成员。由于您只想实现一次,因此在单独的文件中。

  • .h文件是包含在其他文件中的文件。该.cpp文件由IDE编译一次,以实现类的功能。

图书馆

如果遵循此范例,则可以轻松地将整个类(.h.cpp文件)移至库中。然后可以在多个项目之间共享。所需要做的只是制作一个文件夹(例如myLibrary),然后将.h.cpp文件放入其中(例如myLibrary.hmyLibrary.cpp),然后将该文件夹放入libraries保存草图的文件夹中的文件夹内(Sketchbook文件夹)。

重新启动IDE,它现在知道该库。这确实非常简单,现在您可以在多个项目中共享该库。我经常这样做。


这里有更多细节。


好答案。其中最重要的话题,不过,并没有对我变得清晰又:为什么大家都说“你可能按照正常的做法最好关闭:.H +的.cpp”?为什么会更好?为什么可能是一部分?最重要的是:我怎么做呢,也就是说,接口实现(都是整个类代码)都在同一个单个.cpp文件中?非常感谢您!:o)
heltonbiker

添加了另外几段来回答为什么“可能”您应该有单独的文件。
尼克·加蒙

1
你怎么去做呢?就像我的答案所示,将它们放在一起,但是您可能会发现预处理器对您不利。如果将某些完全有效的C ++类定义放在主.ino文件中,则它们将失败。
尼克·加蒙

如果您在两个.cpp文件中包含.H文件,并且该.h文件包含代码,则这也会失败,这是某些人的常见习惯。它是开源的,只需自己修复即可。如果您不愿意这样做,则可能不应该使用开源。@Nick Gammon的美丽解释,比我到目前为止所见都更好。

@ Spiked3与其说是选择我最喜欢的东西,不如说是现在,首先要了解我可以选择的东西。如果我什至不知道我的选择是什么,我该如何做出明智的选择?为什么每个选择都是这样?就像我说的那样,我以前没有使用C ++的经验,并且看来Arduino中的C ++可能需要格外小心,如本答案所示。但我敢肯定,最终我会掌握它并完成我的工作,而无需重新发明轮子(至少我希望如此):)
heltonbiker

6

我的建议是坚持使用典型的C ++处理方式:将每个类的接口和实现分为.h和.cpp文件。

有一些收获:

  • 您至少需要一个.ino文件-我使用符号链接到实例化类的.cpp文件。
  • 您必须提供Arduino环境期望的回调(setu,循环等)。
  • 在某些情况下,您会惊讶于将Arduino IDE与普通IDE区别开的非标准怪异事物,例如自动包含某些库,而没有自动包含其他库。

或者,您可以放弃Arduino IDE并尝试使用Eclipse。正如我所提到的,某些对初学者有用的东西往往会以经验丰富的开发人员的方式出现。


虽然我认为将草图分为多个文件(选项卡或包含文件)有助于将所有内容放置在自己的位置,但我觉得需要两个文件来处理同一件事(.h和.cpp)是一种不必要的冗余/重复。感觉该类被定义了两次,每次我需要更改一个位置时,我都需要更改另一个位置。请注意,这仅适用于像我这样的简单情况,其中给定标头只有一个实现,并且将只使用一次(在一个草图中)。
heltonbiker

它简化了编译器/链接器的工作,并允许您在.cpp文件中包含元素,这些元素不是类的一部分,而是在某些方法中使用的。并且如果该类具有静态memer,则不能将其放在.h文件中。
伊戈尔·斯托帕

长期以来,人们一直认为将.h和.cpp文件分开是不需要的。Java,C#,JS都不需要头文件,甚至cpp iso标准都在试图摆脱它们。问题在于,太多的旧代码可能会因如此重大的改变而中断。这就是我们在C之后拥有CPP的原因,而不仅仅是扩展C。我希望CPP之后的CPX会再次发生同样的情况?

当然,如果下一个修订版本提出了一种执行标头(没有标头)完成的相同任务的方法,但是与此同时,有很多事情没有标头就无法完成:我想看看分布式编译如何可能没有标题也不会发生重大开销。
伊戈尔·斯托帕

6

找到并测试了在同一.cpp文件中声明实现类的方法,而无需使用标头,我只是为了完整性而发布一个答案。因此,关于我的问题“我需要使用多少个文​​件类型,”这个问题的确切措词,当前答案使用两个文件:一个带有包含,设置和循环的.ino文件,以及包含整个文件的.cpp文件(相当简单) )类别,代表玩具车的转向信号。

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
这类似于Java,将方法的实现扩展到类的声明中。除了降低可读性-标头为您提供了简洁形式的方法声明-我想知道是否还有更多不寻常的类声明(例如使用statics,friends等)。但大多数这个例子是不是真的好,因为它包含文件只一次的包含简单的符连接。当您在多个位置包含相同文件并且开始从链接器获取冲突的对象声明时,真正的问题就开始了。
伊戈尔·斯托帕
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.