Einführung in C - Teil 2

Martin Sulzmann

Inhalt

Funktionen

Eine Liste von Funktionsprototypen. Funktionsprototypen spezifizieren wie die einzelnen Funktionen verwendet werden. Alternative Name für Funktionsprototyp sind Funktionsdeklaration oder auch Funktionssignatur.

int func1(int, float);
void func2(int, float);
int func3();
void func4();

Funktionen - Parameterübergabe

Beachte

In C/C++ immer call-by-value (Wertübergabe).

In Java

Beispiel 1 (call-by value)

int inc(int x) {
    x++;
    return x;
}

void test1() {
   int y,z;
   y = inc(3+4);
   z = inc(y);
}

Die Auswertungsschritte im einzelnen.

Betrachte y = inc(3+4):

  1. Auswertung Argument 3+4 liefert 7.

  2. Aufruf inc(7) liefert 8.

  3. Bindung von y an den Wert 8.

Betrachte z = inc(y):

  1. Auswertung von y liefert Wert 8.

  2. Aufruf von inc(8) liefert Wert 9.

  3. Bindung von z an den Wert 9.

Beispiel 2 (call-by-value)

Betrachte Strukturen (= "public classes").

struct position {
  int x;
  float y;
};

void f(struct position p) {
   p.x++;
   // p.x == 2
}

void test2 () { 
  struct position q;
  q.x = 1;
  q.y = 2.0;

  f(q);
  // q.x == 1
}

Betrachte f(q):

  1. Aufruf von Funktion f. Bindung von formalen Parameter an die Werte von q.

  2. Innerhalb von f, inkrementiere x Position.

  3. Nach Rücksprung aus f, Werte von q sind unverändert!

Vergleich zu Java

Gleiches Verhalten im Falle von Beispiel 1 (da primitiver Datentyp int).

Unterschied im Fall von Beispiel 2. In Java, call-by-reference im Fall von Objekten komplexer Klassen. Struktur position ist eine komplexe "public class".

Deshalb in Java. Nach Rücksprung aus f gilt: q.x == 2!

Zusammengefasst

Variablen, Ausdrücke, Werte

Betrachte

3+4

Dies ist ein arithmetischer Ausdruck dessen Auswertung liefert den Wert 7.

Betrachte

int x;
x = 1;

Der Variablen x wird der Wert 1 zugewiesen. Im Detail bedeutet dies:

  1. Die rechte Seite der Zuweisung x = 1 wird ausgewertet (liefert hier 1).

  2. Dieser Wert wird dann x zugewiesen.

Betrachte

int x, y;
x = 1;
y = x + 2;

Die rechte Seite x + 2 wird ausgewertet.

  1. In einem Zwischenschritt wird x ausgewertet. Dies liefert den Wert 1.

  2. In einem weiteren Zwischenschritt wird der Ausdruck 1 + 2 ausgewertet. Dies liefert den Wert 3.

  3. Der Wert 3 wird y zugewiesen.

Betrachte

  struct position p,q;
  p.x = 1;
  p.y = 2.0;

  q = p;

Strukturen sind auch Werte. Der Wert von p ist {1,2.0}. Eine Struktur vom Typ struct position bei der x den Wert 1 und y den Wert 2.0 hat.

Betrachte die Zuweisung q = p.

  1. Rechte Seite wird ausgwertet. Liefert {1,2.0}.

  2. Dieser Wert wird q zugewiesen. D.h. wird kopieren p Elementweise nach q.

Parameterübergabe

In C/C++ immer call-by-value (Wertübergabe).

Java verwendet call-by-value für primitive Typen (int, float, ...). Für komplexe Typen (Objekte) wird call-by-reference verwendet. Call-by-reference bedeutet:

Referenzen in C/C++

C/C++ erlaubt es Referenzen direkt darzustellen und zu manipulieren. Referenzen werden mit Hilfe von Zeigern (Pointern) und Zeigeroperationen emuliert.

