15.2    Erweiterte Zeitfunktionen

Die Standardbibliothek time.h enthält einige vordefinierte Variablen, die auf jedem System zur Verfügung stehen, nicht nur unter Windows. Deshalb gibt es in Bezug auf time.h Strukturen, die Sie stets in identischer Weise verwenden können, und die auch Zeitangaben viel genauer zurückliefern als die clock()-Standardfunktion. Wir wollen nun neben einigen einfachen Beispielen zu grundlegenden Uhrzeit- und Datumsfunktionen zunächst eine plattformunabhängige clock()-Funktion programmieren. Diese soll uns dann eine delay()-Funktion ermöglichen, die auf jeder Plattform identisch funktioniert und die angegebene Zeit in Millisekunden wartet.

An dieser Stelle müssen wir uns aber zunächst einmal mit einigen grundlegenden strukturierten Datentypen vertraut machen, die in time.h definiert sind. Tabelle 15.1 listet die wichtigsten auf.

Typ

Bedeutung

CLOCKS_PER_SEC

Konstante, die angibt, wie viele Taktzyklen die Funktion clock() pro Sekunde benutzt

clock_t

Dieser Datentyp enthält Angaben zur aktuellen CPU-Zeit und ist teilweise systemabhängig.

time_t

Dieser Datentyp enthält Datums- und Zeitangaben und ist teilweise systemabhängig.

struct tm

Dieser Datentyp enthält Informationen zur aktuellen Kalenderzeit und ist vom POSIX-Standard genormt. Es wird standardmäßig der gregorianische Kalender, sowie die aktuelle Zeitzone (die sogenannte Locale) verwendet.

Tabelle 15.1    Die wichtigsten strukturierten Datentypen in time.h

Laut ANSI-C-Standard sollen in der Struktur tm folgende Komponenten enthalten sein:

struct tm-Variable

Bedeutung

int tm_sec;

Sekunden (0–59)

int tm_min;

Minuten (0–59)

int tm_hour;

Stunden (0–23)

int tm_mday;

Monatstag (1–31)

int tm_mon;

Monate (0–11; Januar = 0)

int tm_year;

ab 1900

int tm_wday;

Tag seit Sonntag (0–6; Sonntag = 0)

int tm_yday;

Tag seit 1. Januar (0–365; 1. Januar = 0)

int tm_isdst;

Sommerzeit (tm_isdst > 0)

Winterzeit (tm_istdst == 0)

nicht verfügbar (tm_isdst < 0)

Tabelle 15.2    Bedeutung der einzelnen Strukturvariablen in struct tm

15.2.1    Eine Plattformunabhängige delay()-Funktion

Auf einigen Linux-Systemen, z. B. auf dem Raspberry Pi, kann die Funktion clock() die Zeitwerte auch in Mikrosekunden zurückliefern. In diesem Fall ist dann die Konstante CLOCKS_PER_SEC als 1000000 definiert. Sie fragen sich an dieser Stelle vielleicht, warum dann der Raspberry Pi nicht gleich CLOCKS_PER_SEC benutzt, um eine clock()-Funktion zu implementieren, die die Programmlaufzeit in Millisekunden zurückgibt. Leider haben auch wir zurzeit keine befriedigendere Antwort auf diese Frage, als die Bemerkung, dass der Raspberry Pi ja eher ein Mikrocontroller als ein PC ist. Vielleicht muss er deshalb die Laufzeiten auch präziser messen. Nichtsdestotrotz führte diese kleine Unstimmigkeit bei der clock()-Funktion dazu, dass wir eine eigene Funktion MyClock() entwickelt haben, die auch auf dem Raspberry Pi und anderen Linux-Systemen läuft:

01  unsigned long int MyClock() {
02 unsigned long int speed=CLOCKS_PER_SEC; // z. B. 1000000
03 unsigned long int fact=speed/1000; // z. B. 1000
04 return clock()/fact;
05 }

