Attribute

  • Was sind Attribute?
  • unsigned
  • const
    • Konstanten
    • Read-Only-Speicherbereiche
    • Zeigerkonstanten
  • Scopes
  • static
  • volatile

Was sind Attribute?

Von einem Attribut spricht man, wenn man einen Gegenstand genauer beschreibt. Der rote Ball ist ein Ball, der - wenn man ihn genauer beschreibt - als rot beschrieben wird. Rot ist das Attribut. Wir werden uns hier Datentypen-Attribute ansehen, die die Sprache C kennt. Das bedeutet, dass man der Beschreibung eines Datentyps noch eine genauere Beschreibung hinzufügen kann.

Das Attribut unsigned

Integer-Datentypen zählen von einem maximalen negativen Wert zu einem maximalen positiven Wert. So zählt ein char von -128 bis 127. Einen einzelnen Farbwert (rot, grün oder blau) beschreibt man allerdings in Form seiner Intensität, von keine Intensität (0) bis maximale Intensität (255). Um die maximale Intensität zu speichern, müssen wir uns die Zahl 255 merken und die passt nicht in ein char. Integerdatentypen sind per Voreinstellung immer signed. signed ist übrigens auch ein Schlüsselwort der Sprache C, aber es benutzt eigentlich keiner, weil es die Voreinstellung ist. Wenn wir bei einem signed char also die ungebrauchten negativen Zahlen den positiven Zahlen hinzufügen könnten, dann hätten wir 127 (maximaler positiver Wert) + 128 (maximaler negativer Wert) = 255 als größte Zahl - mit der 0 also zusammen 256 unterschiedliche Werte, die wir mit einem unsigned char ausdrücken können.

Und so sehen Attribute im Quelltext aus - man schreibt einfach unsigned vor den Datentyp.

char           c;   // Wertebereich -128..127 - entspricht signed char
signed   char sc;   // Wertebereich -128..127
unsigned char uc;   // Wertebereich    0..255

signed und unsigned lässt sich mit allen Integerdatentypen verwenden:

Größe (Bit) Größe (Byte) Name Vorzeichen kleinster Wert größter Wert
CHAR_BIT (8) 1 char signed SCHAR_MIN (-128) SCHAR_MAX (127)
unsigned 0 UCHAR_MAX (255)
2*CHAR_BIT (16) 2 short signed SHRT_MIN (-32768) SHRT_MAX (32767)
unsigned 0 USHRT_MAX (65535)
2*CHAR_BIT (16) 2 (s.u.) int signed INT_MIN (-32768) INT_MAX (32767)
unsigned 0 UINT_MAX (65535)
4*CHAR_BIT (32) 4 long signed LONG_MIN (-2147483648) LONG_MAX (2147483647)
unsigned 0 ULONG_MAX (4294967295)
8*CHAR_BIT (64) 8 long long signed (-9.223.372.036.854.775.808) (9.223.372.036.854.775.807)
unsigned 0 (18.446.744.073.709.551.615)


Die angegebenen Größen sind Mindestgrößen! Die wirklichen Größen finden sich in der Headerdatei limits.h. Gerade für den Datentyp int gilt heute eher, dass er 32 Bit breit ist und damit long entspricht. Aber C garantiert das für int nicht, sondern nur 16 Bit, wie bei short.

Auch wenn sich int kürzer schreibt als unsigned int, so steckt für den Compiler, wie auch für den Menschen eine zusätzliche Information in diesem Datentyp. Auf ein Array darf man meistens nur mit positiven Werten zugreifen. Möchte man auf ein Array mit 256 Elementen zugreifen, so darf der Index selbst kein char sein, sondern muss mindestens ein unsigned char sein, da man ansonsten nicht hinter den Beginn des Arrays zugreift, sondern von 128 Elementen vor dem Arraybeginn bis 127 Elemente nach dem Arraybeginn. Die hinteren 128 Elemente erreicht man so nicht, während man gleichzeitig 128 Elemente erreicht, auf die man nicht zugreifen darf. Dies zu vergessen ist ein beliebter Anfängerfehler. Es ist auch nicht in der Verantwortung der Programmiersprache, dem Entwickler zu erklären, was er tun und lassen darf, sondern Du musst dem Computer erklären, was er tun soll. Bei einem Array, bei dem Du hinter den Start zugreifen möchtest, benutze also möglichst Variablen eines Integertyps, der mit dem Attribut unsigned erweitert wurde.

