Go Down

Topic: Objekte in Methoden eigener Klassen erstellen? (Read 389 times) previous topic - next topic

Tommylicious1980

Guten Morgen,
ich frische grade meine C++ Kenntnisse auf und bin auf etwas gestoßen, was ich mir nicht so recht erklären kann. Ich habe eine eigene Klasse erstellt und dann diverse Methoden dazu. Bei der Erstellung eines Objekts ist mir dann aufgefallen, dass dieses Objekt nicht in anderen Methoden verfügbar ist. Demnach stellt sich die Frage, wie ich innerhalb einer Klasse dafür sorgen kann, dass die Objekte, Variablen usw. auch für andere Methoden der selben Klasse verfügbar sind.

Zur Veranschaulichung sagt etwas Code mehr als viele Worte  :smiley-lol:

test.h:

Code: [Select]

#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>

class Test {
public:
// Konstruktor mit Defaultargumenten
Test (bool useOneWire = true);

// Methoden werden Deklariert
            void setupFeatureSerial();
void setupFeatureSerial(int BAUD_RATE);
   
               void setupFeatureOneWire();
               void setupFeatureOneWire(uint8_t OneWire_PIN, uint8_t OneWire_Resolution);
               uint8_t OneWire_getNumberOfDevices();
   
   

private:
              int     BAUD_RATE;
              uint8_t OneWire_PIN;
              uint8_t OneWire_Resolution;
              bool    useOneWire;
};

#endif


test.cpp:
Code: [Select]

#include "test.h"
#include <OneWire.h>
#include <DallasTemperature.h>

// Konstruktor für die Klasse Test (Achtung: Kein Rückgabewert, auch nicht void!!)
Test::Test(bool useOneWire)
{
  this->useOneWire  = useOneWire;
}

// Methoden (Funktionen)
void Test::setupFeatureSerial()
{
  BAUD_RATE = 115200;
  Serial.begin(BAUD_RATE);
  int br = Serial.baudRate();
  Serial.printf("INFO:  Serielle Übertragungsrate ist %d bps", br);
}

void Test::setupFeatureSerial(int BAUD_RATE)
{
  this->BAUD_RATE = BAUD_RATE;
  Serial.begin(BAUD_RATE);
  int br = Serial.baudRate();
  Serial.printf("INFO:  Serielle Übertragungsrate ist %d bps", br);
}

void Test::setupFeatureOneWire()
{
  OneWire_PIN = 16;
  OneWire_Resolution = 10;
  OneWire oneWire(OneWire_PIN);
  DallasTemperature DS18B20(&oneWire);
  DS18B20.begin();
  DS18B20.setResolution(OneWire_Resolution);
}

void Test::setupFeatureOneWire(uint8_t OneWire_PIN, uint8_t OneWire_Resolution)
{
  this->OneWire_PIN = OneWire_PIN;
  this->OneWire_Resolution = OneWire_Resolution;
  OneWire oneWire(OneWire_PIN);
  DallasTemperature DS18B20(&oneWire);
  DS18B20.begin();
  DS18B20.setResolution(OneWire_Resolution); 
}

uint8_t Test::OneWire_getNumberOfDevices()
{
    uint8_t numberOfDevices = DS18B20.getDeviceCount();
    return numberOfDevices;
}


Offensichtlich kann ich wenn ich die test.h in meine test.ino einbinde in "void setup()" problemlos das Serielle Interface aktivieren.
Auch der Aufruf der setupFeatureOneWire geht, wenn ich aber die Methode OneWire_getNumberOfDevices in die .h/.cpp Datei reinschreibe wirft die IDE einen Fehler, weil "DS18B20" in der Methode nicht deklariert wurde.
Das ist auch einleuchtend was mich zunächst auf die Frage der "Lebensdauer" von Objekten und Variablen gebracht hat. Meine Idee ist es jetzt, das erstellte Objekt an einen in der test.h erstellten Prototyp zu übergeben, wobei ich unsicher bin ob es hilfreich wäre mit Zeigern zu Arbeiten. Möglicherweise möchte ich ja mal einen zweiten OneWire-Bus an einem anderen Pin ansprechen oder eine ScanRoutine implementieren um alle möglichen Pins nach einem vorhandenen Bus zu scannen... da muss ich demnach recht flexibel bleiben :D

