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. 1. Erste Schritte mit C
  2. 2. Variablen, Konstanten, Operatoren
  3. 3. Ausgabe und Eingabe von der Konsole
  4. 4. Bedingte Verzweigungen und Fallunterscheidungen
  5. 5. Schleifen (while, do while, for)
  6. 6. Funktionen: benannte Codeblöcke mit Eingabe- und Rückgabewerten
  7. 7. Arrays: Datenstrukturen für mehrere Elemente desselben Datentyps
  8. 8. Adressen und Zeiger
  9. 9. Strukturen: Datenstrukturen für Elemente unterschiedlichen Datentyps
  10. 10. C-Standardbibliotheken
    stdio.h math.h string.h stdlib.h limits.h
  11. 11. Fortgeschrittene Themen
    Funktionszeiger Makro-Programmierung

1. Erste Schritte mit C

C-Quellcode Header-Datei main-Funktion include define Kommentar Compiler IDE
    Top

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 Programm­bibliotheken ü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.

Visual Studio Community Edition

2. Variablen und Konstante

Variable Konstante Datentyp Typumwandlung Operatoren Modulo-Operator sizeof enum
    Top

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
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
Der sizeof -Operator gibt die Größe des Speicherbedarfs einer Variablen / eines Objektes in Bytes an.

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 Steuerzeichen
    Top

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-case
    Top

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 continue
    Top

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 Variablen
    Top

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 Parameter­namen, 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 rand
    Top

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 calloc
    Top

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 typedef
    Top

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 zusammen­gesetzte 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 fscanf

Die 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);
Ein- und Ausgabe-Funktionen haben einige betriebssystem-abhängige Besonderheiten, die vom Standard abweichen. Z.B. wird in Visual C die Funktion scanf_s als sichere Variante der Funktion scanf verwendet. Bei der Angabe von Dateinamen muss man ebenfalls die Konventionen der unterschiedlichen Betriebssysteme berücksichtigen.
Es hängt von der Art der Daten ab, welche Funktionen zu verwenden sind. Bei strukturierten tabellenartigen Daten verwendet man die formatierte Eingabe mit scanf, bei unstrukturierten Texten die Eingabe mit fgets. Um einzelne Zeichen einzulesen, verwendet man getchar.

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 ceil

Die 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));
}
Bei der Verwendung mathematischer Funktionen gibt es ebenfalls einige compiler-abhängige Besonderheiten. Um z.B. in Visual Studio die Konstante M_PI verwenden zu können, muss vor dem Inkludieren der math.h der Befehl #define _USE_MATH_DEFINES verwendet werden.

10-3 String-Manipulation: string.h

strlen memcpy strcpy strcat strcmp strchr

Die 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));
Das vorliegende Beispiel zeigt, wie mit Hilfe der Funktionen strcpy und strcat zwei Zeichenketten aneinandergefügt werden. Der für die neue zusammengefügte Zeichenkette benötigte Speicherplatz wird zuvor aus den Längen der beiden ursprünglichen Zeichenketten mittels strlen berechnet.

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 atof

Funktionen 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 atoi wird eine Zeichenkette in eine ganze Zahl konvertiert, z.B. wird aus "100" die Zahl 100.

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 srand

Zufallszahlen 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();
  }
}
Der Zufallsgenerator erzeugt Pseudo-Zufallszahlen nach einem bestimmten Algorithmus, der einen Seed für die Initialisierung benötigt. Die Verwendung eines konstanten Seeds führt dazu, dass bei jeder Ausführung des Programms dieselben Zufallszahlen erzeugt werden. Die Verwendung eines Seeds, der sich bei jeder Ausführung des Programms ändert, bewirkt, dass auch jedes Mal andere Pseudo-Zufallszahlen erzeugt werden.

Als Seed wird häufig time(NULL) verwendet, d.h. die aktuelle Systemzeit in Sekunden seit 01.01.1970.
Um eine Zufallszahl in einem bestimmten Wertebereich zu erzeugen, muss die ursprünglich generierte Zahl mittels mathematischer Operationen skaliert und verschoben werden.
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);
Mit malloc und calloc reserviert man Speicherplatz und erhält einen Zeiger auf den reservierten Speicherbereich zurück. Mit free wird der Speicherbereich wieder freigegeben.

calloc funktioniert ähnlich wie malloc, mit dem Unterschied, dass calloc() den Speicherbereich gleich auch mit 0 initialisiert.
Systemfunktionen
system getenv

Die 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);
Der Funktionsaufruf system("pause"); führt den Windows-Befehl PAUSE in der Kommandozeile (cmd) aus. Der Funktionsaufruf system("color 02"); setzt die Hintergrundfarbe der Konsole auf Schwarz (0) und die Textfarbe auf grün (2).
Systemumgebungsvariablen wie Path, Temp, OS usw. geben Informationen über das Betriebssystem des Rechners, auf dem das C-Programm ausgeführt wird.

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_MAX
In 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      2147483647L
Der 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

  1. Visual Studio Community Edition: Für die Entwicklung von C-Programmen.
  2. Visual Studio C17-Unterstützung: Installieren der Unterstützung für C11 und C17 in Visual Studio.
  3. GNU GCC Compiler: Dokumentation
  4. Visual Studio C Language Reference, docs.microsoft.com/en-us/cpp/c-language/
  5. yED Graph Editor: Für die Entwicklung von Fluss­diagrammen bzw. Programm­ablauf­plänen.
  6. Jürgen Wolf, C von A bis Z: openbook.rheinwerk-verlag.de/c_von_a_bis_z/, 2020.
  7. Thomas Theis: Einstieg in C. Für Programmiereinsteiger geeignet, Galileo Press, 2014.
  8. Brian Kernighan, Dennis Ritchie, The C programming language. Prentice-Hall, 2010.