Die Funktion MyClock() speichert in Zeile (02) zunächst die Clock-Zyklen pro Sekunde in der Variablen speed. Die Variable speed ist vom Typ unsigned long int. Dies sind meistens mindestens 32 Bit, und Sie sind so auf der sicheren Seite. Der Faktor fact in Zeile (03) ist dann der Divisionsfaktor, den Sie auf den Rückgabewert von clock() anwenden müssen, damit der Rückgabewert in Zeile (04) am Ende wirklich Millisekunden enthält. Läuft Ihre Uhr im Raspberry Pi beispielsweise mit 1.000.000 Zyklen pro Sekunde, müssen Sie den Rückgabewert von clock() durch 1.000 teilen, um den entsprechenden Rückgabewert in Millisekunden zu erhalten.

Nun muss Ihre delay()-Funktion nur noch entsprechend korrigiert werden:

01  void delay(unsigned long int millis) {
02 unsigned long int now=MyClock(); // In Millisekunden
03 while ((MyClock()-now)<millis) { } // In Milliskunden
04 }

Jetzt haben Sie wirklich eine Wartefunktion, die unter Windows, macOS und Linux/Unix identisch arbeitet und die Sie deshalb in unzähligen Situationen einsetzen können.

15.2.2    Der Datentyp time_t

Der Datentyp time_t ist ein Datentyp, der in time.h mit typedef definiert wird. Dies bedeutet, dass time_t für verschiedene Plattformen (auch in unterschiedlicher Weise) implementiert werden kann, aber trotzdem immer auf die gleiche Weise funktioniert. Aber nicht nur dies unterscheidet time_t z. B. von der clock()-Funktion, denn in time_t wird die Zeit, seit der der Systemkern arbeitet, gespeichert. Dies ist normalerweise (wenn Sie keinen eigenen Bezugspunkt festlegen) laut POSIX-Standard die Anzahl der Sekunden, die seit dem 1. Januar 1970, 00:00:00 Uhr, vergangen sind. Genau deshalb enthält diese Zeitangabe quasi sowohl das Datum als auch die Uhrzeit. Diese Zeitangabe kann mit folgender Funktion abgefragt werden:

time_t time(time_t *zeitzeiger);

Der Rückgabewert ist also von Typ time_t. Wird für den Parameter zeitzeiger kein NULL-Zeiger verwendet, befindet sich an dieser Adresse die aktuelle Systemzeit. Hierzu folgt nun ein kleines Listing, das die Zeit in Sekunden seit dem 1. Januar 1970 um 00:00:00 Uhr mithilfe der Funktion time() ausgibt. Dieses Listing ist eine abgespeckte Version eines Beispiels aus dem Buch »C von A bis Z«, das diese Dinge noch ausführlicher erklärt.

00  // Kapitel15/time_t_beispiel.c
01 #include <stdio.h>
02 #include <time.h>

03 int main(void) {
04 time_t t;
05 t=time(NULL);
06 printf("Seit dem 1.1.1970 vergangene Sekunden:%ld\n",t);
07 printf("Seit dem 1.1.1970 vergangene Minuten:%ld\n",t/60);
08 printf("Seit dem 1.1.1970 vergangene Stunden:%ld\n",t/3600);
09 printf("Seit dem 1.1.1970 vergangene Tage:%ld\n",t/3600/24);
10 return 0;
11 }

Listing 15.2    time_t_beispiel.c demonstriert die Verwendung des Datentyps time_t.

Das Programm gibt bei der Ausführung z. B. Folgendes aus:

Seit dem 1.1.1970 vergangene Sekunden:1587725817
Seit dem 1.1.1970 vergangene Minuten:26462096
Seit dem 1.1.1970 vergangene Stunden:441034
Seit dem 1.1.1970 vergangene Tage:18376

Kommen wir nun zurück zu unserem Listing time_t_beispiel.c. Um time_t zu verwenden, müssen Sie in Zeile (02) wieder zusätzlich zu stdio.h auch time.h einbinden. Innerhalb der main()-Funktion wird nun in Zeile (04) die Variable t vom Typ time_t definiert, der die Anzahl der vergangenen Sekunden seit dem 01.01.1970 zugewiesen werden soll. Diese Zuweisung geschieht in Zeile (05) durch die Funktion time(), der als Überparameter NULL übergeben wird. Mit NULL als Parameter wird also automatisch der 01.01.1970 als Bezugspunkt genommen. In den Zeilen (06) bis (09) wird dann nacheinander die Anzahl der vergangenen Sekunden, Minuten, Stunden und Tage seit dem 01.01.1970 in der Konsole ausgegeben.