Also wenn mir da jemand nen Denkschubser geben kann bin ich maximal dankbar!
LG Thomas


combie



Code: [Select]
void Test::setupFeatureOneWire(uint8_t OneWire_PIN, uint8_t OneWire_Resolution)
{
  this->OneWire_PIN = OneWire_PIN;
  this->OneWire_Resolution = OneWire_Resolution;
  OneWire oneWire(OneWire_PIN);
  DallasTemperature DS18B20(&oneWire);
  DS18B20.begin();
  DS18B20.setResolution(OneWire_Resolution); 
}

Hier erzeugst du lokale Instanzen.
Diese sind nur innerhalb der Funktion/Methode gültig.
Verfallen beim verlassen.
"Geltungsbereich" nennt sich das.


---

Alles in allem scheint mir, als wolltest du ein "big hairy object" bauen.
Ich rate ab.
> Das größte Problem, ist die Wahl der richtigen Gedanken <
Frei nach Dale Carnegie

Reiter

Du erstellst in deiner Klasse eine Variable vom Typ DS18B20 und sprichst diese in den Membermethoden mit dem vergebenen Variablennamen an.

Tommylicious1980

Hier erzeugst du lokale Instanzen.
Diese sind nur innerhalb der Funktion/Methode gültig.
Verfallen beim verlassen.
"Geltungsbereich" nennt sich das.
--> Super Hinweis, ich werde mich direkt damit befassen


Alles in allem scheint mir, als wolltest du ein "big hairy object" bauen.
Ich rate ab.
:D Um Himmels Willen (Achtung schlechtes Wortspiel :D ) das habe ich nicht vor, aber ich wollte einfach mal einiges ausprobieren und dabei verwende ich einfach das was ich sowieso schon habe und nutze. Mir ist schon klar, dass das keine gute Idee für ein echtes Programm ist.

Tommylicious1980

Du erstellst in deiner Klasse eine Variable vom Typ DS18B20 und sprichst diese in den Membermethoden mit dem vergebenen Variablennamen an.
Dazu müsste ich dann aber erstmal wissen wie man das Objekt DS18B20 in eine Variable packe.. bisher habe ich noch nie mit Objekten auf diese Art arbeiten müssen und daher fehlt mir hier noch die Idee wie so etwas technisch umgesetzt wird.

combie

#5
Jul 10, 2019, 02:36 pm Last Edit: Jul 10, 2019, 02:46 pm by combie
Quote
Dazu müsste ich dann aber erstmal wissen wie man das Objekt DS18B20 in eine Variable packe.. bisher habe ich noch nie mit Objekten auf diese Art arbeiten müssen und daher fehlt mir hier noch die Idee wie so etwas technisch umgesetzt wird.
Dann erstmal die Theorie!

Denn Thermometer ist eine "Abhängigkeit"
Es hat sich als vorteilhaft erwiesen diesen "Abhängigkeiten" besondere Aufmerksamkeit zu widmen.

Lesenswert: "OOP Dependency Injection Pattern"

Im Falle C++...
constructor injection (bei der Instantiierung übergeben)
parameter injection  (als Funktions/Methodenparameter übergeben)
setter injection  (spezielle Settermethode anbieten)
template injection (Funktion/Struktur/Klasse mit Templateparameter spezialisieren)

Das sind die 4 Techniken, welche eigentlich allesamt günstiger sind, als eine fest eingebaute Abhängigkeit zu haben.

> Das größte Problem, ist die Wahl der richtigen Gedanken <
Frei nach Dale Carnegie

Tommylicious1980

Okay... ich habe mal das ganze vereinfacht und habe jetzt folgendes vor:

Code: [Select]

#include <OneWire.h>
#include <DallasTemperature.h>

setup(){
   initOneWire(); // Soll ein Objekt zurückgeben, oder einen Global verfügbaren Prototypen mit
                        //  initialisieren
   DallasTemperature sensoren(&???); // Soll das Objekt aus OneWire weiterverwenden.
   
}


void initOneWire(){

  pin = 5;
  OneWire onewireobjekt(pin);
}



Warum ich das so mache:
 pin hat hier den Wert 5, soll aber später vielleicht einmal aus einer Konfiguration gelesen werden. Auch möchte ich mit der Funktion initOneWire irgendwann einmal verschiedene Pins durchprüfen ob hier onewire-Geräte gefunden werden können.

Die Gültigkeit endet nach Ende der initOneWire()-Funktion. Das ist mein Problem.
Ich habe auch schon vieles durchprobiert, zum Beispiel Dinge wie:

   OneWire oneWire = OneWire(5);

Das funktioniert, aber auch hier das gleiche Problem, dass ich das "Ergebnis" nicht aus der Funktion herausbekomme...

...ich möchte das wirklich lernen, daher bitte nicht meckern, weil ich auf dem OneWire so rumreite :D Aber da habe ich erstmal das Hauptproblem.

Es gibt ja zum Beispiel nicht nur die Temperatursensoren, sondern auch andere OneWire-Geräte, die aber am gleichen Pin hängen, also könnte ich die Referenz auf das OneWire-Objekt "Welchen Bus sollst du verwenden" auch an andere Funktionen/Objekte weitergeben.

combie

#7
Jul 12, 2019, 12:20 pm Last Edit: Jul 22, 2019, 03:55 pm by combie
Quote
Warum ich das so mache:
 pin hat hier den Wert 5, soll aber später vielleicht einmal aus einer Konfiguration gelesen werden. Auch möchte ich mit der Funktion initOneWire irgendwann einmal verschiedene Pins durchprüfen ob hier onewire-Geräte gefunden werden können.
Wenn du zur Laufzeit Objekte instanziieren möchtest, dann musst du das auch tun.
:o Eine recht triviale Ansage, oder?  :o

Zu diesem Zwecke, kennt C++ das Schlüsselwort: "new"+"delete"



Nachtrag:
Hier mal eine (vermutlich) unsinnige Lösung, für ein (vermutlich) unnötiges Problem.
(verschiedenste Varianten, man sich ausdenken könnte)

Code: [Select]


#include <Streaming.h>
#include <OneWire.h>
#include <DallasTemperature.h>


// ----------------------------

class Sensor
{
  private:
  OneWire *onewire;
  DallasTemperature *dallas;

  void cleanup()
  {
    delete dallas;
    dallas  = nullptr;
   
    delete onewire; 
    onewire = nullptr;
  }

  public:
  Sensor():onewire(nullptr),dallas(nullptr){}

  ~Sensor()
  {
    cleanup();
  }
 
  DallasTemperature *getDallas()
  {
    if(!onewire) exit(1);
    if(!dallas)  exit(2);
    return dallas;
  }

  DallasTemperature *setPin(const byte pin)
  {
    cleanup();
    onewire = new OneWire(pin);
    dallas  = new DallasTemperature(onewire);
    return getDallas();
  }
 
  void release()
  {
    cleanup();
  }
};


// -----------------------


Sensor sensor;

const byte pinliste[] {5,6,7};


// --------------


void setup()
{
 Serial.begin(9600);
 Serial << "Start" << endl;

 for(byte pin:pinliste)
 {
   sensor.setPin(pin)->begin();
   Serial << "es wurden "<< sensor.getDallas()->getDeviceCount() <<" Sensoren an Pin " << pin << " gefunden" << endl;
 }
 sensor.release();
 
}

void loop()
{

}
> Das größte Problem, ist die Wahl der richtigen Gedanken <
Frei nach Dale Carnegie

Tommylicious1980

Wow! Das ist ja mal eine Ansammlung! Also Danke dafür, jetzt muss ich es nur noch auseinandernehmen um es auch zu kapieren. Für die oneworld Geschichte ist das alles sicher nicht so relevant, weil man das ja einmal aktiviert und gut. Aber mir geht es ja tatsächlich darum das ganze zu verstehen und da brauche ich nun mal einen Anwenduungsfall der halbwegs realistisch ist.