Das ganze an unserem Beispiel.

void f2(struct position* p) {
   (*p).x++;
}

void test3() { 
  struct position q;
  q.x = 1;
  q.y = 2.0;

  f2(&q);
}

Zusammengefasst:

  1. struct position* p Deklaration einer Referenz auf einen Wert mit Typ struct position.

  2. Zugriff auf den Wert via *p.

  3. Falls q eine Struktur position, liefert &q die Referenz.

In C++ gibt es eine "direktere" Notation für call-by reference.

void f3(struct position &p) {
   p.x++;
}

int main () { 
  struct position q;
  q.x = 1;
  q.y = 2.0;

  f3(q);
 return 0;
}

Kompletter Programmcode

Zusammenfassung obiger Programmfragmente. Zusätzlich betrachten wir Varianten von inc welche Referenzparameter verwenden.



#include <stdio.h>


int inc(int x) {
    x++;
    return x;
}


void test1() {
   int y,z;
   y = inc(3+4);
   z = inc(y);
}


// inc Variante mit zusaetzlichem Referenzparameter
void inc_a(int x, int* y) {
  x++;
  *y = x;
}

// inc nur mit Referenzparameter
void inc_b(int* x) {
  (*x)++;
}

void test1_a() {
   int y,z;
   inc_a(3+4, &y);
   inc_a(y, &z);
}

void test1_b() {
   int y,z;
   y = 3+4;
   inc_b(&y);
   inc_b(&y);
   z = y;
}


struct position {
  int x;
  float y;
};

void f(struct position p) {
   p.x++;
   // p.x == 2
}

void test2 () { 
  struct position q;
  q.x = 1;
  q.y = 2.0;

  f(q);
  // q.x == 1
  if(q.x == 1)  
    printf("\n q unveraendert");

}

void f2(struct position* p) {
   (*p).x++;
}

void test3 () { 
  struct position q;
  q.x = 1;
  q.y = 2.0;

  f2(&q);
  if(q.x != 1)  
    printf("\n q hat sich geaendert");
}

int main() {
  struct position p,q;
  p.x = 1;
  p.y = 2.0;

  q = p;

  printf("\n %d %f", p.x, p.y);
  printf("\n %d %f", q.x, q.y);

  test1();
  test1_a();
  test1_b();
  test2();
  test3();

  return 1;
}

Mehrere Funktionen (Deklaration versus Definition)

// Deklaration, Vorwärtsreferenz
int f(int);
int g(int);

// Definition
int f(int x) {
  if(x==1) return g(x);
  return 1;
}

int g(int y) {
  if(y==0) return f(y+1);
  return 2;
}

Wieso inferriert der Kompiler nicht automatisch diese Vorwärtsreferenzen? Zum Teil historisch bedingut. Aus Effizienzgründen versuchte man ursprünglich den Quelltext in einem Durchgang ("pass") zu kompilieren.

Funktionen und statische Variablen

Beispiel

#include <stdio.h>

int count(void) {
  static int i = 0; // statische Variable
  i++;
  return i;
}

int main () {
  printf("Aufruf Nr. %d \n", count());
  printf("Aufruf Nr. %d \n", count());
  printf("Aufruf Nr. %d \n", count());
  return 0;
}

Funktionsbezeichner ("Function Qualifier")

Funktionsbezeichner: static, extern, inline

static void f(void) {}
extern void f(void); 
inline void g(void) {}

Beispiel.

inline int inc(int x) { return x+1; }

int main() {
  int y = 1;
  int z;

  z = inc(y+3); // Rechte Seite ersetzt durch "(y+3)+1"
}

Beachte:

inline sollte nur auf "kleine", Performanzkritische Funktionen angewandt werden. Wieso? Durch inline spart man sich den Funktionsaufruf (die Parameterübergabe, anlegen von lokalen Variablen etc) aber der Programmcode wird dupliziert.

