Character Arrays vs. Strings

Über den Beitrag

Wenn ihr in euren Programmen mit Zeichenketten arbeitet, stehen euch dafür Character Arrays und String-Objekte zur Verfügung. Gerade Anfänger greifen meistens zu den String-Objekten, weil sie im Vergleich zu den Character Arrays viel komfortabler sind. Allerdings sind Character Arrays ressourcenschonender und sollten deshalb bevorzugt eingesetzt werden.

In diesem Beitrag werde ich die wesentlichen Operationen, die ihr auf Character Arrays und String-Objekte anwenden könnt, besprechen und gegenüberstellen. Ihr werdet sehen, dass die Character Arrays den String-Objekten in ihrer Funktionalität in nichts nachstehen.

Im Einzelnen behandele ich die folgenden Operationen:

AktionFunktion (Character Arrays)Funktion (String-Objekte)
Größe / Länge bestimmensizeof() / strlen()sizeof() / length()
Verkettenstrcat() / strncat()Operator ′+=′
Duplizierenstrdup()Operator ′=′
In Groß- / Kleinbuchstaben wandeln strupr() / strlwr()toUpperCase() / toLowerCase()
Zeichen nach Index auslesencharArray[index]charAt()
Zeichen ersetzen / löschencharArray[index] setCharAt() / remove()
Ausschnitte (Substrings) erzeugenmemcpy() / strchr() / strrchr()substring()
Zerlegen nach Zeichenstrtok()substring() & indexOf()
Zeichen suchenstrchr() / strrchr() / strstr()indexOf() / lastIndexOf()
Vergleichenstrcmp()equals() / equalsIgnoreCase() /
compareTo() / Operator ′==′
Umwandlung Char Arrays ↔ Stringszu String: Operator ′=′zu Char Array: toCharArray() /
c_str()
Aus Zahlen erzeugendtostrf() / sprintf()String()
In Zahlen umwandeln atof() / atoI() / atol()toInt() / toFloat()
Über Serial einlesenreadBytes() / readBytesUntil()readString() / readStringUntil()
An Funktionen übergeben
Im Beitrag behandelte Funktionen

Nomenklatur

Wenn ich in diesem Beitrag von Character Arrays spreche, dann meine ich damit Arrays vom Datentyp char, die mit dem Nullzeichen '\0' (auch Null-Terminator genannt) enden. Man kann ebenso nicht-nullterminierte Character Arrays erzeugen. Das sind dann aber reine Arrays, auf die ihr die meisten der hier besprochenen Funktionen nicht anwenden könnt. Insofern ist der Begriff „Character Array“ doppeldeutig, aber ich möchte nicht jedes Mal „nullterminiert“ hinzufügen.

Als String-Objekte bezeichne ich die Instanzen der Klasse String, die ihr mit String myString = "...." oder String myString = String (....) erzeugt. Meistens werden String-Objekte einfach nur Strings genannt. Diese Bezeichnung ist aber noch uneindeutiger als die eben besprochenen Character Arrays. Mit dem Begriff „String“ könnte einfach die englische Übersetzung des Oberbegriffs Zeichenkette gemeint sein. Oder schaut mal hier in die Arduino-Referenz, wo Character Arrays unter der Überschrift „string“ behandelt werden.

Um die Verwirrung zu komplettieren, gibt es in der C++ Standardbibliothek die Klasse std::string, die wiederum der eigens für die Arduinowelt entwickelten Klasse String ähnelt. Deswegen bleibe ich in diesem Beitrag konsequent bei dem längeren Begriff String-Objekt. Das ist sperrig, aber hoffentlich eindeutig.

Länge und Größe – oder: Unterschied zwischen Character Arrays und String-Objekten

Im ersten Sketch definieren wir ein Character Array und ein String-Objekt. Dann lassen wir uns deren Länge, also die Anzahl der Zeichen, und die Größe, also den Speicherbedarf in Bytes anzeigen.

Die Länge eines Character Arrays ermittelt ihr mit:

size_t length = strlen( myCharArray );

Die Länge eines String-Objektes ermittelt ihr mit:

size_t length = myString.length();

Die Größe eines Character Arrays oder eines String-Objekts erhaltet ihr wie folgt:

size_t sizeInBytes = sizeof( myCharArrayOrMyString );

Für die weniger Erfahrenen: Das Schöne am Datentyp size_t ist, dass ihr euch keine Gedanken um die tatsächliche Größe des Rückgabewertes machen müsst. Ihr seid unabhängig vom verwendeten Mikrocontroller auf der sicheren Seite.

Nun erst einmal zum Sketch:

void setup(){
    Serial.begin(9600);
    char myCharArray[] = "Hello, I am a character array";
    // char myCharArray[50] = "Hello, I am a character array";
    // char myCharArray[] = {'H','e','l','l','o',',',' ','I',' ','a','m',' ','a',' ','c','h','a','r','a','c','t','e','r',' ','a','r','r','a','y','0'};
    String myString = "Hi all, I am a string";

    Serial.println(myCharArray);
    Serial.print("Size of myCharArray: ");
    size_t charSize = sizeof(myCharArray);
    Serial.println(charSize);

    Serial.print("Length of myCharArray: ");
    size_t charLength = strlen(myCharArray); 
    Serial.println(charLength);
    Serial.println();

    Serial.println(myString);
    Serial.print("Size of myString: ");
    size_t stringSize = sizeof(myString);
    Serial.println(stringSize);
    
    Serial.print("Length of myString: ");
    size_t stringLength = myString.length();
    Serial.println(stringLength);
 }

 void loop(){}

 

Und hier die Ausgabe, die ich auf einem Arduino Nano (ATmega328P) erhalten habe:

Ausgabe length_vs_size.ino
Ausgabe length_vs_size.ino

Betrachtung des Character Arrays

Wie ihr seht, beträgt die Länge des Character Arrays 29. Wenn ihr nachzählt, ist das auch tatsächlich die Anzahl der Zeichen. Die Größe beträgt jedoch 30 Byte, da am Ende der unsichtbare Null-Terminator '\0' angehängt wird. Der Null-Terminator signalisiert das Ende des Character Arrays.

Wenn ihr die Kommentierung von Zeile 3 und Zeile 4 tauscht, bleibt die Länge des Character Arrays erhalten, die Größe ändert sich jedoch auf 50 Byte.

Die alternative Definition von myCharArray in Zeile 5 ist etwas unhandlich, macht aber deutlicher, dass es sich um ein Array handelt. Bei dieser Schreibweise müsst ihr den Null-Terminator selbst anhängen. Oder ihr legt die Länge explizit fest und lasst dabei Platz für Null-Terminator, also z.B.:

char charArray[6] = {'H','e','l','l','o'};

Betrachtung des String-Objektes

Beim String-Objekt wird es merkwürdig. Die Länge gibt korrekt die Anzahl der Zeichen an. Aber wieso ist die Größe des String-Objekts 6 Byte? Nun, das liegt daran, dass das String-Objekt in zwei Teilen gespeichert wird. Die eigentliche Zeichenkette (der String-Buffer) liegt im Heap. Die 6 Byte im Stack sind nur eine Art Inhaltsverzeichnis. Dort werden der Zeiger zur Heap-Adresse des String-Buffers, die Kapazität des String-Objektes und seine Länge gespeichert.

Bei Verwendung anderer Mikrocontroller können die Dinge anders sein. Beispielsweise wird bei einem ESP32 nur dann ein String-Buffer im Heap abgelegt, wenn die Länge der Zeichenkette 13 Zeichen überschreitet. Die Größe des String-Objekts im Stack beträgt grundsätzlich 16 Byte.

Wenn ihr mehr über String-Objekte in Stack und Heap wissen möchtet, dann könntet ihr meinen Beitrag über das SRAM-Management lesen. Dort gehe ich auch auf die berüchtigte Heap-Fragmentierung ein, die oft als Argument gegen die Verwendung von String-Objekten angeführt wird.

Character Arrays und String-Objekte verketten

Character Arrays verketten

So verkettet ihr zwei Character Arrays:

strcat( char* destination, char* source );

Das Character Array „source“ wird an das Array „destination“ angehängt. Dabei müsst ihr selbst dafür Sorge tragen, dass „destination“ groß genug ist, um „source“ aufzunehmen. Ist das nicht der Fall, überschreibt ihr anderen Speicherplatz, in dem ggf. andere Variablen stehen. Die Folgen sind unvorhersehbar. Das Dumme ist, es keine Fehlermeldung beim Kompilieren gibt.

Alternativ hängt ihr nur die ersten x Zeichen von „source“ an „destination“:

strncat( char* destination, char* source, size_t x);

Was soll die Schreibweise „char* charArray“?

Der Name eines Arrays repräsentiert den Zeiger auf den Anfang des Arrays. Wenn ihr also ein Character Array wie folgt definiert:  char myCharArray[] = "....", dann ist myCharArray ein Zeiger vom Typ char. Bei einer Übergabe an eine Funktion wird immer nur der Zeiger übergeben und nicht das ganze Array. Auf die Übergabe von Character Arrays und String-Objekten komme ich später noch im Detail zu sprechen.

String-Objekte verketten

String-Objekte verkettet ihr einfach mit dem „+“-Operator:

String destination += String source;

Das unbestreitbar Angenehme an den String-Objekten ist, dass ihr euch um ihre Länge (vermeintlich) keine Gedanken machen müsst. Die Verlängerung von String-Objekten kann aber zur Heap-Fragmentierung führen. Um das zu verhindern, könnt ihr für das String-Objekt Speicherplatz reservieren:

myString.reserve( unigned int size );

Dabei ist size die erwartete maximale Länge eures String-Objektes.

Hier ein kleiner Beispielsketch zum Thema Verkettung:

void setup(){
   Serial.begin(9600);
   char myCharArray1[13] = "Hello";
   char myCharArray2[] = " World!";

   strcat(myCharArray1, myCharArray2);
   // strncat(myCharArray1, myCharArray2, 5);  // add the first 5 chars
   Serial.println(myCharArray1);

   String myString1 = "Hello";
   String myString2 = " Earth!";

   myString1 += myString2;

   Serial.println(myString1);
}

void loop(){}

Die nicht überraschende Ausgabe ist:

Ausgabe concatenate.ino
Ausgabe concatenate.ino

Duplizieren

Die Kopie eines ein Character Arrays erzeugt ihr so:

char* copy = strdup( const char* original );

Mit String-Objekten ist es noch einfacher:

String copy = String original;

Hier ein Beispiel:

void setup(){
    Serial.begin(9600);
    
    char myCharArray[] = "Hello, I am a character array";
    Serial.print("Original: ");
    Serial.println(myCharArray);

    char* myCharArrayCopy = strdup(myCharArray);
    Serial.print("Copy: ");
    Serial.println(myCharArrayCopy);
    Serial.println();

    String myString = "Hi all, I am a string";
    Serial.print("Original: ");
    Serial.println(myString);

    String myStringCopy = myString;
    Serial.print("Copy: ");
    Serial.println(myStringCopy);    
}

void loop(){}

Und hier, der Vollständigkeit halber, die Ausgabe:

Ausgabe duplicate.ino
Ausgabe duplicate.ino

In Groß- oder Kleinbuchstaben konvertieren

Auch für die Umwandlung von Zeichenketten in Groß- oder Kleinbuchstaben gibt es sehr bequeme Funktionen.

Ein Character Array verwandelt ihr folgendermaßen:

strupr( char* charArray );  // Großbuchstaben

strlwr( char* charArray );  // Kleinbuchstaben

Für String-Objekte verwendet ihr die folgenden Funktionen:

myString.toUpperCase();  bzw.  myString.toLowerCase();

void setup(){
    Serial.begin(9600);
    
    char myCharArray[] = "Hello, I am a character array";
    Serial.print("Original: ");
    Serial.println(myCharArray);
   
    Serial.print("Upper Case: ");
    strupr(myCharArray);
    Serial.println(myCharArray);
   
    Serial.print("Lower Case: ");
    strlwr(myCharArray);
    Serial.println(myCharArray);
    
    Serial.println();

    String myString = "Hi all, I am a string";
    Serial.print("Original: ");
    Serial.println(myString);

    Serial.print("Upper Case: ");
    myString.toUpperCase();
    Serial.println(myString);
    
    Serial.print("Lower Case: ");
    myString.toLowerCase();
    Serial.println(myString);
}

void loop(){}

 

Die Ausgabe lautet:

Ausgabe upper_lower_case.ino
Ausgabe upper_lower_case.ino

Ein Zeichen nach Index auslesen

Wie ihr ein Zeichen an einer bestimmten Stelle des Character Arrays auslest, brauche ich wohl eigentlich nicht zu erklären:

char charAtIndexI = charArray[i];

Für String-Objekte gibt es zu diesem Zweck eine eigene Funktion:

char charAtIndexI = myString.charAt(i);

Dazu folgendes Beispiel:

void setup(){
    Serial.begin(9600);
    char myCharArray[] = "Hello, I am a character array!";

    Serial.print("Tenth character: ");
    char charAtIndex10 = myCharArray[10];
    Serial.println(charAtIndex10);

    String myString = "Hi all, I am a string";
    
    Serial.print("Tenth character: ");
    charAtIndex10 = myString.charAt(10);
    Serial.println(charAtIndex10);
 }

 void loop(){}

Der Sketch erzeugt die Ausgabe:

Ausgabe read_char_at_index_i.ino
Ausgabe read_char_at_index_i.ino

Zeichen ersetzen und löschen

In einem Character Array ersetzt ihr Zeichen, indem ihr über den Index auf sie zugreift:

charArray[index] = 'x'; // replace x by any other character

Um Zeichen zu löschen, müsst ihr ein wenig „basteln“. Ihr lasst die verbleibenden Zeichen hinter den zu löschenden Zeichen nach vorn wandern und hängt dann den Nullterminator dran.

Für String-Objekte gibt es zum Ersetzen und Löschen von Zeichen eigene Funktionen:

myString.setCharAt[index] = 'x';

myString.remove( index, length );

Beispielsketch:

void setup(){
    Serial.begin(9600);
    
    char myCharArray[] = "abcdefghijklm";
    Serial.print("Original: ");
    Serial.println(myCharArray);

    myCharArray[3] = 'c';
    Serial.print("\"d\" replaced by \"c\": ");
    Serial.println(myCharArray);

    remove(myCharArray, sizeof(myCharArray), 3, 5);
    Serial.print("Removed 5 chars from index 3: ");
    Serial.println(myCharArray);
    Serial.println();

    String myString = "nopqrstuvwxyz";
    Serial.print("Original: ");
    Serial.println(myString);
    
    myString.setCharAt(3, 'p');
    Serial.print("\"q\" replaced by \"p\": ");
    Serial.println(myString);

    myString.remove(3, 5);
    Serial.print("Removed 5 chars from index 3: ");
    Serial.println(myString);    
}

void loop(){}

void remove(char *cArr, size_t size, size_t index, size_t len){  // remove for char arrays
    for(size_t i = index; i < (size-len); i++){
        cArr[i] = cArr[i+len];
    }
    cArr[size-len] = '\0';
}

 

Die Ausgabe ergibt, wie erwartet:

Ausgabe replace_remove.ino
Ausgabe replace_remove.ino

Ausschnitte (Substrings)

Ausschnitte von Character Arrays ab einem bestimmten Index

Um Ausschnitte aus Zeichenketten zu erzeugen, wird es bei den Character Arrays wieder ein kleines bisschen unhandlicher. Wir nutzen dazu die memcpy() Funktion. In allgemeiner Form lautet die Funktion:

void* memcpy ( void* destination, const void* source, size_t num );

„Übersetzt“ heißt das: memcpy() kopiert num Bytes, beginnend an der Adresse, auf die der Zeiger source zeigt, und kopiert sie an die Adresse, auf die der Zeiger destination zeigt. Durch Verwendung des Datentyps void* ist memcpy() universell einsetzbar. Es ist völlig egal, auf welchen Datentyp die übergebenen Zeiger zeigen. Um einen Ausschnitt zu erzeugen, nutzen wir die Funktion folgendermaßen:

memcpy( subCharArray, charArray + index, length );

Wenn ihr die Länge von subCharArray festlegt, dann vergesst dabei nicht den Null-Terminator. Unabhängig davon müsst ihr den Null-Terminator auch tatsächlich anhängen. Oder, noch einfacher, intialisiert ihr subCharArray als {'\0'}. Dadurch wird das Array komplett mit Nullzeichen beschrieben.

Ausschnitte von Character Arrays ab einem bestimmten Zeichen

Falls ihr einen Ausschnitt erzeugen wollt, der an einem bestimmten Zeichen des Original-Arrays beginnt und von dort alle Zeichen bis zum Ende erfasst, dann kommt die Funktion strchr() in Betracht. Die folgende Code-Zeile definiert den Zeiger subCharArray, der auf die Adresse zeigt, an der das Zeichen charToBeFound das erste Mal in charArray auftaucht:

char* subCharArray = strchr(const char* charArray, char charToBeFound);

Alternativ verwendet ihr strrchr() (also mit zwei „r“). Die Funktion liefert einen Zeiger, der auf die Stelle zeigt, an der das gesuchte Zeichen das letzte Mal vorkommt.

Ihr müsst euch dabei nur bewusst sein, dass subCharArray immer noch Teil von charArray ist. Änderungen an subCharArray ändern auch charArray. Wollt ihr das nicht, dann solltet ihr mit strdup() oder über memcpy() eine Kopie erzeugen.

Ausschnitte von String-Objekten

Für String-Objekte gibt es die überaus bequeme substring() Funktion. Mit ihr erzeugt ihr einen Ausschnitt, der bei einem bestimmten Index beginnt. Übergebt ihr nur den Index, werden alle restlichen Zeichen übernommen. Alternativ könnt ihr einen zweiten Index übergeben, vor dem der Ausschnitt endet.

String subString = myString.subtring( index );

String subString = myString.substring( startIndex, endIndex ); // substring from startIndex to endIndex - 1

Hier nun das Beispiel:

void setup(){
    Serial.begin(9600);
    
    char myCharArray[] = "abcdefghijklm";
    Serial.print("Original: ");
    Serial.println(myCharArray);

    char mySubCharArray[6] = {'\0'};
    Serial.print("Substring from index 3 to 7: ");
    memcpy(mySubCharArray, myCharArray+3, 5);
    Serial.println(mySubCharArray);

    char mySubCharArray_2[6] = {'\0'};
    Serial.print("Substring 3-7, alternative: ");
    subArray(mySubCharArray_2, myCharArray, 3, 5);
    Serial.println(mySubCharArray_2);
    
    char *mySubCharArray_3 = strchr(myCharArray, 'i');  // strchr(): first occurance / strrchr(): finds last occurance
    Serial.print("Substring, beginning at \"i\": ");
    Serial.println(mySubCharArray_3);
    Serial.println();
       
    String myString = "nopqrstuvwxyz";
    Serial.print("Original: ");
    Serial.println(myString);
    
    String mySubString = myString.substring(3, 8);
    Serial.print("Substring from index 3 to 7: ");
    Serial.println(mySubString);   

    String mySubString_2 = myString.substring(7);
    Serial.print("Substring from index 7: ");
    Serial.println(mySubString_2);   
}

void loop(){}

void subArray(char* destination, char* source, size_t index, size_t len){
    memcpy(destination, source + index, len);
}

 

Der Sketch erzeugt folgende Ausgabe:

Ausgabe substrings.ino
Ausgabe substrings.ino

Die Zeichenkette komplett nach bestimmten Zeichen zerlegen

Manchmal möchte man Zeichenketten nach bestimmten Zeichen zerlegen. Das könnte beispielsweise notwendig sein, wenn eine Zeichenkette mehrere Messwerte enthält, die durch ein bestimmtes Zeichen voneinander getrennt sind (Trennzeichen = Delimiter).

Hier gibt es ausnahmsweise für die Character Arrays die einfachere Lösung:

char* chunk = strtok( char* charArray, char delimiter );

chunk ist ein Character Array, das alle Zeichen bis zum ersten Auftreten des Trennzeichens umfasst. Das Angenehme, aber vielleicht auch Verwirrende ist, dass ihr für das Herauslösen des nächsten Teilstücks den Null-Zeiger übergebt:

char* nextChunk = strtok( NULL, char delimiter );

Das funktioniert, weil sich strtok() die Adresse der Zeichenkette und den Index des letzten Trennzeichens merkt. Wenn kein Trennzeichen mehr gefunden wird, liefert strtok() den Null-Zeiger zurück.

String-Objekte könnt ihr nach Trennzeichen zerlegen, indem ihr mit indexOf() nach den Trennzeichen sucht und mit substring() die Teilstücke erzeugt.

Hier das Beispiel dazu:

void setup(){
    Serial.begin(9600);
    char myCharArray[] = "Hello, I am a character array";
    String myString = "Hi all, I am a string";

    char *cArrChunk = strtok(myCharArray, " ");
    while(cArrChunk != NULL){
        Serial.println(cArrChunk);
        cArrChunk = strtok(NULL, " ");
    }
    Serial.print("myCharArray: ");
    Serial.println(myCharArray);
    Serial.println();

    String stringChunk = myString.substring(0, myString.indexOf(" "));
    while(myString != ""){
        Serial.println(stringChunk);
        myString.remove(0, stringChunk.length()+1);
        stringChunk = myString.substring(0, myString.indexOf(" "));
    }
    Serial.print("myString: ");
    Serial.println(myString);
}

void loop(){}

Hier die Ausgabe:

Ausgabe mince_char_arrays_and_strings.ino
Ausgabe mince_char_arrays_and_strings.ino

Wie ihr seht, werden myCharArray und myString durch die Prozedur geändert. Wenn ihr das nicht wollt, müsst ihr mit Kopien arbeiten.

Zeichen suchen

Wir haben zwar schon zuvor nach bestimmten Zeichen gesucht, aber ich komme hier noch einmal darauf zurück, da wir einige Aspekte bislang nicht betrachtet haben. Dazu gehört die Ermittlung des Index eines bestimmten Zeichens in einem Character Array. Auch fehlt noch die Ermittlung des Index für das letzte Vorkommen eines Zeichens in einem String-Objekt. 

Zeichen in einem Character Array finden

Wie wir gesehen haben, liefert uns strchr() den Zeiger zu der Adresse, an der das gesuchte Zeichen das erste Mal steht. strrchr() liefert den Zeiger zur letzten Adresse, an der das gesuchte Zeichen steht. Um aus diesen Zeigern den Index zu errechnen, subtrahiert ihr einfach den Zeiger zum Character Array:

indexOfCharX = strchr( char* charArray, char charX ) - charArray;

Alternativ könnt ihr natürlich jedes Zeichen eures Arrays mit dem gesuchten Zeichen vergleichen. Das ist aber weniger elegant.

Wenn ihr nicht nach einem Zeichen, sondern nach einer Zeichenkette innerhalb der Zeichenkette sucht, dann steht euch dafür die Funktion strstr() zur Verfügung:

char* ptrToFirstString = strstr( char* charArray, char* searchString );

Wenn strchr(), strrchr() und strstr() ihre Suche erfolglos beenden, geben sie den Null-Zeiger zurück.

Zeichen in einem String-Objekt finden

Wie wir gesehen hatten, finden wir den Index für das erste Vorkommen eines Zeichens in einem String-Objekt mithilfe von indexOf(). Die Funktion nimmt aber auch Character Arrays und Strings, also zusammengesetzte Zeichen entgegen:

int indexOfFirstStringX = myString.indexOf( char* / String stringX);

Den Index für das letzte Vorkommen eines oder mehrerer Zeichen findet Ihr mit:

int indexOfLastStringX = myString.lastIndexOf( char* / String stringX );

Dann könnt ihr noch den Index für das erste oder das letzte Vorkommen eines Zeichens oder einer Zeichenkette ab einem bestimmten Index ermitteln:

int indexOfFirstStringXAfterIndexY = myString.indexOf( char* / String stringX, size_t indexY);

int indexOfLastStringXAfterIndexY = myString.lastIndexOf( char* / String stringX, size_t indexY);

Wenn indexOf() und lastIndexOf() ihre Suche erfolglos beenden, geben sie -1 zurück.

Beispielsketch:

void setup(){
    Serial.begin(9600);
    char myCharArray[] = "I am a character array!";

    Serial.print("Rest of myCharArray beginning at first \"a\": ");
    char *ptrToFirstA = strchr(myCharArray, 'a');
    if (ptrToFirstA != NULL){
        Serial.println(ptrToFirstA);
    }
    else{
        Serial.println("Not found");
    }
    
    Serial.print("Rest of myCharArray beginning at first \"x\": ");
    char *ptrToFirstX = strchr(myCharArray, 'x');
    if (ptrToFirstX != NULL){
        Serial.println(ptrToFirstX);
    }
    else{
        Serial.println("Not found");
    }

    Serial.print("Rest of myCharArray beginning at last \"a\": ");
    char *ptrToLastA = strrchr(myCharArray, 'a');
    Serial.println(ptrToLastA);
    
    Serial.print("Last \"a\" is at position: ");
    int indexOfLastA = findLastIndexOf(myCharArray, sizeof(myCharArray), 'a'); 
    Serial.println(indexOfLastA);

    Serial.print("Alternative Method: Last \"a\" position: ");
    indexOfLastA = ptrToLastA - myCharArray;
    Serial.println(indexOfLastA);

    Serial.print("First \"ar\" position: ");
    char *ptrToFirstAR = strstr(myCharArray, "ar");
    int indexOfFirstAR = ptrToFirstAR - myCharArray;
    Serial.println(indexOfFirstAR);
}

void loop(){}

size_t findLastIndexOf(char *cArr, size_t size, char searchChar){
    size_t lastPos = -1;
    for(size_t i=0; i<size; i++){
        if(cArr[i] == searchChar){
            lastPos = i;
        }
    }
    return lastPos;
}
void setup(){
    Serial.begin(9600);
    String myString = "I am not a character array but a String";

    Serial.print("First index of \"t\": ");
    int indexOfFirstT = myString.indexOf("t");
    Serial.println(indexOfFirstT);

    Serial.print("First index of \"t\" after pos. 7: ");
    int indexOfFirstTAfterPos7 = myString.indexOf("t", 8);
    Serial.println(indexOfFirstTAfterPos7);

    Serial.print("First index of \"x\": ");
    size_t indexOfFirstX = myString.indexOf("x");
    if((int)indexOfFirstX != -1){
        Serial.println(indexOfFirstX);
    }
    else{
        Serial.println("Not found");
    }

    Serial.print("Last index of \"t\": ");
    int indexOfLastT = myString.lastIndexOf("t");
    Serial.println(indexOfLastT);

    Serial.print("Last index of \"ar\": ");
    char searchString[] = "ar";
    //String searchString = "ar";
    int indexOfLastAR = myString.lastIndexOf(searchString);
    Serial.println(indexOfLastAR);
}

void loop(){}

 

Und hier die Ausgaben:

Ausgabe find_characters.ino, li: Character Array, re: String
Ausgabe find_characters.ino, li: Character Array, re: String-Objekt

Vergleichen

Character Arrays vergleichen

Zwei Character Arrays vergleicht ihr so:

int result = strcmp( char* charArray1, char* charArray2 );

Wenn ihr nur die ersten n Zeichen der beiden Arrays vergleichen wollt, dann übergebt ihr n als dritten Parameter:

int result = strcmp( char* charArray_1, char* charArray_2, size_t n );

Die strcmp() Funktion vergleicht die Zeichenketten bei Index 0 beginnend Zeichen für Zeichen. Das tut sie, bis sie auf das erste unterschiedliche Zeichen trifft oder das Ende erreicht bzw. beim letzten der zu vergleichenden Zeichen angekommen ist. Trifft die Funktion auf ein unterschiedliches Zeichen, dann entspricht result der Differenz des ASCII-Codes dieser Zeichen. Ist result negativ, dann hat charArray_1 das Zeichen mit dem niedrigeren ASCII-Code. Ist result positiv, dann ist es umgekehrt.  Wenn result 0 ist, dann sind die beiden Character Arrays identisch, genauer gesagt: sie haben eine identische Zeichenfolge.

Falls ihr den Vergleich unabhängig von Groß- oder Kleinbuchstaben durchführen wollt, dann könnt ihr die Character Arrays mit strupr() oder strlwr() vor dem Vergleich umwandeln. Allerdings werden sie dadurch permanent geändert, ggf. solltet ihr also Kopien anlegen.

String-Objekte vergleichen

Um zu prüfen, ob zwei String-Objekte gleich sind, verwendet ihr die Funktion equals():

bool result = myString.equals( myString_2 );

Falls ihr nur Teile von myString_2 und myString auf Identität prüfen wollt, müsst ihr equals() mit substring() kombinieren. Groß- und Kleinschreibung ignoriert ihr mit der Variante equalsIgnoreCase():

bool result = myString.equalsInoreCase( myString_2 );

Alternativ zu equals() verwendet ihr die Funktion compareTo():

int result = myString.compareTo( myString_2 );

Genau wie strcmp() vergleicht compareTo() die beiden String-Objekte Zeichen für Zeichen. Sind zwei Zeichen unterschiedlich, dann gibt die Funktion die Differenz der ASCII-Codes dieser Zeichen zurück.

void setup(){
    Serial.begin(9600);
    char myCharArray1[] = "I am a character array!";
    char myCharArray2[] = "I am also a character array!";
    char myCharArray3[] = "I am a character array!";
    char myCharArray4[] = "I aM A cHaRaCteR aRrAy!";
    char myCharArray5[] = "abcd";
    char myCharArray6[] = "aacd";
  

    Serial.print("Comparing 1, 2: ");
    int result = strcmp(myCharArray1, myCharArray2);
    Serial.println(result);

    Serial.print("Comparing 1, 2 (first 6 characters): ");
    result = strncmp(myCharArray1, myCharArray2, 6);
    Serial.println(result);

    Serial.print("Comparing 1, 3: ");
    result = strcmp(myCharArray1, myCharArray3);
    Serial.println(result);

    Serial.print("Comparing 1, 4: ");
    result = strcmp(myCharArray1, myCharArray4);
    Serial.println(result);

    Serial.print("Comparing 1, 4, non-case-sensitive: ");
    result = strcmp(strlwr(myCharArray1), strlwr(myCharArray3)); // changes the char arrays permanently!
    Serial.println(result); 
    Serial.println(myCharArray1);

    Serial.print("Comparing 5, 6: ");
    result = strcmp(myCharArray5, myCharArray6);
    Serial.println(result);

    Serial.print("Comparing 6, 5: ");
    result = strcmp(myCharArray6, myCharArray5);
    Serial.println(result);
 }

void loop(){}
void setup(){
   Serial.begin(9600);
   String myString1 = "I am a String!";
   String myString2 = "I am also a String!";
   String myString3 = "I am a String!";
   String myString4 = "I aM A sTrInG!";
   String myString5 = "abcd";
   String myString6 = "aacd";
 

   Serial.print("Comparing 1, 2: ");
   int result = (myString1.equals(myString2));
   Serial.println(result);

   Serial.print("Comparing 1, 2 (first 6 characters): ");
   result = (myString1.substring(0,6)).equals(myString2.substring(0,6));
   Serial.println(result);
  
   Serial.print("Comparing 1, 3: ");
   result = myString1.equals(myString3);
   Serial.println(result);

   Serial.print("Comparing 1, 4: ");
   result = myString1.equals(myString4);
   Serial.println(result);

   Serial.print("Comparing 1, 4, non-case-sensitive: ");
   result = myString1.equalsIgnoreCase(myString4);
   Serial.println(result);
   Serial.println(myString1);

   Serial.print("Comparing 5, 6: ");
   result = myString5.compareTo(myString6);
   Serial.println(result);

   Serial.print("Comparing 6, 5: ");
   result = myString6.compareTo(myString5);
   Serial.println(result);
}

void loop(){}

 

Die Ausgaben dazu sind:

li: Ausgabe compare_char_arrays.ino, re: compare_strings.ino
li: Ausgabe compare_char_arrays.ino, re: compare_strings.ino

Verwenden des „==“ Operators

Wenn ihr den Operator == auf zwei Character Arrays anwendet, dann vergleicht er zwei Zeiger. Ein true  erhaltet ihr nur, wenn die beiden Zeiger auf dieselbe Adresse zeigen, d. h. wenn die beiden Character Arrays nicht nur gleich sind, sondern wirklich identisch.  

Bei String-Objekten hingegen prüft der Operator ==, ob der „Inhalt“, also die String-Buffer gleich sind. equals() und == sind gleichwertig. 

Dazu wieder ein Beispiel:

void setup(){
    Serial.begin(9600);
    char myCharArray1[] = "I am a character array!";
    char myCharArray2[] = "I am a character array!";
    char* myCharArray3 = myCharArray2;
    String myString1 = "I am a String!";
    String myString2 = "I am a String!";

    Serial.print("Char array comparison 1: ");
    if(myCharArray1 == myCharArray2){   // Compares the pointers, not the arrays
        Serial.println("Identical");
    }
    else{
        Serial.println("Different");
    }

    Serial.print("Char array comparison 2: ");
    if(myCharArray2 == myCharArray3){   // Compares the pointers, not the arrays
        Serial.println("Identical");
    }
    else{
        Serial.println("Different");
    }

    Serial.print("Char array comparison 3: ");
    if(myCharArray1 == "I am a character array!"){  // Don't do this!!!!
        Serial.println("Identical");
    }
    else{
        Serial.println("Different");
    }

    Serial.print("String comparison 1: ");
    if(myString1 == myString2){
        Serial.println("Equal");
    }
    else{
        Serial.println("Different");
    }

    Serial.print("String comparison 2: ");
    if(myString1 == "I am a String!"){
        Serial.println("Equal");
    }
    else{
        Serial.println("Different");
    }
 }

 void loop(){}

 

Hier die Ausgabe zu compare_2.ino:

Ausgabe compare_2.ino
Ausgabe compare_2.ino

Character Arrays aus String-Objekten erzeugen und umgekehrt

Ein String-Objekt aus einem Character Array zu erzeugen, ist denkbar einfach:

String myString = ( const char* myCharArray );

Es gibt zwei Funktionen, mit denen ihr Character Arrays aus String-Objekten erzeugen könnt:

myString.toCharArray( char* myCharArray, size_t length );

const char* myCharArray = myString.c_str();

Mit der Funktion toCharArray() kopiert ihr den String-Buffer, also die Zeichenkette, in ein Character Array, das ihr zuvor noch definieren müsst. Der erste an toCharArray() zu übergebende Parameter ist das Character Array, der zweite ist die Länge des Character Arrays. Der Null-Terminator wird automatisch zugefügt. Das müsst ihr bei der Länge berücksichtigen.

Die Funktion c_str() macht etwas komplett anderes, indem sie den Zeiger zum String-Buffer im Heap (also dort, wo der Inhalt des String-Objektes gespeichert ist) zurückgibt. Mit anderen Worten: Hier wird nichts Neues erschaffen, sondern ihr greift direkt auf den String zu. Insofern ist es sinnvoll, dass der Rückgabewert eine Konstante ist. Das schützt euch davor, unabsichtlich euer String-Objekt zu ändern. Wenn ihr das Character Array verändern wollt, dann erstellt ihr am besten mit memcpy() eine Kopie (siehe Beispielsketch). Dann jedoch könntet ihr genauso gut toCharArray() verwenden.

Wozu brauchen wir dann c_str()? Die Funktion bietet sich immer dann an, wenn ihr den Inhalt des Strings unverändert an eine andere Funktion weitergeben wollt. Die Funktion c_str() ist wesentlich schneller und spart SRAM.

void setup(){
    Serial.begin(9600);
    char myCharArray[] = "I was born as a character array!";

    String stringFromCharArray = String(myCharArray);
    Serial.println(stringFromCharArray);

    String myString = "I was born as a String";

    char charArrayFromString[myString.length() + 1];
    myString.toCharArray(charArrayFromString, myString.length() + 1);
    Serial.println(charArrayFromString);

    char charArrayFromString2[myString.length() + 1];
    memcpy(charArrayFromString2, myString.c_str(), myString.length() + 1);
    Serial.println(charArrayFromString2);
}

void loop(){}

Und hier die wenig überraschende Ausgabe:

Ausgabe char_array_to_string_and_string_to_char_array.ino
Ausgabe char_array_to_string_and_string_to_char_array.ino

Zahlen in Character Arrays und String-Objekte konvertieren

Dezimalzahlen in Character Arrays

Fließkommazahlen (double und float) und Ganzzahlen (int, long) lassen sich mithilfe der Funktion dtostrf() sehr bequem in Character Arrays umwandeln:

dtostrf( double myDouble, signed char min_width, unsigned char precision, char* myCharArray );

Dabei ist:

  • myDouble: Die Ausgangszahl. Alternative Datentypen: float, int, long.
  • min_witdth: Minimale Beite der Ausgabe. Ist eure umgewandelte Zahl kürzer, wird sie mit Leerzeichen aufgefüllt und entsprechend nach rechts verschoben.
  • precision: Die Zahl der Nachkommastellen. Wenn ihr Ganzzahlen umwandelt, dann sind die Nachkommastellen Nullen.
  • myCharArray: Das „Ziel“ Character Array.

Ihr müsst selber dafür sorgen, dass das aufnehmende Character Array groß genug ist.

Andere Zahlenformate in Character Arrays umwandeln

Wenn ihr eine Zahl im Hexadezimalsystem in ein Character Array konvertieren wollt, empfiehlt sich die Funktion sprintf(). Ich zeige im Beispielsketch unten, wie das geht. Da man über sprintf() allein schon einen ganzen Beitrag schreiben könnte (was ich irgendwann auch einmal machen werde), verzichte ich auf weitere Erklärungen. Eine Dokumentation der sprintf() Funktion findet ihr hier, eine Dokumentation der zugrundeliegenden printf() Funktion gibt es hier.

Für die Darstellung von Zahlen im Binärsystem als Character Array gibt es keine vorgefertigte Funktion. Für den Beispielsketch habe ich die Funktion toBinary() „gebastelt“. Sie geht die Bits der umzuwandelnden Zahl von oben nach unten durch und prüft, welche der Bits gesetzt sind. Alle voranstehenden Nullen werden abgeschnitten. Irgendwie ist es da mit mir durchgegangen und ich habe noch eine zweite Variante geschrieben. Dabei wird die zu wandelnde Zahl bitweise nach links verschoben und immer wieder geprüft, ob das oberste Bit („msb“) gesetzt ist.

Zahlen in String-Objekte umwandeln

Die Umwandlung von Zahlen in String-Objekte ist wesentlich einfacher. Für Ganzzahlen gilt:

String myString = String( int myInt, base );  // base: BIN, HEX, DEC (optional)

Anstelle von Integerwerten könnt ihr auch Zahlen vom Typ byte, long oder die unsigned Varianten übergeben.

Für Fließkommazahlen verwendet ihr (hier am Beispiel float):

String myString = String( float myFloat, precision ); // prescision: optional

Der Parameter precision gibt die Zahl der Nachkommastellen an. Lasst ihr diesen Parameter weg, werden zwei Nachkommastellen in das String-Objekt übernommen.

Hier ein paar Beispiele:

void setup(){
    Serial.begin(9600);
    int myInt = 4242;
    float myFloat = -123.4567;
    
    Serial.println("Converted to character arrays: ");
    char myCharArray_1[10];
    dtostrf(myFloat, 8, 4, myCharArray_1);
    Serial.print("myFloat, version 1: ");
    Serial.println(myCharArray_1);

    char myCharArray_2[14];
    dtostrf(myFloat, 13, 2, myCharArray_2);
    Serial.print("myFloat, version 2: ");
    Serial.println(myCharArray_2);

    char myCharArray_3[14];
    dtostrf(myInt, 13, 0, myCharArray_3);
    Serial.print("myInt             : ");
    Serial.println(myCharArray_3);

    char myCharArray_4[17];
    toBinary(myInt, myCharArray_4);
    Serial.print("myInt, binary     : ");
    Serial.println(myCharArray_4);
   
    char myCharArray_5[5];
    sprintf(myCharArray_5,"%X", myInt);
    Serial.print("myInt, hexadecimal: ");
    Serial.println(myCharArray_5);
    Serial.println();


    Serial.println("Converted to strings: ");
    String myString_1 = String(myFloat,4);
    Serial.print("myFloat, version 1: ");
    Serial.println(myString_1);

    String myString_2 = "     " + String(myFloat);
    Serial.print("myFloat, version 2: ");
    Serial.println(myString_2);

    String myString_3 = "        " + String(myInt);
    Serial.print("myInt             : ");
    Serial.println(myString_3);

    String myString_4 = String(myInt, BIN);
    Serial.print("myInt, binary     : ");
    Serial.println(myString_4);

    String myString_5 = String(myInt, HEX);
    Serial.print("myInt, hexadecimal: ");
    Serial.println(myString_5);
}

void loop(){}

void toBinary(uint16_t intVal, char *cArr){
    int bitNo = 15;
    int index = 0;
    
    int isBitSet = intVal & bit(bitNo);
    while(!isBitSet && (bitNo > 0)){
        bitNo--;
        isBitSet = intVal & bit(bitNo);
    }
    while(bitNo>=0){
        if(isBitSet){
            cArr[index] = '1';
        }
        else {
            cArr[index] = '0';
        }
        index++;
        bitNo--;
        isBitSet = intVal & bit(bitNo);
    }
    cArr[index] = '\0';  
}

/* Alternative way */
// void toBinary(uint16_t intVal, char *cArr){
//     int bitNo = 15;
//     int index = 0;
    
//     int msb = intVal & 0x8000;
//     while(!msb && (bitNo > 0)){
//         bitNo--;
//         intVal = intVal << 1;
//         msb = intVal & 0x8000;  
//     }
//     while(bitNo>=0){
//         msb = intVal & 0x8000;
//         intVal = intVal << 1;
//         if(msb){
//             cArr[index] = '1';
//         }
//         else {
//             cArr[index] = '0';
//         }
//         index++;
//         bitNo--;
//     }
//     cArr[index] = '\0';  
// }

 

Und hier die Ausgabe:

Ausgabe floats_integers_to_char_arrays_and_strings.ino
Ausgabe floats_integers_to_char_arrays_and_strings.ino

Character Arrays und String-Objekte in Zahlen umwandeln

Die Funktionen atof(), atoi() und atol() erzeugen Fließkommazahlen, Integer oder Long Integer aus Character Arrays:

double myDouble = atof ( char* charArray ); // Alternative: float

int myInt = atoi( char* charArray );

long myLong = atol( char* charArray );

Für die Erzeugung von Zahlen aus String-Objekten stehen euch folgende Optionen zur Verfügung:

float myFloat = myString.toFloat();

int myInt = myString.toInt();  // Alternative: long myLong = myString.toInt();

Ein paar Beispiele:

void setup(){
    Serial.begin(9600);
    char myCharArray_1[] = "123.4567";
    char myCharArray_2[] = "4242";
   
    float myFloatfromCharArray = atof(myCharArray_1);
    Serial.print("Float from char array: ");
    Serial.println(myFloatfromCharArray, 4);

    int myIntfromCharArray = atoi(myCharArray_2);
    Serial.print("Integer from char array: ");
    Serial.println(myIntfromCharArray);
    Serial.println();

    String myString_1 = "123.4567";
    String myString_2 = "4242";
    String myString_3 = "42.42blabla";

    float myFloatfromString = myString_1.toFloat(); // alternative: toDouble()
    Serial.print("Float from String: ");
    Serial.println(myFloatfromString);

    int myIntFromString = myString_2.toInt();
    Serial.print("Integer from String: ");
    Serial.println(myIntFromString);

    float myFloatfromString_2 = myString_3.toFloat();
    Serial.print("Float from String, 2: ");
    Serial.println(myFloatfromString_2);    
}

void loop(){}

 

Und hier die Ausgabe:

Ausgabe char_arrays_and_strings_to_integers_and_floats.ino
Ausgabe char_arrays_and_strings_to_integers_and_floats.ino

Character Arrays und String-Objekte über Serial einlesen

Wenn ihr Daten über die serielle Schnittstelle empfangt, könnt ihr diese in Character Arrays oder String-Objekte umwandeln. Ihr habt die Wahl alle übermittelten Zeichen einzulesen oder dies nur bis zu einem bestimmten Zeichen zu tun.

Zunächst zu den Character Arrays:

size_t length = Serial.readBytesUntil( char terminator, char* charArray, int maxLength );

size_t length = Serial.readBytes( char* myCharArray, int maxLength );

Für String-Objekte habt ihr die folgenden Optionen:

String myString = Serial.readStringUntil( char terminator );

String myString = Serial.readString();

Beispielsketch:

void setup(){
    Serial.begin(9600);
}

void loop(){
    char myCharArray[20] = {'\0'};

    if(Serial.available()){
        Serial.readBytesUntil('\n', myCharArray, sizeof(myCharArray));
        Serial.println(myCharArray);
    }
}

/* alternative loop() */
// void loop(){
//     char myCharArray[11] = {'\0'}; 

//     if(Serial.available()){
//         size_t length = Serial.readBytes(myCharArray, 10);
//         Serial.println(myCharArray);
//         Serial.println(length);
//     }
// }
void setup(){
    Serial.begin(9600);
}

void loop(){
    if(Serial.available()){
        String myString = Serial.readStringUntil('\n');
        Serial.println(myString);
    }
}

/* Alternative that also captures \n and \r */
// void loop(){
//     if(Serial.available()){
//         String myString = Serial.readString();
//         Serial.println(myString);
//     }
// }

Ladet den bzw. die Beispielsketche hoch und sendet Nachrichten über den seriellen Monitor. Ihr könntet dabei die Länge der Nachrichten variieren und schauen, was ihr als Ausgabe erhaltet.

Character Arrays und String-Objekte an Funktionen übergeben

Wenn ihr ein Character Array in einer Funktion bearbeiten wollt, dann übergebt ihr der Funktion den Namen des Character Arrays, der den Zeiger auf das nullte Element darstellt. Ihr bearbeitet damit in der Funktion das Original. Um eine lokale Kopie zu bearbeiten, müsstet ihr diese erst einmal erzeugen.

Falls ihr die Größe des Character Arrays in der Funktion benötigt, müsst ihr diese extra mit übergeben. Die Länge hingegen ist kein Problem, da sie über den Null-Terminator ermittelt werden kann.

Wenn ihr einen originalen String in einer Funktion bearbeiten möchtet, dann müsst ihr dem Parameter in der entgegennehmenden Funktion den Referenzoperator „&“ voranstellen.

Das schauen wir uns im folgenden Beispielsketch an.

void setup(){
    Serial.begin(9600);
    char myCharArray[20] = "I am a char array";
    String myString = "I am a string";

    Serial.println(myCharArray);
    modifyCharArray(myCharArray, sizeof(myCharArray));
    Serial.println(myCharArray);
    Serial.println();

    Serial.println(myString);
    modifyString(myString);
    Serial.println(myString);
}

/* alternative loop() */
void loop(){}

void modifyCharArray(char* cArr, size_t size){
    Serial.print("Size of cArr: ");
    Serial.println(sizeof(cArr));
    Serial.print("Size of myCharArray: ");
    Serial.println(size);
    cArr[strlen(cArr)] = '!';
    cArr[strlen(cArr) + 1] = '\0';
    Serial.println(cArr);
}

void modifyString(String &str){
    str += "!";
    Serial.println(str);
}

 

Wie ihr an der Ausgabe seht, haben wir in den Funktionen die Originale des Character Arrays und des Strings verändert.

Ausgabe passing_char_arrays_and_strings_to_functions.ino
Ausgabe passing_char_arrays_and_strings_to_functions.ino

Danksagung

Der Hintergrund meines Beitragsbildes stammt von Bild von Daan Lenaerts auf Pixabay. Der von mir modifizierte Vordergrund stammt von OpenClipart-Vectors, ebenfalls auf Pixabay.

2 thoughts on “Character Arrays vs. Strings

  1. Thank you for one of the best and clearly written articles on Strings and character arrays. You explain all the available ’string‘ manipulation function very well. I was always confused by strok, your article has clarified its use considerably.

Schreibe einen Kommentar

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