Deklaration und Definition

Eine Deklaration ordnet in einem bestimmten Bereich 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 Variable wird dabei einem Speicherort zugeordnet und eine Funktion wird anhand einer Definition ausprogrammiert. Häufig wird bei der Definition einer Variablen zusätzlich eine Initialisierung des Symbols vorgenommen, wodurch der zu speichernde Wert festgelegt wird.

Deklarationen werden stets mit einem Semikolon ; abgeschlossen. Die Definitionen von Variablen werden ebenfalls mit Semikolon ; abgeschlossen, wohingegen die Definition von Funktionen den ausprogrammierten Code in einem Code-Block erwartet.






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;
}

Details

Es gibt immer wieder Verwirrung um die beiden Begriffe Deklaration und Definition. Eine Deklaration ist nur das Beschreiben eines Symbols und 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.

Deklarationen sowie Variablen-Definitionen müssen stets mit einem Semikolon ; abgeschlossen werden. Zwar sehen diese Ausdrücke dadurch aus wie normale Anweisungen, sind es jedoch semantisch betrachtet nicht. Eine Anweisung besteht aus einer Verkettung von Operatoren und generiert dementsprechend ausführbaren Code. Deklarationen und Definitionen tun dies jedoch nicht. Eine Deklaration dient nur als Hinweis für den Compiler. Variablen-Definitionen stellen höchstens sicher, dass irgendwo genügend Speicherplatz reserviert ist, was jedoch in jedem Falle bereits zur Compilier-Zeit ermittelt werden kann. Ausführbarer Code wird nur dann generiert, wenn zusätzlich zur Definition noch eine Initialisierung stattfindet. Eine detailierte Behandlung der Initialisierung findet sich weiter unten auf dieser Seite.

Das Vergessen des Semikolons 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.

Prototypen

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. 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. Somit steht dem Compiler ab dieser Deklaration die Information zur Verfügung, um was für einen Typ es sich bei dem Symbol handelt.



Prototype


1234




Definition
#include <stdio.h>

void printInt(int x);

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

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

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.

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.

Das Schreiben von Prototypen wird als Prototyping bezeichnet und wird in C hauptsächlich für Funktionen, eher selten auch für Variablen verwendet. In C++ können ganze Klassen als Prototyp geschrieben werden. Dies ist oftmals notwendig, wenn eine Klasse einen Pointer auf eine andere Klasse speichern soll. Ein Prototyp einer Klasse wird folgendermassen geschrieben:

