Variablen und Funktionen

Variablen und Funktionen bezeichnen gespeicherte oder zu berechnende Werte, welche mittels eines Symbols angesprochen werden können. Das Symbol bezeichnet den Namen der Variablen oder Funktion.

Jeder Variablen und jeder Funktion wird ein Typ zugeordnet, was als Deklaration bezeichnet wird. Die tatsächliche Festlegung des Inhalts erfolgt mittels einer sogenannten Definition.

Der Wert einer Variablen befindet sich an einer festgelegten Adresse im Speicher oder in einem Prozessorregister. Durch Angabe des Symbols können die dadurch representierten Werte gelesen oder geschrieben werden, ohne genaue Kenntnis davon, wo sich der durch die Variable representierte Wert befindet.







25.000000
#include <stdio.h>

int main(){
  int   a = 5;
  float b;
  b = a * a;
  printf("%f\n", b);
  return 0;
}

Eine Funktion ist zu verstehen als eine funktionale Einheit, die bei Bedarf mittels Angabe des Symbols und gewünschten Argumenten aufgerufen werden kann. Das Resultat der Funktion wird bei Aufruf frisch berechnet und als sogenannter Rückgabe-Wert zurückgegeben.









Area: 78.539749
#include <stdio.h>

float area(float r){
  return r * r * 3.14159;
} 

int main(){
  float radius = 5;
  printf("Area: %f", area(radius));
  return 0;
}

Details

Variablen speichern Werte, welche sich im Gegensatz zu fest im Programmtext verankerten Werten während der Laufzeit eines Programmes verändern können. Der Wert einer Variable kann also variieren, er ist variabel. In C und C++ werden Variablen somit nicht zusammen mit dem Maschinencode gespeichert, sondern in einer separaten Datenstruktur.

Funktionen sind in C und C++ semantisch betrachtet nichts anderes als erweiterte Variablen. Der Wert, den eine Funktion representiert, wird jedoch nicht wie bei einer Variablen direkt aus dem Speicher gelesen, sondern durch eine Abzweigung des Programmcodes in eine vom restlichen Programm unabhängigen Umgebung neu berechnet und als Rückgabewert an die aufrufende Stelle zurückgegeben. Zusätzlich können einer Funktion Argumente übergeben werden, welche dann innerhalb der Umgebung als Parameter verfügbar sind.

Selbst Operatoren (wie beispielsweise Addition, Multiplikation...) können als Funktionen verstanden werden und in C++ sogar selbst ausprogrammiert werden.

Deklaration und Definition

Eine Deklaration ordnet in einem bestimmten Programmbereich einem Symbol einen Typ zu. Damit wird dem Compiler angekündigt, was genau ein Symbol darstellt, ob es sich um eine Variable oder eine Funktion handelt sowie die Art, wie das Symbol zu interpretieren ist, ohne den genauen Inhalt anzugeben. Eine Deklaration wird in gewissen Situationen auch als Prototyp bezeichnet.

Eine Definition bewirkt die Bereitstellung eines Symbols. Eine Funktion wird anhand einer Definition ausprogrammiert und eine Variable wird einem reservierten Speicherort zugeordnet, was auch als Instanziierung bezeichnet wird. Häufig wird bei der Definition einer Variablen zusätzlich eine Initialisierung des Symbols vorgenommen, wodurch der zu speichernde Wert festgelegt wird.

Es gibt immer wieder Verwirrung um die beiden Begriffe Deklaration und Definition. Eine Deklaration wird verwendet, um dem Compiler ein Symbol anzukündigen, welches erst später definiert wird. Aufgrund einer Deklaration ist dem Compiler das Symbol bekannt und er ist insbesondere über den Typ des Symbols informiert. Eine Definition hingegen meint die konkrete Ausformulierung und Bereitstellung eines Symbols. Mit der Bereitstellung ist gemeint, dass der Compiler gemäss dem Typ des Symbols Speicherplatz reserviert und dem Symbol den Speicherort (eine Adresse im Speicher oder ein Prozessorregister) zuordnet, womit das Symbol schlussendlich eine Referenz auf diesen Speicherort darstellt. Die Definition eines Symbols darf somit nur an einer einzigen Stelle erfolgen. Demgegenüber dürfen jedoch für jedes Symbol beliebig viele Deklarationen geschrieben werden, solange sie das Symbol nicht mit widersprüchlichem Typ deklarieren.

Eine Definition bewirkt stets auch eine Deklaration. Der umgekehrte Fall gilt im Allgemeinen nicht: Eine Deklaration bewirkt NICHT automatisch eine Definition. Dennoch gibt es viele Fälle, bei denen bei einer Deklaration das entsprechende Symbol vom Compiler automatisch auch definiert wird. Wird beispielsweise eine Variable innerhalb von ausführbarem Code mittels einer Deklaration eingeleitet, so wird die Variable dadurch gleichzeitig auch definiert. Wenn jedoch beispielsweise die Variablen-Deklaration als Teil einer übergeordeneten Deklaration (beispielsweise eine Klassendeklaration) geschrieben wird, oder die Variable mit dem extern-Keyword deklariert wird, so findet keine Definition statt.