15.2.3    Der Datentyp struct tm

Der Datentyp time_t verursachte leider in der Vergangenheit einige Probleme, weshalb er nicht mehr verwendet wird bzw. nur als Grundlage für höhere Betriebssystemfunktionen dient. So implementierten z. B. alte 16-Bit-Systeme wie DOS time_t auch nur mit einer Genauigkeit von 16 Bit. Als Konsequenz daraus mussten diese Systeme u. a. das Jahrhundert in Datumsangaben auslassen und ferner andere zusätzliche Informationen, wie z. B. die CPU-Zeit, mit in die Datumsbestimmung einbeziehen. Dies führte dann vor allem im Rechnungswesen zu Problemen, weil einige nach dem 01.01.2000 ausgestellte Rechnungen auf das Jahr 1900 zurückdatiert wurden. Man kann sich hier gut das Gesicht des entsprechenden Unternehmers vorstellen, der einen Brief erhält, in dem steht, dass er für 100 Jahre Steuern nachzahlen soll.

Aber auch nach der Behebung des bekannten Millenniums-Bugs durch Erhöhung der Genauigkeit von time_t auf 32 oder sogar 64 Bit gibt es noch einige Probleme. Es ist z. B. sehr umständlich, die Zeit stets in vergangenen Sekunden seit einem bestimmten Referenzdatum zu zählen und bei Bedarf sogar durch das eigene Programm in die gewünschten Formate umwandeln zu müssen. Diese Arbeit nimmt Ihnen seit C99 time.h ab und stellt mit dem strukturierten Datentyp tm eine sehr flexible Lösung zum Umgang mit Datumsangaben und Uhrzeiten bereit. Sämtliche modernen Compiler (auch der GCC auf dem Raspberry Pi) bieten diese Lösung an.

In den Übersichtstabellen am Anfang des Kapitels wurden bereits die einzelnen Komponenten in dem strukturierten Datenyp tm aufgeführt. Nun sollen einige Beispiele die Verwendung erläutern.

localtime() und gmtime() – Umwandeln von time_t in struct tm

Die Angabe der Zeit als Sekunden seit einem bestimmten Datum ist nicht gerade flexibel, wie Sie weiter oben bereits gesehen haben. Trotzdem muss es für sämtliche Computer (vor allem im Internet) eine einheitliche Lösung geben. Denn erstens weiß ein Computerprozessor von sich aus nicht, was ein Wochentag oder ein Datum ist, und zweitens gibt es unterschiedliche Kalendersysteme. Müssen Sie jetzt doch anfangen, sich eigene Funktionen zu schreiben, mit denen der Rückgabewert der Funktion time() in ein entsprechendes Format umgerechnet wird? Im Endeffekt ist dies so, denn als Grundlage dient in der Tat das inzwischen 64 Bit breite time_t, das nun schlicht so definiert wird:

typedef unsigned long long int time_t;

Zum Glück haben Ihnen aber inzwischen zahlreiche C-Entwickler diese Arbeit abgenommen, und es gibt spätestens seit C11 Standardfunktionen, um time_t in andere Formate umzuwandeln. Die wichtigsten Funktionen, die in den nächsten Beispielen benutzt werden, sind:

struct tm *localtime(const time_t *zeitzeiger);
struct tm *gmtime(const time_t *zeitzeiger);

Beide Funktionen liefern als Rückgabewert die Adresse einer Zeitangabe vom Typ struct tm. Diese Struktur wurde bereits zu Beginn dieses Kapitels behandelt. Die Funktion localtime() wandelt die Kalenderzeit der Adresse time_t *zeitzeiger in die lokale Ortszeit um – sogar unter der Berücksichtigung von Sommer- und Winterzeit. gmtime() wandelt hingegen die Kalenderzeit in die UTC-Zeit um. UTC bedeutet universal time code, ein Code, der auch von Zeitservern im Internet verwendet wird. So kann sich z. B. ein PC beim Start des Betriebssystems zusammen mit den Standortinformationen seines Providers stets die aktuelle Zeit besorgen. Es folgt nun ein beliebtes Beispiel, das in fast in jedem Programmierkurs in ähnlicher Weise auftaucht. Das Programm gibt Ihr Alter in Jahren, Monaten und Tagen aus.

