Ergänzungen

In diesem Kapitel sollen einige weiterführende Themen angeschnitten werden, die in der Praxis der C-Programmierung eine wichtige Rolle spielen. Dem derzeitigen Trend zum PC hin folgend werden einige dieser Ergänzungen auf der Grundlage von DOS und Windows, andere auf der originären C-Plattform UNIX vorgestellt werden. Die entsprechenden Tools (Werkzeuge) existieren jedoch so oder in sehr ähnlicher Form auch auf den jeweils anderen Betriebssystemen.

make und Makefiles

Die Entwicklung vor allem größerer Softwareprojekte wird unter UNIX und von den meisten PC-Entwicklungssystemen mit dem make-Konzept unterstützt. Alternativ dazu bietet z.B. Borland bei seinem Turbo C sogenannte Projectfiles an, die jedoch   lediglich intern anders abgespeichert  einen ähnlichen Ansatz verfolgen.

Das elementare make-Kommando von UNIX wurde bereits angesprochen. Mit

make beispiel

wird aus dem Sourcefile beispiel.c das Programm beispiel (statt a.out) erstellt. In dieser Form entspricht make beispiel also dem Aufruf

cc -o beispiel beispiel.c

make erkennt dabei sogar, ob eine Compilation evtl. nicht erforderlich ist, weil das ausführbare Programm jünger als der C-Quelltext ist! Näheres zu make kann unter UNIX online über man make abgerufen werden.

Komplizierter ist die Situation jedoch bei Multi-File-Projekten, also Programmen, deren Code in mehreren Dateien abgelegt ist. Hier helfen sogenannte Makefiles, die bei UNIX standardmäßig auch Makefile oder makefile heißen. In diesen werden die verschiedenen Abhängigkeiten abgespeichert, so daß das make-Kommando gezielt genau und nur die gerade erforderlichen Neu-Compilationen veranlassen muß. Bei diesem Verfahren spricht man von inkrementellem Compilieren (und Linken), weil nur das erneut compiliert bzw. gebunden wird, was nicht bereits auf dem neuesten Stand vorhanden ist.

