Zeiger und Referenzen

Über den Beitrag

Zeiger und Referenzen – was für trockenes Thema! Warum habe ich ihm dann trotzdem einen ganzen Beitrag gewidmet? Nach meiner Erfahrung haben nicht nur blutige Anfänger gelegentlich Probleme damit, und zwar insbesondere mit den Zeigern. Deshalb habe ich versucht, das Thema systematisch, verständlich und mit vielen Beispielsketchen aufzubereiten. Dieser Beitrag ist für Leser mit weniger Erfahrung wahrscheinlich immer noch recht schwere Kost. Aber es lohnt sich aus meiner Sicht, einmal tiefer in die Materie einzusteigen, denn es ist nicht möglich, dem Thema gänzlich aus dem Wege zu gehen.

Außerdem soll dieser Beitrag das Rüstzeug für den Folgebeitrag liefern, bei dem es um den SRAM gehen wird.

Folgendes kommt auf euch zu:

Zeiger

Was ist ein Zeiger?

Bevor wir zu der Frage kommen, was ein Zeiger (Englisch: Pointer) ist, gehen wir noch einen Schritt zurück: Was ist eigentlich eine Variable? Eine Variable ist ein reservierter Speicherplatz für Daten, die ihr während des Programmlaufes verändern könnt. Die Variable hat einen Namen, einen Datentyp, eine bestimmte Adresse und einen bestimmten Wert. Der Datentyp bestimmt, wie groß der zu reservierende Speicherplatz ist.

Ein Zeiger ist auch eine Variable. Er ist aber insofern besonders, als sein Wert die (Anfangs-)Speicheradresse eines anderen Speicherobjektes ist. Der Zeiger tut also, was sein Name vermuten lässt: Er zeigt auf dieses andere Speicherobjekt. 

Bei der Definition des Zeigers muss der Datentyp des Zielobjektes angegeben werden. Ohne diese zusätzliche Information wäre nicht bekannt, wo das Zielobjekt im Speicher endet und wie es zu interpretieren ist.

Ein einfaches Beispiel

Da diese Erklärungen sehr theoretisch sind, schauen wir uns ein praktisches Beispiel an.

void setup(){
  Serial.begin(9600);
  
  int iVal = 42;
  Serial.print("iVal = ");
  Serial.println(iVal);
  
  int *iPointer;  // alternative: int* iPointer;
  iPointer = &iVal;
  Serial.print("*iPointer = ");
  Serial.println(*iPointer);
  
  iVal += 4200;
  Serial.print("iVal = ");
  Serial.println(iVal);
  Serial.print("*iPointer = ");
  Serial.println(*iPointer);
}

void loop(){}

Und so sieht die Ausgabe aus:

Zeiger in Aktion: Ausgabe pointer_basic.ino
Ausgabe pointer_basic.ino

Erklärungen zum Sketch

Zunächst wird die Integer-Variable iVal deklariert und ihr der Wert 42 zugewiesen.

Mit der Zeile int *iPointer erzeugen wir den Zeiger iPointer. Das Zeichen * legt fest, dass es sich um einen Zeiger handelt. int bestimmt, dass das Ziel, also das Speicherobjekt, auf das der Zeiger zeigt, ein Integer ist.

Zum Zeitpunkt der Deklaration haben wir allerdings noch nicht definiert, wo sich das Ziel befindet, sprich, wohin der Zeiger zeigt. Das erreichen wir mit dem Ausdruck iPointer = &iVal;. Er bewirkt, dass iPointer auf die Adresse von iVal zeigt. Das Zeichen & ist der Adressoperator.

iPointer ist der Zeiger selbst. Um auf den Wert zuzugreifen, auf den iPointer zeigt, müsst ihr dem Zeiger den Indirektionsoperator * voranstellen. Der Indirektionsoperator wird auch als Dereferenzierungsoperator bezeichnet.

Wenn wir nun die Variable iVal verändern, steht sie immer noch an derselben Adresse. Die Anweisung iVal += 4200; hat deshalb keine Auswirkung auf iPointer, wohl aber auf dessen Zielwert *iPointer.

Vertiefung

Zur Vertiefung habe ich den folgenden Sketch geschrieben:

void setup(){
  Serial.begin(9600);
  delay(2000); // needed for some boards

  int iVal = 4242;
  int *iPointer;
  iPointer = &iVal;
   
  long iLongVal = 42424242;
  long *iLongPointer;
  iLongPointer = &iLongVal;

  long long iLongLongVal = 4242424242424242;
  long long *iLongLongPointer;
  iLongLongPointer = &iLongLongVal;
  
  Serial.print("iVal             = ");
  Serial.println(iVal);
  Serial.print("iLongVal         = ");
  Serial.println(iLongVal);
  Serial.println("Sorry, can't print iLongLongVal"); 
   *iLongPointer += 1;
  Serial.print("Updated iLongVal = ");
  Serial.println(iLongVal);
  Serial.println();

  Serial.print("iVal address     = ");
  Serial.println((int)&iVal);
  Serial.print("iPointer value   = ");
  Serial.println((unsigned int)iPointer);
  Serial.print("iPointer address = ");
  Serial.println((int)&iPointer);
  Serial.print("*iPointer        = ");
  Serial.println(*iPointer);
  Serial.println();
   
  Serial.print("Size of iVal         = ");
  Serial.println(sizeof(iVal));
  Serial.print("Size of iLongVal     = ");
  Serial.println(sizeof(iLongVal));
  Serial.print("Size of iLongLongVal = ");
  Serial.println(sizeof(iLongLongVal));
  Serial.println(); 
  
  Serial.print("Size of iVal address         = ");
  Serial.println(sizeof(&iVal));
  Serial.print("Size of iLongVal address     = ");
  Serial.println(sizeof(&iLongVal));  
  Serial.print("Size of iLongLongVal address = ");
  Serial.println(sizeof(&iLongLongVal));  
  Serial.println(); 
  
  Serial.print("Size of iPointer         = ");
  Serial.println(sizeof(iPointer));
  Serial.print("Size of iLongPointer     = ");
  Serial.println(sizeof(iLongPointer));
  Serial.print("Size of iLongLongPointer = ");
  Serial.println(sizeof(iLongLongPointer));
}

void loop(){/* empty */}

 

Ein Hinweis, der nichts mit Zeigern zu tun hat: Bei so vielen Serial.print("text") Anweisungen ist zu überlegen, ob man nicht auf Serial.print(F("text")) ausweicht, um RAM-Speicher auf Kosten von Programmspeicher zu sparen (näheres dazu hier). Ich habe konsequent darauf verzichtet, um den Blick auf das Wesentliche zu konzentrieren. 

Zurück zum Thema: Bevor wir zu den Erklärungen kommen, hier noch die Ausgabe, die ich bei Verwendung eines Arduino Nano erhalten habe:

Zeiger in Aktion: Ausgabe basic_pointer_2.ino
Ausgabe basic_pointer_2.ino

Erklärungen

In dem Sketch werden drei Variablen vom Typ Integer, Long Integer und Long Long Integer definiert und ausgegeben. Long Long Integer können nicht mit Serial.print() ausgegeben werden, aber das ist ein anderes Thema. Zusätzlich zu den drei Variablen werden Zeiger definiert, die auf die Variablen zeigen.

In Zeile 21 wird *iLongPointer inkrementiert. Wie ihr seht, ändert das den Wert von iLongVal. Das ist im Grunde wenig überraschend, da iLongPointer ja auf iLongVal zeigt.

Am Beispiel von iVal schauen wir uns im Detail an, was den Unterschied zwischen der Variablen selbst und einem Zeiger darauf ausmacht. iVal hat den Wert 4242 und befindet sich an der Speicheradresse 2298. Dabei ist 2298 die Anfangsadresse. Bis wohin sich iVal erstreckt, hängt vom Variablentyp und vom verwendeten Mikrocontroller ab. Um die Adresse von iVal zu ermitteln, verwenden wir den Adressoperator &. Um die Adresse auszugeben, muss sie noch explizit in eine Ganzzahl umgewandelt werden, z. B. so: (unsigned long)&iVal.

iPointer hat eine eigene Adresse, nämlich 2296. Der Zugriff auf die Adresse funktioniert genauso wie bei iVal. Der Wert des Zeigers, also das, was an seiner eigenen Speicheradresse steht, ist die Zieladresse (also die Adresse von iVal), nämlich 2298.

Variable vs. Zeiger auf die Variable
Variable vs. Zeiger auf die Variable

In den folgenden Programmzeilen ermittelt der Sketch den Platzbedarf für die verschiedenen Integertypen (int, long, long long). Auf einem ATmega328P basierten Arduino sind das 2, 4 und 8 Byte. „Hausaufgabe“: führt den Sketch mal auf einem ESP32-, ESP8266- oder SAMD-Board aus und schaut euch den Unterschied an.

Die Adressen der Integerwerte haben auf einem ATmega328P basierten Board eine Größe von 2 Byte. Das entspricht dem Platzbedarf der Zeiger. Es ist also egal, wie groß das Objekt ist, auf das der Zeiger zeigt – der Speicherbedarf des Zeigers ist immer gleich. 

Mit einem Zeiger einen Speicherbereich auslesen

Wie ihr gesehen habt, könnt ihr den Wert einer Variable ausgeben, indem ihr den Zeiger auf die Variable mit dem Operator * dereferenziert. Aber ist es denn auch möglich, den Inhalt des Speichers an einer x-beliebigen Adresse auszulesen? Die Antwort ist: Jein! Das Auslesen selbst ist kein Problem – aber bis wohin? Und handelt es sich um ein Zeichen (Character) oder eine Ganzzahl oder ein Float? 

Welcher Datentyp sich hinter den auszulesenden Daten verbirgt, muss man schlicht wissen. Das Auslesen erfolgt indirekt über einen Zeiger. Hier die Anleitung als Pseudo-Code:

datatype *anyPointer;
anyPointer = (datatype*)address;
Value of anyPointer = *anyPointer;
// In short / kurz:
datatype anyPointer = *(datatype*)anyPointer;

In Worten: 1) Definiere einen Zeiger mit dem richtigen Variablentyp, 2) Ordne dem Zeiger die auszulesende Adresse zu, 3) Greife auf den Wert zu, indem du den Zeiger dereferenzierst.

Der folgende Sketch spielt damit ein wenig, indem er den Inhalt eines Speicherbereiches auf unterschiedliche Weise interpretiert.

void setup(){
  Serial.begin(9600);
  delay(2000);
   
  long iLongVal = 42424242;
  long *iLongPointer;
  iLongPointer = &iLongVal;
  
  Serial.print("iLongVal = ");
  Serial.println(iLongVal);
 
  Serial.print("iLongVal address = ");
  Serial.println((int)&iLongVal);

  Serial.print("*iLongPointer = ");
  Serial.print(*iLongPointer);
  Serial.print(" = 0x");
  Serial.println(*iLongPointer, HEX);
  Serial.println();

  byte *jVal;  // alternative to byte: unsigned char
  byte *kVal;
  byte *lVal;
  byte *mVal;
  int *nVal;
  int *oVal;
  long *pVal;
  char *kChar;
  
  jVal = (byte*)&iLongVal;
  kVal = (byte*)((int)&iLongVal + 1); 
  lVal = (byte*)((int)&iLongVal + 2); 
  mVal = (byte*)((int)&iLongVal + 3); 
  nVal = (int*)((int)&iLongVal);
  oVal = (int*)((int)&iLongVal + 2);
  pVal = (long*)((int)&iLongVal);
  kChar = (char*)((int)&iLongVal + 1); 
  
  Serial.print("*jVal @ address of iLongVal   = 0x");
  Serial.println(*jVal, HEX);
  Serial.print("*kVal @ address of iLongVal+1 = 0x");
  Serial.println(*kVal, HEX);
  Serial.print("*lVal @ address of iLongVal+2 = 0x");
  Serial.println(*lVal, HEX);
  Serial.print("*mVal @ address of iLongVal+3 = 0x");
  Serial.println(*mVal, HEX); 
  Serial.print("*nVal @ address of iLongVal   = 0x");
  Serial.println(*nVal, HEX);
  Serial.print("*oVal @ address of iLongVal+2 = 0x");
  Serial.println(*oVal, HEX);
  Serial.print("*pVal @ address of iLongVal   = 0x");
  Serial.println(*pVal, HEX);
  Serial.print("*kChar @ address of iLongVal+1 = ");
  Serial.println(*kChar);  
}

void loop(){/* empty */}

 

Erklärungen

Zunächst definieren wir die Variable iLongVal, damit wir etwas im Speicher stehen haben. Die Variable erstreckt sich über vier Byte. Im nächsten Schritt lesen wir alle vier Byte einzeln aus, dann jeweils zwei als Integer und noch mal alle vier als Long Integer. Zum Schluss nehmen wir ein Byte heraus und interpretieren es als Character. Wie ihr sehen werdet, ist der Wert dieses Bytes 0x57 und das ist nach ASCII-Tabelle ein „W“.

Die Struktur des Inhalts des betrachteten Speicherbereiches wird am besten deutlich, indem wir im Hexadezimalsystem arbeiten. Hier die Ausgabe:

Ausgabe basic_pointer_3.ino
Ausgabe basic_pointer_3.ino

Der praktische Nutzen dieser Vorgehensweise hält sich in engen Grenzen, denn wir sollten ja wissen, was wir wo in den Speicher geschrieben haben. Entschuldigung, dass ich das jetzt erst mitteile 😉 . Allerdings hielt ich diesen Ausflug zur weiteren Vertiefung des Wissens über Zeiger für recht sinnvoll.

Arrays

Genau genommen meint man im Mikrocontroller-/Arduinobereich C-Arrays, wenn man von Arrays spricht. Es gibt in C++ die sehr viel komfortablere Klasse vector. Dass trotzdem C-Arrays verwendet werden, ist einfach eine Ressourcenfrage. C-Arrays sind schneller und brauchen weniger Speicher.

Wenn ihr ein (C-)Array definiert, dann ist der Name des Arrays zugleich der Bezeichner des Zeigers auf das Element 0 des Arrays. Das ist einfacher, als es vielleicht klingt. Es bedeutet lediglich, dass ihr anstelle von anyArray[0] genauso gut *anyArray schreiben könnt.

Der folgende Sketch spielt ein wenig mit den Zeigereigenschaften von Arrays:

void setup() {
  Serial.begin(9600);
  delay(2000); // needed for some boards
  
  int intArray[4] = {4, 42, 424, 4242};
  for(int i=0; i<4; i++){
    Serial.print("intArray[");
    Serial.print(i);
    Serial.print("] = ");
    Serial.println(intArray[i]);
  }
  Serial.println();
  Serial.print("*intArray       = ");
  Serial.println(*intArray);
  Serial.print("*(intArray + 2) = ");
  Serial.println(*(intArray + 2));
  int *arrayElement_3;
  arrayElement_3 = &intArray[3];
  Serial.print("*arrayElement_3 = ");
  Serial.println(*arrayElement_3);
  Serial.println();

  int *sameArray = intArray;
  Serial.println("sameArray[]:");
  for(int i=0; i<4; i++){
    Serial.print(sameArray[i]);
    Serial.print(" ");
  }
  Serial.println();
  Serial.println("Listing by pointer incrementation:");
  for(int *p=intArray; p < intArray + 4; p++){
    Serial.print(*p);
    Serial.print(" ");
  }
  
  Serial.println("\n");
  Serial.print("Size of intArray  = ");
  Serial.println(sizeof(intArray));
  Serial.print("Size of sameArray = ");
  Serial.println(sizeof(sameArray));
  Serial.print("Address of intArray  = ");
  Serial.println((int)&intArray);
  Serial.print("Address of sameArray = ");
  Serial.println((int)&sameArray);
  Serial.print("Value of intArray  = ");
  Serial.println((int)intArray);
  Serial.print("Value of sameArray = ");
  Serial.println((int)sameArray);
}

void loop() {}

 

Hier zunächst die Ausgabe:

Ausgabe von array_play.ino
Ausgabe von array_play.ino

Erklärungen

Mit dem Wissen, das ihr inzwischen über Zeiger habt, ist die Ausgabe des Sketches wahrscheinlich keine komplette Überraschung. Dennoch sind mindestens zwei Aspekte bemerkenswert:

  • *(intArray + 2) liefert den Wert des Elementes 2 des Arrays, obwohl es sich bei dem von mir verwendeten Mikrocontroller „4 Byte entfernt“ von der Startadresse befindet (denn 2 Integer = 4 Byte). Auf einem ESP32 würde es genauso funktionieren, obwohl ein Integer dort eine Größe von 8 Byte hat. intArray + 2 heißt also „zweites Element von intArray“ und nicht „Speicherplatz von intArray + 2 Byte“ o. ä.
    • Entsprechend funktioniert auch das Inkrementieren in der for-Schleife ab Zeile 29.
  • Mithilfe des Zeigers sameArray könnt ihr auf die Elemente von intArray zugreifen. Ihr merkt keinen Unterschied. Trotzdem: sameArray ist nicht intArray, auch wenn beide auf dieselbe Adresse zeigen. So hat sameArray auch nur eine Größe von 2 Byte und nicht 8 Byte wie intArray. Darauf kommen wir bei der Übergabe von Arrays an Funktionen zurück.

Referenzen

Referenzen sind erheblich leichter zu verstehen als Zeiger. Eine Referenz ist schlicht ein anderer Bezeichner für ein und dasselbe Objekt. Ein Alias sozusagen. Das einzig Verwirrende ist, dass bei der Definition von Referenzen wieder das Zeichen & verwendet wird. Mit int &x = y erzeugt ihr den Alias x für die Integervariable y. Das ist nicht zu verwechseln mit dem schon bekannten int x = &y, was der Variablen x den Wert „Adresse von y“ zuweist. Das Zeichen & wird auch als Referenzierungsoperator bezeichnet, was in diesem Zusammenhang passender ist als Adressoperator.

Hier ein einfaches Beispiel:

void setup(){
  Serial.begin(9600);
   
  int iVal = 42;
  int &iRef = iVal; //alternative: int& iRef = iVal;
    
  Serial.print("iVal = ");
  Serial.println(iVal);
  Serial.print("iRef = ");
  Serial.println(iRef);

  iRef++;
  Serial.print("Updated iVal = ");
  Serial.println(iVal);
}

void loop(){/* empty */}

Nachdem wir mit int &iRef = iVal; einen Alias für iVal namens iRef erzeugt haben, lassen sich beide Bezeichner für dieselbe Variable benutzen. Alle Änderungen an iRef wirken sich gleichermaßen auf iVal aus und umgekehrt, denn iRef ist iVal.

Ausgabe reference_basic.ino

Parameterübergabe an Funktionen

Zeiger und Referenzen spielen eine große Rolle bei der Übergabe von Parametern an und von Funktionen oder Objekte. Dabei werden ihre Vorzüge erst richtig deutlich.

Call-by-Value

Als „Call-by-Value“ bezeichnet man die „normale“ Übergabe von Parametern an Funktionen oder Objekte, also die Form der Übergabe, die man für gewöhnlich als Erste lernt. Hier ein Beispiel:

void setup() {
  Serial.begin(9600);
  int iVar = 42;
  Serial.print("iVar in Setup: "); Serial.println(iVar);
  passAsInteger(iVar);
  Serial.print("iVar in Setup: "); Serial.println(iVar);
}

void loop() {/* empty */}

void passAsInteger(int var){
  var *= 2;
  Serial.print("Variable in passAsInteger(): "); Serial.println(var);  
}

Die Variable iVar wird an die Funktion passAsInteger() übergeben und dort verdoppelt. Der Sketch gibt den Wert vor Übergabe, innerhalb der Funktion und nach Rückkehr ins Setup aus. Die Ausgabe dürfte nicht überraschen:

Ausgabe call_by_value.ino
Ausgabe call_by_value.ino

Bei der Übergabe an die Funktion wird eine Kopie von iVal im dynamischen Speicher angelegt. Die Kopie wird nach der Beendigung der Funktion gelöscht und das Original iVar bleibt unverändert.

Call-by-Reference

Anders sieht es aus, wenn wir den Parameter als Referenz übergeben. Dazu müsst ihr lediglich dem Parameter den Referenzierungsoperator & in der empfangenden Funktion voranstellen:

void setup() {
  Serial.begin(9600);
  int iVar = 42;
  Serial.print("iVar in Setup: "); Serial.println(iVar);
  passAsReference(iVar);
  Serial.print("iVar in Setup: "); Serial.println(iVar); 
}

void loop() {/* empty */}

void passAsReference(int &var){
  var *= 2;
  Serial.print("Variable in passAsReference(): "); Serial.println(var);  
}

Die Ausgabe ist:

Ausgabe call_by_reference.ino
Ausgabe call_by_reference.ino

Die Funktion verwendet das Original mit einem anderen Bezeichner. Entsprechend bleibt die Variable nach Beendigung der Funktion verändert.

Die Übergabe als Referenz hat den Vorteil, dass das Anlegen der lokalen Kopie entfällt. Das spart Zeit und dynamischen Speicher.

Zu viele Rückgabewerte?

Es gibt noch einen weiteren Vorteil. Manchmal möchte man mehrere Variablen in einer Funktion bearbeiten und das Ergebnis nach Beendigung der Funktion erhalten. Die Übergabe und die Bearbeitung sind unproblematisch. Allerdings können wir nur einen Wert zurückgeben. Dieses Problem umgehen wir, indem wir die Variablen als Referenz übergeben.

Dazu ein kleines Beispiel:

void setup() {
  Serial.begin(9600);
  int iVar_1 = 42;
  int iVar_2 = 4242;
  Serial.println("Original values:");
  Serial.print("iVar_1: "); Serial.println(iVar_1);
  Serial.print("iVar_2: "); Serial.println(iVar_2);
  swap(iVar_1, iVar_2);
  Serial.println("Swapped values:");
  Serial.print("iVar_1: "); Serial.println(iVar_1);
  Serial.print("iVar_2: "); Serial.println(iVar_2); 
}

void loop() {/* empty */}

void swap(int &val_1, int &val_2){
  int temp = val_1;
  val_1 = val_2;
  val_2 = temp;  
}

Der Sketch tauscht die Werte zweier Variablen. Das wäre mit Call-by-Value so nicht möglich. Hier noch die Ausgabe:

Ausgabe changing_several_variables.ino
Ausgabe changing_several_variables.ino

Wenn ihr einen Parameter per Referenz übergeben, aber in der Funktion nicht verändern wollt, dann bietet es sich an, dem Parameter in der aufnehmenden Funktion das Schlüsselwort const voranzustellen. Das macht es klarer.

„Call-by-Pointer“

Alternativ könnt ihr in der Funktion mit einem Zeiger auf die zu bearbeitende Variable arbeiten. Das müsste dann „Call-by-Pointer“ heißen. Da der Begriff aber unüblich ist, habe ich ihn Anführungszeichen gesetzt.

Beim Aufruf der Funktion stellt ihr dem Parameter den Adressoperator voran. In der aufnehmenden Funktion kommt der Indirektionsoperator zum Einsatz:

void setup() {
  Serial.begin(9600);
  int iVar = 42;
  Serial.print("iVar in Setup: "); Serial.println(iVar);
  passAsPointer(&iVar);
  Serial.print("iVar in Setup: "); Serial.println(iVar); 
}

void loop() {/* empty */}

void passAsPointer(int *var){
  *var *= 2;
  Serial.print("Variable in passAsPointer(): "); Serial.println(*var);  
}

Auch hier arbeiten wir in der Funktion mit dem Original. Daher wird die Variable dauerhaft verändert:

Ausgabe call-by-pointer.ino

Ich würde grundsätzlich die Methode Call-by-Reference bevorzugen. Bei der Übergabe von Arrays sind wir allerdings automatisch bei der „Call-by-Pointer“ Methode.

Objektübergabe

Wenn ihr Objekte als Zeiger übergebt und in der Funktion Objektmethoden verwendet, dann müsst ihr innerhalb der Funktion den Dereferenzierungsoperator verwenden oder den Punktoperator durch den Pfeiloperator ersetzen. Hier ein Beispiel:

void setup() {
  Serial.begin(9600);
  String string = "Hello world";
  Serial.print("string in setup: "); Serial.println(string);
  upperCase(&string);
  Serial.print("string in setup: "); Serial.println(string);
}

void loop() {/* empty */}

void upperCase(String *localString){
  Serial.print("Upper case: ");
  localString->toUpperCase(); // alternative: *localString.toUpperCase;
  Serial.println(*localString); 
}

Und hier die Ausgabe:

Ausgabe using_object_methods_in_functions.ino
Ausgabe using_object_methods_in_functions.ino

Arrays übergeben

Ihr übergebt ein Array einer Funktion, indem ihr den Namen des Arrays als Parameter verwendet. Damit übergebt ihr nicht das ganze Array, sondern lediglich den Zeiger auf das Array. In der Funktion wird dann eine Kopie des Zeigers erzeugt. Wie wir vorhin gesehen haben, geht dabei die Information über die Größe des Arrays, also die Anzahl der Elemente, verloren. Die Größe des Arrays können wir bequem über sizeof(anyArray)/sizeof(datatype) berechnen und übergeben sie als zusätzlichen Parameter.

void setup() {
  Serial.begin(9600);
  int intArray[4] = {0};
  for(int i=0; i<4; i++){
    intArray[i] = i * 10;
  }
  
  Serial.println("Array in setup():");
  for(int i=0; i<4; i++){
    Serial.print(intArray[i]); 
    Serial.print("  ");
  }
  Serial.println();
  Serial.println();
  
  doubleArray(intArray, sizeof(intArray)/sizeof(int));
  
  Serial.println("Array in setup():");
  for(int i=0; i<4; i++){
    Serial.print(intArray[i]); 
    Serial.print("  ");
  }
  Serial.println();
  Serial.println();
}

void loop() {/* empty */}

void doubleArray(int *arr, size_t count){
  Serial.println("Array in doubleArray():");
  for(unsigned int i=0; i<count; i++){
    arr[i] *= 2;
    Serial.print(arr[i]); Serial.print("  ");
  }
  Serial.println();
  Serial.println();
}

 

Da die Funktion mit dem Original arbeitet, ist die Änderung durch die Funktion permanenter Natur.

Ausgabe pass_array.ino
Ausgabe pass_array.ino

Abschließende Worte

Ich denke, dass ich mit diesem Beitrag die wichtigsten Aspekte zum Thema Zeiger und Referenzen abgedeckt habe. Aber viel wichtiger ist, was ihr denkt. Wenn also Fragen offen sind oder Dinge un- oder missverständlich sind, dann teilt mir das gerne mit. Ansonsten hoffe ich, dass ich euch mit dem Beitrag nicht komplett verwirrt habe.

Danksagung

Das Beitragsbild verdanke ich pencil parker auf Pixabay.

4 thoughts on “Zeiger und Referenzen

  1. Hallo Wolfgang,
    vielen Dank für Deine Beispiele und Erläuterungen.
    Was mir in der Auflistung fehlt ist der Umgang mit Strings bzw. besser Characters.
    Man liest ja immer wieder, dass man „string“ vermeiden sollte und besser auf „char“ setzt. Beispiele hierzu wären sehr hilfreich.
    Was mich zudem verwirrt, sind die Möglichkeiten den *-Operator vor und hinter dem Datentyp zu setzen:
    *char oder char* – wann macht man was?
    Danke und Grüße
    Markus

    1. Hi Markus,
      der *-Operator kommt grundsätzlich vor den Variablennamen. Allerdings ist es egal, ob man z.B.
      int* a
      oder
      int *a
      schreibt.
      Oder kannst du mir ein Beispiel geben, wo du es anders gesehen hast?
      Zu den Strings: darauf gehe ich im nächsten Beitrag im Detail ein. Dafür muss man etwas über Heap und Stack lernen und das war mir zu viel für einen Beitrag.
      Stay tuned, wie man auf Neudeutsch sagen würde.
      VG, Wolfgang

      1. Hi Wolfgang,
        Stern hinter dem Datentyp ist nun klar – wenn man es mal weiß, dann ist es sehr banal.
        Freue mich schon auf Deinen nächsten Beitrag!
        Viele Grüße
        Markus

  2. Vielen Dank für Deine Mühe,

    sehr gut und verständlich erklärt.
    Als alter Hobby-Bastler ist es immer wieder erfrischend über manch „alte Angewohnheiten“ (a=b) nachzudenken.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert