Wie eine Struktur initialisieren?

Hallo,

ich habe ein Projekt in dem ich mir einen Ringbuffer bauen möchte. Dazu habe ich folgenden Datentyp deklariert:

struct CanMsg {
  unsigned int id;
  unsigned char len;
  unsigned char data[8];
};

Nun möchte ich diesen initialisieren, z.b. mit 16 leeren Einträgen. Später möchte ich, beim Empfang einer Nachricht einfach nur einen Zeiger auf einen freien Speicherplatz an eine Funktion übergeben die dann dort hinein die Daten ablegt. So hoffe ich am schnellsten zu sein und den verfügbaren Arbeitsspeicher (im Arduino ja knapp) auch zur Verfügung zu haben.

Ich kenne das man solche Strukturen dann mit "malloc(sizeof(..))" zur Laufzeit mit gültigen Zeigern versieht, aber ich glaube es wäre besser das beim Start zu tun. Ansonsten müsste der Arduino ständig neue RAM-Bereiche finden, wieder auflösen, reorganisieren, etc. Ist nicht nötig.

Wie mache ich das? Oder, wie sollte man sowas machen?

Nun möchte ich diesen initialisieren, z.b. mit 16 leeren Einträgen

struct CanMsg {
  unsigned int id;
  unsigned char len;
  unsigned char data[8];
}msgArray[16];

Globale Variable werden mit 0 initialisiert, das sollte als "leer" erkannt werden.

msgArray+0 ... msgArray+15 sind Zeiger auf diese 16 Elemente.

Dynamische Speicherverwaltung ( malloc, new ) geht zwar, ist aber in der Regel overkill: entweder der RAM - Speicher ist verfügbar oder eben nicht. Und ob statischer globaler RAM, dynamischer Heap und Stack kollidieren oder nicht, wird von keinem nicht vorhandenen Betriebssystem abgefangen...

Die Initialisierung habt iht ja schon geklärt.

Zum Füllen solcher Einträge benutze ich gerne eine Methode/Member Funktion.

struct CanMsg {
  unsigned int id;
  unsigned char len;
  unsigned char data[8];
  load(int iId, char iLen, char* iData) {
    id = iId;
    len = min(iLen, 8);
    memcpy(data, iData, len);
  }
}msgArray[16];

Da kann ich alle eventuellen Spezialitäten (Validierung, Setzen von Flags, etc)
schön an einer Stelle zusammenhalten und von überall benutzen.

Oder man macht es in einer extra Zeile:

CanMsg msgArray[16];

Wie eine normale Variable auch. Wenn man in anderen Fällen Werte vorgeben muss gibt es dafür wie üblich geschweifte Klammern

Bitte nochmal für mich zum mitschreiben. Mit der von mir genannten struct-Definition erzeuge ich ja quasi einen neuen Datentypen. Der Platzverbrauch dafür müsste dann beim Arduino doch so sein:

long id = 4 Bytes char len = 1 Byte

char[8] data = 8 Byte

= 13 Bytes

Wenn ich also mit sizeof() nachfrage, müsste das rauskommen, richtig?

Weiterhin wenn ich eine Variable von diesem Typen erzeuge, mit "CanMsg msg;" dann belegt die 13 Bytes RAM, welcher schon zur Compilezeit reserviert wird, richtig?

Dabei enthält die Variable msg selbst nur einen Zeiger auf die Speicheradresse (Startadresse) der Struktur, richtig? Ein solcher Zeiger selbst dürfte 2 Byte lang sein beim Arduino?

Erzeuge ich nun ein Array mit "CanMsg msgArray[16];", dann enthält die Variable msgArray den Zeiger auf die Speicheradresse/Startadresse des Arrays. Ab hier folgen dann 16*13 Bytes, also 208 Bytes welche beim Compilieren vom RAM reserviert werden, richtig?

Um nun ein Element in einem solchen Array anzusprechen bastele ich mir einen Zeiger vom gleichen Typ: CanMsg p; und weise die Startadresse eines Elementes im Array zu, z.B.: p = msgArray[5];

Jetzt, wenn ich Daten aus einem dieser MSGs einfüllen will, könnte ich das doch so tun: p.id = 0x200; p.len = 8; p.data = { 1, 2, 3, 4, 5, 6, 7, 8 };

oder gleich so: p = { 0x200, 8, { 1, 2, 3, 4, 5, 6, 7, 8 } };

Und will ich solch einen Zeiger an eine Funktion übergeben: fillMsg(&p);

Und in der Funktion dann so: void fillMsg( CanMsg * p ) { ... }

Oder, wenn meine Funktion nur einzelne Werte will: fillMsg( &p.id, &p.len, &p.data );

Ich denke ich habs noch nicht ganz verstanden, aber langsam formt sich ein Bild...

mikrotron: Jetzt, wenn ich Daten aus einem dieser MSGs einfüllen will, könnte ich das doch so tun: p.id = 0x200; p.len = 8; p.data = { 1, 2, 3, 4, 5, 6, 7, 8 };