Schauen wir uns das an:

  char text[] = "proggen.org";
  int index = -5;
 
  printf( "Buchstabe an Position %d: %c\n", index, text[ index ] );

Verwendest Du für index eine unsigned-int-Variable kann Dir dies nicht passieren.

Dies solltest Du grundsätzlich tun, wenn Werte nicht negativ werden können. Nehmen wir die Anzahl Daten, zum Beispiel die Größe von Arbeitsspeicher. Man kann keine negative Menge Speicher anfordern: unsigned verwenden! Nehmen wir die Länge eines Strings: Ein String kann nicht kürzer als 0 Zeichen lang sein: unsigned verwenden!

Die bessere Version unserer strLength()-Routine sieht also so aus:

unsigned int strLength( char * string )
{
  unsigned int length = 0;
 
  while( string[ length ] )
    length = length + 1;
 
  return length;  
}

Ohne zu wissen, wie die Funktion arbeitet, können wir an der Signatur

unsigned int strLength( char * string )

schon sehen, dass wir einen positive Ganzzahl zurückbekommen werden - vorher konnten wir das nur vermuten.

const

Const und in C++ "Const-Correctness" sind sehr wichtige Themen, um die Qualität von Software zu erhöhen. Wie unsigned ist const eine zusätzliche Garantie, die man vergibt. Die Garantie bei const lautet, dass sich der Wert nicht verändern wird und das ist eine sehr wichtige Eigenschaft.

Konstanten

Bisher kennen wir vorrangig Datentypen mit Zahlen. Mit const wird aus einer Variablen eine Konstante. Wozu ist das gut?

#include <stdio.h>
 
double pi = 3.141592654;
 
double circle_area( double r )
{
  return pi * r * r;
}
 
double circumference( double r )
{
  return pi = 2 * r;
}
 
int main( void )
{
  do_something_stupid();
 
  printf( "Kreisumfang für Radius 1: %d\n", circumference( 1.0 ));
  printf( "Kreisfläche für Radius 1: %d\n", circle_area( 1.0 ));
 
  return 0;
}

Und damit bekommen wir folgendes Ergebnis:

xin@trinity:~/proggen.org/tutorial$ ./a.out 
Kreisumfang für Radius 1: 2.000000
Kreisfläche für Radius 1: 2.000000

Das belegt mal wieder, dass man Ergebnisse eines Computer niemals unkritisch für wahr nehmen darf. Der Radius eines Kreises mit dem Radius 1 ist 6,28… aber hier hat sich ein Tippfehler in der Funktion circumference eingeschlichen, statt pi * 2 * r steht dort pi = 2 * r. Leider ist dieser Tippfehler kein syntaktischer Fehler, also läuft das Programm - es macht halt nur nicht, was man sich wünscht.

Pi ist aber keine Variable, sondern eine Konstante - mit const lässt sich das auch dem Compiler erklären:

double const pi = 3.141592654;

Einer Konstante kann man aber keinen neuen Wert zuweisen:

xin@trinity:~/proggen.org/tutorial$ gcc const.c 
const.c: In function ‘circumference’:
const.c:12: error: assignment of read-only variable ‘pi’

Da der Compiler nun versteht, dass er pi nicht überschreiben darf, kann er diesen Fehler auch erkennen.

Wir korrigieren die Methode circumference:

double circumference( double radius )
{
  return pi * 2 * radius;
}

Damit lässt sich das Programm kompilieren und ausführen:

Kreisumfang für Radius 1: 6.283185
Kreisfläche für Radius 1: 3.141593    

Read-Only-Speicherbereiche

Wem Konstanten nicht wichtig genug sind, der wird hier eine besonders wichtige Eigenschaft für const kennen lernen: Konstante Speicherbereiche. Wir haben bisher Arrays mit Strings initialisiert:

void function( void )
{
  char arrayVariable[] = "proggen.org";
 
  ...
}

Wir haben gelernt, dass Zeiger und Arrays sich ähnlich verhalten:

  char arrayVariable[] = "proggen.org";
  char * pointer = arrayVariable;

Aber folgendes funktioniert ist gefährlich:

#include <stdio.h>
 
