Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

5 Funktionen

5 Funktionen

Funktionen sind ganz nützliche Hilfsmittel und wurden lediglich erfunden, um es dem Programmierer leichter zu machen. Der Anwender bekommt in der Regel nicht mit, ob Funktionen genutzt werden und schon gar nicht wie viele.

Das Konstrukt Funktion wurde erfunden damit:

  1. Der Quelltext wird besser strukturiert, damit nicht alles an einem Stück steht (Spaghetti-Code). Dies fördert die Übersichtlichkeit. (z.B. Man fasst Blöcke wie Eingaben oder Ausgaben zusammen und verpackt sie in seperate Funktionen)
  2. Funktionen lassen sich in der Regel in andere Dateien auslagern und somit wird jede Quelldatei nicht so riesig und somit wiederum übersichtlicher.
  3. Oft werden Sachen die gleich sind, aber häufig genutzt werden, in Funktionen ausgelagert, damit der Programmierer nicht 1000 mal das gleiche schreiben muss. Dies hat zudem den Vorteil, dass im Falle eines Fehler nur an einer Stelle was getan werden muss und nicht an 1000.

Das Konzept der Funktion sollte Ihnen aus dem Mathematikunterricht durchaus bekannt sein. Beispielsweise lautete mal die Aufgabe in der neunten Klasse wie folgt. Berechnen Sie die y Werte der Funktion f(x)=x2 für x=1 und x=5. Formal dachte man sich dann im Kopf f(1)=12 und f(5)=52 und wer des Kopfrechnens nicht mächtig war, gab das dann in seinen Taschenrechner ein. Aber was ist hier rein formal passiert? Nun, man hat eine Funktion mit dem Namen "f" und einem s.g. Übergabeparameter Namens "x". Im Kopf ruft man jetzt die Funktion f auf und übergibt ihr die 1 bzw. die 5. Innerhalb der Funktion wird jetzt jedes x durch die 1 bzw. 5 ersetzt und das Ergebnis ist dann der s.g. Rückgabewert der Funktion, der der Variablen y zugeordnet wird. Der einzige Unterschied beim Programmieren ist, dass eine Funktion nicht zwangsläufig ein Rückgabewert, also Ergebnis, haben muss und das man bessere Namen für die Funktionen und Übergabeparameter vergibt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.1 Wie werden Funktionen definiert

5.1 Wie werden Funktionen definiert

Das Schema sieht so aus: <DatentypDesRückgabewertes> <Funktionsname>([<Datentyp> <Variable1>, <Datentyp> <Variable2>, ...]) { <Anweisung>|[return <Wert>] }

Bei mehreren Funktionen ist die Reihenfolge im Quelltext wichtig, da man nur Funktionen aufrufen kann, die im Quelltext weiter oben stehen. Definiert man erst "Funktion1" und dann "Funktion2", kann dann zwar "Funktion2" "Funktion1" aufrufen aber nicht umgekehrt. Das kann man sich vorstellen wie in diesem Tutorial. Wenn man davon ausgeht, dass Sie bis jetzt alles gelesen haben, dann kann ich mich zu diesem Zeitpunkt nur auf Sachen beziehen, die bereits behandelt wurden, also weiter oben stehen, wohingegen ich mich schlecht auf Sachen beziehen kann, die erst in Kapitel 20 behandelt werden.

Um auf die kleine Matheaufgabe zurückzukommen, möchte ich ganz kurz zeigen, wie man dies in C programmieren würde, bevor ich dann ein etwas komplexeres Beispiel konstruiere.

 1
 2
 3
 4
 5
 6
 7
 8
					
int f(x) {
	return x * x;
}

// ...

int y1 = f(1);
int y2 = f(2);
					

Prinzipiell können Funktionen jede Art von Datentyp zurückgeben. Falls eine Funktion nichts zurückgeben soll, benutzt man den Pseudodatentyp "void". In diesem Fall darf die Funktion kein "return" besitzen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
34
					
// Funktion die zwar aus auf der Konsole ausgibt, aber nichts zurück gibt ///////
void ErsteAusgabe() {
	printf("Hallo ");
} // ErsteAusgabe /////////////////////////////////////////////////////////////



// Auch hier wieder keine Rückgabe //////////////////////////////////////////////
void ZweiteAusgabe() {
	printf("Welt\n");
} // ZweiteAusgabe //////////////////////////////////////////////////////////////



// Funktion addiert die zwei übergebenen Werte und gibt das Ergebnis zurück /////
int Addiere(int iSummand1, int iSummand2) {
	return iSummand1 + iSummand2;
} // Addiere ////////////////////////////////////////////////////////////////////



// DIESE FUNKTION WIRD IN JEDEM FALL ZUERST AUFGERUFEN //////////////////////////
int main(int argc, char** argv) {
	ErsteAusgabe();		// Die Klammern sind in jedem Fall wichtig
	ZweiteAusgabe();	// Die Aufrufe funktionieren, da die zwei Funktionen über dieser definiert wurden.
	
	int iZahl1 = 10;
	int iZahl2 = 20;
	
	printf("%i", Addiere(iZahl1, iZahl2));

	return 0;
} // main ///////////////////////////////////////////////////////////////////////
					

Ausgabe:

Hallo Welt
30
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

5.5 Standardparameter von Funktionen

5.5 Standardparameter von Funktionen

In gewissen Situationen kommt es vor, dass man eine Funktion bauen soll, welche eine bestimmte Aufgabe erledigen soll, die von verschiedenen Parametern abhängen, die der Programmierer aber, für den Normalfall, nicht immer angeben möchte. Dafür wurden s.g. Defaultparameter erfunden. Man braucht sie beim Funktionsaufruf nur anzugeben, wenn es quasi ein Spezialfall ist.

Hier mal ein Beispiel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
					
// Gibt das Schema einer linearen Funktion an /////////////////////////////////
void PrintLinearFunction(float fAnstieg = 1.0f, float fVerschiebung = 0.0f) {
	// Wenn der Standardanstieg übergeben wurde
	if (fAnstieg == 1.0f) {
		// Wenn die Standardverschiebung übergeben wurde
		if (fVerschiebung == 0.0f) {
			printf("y = x");
		} else {
			printf("y = x + %g", fVerschiebung);
		} // end of if
	} else {
		// Wenn die Standardverschiebung übergeben wurde
		if (fVerschiebung == 0.0f) {
			printf("y = %gx");
		} else {
			printf("y = %gx + %g", fAnstieg, fVerschiebung);
		} // end of if
	} // end of if
} // PrintLinearFunction //////////////////////////////////////////////////////



// Hauptfunktion des Programms ////////////////////////////////////////////////
int main(int argc, char** argv) {
	// Normalerweise würde man hier den Anwender fragen, ob er den Anstieg und die Verschiebung
	// angeben möchte und in einer verschachtelten if Anweisung die Werte einlesen und entsprechend
	// Die Ausgabefunktion aufrufen, aber darum geht es hier nicht.

	PrintLinearFunction();	// Nutzt beide Defaultwerte
	PrintLinearFunction(5.0f);	// Nutzt nur den zweiten Defaultwert

	PrintLinearFunction(8.0f, 3.0f);
	PrintLinearFunction(1.0f, 7.0f);
	
	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

y = x
y = 5x
y = 8x + 3
y = x + 7
		

Die Angaben der Standardwerte müssen mit in der Header-Datei angegeben werden.

Wenn Sie "Zwangsvariablen" und Defaultparameter definieren, müssen Sie darauf aufpassen, dass die Defaultparameter zu Letzt stehen, weil sonst der Compiler dies zurück weißt.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.7 Funktionsprototypen

5.7 Funktionsprototypen

Ihnen ist vielleicht schon aufgefallen, dass ich bisher nur Funktionen aufgerufen habe, welche im Quelltext darüber standen. Im Normalfall muss das auch so sein, da ein Quelltext von oben nach unten abgearbeitet wird und man so nur auf Sachen verweisen kann, die bereits erwähnt wurden. Sie müssen sich das wie in einem Buch vorstellen. Angenommen Sie sind in Kapitel 2 und der Autor schreibt "„... Die Begründung haben wir bereits in Kapitel 20 kennen gelernt.". Aus Sicht des Lesers macht das keinen Sinn, da er ein Buch üblicherweise auch von vorne nach hinten ließt und man somit nicht Sachen wissen kann, welche erst später erwähnt werden.

Man muss also immer darauf achten, dass man seine Funktionen so anordnet, dass benötigte Funktionen weiter oben stehen. Aber muss das wirklich so sein? Angenommen Sie hätten so ein Buch, in welchem der Autor Sie auf ein späteres Kapitel verweist. Was machen Sie? Sicherlich werden Sie zu diesem Kapitel hin springen und dort schnell vorlesen. Aber wie genau funktioniert dieses hin springen? Klappen Sie das Buch an einer willkürlichen Stelle auf und schon haben Sie das richtige Kapitel? Wahrscheinlich nicht. Was Sie sicherlich tun werden ist, das Inhaltsverzeichnis am Anfang des Buches aufzuschlagen, um dort die benötigte Seitenzahl in Erfahrung zu bringen und genau so eine Art Inhaltsverzeichnis kann man auch für Funktionen bauen. Das benötigte Hilfsmittel sind die s.g. Funktionsprototypen. Falls Sie sich also keine Gedanken über die Reihenfolge Ihrer Funktionen machen wollen, dann können Sie am Anfang der Datei alle Funktionen (außer die "main") formal definieren und dann weiß; der Compiler schon im Vorfeld, dass es die Funktion gibt und wo er danach suchen muss.

Als erstes möchte ich noch einmal verdeutlichen, wie die Situation ohne Funktionsprototypen aussieht.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
					
int Kapitel1() {
	// Kann keine dieser Funktionen aufrufen
}

int main(int argc, char* argv) {
	// Kann nur Kapitel1 aufrufen
}

void Kapitel2(int iWert) {
	// Kann nur Kapitel1 aufrufen
}
					

Hier sehen Sie noch einmal, welche Funktion normalerweise welche Funktion aufrufen darf/kann. Um dies zu umgehen, werde ich im folgenden Quelltext, besagte Funktionsprototypen einführen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
					
// Inhaltsverzeichnis für diese Datei ///////////////////////////////////
int		Kapitel1();		// Funktionsprototyp für Kapitel1
void		Kapitel2(int iWert);	// Funktionsprototyp für Kapitel2
//////////////////////////////////////////////////////////////////////////



int Kapitel1() {
	// Kann jetzt Kapitel2 aufrufen
}

int main(int argc, char* argv) {
	// Kann Kapitel1 und jetzt auch Kapitel2 aufrufen
}

void Kapitel2(int iWert) {
	// Kann Kapitel1 aufrufen
}
					

Was Ihnen sofort auffallen sollte ist, dass man also die formale Deklaration des Funktionskopfes, an den Anfang der Datei kopiert. Der einzige und wichtigste Unterschied ist jedoch, dass man nach dem Funktionskopf ein Semikolon setzt! Hier benutzt man keine geschweiften klammern. Würde man dies tun, hätte man keinen Inhaltsverzeichniseintrag, sondern eine normale Funktion. Sie können sich folgende Eselsbrücke merken. Steht hinter einem Funktionskopf ein Semikolon, dann ist dies wie eine Seitenzahl anzusehen, welche nur im Inhaltsverzeichnis eines Buches, hinter einem Kapitelnamen steht. Steht hinter einem Funktionskopf eine geöffnete geschweifte Klammer, dann stellt jene Abschnitt einen ganz normalen Fließtext dar, also diverse Sätze und so etwas finden Sie nur innerhalb des Buches und somit handelt es sich um das eigentliche Kapitel, also um die eigentliche Funktion.

Im übernächsten Kapitel werde ich das Thema Header-Dateien ansprechen und genau dort sind solche Inhaltsverzeichnisse von Nöten und dort werde ich auf die Funktionsweise von Funktionsprototypen nicht weiter eingehen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.2 Was passiert bei einem Funktionsaufruf

5.2 Was passiert bei einem Funktionsaufruf

Zunächst müssen Sie Sich bewusst sein, dass ein Programm auch nur Daten darstellen, die ebenfalls im Speicher liegen. Bei der Ausführung gibt es einen so genannten Programmpointer, welcher immer auf die Zeile verweist, welche ausgeführt werden soll. Dieser kann mit einem Sprungbefehl auf einen anderen Speicherbereich verweisen und genau dies wird bei einem Funktionsaufruf getan. Genau dies soll folgende Grafik veranschaulichen.

Programmablauf bei einem Funktionsaufruf

Wie Sie im rechten Teil sehen können, kann es vorkommen, dass es nicht zwangsläufig zu einem Sprung kommen muss. Der Compiler ist bei einfachen Funktionen in der Lage, den Funktionsaufruf durch den entsprechenden Code zu ersetzen. Dies hat zur Folge, dass die resultierende Anwendung etwas größer wird, allerdings entfallen alle Sachen, die ich gleich erklären werde. Da man aber nicht unbedingt Einfluss darauf hat, ob der Compiler inlint oder nicht (streng genommen schon, aber daran hängen mehrere wichtige Faktoren, auf die es mir jetzt nicht ankommt), gehen wir jetzt mal vom linken Fall aus.

Dabei passieren jetzt mehrere Sachen. Als aller erstes werden alle Registerinhalte gesichert und auf Standartwerte gesetzt. Dies kostet logischerweise Speicherplatz. Als nächstes wird eine Rücksprungadresse auf dem Stack gesichert, damit dem Computer nach Beendigung der Funktion bekannt ist, welche Zeile als nächstes ausgeführt werden soll. Hier wird wieder zusätzlicher Platz benötigt.

Als nächstes wird die komplette Funktion analysiert und alle Variablen auf dem Stack erzeugt, die evtl. benötigt werden. Dieser Aspekt ist ganz interessant, denn ggf. werden gar nicht alle benötigt. Hier zeigt sich, dass es im Grunde egal ist, an welche Stelle man in der Funktion Variablen deklariert. Der Compiler kann bis auf wenige Sonderfälle nicht entscheiden, ob eine Variable tatsächlich benötigt wird und somit erzeugt er alle.

Nach der Abarbeitung der Funktion, werden nun zuerst alle Variablen der Unterfunktion freigegeben. Anschließend springt der Computer zur gespeicherten Rücksprungadresse und weißt ggf. den Wert des Funktionsergebnisses, einer Variable zu. Abschließend werden das Funktionsergebnis und die Rücksprungadresse vom Stack entfernt und die gesicherten Register werden zurückgeholt.

Vereinfachter Aufbau des Stacks bei einem Funktionsaufruf

Der graue Bereich ist also der Bereich, der bei jedem Funktionsaufruf zusätzlich im Stack angelegt werden muss. Daraus ergeben sich jetzt offensichtliche Nachteile.

Mit jedem Funktionsaufruf müssen also viele Sachen im Hintergrund gemacht werden, welche Rechenzeit benötigen. Wenn man also rekursive Funktionen baut, also solche, die sich selbst aufrufen, kann es schnell vorkommen, dass der Stack voll läuft (meistens 1 MB). Der Sprung im Code führt dazu, dass der neue Code nachgeladen werden muss (also vom RAM in den Cache) und in ganz seltenen, aber extremen Fällen, muss der neue Codeabschnitt sogar von der Festplatte nachgeladen werden, was noch mehr Verzögerung bedeutet.

Sie sehen also, dass ein Funktionsaufruf teuer sein kann. Wir sprechen hier von Millisekunden, aber wenn man eine Schleife mit einer Million Durchläufen hat, in welcher eine Funktion aufgerufen wird, können sich diese Millisekunden zu Minuten aufsummieren.

Ich möchte Sie jetzt auf keinen Fall verschrecken! Sie sollten nur im Hinterkopf behalten, dass ein Funktionsaufruf den Quelltext zwar übersichtlicher macht, aber dass man sich diese Übersichtlichkeit teuer erkauft. Ein guter Programmierer setzt Funktionen mit Bedacht ein. Es macht keinen Sinn, eine kleine Berechnung in eine Funktion zu packen (vorausgesetzt der Compiler schafft nicht es, diese Berechnung zu inlinen, wovon man ausgehen muss). Hat man aber eine einzige Funktion, die über 100 Zeilen geht, ist es doch ratsam, jene aufzuteilen.

Spielentwickler handeln hier eher paranoid und versuchen so wenig wie möglich Funktionen zu benutzen, da jede Millisekunde darüber entscheidet, ob das Spiel flüssig laufen wird oder ruckelt. Sie sollten aber fürs erste lieber an die Übersichtlichkeit denken und eher mehr als zu wenige Funktionen benutzen. Erst in dem Moment, an denen das Programm tatsächlich schneller laufen muss (weil Wartezeiten unerträglich werden), sollten Sie anfangen zu Optimieren und Funktionen vereinigen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.3 call-by-value

5.3 call-by-value

Wenn man eine Funktion mit einem oder mehreren Werten aufruft, wird nicht die Variable an sich übergeben, sondern die Funktion legt sich eine neue Variable mit einem neuen Namen an (der durchaus auch gleich sein kann) und der Wert wird rein kopiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
					
// Addiert die zwei übergebenen Werte, addiert eins drauf und gibt Ergebnis zurück
int AddiereUndPlus1(int iSummand1, int iSummand2) {
	// Im Hintergrund wurden die zwei neuen Variablen iSummand1 und iSummand2 erzeugt, wobei
	// der Wert von iZahl1 in iSummand1 rein kopiert wurde und der Wert von iZahl2 in
	// iSummand2. Die Werte in iZahl1 und iZahl2 bleiben ansonsten unberührt. 
	
	int iErgebnis	= iSummand1 + iSummand2;
	iSummand1	= iSummand1 + 1;
	iSummand2	+= 1;	// sieht anders aus wie die Zeile darüber, macht aber das Gleiche
	
	printf("In der Funktion:\n");
	printf("%i\n", iSummand1);
	printf("%i\n", iSummand2);

	return iSummand1 + iSummand2;
} // AddiereUndPlus1 //////////////////////////////////////////////////////////



// DIESE FUNKTION WIRD IN JEDEM FALL ZUERST AUFGERUFEN ////////////////////////
int main(int argc, char** argv) {	
	int iZahl1 = 10;
	int iZahl2 = 20;
	
	printf("Vor dem Aufruf:\n");
	printf("%i\n", iZahl1);
	printf("%i\n", iZahl2);
	
	AddiereUndPlus1(iZahl1, iZahl2);	// Die Funktion gibt zwar was zurück, aber den Wert
						// brauchen wir jetzt nicht
	
	printf("Nach dem Aufruf:\n");
	printf("%i\n", iZahl1);
	printf("%i\n", iZahl2);

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Vor dem Aufruf:
10
20
In der Funktion:
11
21
Nach dem Aufruf:
10
20

		

Was ist jetzt eigentlich passiert. Um dies zu verstehen, schauen wir uns am besten folgende Grafik an.

Programmablauf und Speicherabbild bei call-by-value

In der Funktion "main" werden zuerst zwei Variablen "iZahl1" und "iZahl2" auf dem Stack erzeugt und anschließend mit den Werten "10" und "20" initialisiert. Dies verdeutlicht der erste obere Kasten.

In Zeile 29 erfolgt jetzt der Funktionsaufruf. Dabei passieren wieder mehrere Sachen, welche ich im lertzten Unterkapitel erläuterte. Wichtig ist momentan nur, dass zuerst die Rücksprungadresse auf den Stack gepackt wird und anschließend werden alle Variablen, die in der Funktion vorkommen, erzeugt. Dabei werden die Funktionsargumente, also die Variablen im Funktionskopf, initialisiert. Dies soll der zweite Kasten verdeutlichen. Das Augenmerk sollte hier darauf liegen, dass die Variablen "iSummand1" und "iSummand2" neue zusätzliche Variablen sind und eine Kopie des Originalwertes erhalten.

Nach den ganzen Berechnungen sieht der Speicher wie der dritte Kasten aus und das entspricht dem, was in den Zeilen 11 bis 13 ausgegeben wird.

Nach dem Funktionsende werden die erzeugten Variablen vom Stack entfernt und der Computer macht in der Programmzeile weiter, welche ebenfalls auf dem Stack abgelegt wurde, also Zeile 32. Der Speicher sieht dann wieder so aus, wie im vierten Kasten. Wie Sie sehen, sind die Ausgangsvariablen unverändert und das erklärt auch die Ausgabe in den Zeilen 32 bis 34.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.6 Überladen von Funktionen

5.6 Überladen von Funktionen

Hin und wieder gibt es Situationen, in welchen zwei Funktionen fast identischen Code ausführen, nur mit dem Unterschied, dass die Übergabeparameter einen anderen Type besitzen. Der Programmierer hat somit doppelten Aufwand. Zudem kommt hinzu, dass auch eine Doppelte Fehlerquelle da ist, da man Fehler ggf. an zwei Stellen beheben müsste.

Um dem ganzen aus dem Weg zu gehen, wurde ein Mechanismus erfunden, welcher sich Überladen nennt. Im Folgenden Beispiel wird nicht viel Code ausgeführt und von daher sehen Sie da nicht die Code-Ersparnis, aber es verdeutlicht die Funktionsweise ganz gut.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
					
// Berechnet eine Potenz ////////////////////////////////////////////////////// 
double CalcPower(float fBasis, int iExponent) {
	double dResult	= 1;

	// Nur für positive Exponenten
	if (iExponent >= 0) {
		// Multiplizieren
		for (int iCounter=1; iCounter<=iExponent; iCounter++) {
			dResult *= fBasis;
		} // end of if
	} else {
		return 0;
	} // end of if

	return dResult;
} // CalcPower ////////////////////////////////////////////////////////////////



// Benutzt die Ausgansfunktion, damit nicht alles doppelt geschrieben werden muss
int CalcPower(int iBasis, int iExponent) {
	double dResult = CalcPower((float)iBasis, iExponent);
	return (int)dResult;
} // CalcPower ////////////////////////////////////////////////////////////////



// Hauptfunktion des Programms ////////////////////////////////////////////////
int main(int argc, char* argv[]) {
	float	fBasis1		= 5.2;
	int	iBasis2		= 10;
	int	iExponent	= 3;

	printf("%g\n", CalcPower(fBasis1, iExponent));
	printf("%i\n", CalcPower(iBasis2, iExponent));	// benutzt überladende Funktion

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

140.608
1000
		

Gerade bei gezeigtem Konstrukt, muss darauf geachtet werden, dass die Funktion mit dem eigentlichen Algorithmus, an oberster Stelle steht und alle anderen, die jene benutzen, darunter.

Beim Überladen ist wichtig, dass etwas unterschiedlich ist. Sei es die Anzahl der Übergabeparameter oder, wenn es genauso viele sind, der Typ einer oder aller Variablen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.4 call-by-reference

5.4 call-by-reference

Manchmal ist es vonnöten, dass Funktionen mehrere Werte zurück geben können müssen. Dies geht aber nicht. Von daher hat man sich etwas anderes überlegt. Man übergibt nicht nur die Werte der Variablen, sondern gleich die Adressen bzw. die Referenz der Variable. Somit ist die Funktion in der Lage die Originalwerte zu manipulieren. Die Funktion erzeugt also keine Kopie. (allerdings dürfen die zwei verfahren gemischt werden, aber das lasse ich hier weg). Sie haben dieses Prinzip bereits bei der Funktion "scanf" kennen gelernt. Streng genommen passierte da zwar etwas anderes, aber um es sich vorstellen zu können was im Hintergrund passiert, reicht der Vergleich.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
					
// Addiert die zwei übergebenen Werte, addiert eins drauf und gibt Ergebnis zurück
int AddiereUndPlus1(int &iSummand1, int &iSummand2) {
	// Auffällig ist hier, dass vor dem Namen der Variablen noch so ein & steht. Wir
	// greifen jetzt mit iSummand1 direkt auf iZahl1 zu und können ihren Wert
	// manipulieren. Das Gleiche gilt für iSummand2 und iZahl2. iSummand1 und iSummand2
	// sind jetzt also keine neuen Variablen sondern ein (vereinfacht) "Alias"
	
	int iErgebnis	= iSummand1 + iSummand2;
	iSummand1	+= 1;
	iSummand2++;	// sieht anders aus wie die Zeile darüber, macht aber das gleiche
	
	printf("In der Funktion:\n");
	printf("%i\n", iSummand1);
	printf("%i\n", iSummand2);

	return iSummand1 + iSummand2;
} // AddiereUndPlus1 //////////////////////////////////////////////////////////



// DIESE FUNKTION WIRD IN JEDEM FALL ZUERST AUFGERUFEN ////////////////////////
int main(int argc, char** argv) {
	int iZahl1 = 10;
	int iZahl2 = 20;

	printf("Vor dem Aufruf:\n");
	printf("%i\n", iZahl1);
	printf("%i\n", iZahl2);

	AddiereUndPlus1(iZahl1, iZahl2);	// Die Funktion gibt zwar was zurück, aber den Wert
						// brauchen wir jetzt nicht

	printf("Nach dem Aufruf:\n");
	printf("%i\n", iZahl1);
	printf("%i\n", iZahl2);

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Vor dem Aufruf:
10
20
In der Funktion:
11
21
Nach dem Aufruf:
11
21
		

Wie Sie jetzt anhand der Ausgabe sehen, war die Funktion in der Lage, die übergebenen Variablen so zu verändern, dass diese Änderung auch nach der Funktion ersichtlich ist. Im Gegensatz zu "scanf", stand der "&" Operator allerdings nicht beim Funktionsaufruf, sondern im Funktionskopf bei der Parameterdeklaration. Nach außen hin, passiert das Gleiche, aber wie erwähnt, macht es intern schon einen Unterschied. Dies verdeutlicht noch einmal folgende Grafik.

Programmablauf und Speicherabbild bei call-by-reference

Der erste Kasten sieht wie vorhin aus, denn in der Funktion "main" ist alles wie gehabt.

Nach dem Funktionsaufruf sieht die Sache aber etwas anders aus. Es wird wieder die Rücksprungadresse auf den Stack gepackt, aber dieses Mal wird nur noch die Variable "iErgebnis" neu erzeugt. Da wir jetzt eine Referenz übergeben haben, kann der Computer jetzt mit den ursprünglichen Variablen arbeiten. Dies verdeutlicht der zweite Kasten.

Nachdem wieder die Berechnungen durchgeführt wurden, sieht der Speicher wie im dritten Kasten aus und das erklärt wiederum die Ausgaben in den Zeilen 11 bis 13. Beachten Sie bitte, dass hier tatsächlich die Originalvariablen manipuliert werden. Sie werden in der Funktion lediglich durch eine Art Alias angesprochen.

Nach dem Funktionsaufruf springt der Computer wieder in die entsprechende Zeile und alle nicht mehr benötigten Variablen werden wieder entfernt. Dies zeigt Kasten vier und erklärt auch die Ausgaben in den Zeilen 33 bis 35.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.8 Konstante Funktionsparameter

5.8 Konstante Funktionsparameter

Gerade bei erfahrenen Programmierern sieht man, dass oft das Schlüsselwort "const" bei der Deklaration der Argumente von Funktionen benutzt wird. Dies hat zur Folge, dass die Werte der Parameter nicht innerhalb der Funktion geändert werden können. Dies macht man aber nicht um sich selber einzuschränken, sondern um den Programmierer, welcher diese Funktion aufruft zu gewährleisten bzw. im zu versichern, dass genau mit diesen Werten gearbeitet wird. Hier mal ein Beispiels.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
					
// Addiert zwei Zahlen miteinander und gibt Ergebnis zurück ///////////////////
int Addieren(const int iSummand1, const int iSummand2) {
	return iSummand1 + iSummand2;
} // Addieren ///////////////////////////////////////////////////////////////////

// ...

int iZahl1	= 10;
int iZahl2	= 20;
int iErgebnis	= Addiere(iZahl1, iZahl2);
					

In erster Linie macht man dies nur für einen besseren Programmierstil. Keiner ist gezwungen sich darüber Gedanken machen zu müssen, ob man die Argumente tatsächlich konstant halten will oder nicht. Allerdings macht man sich dann doch mehr Gedanken, wenn man Referenzen übergibt und vor allem dann, wenn man Schnittstellen baut.

Intern hat diese Vorgehensweise auch Auswirkungen. Beispielsweise werden diese Funktionsargumente nicht auf dem Stack angelegt, wie vorhin beschrieben. Sie werden in dem Teil des Programmspeichers abgelegt, welcher speziell für Konstanten gedacht ist. Das hat dann zur Folge, dass man bei einem Funktionsaufruf weniger zusätzlichen Speicher im Stack benötigt.

Der erfahrene Programmierer verwendet also diese Art des Funktionsaufrufes wo immer er kann, aber für den Anfang sollten Sie Sich lieber davon distanzieren, da diese Schreibweise zu Beginn eher umständlich ist und noch andere Nebeneffekte hat, welche man bedenken muss bzw. welche gravierende Auswirkungen auf den Quelltext haben können. Deshalb werde ich auch auf diese Schreibweise, zu Gunsten der Verständlichkeit, verzichten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

5.9 Übungsaufgaben IV

5.9 Übungsaufgaben IV

  1. Erstellen Sie in der "main" eine Variable, die ein Zeichen Aufnehmen kann und benennen Sie sie "cEingabe".
  2. Erstellen Sie oberhalb der "main" eine Funktion mit dem Namen "MainMenu", welche ein Zeichen zurück gibt und keine Übergabeparameter hat. In ihr sollen drei Punkte zur Auswahl stehen (1 = Division von Integern, 2 = Division von Floats, 3 = Programm beenden). Anschließend soll ein Zeichen eingelesen werden. Bauen Sie um die Ein - und Ausgabe eine Schleife, welche so lange dieses Menü zeigen und die Eingabe tätigen soll, bis einer der drei erlaubten Werte eingegeben wurde. Sobald ein gültiger Wert eingegeben wurde, soll jener zurückgegeben werden.
  3. Weißen Sie den Rückgabewert des Menüs der vorhin deklarierten Variable als Initialwert zu und bauen Sie anschließend ein Fallunterscheidung ein, welche auf '1' oder '2' reagiert. Im Standardfall soll der Text "Auf Wiedersehen!" ausgegeben werden.
  4. Erstellen Sie eine Funktion mit dem Namen "Convert", welche einen booleschen Wert zurück geben soll und als Parameter ein Integer als Referenz haben soll. In der Funktion soll nun geprüft werden, ob der Parameter gleich 0 ist und falls dem so ist, soll er soll er auf 1 gesetzt und "true" zurückgegeben werden. Anderen Falles soll nur "false" zurück gegeben werden. Später könnte man so feststellen, dass eine Konvertierung notwendig war und ggf. die Eingabe wiederholen lassen.
  5. Bauen Sie jetzt eine Funktion, welche jene aus Aufgabe 4, überlädt. Anstelle eines Integers, soll ein Float übergeben werden. Die Funktion soll das Gleiche machen, wie die in Aufgabe 4.
  6. Erstellen Sie zwei inline Funktionen, welche sich auch überladen sollen, mit dem Namen "Divide". Ihr sollen einmal zwei Integer und einmal zwei Floats übergeben werden oder den Defaultparameter 1 haben. Der Rückgabetyp soll der gleiche sein, wie die Übergabeparameter. In der Funktion soll der zweite Parameter konvertiert werden und das Ergebnis aus Wert 1 geteilt durch Wert 2, soll zurückgegeben werden. Die Konvertierung ist hier notwendig, damit kein Fehler beim Teilen durch Null auftritt.
  7. Erweitern Sie nun noch die "main", um die notwendigen Ein - und Ausgaben in den Fallunterscheidungen (zwei Werte einlesen, mit Hilfe der Funktionen Berechnung durchführen und Ergebnis ausgeben). Erstellen Sie zum Schluss noch Funktionsprototypen am Anfang der Datei.
Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012