oder gleich so: p = { 0x200, 8, { 1, 2, 3, 4, 5, 6, 7, 8 } };

Das Zweite geht nur bei der Initialisierung, das Erste wird kaum kompilen, oder etwas anderes machen als du denkst.

Ich hatte dir doch ein Beispiel für eine Speicherfunktion gegeben (wenn das auch einen Fehler hatte)

  void load(int iId, char iLen, char* iData) {
    id = iId;
    len = min(iLen, 8);
    memcpy(data, iData, len);
  }

Initialisieren = beim Erstellen der Variable Werte vorbesetzten

Alles andere ist eine Zuweisung und keine Initialisierung

Danke Whandall, aber ich blicke diese Methode noch nicht und wollte mich einfach auf Basics reduzieren. Warum meinst Du das p.id = 0x200; nicht das macht was ich glaube? Wenn ich es richtig verstanden habe, kann ich über den Punkt die Elemente einer Strukturvariablen zugreifen. Wenn p ein Zeiger wär, also nicht eine lokale Variable, dann wäre wohl der Pfeil-Operator richtg, also p->id = 0x200;

Eigentlich muss ich die Variablen bzw. das Array gar nicht initialisieren, zuweisen reicht hier völlig. Denn es wird ja mit den empfangenen Daten überschrieben.

Ich meine dass

p.data = { 1, 2, 3, 4, 5, 6, 7, 8 };

nicht macht was du denkst.

Was steht nach dem Statement in p.data?

Bzw was könnte drin stehen, falls es sich übersetzen ließe...

Mit lokalen Variablen hat das nichts zu tun. Ein Zeiger kann auch lokal sein. Der -> Operator ist eine Kombination aus Dereferenzierung und Zugriff

Wenn du übrigens nur ein Element übergeben willst, ist es einfacher du nimmst eine Referenz:

void fillMsg(CanMsg& p )

Dann brauchst du nicht den & Operator bei der Übergabe und beim Zugriff reicht wie normal ein Punkt. Aber es wird keine Kopie übergeben. Im Unterschied zu Zeigern müssen Referenzen direkt initialisiert werden, da sie nicht NULL sein können

Einen Zeiger bräuchtest du wenn du ein ganzes Array daraus an die Funktion übergeben willst. Das ist hier aber nicht der Fall

Was steht nach dem Statement in p.data? Hmm, ich hätte gedacht, weil data im Struct ein Array of Byte ist, die Inhalte die ich angebe?! Oder, äh, müsste es p.data[] = { 1,2,3,4,5,6,7,8 }; heissen?

{ } Klammern kannst du nur bei der Initialisierung verwenden. Das kompiliert so oder so nicht

Du kannst einem Array nichts zuweisen, nur seinen Elementen, oder dem von ihm verwalteten Speicher. Die Initialisierung sieht nur aus wie eine Zuweisung, das mag anfangs etwas verwirrend sein.

void load(CanMsg& p, int iId, char iLen, char* iData) {
  p.id = iId;
  p.len = min(iLen, 8);
  memcpy(p.data, iData, len);
}
void CanMsg::load(int iId, char iLen, char* iData) {
  id = iId;
  len = min(iLen, 8);
  memcpy(data, iData, len);
}

Die Member-Funktion ich finde angenehmer da der Parameter p wegfällt (implizit this) und man für Member weder . noch -> verwenden muss.

Um nun ein Element in einem solchen Array anzusprechen bastele ich mir einen Zeiger vom gleichen Typ: CanMsg p; und weise die Startadresse eines Elementes im Array zu, z.B.: p = msgArray[5];

-->Du nutzt doch hier weder Zeiger noch Adresse? Hier wird doch ein weiteres Struct erzeugt, in welches Du das Struct aus dem Array kopierst.....

Ich seh schon, da hab ich noch einiges zu lernen... :confused:

Sorry wenn ich Euch damit auf den Sack gehe, aber ich habe echt noch Schwierigkeiten das zu verstehen! Als PHP-Skripter muss man sich über solche Dinge kaum Gedanken machen...

Ich würde gern nochmal einen Schritt zurück und mit Zeigern anfangen, ob ich das alles richtig verstanden habe. Ich schreib einfach mal als ob ich es jemand anderem erklären müsste und ihr könnt das dann bitte korrigieren, kommentieren oder ergänzen:

Wenn man mit

char len;

eine Variable definiert, dann weiss der Compiler zunächst nur von welchem Typ sie ist und wie sie heißt. Auch wird er für den Wert der Variable eine entsprechende Anzahl Bytes im Arbeitspeicher reservieren. An der Speicheradresse können aber irgendwelche Daten liegen, da ich den Wert nicht initialisiert oder sonst irgendwie zugewiesen habe.

Sobald ich dieser Variablen einen Wert zuweise:

len = 8;