int main( void )
{
   char * pointer = "proggen.org";
 
   return 0;
}

Der gcc (Version 4.9.1) akzeptiert das, aber das Verhalten wird sich ändern. Früher hat man die Fehlererkennung durch den Compiler nicht so ernst genommen und so entstand viel Quelltext, der solche Text-Zuweisungen enthält. Damit diese weiterhin kompilieren und nicht urplötzlich tausende Zeilen geändert werden müssen, akzeptiert der gcc derartiges. In C++ ist const mächtiger und damit auch weiter verbreitet: der g++ meckert schonmal:

constptr.c:5: warning: initialization discards qualifiers from pointer target type

Was passiert da eigentlich? Der Text „proggen.org“ ist im ausführbaren Programm gespeichert. Die Variable arrayVariable ist eine Variable, deren Größe entsprechend der Länge des Textes „proggen.org“ gewählt wird. Und dann wird der Text „proggen.org“ in die Variable kopiert. In unserem Array dürfen wir dann den Text nach belieben ändern. Den Text „proggen.org“ dürfen wir aber nicht ändern, denn sonst würde bei jedem Aufruf von „function“ die Variable arrayVariable verändert sein.

Wenn wir ihn nicht ändern dürfen… dann machen wir das doch einfach mal:

#include <stdio.h>
 
int main( void )
{
   char * pointer = "proggen.org";
 
   pointer[0] = 'a';
 
   return 0;
}

kompilieren das und führen es aus:

$ ./a.out 
Speicherzugriffsfehler

Ein String, der in Anführungszeichen steht ist eine Zeiger auf konstante Buchstaben: char const *. Der korrekte Quelltext sieht also so aus:

#include <stdio.h>
 
int main( void )
{
   char const * pointer = "proggen.org";
 
   pointer[0] = 'a';
 
   return 0;
}

Hier kann der Compiler nun auch warnen:

$ gcc const.c 
constptr.c: In function ‘main’:
constptr.c:7: error: assignment of read-only location ‘*pointer’

Interessant ist das nun, wenn Zeiger an Funktionen übergeben werden und man sicher sein möchte, dass die Daten von diesen Funktionen nicht verändert werden - zum Beispiel, weil die Daten nicht verändert werden dürfen.

Schauen wir uns eine Funktion wie strLength nocheinmal an, die keine Daten ändert und eine weitere Funktion, die den übergebenen String mit Punkten überschreibt:

#include <stdio.h>
 
void dots( char * string )
{
  unsigned int pos = 0;
 
  while( string[ pos ] )
  {
    string[ pos ] = '.';  
    pos = pos + 1;
  } 
}
 
unsigned int strLength( char * string )
{
  unsigned int length = 0;
 
  while( string[ length ] )
    length = length + 1;
 
  return length;  
}
 
int main( void )
{
  unsigned int length = strLength( "proggen.org" );
 
  printf( "Der Text ist %d Zeichen lang.\n", length );
 
  dots( "proggen.org" ); 
 
  return 0; 
}

Nach dem Kompilieren erhält man folgende Ausgabe:

$ ./a.out 
Der Text ist 11 Zeichen lang.
Speicherzugriffsfehler

Die Funktion strLength() funktioniert, da wir glücklicherweise nichts ändern. Anschließend wird ausgegeben, wie lang der Text ist und das Programm stürzt in der Funktion dots() ab und stellt klar, dass wir mit konstanten Strings arbeiten. Beide Funktionen bekommen nun statt char * einen char const * übergeben:

#include <stdio.h>
 
void dots( char const * string )
{
  unsigned int pos = 0;
 
  while( string[ pos ] )
  {
    string[ pos ] = '.';  
    pos = pos + 1;
  } 
}
 
unsigned int strLength( char const * string )
{
  unsigned int length = 0;
 
  while( string[ length ] )
    length = length + 1;
 
  return length;  
}
 
int main( void )
{
  char const * ptr = "proggen.org"; 
  unsigned int length = strLength( ptr );
 
  printf( "Der Text ist %d Zeichen lang.\n", length );
 
  dots( ptr ); 
 
  printf( "Der Text ist nach dots() nun: %s\n", ptr );
 
  return 0; 
}

Das lässt sich aber nicht kompilieren:

funcconst.c: In function ‘dots’:
funcconst.c:9: error: assignment of read-only location ‘*(string + (long unsigned int)pos)’

