Wie Objekt in anderer Klasse instanziieren ?

Hallo,

eigentlich habe ich 3 Klassen. Eine "Schalter", eine "Led" und dann eine "Steuerung" die alles miteinander spielen lässt.

Folgenden Code habe ich auf 2 Klassen runtergebrochen um das Problem zu zeigen.
Die Klasse Schalter hat alles was sie selbst benötigt.
Man muss nur noch ein Objekt mit dem Pin Parameter erstellen.

Das wollte ich nun in der Klasse Steuerung erledigen. Diese soll auch öffentlich Instanziiert werden und gibt dann intern ihre Member weiter an die anderen beiden Objekte. So der Plan. Allerdings kann ich keine Instanz der Objekte Schalter und Led erstellen. Die Instanz irgendwie schon. Das klappt an verschiedenen Stellen. Aber ich bekomme entweder keinen Zugriff auf deren Methoden oder ich kann die Instanz nicht erstellen weil zum Bsp. pin_Taster angeblich kein Typ ist.

Nur pin_Taster ist ein Member der eigenen Klasse Steuerung. Der muss doch an allen Stellen in seiner Klasse verfügbar sein.

Ich stehe momentan im Wald.

Klassenvererbung passt auch nicht Schema, die Konstruktoren würden nicht passen.
Die Frage lautet. Wie erstellt man eine Objektinstanz in einer anderen Klasse und kann dann auf die Methoden des Objekts zugreifen?

class Schalter
{
  private:
    const byte pin;
    const byte DEBOUNCE;                 
    unsigned long last_ms;
    bool schalterstellung;
    bool old_state;
    bool state;
    
  public:
    // Konstruktor
    Schalter(byte p) : 
      // Initialisierungsliste
      pin(p),
      DEBOUNCE(30),                     
      last_ms(0),
      schalterstellung(LOW),
      old_state(HIGH),
      state(HIGH)
    {}

    // Methoden
    void begin ()
    {
      pinMode(pin, INPUT_PULLUP);
    }
    
    bool update_Schalter()
    {
      if (millis() - last_ms > DEBOUNCE) {
        last_ms = millis();
        state = digitalRead(pin);
    
        if (state == LOW && old_state == HIGH) {
            schalterstellung = !schalterstellung;
            Serial.print("schalter: ");
            Serial.println(schalterstellung);
        }
        old_state = state; 
      }
      return schalterstellung;
    }
};


class Steuerung
{
  private:
  const byte pin_Taster;
      
  public:
  // Konstruktor   
  Steuerung (byte pT) :         
    // Initialisierungsliste
    pin_Taster(pT)
  { }    

  Schalter schalter(pin_Taster);    // error: 'pin_Taster' is not a type
  
  // Methoden
  void init ()
  {
    //Schalter schalter(pin_Taster);    // wird ohne Fehler kompiliert
    schalter.begin();                   // Zugriff wird nicht angemeckert ???
  }

  bool blinke ()
  {
    bool var = schalter.update_Schalter();    // Zugriffstest > hat kein Zugriff
    return var;
  }
    
};


void setup(void) {
  
}

void loop(void) {

}

Du legst das Objekt Schalter völlig frei in Steuerung an. Dass muss im Konstruktor in abhängigkeit vom Pin angelegt werden und in einer privaten Membervariable verewigt werden.

Die Zeile

  Schalter schalter(pin_Taster);    // error: 'pin_Taster' is not a type

hat keinerlei Bezug zur umgebenden Klasse.

Gruß Tommy

Ein Objekt ist letztlich auch nichts anderes als eine Variable. Also Variable von der Klasse anlegen und in der Initialisierungsliste initialisieren

Hallo,

ich dachte ich muss immer den kompletten Instanzsyntax schreiben und wusste nicht so recht wohin damit, ich hatte zwischendurch schon die wildesten Ideen ... :slight_smile:

so korrekt?

class Steuerung
{
  private:
  Schalter schalter;   // Objekt anlegen
        
  public:
  // Konstruktor   
  Steuerung (byte pT) :         
    // Initialisierungsliste
    schalter(pT)         // Objekt initialisieren
  {}    

  
  
  // Methoden
  void init ()
  {
    schalter.begin();                 
  }

  bool blinke ()
  {
    bool var = schalter.update_Schalter();  
    return var;
  }
    
};

Sinn der Übung ist keine leeren Defaultkonstruktoren zu haben um dann massig Parameter übergeben zu müssen. Die Parameter sollen in ihrer Klasse bleiben. Das sollte nun so hinhauen.
Noch eine Frage die mir gerade einfällt.

public:
  // Konstruktor   
  Steuerung (byte pT) :         
    // Initialisierungsliste
    schalter(pT)
  {}                  // <<---- Wofür ist das?

Ich sehe immer nur diese leere Blockklammer in Bsp. Wofür ist die? Ist auch auch nutzbar? Oder gehört das leer zum Syntax des Konstruktors und muss leer bleiben?