Vielen Dank !

LG Thomas

combie

Quote
Wow!
Schön, dass es dir gefällt!

Von dynamischer Speicherverwaltung möchte ich dir, auf kleinen µC, abraten.
Ist im Einzelfall ok, aber grundsätzlich, eher böse.

Ansonsten:
Ein fröhliches kauen, ich dir wünsche.
> Das größte Problem, ist die Wahl der richtigen Gedanken <
Frei nach Dale Carnegie

Tommylicious1980

#10
Jul 22, 2019, 07:21 am Last Edit: Jul 22, 2019, 08:26 am by Tommylicious1980 Reason: Ergänzungen
Guten Morgen,
jetzt bin ich eine Weile im Urlaub gewesen und wie gut es mal tut, ein Problem an dem man sich festgefressen hat einfach mal liegen zu lassen. Ich würde hier gerne noch ein paar Fragen zu der Klasse stellen, bzw. das wiedergeben, was ich verstanden habe um sicher zu gehen, dass ich das auch verstehe...

Ich lese das so:
Mit
Code: [Select]
OneWire *onewire;
DallasTemperature *dallas;

 

werden Zeiger auf die Werte (nicht die Speicheradressen) vom Datentyp OneWire bzw. DallasTemperature angelegt.

In der Funktion cleanup() wird die Speicheradresse wieder freigegeben, also der Zeiger dallas und onewire wird entfernt. Wie kann es dann sein, dass der folgende Aufruf bei dem die Werte an dieser Adresse mit nullptr gelöscht werden fehlerlos bleibt? Andersherum würde es mir viel logischer erscheinen.


Öffentlich zugegriffen werden kann auf die Funktion release(), die wiederum nur die interne methode cleanup() aufruft. Dadurch wird dann auch klar, dass ich zunächst ein Objekt erstelle, das wiederum später intern zerstört wird. Ein Aufruf ohne vorher ein Objekt erstellt zu haben würde einen Fehler produzieren, da ja kein Speicher freizugeben ist.

Die Funktionen getDallas() und setPin() sind auch öffentlich zugänglich, wobei diese aber beim Aufruf durch den Operator "*" jeweils einen Zeiger auf ein Objekt vom Typ DallasTemperature zurückliefern, womit dann auch sichergestellt ist, dass diese Objekte einen Geltungsbereich auch außerhalb der Klasse besitzen, da diese ja erst durch die Cleanup() Funktion endgültig vernichtet werden.

Ist das so richtig?

(Danke schon mal :D)

Ach ja... ich komme noch nicht so ganz zurecht mit den Zeilen public-Teil:

Code: [Select]

Sensor():onewire(nullptr),dallas(nullptr){};

~Sensor() {
      cleanup();
    }


~Sensor() ist der Destructor, soweit verstehe ich das ja, aber der Konstruktor ist für mich nicht ganz klar, durch die geschweiften Klammern am Ende würde ich ja eine Inlineartige Funktion generieren.  Ich vermute mal ohne es wirklich zu wissen: Wenn ich Sensor() aufrufe, dann erzeuge ich direkt auch  interne: weil diese im private-Teil deklariert sind und durch die Übergabe von nullptr "leere" Objekte für onewire und dallas. Ist das so richtig? Bzw. ist das die "ganze Wahrheit"?

combie

#11
Jul 22, 2019, 11:24 am Last Edit: Jul 22, 2019, 02:49 pm by combie
Quote
Bzw. ist das die "ganze Wahrheit"?
Der Konstruktor hat eine Initialisierungsliste.
Findest du auch so im modernen C++ Handbuch.

Dort werden die beiden privaten Elemente mit nullptr initialisiert.


Alternativ:
Code: [Select]
Sensor()
{
  onewire = nullptr;
  dallas  = nullptr;
}