Die Funktion dots() möchte Änderungen durchführen, aber das darf sie bei einem char const * nicht. Also muss hier definitiv ein char * übergeben werden, also ein Pointer auf veränderbare chars:

Also zurück zu

void dots( char * string )

Auch das lässt sich nicht kompilieren:

$ gcc funcconst.c 
funcconst.c: In function ‘main’:
funcconst.c:31: warning: passing argument 1 of ‘dots’ discards qualifiers from pointer target type
funcconst.c:3: note: expected ‘char *’ but argument is of type ‘const char *’

Die Funktion dots() eignet sich einfach nicht für konstante Zeiger und der C-Compiler warnt uns, dass hier etwas verkehrt läuft. Der C++ Compiler wird deutlicher und quittiert das Programm mit einem error: Das Programm ist mit g++ nicht kompilierbar.

$ g++ funcconst.c 
funcconst.c: In function ‘int main()’:
funcconst.c:31: error: invalid conversion from ‘const char*’ to ‘char*’
funcconst.c:31: error:   initializing argument 1 of ‘void dots(char*)’

Hier müssen wir einen Zeiger auf Speicher übergeben, den wir verändern dürfen.

int main( void )
{
  char array[] = "proggen.org"; 
  unsigned int length = strLength( array );
 
  printf( "Der Text ist %d Zeichen lang.\n", length );
 
  dots( array ); 
 
  printf( "Der Text ist nach dots() nun: %s\n", array );
 
  return 0; 
}

Nach dem kompilieren erhalten wir als Ausgabe:

$ ./a.out 
Der Text ist 11 Zeichen lang.
Der Text ist nach dots() nun: ...........

Wir haben bei strLength() die Garantie, dass der übergebene String nicht verändert wird, wir können also auch Arrays als konstante Werte übergeben. Die Funktion dots() erlaubt keine konstanten Arrays, da sie die Daten ändern möchte. Der Compiler ist in der Lage Fehler zu erkennen und zu melden. Lies Dir auch Warnings durch, Code mit Warnings ist grundsätzlich unzureichend!

Grundregel: Wenn Du qualitative Software herstellen möchtest, sorge dafür, dass Zeiger grundsätzlich auf konstante Daten zeigen, außer Du musst in dieser Funktion Daten ändern. Das zieht sich durch das ganze Programm durch: Eine Funktion, die einen konstanten Parameter erhalten hat, kann ihn nicht an eine Funktion übergeben, die eine variablen Speicherbereich benötigt.

Zeigerkonstanten

Dieses Tutorial ist nur eins von vielen, die sich im Internet finden. In vermutlich allen anderen Tutorials wird das Attribut const vor dem Datentyp geschrieben, also

const char * ptr = "proggen.org";

statt

char const * ptr = "proggen.org";

Das ist in seiner Bedeutung gleichwertig, es macht also für das Programm keinen Unterschied. Ich empfehle trotzdem const nach dem Datentyp zu schreiben, um Verwechslungen auszuschließen. Schauen wir uns folgende Zeigervariablen an:

char const * ptr;
 
ptr = "Hallo proggen.org";
ptr = "Hallo Welt";

Wir können hier den Wert der Zeigervariablen änderen, mal zeigt sie auf das eine Array mit konstanten chars, mal auf ein anderes Array mit konstanten chars. Man kann aber auch Zeigerkonstanten definieren, die man - wie alle anderen Konstanten - sofort mit einem Wert initialisieren, also beschreiben, muss.

char const * const ptr = "Hallo proggen.org";
 
ptr = "Hallo Welt";  // das geht nicht: ptr ist read-only

Bei Zeigerkonstanten steht const hinter dem Zeiger ('*'). Gleichwertig ist auch:

const char * const ptr = "Hallo proggen.org";

Hier steht const vor dem Datentyp und nach dem Zeiger. Und folgendes geht überhaupt nicht:

const char const * ptr = "Hallo proggen.org";

denn hier ist der Datentyp nun doppelt konstant und der Zeiger bleibt variabel.

Eine Zeigerkonstante muss nicht zwangsläufig auf konstante Daten zeigen: die Konstante kann auch durchaus auf variable Daten zeigen:

char array[] = "Hallo proggen.org";
char * const ptr = array;

