C-Programmierung:
Die wichtigsten C-Befehle auf einen Blick
Dies C-Cheatsheet zeigt die Syntax der Programmiersprache C mit Beispielen, gefolgt von jeweils einem Quiz / Selbsttest, um das Verständnis zu prüfen. Zum schnellen Nachschlagen können Sie die C-Kurzreferenz einsetzen, eine tabellarische und sortierbare Übersicht der wichtigsten C-Befehle, oder zum Ausdrucken die Kurzreferenz als PDF-Datei. Als Entwicklungsumgebung verwenden wir Visual Studio Community Edition, diese unterstützt C-Programmierung als Teilmenge der C++-Entwicklung und man kann relativ unkompliziert von C zu C++ übergehen. Für das Testen kleiner Codefragmente kann der Online-Compiler OnlineGDB verwendet werden, dort im Dropdown rechts oben als Programmiersprache C auswählen.
Vorab: Infos rund um die C-Programmierung
C ist eine prozedurale Programmiersprache, d.h. Programme werden mit Hilfe von Funktionen / Prozeduren strukturiert, die Daten (Variablen, Arrays, Strukturen) verarbeiten. Mittels Sequenzen von Einzelanweisungen (Variablen deklarieren / zuweisen, Eingabe, Ausgabe, ...), Verzweigungen, Schleifen und Funktionen kann man Lösungsalgorithmen für eine Vielzahl von Aufgabenstellungen in C implementieren.
Mit C werden vor allem Konsolenprogramme geschrieben, Betriebssysteme und Mikrocontroller programmiert. Die Stärke der Programmiersprache C liegt darin, dass sie hardware-nah ist, mit Hilfe von Zeigervariablen kann man z.B. direkt auf Adressbereiche zugreifen. Für die Entwicklung grafischer Benutzeroberflächen bieten objektorientierte Sprachen wie C++, Java oder C# eine bessere Unterstützung.
Der aktuell gültige C-Standard (C17) wird von den meisten C-Compilern unterstützt, insbesondere auch von Microsoft Visual Studio 2019. Die C-Syntax hat sich seit dem älteren C99-Standard nicht wesentlich geändert. Ein C-Programm, das mit einem bestimmten Compiler und Betriebssystem entwickelt wurde, wird meist auch auf einem anderen laufen. Ggf. sind einige Anpassungen erforderlich, z.B. Verwendung von scanf in GCC vs. scanf_s in Visual Studio.
Vor der Programmierung ist es hilfreich, den Algorithmus bzw. Programmablauf mit Hilfe eines Flussdiagramms oder eines Struktogramms zu visualisieren. Flussdiagramme und Struktogramme sind Diagramme mit standardisierten Elementen, deren Erstellung durch verschiedene Tools unterstützt wird. Schöne Flussdiagramme können z.B. mit yEd Graph Editor erstellt werden.
Übersicht
- 1. Erste Schritte mit C
- 2. Variablen, Konstanten, Operatoren
- 3. Ausgabe und Eingabe von der Konsole
- 4. Bedingte Verzweigungen und Fallunterscheidungen
- 5. Schleifen (while, do while, for)
- 6. Funktionen: benannte Codeblöcke mit Eingabe- und Rückgabewerten
- 7. Arrays: Datenstrukturen für mehrere Elemente desselben Datentyps
- 8. Adressen und Zeiger
- 9. Strukturen: Datenstrukturen für Elemente unterschiedlichen Datentyps
- 10. C-Standardbibliotheken
stdio.h math.h string.h stdlib.h limits.h - 11. Fortgeschrittene Themen
Funktionszeiger Makro-Programmierung
1. Erste Schritte mit C
C-Quellcode Header-Datei main-Funktion include define Kommentar Compiler IDETop |
1-1 Neue Quellcode-Datei anlegen
Ein minimales C-Programm besteht aus einer einzelnen Quellcode-Datei mit der Endung *.c, zum Beispiel myprog.c, die die Anweisungen des Programms sowie Kommentare enthält. Größere C-Programme können noch weitere benutzerdefinierte Header-Dateien (Endung *.h) und Quellcode-Dateien (Endung *.c) enthalten.
Mit Hilfe der #include-Direktiven werden benötigte Programmbibliotheken über ihre Headerdateien importiert, danach können die darin enthaltenen Funktionen verwendet werden. #define-Direktiven werden ebenfalls noch vor die main an den Anfang des Programms gestellt, mit ihrer Hilfe werden Konstante und sogenannte Makros definiert, z.B. #define PI 3.14159. Wichtig: Direktiven sind keine Anweisungen, sie werden nicht mit Semikolon beendet. Jedes C-Programm enthält genau eine main-Funktion, diese ist der Einstiegspunkt des Programms. Die Anweisungen des Programms werden in die main()-Funktion geschrieben, ggf. auch in weitere selbstdefinierte Funktionen. In C werden Anweisungen mit einem Semikolon beendet, Ausnahme sind if-Anweisungen und Schleifen. Mehrzeilige Kommentare werden mit /* und */ umrahmt, einzeilige Kommentare mit // eingeleitet.
/* myprog.c */
// TODO: Hier include-Direktiven einfügen
#include <stdio.h>
#include <stdlib.h>
// TODO: Hier define-Direktiven einfügen
#define PI 3.14159
int main(void){
// TODO: Fügen Sie Ihre Anweisungen hier ein!
printf("Hallo!\n"); // Ausgabe des Textes Hallo!
}
1-2 Programm erstellen und ausführen
Um aus C-Quellcode ein ausführbares Programm zu erstellen, benötigt man einen C-Compiler. Integrierte Entwicklungsumgebungen (engl. Integrated Development Environment, IDE) für C-Programmierung sind z.B. Eclipse IDE for C/C++, NetBeans, Code::Blocks oder Microsofts Visual Studio, diese enthalten einen C-Compiler und unterstützen darüber hinaus die Programmierung mit Syntaxhighlighting, Fehlersuche etc.
Alle Entwicklungsumgebungen haben ähnliche Default-Fensterkonfigurationen, die den Entwicklungsprozess unterstützen. Man erstellt zunächst ein Projekt, dem man anschließend Quelldateien, Headerdateien und andere Ressourcen hinzufügt. Das Projekt gruppiert die zusammengehörenden Dateien. Die IDE stellt meist auch Projektvorlagen bereit, sowie einen Wizard, der durch die Projekterstellung führt.
Der Screenshot zeigt die Anordnung der Fenster in Visual Studio Community Edition: Toolbar, Projekte, Source Code Editor, Ausgabefenster und Fehlerliste.
2. Variablen und Konstante
Variable Konstante Datentyp Typumwandlung Operatoren Modulo-Operator sizeof enumTop |
2-1 Variablen deklarieren
Variable sind benannte Speicherplätze, in denen die Daten des Programms gespeichert werden, z.B. a, b, c, text. Den Wert einer Variablen kann man verändern und überschreiben.
Syntax
Bei der Deklaration einer Variablen wird mit einem Datentyp (int, long long, float, double, char, ...) festgelegt, welche Art von Werten in dieser Variablen gespeichert werden kann. Dem Datentyp kann auch das Schlüsselwort unsigned vorangestellt werden, dies bedeutet, dass vorzeichenlose Zahlenwerte verwendet werden sollen. Deklariert man mehrere Variablen desselben Datentyps, werden sie mit Komma getrennt.
datentyp1 varname1; datentyp2 varname2; datentyp varname1, varname2;
Beispiel
Variablen deklarieren
Der sizeof -Operator gibt die Größe des Speicherbedarfs einer Variablen / eines Objektes in Bytes an.int a = 0, b = 0; // ganze Zahlen
unsigned int c = 100; // vorzeichenlose ganze Zahl
float x = 0.0; double y = 0.0; // Fließkommazahlen
char z = 'A'; // Zeichen
char *text = "Hallo!"; // Zeichenkette
Beispiel
sizeof-Operator
In diesem Beispiel finden wir den Speicherverbrauch der Variablen a und x heraus und stellen fest: Variablen des Datentyps int werden mit 4 Byte, Variablen des Datentyps double werden mit 8 Byte gespeichert.
// Ganze Zahl
int a = -100;
// Fließkommazahl mit doppelter Genauigkeit
double y = 0.999;
// groesse_von_a = 4 Byte = 32 Bit
int groesse_von_a = sizeof(a);
// groesse_von_y = 8 Byte = 64 Bit
int groesse_von_y = sizeof(y);
printf("%d %d\n", groesse_von_a, groesse_von_y);
Der sizeof -Operator wird z.B. bei der dynamischen Speicherallokation verwendet, in Kombination mit den Funktionen malloc und calloc.
2-2 Konstante deklarieren
Im Unterschied zu Variablen bleibt der Wert einer Konstanten immer gleich. Konstanten kann man mit der Präprozessor-Direktive #define als Makro oder mit dem Schlüsselwort const deklarieren. #define-Makros werden ohne Zuweisung (=) und ohne Semikolon am Ende definiert!
Syntax
#include <stdio.h> #define name wert // (1) int main(void){ const datentyp name; // (2) }
Beispiel
Konstante mit define oder const
Hier wird die Konstante PI einmal als Makro mit #define und einmal mit const deklariert. Ihr Wert kann durch Zuweisung nachher nicht mehr verändert werden.
#define PI 3.14159 // Konstante mit #define
int main(void){
const double PI2 = 3.14159; // Konstante mit const
PI = 3.14; // FEHLER!
PI2 = 3.141; // FEHLER!
}
In C können auch Aufzählungen verwendet werden, um Listen ganzzahliger Konstanten zu verwalten, dies mit Hilfe des Schlüsselworts enum.
Syntax
Eine Aufzählung wird durch das Schlüsselwort enum eingeleitet, gefolgt von einem selbstdefinierten Aufzählung-Namen und einer Liste von Elementen, die in geschweifte Klammern gesetzt werden. Den Elementen einer Aufzählung werden defaultmäßig ganzzahlige Werte zugewiesen, die bei 0 beginnen, es können jedoch auch eigene Werte vergeben werden.
enum ENUM_NAME {elem_1, ..., elem_n}; // (1) int main(void){ enum ENUM_NAME var = elem_i; // (2) }
Beispiel
Konstanten mit enum deklarieren
Die Konstante antw hat den Datentyp enum ANTWORT und kann nur einen der festgelegten Werte: ja, nein, vielleicht annehmen.
enum ANTWORT { ja=10, nein=20, vielleicht=15 };
int main(void) {
enum ANTWORT antw = vielleicht;
printf("Antwort = %d\n", antw); // Ausgabe: 15
}
2-3 Zuweisung und Typumwandlung
Einer zuvor deklarierten Variablen kann man mit Hilfe des = Zeichens Werte zuweisen. Dabei ist der Datentyp zu beachten, einer int-Variablen weist man ganzzahlige Werte zu, einer float-Variablen Kommazahlen. Hat der zugewiesene Wert einen anderen Datentyp, wird er implizit in den Datentyp der Variablen umgewandelt. Typumwandlung kann auch explizit erfolgen, indem man den Namen eines Datentyps vor den Namen des zugewiesenen Wertes schreibt. Die Zuweisung double x = (double)10; bewirkt, dass 10 als Fließkommazahl mit doppelter Genauigkeit gespeichert wird.
Syntax
variablen_name = variablen_wert; variablen_name = (datentyp)variablen_wert;
Beispiel
Zuweisung
int a = 0; double b = 0.0; char z = ' ';
// Zuweisung des Wertes 10 an die Variable a
a = 10;
// Zuweisung des Wertes 20.5 an die Variable b
b = 20.5;
// Zuweisung des Zeichens a an die Variable z
z = 'a';
// Nachkommastellen werden abgeschnitten, a = 3
a = 3.99;
Beispiel
Typumwandlung
int a = 10; float b = 1.2345;
double c = 1.9;
// Implizite Typumwandlung a = 10
a = 10.2;
// Explizite Typumwandlung, a = 1
a = (int)b;
// Explizite Typumwandlung, a = 1
a = (int)c;
// Explizite Typumwandlung, b = 20.00(...)
b = (double)20;
2-4 Berechnungen
Berechnungen werden durchgeführt, indem Variablen über Operatoren (+, -, *, /, ...) zu Ausdrücken
verknüpft werden.
C verfügt über Operatoren für abkürzende Zuweisung (+=, *= etc.), Inkrementierung und Dekrementierung (++, --),
Vergleichsoperatoren, deren Ergebnis WAHR oder FALSCH ist (==, !=, >, <, >=, <= ) und logische Operatoren (&&, ||, !),
deren Ergebnis WAHR oder FALSCH ist.
Ein wichtiger Operator ist der Modulo-Operator %:
a % b gibt den Rest der Ganzzahl-Division a / b zurück.
Die Auswertung eines Ausdrucks erfolgt entsprechend einer festgelegten Priorität der Operatoren, die durch Klammern beeinflusst werden kann:
a * b + c ist wie (a * b) + c, jedoch anders als a * (b + c).
Beispiel
Operatoren in C
int a = 25, b = 3, c = 0;
// Inkrementierung - erhöhe Wert um 1
a += 1; // a = 26
// Abkürzende Zuweisung
b *= 2; // b = 6
// Modulo-Operator: Rest der Teilung
c = 17 % 5; // c = 2
// Fließkommadivision: Typumwandlung erforderlich!
double res1 = (double) a / b; // res1 = 4.33
// Ganzzahldivision
int res2 = a / b; // res2 = 4
// Ausgabe: 26, 6, 4.33, 4
printf("%d, %d, %.2f, %d\n", a, b, res1, res2);
// Vergleichs-Operatoren: ==, !=, <, >, <=, >=
int cond1 = (a == (b + 20)); // cond1 = 1, d.h. wahr
int cond2 = (a != b); // cond2 = 1 : 26 ist ungleich 6
// Logische Operatoren: &&, ||, !
int cond = cond1 && cond2; // cond = 0
printf("%d, %d, %d\n", cond1, cond2, cond); // Ausgabe: 1, 0, 0
Beispiel
Berechne Flächeninhalt des Dreiecks mit Seiten a, b, c
#include<math.h>
int main(void){
double a = 10, b = 20, c = 20;
double s = (a + b + c ) / 2;
double F = sqrt(s * (s-a) * (s-b) * (s-c));
}
3. Ausgabe und Eingabe
printf scanf Formatzeichen %d, %f, %lf SteuerzeichenTop |
3-1 Die printf-Funktion
In C erfolgt die Ausgabe auf die Konsole mit Hilfe der printf -Funktion. Die Werte von Variablen können mit Hilfe von Formatzeichen in eine formatierende Zeichenkette eingefügt werden. Zu jedem Datentyp gehören passende Formatzeichen, z.B.: %d oder %i für int (ganze Zahlen), %f für float (Fließkommazahlen mit einfacher Genauigkeit), %lf für double (Fließkommazahlen mit doppelter Genauigkeit). Weitere Steuerzeichen für die Erzeugung einer formatierten Ausgabe sind: \n - erzeugt einen Zeilenumbruch, \t - erzeugt einen Tabulator. Um die Steuerzeichen selber auszugeben, verwendet man \\ - gibt den Rückschrägstrich aus, %% - gibt ein Prozentzeichen aus, \" - gibt Anführungsstriche aus. In der printf-Funktion können lange Zeichenkette mit Hilfe eines einzelnen Rückschrägstrichs auf mehrere Zeilen verteilt werden.
Syntax
Ausgabe einer Zeichenkette
printf(zeichenkette);
Formatierte Ausgabe mit Platzhaltern für Variablen
printf(formatierende_zeichenkette, variablen_liste);
Beispiel
Formatierte Ausgaben
int a = 0; double b = 3.33;
char z = 'a';
printf("====================\n");
printf("a = %d, b = %5.2f\n", a, b, z);
printf("Ihr Zeichen:\n%c\n", z);
char *text1 = "Eins", *text2 = "Zwei";
printf("Ihre Texte:\n%14s%14s\n", text1, text2);
3-2 Die scanf-Funktion
In C werden Werte von der Konsole mit Hilfe der Funktion scanf eingelesen. Die Funktion scanf erhält als ersten Parameter eine formatierende Zeichenkette, z.B. "%d %lf", gefolgt von einer Liste von Variablen, deren Anzahl und Datentyp zu den Formatbeschreibern passen muss. Jeder Variablen muss ein &-Zeichen vorangestellt werden, das die Adresse der Variablen bezeichnet. In Visual Studio wird scanf_s anstelle von scanf verwendet!
Das Einlesen von Zeichenketten ist mit scanf zwar möglich, jedoch sollte man dafür besser die Funktion fgets verwenden, wie im Beispiel unten.
Syntax
scanf("formatzeichen", &variablenname); // In Visual Studio: scanf_s("formatzeichen", &variablenname);
Beispiel
Hier werden drei Variablen eingelesen. Vor jedem Einlesen wird eine Eingabe-Aufforderung für den Anwender ausgegeben. Wichtig: in scanf sollten die Formatzeichen %d, %f, %lf, %c ohne weitere Texte oder Steuerzeichen verwendet werden.
int a = 0; double b = 0.0; char z = ' ';
printf("Eingabe a: "); scanf("%d", &a);
printf("Eingabe b: "); scanf("%lf", &b);
printf("Eingabe z: "); scanf("%c", &z);
// Eingabe eines Zeichens in Visual Studio
printf("Eingabe z: ");scanf_s("%c", &z, 1);
printf("Ihre Eingaben sind: %d %lf %c\n", a, b, z);
// Eingabe einer Zeichenkette mit fgets
char text[100];
printf("Text eingeben: ");
// Maximal 20 Zeichen von der Standardeingabe lesen
fgets(text, 20, stdin);
4. Verzweigungen und Fallunterscheidungen
if else else if Vergleichs-Operatoren (==, !=, ...) Logik-Operatoren (&&, ||, !) switch-caseTop |
4-1 if-else-Anweisung
Eine Verzweigung ist eine Anweisung für die Ablaufsteuerung, die festlegt, welcher von zwei (oder mehr) Anweisungsblöcken, abhängig von einer (oder mehreren) Bedingungen, ausgeführt wird. Sie wird in C, wie in fast allen Programmiersprachen, mit der if- bzw. if-else-Anweisung abgebildet. Die Bedingungen werden mit Hilfe von Vergleichs-Operatoren (==, !=, >, <, >=, <=) und logischen Operatoren (&&, ||, !) formuliert. Um z.B. zu überprüfen, ob ein Jahr ein Schaltjahr ist, lautet die Bedingung: ((jahr % 4 == 0) && !(jahr % 100 == 0)) || (jahr % 400 == 0). Damit wird überprüft, ob jahr teilbar durch 4 ist und nicht teilbar durch 100, oder teilbar durch 400.
Syntax
if-else-Anweisung
Die Wirkung der if-else-Anweisung ist wie folgt:
* Wenn ( if ) die Bedingung bedingung_1 wahr ist, werden die Anweisungen anweisungen_1 ausgeführt,
* sonst wenn ( else if ) die Bedingung bedingung_2 wahr ist, werden die Anweisungen anweisungen_2 ausgeführt,
* sonst ( else ) der Anweisungsblock anweisungen_default.
Es kann keinen, einen oder mehrere else-if-Teile geben. Der optionale else-Zweig greift dann, wenn es keine Übereinstimmung gibt. Die geschweiften Klammern werden dann benötigt, wenn es mehrere Anweisungen in einem Block gibt.
if (bedingung_1) {
anweisungen_1
}
else if (bedingung_2){
anweisungen_2
}
else {
anweisungen_default
}
Beispiel
Berechne Zinssatz abhängig von Betrag
Für betrag = 20000 wird der Zinssatz 2.0 berechnet: Die erste Bedingung 20000 > 50000 wird als FALSCH ausgewertet, also wird auch die nächste Bedingung 20000 > 10000 geprüft, diese nun als WAHR ausgewertet. Die dazu gehörende Anweisung in Zeile 5 wird ausgeführt und danach die if-Anweisung verlassen, d.h. der Code in Zeile 11 ausgeführt.
float betrag = 10000.0, zinssatz = 0.0;
if (betrag > 50000) {
zinssatz = 3.0;
} else if (betrag > 10000) {
zinssatz = 2.0;
} else if (betrag > 0) {
zinssatz = 1.0;
} else {
zinssatz = -0.2;
}
printf("Fertig!\n");
4-2 switch-case-Anweisung
Die switch-case -Anweisung ist eine spezielle bedingte Verzweigung, die verwendet wird, wenn es viele Fallunterscheidungen gibt.
Syntax
switch-case-Anweisung
Ein Ausdruck bzw. der Wert einer Variablen wird mit verschiedenen Werten verglichen. Bei Übereinstimmung wird die entsprechende Anweisung ausgeführt. Die break-Anweisung bewirkt, dass der case-Zweig sofort verlassen wird. Wenn break in einem Zweig weggelassen wird, werden auch die folgenden case-Zweige ausgeführt, bis ein break gefunden wird, oder die switch-Anweisung zu Ende ist. Der optionale default-Zweig greift dann, wenn es keine Übereinstimmung gibt.
switch(ausdr){
case ausdr_1:
anweisungen_1
break;
case ausdr_2:
anweisungen_2
break;
...
default:
anweisungen_default
}
Beispiel
Ausgabe abhängig von Wahl
int wahl;
printf("Eine der Zahlen 1, 2, 3 eingeben: ");
scanf("%d",&wahl);
switch(wahl){
case 1:
printf("Erste Wahl\n");break;
case 2:
printf("Zweite Wahl\n");break;
case 3:
printf("Dritte Wahl\n");break;
case 4: case 5:
printf("Vierte oder fuenfte Wahl\n");
break;
default:
printf("Ungueltige Eingabe!\n");
}
Die switch-case-Anweisung wird dann eingesetzt, wenn anhand der Werte, die eine Variable annehmen kann, viele unterschiedliche Fallunterscheidungen abgebildet werden. Beispiel: Wenn "A": Hilfe anzeigen, wenn "B": Tabelle ausgeben, wen "C": [...], wenn "Z": Programm beenden.
5. Schleifen
while do-while for break continueTop |
C hat drei Arten von Schleifen, die sich darin unterscheiden, wo die Schleifenbedingung geprüft wird: die while-Schleife verwendet eine Ausführungsbedingung, die vor dem Ausführen des Schleifenrumpfs geprüft wird, die do while-Schleife funktioniert über eine Abbruchbedingung, die nach dem Ausführen des Schleifenrumpfs geprüft wird, und die for-Schleife verwendet eine Zählvariable, für die Startwert, Endwert und Schrittweite festgelegt werden. Eine Schleife kann mit break sofort verlassen werden, einzelne Schleifenschritte können mit continue übersprungen werden.
5-1 while- und do-while-Schleife
Eine while-Schleife ermöglicht es, Anweisungen wiederholt auszuführen, und zwar so lange, wie eine Ausführungsbedingung erfüllt ist. Dabei wird die Variable, die in der Bedingung abgefragt wird, nicht automatisch heraufgesetzt, sondern muss im Schleifenrumpf explizit inkrementiert werden. Die do-while-Schleife funktioniert ähnlich, allerdings wird die Bedingung ans Ende gestellt, d.h. die Anweisungen werden auf jeden Fall mindestens einmal durchgeführt.
Syntax
while (bedingung){ anweisungen } do { anweisungen } while (bedingung);
Beispiel
Berechne Summe 1 + 2 + ... + 5
double sum = 0.0; int i = 1;
while (i <= 5) {
sum += i;
i += 1;
}
printf("Summe: %.2f\n", sum);
sum = 0.0; i = 1;
do {
sum += i;
i += 1;
} while (i <= 5);
printf("Summe: %.2f\n", sum);
5-2 for-Schleife
Eine for-Schleife ist eine Zählschleife, die für eine Zählvariable eine Start- und Endbedingung sowie eine Schrittweite festlegt. Die Anweisungen werden für eine vorgegebene Anzahl an Schleifen-Durchläufen wiederholt. Die Angabe der Anzahl wird über die Zählvariable umgesetzt, die automatisch nach jedem Schleifendurchlauf um 1 (bzw. eine andere Schrittweite) erhöht wird.
Syntax
for (count = start;count <= end;count += schritt){ anweisungen }
Beispiel
Berechne Summe 1 + 2 + ... + 5
double sum = 0.0;
for(int i=1;i<=5;i++){
sum += i;
}
printf("Summe:\n");
printf("%.2f\n", sum);
Im folgenden Beispiel wird der Befehl break verwendet, um eine Schleife bei Erfüllung einer Bedingung zu verlassen.
Beispiel
Finde ungeraden Teiler von n
int n = 20;
for (int i = 3; i <= n / 2; i += 2) {
if (n % i == 0) {
printf("Ungerader Teiler gefunden: %d !", i);
break;
}
}
6 Funktionen
Parameter Referenzparameter Rückgabewert void return Geltungsbereich von VariablenTop |
Eine Funktion ist ein benannter Codeblock, der nur ausgeführt wird, wenn er in einer anderen Funktion verwendet wird, dies nennt man den Funktionsaufruf. Funktionen werden einmal definiert und können dann beliebig oft aufgerufen werden.
Man kann Daten oder sogenannte Parameter an eine Funktion übergeben, entweder als Wert oder als Referenz. Referenzparameter werden als Zeigervariablen bzw. Adressen übergeben, sie haben die Besonderheit, dass ihr Wert in der aufrufenden Funktion weiterverwendet werden kann. Eine Funktion kann auch einen Rückgabewert haben, d.h. einen Wert zurückgeben, der in der aufrufenden Funktion weiterverwendet werden kann.
Sobald man ein C-Programm mit selbstdefinierten Funktionen strukturiert, muss man den Geltungsbereich von Variablen berücksichtigen: Lokale Variablen sind innerhalb einer Funktion definiert und können nur dort verwendet werden. Globale Variablen sind außerhalb einer Funktion definiert und können überall im Programm verwendet werden, also auch innerhalb jeder Funktion.
6-1 Funktionen ohne Rückgabewert
Eine Funktion ohne Rückgabewert bzw. mit Rückgabetyp "void" ist eine benannte Gruppierung von Anweisungen. Innerhalb der Funktion können neue Werte berechnet und direkt ausgegeben werden.
Syntax
Die Platzhalter typ_i stehen für Datentypen, param_i sind Parameternamen, und arg_i die tatsächlichen Argumente. Anzahl, Reihenfolge und Datentyp muss bei den Parametern und den tatsächlichen Argumenten übereinstimmen.
/* (2) Funktionsprototyp */
void myfunc(typ_1, typ_2, ... typ_n);
int main(void){
/* (3) Funktionsaufruf */
myfunc(arg_1, arg_2, ... arg_n);
}
/* (1) Funktionsdefinition */
void myfunc(typ_1 param_1, ... typ_n param_n){
// Funktionsrumpf mit Anweisungen
// . . .
}
Beispiel
Funktion trennzeile()
Diese Funktion gibt einfach eine Trennzeile aus, die aus x Sternchen besteht.#include <stdio.h>
void trennzeile();
int main(void){
// Erster Funktionsaufruf
trennzeile();
printf("Hello from K-Town!\n");
// Zweiter Funktionsaufruf
trennzeile();
}
/* Funktionsdefinition */
void trennzeile(void){
printf("****************\n");
}
Beispiel
Formatierungsfunktion ausgabeInEuro()
#include <stdio.h>
void ausgabeInEuro(double);
int main(void){
double x = 49.99;
// Erster Funktionsaufruf
ausgabeInEuro(x);
// Zweiter Funktionsaufruf
ausgabeInEuro(2.5);
}
/* Funktionsdefinition */
void ausgabeInEuro(double betrag){
printf("Betrag: %.2f Euro\n", betrag);
}
Beispiel
Funktion mit Referenzparametern
Der Referenzparameter m speichert den Mittelwert der Wert-Parameter a und b und soll in der main-Funktion weiter verwendet werden. Beachte: m wird als Zeigervariable übergeben, daher *m in Zeile 1 und 2 und &m in Zeile 6.
#include <stdio.h>
void mittelwert(double a, double b, double *m){
*m = (a + b ) / 2;
}
int main(void){
double x = 2.0,y = 4.0, m = 0;
mittelwert(x, y, &m);
printf("Mittelwert= %.2lf\n", m);
}
6-2 Funktionen mit Rückgabewert
Eine Funktion kann auch Daten/Parameter als Rückgabewert zurückgeben, diese können in der aufrufenden Funktion in Berechnungen oder Ausgaben weiter verwendet werden. Damit eine Funktion einen Wert zurückgeben kann, benutzt man das Schlüsselwort "return".
Syntax
Der Parameter r_typ bezeichnet den Datentyp des Rückgabewertes. Wichtig: Eine Funktion kann nur einen Rückgabewert haben!
/* (2) Funktionsprototyp */
r_typ myfunc(typ_1, typ_2, ... typ_n);
int main(void){
/* (3) Funktionsaufrufe */
r_wert_a = myfunc(arg_1a,... arg_na);
r_wert_b = myfunc(arg_1b,... arg_nb);
}
/* (1) Funktionsdefinition */
r_typ myfunc(typ_1 param_1, ... typ_n param_n){
r_typ r_wert;
// Anweisungen, die den r_wert berechnen
// Rückgabewert mit return zurückgeben
return r_wert;
}
Beispiel
Funktion brutto_aus_netto()
Die Funktion brutto_aus_netto() berechnet für einen gegebenen Netto-Betrag und eine gegebene Mehrwertsteuer den Bruttobetrag. Sie hat zwei Parameter, netto (Datentyp double) und mwst (Datentyp int) und einen Rückgabewert vom Datentyp double.
double brutto_aus_netto(double, int);
int main(void){
// Erster Funktionsaufruf
double betrag = 1000.95; int mwst = 19;
printf("Brutto = %.2f\n",
brutto_aus_netto(betrag, mwst) );
// Zweiter Funktionsaufruf
printf("Brutto = %.2f\n",
brutto_aus_netto(1234.55, 19) );
}
/* Funktionsdefinition */
double brutto_aus_netto(double netto, int mwst){
double brutto = netto + netto * mwst / 100;
return brutto;
}
Abgleich Argumente vs. Parameter
Die Argumente im Funktionsaufruf müssen in Anzahl und Datentyp mit den Parametern in der Definition übereinstimmen, jedoch nicht im Namen!
D.h. der Name des übergebenen Argumentes (hier: betrag) in der aufrufenden Funktion muss nicht mit dem Namen des Parameters (hier: netto) übereinstimmen.
Werte zurückgeben: mit Rückgabewert oder mit Referenzparametern
Falls eine Funktion neue Werte berechnet, die in der aufrufenden Funktion weiterverwendet werden sollen,
kann dies entweder durch Verwendung des Rückgabewertes geschehen, oder durch Verwendung von Referenzparametern.
Wann sollte man bei Funktionen also den Rückgabewert verwenden, und wann Referenzparameter?
Falls die Funktion nur einen neuen Wert berechnet, ist die Verwendung des Rückgabewertes besser.
Falls die Funktion jedoch mehrere neue Werte berechnen soll, müssen Referenzparameter verwendet werden.
Die Funktion void mittelwert(double a, double b, double *m) kann demnach als Funktion mit
Rückgabewert umformulieren werden:
Beispiel
Funktion mit Rückgabewert
#include <stdio.h>
double mittelwert(double a, double b){
return (a + b ) / 2;
}
int main(void){
double x = 2.0,y = 4.0, m = 0;
m = mittelwert(x, y);
printf("Mittelwert= %.2lf\n", m);
}
7 Arrays
Element Dimension Index Zufallszahlen randTop |
Ein Array ist eine statische Datenstruktur, die Elemente desselben Datentyps in einem zusammenhängenden Speicherbereich ablegt. "Statische Datenstruktur" bedeutet, dass die Größe des Speicherbereichs vorab fest reserviert ist und nicht mehr verändert werden kann. Ein Element eines Arrays ist ein einzelnes Datenobjekt, dessen Position im Array über sogenannte Indizes festgelegt wird, die bei 0 anfangen. Die Dimension eines Arrays ist die Anzahl der Indizes, die benötigt werden, um ein Element eines Arrays auszuwählen. Ein eindimensionales Array entspricht einer Liste und benötigt nur einen Index, ein zweidimensionales Array entspricht einer Matrix oder Tabelle und benötigt zwei Indizes.
7-1 Eindimensionale Arrays
Eindimensionale Arrays werden verwendet, um Listen zu speichern. Die maximale Größe N des Arrays, d.h. die Anzahl an Elementen, die darin gespeichert werden können, muss zuvor als Konstante definiert werden, entweder als Zahl, oder mittels define, oder mittels const int. Die Zuweisung von Werten an die Elemente eines Arrays erfolgt entweder direkt durch Auflistung der Elemente, oder über Schleifen.
Syntax
dtyp steht für Datentyp, arr steht für den Namen des Arrays.
#define N 10
int main(void){
// Alternativ: const int N = 10;
dtyp arr[N];
// Dekl. und Initialisierung aller Elemente mit 0
dtyp arr[N] = {0};
// Deklaration durch Aufzählen der Elemente
dtyp arr[] = {el1, el2, ..., eln};
}
Beispiel
1D-Arrays deklarieren und verwenden
#define N 11
int main(void){
// Explizite Zuweisung durch Auflisten
float x1[] = {0.0, 0.1, 0.2, 0.3, 0.4, 0.5};
// Deklaration und Initialisierung mit 0
float x2[N] = {0};
// Zuweisung mittels for-Schleife
for(int i=0;i<5;i++)
x2[i] = 0.1*i;
}
Beispiel
1D-Array als Funktionsparameter
Sobald man mit Arrays arbeitet, bietet sich die Verwendung selbstdefinierter Funktionen an,
z.B. für die formatierte Ein- und Ausgabe, für die Berechnung von Summen, statistischer Kennzahlen wie Mittelwert,
Standardabweichung und viele mehr.
Dabei muss die korrekte Verwendung des Arrays als Funktionsparameter beachtet werden.
Bei der Funktionsdefinition schreibt man den Datentyp und Namen des Arrays, gefolgt von eckigen Klammern,
beim Funktionsaufruf nur den Namen des Arrays, ohne eckige Klammern.
Die Beispiel-Funktion print_array() gibt die ersten n Elemente des Arrays formatiert aus:
Funktionsdefinition: void print_array(float x[], int n){...}
Funktionsaufrufe: print_array(x1, n), print_array(x2, 4) etc.
#include <stdio.h>
void print_array(float[], int);
float mittelwert(float[], int);
int main(void) {
float x1[] = {10, 20, 30, 40, 50};
print_array(x1, 3); // Ausgabe: 10.0, 20.0, 30.0
// Mittelwert der ersten 3 Elemente ist 15.0
printf("Mittelwert = %.1f\n", mittelwert(x1, 3));
}
float mittelwert(float x[], int n){
float mwert = 0.0;
for(int i=0;i<n;i++)
mwert += x[i];
return mwert/n;
}
void print_array(float x[], int n) {
for (int i = 0; i < n; i++)
printf("%.1f, ", x[i]);
}
Beispiel
1D-Array mit Zufallszahlen befüllen
In C können ganzzahlige Pseudo-Zufallszahlen im Wertebereich [0, RANDMAX] mit Hilfe der Funktion rand() erstellt werden, der Zufallsgenerator wird mittels der Funktion srand() initialisiert. Im folgenden Beispiel werden Zufallszahlen im Wertebereich [ug, og] auf zwei Arten erstellt: (1) durch Re-Skalierung des Wertebereichs und (2) durch Verwendung des Modulo-Operators.
#include <stdio.h>
#include <stdlib.h>
#define N 10
void print_array(float[], int);
int main(void) {
float a[N] = {0}, b[N] = {0}; int n = 0;
int ug = 10, og = 25; // Untere und obere Grenze
printf("Anz. Elemente: "); scanf("%d", &n);
srand(1); // Initialisiere Zufallsgenerator
for (int i = 0; i < n; i++){
a[i] = ug + (og-ug)*rand()/RAND_MAX; // (1)
b[i] = ug + (rand()%(og-ug+1); // (2)
}
print_array(a, n); print_array(b, n);
}
// TODO: Hier die Funktionsdefinition einfügen
7-2 Zweidimensionale Arrays
Zweidimensionale Arrays werden verwendet, um Tabellen und Matrizen zu speichern. Sie werden deklariert, indem nach dem Arraynamen in eckigen Klammern zuerst die maximale Zeilenanzahl M und danach die maximale Spaltenanzahl N als Konstante angegeben werden. Die Zuweisung der Elemente eines 2D-Arrays kann entweder explizit über Aufzählung erfolgen, oder mittels Schleifen.
Syntax
dtyp steht für Datentyp, arr steht für den Namen des Arrays.
#define M 20 // max. Anzahl Zeilen
#define N 10 // max. Anzahl Spalten
int main(void){
dtyp arr[M][N];
dtyp arr[M][N] = {0};
dtyp arr[][N] = {{el11, el12, el13, ...},
{el21, el22, el23, ...},
..., // weitere Zeilen
};
}
Beispiel
2D-Arrays deklarieren und verwenden
In diesem Beispiel wird die 3x3 Einheitsmatrix zuerst durch explizite Aufzählung und danach mittels for-Schleifen deklariert. Ein zweidimensionales Array wird zeilenweise befüllt bzw. ausgelesen, dafür benötigt man zwei for-Schleifen: Die äußere Schleife mit Zählvariable zIdx iteriert über die Zeilen, die innere Schleife mit Zählvariable sIdx über die Spalten.
#include <stdio.h>
#define M 10 // max. Anzahl Zeilen
#define N 10 // max. Anzahl Spalten
int main(){
float mat[][3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
float tab[M][N] = {0.0};
for(int zIdx=0;zIdx<3;zIdx++){
for(int sIdx=0;sIdx<3;sIdx++){
if (zIdx == sIdx)
tab[sIdx][sIdx] = 1;
}}
}
Beispiel
2D-Array als Funktionsparameter
Wie bei 1D-Arrays muss die korrekte Verwendung des 2D-Arrays als Funktionsparameter beachtet werden: Bei der Funktionsdefinition schreibt man den Namen des Arrays, gefolgt von zwei eckigen Klammern, die erste, für die Zeilendimension, bleibt leer, in die zweite muss die maximale Anzahl der Spalten (hier: N) angegeben werden. Beim Funktionsaufruf wird der Namen des Arrays ohne eckige Klammern angegeben. Die Beispiel-Funktion print_array2d() wird für die formatierte Ausgabe von 2D-Arrays des Datentyps float verwendet, und zwar können jeweils die ersten anzZ Zeilen und anzS Spalten ausgegeben werden.
#include <stdio.h>
#define N 10 // Maximale Anzahl Zeilen
void print_array2d(float arr[][N],int anzZ,int anzS){
for (int i = 0; i < anzZ; i++) {
for (int j = 0; j < anzS; j++) {
printf("%5.1f ", arr[i][j]);
}
printf("\n"); // Zeilenumbruch nach Zeile i
}
printf("\n");
}
int main(void) {
float arr[][N] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};
// Ausgabe: 2 Zeilen und 2 Spalten
print_array2d(arr, 2, 2);
// Ausgabe: 2 Zeilen und 3 Spalten
print_array2d(arr, 3, 3);
}
Beispiel
2D-Array mit Zufallszahlen befüllen
Um selbstdefinierte Funktionen für mathematisch-statistische Kennzahlen zu testen, ist es auch bei 2D-Arrays nützlich, Test-Arrays mit Zufallszahlen zu befüllen. Dies geschieht wieder mit Hilfe der Funktionen srand() und rand() der C-Standardbibliothek stdlib.h.
#include <stdio.h>
#include <stdlib.h>
#define N 10 // Max. Anzahl Zeilen / Spalten
void print_array2d(float arr[][N],int anzZ,int anzS){
int main(void) {
float arr[N][N]; int anzZ = 3, anzS = 4;
for(int i=0;i<anzZ;i++){
for(int j=0;j<anzS;j++){
arr[i][j] = 100*rand()/RAND_MAX;
}
}
print_array2d(arr, anzZ, anzS);
}
// TODO: Hier die Funktionsdefinition einfügen
8 Adressen und Zeiger
Adresse Zeiger Referenzparameter Zeichenketten Dynamische Speicherallokation malloc callocTop |
Bei der Deklaration einer Variablen n legt der Compiler einen Speicherplatz der Größe x (1, 4, oder 8) Byte an
und vergibt dabei eine Adresse (z.B. 0x46fc80),
unter der die Variable wiedergefunden werden kann.
Ein Zeiger / Pointer ist eine Referenzvariable, die die Adresse einer anderen Variablen
speichern kann und z.B.für dynamische Speicherallokation, Textbearbeitung oder Referenzparameter in Funktionen verwendet wird.
Zeigerarithmetik: Die Operationen, die für "normale" Variablen definiert sind, sind für Zeiger nicht definiert,
mit Ausnahme einer speziellen Zeigerarithmetik.
Man kann Zeigervariablen inkrementieren / dekrementieren und allgemeiner ganzzahlige Werte addieren / subtrahieren und
damit einen Speicherbereich durchlaufen.
Weiterhin kann man Zeigervariablen auch vergleichen, d.h. (p1==p2) oder (p1<p2) sind gültige Ausdrücke.
8-1 Zeiger verwenden
Um einen Zeiger / Pointer zu definieren, wird dem Namen der Variablen ein Stern * vorangestellt, z.B. int *z; für
einen Zeiger, der auf int-Variablen zeigen soll.
Alternativ wird auch die Schreibweise int* z; verwendet, diese sagt aus, dass z den Datentyp (int*) hat, also Zeiger auf int.
Zeiger müssen denselben Datentyp haben wie die Variablen, auf die sie zeigen.
Um einem Pointer die Adresse einer anderen Variablen zuzuweisen, wird der Adress-Operator & verwendet.
Um über den Zeiger auf den Inhalt der referenzierten Variablen zuzugreifen, wird der Inhalts-Operator * verwendet,
wie in Zeile 3: *z = 20; bedeutet, dass indirekt über den Zeiger der Wert von a geändert wird.
Syntax
// Variable deklarieren
datentyp var;
// Pointer deklarieren
datentyp *pointer;
// Pointer = Adresse
pointer = &var;
// Zeiger zeigt auf den nächsten Speicherplatz
pointer++;
Beispiel
Wert der Variablen a wird mittels Pointer z geändert.
int a = 10, *z = NULL;
z = &a; // z zeigt auf a
*z = 20; // Wert von a wird auf 20 gesetzt
printf("Wert von a = %d\n",*z);
Array arr wird mittels Pointer p durchlaufen.
double *p = NULL; // p mit NULL initialisieren
double arr[] = {1.2, 2.3, 3.4};
p = &arr[0]; // p zeigt auf den Anfang von arr
p++; // Zeiger p inkrementieren
p zeigt jetzt auf arr[1]
printf("%.2lf\n",*p); // Ausgabe: 2.3
NULL-Zeiger
NULL-Zeiger sind spezielle Zeiger, die für die Zeigerinitialisierung verwendet und für die Überprüfung der korrekten Speicherallokation verwendet werden. In diesem Beispiel z.B. wird durch die Überprüfung des Zeigers p festgestellt, ob die Speicherallokation mit malloc fehlschlägt.
double* p = NULL;
p = malloc(10000000000 * sizeof(double));
if (p == NULL) // Fehler
printf("Speicher kann nicht allokiert werden.\n");
else { // Verwende p wie ein normales Array
p[0] = 1.23;
}
8-2 Anwendungsgebiete von Zeigern
Zeiger haben drei wichtige Anwendungsbereiche:
(1) Arrays und Zeiger können in vielen Ausdrücken austauschbar verwendet werden,
dies wird z.B. bei Zeichenketten verwendet.
(2) Zeiger als Funktionsparameter ermöglichen Parameterübergabe als Referenz.
(3) Zeiger ermöglichen dynamische Speicherverwaltung.
Mit Hilfe der Funktionen malloc, calloc und free kann Speicher im Heap-Bereich allokiert und freigegeben werden.
Weiterhin können mit Hilfe von Zeigern dynamische Datenstrukturen wie z.B.
verkettete Listen, Stacks und Queues umgesetzt werden.
Dynamische Speicherallokation bedeutet, dass die Größe des reservierten Speicherbereichs verändert und freigegeben werden kann. Die C-Standardbibliothek <stdlib.h> stellt für die dynamische Speicherallokation die Funktionen malloc, calloc(), realloc() und free() zur Verfügung, mit denen man einen Speicherbereich für eine Anzahl von Daten desselben Datentyps reservieren bzw. freigeben kann.
Beispiel
Berechne Länge einer Zeichenkette
Verwendete Funktionen: sizeof, malloc, calloc, fgets
Bei der Verwendung von calloc() wird der Speicher mit 0 initialisiert.
// Dynamische Speicherallokation
char *s = (char *)malloc(20*sizeof(char));
// Alternativ mit calloc():
// char *s = (char *)calloc(20, sizeof(char));
// falls kein Speicher reserviert werden kann
if (s == NULL)
return; // beende das Programm
fgets(s, 20, stdin); // Zeichenkette einlesen
int length = 0;
while(*s != '\0'){ // Solange das Endzeichen nicht erreicht wurde
length++; // wird die Länge hochgezählt
s++; // und die Zeigervariable inkrementiert
}
void-Zeiger
void-Zeiger sind keinem speziellen Datentyp zugeordnet, sie können auf einen beliebigen Speicherbereich zeigen. void-Zeiger werden häufig in Zusammenhang mit Funktionen und dynamischer Speicherallokation eingesetzt, dadurch werden Funktionen flexibler und generischer. So gibt z.B. die Funktion malloc einen void-Zeiger zurück. Dabei ist zu beachten, dass vor der Dereferenzierung eines void-Zeigers dieser zunächst durch Typecast auf einen konkreten Datetyp abgebildet werden muss.void* p = calloc(10, sizeof(int));
p[0] = 10; // FALSCH
int* a = (int*)p;
a[0] = 10; // RICHTIG
9 Strukturen
struct union typedefTop |
Eine Struktur ist eine Zusammenfassung mehrerer zusammengehöriger Variablen verschiedenen Datentyps unter einem gemeinsamen Namen. Durch die Deklaration einer Struktur wird zunächst ein neuer Datentyp mit benutzerdefiniertem Namen festgelegt, anschließend kann man neue Variablen deklarieren, die genau diesen Datentyp haben. C besitzt zwei Strukturen, struct und union, die ähnlich deklariert werden, sich in ihrer Wirkung und Verwendung jedoch wesentlich unterscheiden.
9-1 Struct
Ein struct reserviert Speicher für alle Variablen der Gruppierung, und man kann gleichzeitig auf alle Variablen zugreifen. struct wird verwendet, um komplexe Objekte der Realität abzubilden, die durch zusammengesetzte Informationen beschrieben werden.
Strukturen verwenden
Eine Struktur wird definiert, indem man nach dem Schlüsselwort struct den Namen der Struktur angibt, gefolgt von geschweiften Klammern, in die man die zugehörigen Variablen-Deklarationen setzt. Eine Struktur wird verwendet, indem man neue Variablen deklariert, die den Datentyp struct struct_name haben. Danach kann man über die Punkt-Notation auf die Membervariablen der Struktur zugreifen.
Syntax
struct struct_name {
datentyp1 var1;
...
datentyp_n varn;
}
struct struct_name s1, s2;
s1.var1 = wert1;
Beispiel
Struktur für Studenten
Die Struktur struct Student fasst die Variablen zusammen, die die Attribute eines Studenten speichern können.
#include <stdio.h>
int main(void){
struct Student {
char* name;
char* vorname;
int matnr;
};
struct Student st = { "Muster", "Max", 12345 };
printf("%s %s, %d\n", st.name, st.vorname, st.matnr);
st.name = "Test"; st.vorname = "Anna"; st.matnr = 12346;
printf("%s %s, %d\n", st.name, st.vorname, st.matnr);
}
Strukturen mit typedef
In C besteht die Möglichkeit, mit Hilfe des Schlüsselwortes typedef symbolische Namen für Datentypen zu definieren und dadurch das Programm verständlicher zu gestalten. Die Verwendung von typedef ist in Kombination mit Strukturen besonders nützlich, da sonst das Schlüsselwort struct stets mit angegeben werden muss.
Beispiel
In diesem Beispiel wird mit typedef struct Student STUDENT; festgelegt, dass man bei späteren Deklarationen anstelle von struct Student einfach nur STUDENT schreiben kann. D.h. um Variablen vom Datentyp STUDENT zu deklarieren, schreibt man STUDENT st1, st2;
#include <stdio.h>
struct Student {
char* name;
char* vorname;
int matnr;
};
typedef struct Student STUDENT;
int main(void) {
STUDENT st1, st2;
st1.name = "Muster"; st1.vorname = "Max";
st1.matnr = 12345;
printf("%s %s, %d\n",
st1.name, st1.vorname, st1.matnr);
st2.name = "Test"; st2.vorname = "Anna";
st2.matnr = 12346;
printf("%s %s, %d\n",
st2.name, st2.vorname, st2.matnr);
}
9-2 Union
Eine union reserviert einen gemeinsam genutzten Speicherbereich für ihre Variablen. Die Größe des Speicherbereichs wird von der Membervariablen mit dem größten Speicherbedarf bestimmt. Dieser Speicher wird dann bei jeder Wert-Zuweisung an eine Membervariable neu überschrieben. Bei der Verwendung von Unions ist zu beachten, dass man nur jeweils auf die zuletzt zugewiesene Variable auch wieder zugreifen sollte, und nicht gleichzeitig auf mehrere der Membervariablen zugreifen kann.
Union verwenden
Eine Union wird genau wie ein struct definiert: indem man nach dem Schlüsselwort union den Namen der Union angibt, gefolgt von geschweiften Klammern, in die man die zugehörigen Variablen-Deklarationen setzt. Eine Union wird verwendet, indem man neue Variablen deklariert, die den Datentyp union union_name haben, und dann über die Punkt-Notation auf die Membervariablen der Gruppierung zugreift.
Syntax
union union_name {
datentyp1 var1;
...
datentyp_n varn;
}
union union_name u1, u2;
u1.var1 = wert1;
Union wird vor allem in der hardwarenahen Programmierung verwendet, und häufig in Kombination mit struct, um Speicherplatz zu sparen oder über symbolische Namen auf die einzelnen Bits / Bytes der Speicherplätze zuzugreifen. Hier ein Beispiel, weitere gibt es auf den Webseiten von Mikrocontroller-Anbietern, z.B.: Microchip.
union {
unsigned short word;
struct {
unsigned char byte1: 8;
unsigned char byte2: 8;
} bytes;
} w;
w.word = 0x0000;
w.bytes.byte1 = 0xF0;
Beispiel: Union verwenden
In diesem Beispiel wird die grundsätzliche Verwendung einer Union gezeigt.
Es werden eine int- und eine double-Variable gruppiert, der insgesamt reservierte Speicherplatz
beträgt 8 Bytes (die Größe von double). Würde man die beiden Variablen separat deklarieren, wäre der Speicherverbrauch 12 Bytes.
Der Speicherplatz wird zunächst durch die Membervariable i belegt, danach durch die Membervariable f,
dabei geht der Wert von i verloren und darf nicht wie in Zeile 11 verwendet werden.
#include <stdio.h>
int main(void) {
union zahl {
int i; // 4 Bytes
double f; // 8 Bytes
} z;
printf("z belegt %d Bytes Speicher\n", sizeof(z));
z.i = 10; // OK: Speicher wird durch i belegt
printf("z.i^2 = %d\n", z.i*z.i);
z.f = 2.5; // OK: Speicher wird durch f belegt
printf("z.f^3 = %.2f\n", pow(z.f, 3));
// Fehler: i und f nicht gleichzeitig verwenden!
float sum = z.i + z.f; // nicht ok
printf("Summe = %.2f\n", sum); // nicht ok
}
Wenn die Membervariablen kompatible Datentypen haben und die Größen zueinander passen, kann mittels Union der Wert einer Variablen durch die Veränderung des Werts einer anderen Variablen gesetzt werden. Z.B. kann durch Wertzuweisung an eines der Bytes byte1 oder byte2 gezielt das Maschinenwort word verändert werden, welches denselben Speicherbereich belegt.
10 C-Standardbibliotheken
Top |
C verfügt über eine überschaubare Anzahl von Standardbibliotheken mit vordefinierten Funktionen, die über ihre Header-Dateien eingebunden werden.
Dazu gehören:
stdio.h – für Ein- und Ausgabe, stdlib.h – diverse Hilfsfunktionen, math.h – Mathematische Funktionen,
string.h – Zeichenketten-Bearbeitung, time.h – Zeitfunktionen, [...].
Die Referenz der Funktionen mit genauer Beschreibung der Parameter und Verwendung
kann man in der jeweiligen Dokumentation des verwendeten Compilers nachschlagen.
- Verwendet man den GNU C-Compiler, sind die Webseiten GNU C Manual oder auch cplusplus.com/ gute Anlaufstellen, bei der zweiten wird C als eine Untermenge der C++-Referenz geführt.
- Verwendet man hingegen Microsoft Visual Studio, ist die Microsoft Learn-Dokumentation für C++ das relevante Nachschlagewerk, hier wird C ebenfalls als eine Untermenge der C++-Referenz geführt.
Eine Auswahl häufig benötigter Standardbibliotheken mit beispielhafter Verwendung finden Sie hier zusammengestellt.
10-1 Eingabe- und Ausgabe: stdio.h
printf scanf fgets fopen fclose fprintf fscanfDie Funktionen für Ein- und Ausgabe werden über die stdio.h inkludiert. Ein- und Ausgabe erfolgt über Streams, ein Stream kann die Konsole, eine Datei oder eine Zeichenkette sein. Hier findet man Funktionen für die Eingabe von der Konsole (scanf, fgets, getchar) und aus Dateien (fscanf, fread), für die Ausgabe auf die Konsole (printf, putchar), in Dateien (fprintf, fwrite) und Strings (sprintf), für Fehlermeldungen [...].
char text[100];int n = 0; double x = 0.0;
// Formatierte Eingabe und Ausgabe von Zahlen
printf("Zahlen eingeben: ");
scanf("%d %lf", &n, &x);
printf("%d %.2lf", n, x);
// Eingabe eines Textes mit scanf, max 100 Zeichen
printf("Text eingeben, nur Buchstaben, Zahlen und Leerzeichen: ");
scanf("%100[0-9a-zA-Z ]", text); // Standard
scanf_s("%[^\n]100s", text, 100); // In Visual Studio
printf("Ihr Text: %s", text);
// Eingabe eines Textes mit fgets über die Konsole
printf("Text eingeben: ");
fgets(text, 100, stdin);
printf("Ihr Text: %s", text);
Die korrekte Verwendung der C-Funktionen für Ein- und Ausgabe in Dateien erfordert einige Übung,
da hier verschiedene Anwendungsfälle (zeilenweises / formatiertes / blockweises Einlesen)
zu beachten sind und eher Fehler auftreten können (Datei nicht vorhanden oder falsch formatiert, kein Zugriff),
die mit passenden Abfragen behandelt werden müssen.
Erläuterung mit Beispiel:
Ein- und Ausgabe in Dateien
10-2 Mathematische Funktionen: math.h
sin pow sqrt fabs trunc ceilDie Headerdatei math.h deklariert mathematische Funktionen: trigonometrische Funktionen (sin, cos, sinh, cosh, tan ...), Exponentialfunktion (exp), Logarithmen (log, log10, log2), Potenzfunktion (pow), Wurzelfunktion (sqrt), Absolutwert (fabs), Rundungsfunktionen (floor, ceil, trunc), Minimum- und Maximum-Funktionen(fmin, fmax). Hier werden auch diverse mathematische Konstanten deklariert.
#include <stdio.h>
#include <math.h>
int main(void) {
float x = 4.0;
printf("Absolutwert von %.2f = %.2f\n", x, fabs(x));
printf("Potenz %.2f^3 = %.2f\n", x, pow(x, 3));
printf("Wurzel aus %.2f = %.2f\n", x, sqrt(x));
printf("Sinus sin(%.2f) = %.2f\n", x, sin(x));
}
10-3 String-Manipulation: string.h
strlen memcpy strcpy strcat strcmp strchrDie Headerdatei string.h deklariert Funktionen für die Bearbeitung von Zeichenketten: Länge einer Zeichenkette bestimmen (strlen), Zeichenketten kopieren (strcpy), vergleichen (strcmp), aneinanderfügen (strcat). Weiterhin: Zeichen in einer Zeichenkette suchen (strchr) [...].
char *text1 = "Lorem ipsum ", *text2 = "dolor sit amet,";
char *text = NULL;
// Berechne den benötigten Speicherplatz
int n = strlen(text1) + strlen(text2) + 1;
// Dynamische Speicherallokation
text = (char *)malloc(n*sizeof(char));
// Kopiere text1 nach text
strcpy(text, text1);
// Füge text2 an
strcat(text, text2);
printf("Angefuegter Text:\n%s\n", text);
printf("hat Laenge %zu\n", strlen(text));
10-4 Diverse Hilfsfunktionen: stdlib.h
Top |
In der Headerdatei stdlib.h wurden Makros und Hilfsfunktionen für unterschiedliche Anwendungsbereiche zusammengepackt: Funktionen für Typkonvertierung, Funktionen zum Erzeugen von Zufallszahlen, Funktionen für Dynamische Speicherallokation, Systemfunktionen.
Funktionen für Typkonvertierung
atoi atofFunktionen für Typkonvertierung werden verwendet, um Zeichenketten in Zahlenwerte zu konvertieren. Dies wird z.B. beim Einlesen von Daten über Eingabefelder verwendet.
// Konvertierungsfunktionen
// Zeichenkette "100"
char *z = "100";
// Ausgabe: 100, 100.00
printf("%d, %.2f\n", atoi(z), atof(z));
Mit atof wird eine Zeichenkette in eine Fließkommazahl konvertiert, z.B. wird aus "100" die Zahl 100.0.
Funktionen zur Erzeugung von Zufallszahlen
rand srandZufallszahlen werden in der Programmierung für verschiedene Zwecke benötigt, z.B. bei der Randomisierung von Verschlüsselungsalgorithmen oder um Test-Daten für neue Programme zu erzeugen. Mit srand initialisiert man den Zufallsgenerator, mit rand erzeugt man eine ganzzahlige Pseudo-Zufallszahl im Bereich 0 bis RAND_MAX = 32767.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void){
// (1) Initialisiere Zufallsgenerator mit Seed 1
srand(1);
// oder (2) Initialisiere Zufallsgenerator mit aktueller Systemzeit
srand(time(NULL));
while(1){ // Endlosschleife
// Erzeuge eine Zufallszahl im Wertebereich [0, RAND_MAX]
printf("%d\n", rand());
// Erzeuge eine Zufallszahl im Wertebereich [100, 150]
printf("%d\n", 100 + (150-100)*rand()/RAND_MAX );
getchar();
}
}
Als Seed wird häufig time(NULL) verwendet, d.h. die aktuelle Systemzeit in Sekunden seit 01.01.1970.
Dynamische Speicherallokation
malloc calloc realloc free
Bei der Deklaration eines Arrays, z.B. double a[100][1000]; wird der reservierte Speicherbereich vorab im Stack-Speicher des Programms reserviert, man spricht von statischer Speicherallokation. Der maximal verfügbare Stack-Speicher eines Programms ist begrenzt, er beträgt unter Windows 1MB, unter Linux 8 MB. Dort werden lokale Variablen, Funktionsparameter etc. abgelegt. Daher wird bei Programmen, die große Datenmengen verarbeiten, die dynamische Speicherallokation mit Hilfe der Funktionen malloc, calloc, realloc und free verwendet. Dynamische Speicherallokation bedeutet, dass der benötigte Speicher im größeren Heap-Speicher angelegt wird, und dass die Größe des reservierten Speicherbereichs verändert werden kann.
int *p = (int*)calloc(100, sizeof(int));
if(p == NULL){
printf("Fehler bei der Speicherallokation\n");exit(1);
}
printf("Allokation erfolgreich\n");
for(int i=0;i<n;i++)
p[i] = 2*i+1;
// Führe Berechnungen mit den Daten aus ...
// Zuletzt: Speicher wiedr freigeben
free(p);
calloc funktioniert ähnlich wie malloc, mit dem Unterschied, dass calloc() den Speicherbereich gleich auch mit 0 initialisiert.
Systemfunktionen
system getenvDie Systemfunktionen system und getenv werden verwendet, um Befehle des Betriebssystems aus dem C-Programm heraus ausführen zu können. Mit system kann man Befehle aufrufen, die dann vom Betriebssystem ausgeführt werden. Die Parameter der system-Funktion hängen vom jeweiligen Betriebssystem ab, daher sollte diese Funktion aus Kompatibilitätsgründen nur sparsam verwendet werden. Mit getenv kann der Wert einer Systemumgebungsvariablen herausgefunden werden.
// Beispiel: system-Funktion (Visual C, Windows)
system("title=Test"); // Titel der Konsole setzen
system("PAUSE"); // Programm anhalten
system("color 02"); // Farben der Konsole setzen
// Beispiel: getenv-Funktion verwenden (Visual C, Windows)
size_t anzZeichen = 0;
// (1) Bestimme die Länge der Path-Variablen (anzZeichen)
getenv_s(&anzZeichen, NULL, 0, "Path");
if (anzZeichen == 0)
exit(1);
// (2) Reserviere Speicher, um den Wert der Path-Variablen zu speichern
char *mypath = (char*)malloc(anzZeichen * sizeof(char));
if (!mypath){
printf("Speicherallokation fehlgeschlagen!\n"); exit(1);
}
// (3) Lese Inhalt der Systemumgebungsvariablen Path
getenv_s(&anzZeichen, mypath, anzZeichen, "Path");
printf("Path:\n%s\n", mypath);
Die Path-Variable enthält eine Liste von Pfadangaben, die mit einem Semikolon getrennt sind, und in dieser Liste sucht das Betriebssystem nach den installierten ausführbaren Programmen.
Der Funktionsaufruf getenv_s(&anzZeichen, mypath, anzZeichen, "Path") liest genau diese Liste aus und speichert sie in der Zeichenkette mypath.
10-5 Wertebereiche: limits.h
INT_MIN INT_MAX SHRT_MIN SHRT_MAXIn der Headerdatei limits.h werden Konstanten / Makros definiert, die die Wertebereiche ganzzahliger Datentypen festlegen. Dort finden Sie Makros wie die folgenden:
#define INT_MIN (-2147483647 - 1) #define INT_MAX 2147483647 #define LONG_MIN (-2147483647L - 1) #define LONG_MAX 2147483647LDer folgende Code gibt die Wertebereiche der Datentypen short und int aus.
printf("%14s| %14hd| %14hd\n",
"Wertebereich short", SHRT_MIN, SHRT_MAX);
printf("%14s| %14d| %14d\n",
"Wertebereich int", INT_MIN, INT_MAX);
11. Fortgeschrittene Themen der C-Programmierung
Top |
Zu den fortgeschritteneren Themen der C-Programmierung gehören: Makro-Programmierung, erweiterte Verwendung von Zeigern in Kombination mit Funktionen, Strukturen und mehrdimensionalen Arrays, Ein- und Ausgabe in Dateien, Arbeiten mit Datum und Uhrzeit über die Funktionen der time.h, Verwendung systemabhängiger Funktionen und Datentypen.
Funktionszeiger
Funktionszeiger sind Zeiger, die die Anfangsadresse von Funktionen enthalten. Die Deklaration eines Funktionszeigers fptr, der auf die Anfangsadresse einer Funktion f mit einem double-Parameter und Rückgabewert double zeigt, erfolgt wie in Zeile 3 des Beispiels: Rückgabewert und Parameter der Funktion müssen auch für den Funktionszeiger korrekt angegeben werden.
double f(double x) { return x*x; }
int main(void) {
double (*fptr) (double) = f;
printf("%.0lf, %.0lf\n", fptr(2), fptr(3)); // Ausgabe: 4, 9
}
Funktionszeiger werden insbesondere eingesetzt, um Funktionen als Parameter an andere Funktionen zu übergeben.
Erläuterung mit Beispiel: Funktionszeiger verwenden
Makro-Programmierung
Ein C-Makro ist ein Bezeichner, dem man mittels #define-Direktive einen Wert, einen Ausdruck, oder allgemeiner ein Codefragment zuordnet, in der Form #define makro_name ausdruck, z.B. #define N 100. Makros werden noch vor dem Compilieren durch den Präprozessor "expandiert", d.h. jedes Auftreten eines Makros im Code wird durch den entsprechenden Ausdruck ersetzt. Komplexere C-Makros haben mehrere Argumente und enthalten Schleifen oder Funktionsaufrufe, hier werden weitere Direktiven (#if, #elif, #else, #endif) und Regeln eingesetzt. Bei der Verwendung von Makros sollte die korrekte Syntax besonders genau beachtet werden, insbesondere sollte man den Ausdruck und die Argumente in Klammern setzen, da sonst schwer nachvollziehbare Fehler auftreten.
#include <stdio.h>
#define QUADRAT(x) ((x)*(x))
#define QUADRAT2(x) x*x
int main(void) {
printf("(2+3)^2 = %d\n", QUADRAT(2+3)); // (2+3)*(2+3) = 25
printf("(2+3)^2 = %d\n", QUADRAT2(2+3)); // 2+3*2+3 = 11
}
Makros werden auf unterschiedliche Weise eingesetzt: um Konstanten zu definieren, um eigene Bezeichner
für häufig verwendeten Code zu vergeben, oder um kontextabhängigen Code zu programmieren.
Erläuterung mit Beispiel: Makros verwenden
Dies kompakte C-Tutorial bietet einen Einstieg in die C-Programmierung mit Fokus auf die grundlegenden C-Befehle. Sobald man diese beherrscht, kann man in fortgeschrittenere Themen der C-Programmierung einsteigen, insbesondere Funktionszeiger, Zeiger auf Zeiger, Makro-Programmierung, Ein- und Ausgabe in Dateien, Arbeiten mit Datum und Uhrzeit über die Funktionen der time.h und viele mehr.
Tools & Quellen
- Visual Studio Community Edition: Für die Entwicklung von C-Programmen.
- Visual Studio C17-Unterstützung: Installieren der Unterstützung für C11 und C17 in Visual Studio.
- GNU GCC Compiler: Dokumentation
- Visual Studio C Language Reference, docs.microsoft.com/en-us/cpp/c-language/
- yED Graph Editor: Für die Entwicklung von Flussdiagrammen bzw. Programmablaufplänen.
- Jürgen Wolf, C von A bis Z: openbook.rheinwerk-verlag.de/c_von_a_bis_z/, 2020.
- Thomas Theis: Einstieg in C. Für Programmiereinsteiger geeignet, Galileo Press, 2014.
- Brian Kernighan, Dennis Ritchie, The C programming language. Prentice-Hall, 2010.