Das UNIX-Kommando mkmf (make makefile) erstellt (zu allen .c und .h Dateien in einem Verzeichnis ein entsprechendes Makefile; dies soll anhand des nachstehenden Mini-Projektes illustriert werden.

Gehen wir aus von der folgenden Datei main.c, die ein kleines Hauptprogramm beinhaltet.

/* main.c */
#include <stdio.h>
#include "main.h"
int a[MAXIMUM][MAXIMUM];
void main(void)
{
   InitArray(a);
   PrintArray(a);
} /* end main */

In diesem Hauptprogramm werden zwei Funktionen aufgerufen, die nicht in dieser Quelldatei zu finden sind; deklariert werden sie in der Headerdatei main.h, die nachstehend gezeigt wird.

Das Vorgehen, mit #ifndef abzufragen, ob eine symbolische Konstante bereits definiert worden ist, ist typisch für Headerdateien: damit soll verhindert werden, daß ein und dasselbe Headerfile in einem Projekt (versehentlich) mehrfach eingebunden wird, wo es nicht erforderlich oder eventuell auch gar nicht zulässig ist.

/* main.h */
#ifndef MAIN_H__
#define MAIN_H__
#define MAXIMUM (10)
void InitArray(int a[MAXIMUM][MAXIMUM]);
void PrintArray(int a[MAXIMUM][MAXIMUM]);
#endif /* MAIN_H__ */
Die Dateien init.c und print.c beinhalten jeweils den Quelltext für die Funktionen InitArray() bzw. PrintArray(), die in main.c verwendet werden.
/* init.c */
#include "main.h"
void InitArray(int a[MAXIMUM][MAXIMUM])
{
   int i, j;
   for (i=0; i<MAXIMUM; i++)
      for (j=0; j<MAXIMUM; j++)
         a[i][j]=i*MAXIMUM+j;
} /* end InitArray */

/* print.c */
#include "main.h"
#include <stdio.h>
void PrintArray(int a[MAXIMUM][MAXIMUM])
{
   int i, j;
   for (i=0; i<MAXIMUM; i++)
   {
      for (j=0; j<MAXIMUM; j++)
         printf("%4d ",a[i][j]);
      printf("\n");
   }
   printf("\n");
} /* end PrintArray */

Wird in einem Verzeichnis, in dem sich diese vier Dateien (main.c, main.h, init.c, print.c) befinden, das Kommando mkmf abgesetzt, so entsteht (hier nur gekürzt und vereinfacht wiedergegeben) folgendes Makefile. Der Backslash \ dient innerhalb des Makefiles (wie bei C) als Fortsetzungszeichen; das Zeichen # ist bei Makefiles der Beginn eines Zeilenkommentars.

# Makefile
# Dieses Makefile wurde mit mkmf automatisch erzeugt!

HDRS = main.h
LD = cc
OBJS = init.o \
main.o \
print.o
PROGRAM = main
SRCS = init.c \
main.c \
print.c

all: $(PROGRAM)

$(PROGRAM): $(OBJS) $(LIBS)

@echo "Linking $(PROGRAM) ..."

@$(LD) $(LDFLAGS) $(OBJS) $(LIBS) -o $(PROGRAM)

@echo "done"


clean:; @rm -f $(OBJS) core


###

init.o: main.h

main.o: /usr/include/stdio.h main.h

print.o: /usr/include/stdio.h main.h


Und so arbeitet make mit dem Makefile zusammen: wird make (oder hier synonym dazu: make all) aufgerufen, so wird nachgesehen, ob es das ausführbare Programm main bereits gibt; wenn ja, dann wird überprüft, ob es jünger als alle beteiligten .o-Dateien (Objectfiles) ist. Wenn nein, dann werden die Abhängigkeiten zwischen den .o-Dateien und den im Makefile genannten Headerfiles und (implizit) den gleichnamigen .c-Dateien überprüft.

Im nachstehenden Beispiel sind zunächst main und die Objectfiles gar nicht vorhanden, so daß make alles neu erstellen muß. Es werden also sämtliche .c-Dateien compiliert, dann sämtliche .o-Dateien zusammengebunden zur ausführbaren Programmdatei main.

Ein Ablaufprotokoll mit make unter UNIX[31]:

$ mkmf /* Makefile wird erstellt */

$ make


cc -O -c init.c

cc -O -c main.c

cc -O -c print.c

Linking main ...

done

$ touch init.c

/* touch tut so, als ob init.c verändert worden wäre, */

/* konkret wird das Dateizugriffsdatum aktualisiert, */

/* make auf diese Weise also "betrogen". */

$ make

cc -O -c init.c

Linking main ...

done

$ touch main.h

$ make all

cc -O -c init.c

cc -O -c main.c

cc -O -c print.c

Linking main ...

done

$ make all

Target `main' is up to date. Stop.

$


Debugger

Ein Debugger[32] ist ein Programm, das einem Software-Entwickler hilft, den eigenen Code auf Fehler zu untersuchen. Dabei ermöglicht ein guter Debugger es, sich den Code auf Assemblerebene und im Falle von symbolischen Debuggern im Quelltext (z.B. in C) anzusehen und ihn schrittweise ablaufen zu lassen. Dabei kann jede Anweisung einzeln abgearbeitet werden, es kann aber auch nach jedem Funktionsaufruf oder an sonst zu definierenden Stellen, sogenannten Breakpoints, angehalten werden. Der Debugger zeigt dabei wahlweise auch die aktuellen Registerinhalte, Funktionsaufrufe, Stack- und Variablenbelegungen an.

Solche Debugger gibt es praktisch für jede Betriebssystemplattform; die Arbeitsweise ist im wesentlichen immer die gleiche: man vermutet einen Fehler in einer Funktion f() und sieht sich daher den Ablauf dieser Funktion genauer an, zum Beispiel im Einzelschrittverfahren. Dabei betrachtet man auch die jeweils aktuellen Werte der Variablen, insbesondere eventueller Pointer[33].

Betrachten wir etwa das folgende kleine Programm test1.c: wir sehen recht schnell, daß hier mit a[4] ein ungültiger Array-Zugriff erfolgt, denn das deklarierte Array besteht lediglich aus den Komponenten a[0] bis a[3]. Dies wird jedoch kein normaler C-Compiler als Fehler melden, denn hinter der Indexschreibweise steckt bekanntlich die Zeigerarithmetik.

/* test1.c - Demo zum Debugger */
void main(void)
{
   int dummy=99, a[4], i;
   for (i=1; i<=4; i++)
   { /* Fehlerhafter Indexzugriff! */
      a[i]=i-4;
      printf("a[%d]=%5d\n",i,a[i]);
   }
} /* end main */
/* end of file test1.c */

In einem Debugger können wir uns nun die schrittweise Abarbeitung dieses kleinen Programms sowie die Belegung der verschiedenen Variablen-Speicherplätze ansehen. Insbesondere können wir somit nachvollziehen, wieso dieses kleine Programm in eine Endlosschleife gerät![34]

Beispielhaft für die Vielzahl von Debuggern, die es auf dem Markt gibt, werden hier zwei Schnappschüsse des Symantec C++ 7.0 Debuggers (für Windows 3.1/95 und Windows NT) gezeigt (Hier klicken für ein Bildchen.).

Im obigen Bild sehen wir die Entwicklungsumgebung des Symantec C++ Compilers[35]. Das Projekt TEST1.PRJ, bestehend aus der einen Quelltextdatei TEST1.C sowie   windows-typisch  einer Definitionsdatei TEST1.DEF, ist im Debug-Modus geladen, d.h. zu der ausführbaren Datei werden Debugger-Informationen hinzugefügt, die das symbolische Debuggen also mit Referenz auf den C-Quelltext ermöglichen.

Im nachstehenden Bildschirmschnappschuß sieht man das Quelltextfenster, in dem ein dicker Pfeil auf die aktuelle Quelltextposition verweist; daneben ist das "Application Window", in dem das ausführbare Programm   hier ein textbasiertes DOS-Programm  abläuft. Darunter werden in dem Fenster "Data/Object" die aktuellen Variablenbelegungen angezeigt (Hier klicken für ein Bildchen.).

Wie in dem Anwendungsfenster zu sehen ist: das Programm liefert nicht die erwarteten vier Ausgabezeilen für a[1] bis   fehlerhafterweise  a[4], sondern nach a[1] bis a[3] wird plötzlich a[0] gezeigt! Der Debugger hilft hier bei der Fehlersuche indem er zeigt, daß beim Index i=4 "a[4]" (d.h. *(a+4)) auf 0 gesetzt wird; und die Adresse a+4 verweist (bei diesem Compiler) gerade auf den Speicherplatz i (Hier klicken für ein Bildchen.)!

Daneben können, wie das obige Bild zeigt, auch der Assemblercode, die Speicher- oder Registerinhalte angesehen werden[36].

Profiler

Unter einem Profiler versteht man ein Dienstprogramm, welches die Performance (Leistungswerte) eines Programmes analysiert, indem es feststellt, wie lange welche Quellcode-Zeilen oder Module aktiv sind, wie oft Module oder einzelne Anweisungen aufgerufen werden (und von wem) oder auf welche Dateien wie oft und für wie lange von dem Programm zugegriffen wird. Gleichzeitig können weitere Aktivitäten überwacht bzw. protokolliert werden, z.B. Druckausgaben, Systemaufrufe, Tastatureingaben. Zur Ermittlung eventuell ineffizienter Programmteile dient der Profiler ähnlich wie ein Debugger bei der Fehlersuche.

Im folgenden soll aus Übersichtlichkeitsgründen nur ein ganz kleines Beispiel eines C-Programms mit Borland's PC-Turbo Profiler ansatzweise analysiert werden. Ohne die graphische Aufbereitung stehen unter UNIX ebenfalls Profiler zur Verfügung; mit man gprof kann der Manual-Eintrag zum Profiler gprof aufgelistet werden. Dieser korrespondiert mit der C-Compileroption -G, der eine Programmdatei für die Analyse mit gprof erstellt.

Zunächst der Quelltext des kleinen Demonstrationsprogramms, in dem exemplarisch untersucht werden soll, ob eine for-Schleife der Art

for (i=0; i<strlen(s); i++)

besonders ineffizient ist, da die Funktion strlen() hier bei jedem Schleifendurchlauf erneut aufgerufen wird[37].

/* proftest.c
 * Kleines Demonstrationsprogramm für den Turbo Profiler von Borland.
 */
#include <stdio.h>
#include <string.h>
void main(void)
{
   char s[] = "Programmieren in C";
   int i, sl;
   for (i=0; i<strlen(s); i++)
      putchar(s[i]);
   sl=strlen(s);
   for (i=0; i<sl; i++)
      putchar(s[i]);
} /* end main */

Das entsprechend (in diesem Fall mit Borlands Turbo C) compilierte EXE-Programm wird nun in den Profiler geladen.

Der Profiler kann nun eine Statistik über einen oder mehrere Programmläufe (im gezeigten Bild sind es zehn) erstellen. Im nachstehend gezeigten Bild sehen wir einen Bildschirmabzug des Turbo Profiler von Borland (Hier klicken für ein Bildchen.).

Im oberen Teil ist der Quellcode zu sehen, im unteren das sogenannte Execution Profile, das Ausführungsprofil. Hierbei wird gezeigt, wie oft (hier: 180 mal) und wie lange jeweils einzelne Anweisungen durchlaufen wurden. Hier wurde das putchar() in der Schleifenformulierung

for (i=0; i<strlen(s); i++)

180mal aufgerufen, dafür wurden 0,2519 Sekunden benötigt. Demgegenüber haben die putchar()-Aufrufe in der Schleifenkonstruktion

for (i=0; i<sl; i++)

bei gleicher Aufrufzahl nur 0,1775 Sekunden gebraucht.

Dieses einfache Beispiel zeigt somit schon recht gut, wie ein Profiler bei der statistischen Analyse und dem Aufspüren von Engpässen (bottle necks) behilflich sein kann (Hier klicken für ein Bildchen.).

Daneben erlauben Profiler in der Regel noch eine Reihe weiterer Untersuchungsmethoden, beispielsweise können die Aufruf-Hierarchien (welche Funktion ruft welche auf?) oder   etwas mehr low-level orientiert  wie im obigen Bild gezeigt der generierte Assemblercode angezeigt werden.

Cross-Reference-Listen mit cxref

Ein weiteres Hilfsmittel bei komplexeren Programmierungen sind sogenannte Cross-Reference-Listen, bei denen textlich oder graphisch dargestellt wird, welche Funktionen welche anderen Funktionen (in welchen Modulen) aufrufen und welche Konstanten (aus welchen Headerfiles) sie verwenden.

Nachstehend exemplarisch die Online-Hilfe zum UNIX-Kommando cxref, das eine solche Cross-Reference-Liste erzeugt. Danach folgt ein kleines Beispielprogramm (mit nur einem Modul) und die von cxref dazu produzierte   hier allerdings stark verkürzt wiedergegebene  Cross-Reference-Liste.


NAME

cxref - generate C program cross-reference

SYNOPSIS

cxref [options] files

DESCRIPTION

cxref analyzes a collection of C files and attempts to build a cross-

reference table. cxref utilizes a special version of cpp to include

#defined information in its symbol table. It produces a listing on

standard output of all symbols (auto, static, and global) in each file

separately, or with the -c option, in combination. Each symbol

contains an asterisk (*) before the declaring reference. Output is

sorted in ascending collation order (see Environment Variables below).

Options

In addition to the -D, -I, and -U options (which are identical to

their interpretation by cc (see cc(1)), cxref: recognizes the

following options:

-c Print a combined cross-reference of all input files.

-w num Width option; format output no wider than num

(decimal) columns. This option defaults to 80 if num

is not specified or is less than 51.

-o file Direct output to the named file.

-s Operate silently; do not print input file names.

-t Format listing for 80-column width.

-Aa Choose ANSI mode. If not specified, compatibility

mode (-Ac option) is selected by default.

-Ac Choose compatibility mode. This option is selected

by default if neither -Aa nor -Ac is specified.

EXTERNAL INFLUENCES

Environment Variables

LCCOLLATE determines the order in which the output is sorted.

If LCCOLLATE is not specified in the environment or is set to the

empty string, the value of LANG is used as a default. If LANG is not

specified or is set to the empty string, a default of ``C'' (see

lang(5)) is used instead of LANG. If any internationalization

variable contains an invalid setting, cxref behaves as if all

internationalization variables are set to ``C'' (see environ(5)).

International Code Set Support

Single- and multi-byte character code sets are supported with the

exception that multi-byte character file names are not supported.

DIAGNOSTICS

Error messages are unusually cryptic, but usually mean that you cannot

compile these files anyway.

EXAMPLES

Create a combined cross-reference of the files orange.c, blue.c, and

color.h:

cxref -c orange.c blue.c color.h

Create a combined cross-reference of the files orange.c, blue.c, and

color.h: and direct the output to the file rainbow.x:

cxref -c -o rainbow.x orange.c blue.c color.h

WARNINGS

cxref considers a formal argument in a #define macro definition to be

a declaration of that symbol. For example, a program that #includes

ctype.h will contain many declarations of the variable c.

cxref uses a special version of the C compiler front end. This means

that a file that will not compile probably cannot be successfully

processed by cxref. In addition, cxref generates references only for

those source lines that are actually compiled. This means that lines

that are excluded by #ifdefs and the like (see cpp(1)) are not cross-

referenced.


cxref does not parse the CCOPTS environment variable.


FILES

/lib/cpp C-preprocessor.

/lib/xpass Compatibility-mode special version of C compiler

front end.

/lib/xpass.ansi ANSI-mode special version of C compiler front end.


STANDARDS CONFORMANCE

cxref: SVID2, XPG2, XPG3

/* cxrefdemo.c */
#include <stdio.h>
void CallMe1(void);
void CallMe2(void);
void main(void)
{
   CallMe1();
} /* end main */
void CallMe1(void)
{
   CallMe2();
   CallMe2();
} /* end CallMe1 */
void CallMe2(void)
{
   printf("Hello, world!\n");
} /* end CallMe2 */

Cross-Reference-Liste zu cxrefdemo.c

cxrefdemo.c (stark gekürzt):

SYMBOL FILE FUNCTION LINE

BUFSIZ /usr/include/stdio.h -- *16

CallMe1() cxrefdemo.c -- *3 11

cxrefdemo.c main 8

CallMe2() cxrefdemo.c -- *4 17

cxrefdemo.c CallMe1 13 14

EOF /usr/include/stdio.h -- 96 *97

stdin /usr/include/stdio.h -- *100

stdout /usr/include/stdio.h -- *101