Variable declaration
Function declaration


Function definition
Variable definition
#include <cstdio>
#include <cmath>

class Vector2{
public:
  float a[2];
  void normalize();
};

void Vector2::normalize(){
  float length = sqrtf(a[0]*a[0] + a[1]*a[1]);
  a[0] /= length; a[1] /= length;
}

Deklarationen und Prototypen

Deklarationen sind sehr vielseitig. Die vielen Möglichkeiten sind im Detail bei den Syntax-Beschreibungen von Variablen und Funktionen beschrieben.

Jedes Symbol muss vor dessen Verwendung im Code irgendwo vorher im Programmtext deklariert worden sein. Da es bei komplexeren Programmen sehr gut möglich ist, dass Funktionen und Variablen nicht strikt sequentiell hintereinander angeordnet werden können (beispielsweise bei wechselwirkendem Aufruf von Funktionen oder durch das Einbinden von weiteren Dateien), werden sogenannte Prototypen verwendet.

Ein Prototyp ist grundsätzlich nichts anderes als eine Deklaration, welche vor der Verwendung eines Symbols angegeben wird. Dem Compiler steht ab einer Deklaration die Information zur Verfügung, um was für einen Typ es sich bei dem Symbol später handeln wird.



Prototype


1234




Definition
#include <stdio.h>

void printInt(int x);

int main(){
  printInt(1234);
  return 0;
}

void printInt(int x){
  printf("%d\n", x);
}

Ein Prototyp erstellt keinen ausführbaren Code und reserviert keinen Platz, sondern teilt dem Compiler lediglich mit, dass das entsprechende Symbol existiert und voraussichtlich an anderer Stelle definiert wird. Ein Prototyp wird somit manchmal auch als forward declaration bezeichnet. Auf dieser Seite wird der deutsche Begriff Vorausdeklaration verwendet.

Der Begriff Prototyp ist grundsätzlich austauschbar mit Deklaration. Es hat sich jedoch eingebürgert, eine Deklaration als Prototyp zu bezeichnen, wenn sie als Gerüst betrachtet werden kann, also nur die notwendigsten Informationen angibt. Eine Funktions-Deklaration ohne Parameternamen beispielsweise wird eher als Prototyp bezeichnet denn als Deklaration.

Es ist zu beachten, dass die Parameternamen von Funktionsdeklarationen für den Compiler keine Hinweise enthalten und somit ignoriert werden. Es ist somit ohne Probleme möglich, bei jeder Deklaration eines Symbols andere Parameternamen zu verwenden (oder sie gar wegzulassen) als bei der schlussendlichen Definition.

Das Schreiben von Prototypen wird als Prototyping bezeichnet. In C wird der Begriff Prototype grundsätzlich nur für Funktionen verwendet. Doch auch Variablen können vor ihrer eigentlichen Definition irgendwo deklariert werden, wenngleich dies eher selten vorkommt.

Prototypen liefern dem Compiler genügend Hinweise, um sämtliche Verwendungen eines Symbols abzuarbeiten. Beim Linken werden die Symbole jedoch den Definitionen zugeordnet. Findet somit niemals eine Definition des Symbols statt, so entsteht ein Linkerfehler.

Auch Typen können vorausdeklariert werden. Mittels des typedef-Keywords können Symbole Typen zugeordnet werden. Manchmal jedoch ist die tatsächliche Definition eines Typs erst später im Code zu finden oder gar vollständig unsichtbar. Wenn ein Typ so versteckt ist, so wird er als sogenannt opaquer Typ bezeichnet. Er kann im Sourcecode nur als Pointer verwendet werden.

In C++ können ganze Klassen als Vorausdeklaration geschrieben werden. Dies ist oftmals notwendig, wenn eine Klasse einen Pointer auf eine andere Klasse speichern soll. Eine Vorausdeklaration einer Klasse wird folgendermassen geschrieben:

class TestClass;

Prototypen können grundsätzlich irgendwo vor der Verwendung des Symbols geschrieben werden. Es muss jedoch darauf geachtet werden, dass es sich tatsächlich um eine Deklaration und nicht etwa eine Definition handelt. Ausserdem muss darauf geachtet werden, dass zum einen die Deklaration des Symbols vom Bereich der Verwendung aus sichtbar ist und zum anderen die Definition des Symbols vom Bereich der Deklaration aus sichtbar ist. Aus diesem Grund werden Prototypen und Vorausdeklarationen normalerweise im globalen Bereich und zudem am Anfang einer Datei vor sämtlichen Implementationen geschrieben.

Deklarationen werden stets mit einem Semikolon ; abgeschlossen. Das Vergessen des Semikolons bei einer Deklaration kann teilweise zu üblen Verwirrungen des Compilers führen, insbesondere, wenn beispielsweise das Semikolon am Ende einer Klassen-Deklaration vergessen geht. Sollten somit während des Compilierens plötzlich hunderte von Fehlern auftreten, und wenn eine Vielzahl dieser Fehler zudem Code betrifft, der niemals bearbeitet wurde (wie beispielsweise die verwendeten Bibliotheken), so ist dies ein untrügliches Zeichen dafür, dass irgendwo ein Semikolon vergessen ging.

Definitionen

Der Compiler ordnet nach einer Definition jedem Symbol die entsprechenden Speicher-Adresse oder das entsprechende Register zu, wodurch der Wert oder die Funktion eindeutig auffindbar, also lokalisierbar wird. Nach einer Definition widerspiegelt ein Symbol die entsprechende Variable oder Funktion selbst. Es handelt sich nicht um einen Pointer, sondern um eine Referenz auf den festgelegten Inhalt. Jedes spätere Auftreten des Variablen- oder Funktions-Namens innerhalb einer Anweisung wird vom Compiler in den entsprechenden Wert oder den entsprechenden Funktionsaufruf umgewandelt.

Variablen-Definitionen werden am häufigsten innerhalb von Funktionen geschrieben. Durch die Verwendung von Anweisungs-Blöcken kann eine Funktion in mehrere ineinander verschachtelte Bereiche unterteilt werden, wo Variablen unabhängig voneinander definiert werden können. Bei älteren C-Standards (vor C99) müssen sämtliche benötigten Definitionen am Anfang des enthaltenen Anweisungs-Blocks geschrieben werden, bei vielen neueren Compilern sowie allgemein in C++ gilt diese Restriktion jedoch nicht mehr.

Eine Variablen-Definition kann sich jedoch auch ausserhalb einer Funktion befinden. In C werden solche Variablen somit korrekterweise externe Variablen genannt. Hier auf ManderC wird jedoch dieser Begriff nicht verwendet und stattdessen von globalen Variablen gesprochen, was nicht exakt dasselbe ist, aber sich klarer von dem Begriff der sogenannt externen Bindung abgrenzt, welche von weitaus grösserer Bedeutung ist. Genauere Informationen können bei der extern-Speicherklasse nachgelesen werden.

Funktions-Definitionen beinhalten die Ausformulierung des Codes, welcher durch die Funktion ausgeführt werden soll. Details können bei der Funktions-Syntax nachgelesen werden.

Funktions-Definitionen stehen ausserhalb von ausführbarem Code innerhalb des globalen Bereiches oder in C++ auch innerhalb eines Namespaces. Funktions-Definitionen innerhalb von ausführbarem Code (also Funktionen innerhalb von Funktionen) werden nested Functions genannt und werden nach heutigem Bewusstsein nicht mehr benutzt. Manche Compiler der Sprache C erlauben nested Functions, diese Option ist heutzutage jedoch standardmässig ausgeschaltet. In C++ sind nested Functions nicht erlaubt.

Auftreten in Dateien

Sowohl Deklaration als auch Definitionen können grundsätzlich in jeder Datei vorkommen. Dennoch befinden sich Definitionen vorwiegend in Implementations-Dateien (.c, .cpp) und Deklarationen beziehungsweise Prototypen vorwiegend in Header-Dateien (.h). Header-Dateien werden normalerweise zu Beginn einer Implementations-Datei eingebunden, womit sämtliche Deklarationen der Header-Datei vor der Implementation automatisch dem Compiler bekannt sind.

Deklarationen stellen nicht nur ein notwendiges Programmierwerkzeug dar, sondern erlauben es, zusätzliche Informationen zu liefern, wie das deklarierte Symbol zu verwenden ist. Die Deklarationen in Header-Dateien sind oftmals versehen mit vielen erläuternden Kommentaren, gut lesbarer Anordnung sowie eingängigen Parameternamen, welche die Verwendung jedes einzelnen Parameters offensichtlich macht.

// Reads the a number of Bytes from the given
// file and stores it in the given buffer.
// Note: Buffer must be big enough!
void readBytesFromFile( void* buffer,
                        int   filedescriptor,
                        int   numbytes);

Deklarationen dienen somit als Interface für die Programmierung, was häufig als API (Application Programming Interface) bezeichnet wird. Durch die Kenntnis der Deklarationen von Variablen und Funktionen kann auf die gegebene Funkionalität zugegriffen werden, ohne genau wissen zu müssen (beziehungsweise ohne sich darum kümmern zu müssen), wie sie ausprogrammiert wurde. Vorcompilierte Bibliotheken stellen stets eine oder mehrere Header-Dateien als Interface zur Verfügung.

Nächstes Kapitel: Anweisungen und Kontrollstrukturen