00  // Kapitel15/geburtsdatum.c
01 #include<stdio.h>
02 #include<stdlib.h>
03 #include<time.h>
04 struct tm *tmnow;

05 void today(void) {
06 time_t tnow;
07 time(&tnow);
08 tmnow = localtime(&tnow);
09 printf("Heute ist der ");
10 printf("%d.%d.%d\n", tmnow->tm_mday, tmnow->tm_mon + 1,
tmnow->tm_year + 1900);
11 }

12 int main(void) {
13 int tag, monat, jahr;
14 unsigned int i=0, tmp;

15 printf("Bitte geben Sie Ihren Geburtstag ein!\n");
16 printf("Tag:");
17 scanf("%d",&tag);
18 printf("Monat:");
19 scanf("%d",&monat);
20 printf("Jahr (jjjj):");
21 scanf("%d",&jahr);
22 today();
23 if(tmnow->tm_mon < monat) {
24 i = 1;
25 tmp=tmnow->tm_mon+1-monat;
26 monat=tmp+12;
27 }
28 else {
29 tmp=tmnow->tm_mon+1-monat;
30 monat=tmp;
31 }
32 if(monat == 12) {
33 monat = 0;
34 i = 0;
35 }
36 printf("Sie sind %d Jahre, %d Monat(e) und %d Tag(e) alt\n",
tmnow->tm_year+1900-jahr-i,monat, tmnow->tm_mday-tag);
37 return 0;
38 }

Listing 15.3    geburtsdatum.c fragt nach Ihrem Geburtsdatum und gibt aus, wie alt Sie in Jahren, Monaten und Tagen sind.

Das Hauptprogramm in dem Listig geburtsdatum.c ist eigentlich ganz simpel. Wenn Sie einmal einige grundlegende Dinge verstanden haben, werden Sie dies erkennen. Zuerst werden Sie aufgefordert, Tag, Monat und Jahr im Format TT:MM:JJJJ einzugeben. Der Einfachheit halber werden hier in den Zeilen (15) bis (21) die einzelnen Bestandteile des Geburtsdatums separat durch scanf() abgefragt, also erst der Tag, dann der Monat und dann das Jahr. Die Variablen tag, monat und jahr sind hier vom Typ int, das heißt, dass das Programm Jahreszahlen bis maximal zum Jahr 32767 (0x7fff hex) verarbeiten kann. Dies stellt aber hier kein Problem dar.

Die eigentliche Crux liegt woanders, nämlich beim Fixpunkt des Datentyps time_t, der ja standardmäßig auf den 01.01.1970 festgelegt ist. Außerdem wollen Sie ja feststellen, wie alt Sie zurzeit sind, müssen also zumindest das aktuelle Systemdatum kennen und zur Not in eine leicht lesbare Form umwandeln. Genau zu diesem Zweck ist dem Hauptprogramm die Funktion today() vorgelagert, die von diesem in Zeile (22) aufgerufen wird.

Die Funktion today() legt erst einmal in Zeile (06) die lokale Variable tnow vom Typ time_t an. Anschließend wird in Zeile (07) die aktuelle Zeit »wie gewohnt« zurückgegeben, nämlich in Sekunden, die seit dem 01.01.1970 vergangen sind. In Zeile (08) erhält nun die globale Variable tmnow vom Typ struct *tm die vorher durch time() ermittelte lokale Rechnerzeit in dem lesbaren Format, das durch den strukturierten Datentyp struct tm definiert ist. Diese Wandlung übernimmt die Funktion localtime(), der die Anfangsadresse der globalen Variablen tmnow übergeben wird und anschließend die Struktur mit den entsprechenden Werten füllt. Zur Kontrolle gibt die Funktion today() in den Zeilen (09) bis (11) den heutigen Tag in einer lesbaren Form aus.

Machen wir nun mit dem Rest des Hauptprogramms weiter (Zeilen (23) bis (37)): Wir müssen jetzt unser Geburtsdatum mit der zuvor ermittelten lokalen Rechnerzeit vergleichen, die in der globalen Variable tmnow gespeichert ist. Da tmnow ein strukturierter Datentyp ist, können wir aber die einzelnen Komponenten von tmnow mit den einzelnen Komponenten unseres Geburtsdatums vergleichen (genau deshalb wurden zuvor der Tag, der Monat und das Jahr separat eingegeben).