class TestClass;

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

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, einem Programmierer 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 den Programmierer, was häufig als API (Application Programming Interface) bezeichnet wird. Durch die Kenntnis der Deklarationen von Variablen und Funktionen kann ein Programmierer auf die gegebene Funkionalität zugreifen, ohne genau zu wissen (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.

Definitionen

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. Die Definition legt die semantische Interpretation eines Symbols fest, sprich: Was verbirgt sich hinter einem Symbol.

Variablen-Definitionen werden am häufigsten innerhalb von Funktionen geschrieben. Eine Variablen-Definition kann sich jedoch auch innerhalb des globalen Bereiches oder in C++ auch innerhalb eines Namespaces befinden. In C müssen innerhalb einer Funktion sämtliche benötigten Definitionen am Anfang der Funktion geschrieben werden, in C++ gilt diese Restriktion nicht mehr.

Funktions-Definitionen beinhalten die Ausformulierung des Codes, welcher durch die Funktion ausgeführt werden soll. Details können bei den Funktions-Definitionen 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. Die Sprache C erlaubte nested Functions, moderne Compiler haben diese Option jedoch standardmässig ausgeschaltet. In C++ sind nested Functions nicht erlaubt.

Initialisierung von Variablen

Bei der Definition einer Variablen ist es möglich oder gar erforderlich, sie auch zu initialisieren, sprich, der Variablen einen Start-Wert anzugeben:

int x = 1;
float blue[3] = {0., 0., 1.};
Particle p = oldparticle;
static int numerrors = 0;

Initialisierungen sind bei jeder Variablen-Definition erlaubt. Es sei jedoch wiederholt darauf hingewiesen, dass dies nur für Definitionen gilt, auf keinen Fall für Deklarationen. Es ist somit nicht möglich, beispielsweise eine mit dem Keyword extern deklarierte Variable oder eine Variable innerhalb einer Klassendeklaration zu initialisieren. Die entsprechenden Initialisierungen müssen an anderer Stelle (vorzugsweise in einer separaten Implementations-Datei) vorgenommen werden.

Eine Initialisierung sieht mit dem Gleichheitszeichen = und dem abschliessenden Semikolon ; zwar aus wie eine normale Anweisung, ist jedoch semantisch nicht als solche zu betrachten. Vielmehr handelt es sich um eine Angabe für den Compiler, mit welchem Wert eine Variable geladen sein soll BEVOR sie gültig wird. Ein Compiler wird je nach Situation unterschiedlichen Code generieren:

Wurde die Variable mit dem static-Keyword deklariert, so darf die Initialisierung genau nur ein einziges Mal stattfinden. Der Compiler löst dies beispielsweise, indem er alle statischen Variablen im globalen Bereich definiert und noch vor dem Aufruf der main-Funktion die Variablen initialisiert. Eingebaute Typen werden hierbei direkt in das Binary codiert womit überhaupt kein Code für die Initialisierung ausgeführt werden muss.

Bei der Initialisierung von Objekten sorgt der Compiler dafür, dass die Variablen direkt mit dem entsprechenden Standard-Konstruktor, Copy-Konstruktor oder Konvertierungs-Konstruktor gesetzt werden. Um innerhalb eines Konstruktors die Member der Klasse vor dem eigentlichen Abarbeiten des Codes zu initialisieren, wird der Member-Initialisierungs-Operator benötigt. Anmerkung: Eine Initialisierung vor dem eigentlichen Ablauf einer Funktion findet sich auch in C bei den (nicht zu empfehlenden) old-style-Parametern.

PODs können mittels Aggregat-Initialisierung mit Werten gefüllt werden. Dadurch können mehrere Werte gleichzeitig initialisiert werden, was durch den Compiler erheblich beschleunigt werden kann. Aggregate sind jedoch explizit nur bei Initialisierungen erlaubt.

Handelt es sich bei dem Typ der Variablen um eine komplexe Klasse mit einem aufwändigen Konstruktor oder gar um ein grosses Array aus solchen Objekten, so kann eine Initialisierung sehr viel Zeit kosten. Es sei angemerkt, dass bei Objekten von Klassen mit virtuellen Methoden der Compiler zudem implizit in jeden Konstruktor (auch einen leeren Standardkonstruktor) die Initialisierung einer versteckten Variablen (die sogenannte vtable) einfügt.

Wenn irgend eine kostspielige Initialisierung innerhalb einer Funktion steht, welche sehr oft aufgerufen wird, so wirkt sich dies dramatisch auf die Laufzeit des Programmes aus. In solchen Fällen könnte die Verwendung des static-Keywords Abhilfe schaffen, es sei jedoch nochmals darauf hingewiesen, dass beim static-Keyword die Initialisierung nur ein einziges Mal stattfindet.

Im Gegensatz zu C sind in C++ Initialisierungen an gewissen Stellen zwingend. Wenn eine Variable als Typ eine Referenz & besitzt oder sie mit dem const-Keyword deklariert wurde, so MUSS sie initialisiert werden. Die Initialisierung muss direkt bei der Definition der Variablen stehen. Ist die Variable als Teil einer Klassendeklaration deklariert, so muss sie beim Konstruktor mittels Member-Initialisierung gesetzt werden. Des weiteren müssen, wenn bei einer Parameterliste einer Funktion ein Standardwert angegeben wird, alle nachfolgenden Parameter ebenfalls initialisiert werden.