Eigene Bibliotheken

Werbung
Grundlagen der Programmiersprache C für
Studierende der Naturwissenschaften
Teil 10: Eigene Bibliotheken
Patrick Schön
Abteilung für Angewandte Mathematik
Vorlesung vom 14. Juli 2014
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Klausur
I
Die Klausur findet Montag (20. Juli) um 16:15 Uhr in diesem
Raum statt. Die Bearbeitungszeit ist 60 Minuten.
I
Eine Anmeldung zur Klausur ist nicht erforderlich.
I
Bringen Sie zur Klausur Ihren Studierendenausweis oder
Personalausweis mit.
I
Hilfsmittel wie Skripte oder Taschenrechner werden nicht
benötigt und sind auch nicht erlaubt.
I
Stoffumfang der Klausur entspricht den Folien und
Übungsaufgaben auf der Kurs-Homepage.
Zulassungsvoraussetzungen
Zur Klausur wird zugelassen. . .
I
wer den Kurs über das Zentrum für Schlüsselqualifikationen
(ZfS) belegt hat,
I
wer höchstens zwei Mal in den Übungen gefehlt hat,
I
und wer mindestens 50% der Punkte aus den Aufgaben erhalten
hat.
Korrektur und Ergebnis
I
Die Korrektur wird einige Tage nach der Klausur beendet sein.
Die Ergebnisse werden in anonymisierter Form online
veröffentlicht.
I
Die Klausur ist nicht benotet, d. h. es wird nur zwischen
bestanden und nicht bestanden unterschieden.
I
Die Klausureinsicht wird auf der Kurs-Homepage bekannt
gegeben
I
Die endgültige Leistungsverbuchung nimmt das ZfS vor. Dies
kann einige Wochen in Anspruch nehmen.
Nachklausur
Die Nachklausur findet voraussichtlich am Montag, den 24. August
2015 statt. Raum und Uhrzeit werden auf der Kurs-Homepage
bekannt gegeben.
Zur Nachklausur ist automatisch zugelassen,. . .
I
wer die Zulassungsvoraussetzungen erfüllt hat (s. o.),
I
wer die Klausur im ersten Versuch nicht bestanden hat
I
oder wer beim ersten Klausurtermin verhindert oder aus anderen
Gründen nicht anwesend war.
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Eigene Bibliotheken
Wenn einzelne Module ausreichend getestet und für gut befunden
wurden, kann man sie in eigenen Bibliotheken zusammenfassen.
Eigene Bibliotheken kann man erzeugen, indem man Objektdateien
unter der Verwendung des Kommandos
$ ar
zusammenfügt.
Eigene Bibliotheken
Wir möchten als Beispiel einen Bibliothek erstellen, die algebraische
Operationen mit vollbesetzten Matrizen vom Datentyp double
realisiert.
Für eine Matrix führen wir eine Struktur ein:
struct double_matrix{
double **index;
double *values;
int n, m;
};
I
n und m speichert die Dimension der Matrix
I
index soll den Zugriff auf die Matrix realisieren
I
values speichert die Einträge ai,j der Matrix
I
es soll gelten ai,j = Matrix.index[i][j]
Wdh: Matrix und Konsekutiver Speicher
Zunächst wir ein Speicherbereich für die ganze Matrix angelegt:
/* allocate memory first */
buffer = (double *) malloc(m*n*sizeof(double));
if(!buffer) return 1;
Dann initialisieren wir A als Doppelzeiger:
/* initialize A */
A = (double **) malloc(m*sizeof(double *));
if(!A) return 1;
for(i = 0; i < m; ++i)
A[i] = &buffer[i*n] ;
Indexzugriff über ai,j =A[i][j]. Das Freigeben des Speichers
erfolgt durch:
/* free storage */
free(A);
free(buffer);
Header matrix.h
Die Definition der Struktur schreiben wir in eine neue Header-Datei
matrix.h.
Um Schreibarbeit zu ersparen definieren wir den neuen Datentyp
Matrix, durch
struct double_matrix{
double **index;
double *values;
int n, m;
};
typedef struct double_matrix
Matrix;
Header matrix.h
Wir möchten für unseren Datentyp noch einige grundlegende
Operationen festlegen:
int initDoubleMatrix( Matrix *A, int m, int n);
void freeDoubleMatrix( Matrix *A );
void printDoubleMatrix( Matrix *A);
Diese Funktionen soll das Initialisieren einer Matrix unseres
Datentyps, die Rückgabe des angelegten Speichers und eine
einfache Ausgabe auf der Konsole realisieren.
Die tatsächliche Implementierung der Funktionen (Definition) steht
dann später in der Datei matrix.c.
matrix.c
int initDoubleMatrix( Matrix *A, int m, int n) {
int i, j;
// Speicher allozieren:
A->values = (double*)malloc(m*n*sizeof(double));
if( A->values == NULL ) return 1;
A->index = (double**)malloc(m*sizeof(double*));
if( A->index == NULL ) return 1;
A->m = m;
A->n = n;
//Speicherbloecke zuweisen:
for(i = 0; i < m; i++)
A->index[i] = &A->values[i*n];
// Matrix mit 0 Eintraegen initialisieren
for( i = 0; i < m; i++ )
for( j = 0; j < n; j++ )
A->index[i][j] = 0.;
return 0;
}
matrix.c
void freeDoubleMatrix( Matrix *A ) {
free(A->index);
free(A->values);
}
void printDoubleMatrix( Matrix * A) {
int i,j;
printf(" m = %d, n = %d \n", A->m, A->n);
//Ausgabe der Matrix:
for( i = 0; i < A->m; i++){
for( j = 0; j < A->n; j++ )
printf("%lf ", A->index[i][j] );
printf("\n");
}
}
Header einbinden
Die Quelltextdatei matrix.c muss dann neben den Header aus der
Standardbibliothek:
#include<stdio.h>
#include<stdlib.h>
auch den selbsterstellten Header matrix.h einbinden:
#include"matrix.h"
damit innerhalb von matrix.c der Datentyp Matrix bekannt ist.
Weitere Module erstellen
Wir möchten nun die Bibliothek um eine Funktion erweitern, die
Matrix-Vektor-Multiplikation mit unserem Datentyp realisiert.
Als n-Vektor verstehen wir ein double-Array der Länge n:
double *x = (double*)malloc( n * sizeof(double) );
Die Funktion können wir also folgendermaßen deklarieren:
void matrix_vector( Matrix *A, double *x, double *y);
Dabei soll eine Matrix A mit einem Vektor x multipliziert werden. Das
Ergebnis wird in y gespeichert.
Die Funktion soll dann in einer neuen Datei matrix_vektor.h
deklariert werden.
Header Guards
Um die Funktion
void matrix_vector( Matrix *A, double *x, double *y);
in matrix _ vektor.h zu deklarieren, müssen wir den Datentyp
Matrix kennen. Wir möchten also matrix.h einbinden, um sicher
zu sein, dass der Datentyp bekannt ist.
Um bei mehrfachem Einbinden, mehrfache Definitionen von Funktion
und Datentypen zu verhindern, baut man sogenannte Header Guards
ein:
#ifndef EXAMPLE_H
#define EXAMPLE_H
//some code
#endif // EXAMPLE_H
example.h
Header Guards
Header Guards (oder auch Include Guard, Include Wachter ) sorgen
dafür, dass eine Headerdatei nur einmal eingebunden wird.
Für jede Headerdatei wird ein eindeutiges Makro verwendet, hier:
EXAMPLE_H.
#ifndef EXAMPLE_H fragt ob EXAMPLE_HH schon definiert wurde,
falls ja ist die Datei schon kompiliert und der Präprozessor
überspringt den #ifndef ... #endif Block.
#ifndef EXAMPLE_H
#define EXAMPLE_H
//some code
#endif // EXAMPLE_H
matrix.h mit Header Guard
Die fertige Headerdatei matrix.h mit Header Guard, könnte dann
so aussehen:
1
2
#ifndef MATRIX_H
#define MATRIX_H
3
4
5
6
7
8
9
struct double_matrix{
double **index;
double *values;
int n, m;
};
typedef struct double_matrix
Matrix;
10
11
12
13
int initDoubleMatrix( Matrix *A, int m, int n);
void freeDoubleMatrix( Matrix *A );
void printDoubleMatrix( Matrix *A);
14
15
#endif // MATRIX_H
matrix_vektor.h
Für die Matrix-Vektor-Multiplikation vereinbaren wir den Header
matrix_vektor.h:
1
2
#ifndef MATRIX_VECTOR_H
#define MATRIX_VECTOR_H
3
4
#include "matrix.h"
5
6
void matrix_vector( Matrix *A, double *x, double *res);
7
8
#endif // MATRIX_VECTOR_H
matrix_vektor.c
Die tatsächliche Implementierung der Matrix-Vektor-Multiplikation
erfolgt dann in matrix_vektor.c:
1
2
3
#include<stdio.h>
#include<stdlib.h>
#include "matrix.h"
4
5
6
7
8
9
10
11
12
void matrix_vector( Matrix *A, double *x, double *res) {
int i, j;
for( i = 0; i < A->m; i++)
res[i] = 0.;
for( i = 0; i < A->m; i++)
for( j = 0; j < A->n; j++)
res[i] += A->index[i][j] * x[j];
}
Bibliothek erstellen
Insgesammt haben wir nun die Module
I matrix.c mit Header matrix.h
I
matrix_vektor.c mit Header matrix_vektor.h
Wir wollen nun diese Module zu einer Bibliothek verbinden. Zunächst
werden die Objektdateien matrix.c und matrix_vektor.o
erstellt über
$ gcc -c matrix.c
$ gcc -c matrix_vektor.c
Mit ar -r können sie diese zu einer Bibliothek
libDoubleMatrix.a zusammenfassen:
$ ar -r libDoubleMatrix.a matrix.o matrix_vector.o
Bibliothek verwenden
Möchten man nun die Funktionalität von libDoubleMatrix.a in
einem Programm main.c verwenden, müssen zunächst beide
Header eingebunden werden:
#include "matrix.h"
#include "matrix_vektor.h"
und linkt das Programm durch
$ gcc -o prog_name main.c libDoubleMatrix.a
Bibliothek verwenden
Alternativ kann man einen Header doubleMatrix.h erstellen, der
die Präprozessor-Direktiven :
#include "matrix.h"
#include "matrix_vektor.h"
enthält. Dann muss im Hauptprogramm nur doubleMatrix.h
eingebunden werden.
ist die Bibliothek in einem anderen Verzeichnis untergebracht, muss
das beim Linken mit der Option -L angegeben werden und die
Bibliothek wird mit -l angegeben.
$ gcc -o prog_name main.c -L<Verzeichnis> -lDoubleMatrix
Der Präfix lib und der Suffix .a werden dabei weggelassen.
einfaches Makefile
einfaches Makefile, um die Bibliothek libDoubleMatrix.a zu
erstellen:
1
2
install: matrix.o matrix_vector.o
ar -r libDoubleMatrix.a matrix.o matrix_vector.o
3
4
5
matrix.o: matrix.c
gcc -c matrix.c
6
7
8
matrix_vector.o: matrix_vector.c
gcc -c matrix_vector.c
9
10
11
clear:
rm -f *.o *.a
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Zeigertypen und -variablen
Für jeden Typ typename erhalten wir einen Zeigertyp
typename *
Ein Zeiger ist eine Variable von einem Zeigertyp. Der Typ typename
gibt an, auf welchen Typ ein Zeiger verweist:
typename *identifier;
Beispiele:
int *p;
double *q;
/* pointer to int */
/* pointer to double */
Der Adressoperator &
Jedes Objekt (Variablen, Konstanten, Funktionen, etc.) belegt Platz
im Speicher. Der Speicher eines Systems ist adressiert, d. h. jede
Position im Speicher hat eine Adresse.
Jedem Objekt ist damit eine eindeutige Adresse zugeordnet. Die
Adresse eines Objekts ermitteln wir mit dem Adressoperator &:
double x;
&x;
Der Typ des Ausdrucks &x ist ein Zeigertyp:
double *p = &x;
Initialisierung von Zeigern
In einem Zeiger kann die Adresse eines Objekts gespeichert werden.
double x;
double *p = &x;
int a;
int *q = &a;
Die Typen der Ausdrücke links und rechts einer Zuweisung müssen
übereinstimmen!
Ein besonderer Zeigerwert ist die Null:
double *p = 0;
int *q = 0;
Jeder Zeiger (egal welchen Typs) kann mit diesem Wert belegt
werden.
Dereferenzieren von Zeigern
Soll auf das Objekt zugegriffen werden, auf das ein Zeiger verweist,
spricht man vom Dereferenzieren des Zeigers.
Mit dem Inhaltsoperator * spricht man den Speicherbereich an, auf
den ein Zeiger verweist:
double x = 1.;
double *p = &x;
*p = 2.;
printf("x = %f\n", x);
/* x = 2. */
Das Dereferenzieren eines ungültigen Zeigers ist verboten und führt
in der Regel zum sofortigen Programmabbruch:
double *p = 0;
*p; /* error: p not properly initialized */
Übung zu Zeigern
Sei a vom Typ int und p vom Typ int *.
Welche der folgenden Anweisungen lässt p auf a zeigen?
1. *p = a;
2. p = *a;
3. &p = a;
4. p = &a;
Übung zu Zeigern
Sei a vom Typ int und p vom Typ int *.
Welche der folgenden Anweisungen lässt p auf a zeigen?
1. *p = a;
2. p = *a;
3. &p = a;
4. p = &a;
Übung zu Zeigern
Sei b vom Typ int und q vom Typ int *.
Welche der folgenden Anweisungen weist b den Wert des Objekts
zu, auf das q zeigt?
1. *q = b;
2. &b = q;
3. b = *q;
4. b = &q;
Übung zu Zeigern
Sei b vom Typ int und q vom Typ int *.
Welche der folgenden Anweisungen weist b den Wert des Objekts
zu, auf das q zeigt?
1. *q = b;
2. &b = q;
3. b = *q;
4. b = &q;
Felder
Für jeden Datentyp (elementar oder benutzerdefiniert) kann ein Feld
angelegt werden.
Ein Feld ist eine zusammenhängende Folge von Objekten gleichen
Typs:
a[0] a[1] a[2]
120
124
128
132
136
Zur Vereinbarung muss neben Typ und Name die Länge des Feldes
angegeben werden:
int a[3];
Zugriff auf Feldelemente
Auf die einzelnen Elemente eines Felds wird über einen Index
zugegriffen:
int a[3];
a[0], a[1], a[2];
/* access all entries */
In C beginnen Indizierungen immer mit dem Wert 0. Der letzte gültige
Index eines Felds der Länge N ist dann N − 1.
Achtung: Beim Elementzugriff findet keine Bereichsüberprüfung statt:
int a[3];
a[99]; /* wrong, invalid index */
Zusammenhang von Zeigern und Felder
Sei
int a[3];
ein Feld. Sein Name
a
ist ein Ausdruck vom Typ int *.
Sein Wert entspricht der Adresse des ersten Elements von a:
a == &a[0]
Die folgenden Zuweisungen sind daher identisch:
int *p = &a[0];
int *p = a;
Unterschiede von Zeigern und Feldern
Ein Zeiger ist eine Variable, deren Wert sich (durch Zuweisungen,
Inkrementoperatoren, etc.) ändern kann:
int a[3];
int *p = a;
++p; /* p points to a[0] */
Ein Feldname ist keine Variable. Ausdrücke wie
a = p
oder
++a
sind daher nicht erlaubt.
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Zeichenketten
Eine Zeichenkette ist eine zusammenhängende Folge von
char-Einträgen.
char str[32];
sprintf(str, "hello");
stdio.h */
/* sprintf is contained in
Ein besonderes Zeichen, das Terminierungszeichen ’\0’, markiert
das Ende einer Zeichenkette:
'h' 'e' 'l' 'l' 'o' '\0''X' 'Y' 'Z'
0
1
2
3
4
5
6
7
8
Stringkonstanten
Stringkonstanten sind Zeichenketten, die in doppelte
Anführungszeichen gesetzt sind:
"hello"
Ein Feld vom Typ char lässt sich mit einer Stringkonstanten
initialisieren:
char str[] = "hello";
Bei der Initialisierung eines char-Feldes mit einer Stringkonstanten
wird automatisch das Terminierungszeichen hinzugefügt. Das Feld
str belegt also 6 Byte:
sizeof(str) = 6.
Stringhandling
Die C-Standardbibliothek stellt eine Reihe von Funktionen zum
Arbeiten mit Strings bereit, z. B.:
size_t strlen(const char *str);
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
Alle hier beschriebenen Funktionen sind in der Datei string.h
enthalten.
Übung zu Vereinbarungen
Welche der folgenden Vereinbarungen sind korrekt?
Aus welchen Gründen sind die übrigen falsch?
1. double größe;
2. unsigned int u;
3. char c = ’z’;
4. char str[] = ’hallo’;
5. double b[ 2 ] = { 9., 3. };
Übung zu Vereinbarungen
Welche der folgenden Vereinbarungen sind korrekt?
Aus welchen Gründen sind die übrigen falsch?
1. double größe;
2. unsigned int u;
3. char c = ’z’;
4. char str[] = ’hallo’;
5. double b[ 2 ] = { 9., 3. };
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Vereinbarung von Strukturen
Strukturen gehören zu den benutzerdefinierten Datentypen.
Eine Struktur wird definiert durch Angabe des Schlüsselworts
struct, ihres Namens und ihrer Elemente in geschweiften
Klammern mit abschließendem Strichpunkt.
Beispiel:
struct Student
{
char name[64];
char matrikelnr[16];
};
Variablen von strukturierten Datentypen
Eine Variable vom Typ struct Student wird wie folgt definiert:
struct Student student;
Mit der Definition kann eine Variable von strukturiertem Datentyp
initialisiert werden:
struct Student student = {"Max Mustermann",
"1234567890"};
Zeiger auf Objekte von strukturiertem Datentyp kann man so
anlegen:
struct Student *pStudent = &student;
Merke: Das Schlüsselwort struct muss immer mit angegeben
werden.
Auswahloperatoren
Die Auswahloperatoren . und -> erlauben den Zugriff auf die
Elemente einer Struktur.
Beispiel:
struct Student student;
sprintf(student.name, "Max Mustermann");
sprintf(student.matrikelnr, "1234567890");
Der Auswahloperator -> darf nur auf Zeiger angewandt werden:
student->name;
/* equals: (*student).name */
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Funktionen vereinbaren
Bei Funktionen unterscheidet man zwischen Deklaration und
Definition:
I
Die Deklaration vereinbart lediglich Rückgabetyp, Namen und
Parameterliste der Funktion.
I
Erst die Definition legt die Aktionen fest, die bei Funktionsaufruf
ausgeführt werden sollen.
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
square ist eine Funktion, die eine double-Größe erwartet und
ihr Quadrat zurückgibt:
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
square ist eine Funktion, die eine double-Größe erwartet und
ihr Quadrat zurückgibt:
double square(double x);
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
sort ist eine Funktion, die ein Feld elements von
Integer-Zahlen und dessen Länge N als Parameter erhält und
einen Zeiger auf das erste Feldelement zurückgibt.
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
sort ist eine Funktion, die ein Feld elements von
Integer-Zahlen und dessen Länge N als Parameter erhält und
einen Zeiger auf das erste Feldelement zurückgibt.
int *sort(int elements[], int N);
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
innerProduct ist eine Funktion, die zwei konstante Zeiger auf
struct Vector übergeben bekommt und eine double-Größe
zurückgibt:
Übung zu Funktionsdeklarationen
Wie lauten die Deklarationen der folgenden Funktionen?
I
innerProduct ist eine Funktion, die zwei konstante Zeiger auf
struct Vector übergeben bekommt und eine double-Größe
zurückgibt:
double innerProduct(const struct Vector *x,
const struct Vector *y);
Funktionszeiger
Ist der Prototyp einer Funktion (bestehend aus Rückgabetyp, Namen
und Parameterliste) bekannt, lässt sich ein Zeiger auf die Funktion
definieren.
Beispiel:
double square(double x);
double (*f)(double) = square;
Um eine Funktion über einen Zeiger auszuwerten, gibt es mehrere
Möglichkeiten:
double x = f(2.);
double x = (*f)(2.);
/* x = 4. */
/* x = 4. */
Übergabe per Wert
Betrachten wir die folgende einfache Funktion:
void setToZero(double x) {
x = 0;
}
Beim Funktionsaufruf
double x = 1.;
setToZero(x);
printf("x = %f\n", x);
/* x = 1. */
wird lediglich der Funktionswert des Arguments an die Funktion
übergeben.
Übergabe per Referenz
Als Übergabe per Referenz bezeichnet man die Übergabe von
Zeigern an Funktionen:
void setToZero(double *x) {
*x = 0;
}
...
double x = 1.;
setToZero(&x);
printf("x = %f\n", x);
/* x = 0. */
Zwar wird wieder eine Kopie an die Funktion übergeben (die des
Zeigers!). Die Funktion greift aber auf den Speicherplatz von x zu,
und die lokalen Änderungen werden im gesamten Programm
wirksam.
Übergabe per Referenz
Eine Funktion erhält bei ihrem Aufruf Kopien der Argumente (call by
value).
Das gilt auch für den Fall, dass Zeiger an eine Funktion übergeben
werden:
void swap(int *a, int *b);
int main(void)
{
int a = 1, b = 2;
swap(&a, &b);
return 0;
}
Jede Kopie eines Zeigers verweist aber auf die gleiche
Speicheradresse. Wird diese beschrieben, ändert sich der Zustand
der dort abgelegten Variable.
Programmbeispiel Variablentausch
Quelltext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
void swap ( int *p, int *q )
{
int temp;
temp = *p;
*p = *q;
*q = temp;
}
int main ( void )
{
int a = 1, b = 2 ;
int *p = &a, *q = &b;
printf( "a = %d, b = %d\n", a, b );
swap( p, q );
printf( "a = %d, b = %d\n", a, b );
return 0;
}
/* a = 1, b = 2 */
/* a = 2, b = 1 */
Übergabe per Referenz II
Felder werden stets per Referenz (nicht als Kopie) an Funktionen
übergeben:
void initialize(double x[], int len)
{
int i;
for(i = 0; i < len; ++i)
x[i] = 0.;
}
int main(void)
{
double x[3];
initialize(double x[3]);
return 0;
}
/ * x = 0 */
Die obige Funktion ändert tatsächlich die in x gespeicherten Werte.
Die Funktion main
Ein C-Programm besteht immer aus mindestens einer Funktion: der
Funktion main. Sie markiert den Programmanfang.
Wir verwenden als Rückgabetyp von main immer int. Die
Parameterliste von main kann leer sein:
int main(void);
Der Funktion können aber auch Parameter übergeben werden:
int main(int argc, char *argv[]);
→ Das erste Argument gibt die Anzahl der Parameter an (inklusive
des Namens des Executables).
→ Beim zweiten Argument argv handelt es sich um ein Feld von
Zeichenketten.
Parameter übergeben an main
1
#include <stdio.h>
2
3
4
int main(int argc, char *argv[]) {
int i;
5
for(i=0; i < argc; i++) {
printf("argv[%d] = %s ", i, argv[i]);
printf("\n");
}
return 0;
6
7
8
9
10
11
}
Rückgabewerte von main
Der Rückgabewert von main gibt üblicherweise Auskunft über den
Zustand des Programms bei Programmende:
→ Typischerweise bedeutet einer der Werte 0 oder
EXIT_SUCCESS, dass das Programm normal beendet wurde:
return 0;
return EXIT_SUCCESS;
→ Ein Rückgabewert ungleich 0 oder EXIT_FAILURE hingegen
weist auf einen fehlerhaften oder ungewöhnlichen Abbruch hin.
return 1;
return EXIT_FAILURE;
Die Konstanten EXIT_SUCCESS und EXIT_FAILURE sind in der
Datei stdlib.h definiert. Die Datei wird eingebunden mit:
#include <stdlib.h>
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Funktionen zur dynamischen Speicherverwaltung
Die folgenden Methoden zur dynamischen Speicherverwaltung sind
in der Datei stdlib.h enthalten:
/* allocate memory */
void *malloc(size_t size);
/* allocate and initialize memory */
void *calloc(size_t number, size_t size);
/* deallocate memory */
void free(void *ptr);
/* try to resize memory at given address */
void *realloc(void *ptr, size_t size);
Anfordern von Speicher
In der Regel müssen wir Speicher für eine Zahl n von Objekten von
bekanntem Typ typename anlegen.
Der Speicherbedarf hierfür beträgt gerade
n*sizeof(typename);
Beispiele (Speicher für 100 Integer-Werte reservieren):
int *p = (*int) malloc(100*sizeof(int));
int *q = (*int) calloc(100,sizeof(int));
Was ist der Unterschied zwischen malloc und calloc?
Fehlerbehandlung
Der Rückgabewert der Methoden zum Anfordern von Speicher
void *malloc(size_t size);
void *calloc(size_t number, size_t size);
void *realloc (void *ptr, size_t size);
ist jeweils ein Zeiger vom Typ void *.
Im Fehlerfall, d. h. konnte der Speicher nicht angelegt werden, hat
der Rückgabewert den Wert Null,sprich der Zeiger ist ungültig.
Beispiel:
size_t size = 4;
void *p = malloc(size);
if(!p)
printf("error");
Zugriff auf dynamisch erzeugten Speicher
Mit malloc oder calloc angelegter Speicher ist
zusammenhängend. Auf die Elemente des erzeugten Speicherblocks
wird über den Operator [] und einen Index zugegriffen.
Beispiel:
int i, size = 4;
int *p = (int*) malloc(size*sizeof(int));
assert(p);
/* remember to include assert.h */
for(i = 0; i < size; ++i)
p[i] = 0;
Freigeben von Speicher
Dynamisch angelegter Speicher muss freigegeben werden, wenn er
nicht mehr verwendet wird und anderen Prozessen zur Verfügung
stehen soll.
Die Funktion
void free(void *ptr);
gibt den Speicherbereich, auf den ptr zeigt und der über malloc
oder calloc angelegt wurde, wieder frei.
Beispiel:
int *p = (int*) malloc(4*sizeof(int));
...
free(p);
Auf freigegebenen Speicher darf nicht mehr zugegriffen werden.
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Ausdrücke
Ein Ausdruck ist eine Kombination aus Variablen, Konstanten,
Operatoren und Rückgabewerten von Funktionen.
I
Die Auswertung eines Ausdrucks ergibt einen Wert
I
ein Ausdruck hat immer einen (Daten-) Typ.
I
Ausdrücke können durch Operatoren zu neuen Ausdücken
zusammengesetzt werden.
I
Ein Ausdruck wird durch ein nachfolgendes Semikolon zur
Anweisung. Der Zuweisungsausdruck y = sqrt(x) wird zur
Zuweisungsanweisung y = sqrt(x);.
Ausdrücke
Beispiel:
int a, b;
double x, y;
a = 4 + 3;
x = 4.0;
y = sqrt(x);
I
Konstante 4 hat den Wert 4 und den Typ int. Die Konstante 4.0
hat den Wert 4 und den Typ double.
I
der Ausdruck 4 + 3 hat den Wert 7 und den Typ int.
I
Die Variable b den Typ int und unbestimmten Wert.
I
Der Zuweisungsausdruck (Beispiel (y = sqrt(x))) hat den
Typ und den Wert des linken Operanten nach Auswertung der
Zuweisung.
Seiteneffekte
Jeder Zuweisungsausdruck darf selbst wieder in einem Ausdruck
auftauchen:
int a = 1, b;
b = (a += 1) + 1;
/* a = 2, b = 3 */
Solche verschachtelten Ausdrücke verursachen sog. Seiten- oder
Nebeneffekte, d. h. neben der Auswertung des Ausdrucks wird durch
die Zuweisung auch der Wert einer Variablen geändert.
Seiteneffekte machen den Code schwer lesbar und sind damit
potentielle Fehlerquellen.
Beispiel (Inkrement-Operatoren):
int
i =
j =
k =
i, j, k;
1;
i++; /* j = 1, side effect: i = 2 */
++i; /* k = 3, side effect: i = 3 */
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Unäre, binäre und ternäre Operatoren
Durch Operatoren werden Ausdrücke (Argumente, Operanden)
verknüpft und zu zusammengesetzten Ausdrücken. Operatoren
können ein, zwei oder drei Argumente haben.
Ein Operator heißt. . .
unär, falls der Operator auf nur einen Operanden wirkt.
Beispiel: +, - (Vorzeichenoperatoren),
binär, falls der Operator auf zwei Operanden wirkt.
Beispiel: +, -, *, / % (arithmetische Operatoren),
ternär, falls der Operator auf drei Operanden wirkt.
Beispiel: ?: (Konditionaloperator).
Priorität und Assoziativität von Operatoren
Typ und Wert eines zusammengesetzten Ausdrücks müssen
eindeutig bestimmt sein.
Ihre Auswertung erfolgt unter Berücksichtigung von. . .
Priorität Rangfolge, die festlegt, in welcher Reihenfolge
Operatoren innerhalb eines Ausdrucks ausgewertet
werden
Assoziativität gibt an, in welcher Richtung Operatoren und
Operanden zusammengefasst werden
Operatoren in C
Operator
Assoziativität
() [] -> .
! ∼ ++ −− + - * & (type) sizeof
* / %
+ << >>
< <= > >=
== !=
&
ˆ
|
&&
||
?:
= += -= *= /+ %= &= ˆ= |= <<= >>=
,
von links nach rechts
von rechts nach links
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von links nach rechts
von rechts nach links
von rechts nach links
von links nach rechts
Tabelle : Operatoren absteigend nach Priorität gelistet1
1 Quelle:
Kernighan, Ritchie: Programmieren in C
Beispiele: Priorität und Assoziativität
I
Das Assoziativgesetz, Punkt vor Strich, soll gelten. Die
arithmetischen Operatorn +,- binden schwächer als *,/.
Der Audruck 1.0 + 2.0 * 3.0 hat den Wert 7.
I
Der Klammeroperator () hat höhere Prioriät als +,-,*,/. Der
Ausdruck (1.0 + 2.0 ) * 3.0 hat den Wert 9.
I
Vergleichsoperatoren < werden von links nach rechts gelesen.
Beispiel:
double a = .1, b = .2, c = .3;
printf("%d\n", a < b < c );
printf("%d\n", a < ( b < c ) );
Beispiele: Priorität und Assoziativität
I
Das Assoziativgesetz, Punkt vor Strich, soll gelten. Die
arithmetischen Operatorn +,- binden schwächer als *,/.
Der Audruck 1.0 + 2.0 * 3.0 hat den Wert 7.
I
Der Klammeroperator () hat höhere Prioriät als +,-,*,/. Der
Ausdruck (1.0 + 2.0 ) * 3.0 hat den Wert 9.
I
Vergleichsoperatoren < werden von links nach rechts gelesen.
Beispiel:
double a = .1, b = .2, c = .3;
printf("%d\n", a < b < c );
printf("%d\n", a < ( b < c ) );
\\ 0
\\ 1
Zuweisungsoperator
Zuweisungsoperatoren haben mit die geringste Priorität aller
Operatoren. So wird sichergestellt, dass zuerst der Ausdruck auf der
rechten Seite der Zuweisung ausgewertet wird.
Beispiel:
double x = (1.0 + 2.0 ) * 3.0 ;
Entscheidend ist, dass auf der linken Seite einer Zuweisung nur
Variablen zugelassen sind:
x
= 1. /* Okay. */
1. = 1. /* Error! */
a*x = 1. /* Error! */
Zuweisungen sind nicht kommutativ! Die Variable, die mit einem Wert
belegt werden soll, steht links:
a = b
b = a
/* assign value of b to a */
/* assign value of a to b */
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Implizite Typkonversion
Operatoren können Variablen verschiedener Datentypen verbinden.
int x = 1;
double y = 2.5;
double sumDouble = x + y;
int sumInt = x + y;
printf("sumInt = %d\n", sumInt);
/* Output: sumInt = 3 */
printf("sumDouble = %lf\n", sumDouble);
/* Output: sumDouble = 3.500000 */
I
x + y hat den Datentyp double, bei der Addition wird x in ein
double konvertiert.
I
sumInt hat den Typ int, d.h. x+y wird in ein int konvertiert.
Implizite Typkonversion
I
Haben die Operanden eines arithmetischen Operators den
gleichen Typ, so ist auch das Ergebnis von diesem Datentypen.
I
Sind die Operanden von unterschiedlichem Typ, so wird der
Operand mit ”ungenauerem” Typ, zuerst zum ”genaueren” Typ
konvertiert, bspw. ist 1 / 5. eine Konstante vom Typ
double.
I
Konvertierung von double nach int erfolgt durch Abschneiden,
nicht durch Rundung!
int n = 3.7;
printf("n = %d\n", n); /* Output: n = 3 */
Implizite Typkonversion
I
Klammern entscheiden die Reihenfolge in der die Operatoren
angewendet werden.
double
double
double
double
x_1
x_2
x_3
x_4
=
=
=
=
printf("x_1
printf("x_2
printf("x_3
printf("x_4
=
=
=
=
2 /
2 /
10.
10.
4;
4.;
* 2 / 4;
* (2 / 4);
%lf\n",
%lf\n",
%lf\n",
%lf\n",
x_1);
x_2);
x_3);
x_4);
Implizite Typkonversion
I
Klammern entscheiden die Reihenfolge in der die Operatoren
angewendet werden.
double
double
double
double
x_1
x_2
x_3
x_4
=
=
=
=
printf("x_1
printf("x_2
printf("x_3
printf("x_4
I
=
=
=
=
2 /
2 /
10.
10.
4;
4.;
* 2 / 4;
* (2 / 4);
%lf\n",
%lf\n",
%lf\n",
%lf\n",
x_1);
x_2);
x_3);
x_4);
Der Code produziert den Output:
x_1
x_2
x_3
x_4
=
=
=
=
0.000000
0.500000
5.000000
0.000000
Explizite Typkonversion
I
Man kann dem Compiler mitteilen, in welcher Form eine Variable
interpretiert werden muss.
I
Man stellt dazu den Ziel-Typ in Klammern voran
double x_1 = (double) (2 / 4);
double x_2 = (double) 2 / 4;
double x_3 = 2 / (double) 4;
Explizite Typkonversion
I
Man kann dem Compiler mitteilen, in welcher Form eine Variable
interpretiert werden muss.
I
Man stellt dazu den Ziel-Typ in Klammern voran
double x_1 = (double) (2 / 4);
double x_2 = (double) 2 / 4;
double x_3 = 2 / (double) 4;
I
Die Variablen haben den Wert:
x_1 = 0.000000
x_2 = 0.500000
x_3 = 0.500000
Gliederung
Organisatorisches zur Klausur
eigene Bibliotheken
Zeiger und Felder (Wdh.)
Zeichenketten (Wdh.)
Strukturierte Datentypen (Wdh.)
Funktionen und Funktionsaufrufe (Wdh.)
Dynamische Speicherverwaltung (Wdh.)
Ausdrücke, Anweisungen (Wdh.)
Operatoren (Wdh.)
Typkonversion (Wdh.)
Testfragen
Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der
folgenden Anweisungen lässt p auf a zeigen?
1. *p = a;
2. p = *a;
3. &p = a;
4. p = &a;
Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der
folgenden Anweisungen lässt p auf a zeigen?
1. *p = a;
2. p = *a;
3. &p = a;
4. p = &a;
Aufgabe 2: Sei a vom Typ int und p vom Typ int * . Welche der
folgenden Anweisungen weist a den Wert des Objekts zu, auf das p
zeigt?
1. *p = a;
2. &a = p;
3. a = *p;
4. a = &p;
Aufgabe 1: Sei a vom Typ int und p vom Typ int * . Welche der
folgenden Anweisungen lässt p auf a zeigen?
1. *p = a;
2. p = *a;
3. &p = a;
4. p = &a;
Aufgabe 2: Sei a vom Typ int und p vom Typ int * . Welche der
folgenden Anweisungen weist a den Wert des Objekts zu, auf das p
zeigt?
1. *p = a;
2. &a = p;
3. a = *p;
4. a = &p;
Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu
p = &a[2] äquivalent, welche?
1. p = a[2]
2. &p = a[2]
3. *p = a + 2
4. p = a + 2
Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu
p = &a[2] äquivalent, welche?
1. p = a[2]
2. &p = a[2]
3. *p = a + 2
4. p = a + 2
Aufgabe 4: Zum Speichern einer Matrix werde die Variable double
**A verwendet. Welche der folgenden Ausdrücke sind zu A[i][j]
äquivalent?
1. *(A[i]+j)
2. *&A[i][j]
3. *(*(A+i)+j)
4. **(A+i+j)
Aufgabe 3: Durch int a[5]; wurde ein Feld von Integer-Werten vereinbart. Sei p vom Typ int * . Eine der folgenden Zuweisungen ist zu
p = &a[2] äquivalent, welche?
1. p = a[2]
2. &p = a[2]
3. *p = a + 2
4. p = a + 2
Aufgabe 4: Zum Speichern einer Matrix wurde die Variable double
**A verwendet. Welche der folgenden Ausdrücke sind zu A[i][j]
äquivalent?
1. *(A[i]+j)
2. *&A[i][j]
3. *(*(A+i)+j)
4. **(A+i+j)
Autoren
Autoren die an diesem Skript mitgewirkt haben:
I
2011–2014 : Christoph Gersbacher
I
2014–2015 : Patrick Schön
This work is licensed under a Creative Commons
Attribution-ShareAlike 4.0 International (CC
BY-SA 4.0) License.
http://creativecommons.org/
licenses/by-sa/4.0/legalcode
Zugehörige Unterlagen
Herunterladen