inline kann nur auf Funktionen und statische Methoden (in C++) angewandt werden. Wieso? Der Compiler muss statisch (= zur Compilezeit) Zugriff auf den Programmcode der Funktion haben. Dies ist nur für Funktionen und statische Methoden (in C++) möglich, nicht aber für virtuelle Methoden (in C++).

Header und Source Files

Header und Source Beispiel

// fact.h Header mit Funktions-prototyp
int fact(int n);
// fact.c Source
#include "fact.h"
int fact(int n) {
  return n ? n*fact(n-1) : 1;  
}
// fact_main.c Anwendung
#include <stdio.h>
#include "fact.h"

int main () {
  printf("fact(3)=%d \n", fact(3));
  return 0;
}

Beachte:

Im .h File stehen in der Regel nur Deklarationen.

.c File "inkludiert" die Deklaration. Dadurch wird garantiert, dass die Deklaration mit der Definition übereinstimmt.

#include sucht nach File mit dem Namen.

#include "file.h" sucht nach file.h ausgehend vom aktuellen Projektordner.

#include <file.h> sucht nach file.h unter Berücksichtung von Umgebungsvariablen (dadurch können Bibliotheken lokalisert werden) und sucht erst dann ausgehend vom aktuellen Projektordner.

Separate Kompilierung und Linken

IDEs wie Visual Studio:

Kommandzeile, z.B. cygwin

Per Hand

$gcc.exe -c fact.c -o fact.o
$gcc.exe fact.o fact_main.c

make

    Ziel : Quellen
           Aktion
CC=gcc.exe
fact_main.exe : fact_main.o fact.o
                $(CC) fact.o fact_main.o -o fact_main.exe
fact_main.o: fact_main.c
                $(CC) -c fact_main.c
fact.o: fact.c fact.h
                $(CC) -c fact.c

Präprozessor

Präprozessor

      #include and #define
      #ifdef, #ifndef, #endif, ... 

#include

#include "filename" 
#include <filename>

#define (Macro)

Form

#define Name Ersetzung
#include <stdio.h>

#define PI 3.141592654
#define MAX(A,B) ((A)>(B)?(A):(B))
#define SQUARE(X) X*X

int main() {
  int x, y;
  x = 1;
  y = 2;

  printf("%d \n", SQUARE(x+y));

  printf("%d \n", SQUARE((x+y)));

  return 1;
}

Konditionale Präprozessor Befehle

Zwei Standardbeispiele

Konditionale Kompilierung

Wechsel zwischen verschiedenen Versionen (ohne das auskommentiert werden muss)

#include <stdio.h>

#define VERSION_STABLE

// Stabile Variante
#ifdef VERSION_STABLE

int square(int x) {
  return x * x;
}


#endif

// Experimentelle Variante
#ifndef VERSION_STABLE

int square(int x) {
  if (x == 1) return 1;
  return x * x;
}

#endif

int main() {
  printf("%d", square(3));
}

Once-Only Headers

Falls ein Header zweimal inkludiert wird, wird er auch zweimal verarbeitet.

In der Regel unproblematisch weil C mehrfache Deklarationen erlaubt. Z.B.

int x;

int x;

Zweimal den gleichen Header verarbeiten ist aber sicher Zeitverschwendung.

Im Header (vor allem in C++) befinden sich aber auch oft Definitionen. Dann wird es problematisch.

Wie können wir verhindern, dass ein Header nur einmal inkludiert wird? Wir wenden folgenden Trick an.

#ifndef __FACT_HEADER__
#define __FACT_HEADER__

int fact(int n);

#endif 

Standardbibliotheken

http://www.cplusplus.com/reference/clibrary/cstdlib/

http://www.cplusplus.com/reference/clibrary/cctype/

http://www.cplusplus.com/reference/clibrary/cstring/

Zusammenfassung