Und hier besteht nun eine gewisse Verwechlungsgefahr, wenn man const mal vor oder nach dem Element schreibt, dass man konstant halten möchte:

char const * ptr;

Schreibt man const grundsätzlich hinter das konstant zu haltene Element ist sofort klar, was gemeint ist.

Dies gilt auch in Hinblick zu C++, wo es konstante Methoden gibt. Würde man hier const vor die Signatur schreiben, sähe das so aus:

const char * method()

Das geht nicht - das besagt, dass ein Zeiger auf konstante Chars zurückgegeben wird. Auch hier wird const hinter die betreffende Methode geschrieben:

char * method() const

Was konstante Methoden sind, lernt ihr, wenn ihr C++ lernt. Eindeutige Regeln ohne überflüssige Ausnahmen zu verwenden, solltet ihr so früh wie möglich lernen!

Scopes

Bevor wir uns den letzten beiden Attribut widmen, schauen wir uns an, was Scopes sind und was sie für den Programmierer bedeuten.

Zunächst unterscheiden wir zwischen lokalen und globalen Variablen. Globale Variablen sind für alle Funktionen in diesem Quelltext sichtbar, lokale Variablen sind nur innerhalb der Funktion sichtbar:

#include <stdio.h>
 
int global;
 
void func()
{
  global = 1;
}
 
int main( void )
{
  int lokal;
 
  func();
  lokal  = 2;
 
  printf( "global: %d; lokal: %d\n", global, lokal );
 
  return 0;
}

Globale Variablen existieren von Programmbeginn bis Programmende. Im Prinzip geht es der Variablen lokal nicht anders, wird sie doch am Anfang der Funktion main() erzeugt und am Ende von main() vernichtet. Dennoch lebt die Variable global ein klein wenig länger als lokal:

Wann immer wir eine geschweifte Klammer öffnen um Anweisungen zusammen zu fassen, öffnen wir einen sogenannten Scope. Das ist das Umfeld, in dem die Variablen existieren - sobald die Klammer geschlossen wird, wird auch die Variable zerstört:

int global;
 
int main( void )
{                    // Scope 1
  int a = 4;
 
  if( a == 4 || a == 5 )
  {                  // Scope 2
    int b = a / 5;
 
    if( b == 1 )
    {                // Scope 3
      int c = 7;
 
      printf( "%d\n", c );
    }                // Variable c stirbt
 
    /* c existiert nicht mehr */
 
  }                  // Variable b stirbt
 
  /* Hier ist b und c nicht mehr existent */
 
  return 0;
}                    // Variable a stirbt

Man kann immer auf die Variablen im eigenen Scope und den darüber angeordneten Scopes zugreifen. Globale Variablen liegen über allen Scopes.

Scopes beschränken also die Lebensdauer von Variablen, globale Variablen liegen gewissermaßen in einem übergeordneten Scope, der erst zerstört wird, nachdem die Funktion main() verlassen wurde. Es gibt ein Leben nach main() - und genauso eins davor, zum Beispiel, um die Variablen anzulegen. Unser Programm arbeitet also bereits, bevor main() gerufen wird. main() wird erst gestartet, nachdem das globale Scope eingerichtet wurde und alle anderen erforderlichen Vorbereitungen durchgeführt sind.

static

Das Attribut static bewirkt, dass eine beschriebene Variable eine statische Adresse erhält, die sich nicht ändert. Das bedeutet zum einen, dass es sich um genau eine Variable handelt - klar, sagst Du, wieviele auch sonst?

Nun wir haben gerade gelesen, dass es Scopes gibt und wann immer eine Funktion gerufen wird, Variablen erzeugt und am Schluss wieder vernichtet werden. Das ist aber nicht immer gewünscht. Stellen wir uns eine Funktion count() vor, die zählt, wie oft sie aufgerufen wurde:

int count()
{
  int counter = 0;
 
  counter = counter + 1;
 
  return counter;
}

Die Variable counter wird jedesmal neu angelegt, mit 0 initialisiert und anschließend um 1 hochgezählt. Die Funktion gibt also grundsätzlich 1 zurück. So war das nicht gedacht. Wir brauchen also eine Variable, die am Ende des Funktionsaufrufs nicht einfach verschwindet. Wir wissen, dass der globale Scope erst nach dem Verlassen von main() zerstört wird, der Wert der Variablen also erhalten bleibt:

