Einbinden *.h und *cpp

Hallo,

da in Visual Studio mit Platformio der Ansatz mit mehreren *.ino wie in der IDE nicht funktioniert habe ich mir mal einen Versuch zusammengeschrieben um mit Headerdateien zu arbeiten.

Obwohl die Arbeitsumgebung keine Fehler anzeigt kompiliert das Ganze nicht, angeblich ist die Funktion plus() nicht bekannt.

c:/users/xxx/.platformio/packages/toolchain-xtensa-esp32/bin/../lib/gcc/xtensa-esp32-elf/8.4.0/../../../../xtensa-esp32-elf/bin/ld.exe: .pio\build\esp32cam\src\main.cpp.o: in function loop()': C:\Users\xxx\Documents\platformio\esp32_cam/src/main.cpp:11: undefined reference to plus()'
collect2.exe: error: ld returned 1 exit status
*** [.pio\build\esp32cam\firmware.elf] Error 1

In der IDE wird es noch verrückter, der Compiler meldet eine multiple Definition von i in versuch.h, Zeile 4 mit der Ansage, es wurde zum ersten Mal in versuch.h, Zeile 4 :joy: definiert.

Wo ist denn hier mein Fehler?

main.cpp

#include <Arduino.h>
#include "versuch.h"

void setup() {
  Serial.begin(115200);
  Serial.println("setup");
}

void loop() {
  Serial.print("loop ");
  Serial.println(i);                     // Zeile 11
  plus();
  delay(1000);
}

versuch.h

#ifndef VERSUCH_H

#define VERSUCH_H

int i = 0;                     // Zeile vier 

void plus();

#endif

versuch.cpp

#include "versuch.h"

void plus() {
    i++;
    return;
}

Versuchen Sie es so

versuch.h

#ifndef VERSUCH_H
#define VERSUCH_H
extern int i ;  // Zeile vier 
void plus();
#endif

versuch.cpp

#include "versuch.h"
int i = 0;  // Zeile vier 
void plus() {
    i++;
    return;
}

Das funktioniert in der IDE ...
Widerspricht aber allem was ich so gefunden habe?

Das bedeutet: Wenn Sie eine Klasse oder Funktion oder globale Variable definieren, müssen Sie in jeder zusätzlichen CPP-Datei, die sie verwendet, eine Deklaration dieser Datei angeben. Jede Deklaration dieser Sache muss in allen Dateien exakt identisch sein. Eine leichte Inkonsistenz verursacht Fehler oder unbeabsichtigtes Verhalten, wenn der Linker versucht, alle Kompilierungseinheiten in einem einzigen Programm zusammenzuführen.

Um das Fehlerpotenzial zu minimieren, hat C++ die Konvention der Verwendung von Headerdateien für Deklarationen übernommen. Sie erstellen die Deklarationen in einer Headerdatei und verwenden dann die #include-Direktive in jeder CPP-Datei oder einer anderen Headerdatei, die diese Deklaration erfordert. Die #include Direktive fügt vor der Kompilierung eine Kopie der Headerdatei direkt in die CPP-Datei ein.
*.h - Deklaration
*.cpp -Definition

Ob das Problem in Visual Studio so auch auftritt weiß ich nicht, da sich dieses ja an der Funktion stört.

Dies ist wichtig.

In Ihrem Code haben Sie eine Definition der Variablen i in der .h-Datei platziert.

Indem Sie #include "versuch.h" sowohl in der Hauptdatei als auch in der .cpp-Datei verwenden, haben Sie zweimal die Definition der Variablen i.

Deshalb habe ich eine Deklaration der Variablen i als extern in der .h-Datei und nur eine Definition der Variablen i in der .cpp-Datei platziert.

Verstehe ich nicht, jede Library wird doch so eingebunden.

In der main.cpp wird libxy.h eingebunden. Und in der libxy.cpp ebenfalls.

Und es wird ausdrücklich so beschrieben:

Sie erstellen die Deklarationen in einer Headerdatei und verwenden dann die #include-Direktive in jeder CPP-Datei oder einer anderen Headerdatei, die diese Deklaration erfordert. Die #include Direktive fügt vor der Kompilierung eine Kopie der Headerdatei direkt in die CPP-Datei ein.

Weiterhin gibt es unter VS keinen Fehler, es wird anstandslos kompiliert wenn ich die Funktion plus() entferne.

cu

Ja!
Aber die Ersteller der Libs wissen wie Compiler und Linker funktionieren!

Dein i wird 2 mal definiert. Da es in der *.h definiert wird.
Die *.h wird 2 mal eingebunden.
Damit ist die Definition doppelt und wird vom Linker angemeckert.
Mit Recht!

Übrigens du kannst auch in deinem Fall auf die zusätzliche *.cpp verzichten!

versuch.h

#pragma once

inline int i = 0;                 

inline void plus()
{
  i++;
}

So ist es auch richtig!

Du erstellst eine Definition.

Eine Definition ist allerdings was anderes, als nur eine Deklaration

In jede *.cpp weil eben so beschrieben.
Allerdings habe ich definiert, nicht deklariert. :grimacing: , und das ist falsch.

Das entsprechend geänderte Programm (deklariert in versuch.h, definiert in main.cpp) bringt in Platformio keinen Fehler, kompiliert und läuft, im Gegensatz zur IDE, die immer noch doppelt gemoppelt sagt.
Laut Beschreibungen sollten doppelte Einbindung durch die entsprechenden Anweisungen, die ja vorhanden sind abgefangen werden.

Platformio meckert leider immer noch die fehlende Deklaration der Funktion an.

Die Möglichkeit alles in der *.h zu machen kenne ich und verwende sie. Ich wollte aber eben "weiterbilden".

main.cpp

#include <Arduino.h>
#include "versuch.h"

void setup() {
  Serial.begin(115200);
  Serial.println("setup");

  i = 25;
  
}

void loop() {
  Serial.print("loop ");
  Serial.println(i);
  i++;
  //plus();
  delay(1000);
}

versuch.h

#ifndef VERSUCH_H

#define VERSUCH_H

int i;

void plus();

#endif

versuch.cpp

#include "versuch.h"

void plus() {
    i++;
    return;
}

cu

  1. der Include Guard ist alt
  2. int i; ist weiterhin eine Definition und damit falsch.

Und wie de?????? man dann, so dass i in beiden cpp bekannt ist?

Das Problem mit der Funktion lag sicher am falschen positionieren der versuch.cpp, jedenfalls kommt der Fehler jetzt nicht mehr. Dadurch wurde sie anscheinend gar nicht kompiliert, so dass
das i-Problem hier nicht aufgeschlagen ist.

Allerdings,

ist die versuch.h in main.cpp und versuch.cpp eingebunden meldet der Compiler die doppelte Definition,
fehlt die versuch.h in versuch.cpp bemängelt er die fehlende Deklaration.

An sich für mich ein Widerspruch, da die erste Einbindung somit zwei unterschiedliche Ergebnisse hätte.

cu

In #2 und #6 stehen doch mögliche Lösungen für das Problem.

Allerdings halte ich den schreibenden Zugriff auf eine Variable aus zwei Dateien für einen Designfehler.
Es handelt sich dann de facto um eine globale Variable - da nutzt die scheinbare Kapselung in versuch nix.
Wenn es eine Klasse wäre, gäbe es vermutlich get- und set-Methoden.

Unterscheide zwischen Präprozessor, Compiler und Linker!
Denke in Übersetzungseinheiten.
Dann lösen sich alle Widersprüche.

Dann bilde dich weiter in Richtung template - dann bist du schnell wieder bei .h only...

https://en.cppreference.com/w/cpp/language/templates

*.h only geht auch mit Variablen und Funktionen.
Siehe Posting #6

Aber hast schon recht!
Mit Classen und Templates wirds noch interessanter/spannender.

extern int i; // in der .h

int i; in einer der .cpp oder .ino

Das ist aber leider ganz altmodisch. Und natürlich wird man eine globale Variable nie i nennen.

Naja, das ist ein "Probierstück" :slightly_smiling_face: zum verstehen.

In den Rest werde ich mich mal einlesen, der Link von @michael_x hat schon etwas erhellt, warum der Fehler kommt.
Wobei es in einem echten Programm sicher keine derartige Konstellation der Variablen gibt.

cu

Wenn Sie #include <xxx.h> verwenden, kopiert der Präprozessor den Text des Header-Datei xxx.h direkt in die andere Datei, bevor er mit dem Kompilieren beginnt.

Also in Ihrem ursprünglichen Fall forderten Sie den Compiler auf, zu kompilieren

main.cpp

#ifndef VERSUCH_H
#define VERSUCH_H
int i = 0;                     // Zeile vier 
void plus();
#endif

void setup() {
  Serial.begin(115200);
  Serial.println("setup");
}

void loop() {
  Serial.print("loop ");
  Serial.println(i);                     // Zeile 11
  plus();
  delay(1000);
}

und zu kompilieren

versuch.cpp

#ifndef VERSUCH_H
#define VERSUCH_H
int i = 0;                     // Zeile vier 
void plus();
#endif


void plus() {
    i++;
    return;
}

Sie haben zwei Dateien zu kompilieren und beide Dateien definieren eine globale Variable i. Der Compiler wird jede der beiden Dateien separat kompilieren und beim Linken feststellen, dass es einen Konflikt mit der Variable i gibt, die zweimal deklariert ist. Daher kann er keine Binärdatei generieren.

Wenn Sie jetzt in der .h-Datei die Deklaration von i als externe Variable setzen, kann der Compiler immer noch main.cpp kompilieren, da er den Typ von i kennt und dem Entwickler vertraut, dass diese Variable anderswo definiert wird und dass dies beim Linken gelöst wird. Er kompiliert dann versuch.cpp, das die Definition von i korrekt macht, und daher ist die Verknüpfungsphase möglich und alles funktioniert gut.

Deshalb sollten Variablen normalerweise in einer .cpp-Datei und nicht in einer .h-Datei definiert werden.

Die Zeiten haben sich geändert!
Es sind (in diesem Jahrhundert) mindestens weitere 2 Möglichkeiten hinzugekommen, welche beide sehr nützlich sind und da nimmt man doch einfach die am besten passende.
Das ist die "neue Normalität".

Dazu:
Warum sollte man den alten Include Guard verwenden, wenn es auch einen modernen gibt?

Weil ich aus dem letzten Jahrtausend stamme :slight_smile: (Und weil der gesamte Arduino-Code und die klassischen Bibliotheken diese Methode verwenden, also bleibt es irgendwie konsistent - aber ja, es gibt andere Möglichkeiten, damit umzugehen)

Die Ursache ist mir schon klar, ich war aber eben der Meinung int i; sei eine Deklaration.
Und da -> siehe oben.

Das Ganze ist ein Versuch in Platformio eine Struktur zu erhalten, die nicht unendlich viele Befehle aneinander reiht. In der IDE kann man einfach mehrere *.ino anlegen und so logische Bestandsteile trennen.
Vielleicht stellt sich das Problem zukünftig gar nicht, da es ja nur bei globalen Variablen auftaucht wie halt dummerweise in meinem Versuch.
Und wenn kenne ich jetzt ja die Lösung.

cu