wird an der Speicherstelle der in der Variablen für den Wert vorgesehen ist, dieser neue Wert abgelegt. Somit ist der Name einer Variablen eigentlich nichts anderes wie ein Synonym für eine Speicheradresse.

Stellt man dem Variablennamen den Dereferenzierungsoperator "&" voran ("&len"), liefert dies nicht den Wert sondern die Speicheradresse des Wertes.

Durch voranstellen eines "*" bei der Deklaration einer Variablen wird festgelegt, das an der Speicheradresse dieser nicht der Wert sondern eine Speicheradresse auf den Wert zu finden ist:

char *p;

In eine solche Variable kann man nun eine Speicheradresse eines Wertes hineinkopieren:

p = &len;

Wichtig ist, das der Zeiger vom gleichen Typ ist wie die Variable selbst.

Nun zeigt "p" indirekt auf die gleiche Speicheradresse wie "len". Indirekt bedeutet, das die Variable "p" nicht wie eine normale Variable direkt auf die Speicheradresse von "len" zeigt, sondern auf eine Speicheradresse an der die Speicheradresse von "len" liegt. Dadurch kann man auch nicht direkt mit "p = ..." arbeiten, wie es mit "len" tun würde, sondern muss dereferenzieren. Um also den Wert von "len" über die Zeigervariablen "p" ändern zu wollen müsste man so schreiben:

*p = 3;

Für den Compiler bedeutet das: Ermittele die Speicheradresse vom Typ char an der durch p definierten Stelle und weise dieser den Wert 3 zu.

Mit dieser Methode kann man also die Speicheradressen von Variablen an Funktionen übergeben. Diese können dann die Werte darin direkt verändern. Dies ist hilfreich wenn man möchte das die funktion einen vorhandenen Wert ändert, oder einen noch nicht definierten Wert erzeugt:

void changeval ( char *p ) {
    *p = 8;
}

char len = 4;
changeval(&len);

char len2;
changeval(&len2);

Wenn dir Zeiger zu kompliziert sind verwende Referenzen. Für das was du willst brauchst du keine Zeiger. Referenzen sind letztlich auch nur Adressen, aber man kann weniger Unsinn damit machen, da sie nicht NULL sein können und keine Zeiger-Arithmetik möglich ist. Auch muss man nur der Deklaration sagen, dass es eine Referenz ist, aber der Zuweisung und Zugriff werden sie nicht anders behandelt wie normale Variablen.

Für call-by-reference verwendet man in C++ üblicherweise Referenzen. Zeiger als Parameter brauchst du hauptsächlich wenn du Arrays an eine Funktion übergeben musst, da Array-Variablen zu Zeigern auf das erste Element zerfallen. Das stammt aus C Zeiten wo es keine Referenzen gab

mikrotron: An der Speicheradresse können aber irgendwelche Daten liegen, da ich den Wert nicht initialisiert oder sonst irgendwie zugewiesen habe.

Nur wenn es eine lokale Variable ist. Globale Variablen werden mit 0 initialisiert

Stellt man dem Variablennamen den Dereferenzierungsoperator "&" voran ("&len")

& = Adresse von. Dereferenzierung ist das Gegenteil.

Dereferenzierung geht mit *. Damit bekommst du den Inhalt des Speichers auf den die Adresse zeigt.

Nun zeigt "p" indirekt auf die gleiche Speicheradresse wie "len". Indirekt bedeutet, das die Variable "p" nicht wie eine normale Variable direkt auf die Speicheradresse von "len" zeigt, sondern auf eine Speicheradresse an der die Speicheradresse von "len" liegt.

Nein. Da steht direkt die Adresse drin. Die kann man sich auch ausgeben lassen

Mit Zeigern kann man halt noch andere Dinge tun. z.B. können sich auf nichts zeigen. Und man kann Zeiger inkrementieren und dekrementieren um durch einen Speicherbereich zu iterieren.

Habs mal kurz überflogen...so wird schon eher ein Schuh draus... Deinen Code hast Du auch selber getestet, und die Ergebnisse, die er bringt?

Unter diesem Link findest Du eine ganz gute Stück-für-Stück-Erklärung zum Thema Zeiger ;)

Vielen Dank für das Feedback. Stimmt natürlich alles was ihr schreibt und so gaaanz langsam fang ich an zu verstehen :-)

Ich lese jetzt erstmal fleißig die Infos im angegebenen Link und mache Codeversuche damit. Ich war noch auf der Suche nach einem Arduino Simulator damit ich schneller testen kann, auch ohne Hardware. Ist aber nicht so leicht, obwohl das Angebot schon umfänglich ist... finde aber nichts recht passendes. Ich hatte gehofft einfach in einem Editor code zu schreiben, diesen dann direkt compilieren und ausführen zu lassen. Idealerweise gleich mit einem Debugger im Variableninhalte direkt zu lesen und nicht irgendwie ausgeben zu müssen.

Manches ging jetzt in ehrlicherweise in einer Bash auf nem Ubuntu schneller und einfacher. Besonders weil man da printf nutzen kann.