Ich bevorzuge die Initialisierungsliste, wo immer, mir das möglich ist.
Denn sie kann auch Dinge, welche als const(read only) deklariert sind, und Referenzen, initialisieren. Ebenso bevorzuge ich Referenzen (vor Zeigerwirtschaft), aber in diesem speziellen Fall ist das wohl alternativ los.

Quote
In der Funktion cleanup() wird die Speicheradresse wieder freigegeben, also der Zeiger dallas und onewire wird entfernt. Wie kann es dann sein, dass der folgende Aufruf bei dem die Werte an dieser Adresse mit nullptr gelöscht werden fehlerlos bleibt? Andersherum würde es mir viel logischer erscheinen.
Hier ist der Text nicht in Ordnung!
(oder die Idee dahinter)

Der Speicherbereich wird frei gegeben (delete), nicht die Adresse und damit auch nicht der Zeiger an sich.
Der Zeiger, und damit die Adresse, wird auf nullptr gesetzt, und somit kenntlich gemacht, dass diese Zeiger ab jetzt auf Nix vernünftiges zeigen.
Klarer?


Quote
werden Zeiger auf die Werte (nicht die Speicheradressen) vom Datentyp OneWire bzw. DallasTemperature angelegt.
Es werden Zeiger definiert, welche die Adresse einer Instanz der betreffenden Klasse halten können.
Speicherplatz. für die Instanzen. wird an der Stelle noch nicht reserviert. Das erfolgt erst später per new.



Ich hoffe, dass ich einige grundlegende Klarheiten beseitigen konnte.
> Das größte Problem, ist die Wahl der richtigen Gedanken <
Frei nach Dale Carnegie

Serenifly

Was man bei Initialisierungslisten verstehen muss ist das Variablen und Konstanten direkt beim Erstellen des Objekts initialisiert werden. Wenn man im Konstruktor ist, ist es schon zu spät. Da unterscheidet sich C++ deutlich von anderen Sprachen.
Man kann Dinge im Konstruktor-Körper initialisieren, aber die wurden schon vorher Default-initialisiert. Bestenfalls macht man es also doppelt. Und Konstanten oder Referenzen müssen eben sofort initialisiert werden und können danach nicht mehr andere Werte zugewiesen bekommen. Da muss man dann die Liste verwenden

Tommylicious1980

Was man bei Initialisierungslisten verstehen muss ist das Variablen und Konstanten direkt beim Erstellen des Objekts initialisiert werden. Wenn man im Konstruktor ist, ist es schon zu spät. Da unterscheidet sich C++ deutlich von anderen Sprachen.
Man kann Dinge im Konstruktor-Körper initialisieren, aber die wurden schon vorher Default-initialisiert. Bestenfalls macht man es also doppelt. Und Konstanten oder Referenzen müssen eben sofort initialisiert werden und können danach nicht mehr andere Werte zugewiesen bekommen. Da muss man dann die Liste verwenden
...verstanden! Alles was man explizit vorgibt schützt einen später vor Überraschungen :D

Tommylicious1980

Hier ist der Text nicht in Ordnung!
(oder die Idee dahinter)

Der Speicherbereich wird frei gegeben (delete), nicht die Adresse und damit auch nicht der Zeiger an sich.
Der Zeiger, und damit die Adresse, wird auf nullptr gesetzt, und somit kenntlich gemacht, dass diese Zeiger ab jetzt auf Nix vernünftiges zeigen.
Klarer?
Ja, so verstehe ich es und bin damit einer unglücklichen Formulierung (die ich vorher beim Recherchieren gefunden hatte) aufgesessen. "Klarheit" beseitigt ;) Danke.

Es werden Zeiger definiert, welche die Adresse einer Instanz der betreffenden Klasse halten können.
Speicherplatz. für die Instanzen. wird an der Stelle noch nicht reserviert. Das erfolgt erst später per new.
Und auch hier habe ich es jetzt wohl verstanden, das ist auch durchaus praktisch, weil ich so ja viel flexibler bin und wenn etwas im Programmverlauf nicht gebraucht wird, dann muss ich dafür auch nicht (möglicherweise  große) Speichermengen nutzlos verbraten, die an anderer Stelle gebraucht werden.

Go Up