Die Zeilen (23) bis (27) vergleichen nun die Monatskomponente unseres Geburtsdatums mit der Monatskomponente der lokalen Rechnerzeit. Dies ist nötig, weil es vorkommen kann, dass die Differenz negativ ist (z. B. wenn wir im Mai geboren sind, es aber bereits Dezember ist). Bei einer negativen Differenz (z. B. bei 5 12 = -7) müssen wir die entsprechenden Komponenten durch einen Ringtausch austauschen und den Geburtsmonat in der Variable tmp zwischenspeichern. Genau diesen Ringtausch führen die Zeilen (23) bis (27) aus, wenn die if-Bedingung in Zeile (23) wahr ist.

Der else-Zweig wird ausgeführt, wenn keine negative Differenz zwischen dem aktuellen Monat und unserem Geburtsmonat besteht. In diesem Fall kann in der temporären Variablen tmp einfach die Differenz zwischen tmnow->tm_mon+1 und der int-Variablen monat benutzt werden.

Der Ausdruck mon+1 wird deshalb verwendet, weil die Zählung der Monate im Computer bei 0 anfängt, wir aber normalerweise mit 1=Januar anfangen. Die Differenz der Jahre kann hingegen nicht negativ werden und muss nicht geprüft werden, da das Geburtsjahr stets vor dem aktuellen Datum liegt. Sie können ja Ihren PC nicht gekauft haben, bevor Sie geboren wurden, es sei denn, Sie besitzen eine Zeitmaschine. Da aber Zeitreisen bekanntlich sehr viel durcheinanderbringen, kann Ihnen in diesem Fall auch kein C-Programm mehr helfen.

Kommen wir nun zu einer kleinen Übung: Sie haben vielleicht schon bemerkt, dass das letzte Programm globale Variablen verwendet. Dies gilt allgemein als schlechter Programmierstil. Denken Sie darüber nach, wie Sie die globale Variable tmnow entfernen können, z. B. indem Sie diese als Zeiger an die Funktion today() übergeben (Stichwort Call by Reference). Denken Sie ferner darüber nach, ob die Differenz der Tage zwischen der lokalen Rechnerzeit und Ihrem Geburtsdatum nicht auch negativ werden kann, und beheben Sie gegebenenfalls dieses Problem.

Das Programm geburtsdatum.c gibt bei der Ausführung z. B. Folgendes aus:

Bitte geben Sie Ihren Geburtstag ein!
Tag:23
Monat:05
Jahr (jjjj):1974
Heute ist der 24.4.2020
Sie sind 45 Jahre, 11 Monat(e) und 1 Tag(e) alt

mktime() – Umwandeln von struct tm in time_t

Kommen wir jetzt zum Gegenstück der Funktionen localtime() und gmtime(). Mit der folgenden Funktion können Sie eine Struktur vom Typ tm in den Datentyp time_t umwandeln:

time_t mktime(struct tm *zeitzeiger);

Auf diese Weise wird eine Zeitangabe im struct tm-Format wieder in eine Zeit im time_t-Format umgewandelt. Sie erhalten also hier wieder die unflexible Zeitangabe in Sekunden, die seit dem 01.01.1970 vergangen sind. Ist die in dem übergebenen Parameter verwendete Kalenderzeit nicht darstellbar, gibt diese Funktion -1 zurück. Die echten Werte der Komponenten tm_yday und tm_wday in zeitzeiger werden ignoriert. Die ursprünglichen Werte der Felder tm_sec, tm_min, tm_hour, tm_mday und tm_mon sind nicht auf den durch die tm-Struktur festgelegten Bereich beschränkt. Befinden sich die Felder nicht im korrekten Bereich, werden diese angepasst.

Das heißt konkret: Wird z. B. das fehlerhafte Datum 38.03.2019 eingegeben, muss die Funktion mktime() dieses Datum richtig setzen. Bei richtiger Rückgabe erhalten Sie entsprechende Werte für tm_yday und tm_wday. Der zulässige Bereich für die Kalenderzeit liegt zwischen dem 01. Januar 1970, 00:00:00 Uhr, und dem 19. Januar 2038, 03:14:07 Uhr.