Ist auch auch nutzbar?

Sicher. Wird nur relativ früh ausgeführt. Globale Objekte werden instantiiert lange bevor setup() ausgeführt wird.
Serial.print geht da z.B. noch nicht.

Hallo,

habs gerade probiert. Ich kann in diese Klammer das pinMode direkt reinschreiben.

public:
    // Konstruktor
    Schalter(byte p) : 
      // Initialisierungsliste
      pin(p),
      DEBOUNCE(30),                     // Taster Entprellzeit
      last_ms(0),
      schalterstellung(LOW),
      old_state(HIGH),
      state(HIGH)
    {
      pinMode(pin, INPUT_PULLUP);
    }

Damit entfallen die begin/init Methoden. Ich hoffe das ist kein Zufall. Oder sollte man das so nicht machen?

Hallo,

die letzte Frage ist scheinbar zu einfach. :wink:

Ich danke Euch für die Hilfe.

ich sehe es so:

im Arduino-IDE würdest einen pinmode auch erst im setup() machen.
Den constructor rufst aber schon vor dem Setup auf.
Also gebe ich da nichts rein, was sonst auch erst im Setup käme.

gibt auch einige Beiträge dazu ... pinMode() in class constructor? seem not to work - Programming Questions - Arduino Forum
calling pinMode in the constructor - Programming Questions - Arduino Forum

alles mit Vorbehalt, eine Spezialistenantwort würde mich auch interessieren :wink:

Der Konstruktor ist nur eine spezielle Methode. Deshalb der Rumpf

In andere Sprachen wird auch die gesamte Initialisierung darin gemacht. C++ hat aber die Eigenheit dass das alles schon vorher initialisiert wird. Deshalb macht man das besser mit der Liste außerhalb. So wird es nur einmal gemacht und man kann auch Konstanten, Referenzen oder Objekte initialisieren

Hallo,

für mich leider noch etwas unverständlich formuliert. Meinst mit Liste den leeren geschweiften Klammernblock?

Selbst wenn das dort zu zeitig initialisiert wird, pinMode setzt maximal 2 Portregister, da sollte nichts schief gehen.

Die geschweiften Klammern sind der Methodenrumpf oder Methodenkörper. Das ist ein ganz normaler Block wie bei jeder anderen Funktion/Methode auch

Hallo,

gut, dann reden wir doch nicht von verschiedenen Dingen. Deswegen muss ich nochmal nachfragen, denn die Frage ob man im Methodenrumpf pinMode ausführen darf wurde noch nicht beantwortet.

In andere Sprachen wird auch die gesamte Initialisierung darin gemacht.
Im Methodenrumpf, okay.

C++ hat aber die Eigenheit dass das alles schon vorher initialisiert wird.
Alles im Methodenrumpf wird vor der Initialisierungsliste abgearbeitet. So verstehe ich das.

Deshalb macht man das besser mit der Liste außerhalb. So wird es nur einmal gemacht und man kann auch Konstanten,
Referenzen oder Objekte initialisieren.
Hier entstehen Fragezeichen bezogen auf die pinMode Frage. pinMode kann man nicht in der Initialisierungsliste ausführen. Funktioniert nur im Methodenrumpf. Wir wissen jetzt das pinMode vor der Initialisierungsliste ausgeführt wird. Praktisch funktioniert das seltsamerweise. Nur woher kennt pinMode dann die pin Nummer, wenn die Member noch nicht initialisiert sind? Und wie hälst du es? Lässt du den Methodenrumpf generell leer oder wofür nutzt du ihn dann? Ist alles ohne Gewähr, ist mir schon klar. :wink:

Der Methodenrumpf wird nach der Initialisierungsliste ausgeführt. Deshalb kennt er den Pin.

Gruß Tommy

Deswegen muss ich nochmal nachfragen, denn die Frage ob man im Methodenrumpf pinMode ausführen darf wurde noch nicht beantwortet.

Im Methodenrumpf des Konstruktors darf man KEIN pinMode() durchführen.

Die Begründung ist ganz schlicht.....

Zum Zeitpunkt des Konstruktoraufrufs ist nicht automatisch gesichert, dass die Peripherie schon passend vorbereitet wurde.
Beim (Arduino typischen) AVR wird es bei Pins immer funktionieren.
Bei Timern nicht, da sie in der main() verändert werden.

Allerdings sieht es auf den ARM, XMega und ESP da schon ganz anders aus.
Da kann man sich auf nix verlassen, bis die main() dran kommt.

Darum:
Prinzipiell verboten!
Kann im Einzelfall funktionieren.

Was dann insgesamt gesehen auch der Grund für die vielen XXX.begin() Methoden in der Arduino Welt ist.

Doc_Arduino:
Alles im Methodenrumpf wird vor der Initialisierungsliste abgearbeitet. So verstehe ich das.

Nein. Der ganze Sinn der Initialisierungsliste ist dass die vorher ausgeführt wird! Sonst wäre es überflüssig und du könntest das wie in anderen Sprachen auch im Rumpf machen.

In C++ werden Variablen ganz am Anfang beim Erstellen des Objekts initialisiert. Wenn der Rumpf ausgeführt wird ist es zu spät. Du kannst dann noch Zuweisungen erledigen (die die Default-Initialisierung überschreiben), aber da man keine Referenzen oder Konstanten initialisieren da das schon gemacht wurde. Deshalb die Liste damit Dinge schon beim ersten mal die richtigen Werte habe

Hallo,

schönes Verwirrspiel hier. :slight_smile: Also folgt das alles dann doch der Logik wie es geschrieben steht im Konstruktor. Erst Initialisierungsliste und dann alles im Methodenrumpf. Soweit ist das logisch nachvollziehbar. Gut das wir nochmals darüber geredet haben. Ich war zwischendurch schon vom Glauben abgefallen. :wink:

Was ich noch nicht verstehe ist warum die Hardware des µC zu dem Zeitpunkt noch nicht im definierten Zustand sein soll? Nach einem Reset ist diese doch sofort im Defaultzustand. Ports sind Eingänge und Timerregister zum Bsp. usw. alle genullt. Beim nackten AVR (ohne Arduino Framework) würde jetzt main abgespult, alle Objekte erstellt mit allen drum und dran und die Ports entsprechend eingestellt im Kontruktormethodenrumpf. Tja und dann gehts in while(1) los. Soweit meine Logik.

Mit Arduino Framework wäre nun die Frage wann dieses aufgerufen wird. Serial.print funktioniert im Methodenrumpf nicht zu 100%. Habs gestestet. Es kommt ein Mischung aus richtigen, verstümmelten und fehlenden Zeichen. Kurzum ihr lasst den Methodenrumpf immer leer?

class Schalter
{
  private:
    const byte pin;
    const byte DEBOUNCE;                 
    unsigned long last_ms;
    bool schalterstellung;
    bool old_state;
    bool state;
    
  public:
    // Konstruktor
    Schalter(byte p) : 
      // Initialisierungsliste
      pin(p),
      DEBOUNCE(30),                     
      last_ms(0),
      schalterstellung(LOW),
      old_state(HIGH),
      state(HIGH)
    {
      Serial.begin(9600);
      Serial.println("befinde mich im Konstruktor Schalter");
      pinMode(pin, INPUT_PULLUP);
    }
...
...

Was ich noch nicht verstehe ist warum die Hardware des µC zu dem Zeitpunkt noch nicht im definierten Zustand sein soll?

z.B. müssen die IOs des ARM mit Takt versorgt werden, damit diese überhaupt deinen Kommandos gehorchen.

Um diesen Takt zu erzeugen, muss Code ausgeführt werden
Der Stack und Heap muss initialisiert werden
Um Variablen außerhalb von main() zu initialisieren muss Code ausgeführt werden.
Ebenso für statische Variablen in Klassen und Funktionen
Auch die Konstruktoren von deinen Objekten außerhalb von main() wollen ausgeführt werden.
usw...

Es kommt also eine ganze Latte von Code zur Ausführung, bevor main() an die Reihe kommt.
Diese Fragmente kann der Linker zusammenkleben, wie er will.
Eine garantierte Ausführungsreihenfolge gibt es nicht, abgesehen von der Benennung der InitSections.
Aber auf diese Verteilung haben wir (erst mal) keinen Einfluss.

Kurzum ihr lasst den Methodenrumpf immer leer?

Nein!
Der Konstruktor ist dafür zuständig, das Objekt zu initialisieren!

Wenn dafür Code im Body nötig ist, dann ist das so.
Und wenn nicht, lässt man ihn leer.

Da kann man kein "immer" unterbringen.

Serial.print funktioniert im Methodenrumpf nicht zu 100%. Habs gestestet.

In einem Konstruktoraufruf außerhalb der Main, ist nicht gewährleistet, dass der Serial Konstruktor schon durchgelaufen ist.
Und Serial.begin() war ganz sicher noch nicht an der Reihe.
Daher die Fehlfunktion.
.

Hallo,

ich nehme es so zur Kenntnis. Ich danke dir/euch.

Das ist ja auch der Grund, weshalb fast alle Libs eine 'init' oder 'begin' Methode haben.
Alternative wäre, dein Objekt erst in setup() mit 'new' zu instanziieren. Da bist Du sicher, dass alle globalen Objekte existieren, und kannst sie im Konstruktor auch nutzen. Wenn das Objekt nie gelöscht wird, gibt's auch keine Probleme mit Segmentierung im Speicher.

Hallo,

ja, dass erklärt dann abschließend alles und passt ins Schema.
Man lernt immer etwas dazu. :slight_smile: