FAQ

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Ein Verzeichnis von im Forum oft gestellten und immer wieder beantworteten Fragen und den zugehörigen Antworten:

Wie kann ich Zahlen auf LCD/UART ausgeben?

Die Bibliothek, die Sie benutzen, stellt nur eine Funktion zur Verfügung, mit der man einen String ausgeben kann: <c> lcd_out( "Hallo Welt" ); </c> Um also eine Zahl auszugeben, muss von dieser Zahl zunächst ihre String-Repräsentierung ermittelt werden. Dazu gibt es mehrere Möglichkeiten:

sprintf()

<c>

 char Buffer[20];
 sprintf( Buffer, "%d", i );
 lcd_out( Buffer );

</c> Diese Methode funktioniert auch bei long oder float Werten. Unbedingt beachtet werden muss allerdings, dass die Typkennzeichnungen im Format-String mit den tatsächlichen Typen der auszugebenden Werten übereinstimmt. Dann hat man allerdings dieselben Möglichkeiten zur Formatierung wie bei printf().

itoa()

itoa() ist keine C-Standardfunktion (wohl aber ihre Umkehrung atoi() ). Auf manchen Compilern heisst diese Funktion dann folgerichtig _itoa(), wobei der führende _ eben anzeigt, dass es sich um eine Erweiterung des C-Standards handelt. <c>

 char Buffer[20];
 itoa( i, Buffer, 10 );
 lcd_out( Buffer );

</c>

Wie funktioniert String-Verarbeitung in C?

In C gibt es, anders als in anderen Programmiersprachen, keinen eigenen String-Datentyp. Als Ersatz dafür werden Character-Arrays benutzt, in denen die einzelnen Character (=Zeichen) gespeichert werden. Allerdings gibt es noch einen Zusatz: Das letzte Zeichen eines Strings ist immer ein '\0'-Zeichen, dass das Ende des Strings markiert. Schlieslich kann ja das Array wesentlich grö�?er sein, als der in ihm gespeicherte String und irgendwie müssen ja diverse Funktionen das tatsächliche Ende eines Strings erkennen können.

Möchte mal also die Zeichenkette "Hello World" in einem String speichern, so wird dafür ein Array mit mindestens der Länge 12 benötigt. 11 für die Zeichen die "Hello World" bilden, plus eine zusätzliche Position für das abschliesende '\0'-Zeichen.

Einige Stringfunktionen

Arrays sind in C keine vollwertigen Datentypen, z.B. ist es nicht möglich ein Array in einem Rutsch ein anderes Array zuzuweisen oder 2 Arrays miteinander zu vergleichen. Genau das möchte man aber in der Stringverarbeitung häufig, sodass es dafür Standardfunktionen gibt, die allesammt im Headerfile "string.h" zusammengefasst sind und deren Namen alle mit str... beginnen. Allen diesen Funktionen gemeinsam ist, dass sie sich nicht um die korrekte Bereitstellung von Arrays kümmern, sondern davon ausgehen, dass dies vom Programmierer korrekt erledigt wird.

strcpy( char* dest, const char* src )

Kopieren eines Strings von der Speicherfläche auf die src zeigt, zur Speicherfläche, auf die dest zeigt.

strcat( char* dest, const char* src )

Anhängen eines Strings an einen bestehenden String

strcmp( const char* str1, const char* str2 )

Vergleichen 2-er Strings. Das Ergebnis ist 0, wenn die beiden Strings identisch sind.

strlen( const char* str )

Die Länge eines Strings feststellen. Die Länge beinhaltet nicht das abschliessende '\0' Zeichen.

Beispiele

<c>

  1. include "string.h"

int main() {

 char Meldung[14];
 strcpy( Meldung, "Hello World" );

} </c> Mittels der Deklaration <c>

 char Meldung[14];

</c> wird ein Array bereitgestellt, welches maximal 14 Zeichen aufnehmen kann. Hello World verbraucht für die lesbaren Zeichen 11 Array-Positionen, dazu noch das obligatorische abschliessende '\0' Zeichen, macht in Summe 12 Positionen. Eine Deklaration von 14 Zeichen ist also mehr als minimal notwendig wäre. Das macht aber nichts, da durch das abschliessende '\0' Zeichen immer feststellbar ist, an welcher Stelle der tatsächliche String zu Ende ist. Die restlichen 2 Array-Positionen sind zur Zeit halt einfach unbenutzt. strcpy() kopiert den 2.ten angegebenen String an die Position auf die sein erstes Argument zeigt. Im obigen Beispiel zeigt das 1.te Argument auf den Beginn von Meldung, also auf das Array. Folgerichtig wird der String "Hello World" in das Array Meldung umkopiert. Nach Ausführung der strcpy() Funktion enthält also Meldung den Inhalt:

    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    | H | e | l | l | o |   | W | o | r | l | d | \0|   |   |
    +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    

Möchte man an diesen Text jetzt noch etwas anfügen, z.B. ein "?", so würde das so aussehen:

<c>

  1. include "string.h"

int main() {

 char Meldung[14];
 strcpy( Meldung, "Hello World" );
 strcat( Meldung, "?" );

} </c>

Man beachte: auch wenn hier scheinbar nur ein einzelnes Zeichen angehängt wird, so handelt es sich doch um einen String. Strings werden in C immer mit einem " eingeleitet und abgeschlossen. Im Gegensatz zu einzelnen Zeichen, die in einfache ' eingefasst werden. "?" ist also nicht dasselbe wie '?'! Das erste ist ein String (der mit dem obligatorischen '\0' Zeichen insgesammt aus 2 Zeichen besteht), während letzteres ein einzelnes Zeichen darstellt!

Da Meldung maximal 14 Zeichen umfassen kann, der Text "Hello World?" aber nur aus 13 Zeichen besteht, funktioniert Obiges auch ohne Probleme. Der Array-Inhalt sieht dann wie folgt aus:

    +---+---+---+---+---+---+---+---+---+---+---+---+---+
    | H | e | l | l |   | W | o | r | l | d | ? | \0|   |
    +---+---+---+---+---+---+---+---+---+---+---+---+---+

Ein schwerwiegender Fehler wäre es, wenn der komplette String nach dem strcat() aus mehr als 14 Zeichen (das '\0'-Zeichen nicht vergessen!) bestehen würde.

<c>

  1. include "string.h"

int main() {

 char Meldung[14];
 strcpy( Meldung, "Hello World" );
 strcat( Meldung, " von mir" );

} </c>

würde also das Array überlaufen lassen.

strlen() liefert die Länge eines Strings. Die Längenangabe beinhaltet dabei nicht das abschliessende '\0' Zeichen:

<c>

  1. include "string.h"

int main() {

 char Meldung[14];
 int  Len;
 
 strcpy( Meldung, "Hello World" );
 Len = strlen( Meldung );
 
 /* Hier enthaelt Len den Wert 11 */
 Len = strlen( "Hallo Welt" );
 /* Hier enthält Len den Wert 10 */

} </c>

strcmp() schlussendlich vergleicht 2 Strings auf Gleichheit. Der Rückgabewert spiegelt dabei die Position des ersten Unterschiedes in den beiden Strings wieder. Folgerichtig sagt ein Wert von 0 daher aus, dass die beiden Strings identisch sind:

<c>

  1. include "string.h"


int main() {

 char Meldung1[14];
 char Meldung2[14];
 strcpy( Meldung1, "Hello World" );
 strcpy( Meldung2, "Hallo Welt" );
 if( strcmp( Meldung1, Meldung2 ) == 0 ) {
   /* die Strings sind identisch */
 }
 else {
   /* die Strings sind nicht identisch */
 }
 
 if( strcmp( Meldung2, "Hallo Welt" ) == 0 ) {
   /* Meldung2 war "Hallo Welt" */
 }

}

</c>

Es gibt noch weitere String-Funktionen, dafür sei aber auf die Verwendung der zum Compiler gehörenden Dokumentation bzw. auf einführende Literatur zum Thema 'Programmieren in C' verwiesen.

Funktionszeiger

Um Menüs oder ähnliche Dinge aufzubauen ist es oft praktisch ein Array von Funktionszeigern zu definieren. Der Aufruf einer Funktion kann dann indirekt über eine Variable erfolgen, wobei die Variable die Adresse der aufzurufenden Funktion enthält.

Um mit Funktionszeigern zu arbeiten ist es praktisch sinnvoll sich einen typedef für den Typ des Funktionszeigers zu definieren. Ein typedef definiert einen neuen (kürzeren) Namen für einen Datentyp. Und wie wir sehen werden ist der Datentyp eines Funktionszeigers ganz schön umfangreich in der Schreibweise.

typedef

Einen typedef zu definieren ist eigentlich ganz einfach: Man schreibt die Deklaration so, als ob man eine Variable definieren würde. Vor das ganze Konstrukt kommt das Schlüsselwort typedef. Es bewirkt, dass der Name an der Position des Variablennamens zum Namen für den neuen Datentyp wird, der dann in weiterer Folge wie jeder andere Datentyp benutzt werden kann.

Wir wollen einen Funktionszeiger auf eine Funktion definieren, die keine Argumente entgegen nimmt und auch nichts liefert. Also Funktionen nach dem Muster:

<c> void foo( void ) { } </c>

Ein entsprechender typedef würde zB so aussehen:

<c> typedef void (*VoidFnct)( void ); </c>

Das vereinbart einen neuen Datentyp VoidFnct. Dieser ist ein Funktionszeiger auf Funktionen, die keine Argumente nehmen und auch nichts zurückliefern.

<c> typedef int (*IntFnct)( void ); typedef int (*IntFnct2)( int ); </c>

IntFnct ist ein Zeiger auf eine Funktion, die keine Argumente nimmt aber einen int zurückliefert. IntFnct2 hingegen ist ein Zeiger auf eine Funktion, die einen int als Argument nimmt und einen int zurückliefert. Andere Argumenttypen bzw. Rückgabetypen folgen dem gleichen Muster. Wichtig ist, dass sowohl Argumenttypen als auch Rückgabetypen Teil der Signatur eines Funktionszeigers ist. Es ist also nicht möglich einen Funktionszeigertyp zu vereinbaren, der auf beliebige Funktionen mit beliebigen Argumenttypen bzw. Rückgabetypen verweist. Hier muss man ev. auf einen cast ausweichen. Generell ist das aber meist keine gute Idee.

Funktionszeigertabellen

Mit einem typedef ist es nun ein leichtes ein Array von Funktionszeigern zu vereinbaren:

<c> typedef void (*VoidFnct)( void );

VoidFnct MeineFunktionen[5]; </c>

Dies vereinbart MeineFunktionen als ein Array von Funktionszeigern, wobei jeder Funktionszeiger auf eine Funktion vom Typ void-void zeigt.

<c> typedef void (*VoidFnct)( void );

void Funct1() {

 printf( "Dies ist Funktion 1\n" );

}

void Funct2() {

 printf( "Dies ist Funktion 2\n" );

}

VoidFnct MeineFunktionen[] = { Funct1, Funct2 };

int main() {

 //
 // ruft die Funktion auf, deren Adresse in MeineFunktionen[0]
 // steht. In diesem Fall wäre das Funct1()
 //
 MeineFunktionen[0]();
 //
 // und jetzt die MeineFunktionen[1]
 //
 MeineFunktionen[1]();
 //
 // jetzt wird MeineFunktionen[0] umgeleitet auf Funct2()
 // Achtung: Auf der rechten Seite wird kein () angegeben.
 // Ansonsten würde ja die Funktione Funct2 aufgerufen. Wir
 // wollen aber nur ihre Speicheradresse haben! Daher unterbleibt
 // das ()
 //
 MeineFunktionen[0] = Funct2;
 //
 // welche Funktion wird jetzt aufgerufen?
 //
 MeineFunktionen[0]();
 // Richtig: Die Funktion, deren Adresse in MeineFunktionen[0]
 //          steht. Und das ist jetzt Funct2.

} </c>

Besonders bei Menüs ist es oft hilfreich, sich eine Struktur bestehend aus dem Menütext und der aufzurufenden Funktion zu definieren

<c> typedef void (*VoidFnct)( void );

struct MenuEntry {

 char     Text[20];
 VoidFnct Function;

}; </c>

Ein Menü ist dann einfach ein Array aus derartigen Strukturelementen

<c>

void HandleEdit() { }

void HandleCopy() { }

void HandlePaste() { }

struct MenuEntry MainMenu[] = {

{ "Edit", HandleEdit },
{ "Copy", HandleCopy },
{ "Paste", HandlePaste }

};

void DoMenu( int NrEntries, struct MenuEntry[] Menu ) {

 int i;
 int Auswahl;
 
 do {
   //
   // Das Menue anzeigen. Für jeden Menuepunkt noch eine Zahl
   // davor stellen, damit der Benutzer auch was zum Eingeben hat
   //
   for( i = 0; i < NrEntries; ++i )
     printf( "%d) %s\n", i + 1, Menu[i].Text ); 
   printf( "9) Exit\n\n" );
   // 
   // Jetzt die Benutzereingabe abwarten ...
   //
   printf( "Ihre Eingabe: " );
   scanf( "%d", &Auswahl );
   //
   // ... und auswerten
   //
   if( Auswahl == 9 )
     return;
   Auswahl = Auswahl - 1;
   if( Auswahl < 0 || Auswahl > NrEntries )
     printf( "Ungültige Eingabe\n" );
   else
     // Die Eingabe war gültig. Zugehörige Funktion aufrufen
     Menu[Auswahl].Function();
 }

}

</c>

Auf einem Mikrocontroller wird man natürlich die Ein/Ausgabe nicht über printf/scanf abwickeln. Hier geht es aber um das Prinzip der Funktionszeiger und wie man mit ihnen arbeitet, daher wurde die allereinfachste Art der Benutzerinteraktion gewählt. Gegebenenfalls muss printf und scanf durch die Möglichkeiten auf dem konkreten System ersetzt werden. Auch ist die Art und Weise wie das Menü präsentiert bzw. die Benutzereingabe ausgewertet wird, nicht der Weisheit letzter Schluss. Anstatt den Benutzer Zahlen eingeben zu lassen, könnte man auch einen Auswahl-Balken vom Benutzer mit 2 Tasten über die Menüeinträge bewegen lassen. Oder einen Drehencoder nehmen, ...