Das folgende Beispiel soll zeigen, wie Sie den genauen Wochentag durch diese Funktion ermitteln können. Versuchen Sie, als kleine abschließende Übung, das nächste Beispiel ohne Hilfestellungen zu verstehen. Keine Angst, denn dies ist mit einigem Nachdenken machbar. Wir stellen Ihnen hier keine »Knobelaufgaben«, die Ihnen die C-Programmierung verleiden können.

00  // Kapitel15/wochentag.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <time.h>

04 char *wday[] = {
05 "Sonntag", "Montag", "Dienstag", "Mittwoch",
06 "Donnerstag", "Freitag", "Samstag", "??????"
07 };

08 int main(void) {
9 struct tm time_check;
10 int year, month, day;

11 /* Jahr, Monat und Tag eingeben zum
12 * Herausfinden des Wochentags */


13 printf("Jahr:");
14 scanf("%d",&year);
15 printf("Monat:");
16 scanf("%d",&month);
17 printf("Tag:");
18 scanf("%d",&day);

19 /* Wir füllen unsere Struktur struct tm time_check
20 * mit Werten. */


21 time_check.tm_year = year - 1900;
22 time_check.tm_mon = month - 1;
23 time_check.tm_mday = day;

24 /* 00:00:01 Uhr */

25 time_check.tm_hour = 0;
26 time_check.tm_min = 0;
27 time_check.tm_sec = 1;
28 time_check.tm_isdst = -1;

29 if(mktime(&time_check) == -1)
30 time_check.tm_wday = 7; /* = unbekannter Tag */

31 /* Der Tag des Datums wird ausgegeben. */

32 printf("Dieser Tag ist/war ein %s\n", wday[time_check.tm_wday]);
33 return 0;
34 }

Listing 15.4    wochentag.c gibt zu einem bestimmten Datum den Wochentag als Text aus, also z. B. als »Sonntag« und nicht als Zahl 0.

Das Programm wochentag.c gibt bei der Ausführung z. B. Folgendes aus:

Jahr:2020
Monat:04
Tag:24
Dieser Tag ist/war ein Freitag

Die letzten beiden Beispiele sind abgespeckte Varianten von Beispielen aus dem Buch »C von A bis Z«. An dieser Stelle schließen wir nun mit den Zeitfunktionen ab, denn mehr »Stoff« passt sicherlich nicht in einen Grundkurs. An dieser Stelle sind Sie quasi fertig, es folgt nun kein weiteres Kapitel mehr. Sozusagen »geschenkt« haben wir uns alles, was mit der Programmierung von grafischen Oberflächen oder mit der Abfrage von Gerätedateien (also z. B. Druckern, Mäusen oder Joysticks) zu tun hat. Dies würde sicherlich ein weiteres Buch füllen. Auch auf den Raspberry Pi sind wir nur am Rande eingegangen und haben Ihnen z. B. keine ausführliche Installationsanleitung für diesen geliefert.

Wie geht es jetzt weiter? Wenn Sie nun die Nase von C voll haben, brauchen Sie nichts weiter zu tun und können dieses Buch zur Seite legen. Wenn Sie aber noch mehr wissen wollen, können Sie sich z. B. nun, nachdem Sie sich die Lösungen zu den Übungsaufgaben angesehen haben, »C von A bis Z« besorgen. Dort wird das Wissen, das Sie jetzt erworben haben, weiter vertieft, und Sie sind anschließend wirklich dazu fähig, ein Profi zu werden. Sie können dann z. B. in die Programmierung grafischer Oberflächen oder das Erstellen von 3D-Anwendungen einsteigen. Ferner können Sie sich jetzt auch erst einmal mit C++ befassen, einer Variante von C, die Ihnen die objektorientierte Entwicklung ermöglicht. C++ wird sehr oft in Teams eingesetzt. Die gleiche Aussage gilt auch für Java, das sehr stark an C++ angelehnt ist. Auch in diese Sprache können Sie nun mühelos einsteigen. Egal wie Sie jedoch jetzt weitermachen, wir wünschen Ihnen vor allem eins:

Viel Spaß!