int counter = 0;
 
int count()
{
  counter = counter + 1;
 
  return counter;
}

So funktioniert es! Aber dennoch ist die Lösung eher unschön, denn jeder kann die Variable counter verändern - wir wollen ein Problem lösen, müssen aber zwei Identifier, nämlich count und counter, in den globalen Namensraum setzen. Vermutlich kommt es schnell zu Verwechslungen, ob die Funktion nun count() oder counter() heißt. Es wäre also schön, wenn die Variable counter außerhalb der Funktion count() nicht sichtbar wäre. Und hier kommt static das erste Mal ins Spiel. Alle globalen Variablen sind statisch, also einmalig an einer festen Adresse.

Mit dem Schlüsselwort static entkoppelt man eine Variable aus dem Scope, sie wird nach dem Verlassen des Scopes nicht zerstört. Sie landet im Speicherbereich des globalen Scopes, ist allerdings dort nicht sichtbar. Sichtbar bleibt sie nur im Scope, in dem sie angemeldet wurde. Die Initialisierung einer statischen Variable wird dann ausgeführt, wenn der Programmablauf erstmals an ihrer Definition vorbeikommt. Liegt diese Variable also in einer Funktion, wird sie beim ersten Aufruf der Funktion angelegt und initialisiert. Die Initialisierung einer statischen Variable ist einmalig und wird nicht bei jedem Aufruf der Funktion durchgeführt.

#include <stdio.h>
 
int count()
{                         // Scope in dem counter bekannt ist
  static int counter = 0; 
 
  counter = counter + 1;
 
  return counter;
}                         // counter wird nicht vernichtet!
 
int main( void )
{                         // counter ist nicht sichtbar
  printf( "Zähle: %d\n", count() );
  printf( "Zähle: %d\n", count() );
  printf( "Zähle: %d\n", count() );
 
//  printf( "Aktueller Zählstand: %d\n", counter ); // <- das geht nicht!
 
  return 0;
}

counter verhält sich wie eine globale Variable, aber man kann sie nur in dem Scope nutzen, in dem sie deklariert wurde, bzw. in Scopes, die unterhalb der Deklaration liegen.

volatile

volatile bedeutet soviel wie 'flüchtig' oder 'sprunghaft'. C wird gerne in der hardwarenahen Programmierung verwendet und wenn auf einen Speicherbereich mehrere Prozessoren schreibend zugreifen können, so darf man nicht davon ausgehen, dass diese Speicherbereiche unverändert bleiben, wenn man selbst keine Änderung daran vornimmt.

Prozessoren können viel schneller rechnen, wenn sie Variablen in den Prozessorkern laden und dann damit arbeiten. Das wissen die Compilerhersteller und so wird bei Schleifen wie

int result = 0;
int i = 0;
 
for( ; i < 10; i = i + 1 )
  result = result * i;

i nicht grundsätzlich in jedem Schritt auf den Speicher zurückgeschrieben, sondern nur einmal erst nachdem die Schleife durchgelaufen ist. Das spart Zeit, da der Prozessorkern sehr viel schneller ist, als der Arbeitsspeicher. Wenn nun ein anderer Prozessor darüber informiert werden muss, wo die Schleife gerade dran ist, dann muss die Variable i aber immer wieder in den Speicher zurückgeschrieben werden, damit der andere Prozessor den Wert aus dem Speicher auslesen kann. Um hier eine Optimierung zu verhindern und dem C Compiler zu sagen, dass er den Wert in den Speicher schreiben muss, benutzt man volatile:

int result = 0;
volatile int i = 0;
 
for( ; i < 10; i = i + 1 )
  result = result * i;

volatile ist wie gesagt in der Hardwareprogrammierung wichtig und für dieses Tutorial hier nur vollständigkeitshalber aufgeführt.

Ziel dieser Lektion

Dir sollten nun die Bedeutung von unsigned, const und static bekannt sein. Auch Scopes sollten Dir bewusst sein. volatile darfst Du soweit wieder vergessen und nur im Hinterkopf behalten, dass da was war: Es wird eher selten verwendet.

Das kommende Kapitel 'Strukturen' hingegen ist eher eine Grundlage moderner Software-Architektur und sollte nicht vergessen werden. ;-)