Introduction

This text gives an overview for the following programming languages:

C

This is the famous first C example:

#include <stdio.h>

int main(int argc, char **argv)
{
   printf("hello world\n");
}

Variables and types

Every variable must be declared.

This example shows the main() function as an entrypoint for the program. It contains one local variable num.

#include <stdio.h>

int main() /* a sample program */
{
   int num;

   num = 1;
   printf("%d is an integer number\n",  num );

   return 0;
}

help() is a simple function.

#include <stdio.h>

void help()
{
   printf("hier is hulp\n");
}

int main()
{
   printf("ik heb hulp nodig\n");
   help();
   printf("dank u");

   return 0;
}

Other types are double, float en char.

A declaration without initialization:

int a,b,c;

The int type

This type is used for integer numbers.

Type Size
int 4 bytes
range: -2147483648 tot +2147483647

This size and range is valid in 32 bit Linux.

With initialization:

int raindays = 25;
int hours,minutes = 3;

Only minuten is initialized with the value 3, hours is not initialized.

Integer constants can be written in base 10, 8 and 16.

Example Base
123 decimal
0400 octal
0xF3ca hexadecimal

A number starting with 0 is octal, 0x is hexadecimal.

Extra words to be added before the type are short, long en unsigned.

On a 32 bit Linux the gcc allows the following types:

Type Size, range
short int, short size: 2 bytes, range: -32768 tot +32767
long int, int or long size: 4 bytes, range: -2147483648 tot +2147483647
long long int or long long size: 8 bytes, range: −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807, from −2^63 to 2^63 − 1
unsigned short size: 2 bytes, range: 0 tot +65535
unsigned long or unsigned int size: 4 bytes, range:0 tot +4294967295
unsigned long long int or unsigned long long size: 8 bytes, range: 0 to 18,446,744,073,709,551,615, to 2^64 − 1

long constants have the de letter L at the end.

123L 045L 0x1234L

These are the suffixes:

None
type: int, long int or long long int
L
type: long int or long long int
LL
type: long long int
U
type: unsigned int, unsigned long int or unsigned long long int
UL
type: unsigned long int or unsigned long long int
ULL
type: unsigned long long int

When calling printf() the right % notation must be used.

int main()
{
   unsigned un = 40000;
   long ln = 2000000000;
   unsigned long uln = 4000000000;

   printf("un: %u ln: %ld uln: %lu\n",un,ln,uln);

   return 0;
}

The following overview shows all the % notations:

char short int int long int long long int
with sign %hhd %hd %d %ld %lld
without sign %hhu %hu %u %lu %llu

Het char type

This type takes 1 byte memory and the values are from -128 to 127. Constants look like this:

'A' 'c' '0'

Contstants with a special meaning are:

'\n'

new line

'\t'

tab

'\a'

bell signal

'\b'

backspace

'\f'

formfeed

'\r'

carriage return

'\t'

horizontal tab

'\v'

vertical tab

'\\'

backslash

'\?'

question mark

'\''

single quote

'\"'

double quote

'\0'

null character

Code in octal:

'\123'

code in hexadecimal:

'\x1b'

The types float, double and long double

For double numbers these types are available:

suffix
float f or F size: 4 bytes, range: 3.4e-38 tot 3.4e+38
double: none size: 8 bytes, range: 1.7e-308 tot 1.7e+308
long double l or L size: 12 bytes, mantisse 19 cijfers, exponent: −16382 tot 16383, precision: 80 bit

These sizes and ranges are valid for the GNU C compiler on a 32 Linux platform.

Examples:

123.45F
.556L
46.
12e-3F
15.5E20

When printing double and float variables these % notations are helpful:

  • %f normal notation
  • %e exponent notation
  • %g automatic choice between normal or exponent notation

The enumeration type

Use symbols ad values.

enum days =
{
   sundag,monday,tuesday,wednesday,thursday,
   friday,saturday
} today, tomorrow;

The internal values start at 0.

enum days yesterday;
yesterday = wednesday;

You can choose your own values.

enum year
{
   America=1776, Bastille=1789, VanGogh=1890
} fact;

The sizeof() function

int sizeof() is a builtin function and returns the size in bytes of a type ar a variable.

#include <stdio.h>

int main()
{
   printf("length char: %d\n",sizeof(char));
   printf("length short int: %d\n",sizeof(short int));
   printf("length int: %d\n",sizeof(int));
   printf("length long int: %d\n",sizeof(long int));
   printf("length long long int: %d\n",sizeof(long long int));
   printf("length float: %d\n",sizeof(float));
   printf("length double: %d\n",sizeof(double));
   printf("length long double: %d",sizeof(long double));

   return 0;
}

This is the output on a 32 bit Linux machine.

length char: 1
length short int: 2
length int: 4
length long int: 4
length long long int: 8
length float: 4
length double: 8
length long double: 12

Character strings, #define, printf(),scanf()

Strings

Stringconstants have a concluding zero at the end.

printf("abcde");

In memory this sequence of bytes is saved.

'a' 'b' 'c' 'd' 'e' '\0'

If you declare an array to save a string, the size must be big enough.

char name[40];

sizeof(name) will return 40 as the size of the array is 40 bytes. If you use strlen(), you get the effective zero based length.

Tekstvervanging met #define

Use #define sparingly to define constants. Don’t use it as an alternate way of defining functions. The preprocessor is doing the text replacement.

C source —> preprocessor —> compiler

Some examples:

#define PI 3.14159
#define DOLLAR '$'
#define DEBUG  1

Conversion notations for printf()

These are the percent notations tio be used in printf():

%d %i
integer decimal number
%o
octal

There is no leading 0 in the output.

%x %X
hexadecimal

There is no leading 0x or 0X.

%u
decimal number without sign
%c
char
%s
string

\0 marks the end of the string.

%f
floating point without e notation
%e %E
floating point with e notatie
%g %G
automatic choice whether with or without e notation
%p
pointer
%%
percent character

Extra

-
align left instead of right

printf("%-10d", 123);

+
always add the sign character.
space
Als het eerste teken geen plus- of minteken is, wordt een spatie voor het getal gezet.
0
output leading zero’s
#
alternative form
getal
minimal field width

printf("%8d", 0x1234);

.getal
precision

printf("%6.2f",10/3);

*
variable format

printf("%*.*f", widht,precision, 1.0/3.0 );

Some other extra characters work as modifiers to determine the size.

h
short instead of int
l
long instead of int

printf("%ld", 0x1234L);

L
long double in plaats van double

Conversion notations for scanf()

int num;
scanf("%d",&num);

The formatstring contains one or more % notations. Other characters are allowed:

  • spaces or tabs: these will be ignored.
  • normal characters, must be in the input.

Use * to ignore the input, for example in %*s.

These are the percent notations:

%d
decimal integer
%i
integer number, leading 0 for octal, 0x for hexadecimal
%o
octal integer
%x %X
hexadecimal
%u
decimal without sign
%c
character
%s
string of non-white
%f %e %
floating point number
%p
pointer
%n
read the number of input fields.
[…]
pattern to be input
[^…]
inverse pattern
%%
percent character

Assignment, operators and expression

Assignment

a = 1;

Special form:

a = b = c = 1;

The order is c gets 1, then b and at last a.

Arithmic operators

Arithmic operators are +, -, * en /.

39/5 yiekds 7

39.0/5 yields 7.8

Integer modulo operator:

39%5 yields 4

  • binary: + - * /
  • unary: + -

This is possible:

a + +(b - c)

There is no power operator.

prioriteit operator
hoog () van links naar rechts
- + unair
\* / %
+ - binair van links naar rechts
laag = van rechts naar links

Increment and decrement ++ and --

You can choose at which side the ++ or --:

a++;
a--;

or

++a;
--a;

Watch out:

x*y++ is x\*(y++)

Use postfix of prefix notatie:

m = 0;
n = ++m; // n gets 1

Bit operators

These bit operators can only be used with integers.

& bitwise and
` `
^ bitwise exclusieve or
<< shift left
>> shift right
~ one’s complement (unary)

The last operator is unary, the rest is binary; Shift operators have the number of bits to be shifted the second operand.

x = x | 0x10;  // set bit 4
x = x & ~0x10; // clear bit 4

Short form of assignment and operator

x = x + a becomes x += a
x = x - a x -= a
x = x * a x *= a
x = x / a x /= a
x = x % a x %= a
x = x << a x <<= a
x = x >> a x >>= a
x = x & a x &= a
x = x ^ a x ^= a
`x = x a`

Expressions

5
-125
1+1
a = 3
b = ++b % 4
c > 3.14

Actions

This program calculates the sum of 1 to 20.

int main()
{
   int teller, som; //declaration

   teller = 0;     // assignment
   som = 0;        // assignment
   while (teller++ < 20)     // while
   {
      som = som + teller;    // assignment with expression
   }
   printf("som = %d\n",som); // function call

   return 0;
}

Every statement has an ending ;. The while statement has a single statement which means that the braces could be omitted but nowaydays this is considered as bad practice.

Type conversion

Automatic conversion

The compiler will warn you when an automatic possibly faulty conversion occurs.

In function ‘main’:
warning: overflow in implicit constant conversion [-Woverflow]
    char c = 200 + 321;

Cast statement

Write a type between parntheses.

int m;

m = 1.6 + 1.5;             // yields 3
m = (int) 1.6 + (int) 1.5; // yields 2

When casting to int the number will be truncated. No rounding is applied.

Making choices

The if statement

Since has no explicit boolean type the result of the conditional expression is 0 or not zero.

  • not 0 : execute the yes side
  • 0 : execute the no side if it exists

This is an if without else.

if (a == b)
{
   printf("two equal numbers:\n");
   printf("%d en %d\n", a, b);
}

We kunnen ook een opdracht laten uitvoeren als de voorwaarde niet waar is. Dit wordt aangegeven door het woord else.

if (a == 0)
{
   printf("the number is 0\n");
}
else
{
   printf("the number is not 0\n");
}

More than one if is possible:

if (a == 0)
{
   printf("null\n");
}
else
{
   if (a > 0)
   {
      printf("positive\n");
   }
   else
   {
      printf("negative\n");
   }
}

Comparing numbers

Numbers can be compared:

< less than
> bigger than
<= less than or equal
>= bigger than or equal
== equal
!= not equal

The assignment and comparison can be combined.

if ((a = b) == 0)

The priority of relational operatoren is lower than that of the arithmic operators. The expression a + b == 0 can be read as (a + b) == 0.

Logical operators

// count the lowercase letters in a line
int main()
{
   char t;
   int  n = 0;

   while ((t=getchar()) != '\n')
   {
      if (t >= 'a' && t <= 'z')
      {
         n++;
      }
   }
   printf("The number is %d\n", n);

   return 0;
}

These are the logical operators:

  • && logical and
  • || logical or
  • ! logical not

The order of evaluation is from left to right. If the result is already determined after the evaluation of the first expression, the second one is not evaluated.

0 && expr2  // always yield  0
1 || expr2  // always yields 1

This short circuit mechanism is handy to guard against forbidden calculations.

if ( n != 0 && 12/n == 2)
{
   printf("n is 5 or 6\n");
}

The priority of the logical operators are:

  • ! has a highr than && and ||
  • && has a higher priority than || .

The logical operators have a lower prioriteit than the relationele ones.

If you combine bit and relational operators, mind the fact that bit operators have a lower priority than the relational ones. The next example needs parentheses.

(x & 0x8) == 0 // test whether bit is 0

Conditional expression ?:

This expression a choice between two values.

a = (b < 0) ? -b : b;

This can be written with if:

if (b < 0)
{
   a = -b;
}
else
{
   a = b;
}

More than one choice with switch

int main()
{
   char letter;

   printf("geef een letter en ik geef je een vogelnaam\n");
   while ( ( letter=getchar() ) != '#')
   {
      switch (letter)
      {
         case 'c' :
            printf("cormorant, phalacrocorax carbo\n");
            break;
         case 'r' :
            printf("ringed plover, charadrius hiaticula\n");
            break;
         case 'f' :
            printf("citril finch, serinus citrinella\n");
            break;
         case 'd' :
            printf("dune pipit, anthus campestris\n");
            break;
         case 'e' :
            printf("eider, somateria mollissima\n");
            break;
         default :
            printf("no other letters allowed\n");
            break;
      }
   }

   return 0;
}

Use only int or char expressions. More than one case is allowed.

case 'F' :
case 'f' :
   printf("fitis, phylloscopus trochilus\n");
   break;

Loops and other control expressions

while loop

This is the general form:

while (conditional expression)
   action;

while (conditional expression)
{
   action1;
   action2;
}

As long as the conditional expression returns true, the while actions will be executed.

Here are some examples each of which shows a different way to increment i.

  • no end:
i = 1;
while (i < 10)
{
   printf("dit is i: %d\n", i);
}
  • result: 2 - 9
i = 1;
while (++i < 10)
{
   printf("dit is i: %d\n", i);
}
  • result: 2 - 10
i = 1;
while (i++ < 10)
{
   printf("dit is i: %d\n", i);
}
  • result: 1 - 9
i = 1;
while (i < 10)
{
   printf("dit is i: %d\n", i);
   i++;
}

for herhalingsopdracht

The for loop is handy when the number of iteration is known in advance.

for (i = 1; i < 10; i++)
{
   printf("dit is i: %d\n", i);
}

Some other examples:

  • empty action
for (n = 1; n <= 10000; n++)
{

}
  • step is not 1
for (n = 2; n < 100; n += 11)
{
   printf("%d\n", n);
}
  • step change with *
for (bedrag = 100; bedrag < 200; bedrag *= 1.08)
{
   printf("bedrag: %.2f\n", bedrag);
}
  • char als loop counter
for (t = 'a'; t <= 'z'; t++)
{
   printf("%c", t);
}
  • the last part of the for left out
for (u = 1; u < 1000; )
{
   u *= 2;
}
  • empty parts in for
for ( ; ; )
{
   printf("hallo\n");
}

De algemene vorm van de for opdracht is:

for ( initialization ; test ; change )
   action

Use , if one of the paths between de brackets of the for needs an extra action.

for (j=1, amount = 100; amount < 200; j++, amount *= 1.08)
{
   printf("year: %d amount: %.2f\n", j, amount);
}

do while loop

This type of loop has its conditional expression at the end. This means that the actions will be executed at least once.

The general form is:

do
   action
while ( conditional expression );

This example reads the characters of the input line.

do
{
   scanf("%c", &teken);
   printf("%c heeft als code %d\n", teken, teken);
} while (teken != '\n');

break en continue with loopq

break and continue infuence the way the actions within te loop are executed.

break

In this example break is used twice to stop the loop.

i = 0;
while (1 == 1)
{
   printf("geef een getal: ");
   scanf("%d", &getal);
   if (getal == 0)
   {
      break;
   }
   printf("kwadraat van %d is %d\n",
      getal, getal*getal);
   if (++i > 20)
   {
      break;
   }
}

This is the same example rewritten without break.

#define FALSE 0
#define TRUE 1

einde = FALSE;
i = 0;
while ( !einde )
{
   printf("geef een getal: ");
   scanf("%d", &getal);
   if (getal == 0)
   {
      einde = TRUE;
   }
   else
   {
      printf("kwadraat van %d is %d\n",
         getal, getal*getal);
      if (++i > 20)
      {
         einde = TRUE;
      }
   }
}

continue

This keyword forces the loop to restart. It is a way to jump over a number of actions.

while( (ch = getchar() ) != EOF)
{
   if (ch == ' ')
   {
      continue;
   }
   putchar( ch );
   teller++;
}

And written without continue:

while( (ch = getchar() ) != EOF)
{
   if (ch != ' ')
   {
      putchar( ch );
      teller++;
   }
}

goto

The goto keyword can be used to jump to a different point in the program. It should be avoided. 1.

Functies

Kennismaking

A function groups a set of actions. Each function has a name which is used to call the function.

void lijn()
{
   int i;

   for (i=0; i<18; i++)
   {
      printf("*");
   }
   printf("\n");
}

int main()
{
   lijn();
   printf("This is the C course\n");
   lijn();

   return 0;
}

Within a function local variables can be declared and used. The space for these variables is reserved on the stack and the variables have a limited lifecycle. At the return of the function the stack is cleaned up and the bytes used to hold the values of the local variables is released.

Parameters

The next example shows the use of a single parameter.

void space(int number)
{
   int i;

   for (i=0; i < number; i++)
   {
      printf(" ");
   }
}

int main()
{
   printf("Dit is the C course\n");
   space(16);
   printf("+++\n");

   return 0;
}

The following terminology is used when talking about parameters:

formal parameter
the variabele as it is declared while writing the function
actual parameter
the actual value which is passed on to the function.

It is possible to write more than one parameter.

tlijn(char t, int n)
{
   int i;

   for (i=0; i < n; i++)
   {
      printf("%c", t);
   }
}

When the function tlijn() is called, give it 2 actual parameters:

tlijn('+', 20);
tlijn('=', 45);

Return en functietype

Als we een resultaat van een functie willen bekomen, dan wordt dit doorgegeven met de return opdracht. We moeten dan wel aangeven wat voor soort waarde met de return doorgegeven wordt. Daarom plaatsen we een type voor de functienaam. Dus niet alleen variabelen en constanten zijn van een bepaald type, ook functies worden met een type verbonden. Als we de functieoproep in een uitdrukking plaatsen, dan wordt de oproep vervangen door het resultaat van de functie.

int eigen_abs(int a) /* int : functietype */
{
   if (a < 0)
      return( -a );
   else
      return( a );
}

int main()
{
   int x,y,z;

   printf("geef 2 getallen:");
   scanf("%d %d",&x,&y);
   z = eigen_abs(x) + eigen_abs(y);
   printf("%d\n", z);

   return 0;
}

Het functietype mag niet weggelaten worden. Als we helemaal geen resultaat willen teruggeven, dan moet dit expliciet aangegeven worden met het woord void (leeg). Hetzelfde kunnen we doen als een functie geen parameters ontvangt. We plaatsen dan niets tussen de functiehaken. Een functie die geen parameters ontvangt en geen resultaat geeft schrijven we zo:

void doe()
{

}

We geven nu nog een voorbeeld met een ander functietype.

float gemiddelde(float a, float b, float c)
{
   return( (a + b + c)/3 );
}

We moeten hier toch nog zeggen dat het niet mogelijk is om een functie een doorgegeven variabele te laten wijzigen.

void verhoog(int a)
{
   a++;
}

void main()
{
   int b = 1;

   verhoog(b);
}

Omdat de functie verhoog() met een kopie van b werkt, wordt alleen a verhoogd. De variabele b blijft hier ongewijzigd. Men spreekt in dit geval van waardeparameter.

De & operator

De & operator bij een variabelenaam geeft het adres van die variabele. We kunnen nagaan waar een variabele zich in het geheugen bevindt.

v = 12;
printf("het getal %d staat in adres %u\n",
                    v, &amp;v);

Resultaat:

het getal 12 staat in adres 65502

Met het volgende voorbeeld zien we dat twee variabelen met dezelfde naam een verschillend adres hebben. Het zijn dus verschillende variabelen.

void fu()
{
   int a = 7;

   printf("fu: a = %d &a = %u\n", a, &a);
}

int main()
{
   int a = 5;

   printf("main: a = %d &a = %u\n", a, &a);
   fu();

   return 0;
}

Resultaat:

main: a = 5 &a = 65502
fu: a = 7 &a = 65496

Pointers en adresparameters

De volgende functie is bedoeld om de inhoud van twee variabelen te verwisselen. Deze versie is niet correct omdat alleen de kopies van de doorgegeven variabelen verwisseld worden en niet de originelen.

void verwissel(int u, int v)
{
   int   help;

   help = u;
   u = v;
   v = help;
}

int main()
{
   int   x = 3, y = 4;

   printf("x: %d, y %d\n", x, y);
   verwissel(x,y);
   printf("x: %d, y %d\n", x, y);

   return 0;
}

De variabelen x en y blijven dus ongewijzigd. We kunnen hier ook geen return gebruiken omdat deze slechts 1 waarde teruggeeft. De oplossing is als volgt: we geven als actuele parameters niet de inhoud van x en y door, maar wel de adressen van x en y. Dit kunnen we doen met de adres operator. Dit betekent dan wel dat we als formele parameters in de functie verwissel() variabelen moeten voorzien, die in staat zijn om adressen op te slaan. Deze soort variabelen noemt men pointers.

Vooraleer we pointers uitleggen, verklaren we eerst de declaratie van een gewone variabele. Bij de declaratie

int   getal = 123;

is getal van het type int en is &getal het adres van deze variabele.

!<>

De inhoud van getal is 123 en het adres van getal is 1000. De uitdrukking &getal is van het pointertype en stelt een constante voor. We kunnen deze constante toekennen aan een pointervariabele:

ptr = &getal;

Dit wil zeggen dat ptr moet gedeclareerd worden als een pointervariabele.

int *ptr;

Dit wordt zo gelezen: ptr is een pointer naar een int. De operator \* betekent hier pointer. De variabele ptr kan als volgt gebruikt worden:

ptr = &getal;
a = *ptr;

De eerste opdracht plaatst het adres van getal in ptr. De tweede opdracht neemt de inhoud van de int variabele die aangewezen wordt door ptr en plaatst deze waarde in a. De variabele a krijgt dus de waarde van getal. De \* operator is hier de operator voor indirecte verwijzing.

De situatie van deze variabelen kan zo weergegeven worden:

!<>

Bij de declaratie wordt vastgelegd dat ptr een pointer naar int is. We kunnen dus wel het adres van een int variabele in ptr plaatsen maar niet het adres van een char variabele.

De functie verwissel() is nu herschreven met pointers als formele parameter:

void verwissel(int *u, int *v)
{
   int   help;

   help = *u;
   *u = *v;
   *v = help;
}

int main()
{
   int   x = 3, y = 4;

   verwissel(&x,&y);
   printf("x: %d, y %d\n", x, y);

   return 0;
}

Wanneer verwissel() opgeroepen wordt, krijgt de variabele u als inhoud het adres van x en v het adres van y. De inhoud van deze twee aangewezen variabelen wordt dan verwisseld.

We kunnen parameters als volgt samenvatten. Als we informatie doorgeven, kunnen we de inhoud van die variabele doorgeven:

// waarde:
int x;

fun1( x );

Ofwel kunnen we het adres van die variabele doorgeven:

// adres:
int x;

fun2( &x );

In het eerste geval wordt de waarde van de variabele doorgegeven en is er sprake van call by value. In het tweede geval wordt het adres van de variabele doorgegeven en is er sprake van call by reference. Merk op dat in het tweede geval (adres doorgeven) het mogelijk is om in de functie de inhoud van de variabele, waarvan het adres is doorgegeven, te wijzigen.

Geheugenklassen

Elke variabele in een C programma behoort tot een geheugenklasse. Deze klasse bepaalt de levensduur en de bereikbaarheid van de variabele. Voor elke variabele kiezen we een gepaste klasse.

De klasse waartoe een variabele behoort, kunnen we bepalen met een sleutelwoord bij de declaratie. De volgende sleutelwoorden worden hier besproken: auto, extern, static en register. Een van deze woorden kan voor het type geplaatst worden bij een declaratie.

geheugenklasse + type + variabelenaam

Automatische variabelen

Dit zijn alle variabelen binnen een functie. We kunnen deze variabelen ook aanduiden met de term lokale variabelen. De ruimte voor deze variabelen en ook voor de formele parameters wordt gereserveerd op de stack. Vermits de stack een beperkte geheugenruimte omvat, moeten we de hoeveelheid lokale variabelen beperken.

void fu()
{
   int klad;

   klad = 1;
}

Deze variabelen bestaan alleen tijdens de uitvoering van de functie. Dit betekent dat er bij de start van de functie geheugen wordt gereserveerd voor de automatische variabelen. Dit geheugen wordt terug vrijgegeven bij het verlaten van de functie. We zouden het woord auto kunnen gebruiken, maar dit wordt altijd weggelaten. Variabelen binnen een functie gedeclareerd zonder een geheugenklasse zijn altijd automatisch of lokaal.

Het is duidelijk dat we geen lokale variabele kunnen gebruiken voor gegevens op te slaan die tijdens de hele uitvoering van het programma moeten blijven bestaan.

Externe variabelen

De term extern wordt bij gebruikt voor de globale variabelen. Hiermee bedoelen we de variabelen die buiten de functies gedeclareerd worden.

Het woord extern kan bij een declaratie buiten een functie voorkomen. We hebben hier te maken met een verwijzing en geen geheugenreservatie.

extern int waarde;   /* geen geheugen allocatie */

void fu()
{
   waarde = 3;
}

Hier wordt aangegeven dat de variabele waarde in een ander bestand gedeclareerd is. In C kunnen we met meerdere programmabestanden werken die gemeenschappelijke variabelen hebben.

static variabele

Hiermee bedoelen we variabelen die altijd bestaan, ook al staat de declaratie binnen een functie. De externe variabelen zijn statisch omdat ze altijd bestaan tijdens de levensduur van het programma.

Gebruik static binnen functie

We geven een voorbeeld.

void probeer()
{
   int  tijdelijk = 1;
   static int altijd = 1;

   printf("tijdelijk %d , altijd %d\n",
   tijdelijk++, altijd++);
}

int main()
{
   int i;

   for (i=1; i < 10; i++)
   {
      printf("%d :", i);
      probeer();
   }

   return 0;
}

De functie probeer() heeft twee variabelen tijdelijk en altijd. De variabele tijdelijk is automatisch, ze bestaat enkel tijdens de uitvoering van probeer(). De variabele altijd is statisch en bestaat tijdens de hele uitvoering van het programma. De variabele tijdelijk krijgt de initialisatiewaarde bij elke oproep van probeer(). De variabele altijd wordt slechts eenmaal geïnitialiseerd, namelijk bij de start van het programma.

Het woord static maakt van een tijdelijke variabele een variabele die altijd bestaat. Dit kan soms handig zijn, maar het kan ook ongewenste zijeffecten leveren.

Gebruik static buiten functie

Hiermee creëren we een externe variabele die enkel bekend is binnen het bestand. Het volgende voorbeeld maakt dit duidelijk.

#include <header.h>

int   a;
static int b;

static void fu1()
{
   fu2();
}

int main()
{
   fu1();
   fu3();

   return 0;
}

#include <header.h>

void fu3()
{
   printf("%d\n",a);
}

void fu2()
{
   fu3();
}

Met behulp van de #include aanwijzing wordt het bestand header.h ingelast in bestand 1 en bestand 2. Deze bevat de volgende tekst:

void fu2();
void fu3();

extern int a;

Dit zijn aanwijzingen hoe de functies fu2(), fu3() en de variabele a gebruikt moeten worden. De notatie voor de functies noemt men een functieprototype. Hierdoor is het mogelijk dat de compiler een foutmelding geeft als een functie uit een ander bestand, verkeerd opgeroepen wordt. De prototypes worden ook ingelast in het bestand waar de functies vastgelegd worden. Hierdoor wordt gegarandeerd dat de prototypes precies overeenstemmen met de functies zelf. De twee bestanden worden afzonderlijk gecompileerd en daarna samengevoegd in de linkfase.

In het voorbeeld is de variabele a is bekend in main(), fu1(), fu2() en fu3(). De variabele b is alleen bekend in main() en fu1().

Tot slot geven we nog een overzicht dat al de geheugenklassen weergeeft.

+—————-+—————-+—————-+—————-+—————-+ | | soort klasse | woord | levensduur | bereik | +—————-+—————-+—————-+—————-+—————-+ | binnen functie | automatisch | auto | tijdelijk | lokaal | | | | | | | | | register | register | tijdelijk | lokaal | | | | | | | | | statisch | static | altijd | lokaal | +—————-+—————-+—————-+—————-+—————-+ | buiten functie | extern | extern | altijd | in alle | | | | | | bestanden | | | extern static | static | altijd | | | | | | | in 1 bestand | +—————-+—————-+—————-+—————-+—————-+

Arrays en pointers

Arrays zijn variabelen die meerdere waarden van een zelfde soort kunnen opslaan. Pointers zijn verwijzingen naar andere variabelen. We behandelen eerst arrays en daarna het verband met pointers.

Array voorbeelden

int getal[10];
float r[100];
char t[20];

Elk van deze variabelen is een array. De array getallen bevat 10 elementen:

getal[0], getal[1], ... , getal[9]

De index die gebruikt wordt om de elementen te bereiken, start bij 0 en loopt tot het aantal elementen - 1. We kunnen dus niet zelf een bereik voor de index kiezen zoals in Pascal. Het volgende voorbeeld toont hoe arrays gebruikt kunnen worden.

#define DIM 10

int main()
{
   int som, i, getallen[DIM];

   for (i=0; i<DIM; i++)
   {
      scanf("%d",&getallen[i]);
   }
   printf("dit zijn de getallen\n");
   for (i=0; i<DIM; i++)
   {
      printf("%5d",getallen[i]);
   }
   printf("\n");
   for (i=0, som=0; i<DIM; i++)
   {
      som += getallen[i];
   }
   printf("het gemiddelde is %d\n",som/DIM);
}

Initialisatie van arrays

Net zoals enkelvoudige variabelen kunnen ook arrays geïnitialiseerd worden. Dit kan alleen bij externe en statische arrays.

/* dagen per maand */
int dagen[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

int main()
{
   int i;

   for (i=0; i<12; i++)
   {
      printf("%d dagen in maand %d\n",dagen[i],i+1);
   }

   return 0;
}

De waarden waarmee de array gevuld wordt, worden tussen accolades geplaatst. Indien er te weinig waarden zijn, dan worden de laatste elementen van de array met 0 gevuld. In de extern verwijzing binnen main() mag de afmeting van de array weggelaten worden.

Hier is een andere versie:

/* dagen per maand */
int dagen[] = {31,28,31,30,31,30,31,31,30,31,30,31};

int main()
{
   int i;

   for (i=0; i<sizeof(dagen)/sizeof(int); i++)
   {
      printf("%d dagen in maand %d\n",dagen[i],i+1);
   }
   return 0;
}

In deze versie is de lengte van de array weggelaten. De lengte wordt nu bepaald door het aantal getallen tussen accolades. De lengte mag alleen maar weggelaten worden als de array geïnitialiseerd wordt.

Verband tussen pointers en arrays

De arraynaam is een pointer naar eerste element. Dit verband verduidelijken we met een voorbeeld.

int rij[20];

Bij deze array is rij\[0\] het eerste element. Het adres hiervan is &rij\[0\]. Dit kan ook korter geschreven worden: rij en &rij\[0\] zijn hetzelfde. Ze duiden allebei het adres van de array aan. Het zijn allebei pointerconstanten.

In het volgende voorbeeld wordt er met pointers gerekend.

int main()
{
   int getallen[4], *pget, i;
   char tekens[4], *ptek;

   pget = getallen;
   ptek = tekens;
   for (i=0; i<4; i++)
   {
      printf("pointers + %d: %u %u\n",
         i, pget + i, ptek + i);
   }
   return 0;
}

De eerste toekenning plaatst het adres van de array getallen in de pointervariabele pget. De tweede toekenning doet een gelijkaardige bewerking. In de printf() opdracht wordt de lusteller i opgeteld bij de inhoud van de pointers. Dit resultaat komt op het scherm:

pointers + 065486 65498
pointers + 165488 65499
pointers + 265490 65500
pointers + 365492 65501

De eerste regel geeft de adressen van de eerste elementen van de arrays. De volgende regel geeft de adressen van de tweede elementen enzovoort. We zien dus het volgende: als we de inhoud pointer verhogen met 1, dan wordt het adres, dat in de pointer variabele wordt opgeslagen, verhoogd met de breedte van het aangeduide element. De pointer pget wijst naar int, int is 2 bytes breed dus wordt er 2 opgeteld bij de inhoud van pget. Dezelfde regel kunnen we toepassen voor de pointer ptek. Die wordt verhoogd met 1 ( breedte char ).

De array getal kan zo voorgesteld worden:

!<>

getal + 2 en &getal\[2\] stellen beide hetzelfde adres voor.

\*(getal + 2) en getal\[2\] stellen beide dezelfde waarde voor.

Opgelet: \*getal + 2 is de waarde van het eerste element verhoogd met 2. Deze uitdrukking is dus niet hetzelfde als \*(getal + 2). De haken zijn nodig omdat \* een hogere prioriteit heeft dan +.

Hetzelfde probleem ontstaat bij de interpretatie van \*p++ . Is dit (\*p)++ of \*(p++) ? Het antwoord is de tweede uitdrukking omdat \* en ++ dezelfde prioriteit hebben en unaire operatoren van rechts naar links groeperen.

Arrays als functieparameters

Als formele parameter kunnen we arrays gebruiken. De afmeting van de array mag weggelaten worden.

void druk(int rij[])
{

}

int main()
{
   int reeks[50];

   druk(reeks);

   return 0;
}

Bij formele parameters is int rij\[\] een pointer variabele, geen array variabele. We geven hier niet de inhoud van de array door, maar wel het adres. Dus int rij\[\] en int \*rij zijn hetzelfde als formele parameter.

We kunnen de lengte van de array doorgeven:

void druk(int rij[], int lengte)
{
   int i;

   for (i=0; i<lengte; i++)
   {
      printf("%d\n", rij[i]);
   }
}

Deze versie doet identiek hetzelfde, alleen de toegang tot de array is gewijzigd:

void druk(int rij[], int lengte)
{
   int i;

   for (i=0; i<lengte; i++)
   {
      printf("%d\n", *(rij + i) );
   }
}

En tot slot de snelste versie:

void druk(int rij[], int lengte)
{
   register int*p, *peinde;

   p = rij;/* bew 1 */
   peinde = &rij[lengte];
   while (peinde - p >0)/* bew 5 */
   {
      printf("%d\n", *p );/* bew 2 */
      p++;/* bew 4 */
   }
}

In deze laatste versie wordt de pointervariabele p gebruikt om de elementen van de array te bereiken. Met de ++ operator wijst p telkens naar het volgende element in de array. De herhaling stopt als p het eerste adres aanwijst dat niet tot de array behoort.

Hier is een samenvatting van de pointerbewerkingen:

  • ​1. toekenning

Een adres wordt toegekend aan een pointervariabele.

  • ​2. waarde ophalen

De \* bewerking vindt de waarde die door de pointer wordt aangeduid.

  • ​3. een pointeradres nemen

De pointer int \*p bevindt zich op adres &p. Dit kan dienen als actuele parameter voor een functie die de doorgegeven pointer wijzigt.

  • ​4. een pointer verhogen

Na deze bewerking wijst de pointer naar het volgende element.

  • ​5. het verschil tussen 2 pointers

Dit geeft het aantal elementen dat zich tussen de 2 aangeduide posities bevindt.

Arrays met meerdere dimensies

Bij de declaratie plaatsen we meerdere indexen na de arraynaam. Elke index staat apart tussen de rechte haken.

double matrix[3][4];

In het volgende voorbeeld zien we de initialisatie en het gebruik van een meerdimensionele array.

#include <stdio.h>

int main()
{
   static double matrix[3][4] =
   {
      { 2,5,9,7 },
      { 8,1,3,4 },
      { 10,5,45,23 }
   };
   int i,j;

   for (i=0; i<3; i++)
   {
      for (j=0; j<4; j++)
      {
         printf("%5.2f ", matrix[i][j] );
      }
      printf("\n");
   }
   return 0;
}

De variabele matrix kunnen we voorstellen als een matrix die bestaat uit 3 rijen met elk 4 elementen. De variabele wordt rij per rij geïnitialiseerd (alleen statische en externe arrays kunnen geïnitialiseerd worden). De getallen 2, 5, 9 en 7 komen terecht in de eerste rij. We kunnen ze terugvinden in de elementen matrix\[0\]\[0\], matrix\[0\]\[1\], matrix\[0\]\[2\] en matrix\[0\]\[3\]. Op dezelfde wijze worden de twee andere rijen gevuld. Het is ook mogelijk om de binnenste paren accolades, die telkens een rij getallen afsluiten, weg te laten. Dit is identiek in werking maar is minder overzichtelijk.

Pointers naar functies

Zoals reeds vermeld moet bij de declaratie van een pointer aangegeven worden naar welk type deze pointer wijst. We kunnen in de taal C ook een functietype gebruiken als het aangewezen type. We geven een voorbeeld:

int (*pf)(int a,int b);

De variabele pf is een pointer die wijst naar een functie die 2 int verwacht en een int als resultaat. Deze variabele krijgt met een toekenning een waarde.

int fu(int a,int b)
{
   return( a + b);
}

pf = fu;

De pointer pf krijgt als waarde het adres van de functie fu. We kunnen de aangewezen functie oproepen via de pointervariabele.

c = (*pf)(1,2);

Tekenstrings en stringfunctions

Strings definiëren

Een string is een opeenvolging van char constanten, waarbij het einde aangeduid wordt door 0. We kunnen een stringconstante samenstellen met 2 aanhalingstekens:

"dit is een string"

Deze constante heeft een dubbele functie: ze zorgt voor opslag van de tekens in het geheugen en ze fungeert als constante van het type pointer naar char. In het volgende voorbeeld wordt het adres van een stringconstante opgeslagen in een pointervariabele.

char *pstr;

pstr = "dit is een string";
printf("%s",pstr);

Via initialisatie wordt een stringconstante opgeslagen in een char arrayvariabele. Tussen de rechte haken hoeft geen afmeting vermeld te worden.

char str1[] = {'a','b','c','d','e','\0' };

ofwel

char str1[] = "abcde";

De lengte van array is 6: 5 tekens + 1 nul. Als we de naam str1 gebruiken, dan is dit een pointer naar het eerste element. Zo kunnen we enkele gelijke uitdrukkingen opstellen:

str1 en &str1\[0\]

\*str1 en ``

\*(str1+2) en str\[2\] en ``

Er is een verschil tussen de array en de pointer declaratie, maar wel zijn het allebei geïnitialiseerde variabelen:

char *ptekst = "een";
char atekst[]= "twee";

De variabele ptekst is een pointer die geïnitialiseerd wordt met het adres van de string "een". Deze string bevindt zich elders in het geheugen. De variabele atekst is een array die geïnitialiseerd wordt met de string "twee". Dit betekent dat atekst plaats heeft voor 5 tekens. We kunnen de geheugenverdeling zo voorstellen:

!<>

We hebben de volgende overeenkomsten:

&ptekst ---> 30
ptekst       120
*ptekst      'e'
ptekst[0]    'e'
atekst       34
*atekst      't'
atekst[0]    't'

ptekst is een pointervariabele en kan dus gewijzigd worden; atekst niet:

while ( *ptekst != 0)
{
   putchar ( *ptekst++ );
}

Deze herhaling drukt alle tekens van de string op het scherm.

atekst is een pointerconstante die wijst naar het eerste element van de array. atekst kan niet gewijzigd worden.

atekst++;  // FOUT

De inhoud van de array kan wel gewijzigd worden:

atekst[0] = 'p';

Arrays van tekenstrings

We declareren de volgende variabele:

char *kleuren[3] ={ "wit",
                    "zwart", "azuurblauw" };

De variabele kleuren is een array van pointers die wijzen naar char elementen. De pointers zijn elk geïnitialiseerd met het adres van een string. De uitdrukkingen kleuren\[0\], kleuren\[1\], en kleuren\[2\] zijn de 3 pointers. Als we er een \* bijplaatsen krijgen we: \*kleuren\[0\] is de eerste letter van de eerste string. In kleuren worden alleen adressen opgeslagen; de strings zelf worden elders in het geheugen opgeslagen.

!<>

Deze variabele kan ook anders gedeclareerd worden. Het is nu een array met 2 dimensies. De strings worden in de array zelf opgeslagen. Voor de string "wit" betekent dit dat slechts een deel van de rij gebruikt wordt. Een deel van de array blijft dus onbenut.

char kleuren[3][11] ={ "wit",
                    "zwart", "azuurblauw" };

!<>

====Stringin- en uitgave

We creëren eerst plaats voor de in te lezen string.

char *naam;

scanf("%s", naam);

Deze declaratie van naam levert een crash op; naam is een pointervariabele, die geen waarde gekregen heeft. scanf() gebruikt de inhoud van naam als adres om de ingelezen tekens op te slaan. Het resultaat is dus onvoorspelbaar. Een betere declaratie is dit:

char naam[81];

Stringin- en uitvoer doen we met de gets() en puts() functies.

int main()
{
   char naam[20][81]; /* plaats voor 20 namen */
   int   i,n;

   n = 0;
   while (gets(naam[n]) != NULL)
   {
      n++;
   }
   for (i=0; i<n; i++)
   {
      puts(naam[i]);
   }
}

Het programma leest een aantal strings in met gets() en geeft daarna deze strings weer op het scherm. De functie gets() levert als resultaat het adres van de ingelezen string. Als de voorwaarde EOF (dit is end of file) voorkwam tijdens de ingave, is het resultaat 0. Deze eigenschap wordt in het programma gebruikt om de herhaling van de ingave stop te zetten. Let op de notatie naam\[n\], dit is hetzelfde als &naam\[n\]\[0\].

Er zijn een aantal verschillen ten opzichte van printf("%s") en scanf("%s"). gets() leest alle ingegeven tekens in tot de return; de return zelf wordt niet opgenomen in de string. scanf("%s") start de string na de eerste whitespace (tab, newline of spatie) en stopt voor de volgende whitespace. Dit kan gebruikt worden om woorden uit een regel in te lezen. puts() doet altijd een newline op het einde van de string, printf() alleen als \n vermeld wordt.

Enkele stringfuncties

We bespreken enkele van de belangrijkste stringfuncties.

strlen()

Deze functie berekent de lengte van een string in bytes.

void pas(char *string, int lengte)
{
   if (strlen(string)>lengte)
   {
      *(string + lengte) = '\0';
   }
}

De functie pas() kort een string in tot een gegeven lengte. Dit wordt gedaan door een 0 in de string bij te plaatsen.

strcat()

Deze functie voegt 2 strings samen.

int main()
{
   char naam[80];

   gets(naam);
   strcat(naam," is een mooie naam\n");
   puts(naam);

   return 0;
}

De functie strcat() ontvangt 2 char pointers. De string aangeduid door de eerste pointer wordt uitgebreid met de string aangeduid door de tweede pointer. De eerste string moet voldoende plaats hebben, anders worden andere variabelen overschreven.

strcmp()

Deze functie vergelijkt 2 strings. Als de strings identiek zijn, is het resultaat 0, anders is het resultaat verschillend van 0.

int main()
{
   char antw[40];

   puts("waar woonde Julia ?");
   gets(antw);
   if (strcmp(antw, "Verona") == 0)
   {
      puts("goed");
   }
   else
   {
      puts("fout");
   }
   return 0;
}

strcpy()

Deze functie kopieert een string.

char kopie[40],*p;
p = "origineel";
strcpy(kopie,p);

In dit voorbeeld worden de letters van de string een voor een gekopieerd naar de array kopie.

Argumenten op de opdrachtregel

De argumenten die bij de programmastart worden doorgegeven, zijn bereikbaar vanuit het programma. Hiervoor wordt main() voorzien met twee formele parameters.

int main(int argc, char *argv[])
{
   int i;

   for (i=0; i<argc; i++)
   {
      printf("%s ",argv[i]);
   }
   printf("\n");

   return 0;
}

De eerste parameter argc geeft aan hoeveel argumenten er bij de programmastart meegegeven zijn. In dit aantal is de programmanaam meegerekend. De tweede parameter argv is een tabel van pointers naar char. Elke pointer wijst naar het eerste teken van een argumentstring. Deze strings zijn afgesloten met een 0. argv\[0\] wijst naar de programmanaam, argv\[1\] is het eerste argument, enzoverder. Het gebruik van een pointertabel laat een variabel aantal argumenten toe.

Strings sorteren

Tot slot is hier nog een programmavoorbeeld, dat strings sorteert.

#include <stdio.h>
#include <string.h>

#define SLEN 81
#define DIM 20
#define STOP""

void strsort(char *strings[], int num)
{
   char *temp;
   int klein, zoek;

   for (klein=0; klein<num-1; klein++)
   {
      for (zoek=klein+1; zoek<num; zoek++)
      {
         if ( strcmp(strings[klein],strings[zoek]) >0)
         {
            temp = strings[klein];
            strings[klein] = strings[zoek];
            strings[zoek] = temp;
         }
      }
   }
}

int main()
{
   static char ingave[DIM][SLEN]; /* array voor ingave */
   char *pstr[DIM];               /* pointer tabel */
   int tel = 0;
   int k;

   printf("geef strings in\n");
   printf("eindig met een lege string\n");
   while( tel<DIM && gets(ingave[tel]) != NULL
          && strcmp(ingave[tel],STOP) != 0)
   {
      pstr[tel] = ingave[tel];
      tel++;
   }

   /* sorteer met pointers */
   strsort(pstr, tel);

   puts("\ndit is gesorteerd\n");
   for (k=0; k<tel; k++)
   {
      puts(pstr[k]);
   }
}

Dit programma leest eerst een aantal strings in. De strings komen in de tweedimensionele array ingave terecht. De herhaling van de ingave stopt als er geen plaats meer is voor strings of als EOF optreedt of als er een lege string ingegeven wordt. Tijdens de ingave wordt de pointertabel pstr gevuld met het adres van elke string.

Met deze tabel pstr wordt het sorteren uitgevoerd. In plaats van strings te kopiëren (veel tekens kopiëren) worden er pointers gekopieerd. De pointertabel pstr wordt samen met het aantal strings doorgegeven aan de functie strsort(). Deze functie start bij de eerste string en gaat na of er verder nog strings zijn die kleiner zijn. Kleiner betekent hier: komt eerst in de alfabetische rangschikking. Hier wordt gebruik gemaakt van de eigenschap dat strcmp() iets zegt over de alfabetische volgorde als de 2 strings verschillend zijn. De mogelijke resultaten zijn:

strcmp("a", "a")    // geeft 0
strcmp("b", "a")    //       1 (positief)
strcmp("a", "b")    //      -1 (negatief)

Indien een kleinere string gevonden wordt, dan worden de pointers die wijzen naar de eerste en de gevonden string verwisseld. Hetzelfde wordt herhaald voor de tweede tot en met de voorlaatste string.

Overzicht string functies

De prototypes van de functies voor stringmanipulatie zijn terug te vinden in de headerbestand string.h.

strcpy()

Kopieert string src naar dest.

Prototype:

char *strcpy(char *dest, const char *src);

Geeft dest terug.

#include <stdio.h>
#include <string.h>

int main()
{
   char string[10];
   char *str1 = "abcdefghi";

   strcpy(string, str1);
   printf("%s\n", string);
   return 0;
}

strncpy()

Kopieert maximum maxlen tekens van src naar dest.

Prototype:

char *strncpy(char *dest, const char *src, size_t maxlen);

Indien maxlen tekens gekopieerd worden, wordt geen nul teken achteraan bijgevoegd; de inhoud van dest is niet met een nul beëindigd.

Geeft dest terug.

#include <stdio.h>
#include <string.h>

int main()
{
   char string[10];
   char *str1 = "abcdefghi";

   strncpy(string, str1, 3);
   string[3] = '\0';
   printf("%s\n", string);
   return 0;
}

strcat()

Voegt src bij dest.

Prototype:

char *strcat(char *dest, const char *src);

Geeft dest terug.

#include <string.h>
#include <stdio.h>

int main()
{
   char destination[25];
   char *blank = " ", *c =
                        "C++", *Borland = "Borland";

   strcpy(destination, Borland);
   strcat(destination, blank);
   strcat(destination, c);

   printf("%s\n", destination);

   return 0;
}

strncat()

Voegt maximum maxlen tekens van src bij dest.

Prototype:

char *strncat(char *dest, const char *src, size_t maxlen);

Geeft dest terug.

#include <string.h>
#include <stdio.h>

int main()
{
   char destination[25];
   char *source = " States";

   strcpy(destination, "United");
   strncat(destination, source, 7);
   printf("%s\n", destination);

   return 0;
}

strcmp()

Vergelijkt een string met een andere

Prototype:

int strcmp(const char *s1, const char *s2);

Geeft een waarde terug:

< 0 indien s1 kleiner dan s2

== 0 indien s1 gelijk is aan s2

> 0 indien s1 groter is dan s2

Voert een vergelijking met teken uit.

#include <string.h>
#include <stdio.h>

int main()
{
   char *buf1 = "aaa";
   char *buf2 = "bbb",
   char *buf3 = "ccc";
   int ptr;

   ptr = strcmp(buf2, buf1);
   if (ptr > 0)
   {
      printf("buffer 2 is greater than
                        buffer 1\n");    // ja
   }
   else
   {
      printf("buffer 2 is less than buffer 1\n");
   }
   ptr = strcmp(buf2, buf3);
   if (ptr > 0)
   {
      printf("buffer 2 is greater than
                        buffer 3\n");
   }
   else
   {
      printf("buffer 2 is less than buffer
                        3\n");       // ja
   }

   return 0;
}

strncmp()

Vergelijkt maximum maxlen tekens van de ene string met de andere.

Prototype:

int strncmp(const char *s1, const char *s2,
                        size_t maxlen);

Geeft een waarde terug:

< 0 indien s1 kleiner dan s2

== 0 indien s1 gelijk is aan s2

> 0 indien s1 groter is dan s2

Voert een vergelijking met teken (signed char) uit.

#include <string.h>
#include <stdio.h>

int main()
{
   char *buf1 = "aaabbb", *buf2 =
                        "bbbccc", *buf3 = "ccc";
   int ptr;

   ptr = strncmp(buf2,buf1,3);
   if (ptr > 0)
   {
      printf("buffer 2 is greater than
                        buffer 1\n");    // ja
   }
   else
   {
      printf("buffer 2 is less than buffer 1\n");
   }

   ptr = strncmp(buf2,buf3,3);
   if (ptr > 0)
   {
      printf("buffer 2 is greater than
                        buffer 3\n");
   }
   else
   {
      printf("buffer 2 is less than buffer
                        3\n");       // ja
   }
   return(0);
}

strchr()

Zoekt een teken c in s.

Prototype:

char *strchr(const char *s, int c);

Geeft een pointer terug naar de eerste plaats waar het teken c in s voorkomt; indien c niet voorkomt in s, geeft strchr NULL terug.

#include <string.h>
#include <stdio.h>

int main()
{
   char string[15];
   char *ptr, c = 'r';

   strcpy(string, "This is a string");
   ptr = strchr(string, c);
   if (ptr)
   {
      printf("The character %c is at
                        position: %d\n", c,
                   ptr-string); // 12
   }
   else
   {
      printf("The character was not found\n");
   }
   return 0;
}

strrchr()

Zoekt de laatste plaats waar c in s voorkomt.

Prototype:

char *strrchr(const char *s, int c);

Geeft een pointer terug naar de laatste plaats waar c voorkomt, of NULL indien c niet voorkomt in s.

#include <string.h>
#include <stdio.h>

int main()
{
   char string[15];
   char *ptr, c = 'i';

   strcpy(string, "This is a string");
   ptr = strrchr(string, c);
   if (ptr)
   {
      printf("The character %c is at
                        position: %d\n", c,
                   ptr-string); // 13
   }
   else
   {
      printf("The character was not found\n");
   }
   return 0;
}

strspn()

Doorzoekt een string naar een segment dat is een subset van een reeks tekens.

Prototype:

size_t strspn(const char *s1, const char *s2);

Geeft de lengte van het initiële segment van s1 dat volledig bestaat uit tekens uit s2.

#include <stdio.h>
#include <string.h>
#include <alloc.h>

int main()
{
   char *string1 = "1234567890";
   char *string2 = "123DC8";
   int length;

   length = strspn(string1, string2);
   printf("strings different at position
                        %d\n",length); // 3
   return 0;
}

strcspn()

Doorzoekt een string.

Prototype:

size_t strcspn(const char *s1, const char *s2);

Geeft de lengte van het initiële segment van s1 terug dat volledig bestaat uit tekens niet in s2.

#include <stdio.h>
#include <string.h>
#include <alloc.h>

int main()
{
   char *string1 = "1234567890";
   char *string2 = "747DC8";
   int length;

   length = strcspn(string1, string2);
   printf("strings intersect at position
                        %d\n", length); // 3

   return 0;
}

=====strpbrk()

Doorzoekt een string voor de eerste plaats waar een willekeurig teken uit de tweede string voorkomt.

Prototype:

char *strpbrk(const char *s1, const char *s2);

Geeft een pointer terug naar de eerste plaats waar een van de tekens uit s2 in s1 voorkomt. Indien geen van de s2 tekens in s1 voorkomen, wordt NULL teruggegeven.

#include <stdio.h>
#include <string.h>

int main()
{
   char *string1 = "abcdefghijklmnopqrstuvwxyz";
   char *string2 = "onm";
   char *ptr;

   ptr = strpbrk(string1, string2);

   if (ptr)
   {
      printf("found first character:
                        %c\n",*ptr);// 'm'
   }
   else
   {
      printf("strpbrk didn't find
                        character in set\n");
   }
   return 0;
}

strstr()

Zoekt de eerste plaats waar een substring in een andere string voorkomt.

Prototype:

char *strstr(const char *s1, const char *s2);

Geeft een pointer terug naar het element in s1 dat s2 bevat (wijst naar s2 in s1), of NULL indien s2 niet voorkomt in s1.

#include <stdio.h>
#include <string.h>

int main()
{
   char *str1 = "Borland International";
   char *str2 = "nation", *ptr;

   ptr = strstr(str1, str2);
   printf("The substring is: %s\n",
                        ptr); // "national"
   return 0;
}

strlen()

Berekent de lengte van een string.

Prototype:

size_t strlen(const char *s);

Geeft het aantal tekens in s terug, de eindnul wordt niet meegeteld.

#include <stdio.h>
#include <string.h>

int main()
{
   char *string = "Linux";

   printf("%d\n", strlen(string)); // 5
   return 0;
}

strtok()

Zoekt in s1 naar het eerste teken dat niet voorkomt in in s2.

Prototype:

char *strtok(char *s1, const char *s2);

s2 definieert scheidingstekens. strtok() interpreteert de string s1 als een reeks tokens gescheiden door een reeks tekens uit s2.

Indien geen tokens gevonden worden in s1, wordt NULL teruggegeven.

Indien het token gevonden is , wordt een nulteken in s1 geschreven volgend op het token, en strtok geeft een pointer terug naar het token.

Volgende oproepen van strtok() met NULL als eerste argument gebruiken de vorige s1 string, vanaf het laatst gevonden token.

#include <stdio.h>
#include <string.h>

int main()
{
  char s[] ="aze ry   iio sdf";
  char *p;

  p = strtok(s," ");
  while(p != NULL)
  {
     printf("%s\n",p);
     p = strtok(NULL," ");
  }

  return 0;
}

Structuren

malloc() en free()

Met de functies malloc() en free() kan je blokken geheugen op dynamische wijze reserveren en vrijgeven. Met malloc() doe je de reservatie en met free() wordt een blok geheugen vrijgegeven. De prototypes van beide functies zien er zo uit:

void *malloc(size_t size);
void free(void *ptr);

De functie malloc() verwacht als parameter de grootte van het blok geheugen in bytes. Als resultaat geeft de functie een pointer naar de eerste byte van het blok terug. Een blok geheugen dat je zo gereserveerd hebt, moet je met free teruggeven. Als je dat niet doet (vrijwillig of onvrijwillig), is dat een fout. Er is dan sprake van een geheugenlek. Met het commando valgrind in Linux kan je nagaan of er geheugenlekken in een programma voorkomen.

Het volgende voorbeeld toont hoe je dynamisch een array van 100 int’s kan reserveren. Je moet wel de grootte van het blok geheugen berekenen en aan malloc() doorgeven. In dit geval is dit 100 \* sizeof(100). Je ziet in het voorbeeld dat het adres dat malloc() teruggeeft, met een cast omgezet wordt van void * naar int \*.

#include <stdio.h>
#include <stdlib.h>

int main()
{
  int *p = (int *) malloc(100 * sizeof(4));
  int   i;

  for (i=0; i<100; i++)
  {
     p[i] = i*i;
  }

  free(p);
  return 0;
}

Het voordeel van de werkwijze in dit voorbeeld is de mogelijkheid om de grootte van het blok geheugen pas te bepalen op het moment dat de reservatie gebeurt. Hierdoor kan een programma zich aanpassen aan de geheugenbehoefte van het moment zelf. Als je arrays met een vaste afmeting declareert, heb je die flexibiliteit niet. Merk op dat de handelswijze in het voorbeeld overeenkomt met de wijze waarop arrays in Java worden gereserveerd. In Java is dat altijd dynamisch.

Een verschil tussen Java en C/C++ is het feit dat in Java de vrijgave van blokken geheugen automatisch verloopt. Het dynamisch geheugen wordt bijgehouden in een zone die men de heap noemt. Dit is geldig voor zowel C/C++ als Java. In C moet je na gebruik het geheugen vrijgeven met free(); in C++ doe je dat met de delete operator. In Java mag je deze stap achterwege laten. In Java worden regelmatig alle blokken geheugen opgespoord die niet meer in gebruik zijn. Deze blokken worden dan automatisch vrijgegeven. Deze bewerking loopt in de achtergrond en noemt men garbage collection. Je moet dus begrijpen dat de correct werkende C/C++ programma’s geen geheugenlekken mogen hebben. Bij langdurig lopende programma’s kunnen geheugenlekken een tekort aan dynamisch geheugen veroorzaken. Dit leidt tot malloc() oproepen die een NULL als resultaat teruggeven om te melden dat er geen geheugen meer beschikbaar is. Dit kan pointerfouten veroorzaken.

Types maken met typedef

We bespreken eerst de mogelijkheid om aan zelf gedefinieerde types een naam te geven. Dit bespaart schrijfwerk bij het declareren van variabelen en parameters. Dit laat ook toe om globaal het type van een soort gegeven te wijzigen. We creëren types METER, VECTOR en STRING met behulp van typedef.

typedef int METER;
typedef double VECTOR[3];
typedef char STRING[80];

Met typedef leggen we vast dat het nieuwe type METER overeenkomt met het type int. Net zoals bij #define wordt het type METER in hoofdletters geschreven. Dit is geen verplichting, maar wordt door veel programmeurs toegepast om het onderscheid te maken tussen constanten, types en variabelen. Met deze nieuwe types declareren we variabelen.

METER afstand;
VECTOR vect1,vect2;
STRING tekst="abcdefghijklmnopqrstuvwxyz";

Een zelf gedefinieerd type kan ook bij functies gebruikt worden.

void druk(STRING v)
{
   printf("dit is de string: %s\n", v);
}

Het is zo dat het gebruik van typedef de leesbaarheid van het programma verbetert.

Structuur

Met een structuur kunnen we een type ontwerpen, dat gegevens van een verschillende soort samenbrengt. We doen dit met het woord struct.

struct boek
{
   char titel[40];
   char auteur[40];
   float prijs;
};

Deze declaratie creëert een structuur met de naam boek. Ze bestaat uit 2 char arrays en een float. Met deze structuur declareren we een variabele.

struct boek roman1;

Een variabele kan gedeclareerd worden met initialisatie.

struct boek roman2 = {
"De loteling","Conscience",399.5 };

De roman2 bevat de velden titel, auteur en prijs. Het veld titel krijgt de waarde "De loteling", auteur wordt "Conscience" en prijs wordt 399.5 .

We kunnen een structuur vastleggen als een nieuw type met typedef.

typedef struct
{
   double x;
   double y;
} PUNT;

Merk op dat er geen structuurnaam, maar alleen een typenaam is. Met het type PUNT declareren we enkele variabelen.

PUNT p1,p2,p3;

Deze variabelen hebben elk de velden x en y, waarin een double opgeslagen wordt. De toegang tot velden wordt getoond in de volgende functie.

PUNT helft(PUNT pa,PUNT pb)
{
   PUNT pc;

   pc.x = (pa.x + pb.x)/2;
   pc.y = (pa.y + pb.y)/2;
   return( pc );
}

Deze functie ontvangt 2 variabelen van het type PUNT en levert een resultaat van hetzelfde type. We kunnen een veld bereiken door de variabelenaam uit te breiden met een punt en de veldnaam.

p1.x = 1;
p1.y = 2;
p2 = p1;/* x en y velden worden gekopieerd */
p3 = helft( p1, p2 );

Wanneer een structuurvariabele wordt toegekend aan een andere, wordt de hele structuur gekopieerd. Dus elk veld van de ene variabele wordt gekopieerd naar elk veld van de andere.

Arrays van structuren

We declareren polygoon als een array van 50 elementen van het type PUNT.

PUNT polygoon[50];

Dit zijn de velden van het eerste element van polygoon.

polygoon[0].x
polygoon[0].y

Dit zijn de velden van het laatste element.

polygoon[49].x
polygoon[49].y

Het volgende voorbeeld berekent de lengte van een polygoon als de som van de afstand tussen de opeenvolgende punten.

double afstand(PUNT pa, PUNT pb)
{
   double x,y;

   x = pa.x - pb.x;
   y = pa.y - pb.y;
   return( sqrt(x*x + y*y) );
}

lengte = 0;
for (i=0; i<49; i++)
{
   lengte += afstand(polygoon[i], polygoon[i+1]);
}

Bij de oproep van afstand() zien we de notatie polygoon\[i\]. Deze uitdrukking is van het type PUNT en dit komt overeen met de declaratie van de formele parameters van afstand().

Pointers naar structuren

Net zoals een pointer naar een int type kunnen we een pointer declareren die naar het type PUNT wijst.

PUNT *p_ptr;

We zien dat door het gebruik van het type PUNT in plaats van de hele structuurnotatie, de declaratie leesbaar blijft. Vermits p_ptr een pointervariabele is, kan hierin het adres van een PUNT variabele geplaatst worden.

p_ptr = &p1;

Om een veld van de aangewezen structuur te bereiken, schrijven we:

(*p_ptr).x

Deze waarde is dezelfde als p1.x omdat p_ptr naar p1 wijst. De haken zijn nodig omdat . een hogere prioriteit heeft dan \* . We schrijven dit in een andere vorm.

p_ptr->x

De notatie -> is dus een samentrekking van het sterretje en het punt.

In de functie maaknul() wordt geen structuur doorgegeven maar wel een pointer naar een structuur. Dit is nodig omdat de PUNT variabele waarvan het adres doorgegeven wordt aan de functie, gewijzigd wordt.

void maaknul(PUNT *p)
{
   p->x = 0;
   p->y = 0;
}

Dit is het gebruik van de functie:

PUNT p1,p2,p3;

maaknul( &p1 );
maaknul( &p2 );
maaknul( &p3 );

Structuur binnen structuur

Het is mogelijk om als type voor een veld een zelf gedefinieerd type te gebruiken.

typedef struct
{
   int jaar;
   int maand;
   int dag;
} DATUM;

typdef struct
{
   DATUM van;
   DATUM tot;
} PERIODE;

PERIODE contract;

De variabele contract is van het type PERIODE en bestaat dus uit de velden van en tot. Deze velden zijn op hun beurt structuren. Ze bestaan uit de velden jaar, dag en maand.

De velden kunnen zo bereikt worden:

contract.tot.jaar
contract.van.dag

Deze uitdrukkingen moeten we zo interpreteren:

(contract.van).dag

De . operator groepeert dus van links naar rechts.

Unions

Soms is het nodig om een bepaalde waarde onder verschillende vormen bereikbaar te maken. Dit doen we met union.

typedef union
{
   float fwaarde;
   long lwaarde;
} MASKER;

De schrijfwijze is identiek met die van struct. We hoeven maar het woord struct te vervangen door union. De betekenis is anders. In tegenstelling tot struct wordt hier maar een keer geheugenruimte gereserveerd. In dit voorbeeld hebben de types float en long dezelfde afmeting. Er wordt dus 4 bytes geheugen gereserveerd. Indien de velden een verschillende afmeting hebben, dan bepaalt het grootste veld de hoeveelheid gereserveerd geheugen.

MASKER getal;
getal.fwaarde = 3.14159;
printf("voorstelling van %f in hex is %lx\n",
getal.fwaarde,getal.lwaarde);

Resultaat:

voorstelling van 3.141590 in hex is 40490fcf

In dit voorbeeld wordt er voor getal 4 bytes gereserveerd. Dit geheugen is bereikbaar met twee namen: getal.fwaarde en getal.lwaarde. We plaatsen een float-constante in getal en daarna toont printf() op welke wijze dit opgeslagen wordt.

Bitvelden

In sommige gevallen is het bereik van de werkelijke waarden van een veld slechts een fractie van het maximale bereik. In dat geval is het wenselijk om de velden te declareren op bitniveau.

typedef struct
{
   unsigned int jaar:12;/* 0 - 4095*/
   unsigned int maand:4;/* 0 - 15*/
   unsigned int dag:5;/* 0 - 31*/
   unsigned int ongeveer:1;/* 0 - 1*/
} CDATUM;

Elk veld heeft nu een aangepast bereik. Dit verkrijgen we door na elke veldnaam dubbele punt en bitbreedte bij te voegen. De veldbreedte in bit mag niet groter zijn dan de woordbreedte van de computer. Als type voor een bitveld mogen we alleen maar unsigned of signed int gebruiken.

De veldnaam mag weggelaten worden. Hiermee kan men ongebruikte bits overslaan.

struct metbits
{
   int i:2;
   unsigned j:5;
   int:4;
   int k:1;
   unsigned m:4;
} a,b,c;

De bitverdeling van a, b en c ziet er als volgt uit:

!<>

De bits 7 tot 10 zijn niet gebruikt.

Structuren en lijsten

Zelfreferentiële structuren

Dit zijn structuren die een of meerdere pointers bevatten die naar eenzelfde soort structuur verwijzen. Deze structuren kunnen gebruikt worden om gegevens op een dynamische manier te organiseren. Men maakt bijvoorbeeld een ketting van zelfreferentiële structuren. Elk knooppunt in deze ketting bevat 1 of meerdere gegevenselementen en bevat ook een verwijzing naar het volgende knooppunt.

Hier is een voorbeeld van een zelfreferentiële structuur.

struct knoop
{
   int  data;
   struct knoop *verder;
};
typedef struct knoop KNOOP;

Het veld verder in deze structuur verwijst naar een andere variabele van het type s+truct knoop+. Het type KNOOP is een synoniem voor struct knoop. We declareren enkele variabelen.

KNOOP a,b,c;

Deze variabelen worden gevuld met gegevens.

a.data = 1;
b.data = 2;
c.data = 3;
a.verder = b.verder = c.verder = NULL;

De velden verder worden voorlopig niet gebruikt en ze krijgen daarom de waarde NULL (is gelijk aan 0). NULL wordt gebruikt om aan te geven dat een pointer naar niets wijst. De huidige toestand stellen we grafisch voor.

!<>

We maken nu de verbinding tussen a, b en c.

a.verder = &b;
b.verder = &c;

!<>

Nu zijn de gegevens vanuit a bereikbaar.

a.data-->1
a.verder->data2
a.verder->verder->data3

Niet gesorteerde gebonden lijsten

De gegevensorganisatie die we daarnet besproken hebben, is een gebonden lijst. We hebben het nu verder over functies die een niet gesorteerde gebonden lijst manipuleren. We creëren een pointervariabele die naar het eerste element wijst.

KNOOP *p_eerste = NULL;

Deze variabele wordt met NULL geïnitialiseerd. Hiermee wordt aangegeven dat de lijst leeg is.

void voegbij(KNOOP **ptr, int getal)
{
   register KNOOP *nieuw_p;

   nieuw_p = (KNOOP *) malloc(sizeof(KNOOP));
   nieuw_p->data = getal;
   nieuw_p->verder = *ptr;
   *ptr = nieuw_p;
}

Deze functie kan zo gebruikt worden:

voegbij( &p_eerste, 4 );
voegbij( &p_eerste, 5 );
voegbij( &p_eerste, 6 );

Het eerste wat opvalt, is de formele parameter KNOOP \*\*ptr. Dit is een dubbele pointer2: ptr heeft als inhoud het adres van een pointer die wijst naar een KNOOP. De actuele parameter is van hetzelfde type: &p_eerste is het adres van een pointer. We geven niet de inhoud van een pointer door, maar wel het adres van die pointer. Dit is nodig omdat p_eerste in de functie gewijzigd moet kunnen worden. Dit gebeurt als het eerste element van de lijst gewist wordt of als er een ander element het eerste wordt.

Het eerste wat voegbij() doet, is het oproepen van malloc(). Dit is een functie die geheugen reserveert. De functie verwacht als actuele parameter het aantal benodigde bytes en levert als resultaat een pointer naar het aangevraagde geheugen. In dit geval hebben we geheugen nodig voor een element van het type KNOOP: dit is sizeof(KNOOP) bytes. Het resultaat van malloc() is een pointer naar char. Dit adres wordt met een cast omgezet tot een pointer naar KNOOP. Het bij te voegen getal wordt in het veld data geplaatst. Het veld verder van het nieuwe element moet nu wijzen naar het eerste element van de oude lijst.

nieuw_p->verder = *ptr;

Het nieuwe element wijst dus naar het element dat vroeger door p_eerste aangewezen werd. ptr bevat het adres van p_eerste. Dus \*ptr is hetzelfde als de inhoud van p_eerste. Tot slot wordt p_eerste gewijzigd.

Dit gebeurt onrechtstreeks:

*ptr = nieuw_p;

p_eerste wijst naar het nieuwe element en dit op zijn beurt wijst naar de oude lijst.

Toestand voor het bijvoegen van het getal 5:

!<>

Toestand erna:

!<>

De inhoud van een lijst kan zichtbaar gemaakt worden met de functie druk().

void druk(KNOOP *ptr)
{
   while (ptr != NULL)
   {
      printf("%d\n", ptr->data);
      ptr = ptr->verder;
   }
}

De functie wordt zo gebruikt:

druk(p_eerste);

De formele parameter ptr is een kopie van de inhoud van p_eerste. Deze kopie mag zonder meer gewijzigd worden zonder dat p_eerste verandert.

De pointer doorloopt de hele lijst tot het einde bereikt is en drukt bij elke herhaling een getal op het scherm.

Hier is een andere versie:

void druk(KNOOP *ptr)
{
   if (ptr != NULL)
   {
      printf("%d\n", ptr->data);
      druk( ptr->verder );
   }
}

Deze versie werkt recursief. Dit wil zeggen dat druk() zichzelf oproept. De functie drukt het getal, dat aangeduid wordt door ptr en drukt dan de rest van de lijst door zichzelf nog eens op te roepen.

De volgende functie zoekt een getal in een lijst en veegt het uit als het gevonden wordt.

void veeguit(KNOOP **ptr; int getal)
{
   KNOOP **p1, *p2;

   /* zoeken */
   p1 = ptr;
   while ( *p1 != NULL && (*p1)->data != getal)
   {
      p1 =&amp;((*p1)->verder);
   }

   if (*p1 != NULL)/* gevonden */
   {
      /* uitvegen */
      p2 = *p1; /* bewaar adres gevonden element */
      *p1 = (*p1)->verder;
      free(p2); /* geheugenvrijgave */
   }
   else
      printf("niet gevonden\n")
}

Aan deze functie wordt het adres van p_eerste doorgegeven, omdat ook hier p_eerste gewijzigd moet kunnen worden. Na het starten wordt een kopie gemaakt naar ptr. Deze pointer wordt gebruikt om telkens op te schuiven naar het volgende element tijdens het zoeken. Dit proces gaat verder zolang het einde van de lijst niet bereikt is

*p1 != NULL

en het getal niet gevonden is.

(*p1)->data != getal

We zien telkens een \* voor p1. Dit is nodig omdat p1 het adres bevat van een KNOOP pointer. \*p1 is dus een pointer naar KNOOP.

Tijdens de herhaling wordt p1 gewijzigd: p1 wijst dan naar het adres van de pointer die wijst naar het volgende element.

Welke waarde krijgt p1?

*p1                 // is adres huidige KNOOP element
(*p1)->verder       //    adres volgende KNOOP element
&((p1)->verder)     //    adres van de pointer die het adres bevat van het volgende KNOOP element

Als de herhaling stopt en \*p1 is NULL, dan is het einde van de lijst bereikt en is het getal niet gevonden. In het andere geval moet het gevonden element geschrapt worden.

Toestand voor het uitvegen van 5:

!<>

Dit is het uitvegen:

!<>

veeguit( &p_eerste, 5);

Toestand erna:

Na het vinden van het getal 5 wijst \*p1 naar het gevonden KNOOP element. Dit adres wordt opzijgezet in p2 en de pointer \*p1 krijgt een nieuwe waarde. Hierdoor wordt het uit te vegen element overgeslagen.

Merk op dat er zich een speciale situatie voordoet als het eerste element van de lijst geschrapt wordt. In dat geval doet de while opdracht geen herhaling en bevat p1 het adres van p_eerste. Hieruit volgt dat p_eerste gewijzigd wordt.

Bestandsin- en uitvoer

In dit hoofdstuk worden een aantal functies beschreven voor het lezen en schrijven van bestanden. Tenzij anders aangegeven zijn de prototypes allemaal terug te vinden in stdio.h. De in- en uitvoerfuncties maken deel uit van de ANSI standaard en maken gebruik van filepointers. Meestal bestaan er op elk systeem ook nog functies die dichter bij de hardware staan en specifiek zijn voor het desbetreffende operating system. Om die reden worden deze functies hier niet beschreven.

====fopen()

Vooraleer er gelezen of geschreven wordt van of naar een bestand, moet een bestand geopend worden. Bij deze actie wordt een filepointer geassocieerd met de file. In de overige functies moet een filepointer meegegeven worden als referentie naar het bestand. In C zijn er twee voorgedefinieerde filepointers voor in- en uitvoer: stdin, stdout en stderr. Ze worden gebruikt voor de invoer van het toetsenbord, de uitvoer naar het scherm en foutuitvoer.

Prototype:

FILE *fopen(const char *filename, const char *mode);

Hierbij is filename een string die het pad van het bestand bevat en kunnen in de string mode de volgende letters voorkomen.

+————————————–+————————————–+ | Letter | Betekenis | +————————————–+————————————–+ | r | open enkel om te lezen | +————————————–+————————————–+ | w | creëer voor schrijven; overschrijft | | | een bestaand bestand | +————————————–+————————————–+ | a | bijvoegen, open voor schrijven op | | | het einde van het bestand of creëer | | | voor schrijven | +————————————–+————————————–+ | + | nog een letter volgt (bv combinatie | | | lezen en schrijven) | +————————————–+————————————–+ | b | open in binaire modus | +————————————–+————————————–+ | t | open in tekstmodus | +————————————–+————————————–+

In de binaire modus worden bytes letterlijk geschreven en gelezen. In de tekstmodus wordt de carriage return/linefeed combinatie vervangen door een enkele linefeed. Bij het lezen van een tekstbestand wordt dus nooit een carriage return aan het programma gegeven. Bij het schrijven gebeurt het omgekeerde.

De functie fopen() geeft als resultaat een filepointer of NULL bij fout.

fclose()

Met deze functie wordt een bestand gesloten.

Prototype:

int fclose(FILE *stream);

De functie geeft als resultaat een 0 bij succes of EOF bij fout.

/* Programma dat een kopie maakt van een bestand */

#include <stdio.h>

int main()
{
  FILE *in, *out;

  if ((in = fopen("file.dat",
                    "rt")) == NULL)
  {
     fprintf(stderr, "Cannot open input file.\n");
     return 1;
  }

  if ((out = fopen("file.bak",
                    "wt")) == NULL)
  {
     fprintf(stderr, "Cannot open output file.\n");
     return 1;
  }

  while (!feof(in))
     fputc(fgetc(in), out);

  fclose(in);
  fclose(out);
  return 0;
}

ferror()

Deze macro geeft als resultaat een waarde verschillend van nul als er een fout is opgetreden bij deze filepointer.

Prototype:

int ferror(FILE *stream);

perror()

Deze functie drukt een foutmelding op het scherm via stderr.

Prototype:

void perror(const char *s);

Eerst wordt de string s gedrukt. Daarna volgt een dubbele punt en een foutmelding die overeenkomt met de huidige waarde van errno.

strerror()

Deze functie geeft als resultaat een foutmelding-string die overeenkomt met het doorgegeven foutnummer.

Prototype (ook in string.h):

char *strerror(int errnum);

_strerror()

Deze functie geeft een string met een foutmelding terug. Het formaat is zoals bij perrror.

Prototype (ook in string.h):

char *_strerror(const char *s);

Het volgende voorbeeld toont hoe foutmeldingen op het scherm kunnen geplaatst worden.

#include <stdio.h>
#include <errno.h>

int main()
{
  FILE *stream;

  /* open a file for writing */
  stream = fopen("DUMM.FIL", "r");

  /* force an error condition by attempting to read */
  (void) getc(stream);

  if (ferror(stream)) /* test for an error on the
                    stream */
  {
     /* display an error message */
     printf("Error reading from DUMMY.FIL\n");
     perror("fout");

     printf("%s\n",strerror(errno));

     printf("%s\n", _strerror("Custom"));

     /* reset the error and EOF indicators */
     clearerr(stream);
  }

  fclose(stream);
  return 0;
}

fwrite()

Met deze functie kan informatie naar een bestand geschreven worden. De functie schrijft n elementen van afmeting size bytes naar het bestand. De pointer ptr wijst naar de te schrijven informatie.

Prototype:

size_t fwrite(const void *ptr, size_t size, size_t n,
                    FILE *stream);

De functie geeft als resultaat het aantal elementen (niet bytes) dat weggeschreven is.

Het volgende voorbeeld toont hoe een structuur naar een binair bestand geschreven wordt.

#include <stdio.h>

struct mystruct
{
   int i;
   char ch;
};

int main()
{
  FILE *stream;
  struct mystruct s;

      /* open file */
  if ((stream = fopen("TEST.$$$",
                    "wb")) == NULL)
  {
     fprintf(stderr, "Cannot open output file.\n");
     return 1;
  }
  s.i = 0;
  s.ch = 'A';

      /* write struct s to file */
  fwrite(&s, sizeof(s), 1, stream);
  fclose(stream); /* close file */
  return 0;
}

fread()

Deze functie leest uit een bestand. Er worden n elementen van afmeting size bytes in de array ptr gelezen.

Prototype:

size_t fread(void *ptr, size_t size, size_t n, FILE *stream);

De functie levert als resultaat het aantal elementen (niet bytes) dat effectief gelezen is.

fseek()

Deze functie verplaatst de wijzer die de positie aangeeft waar eerstvolgend gelezen of geschreven wordt.

Prototype:

int fseek(FILE *stream, long offset, int fromwhere);

offset is de nieuwe positie relatief ten opzichte van de positie gespecificeerd met fromwhere.

De functie geeft 0 bij succes of nonzero bij fout.

De parameter fromwhere kan een van de volgende waarden zijn:

  • SEEK_SET verplaats vanaf het begin van het bestand

  • SEEK_CUR verplaats vanaf de huidige positie

  • SEEK_END verplaats vanaf het einde van het bestand

fgets()

Deze functie leest een regel uit een bestand.

Prototype:

char *fgets(char *s, int n, FILE *stream);

De parameter n geeft aan voor hoeveel tekens er plaats is in de buffer s.

Bij succes, wordt de string s of NULL bij einde van het bestand of fout teruggegeven.

#include <string.h>
#include <stdio.h>

int main()
{
  FILE *stream;
  char string[] = "This is a test";
  char msg[20];

  /* open a file for update */
  stream = fopen("DUMMY.FIL", "w+");

  /* write a string into the file */
  fwrite(string, strlen(string), 1, stream);

  /* seek to the start of the file */
  fseek(stream, 0, SEEK_SET);

  /* read a string from the file */
  fgets(msg, strlen(string)+1, stream);

  /* display the string */
  printf("%s", msg);

  fclose(stream);
  return 0;
}

fputs()

Met deze functie wordt een regel naar een bestand geschreven.

Prototype:

int fputs(const char *s, FILE *stream);

De functie geeft als resultaat bij succes het laatst weggeschreven teken of EOF bij fout.

fgetc()

Met deze functie wordt een teken gelezen uit een bestand.

Prototype:

int fgetc(FILE *stream);

De functie geeft het teken of EOF terug.

fputc()

Met deze functie wordt een teken naar een bestand geschreven.

Prototype:

int fputc(int c, FILE *stream);

fprintf()

Dit is de file-variant van printf.

Prototype:

int fprintf(FILE *stream, const char *format, ...);

De functie geeft als resultaat het aantal geschreven bytes of EOF bij fout.

fscanf()

Dit is de file-variant van scanf.

Prototype:

int fscanf(FILE *stream, const char *format, ... );

De functie geeft als resultaat het aantal in variabelen opgeborgen waarde.

Gereserveerde woorden

auto      extern   sizeof
break     float    static
case      for      struct
char      goto     switch
const     if       typedef
continue  int      union
default   long     unsigned
do        register void
double    return   volatile
else      short    while
enum      signed

Prioriteiten van de operatoren

+————————————–+————————————–+ | Operator | Groepering | +————————————–+————————————–+ | +() \[\] -> | | +————————————–+————————————–+ | ! \~ ++ -- - (type) \* &amp; sizeof | rechts -\> links (unair) | | | | +————————————–+————————————–+ | \* / % | links -> rechts | +————————————–+————————————–+ | + - | links -> rechts | +————————————–+————————————–+ | << >> | | +————————————–+————————————–+ | < <= > >= | links -> rechts | +————————————–+————————————–+ | == != | links -> rechts | +————————————–+————————————–+ | & | links -> rechts | +————————————–+————————————–+ | \^ | links -> rechts | +————————————–+————————————–+ | + | + | +————————————–+————————————–+ | links -> rechts | && | +————————————–+————————————–+ | links -> rechts | + | +————————————–+————————————–+ | | + | +————————————–+————————————–+ | links -> rechts | ?: | +————————————–+————————————–+ | links -> rechts | +$$= += -= \*= /= %= \^= &= | +————————————–+————————————–+ | =$$+ | rechts -> links | +————————————–+————————————–+ | , | links -> rechts | +————————————–+————————————–+

C++

Inleiding

In de geschiedenis van de programmeertalen zijn er regelmatig nieuwe paradigma’s opgedoken. Met elk van die paradigma’s werd een nieuwe programmeerstijl verdedigd. Een voorbeeld hiervan is het gestructureerd programmeren, dat jaren geleden het tijdperk van de goto-loze programmeerstijl inluidde. Deze programmeerstijl was een stap vooruit in het schrijven van duidelijke en leesbare programma’s. Een relatief recente programmeerstijl is het object georiënteerd programmeren. Het is de verdienste van de programmeertaal C++ en later Java gevolgd door Python en Ruby om de object georiënteerde programmeerstijl populair te maken.

Met dit deel krijg je een eerste kennismaking met een object georiënteerde taal. De kennismaking verloopt aan de hand van C++ voorbeelden. Een andere mogelijkheid zou zijn gebruik maken van de recentere taal Java. Deze taal heeft veel weg van C++ maar is ontdaan van alle onhebbelijkheden die in C++ voorkomen. Java laat toe om een programma op verschillende platformen te draaien zonder te hercompileren. Java is ondertussen goed ingeburgerd als eerste OO programmeertaal in de academische bachelor. De belangrijkste reden om na Java toch nog C++ te onderwijzen is dat C++ nog veel gebruikt wordt en in bepaalde toepassingsdomeinen beter voldoet dan Java.

Eigenschappen van object georiënteerde talen

Object georiënteerde talen hebben een aantal specifieke kenmerken die ze onderscheiden van niet object georiënteerde talen. Deze eigenschappen zijn:

Inkapseling van gegevens en methoden

Het is een gekende techniek om gegevens die een sterke relatie met elkaar hebben, te groeperen in een structuur of wat met in object georiënteerde termen een klasse noemt. Deze groepering maakt het gemakkelijker om het overzicht op de gegevens binnen een programma te bewaren.

Hiermee wordt bedoeld dat niet alleen gegevens maar ook acties die inwerken op deze gegevens worden gegroepeerd binnen een klasse. Deze techniek laat toe om de gegevens binnen een klasse af te schermen van de buitenwereld. Wie deze gegevens wil raadplegen om veranderen moet dit doen via speciale functies die bij de klasse horen. De toegang tot de gegevens is niet direct maar indirect. De acties of functies binnen een klasseworden in de object georiënteerde wereld methoden genoemd. Het voordeel van deze techniek is de mogelijkheid om achteraf nog wijze waarop de gegevens binnen de klasse opgeslagen worden, gemakkelijk te wijzigen.

Erfenis

Als op een zekere dag blijkt dat de gegevensopslag binnen een klasse uitgebreid moet worden, dan zal men in plaats van de klasse te wijzigen een afleiding maken van deze klasse. De nieuwe klassen erft alle gegevens en methoden van de klasse waarvan geërfd wordt. Aan de nieuwe klassen kunnen andere gegevens en methoden toegevoegd worden of kan een overgeërfde methode vervangen worden door een nieuwe versie. Dit is het principe van de erfenis.

Late of dynamische verbinding

In de klassieke programmeertalen zorgt de linker ervoor dat de werkelijke adressen van de functies ingevuld wordt bij elke CALL instructie. Dit is het principe van de vroege of statische verbinding. In object georiënteerde talen kan deze verbinding uitgesteld worden tot het uitvoeren van het programma. Vlak voor de uitvoering van de CALL wordt het adres van de functies opgezocht. Dit mechanisme geeft een verhoogde flexibiliteit en is de kern bij het object georiënteerd programmeren.

In C++ kaj je kiezen tussen vroege en late verbinding. In Java heb je die keuze niet meer; hier is het altijd de late verbinding.

Polymorfie

Deze term wordt gebruikt om aan te geven dat de oproep van een functie of methode soms tot een ander gedrag leidt. Het gedrag van de opgeroepen methode is afhankelijk van de gegevens. Deze speciale afhankelijkheid tussen gegevens en methode maakt het mogelijk om delen van programma’s te ontwerpen die een zekere vorm van algemeenheid bewaren. Hiermee verhoogt de kans dat deze programmadelen later herbruikt worden. Het mechanisme om polymorfie toe te laten is de late verbinding. Een voorbeeld uit het dagelijkse leven maakt polymorfie duidelijk: als we iemand de opdracht geven om een voertuig dan zal de man (of vrouw) in kwestie zijn gedrag aanpassen naargelang hij (of zij) een gewone auto moet wassen of een autobus.

Eén van de object georiënteerde talen is C++. Andere talen zijn Eiffel, Smalltalk, ObjectC, Simula en Turbo Pascal. Verder zijn er object georiënteerde uitbreidingen op reeds bestaande talen; dit geldt onder andere voor Lisp en Prolog. Aan dit lijstje wordt uiteraard de jongste tal Java, Python en Ruby toegevoegd.

Geschiedenis van C++

De taal C++ is ontworpen door Bjarne Stroustrup en is volledig gebaseerd op C. C op zijn beurt is afgeleid van zijn voorganger BCPL. De commentaarstarter // die in C++ ingevoerd is, bestond al in BCPL. Heel wat concepten van C++ (de naam C++ werd in de zomer van 1983 uitgevonden) zijn afgeleid van andere programmeertalen. Het klasseconcept met afleidingen en virtuele functies werd van Simula67 overgenomen. De mogelijkheid om bewerkingstekens een andere betekenis te geven en om overal in het programma declaraties van variabelen te schrijven is van Algol68 overgenomen. Zo ook komen de ideeën voor templates en exceptions uit Ada, Clu en ML. C++ werd door de auteur ontworpen voor eigen gebruik. De eerste versie was geen eigen compiler maar wel een C naar C++ omzetter. Dit programma heette cfront. Door de stijgende populariteit van C++ bleek toch een standaardisatie noodzakelijk. Deze stap in gang gezet binnen ANSI als de X3J16 commissie in 1989. De ANSI C++ standaardisatie zal vermoedelijk ook deel uitmaken van een internationale ISO standaard. Tijdens de periode van het ontstaan tot de standaardisatie is C++ geëvolueerd. Een aantal wijzigingen zijn er gekomen na het opdoen van praktische ervaring.

Oorspronkelijk werd C ontworpen om assembler te vervangen. Ook C++ kan in deze optiek gebruikt verder gebruikt worden. Met andere woorden C++ staat dicht bij de machine. Van de andere kant is C++ bedoeld om de complexiteit van een probleem in kaart te brengen. Door een klassehiërarchie op te bouwen is het mogelijk om een klare kijk te behouden op de gegevens en bijbehorende acties in een probleem. Hierdoor is het mogelijk dat één persoon met C++ programma’s van meer dan 25.000 regels kan ontwerpen en onderhouden. In C zou dit veel moeilijker zijn.

De evolutie van C++ loopt nog altijd verder. In 2011 is de laatste standaard opgesteld. Deze staat bekend onder de naam C++11. De vernieuwingen die hierin voorgesteld worden, zijn niet opgenomen in deze cursustekst.

Waarom object georiënteerd programmeren?

Veel C programmeurs blijven liever bij C en zijn niet geneigd om C++ te leren. Hun argumenten zijn dikwijls als volgt:

  • C++ is een moeilijke taal. De concepten zijn veel abstracter en de mechanismen in de taal zijn op het eerste zicht niet erg duidelijk. Waarom een moeilijke taal zoals C++ gebruiken als het met eens simpele taal zoals C ook gaat.

  • Bij C++ is het veel moeilijker om de programmauitvoering te volgen. Om de haverklap worden constructors uitgevoerd. Door het mechanisme van de late binding weet je niet direct waar je terecht komt bij de stap voor stap uitvoering.

  • Sommigen willen toch de principes van het object georiënteerd programmeren volgen, maar doen dit liever in C. Deze werkwijze is nogal omslachtig.

  • Bij C++ is de programmeur verplicht om tijdens de analyse zijn denkproces aan te passen aan het object georiënteerd ontwerpen. Dit onvermijdelijke denkwerk gebeurt reeds voor het programmeren en kan niet omzeild worden. In deze analysefase ben je verplicht om diep te denken over gegevens en acties en hun onderling relaties. Het resultaat van dit denkwerk is het objectenmodel van het probleem. Dit model dient als leidraad tijdens het programmeerwerk. Tijdens de testfase zal dan blijken dat er minder denk- en programmeerfouten zijn en bijgevolg het programma sneller afwerkt zal zijn.

Programmeerparadigma’s

Tijdens de geschiedenis van de programmeertalen zijn er heel wat programmeerprincipes opgedoken. Eén van de eerste principes is het programmeren met procedures3 Hierbij is het de bedoeling om alle acties die nodig zijn om een bepaalde taak uit te voeren worden in een routine, procedure of functie ondergebracht. De analyse die deze groepering vooraf gaat is vooral gericht naar uitvoerbare acties. Er ontstaat een hiërarchie van procedures.

Voor het controleren van de volgorde waarin acties worden uitgevoerd werd eerst de goto gebruikt. Tegenwoordig wordt deze constructie niet meer gebruikt (tenzij in assembler). De if-then-else is er in de plaats gekomen. Dit principe heet gestructureerd programmeren. Deze stijl wordt alle moderne programmeertaal mogelijk gemaakt.

Na een aantal jaren is gebleken dat een goede data-analyse belangrijk is. Een aantal procedures die betrekking hebben op dezelfde gegevens worden in een module gegroepeerd. De data binnen de module mag niet rechtstreeks toegankelijk zijn, maar gebeurt door middel van procedures. Dit is het modulair programmeren. Hierbij wordt het principe van de data-hiding toegepast. De wijze waarop de data is opgeslagen is niet gekend door de gebruiker van de module. Het modulair programmeren laat ook toe grotere programma’s te ontwerpen. In C is een module een apart .c bestand waarin de gegevens in globale variabelen worden bijgehouden. Om de data-hiding mogelijk te maken moet het woord static aan elke declaratie voorafgaan. Deze methode heeft een nadeel: elke module stelt slechts één gegevensgroep voor, bijvoorbeeld een tabel met strings. Als we meerdere gegevensgroepen willen maken moeten we struct gebruiken. Hiermee verliezen we de data-hiding. De taal C is daarom niet geschikt om op een succesvolle manier gegevens voor de gebruiker te verbergen. Door dit nadeel is het beter om C te verlaten en C++ te gebruiken. C++ kent het principe van de klasse. Hiermee is een perfecte data-hiding mogelijk.

Bij het modulair programmeren ontstaan modules. Bij nieuwe projecten is het dikwijls lastig om bestaande modules opnieuw te gebruiken. Dikwijls zijn er kleine aanpassingen nodig of wordt toch maar de hele module herschreven. Het herbruiken van reeds bestaande programmatuur is niet gemakkelijk bij het modulair programmeren. Bij het object georiënteerd programmeren is het mogelijk om afleidingen te maken van bestaande klassen. In de nieuwe klassen kunnen dan de kleine wijzigingen gebeuren. Een andere ontwerptechniek is het ontwerpen van een klasse waarin een aantal functies alleen maar als prototype voorkomen. In de afleiding van deze klassen wordt de implementatie van deze functies ingevuld. Door deze techniek is het mogelijk om een klasse te maken die nog algemeenheid als eigenschap hebben. Een algemene (of in OO termen: abstracte) klasse is niet bedoeld om hiermee variabelen te declareren. Van een abstracte klasse worden afleidingen gemaakt en deze afleidingen worden gebruikt om variabelen te declareren. Door de techniek van de afleiding ontstaan er hiërarchieën van klassen. Die maken het mogelijk om de functionaliteit en gegevens in een groot programma goed in kaart te brengen. C programmeurs die klagen over de moeilijkheidsgraad van C++, klagen over het feit dat ze een OO analyse van gegevens/acties moeten uitvoeren; iets dat zij tot nu toe nooit deden. Daarom is C++ leren niet zomaar weer een nieuwe taal leren, het is een andere manier van denken.

Als besluit op deze inleiding geven we de tegenargumenten die voor C++ pleiten:

  • De concepten van een OO taal zijn abstracter omdat het denken tijdens een OO analyse abstracter is. Deze hogere graad van denken loont wel de moeite. Hiermee worden vroegtijdig de elementen (gegevens en bijbehorende acties) van het probleem gestructureerd en worden tegenstrijdigheden die later zouden kunnen opduiken bij onderhoud of wijziging vermeden.

  • Het is inderdaad veel moeilijker om een C++ programma stap voor stap te volgen, want we worden geconfronteerd met alle details van alle klassen die doorlopen worden. Het is beter om niet stap voor stap te debuggen, maar wel op klasseniveau. Test elke klasse één voor één. Door de data-hiding is de relatie tussen de verschillende klassen minimaal. Elke klasse kan daarom als een op zich bestaand domein beschouwd worden.

  • Het is beter om OO te programmeren met een taal die dit principe ondersteund. Een goede programmeur kan wel in zekere mate OO principes in C toepassen, maar dit gaat gepaard met veel pointers naar functies, void \* pointers en cast bewerkingen. Dit zijn allemaal constructies die gemakkelijk fouten introduceren.

  • Niet object georiënteerde verschillen tussen C en C++

Normaal gezien is het mogelijk om een C programma te compileren met behulp van een C++ compiler. Er zijn wel enkele kleine verschillen tussen beide compilers voor wat betreft de C syntax.

C constructies met een andere betekenis in C++

We geven hier een overzicht van de belangrijkste taal elementen die anders zijn in C++ dan in C.

  • In C++ is er geen beperking op de lengte van namen.

  • De C++ compiler moet eerst een prototype van een functie gezien hebben voor dat deze functie opgeroepen kan worden.

  • Een karakterconstante is in C++ van het type char, in C is dit int.

Referentietype

Dit is een nieuw type in C++. Het referentietype maakt het mogelijk om een variabele te declareren die als synomiem van een andere variabele dienst doet.

int a;
int &x = a;
int &y; // Fout: initialisatie ontbreekt

x = 5;   // a is nu 5

De variabele is een gewoon geheel getal. De variabele x is een referentievariabele die verwijst naar de variabele a. Voor x is er geen opslagruimte voor een int. Als x gewijzigd wordt, wordt de inhoud van a+gewijzigd. Een referentievariabele moet geïnitialiseerd worden bij de declaratie. Daarom is de declaratie van +y fout. Het is niet mogelijk om na de declaratie de referentievariabele naar een andere variabele te laten verwijzen.

De referentievariabele wordt dikwijls als formele parameter gebruikt. Hier is een klassiek voorbeeld:

void swap(int &x, int &y)
{
   int h;

   h = x;
   x = y;
   y = h;
}

int main()
{
   int a = 5;
   int b = 6;

   swap (a, b);   // verwissel a en b

   return 0;
}

Bij het gebruik als parameter gedraagt een referentietype zich als een VAR parameter in Pascal. Het voordeel is het feit dat in de functie swap de variabelen x en y er als een gewone variabele uitzien en niet als een pointervariabele.

De klasse in C++

Een klasse definiëren

C++ kent niet alleen het type struct maar ook het type class. Beiden kunnen gebruikt worden om gegevens in te kapselen. Dit doen we om gegevens, die sterk verbonden zijn, samen te brengen onder een noemer. Het sleutelwoord class is nieuw in C++ en laat de bescherming van de gegevens binnen een klasse toe.

Het eerste voorbeeld heeft wat te maken met grafische weergave van gegevens. In een grafische omgeving moeten we de coördinaten van een punt op het scherm bijhouden. Dit doen we door de coördinaten van een plaats op te slaan in een klasse:

class Punt
{
   int x;
   int y;
};

De klasse Punt bevat dus de velden x en y. Deze vorm van groeperen kennen we al van C. Met de klasse Punt kan een variabele gedeclareerd worden en we kunnen trachten om de velden x en y te bereiken.

Punt p1;

De variabele p1 is van het type Punt en is in staat om 2 coördinaten te onthouden. Dit is niet meteen zichtbaar aan de variabele. Dit is het principe van de inkapseling. We gebruiken de term object om een variabele van een zekere klasse aan te duiden. Als we het in de toekomst over objecten hebben, dan bedoelen we hiermee variabelen of stukken dynamisch geheugen waarin zich informatie van een zekere klasse bevindt.

Toegang tot leden

Als we de leden van het object p1 willen bereiken, dan zouden we het volgende kunnen uitproberen:

int main()
{
   Punt p1;

   p1.x = 50;
   p1.y = 70;

   return 0;
}

Helaas geeft dit programma twee compilatiefouten. De twee velden x en y zijn niet toegankelijk van buiten het object. Als we binnen de definitie van de klassen niet een van de woorden private, protected of public zijn alle leden privaat, private dus. We hadden evengoed dit kunnen schrijven:

class Punt
{
private:
   int x;
   int y;
};

Op deze wijze wordt duidelijk weergegeven dat de leden x en y niet publiek toegankelijk zijn.

Een constructor bijvoegen

Omdat de leden van de klasse Punt niet publiek toegankelijk zijn is er een probleem om bijvoorbeeld de variabele p1 te initialiseren of om de waarden x en y van een Punt object te weten te komen. Daarom zijn we verplicht tot de leden van een klasse via lidfuncties te organiseren. Een lidfunctie is een functie die deel uitmaakt van een klasse. Dikwijls wordt ook de term methode voor dit soort functies gebruikt. Er is een speciale vorm van een lidfunctie die enkel voor de initialisatie van een object wordt gebruikt. Deze vorm wordt constructor genoemd. Een constructor krijgt als naam de naam van de klasse.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy);
};

Binnen de klassebeschrijving noteren we een prototype van een functie. Omdat we voor de functienaam de naam van de klasse kiezen, is deze functie een constructor. We voorzien de constructor van twee formele parameters ix en iy. Dit betekent dat we aan de constructor twee getallen kunnen meegeven die dienen voor de initialisatie van het object. Bij een constructor mogen nooit een return type noteren; parameters mogen wel. In dit voorbeeld zijn er twee parameters.

Wat deze constructor moet doen, is nog niet vastgelegd. Binnen de klasse staat alleen maar een prototype. Nu zijn er twee manieren om de implementatie vast te leggen.

Een constructor implementatie buiten de klasse

We noteren de implementatie van de constructor buiten de accolades van de klassebeschrijving. Om aan te geven dat het hier gaat over een lidfunctie van de klasse Punt moeten we nog eens de klassenaam aan de functienaam laten voorafgaan. Tussen de klassenaam en de functienaam schrijven we twee dubbele punten.

Punt::Punt(int ix, int iy)
{
   x = ix;
   y = iy;
}

Omdat het hier om een constructor gaat, schrijven we tweemaal Punt . Eenmaal als klassenaam en als functienaam. Binnen de acties van een lidfunctie zijn de dataleden van een klasse vrij toegankelijk. De namen x en y binnen de constructor zijn de twee dataleden van een Punt object. Aan elk van de dataleden wordt een startwaarde toegekend.

Voor het toekennen van een startwaarde is er bij constructors aan andere schrijfwijze mogelijk. Hierbij wordt na een dubbele punt de lijst van te initialiseren datavelden geschreven met telkens de startwaarde erbij:

Punt::Punt(int ix, int iy) : x(ix), y(iy)
{
}

Deze schrijfwijze mag alleen maar bij constructors toegepast worden.

Een constructor implementatie binnen de klasse

We kunnen de implementatie van de constructor ook binnen de klasse noteren. Het voordeel is dat we minder schrijfwerk hebben. Het nadeel is dat als we de implementatie van de constructor wijzigen, dan moet er binnen de klassedefinitie gewijzigd worden. Vermits we alle klassedefinities in headerbestanden onderbrengen, veroorzaakt dit de hercompilatie van alle .cpp bestanden die van deze klasse gebruik maken.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) : x(ix), y(iy)
   {
   }
};

Voor een constructor maakt het geen verschil uit welke twee opties gekozen wordt. Voor lidfuncties, die dus geen constructor zijn, is er wel een verschil tussen implementatie binnen of buiten de klasse. Dit onderscheid bespreken we later.

Objecten declareren

Zoals we met een eenvoudig type een variabele kunnen declareren, kunnen we met een klasse een object declareren.

#include <iostream.h>

Punt pg(23, 34);

int main()
{
   Punt p1(30, 40);
   Punt p2 = Punt(44, 55);

   cout << "main\n";

   return 0;
}

Omdat er een constructor is die twee gehele getallen verwacht als parameter, moeten we bij de initialisatie tussen ronde haken twee getallen voorzien. Het is hier dat de constructor in actie treedt. Een constructor kunnen we nooit zelf starten. Een constructor wordt uitgevoerd voor een object, zodra dit object tot leven komt. Voor pg is dit zelfs voor de start van main(). Dit betekent dat voor het starten van main() de dataleden x en y van het object pg met 23+en 34 worden gevuld. Daarna start main()+ en daarna wordt achtereenvolgens de constructor voor p1 en p2 opgeroepen. Dan pas start de uitvoering van printf(). Voor p1 en p2 is telkens een andere schrijfwijze van de initialisering toegepast.

Meerdere constructors

Het is mogelijk om meerdere constructors te voorzien binnen een klasse. Het aantal en het type parameters van alle constructors moeten verschillend zijn. Met andere woorden: elke constructor heeft aan ander prototype. In de klasse Punt hadden we al een constructor met als parameters de coördinaten van een punt. We voegen nu een constructor bij die geen parameters heeft.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) : x(ix), y(iy)
   {
   }
   Punt() : x(0), y(0)
   {
   }
};

De implementatie van deze nieuwe constructor plaatsen we voor het gemak in de klasse zelf. Deze constructor zorgt ervoor de dataleden x en y automatisch 0 worden als we de expliciete initialisatie weglaten bij de declaratie van een object. In de volgende versie van main zien we twee objecten van de klasse Punt; één met en één zonder initialisatie.

int main()
{
   Punt p1(30, 40);
   Punt p2;

   return 0;
}

Het object p1 wordt met de eerste constructor geïnitialiseerd en p2 wordt geïnitialiseerd met de tweede constructor. Dit betekent dat de p2.x en p2.y allebei nul worden. De compiler beslist aan de hand van het aantal en het type actuele parameters welke constructor bij de declaratie opgeroepen wordt. Dit is meteen ook de reden waarom er geen twee constructors met hetzelfde prototype mogen zijn.

Een object initialiseren door een ander object

Het is mogelijk om bij de declaratie een object te initialiseren met een ander object.

int main()
{
   Punt p1(30, 40);
   Punt p2 = p1;

   return 0;
}

We zouden kunnen verwachten dat de compiler zou eisen dat er een constructor met prototype

Punt(const Punt &x);

voorkomt in de klasse. We hoeven deze constructor echter niet te definiëren omdat dit automatisch gebeurt. Elke klasse krijgt automatisch een constructor die dient voor de initialisatie met een object van dezelfde klasse. Deze constructor wordt copy constructor genoemd; hij kopieert één voor één alle dataleden van het ene naar het andere object. Dit gedrag is hetzelfde als bij het kopiëren van structuren in C. Bij de klassen Punt is het lidsgewijs kopiëren het juiste gedrag. In het bovenstaande voorbeeld worden de datavelden x en y van p1 naar p2 gekopieerd.

Objecten kopiëren

Zo kunnen we ook objecten kopiëren met een toekenning. Deze operator kopieert zoals bij de copy constructor alle dataleden.

int main()
{
   Punt p1(30, 40);
   Punt p2;

   p2 = p1;

   return 0;
}

Voor deze bewerking heeft de compiler automatisch een operator voor de = bewerking binnen de klasse bijgevoegd. Het bijvoegen van bewerkingen bekijken we later nog wel.

Lidfuncties in een klasse bijvoegen

De twee dataleden van de klasse Punt zijn privaat. Dit betekent dat we niet rechtstreeks toegang krijgen tot de dataleden. Daarom voegen we een klassefunctie bij die de coördinaten van een Punt op het scherm drukt.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) : x(ix), y(iy)
   {
   }
   Punt() x(0), y(0)
   {
   }
   void druk();
};

void Punt::druk()
{
   cout << "<" << x <<
"," << y << ">";
}

De klassefunctie druk kan zonder meer toegang krijgen tot de dataleden van het object in kwestie. De functie druk() kan alleen maar gestart worden met een concreet object:

void main()
{
   Punt p1(67,78);
   Punt p2(34,98);

   p1.druk();
   p2.druk();
}

De notatie van de oproep van een klassefunctie is dezelfde als in C voor de toegang tot een veld van een structuur. De naam van het object wordt gevolgd door een punt en de naam van de klassefunctie.

Inline uitvoering van een lidfunctie

Het is mogelijk om de tijd die nodig is voor de oproep en de terugkeer van een functie te elimineren. Dit is nodig als een klassefunctie zeer kort is.

We gaan twee lidfuncties aan de klasse Punt bijvoegen om de waarden van dataleden x en y terug te geven. We tonen twee versies van de implementatie van de lidfuncties.

Een lidfunctie implementatie buiten de klasse

Binnen de klasse Punt worden twee prototypes voor haalx en haaly bijgevoegd.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) : x(ix), y(iy)
   {
   }
   Punt() : x(0), y(0)
   {
   }
   void druk();
   int haalx();
   int haaly();
};

De implementaties van de twee functies ziet er zo uit:

int Punt::haalx()
{
   return ( y );
}

int Punt::haaly()
{
   return ( y );
}

We kunnen deze twee functies expliciet gebruiken om de waarden van de coördinaten op te halen zonder grens van de inkapseling te overtreden. Dit wordt als volgt gedaan:

int main()
{
   Punt pp(67, 89);

   cout << "<" << pp.haalx()
        << "," << pp.haaly() <<">\n";

   return 0;
}

Bij deze implementatie is er sprake van een echte subroutine. Er is bijgevolg een oproep en een terugkeer. Het nadeel van deze twee functies is dat ze zeer kort zijn; dit betekent dat er meer tijd besteed wordt aan de oproep en de terugkeer (jsr en ret instructies) dan aan de uitvoering van de acties van de functies. Daarom kan gekozen worden voor de inline uitvoering van dit soort van korte functies.

Een lidfunctie implementatie binnen de klasse

We plaatsen de opdrachtregel van de klassefuncties binnen de definitie van de klasse.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) { x = ix; y = iy; }
   Punt() { x = 0; y = 0; }
   void druk();
   int haalx()
   {
      return( x );
   }
   int haaly()
   {
      return( y );
   }
};

Door deze notatievorm wordt de functie inline uitgevoerd. Elke oproep in C++ notatie wordt vervangen door de instructies van de opdrachtregel. Het is evident dat deze oplossing alleen efficiënt is bij zeer korte klassefuncties. Het gevolg is wel dat het programma in zijn totale lengte (in machineinstructies uitgedrukt) langer wordt omdat het principe van de subroutine niet wordt toegepast.

Er is een alternatieve implementatie van een inline klassefunctie mogelijk. De implementatie wordt dan toch buiten de klassedefinitie geschreven, maar het prototype bij de implementatie van de klassefunctie wordt voorafgegaan door het woord inline.

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix, int iy) : x(ix), y(iy)
   {
   }
   Punt() : x(0), y(0)
   {
   }
   void druk();
   int haalx();
   int haaly();
};

inline int Punt::haalx()
{
   return ( y );
}

inline int Punt::haaly()
{
   return ( y );
}

Bewerkingen in een klasse

C++ kent de mogelijkheid om een nieuwe betekenis te geven aan een bewerkingsteken afhankelijk van de klasse waarop de bewerking betrekking heeft. Zo kan men een andere betekenis geven aan de optelling bij breuken en bij complexe getallen. We geven een voorbeeld dat handelt over breuken.

Bewerkingen als functies in een klasse

We ontwerpen de klasse om een breuk op te slaan. De klasse krijgt twee dataleden: een voor de teller en een voor de noemer. Beide worden opgeslagen als een geheel getal. De constructor is voorzien van verstekwaarden; zo krijgt een breuk de waarde 0/1 wanneer de initialisatie ontbreekt.

// breuk.h
class Breuk
{
public:
   Breuk(int t=0, int n=1) : teller(t), noemer(n)
   {
   }
   Breuk &operator++();
   Breuk operator+(Breuk b);

private:
   int teller;
   int noemer;
};

Wanneer we een bewerkingsteken willen definiëren dan geven we de bijbehorende functie een speciale naam. We combineren het sleutelwoord operator met het bewerkingsteken in kwestie. Voor de ++ bewerking wordt dit:

Breuk &operator++();

Deze functie geeft een Breuk als referentietype terug. Dit is nodig omdat de bewerking in uitdrukkingen kan voorkomen. Het terugkeer type is het referentietype omdat de bewerking zowel links of rechts van een toekenning kan voorkomen.

Breuk b1;
Breuk b2;

b1++ = b2++;

Op dezelfde wijze wordt de functienaam voor de optelling samengesteld:

Breuk operator+(Breuk b);

In dit geval is er een parameter: dit is de breuk die bij andere breuk wordt opgeteld. Het resultaat van de bewerking is het Breuk type. De bewerking kan zo gebruikt worden:

b3 = b1+b2;

We zouden de bewerking ook als een functie kunnen starten

b3 = b1.operator+(b2);

Deze schrijfwijze is alleen maar nuttig om te zien hoe de bewerking gestart wordt.

De implementatie van beide bewerkingen worden als gewone klassefuncties geschreven. Voor de ++ bewerking wordt de noemer éénmaal bij de teller opgeteld. Met

return *this;

wordt een referentie naar het huidige object als resultaat teruggegeven.

// breuk.cpp
#include <iostream.h>
#include "breuk.h"

Breuk &Breuk::operator++()
{
   teller += noemer;
   return *this;
}

Breuk Breuk::operator+(Breuk b)
{
   Breuk nb;

   nb.teller = teller * b.noemer + noemer * b.teller;
   nb.noemer = noemer * b.noemer;

   return nb;
}

In de + bewerking wordt een nieuwe Breuk gemaakt. De som van de twee op te tellen breuken wordt in deze nieuwe variabele geplaatst. Met return, in dit geval de som, gaat het resultaat terug naar de oproeper.

Vriendfuncties van een klasse

In sommige gevallen is het nodig om een bewerking als een functie buiten de klasse te definiëren. Dit is het geval bij de functie die de uitvoer van een Breuk verzorgt. Omdat deze functie toegang moet krijgen tot de private leden van de klasse Breuk maken we de functie een vriend van de klasse Breuk.

friend ostream &operator<<(ostream &os, Breuk b);

Het bovenstaande prototype wordt in de klasse bijgevoegd. Met het woord friend gevolgd door een prototype wordt aangegeven dat een niet-klasse functie toegang krijgt tot alle private leden.

Voor de uitvoer wordt het naar links schuif teken << gebruikt. We schrijven dit teken na het woord operator. De parameters van de uitvoerbewerking zijn het uitvoerkanaal en de Breuk die getoond moet worden. ostream is het type van het uitvoerkanaal. cout behoort tot dit type. Als terugkeertype zien we een referentie naar ostream. We geven het uitvoerkanaal terug als referentie. Dit is nodig omdat de uitvoerbewerking samen met het uitvoerkanaal en de breuk opnieuw als een ostream aanzien wordt. Hierdoor kan men meerdere uitvoerbewerkingen na elkaar schrijven.

(cout << b1) << b2;

In de bovenstaande uitvoer wordt cout << b1 opnieuw als een uitvoerkanaal aanzien. Naar dit kanaal wordt de uitvoer van b2 gestuurd.

De implementatie van de uitvoerbewerking gaat na of de noemer 1 is. Indien ja, wordt alleen de teller getoond. Anders worden teller en noemer gescheiden met een deelstreep getoond.

ostream & operator<<(ostream &os, Breuk b)
{
   if (b.noemer == 1)
   {
      cout << b.teller;
   }
   else
   {
      cout << b.teller << "/" << b.noemer;
   }
   return os;
}

De uitvoerbewerking geeft als resultaat het doorgegeven uitvoerkanaal terug.

We tonen nog een voorbeeld van een hoofdprogramma waarin de klasse Breuk gebruikt wordt.

#include <iostream.h>
#include "breuk.h"

void main()
{
   Breuk b;

   cout << b << endl;
   b++;
   cout << b << endl;

   Breuk c(1,4);
   Breuk d(1,2);
   Breuk e;

   e = c + d;
   cout << e << endl;
}

Dynamische objecten

Zoals C kent C++ ook het principe van het dynamisch reserveren van geheugen voor gegevensopslag. In C++ zijn voor dit doel de operatoren new en delete ingevoerd.

De new bewerking

Met de new bewerking kan geheugen op dynamische wijze gereserveerd worden. In tegenstelling tot C waar malloc() een ingebouwde functie is, is in C++ new een ingebouwde bewerking. Deze bewerking wordt toegepast op de typeinformatie.

new bij een niet-klasse

De eerste vorm waarin new gebruikt kan worden is de toepassing op een enkelvoudig type. Als we bijvoorbeeld geheugen voor één int willen reserveren dan kan dit zo:

int *pi = new int;

*pi = 5;

De toepassing van de bewerking new op het type levert het adres op van een blokje geheugen. In dit geheugen is plaats voor één getal van het type int. In tegenstelling tot C is er geen cast-bewerking nodig.

new bij een klasse

Dikwijls wordt de new bewerking gebruikt om geheugen te reserveren voor objecten. We gebruiken dan als type-informatie de klassenaam. In het volgende voorbeeld wordt een object van de klasse Punt gereserveerd.

Punt *pu = new Punt;

ofwel, in een andere schrijfwijze:

Punt *pu;

pu = new Punt;

Bij het uitvoeren van de new bewerking gebeuren er eigenlijk twee stappen:

  • new reserveert geheugen als nodig is voor de klasse.

  • Indien de reservatie gelukt is, wordt nog de constructor uitgevoerd.

In het voorgaande voorbeeld wordt de constructor zonder parameter uitgevoerd. Hierdoor worden de dataleden allebei nul.

Bij de klasse Punt is het mogelijk om bij het dynamisch reserveren van een object meteen ook gegevens voor de initialisatie mee te geven. We maken dan gebruik van de constructor met twee parameters.

void fu()
{
   Punt *pa;
   Punt *pb;

   pa = new Punt(23,34);
   pb = new Punt(45,56);

   pa->druk();
   pb->druk();
}

Als in het bovenstaande voorbeeld de functie fu() gestart wordt, worden er twee objecten in dynamisch geheugen gereserveerd. Omdat bij new na de klassenaam twee getallen voorkomen, wordt de constructor met twee parameters gestart. Na het reserveren van twee objecten wordt met de methode druk() de coördinaten in pa en pb op het scherm geschreven. Omdat pa en pb pointers zijn, moet een pijl gebruikt worden om methoden te bereiken.

De delete bewerking

Als in het voorgaande voorbeeld het einde van de functie bereikt wordt, houden de pointers pa en pb op te bestaan. Vermits ze allebei wijzen naar dynamisch gereserveerd geheugen, zou hierdoor een geheugenlek ontstaan. Daarom moet vóór het einde van de functie het geheugen vrijgegeven worden. Dit doen we met de delete bewerking.

void fu()
{
   Punt *pa;
   Punt *pb;

   pa = new Punt(23,34);
   pb = new Punt(45,56);

   pa->druk();
   pb->druk();

   delete pa;
   delete pb;
}

Na het woord delete schrijven we de naam van de pointervariabele die wijst naar het dynamisch geheugen. Voor elke new bewerking die in een programma voorkomt moet er een overeenkomstige delete bewerking zijn.

new en delete bij arrays

Bij het gebruik van de bewerking new bestaat de mogelijkheid om geheugen voor arrays te reserveren. We schrijven dan na new een arraytype. De waarde tussen de rechte haken mag wel een variabele zijn. Hierdoor kan de lengte van de array dynamisch bepaald zijn. In het volgende voorbeeld krijgt p het adres van een blok van 100 char’s.

char *p = new char[100]
delete [] p;

Bij het vrijgeven van het geheugen met delete moet aangegeven worden dat het gaat over een array. Daarom moeten voor de variabelenaam rechte haken geschreven worden. De vrijgave van het geheugen moet expliciet geschreven worden voordat een pointervariabele ophoudt te bestaan.

Het gebruik van new binnen een klasse

Het is mogelijk om de hoeveelheid geheugen die nodig is binnen de klasse ook dynamisch te reserveren. Op deze manier zijn er geen beperkingen op de lengte van de binnen een klasse opgeslagen gegevens.

In het volgende voorbeeld wordt een klasse Tekst gedemonstreerd. Deze klassen kan gebruikt worden om tekstobjecten te creëren. Om te vermijden dat er conflicten ontstaan als er lange teksten opgeslagen moeten worden, is de opslag van de string binnen een object dynamisch. De klasse Tekst ziet er als volgt uit:

class Tekst
{
private:

   char *ptekst;

public:
   Tekst(const char *pv = "");
   ~Tekst();
   char *str() const
   {
      return( ptekst );
   }
   Tekst &operator=(const Tekst &t);
   Tekst &operator+=(const Tekst &t);
   Tekst operator+(const Tekst &t);
};

Voor de opslag van de string binnen het object is er het private datalid ptekst. De constructor

Tekst(const char *pv = "");

wordt opgeroepen als een +Tekst+ object met een char string initialiseren. Indien de parameter bij de oproep van de constructor ontbreekt, dan wordt een lege string als verstekwaarde gebruikt.

De implementatie van de constructor is als volgt:

Tekst::Tekst(const char *pv)
{
   ptekst = new char [strlen(pv) + 1];
   strcpy(ptekst, pv);
}

Met new worden zoveel bytes gereserveerd als de string lang is. Er is ook een extra byte voor de nulwaarde op het einde. Het resultaat van strlen() is immers de lengte van de string zonder de eindnul meegerekend. De originele string wordt in het gereserveerde geheugen gekopieerd. De kopieerbewerking is nodig om ervoor te zorgen dat object een eigen string in eigendom heeft. Indien we alleen het adres van de string zouden kopiëren, dan ontstaat er een situatie waarin een object verwijst naar geheugen die niet door het object wordt beheerd. Dit zou gevaarlijke situatie zijn.

Een destructor bijvoegen

Omdat er in de constructor dynamisch geheugen wordt gereserveerd, is het nodig dat in de klasse ook een destructor bestaat. Het prototype wordt met een tilde geschreven:

~Tekst();

Een destructor heeft geen parameters en geen terugkeertype. Een destructor kan wel virtueel zijn (een constructor daarentegen niet). Ook in de implementatie komt de tilde voor.

Tekst::~Tekst()
{
   cout << "delete " << ptekst << "\n"; // alleen voor test
   delete [] ptekst;
}

In deze destructor wordt met delete het geheugen van de string vrijgegeven. De uitvoerbewerking staat er alleen maar om te kunnen zien wanneer de destructor uitgevoerd wordt en is daarom niet noodzakelijk.

De klasse Tekst gebruiken

Het gebruik van de klasse Tekst is als volgt:

#include <iostream.h>
#include "tekst.h"

int main()
{
   Tekst t("hallo");

   cout << t.str() << "\n";

   return 0;
}

De definitie van de klasse plaatsen we best in een headerbestand. Zo bevindt de definitie van Tekst zich in het bestand tekst.h. De tekst t wordt geïnitialiseerd met de string "hallo". Vlak voor het einde van main() wordt de destructor opgeroepen voor het object t. met de methode str() verkrijgen we het adres van de opgeslagen tekst. De klasse Tekst kunnen we gebruiken zonder dat we iets zien van de wijze waarop de implementatie binnen de klasse is gemaakt. Deze inkapseling is één van de principes van het object georiënteerd programmeren.

Bewerkingen in een klasse bijvoegen

In het voorgaande voorbeeld is de klasse Tekst eerder beperkt. Daarom voegen we een tweetal bewerkingen bij in de klasse. We zouden deze bewerkingen kunnen bijvoegen in de vorm van klassefuncties zoals bijvoorbeeld druk() in de klasse Punt. Een ander alternatief is het veranderen van de betekenis van bewerkingstekens binnen een klasse. Dit betekent dat een bewerkingsteken een nieuwe betekenis krijgt. Volgens dit principe gaan we het = teken, += teken en het + teken koppelen aan een klassefunctie binnen de klasse Tekst. Binnen de definitie van de klasse Tekst worden de prototypes voor deze twee bewerkingen bijgevoegd.

Tekst &operator=(const Tekst &t);
Tekst &operator+=(const Tekst &t);
Tekst operator+(const Tekst &t);

Het zijn twee klassefuncties met een speciale naam. We laten het woord operator volgen door het bewerkingsteken dat we een nieuwe betekenis willen geven. De klassefuncties operator=, operator+= en operator+ hebben één formele parameter. Via deze parameter wordt de rechter-operand van de bewerking doorgegeven. De linker-operand wordt doorgegeven via de impliciete pointer naar het object. Met de plus operator kunnen we dan schrijven:

Tekst t1("dag ");
Tekst t2("wereld");
Tekst t3("");

t3 = t1 + t2;

Deze optelling zou ook als volgt geschreven kunnen worden:

t3 = t1.operator+(t2);

Deze schrijfwijze is niet zo goed leesbaar, maar geeft wel duidelijk weer hoe de twee operands aan de optelling worden doorgegeven. De implementaties van de = en += bewerkingen zien er zo uit:

Tekst &Tekst::operator=(const Tekst &t)
{
   delete ptekst;// verwijder de oude tekst
   ptekst = new char [strlen(t.str() ) + 1]; // ruimte voor nieuwe tekst
   strcpy(ptekst, t.str() );// kopieer tekst
   return( *this );
}

Tekst &Tekst::operator+=(const Tekst &t)
{
   char *poud;

   poud = ptekst;// hou oude tekst opzij

// reserveer ruimte voor nieuwe tekst
   ptekst = new char [strlen(poud) + strlen(t.str() ) + 1];

   strcpy(ptekst, poud);// kopieer eerste tekst
   strcat(ptekst, t.str() ); // voeg tweede tekst erbij
   delete poud;// verwijder oude tekst
   return( *this );
}

Bij elk van de twee bewerkingen wordt opnieuw dynamisch geheugen gereserveerd omdat de lengte van de nieuwe tekst, die in een object opgeslagen wordt, groter kan zijn dan de oude tekst. Telkens wordt het betrokken object via return teruggegeven. Dit is nodig omdat het resultaat van de bewerking ook van het type Tekst is.

De + bewerking kan kort geschreven worden:

Tekst Tekst::operator+(const Tekst &t)
{
   Tekst nt;// nieuwe tekst

   nt = *this;// kopieer eerste tekst
   nt += t;// voeg tweede tekst bij
   return( nt );// geef nieuwe tekst terug als resultaat
}

We maken gebruik van een lokaal Tekst object nt. Met een toekenning en daarna += bewerking worden de twee bronteksten samengevoegd in een nieuwe tekst. Deze nieuwe tekst wordt als resultaat teruggegeven. In de + bewerking wordt gebruik gemaakt van de eerder ontworpen = en += bewerkingen. Het terugkeer type is in dit geval géén referentietype.

Bewerkingen in een klasse gebruiken

Het gebruik van de klasse Tekst is als volgt:

#include <iostream.h>
#include "tekst.h"

int main()
{
   Tekst t("hallo");
   Tekst t2;
   Tekst t3("o");

   cout << t.str() << "\n";
   t2 = t;
   t2 = t + t3;
   cout << t2.str() << "\n";

   return 0;
}

Merk op dat er een verschil is tussen de twee bewerkingen met het = teken in het volgende fragment:

{
   Tekst ta("1234");
   Tekst tb = ta;
   Tekst tc("");

   tc = ta;
}

Bij het eerste = teken wordt de copyconstructor gestart om tb te initialiseren; bij het tweede = teken wordt de klassefunctie operator=() gestart. Let op: de copyconstructor hebben we niet zelf bijgevoegd in de klasse Tekst. Daarom wordt de default copyconstructor uitgevoerd. Deze is evenwel niet geschikt voor gebruik van zodra binnen een klasse zelf dynamisch geheugen wordt bijgehouden. Dit is de reden waarom het bovenstaand fragment problemen kan geven zolang geen eigen versie van de copyconstructor binnen de klasse Tekst wordt bijgevoegd.

Objecten binnen objecten

In vele gevallen is het nuttig om een klasse te beschouwen als een enkelvoudig type. We gaan dan gemakkelijker klassen gebruiken om daarmee nieuwe klassen samen te stellen. De techniek die we nu voorstellen is het gebruik van een bestaande klasse als type vaar dataleden van een nieuwe klasse. Het voorbeeld, dat we geven, heeft te maken met lijnen. Als we de gegevens van een lijn willen bijhouden, dan moeten we het begin- en eindpunt van de lijn opslaan. Een lijn bestaat uit twee punten of anders gezegd: de klasse Lijn bevat twee dataleden van de klasse Punt. Dit principe wordt aggregatie genoemd. We demonstreren dit met een voorbeeld.

#include <iostream.h>
#include <math.h>
#include "punt.h"


class Lijn
{
private:
   Punt p1;
   Punt p2;

public:
   Lijn(int x1, int y1, int x2, int y2) : p1(x1,y1),
p2(x2, y2)
   {
   }
   double lengte();
};

double Lijn::lengte()
{
   double dx, dy;

   dx = p1.haalx() - p2.haalx();
   dy = p1.haaly() - p2.haaly();
   return( sqrt(dx*dx + dy*dy) );
}

int main()
{
   Lijn ln(1,2,4,6);

   cout << ln.lengte() << "\n";

   return 0;
}

We maken in het voorbeeld een klasse Lijn. Deze klasse bevat twee dataleden van het type Punt. Hiermee wordt het verband uitgedrukt dat een lijn twee punten verbindt. De Punt dataleden p1 en p2 zijn privaat. Dit betekent dat ze niet vrij toegankelijk zijn van buiten de klasse. De klasse Lijn kent één constructor. Deze constructor verwacht vier getallen als parameter. Dit zijn de twee coördinatenparen voor de begin- en eindpunten. Voor deze constructor is een speciale schrijfwijze toegepast. Als we de constructor als volgt zouden schrijven, dan zou de compiler een foutmelding geven:

Lijn::Lijn(int x1, int y1, int x2, int y2)
{
   p1.x = x1;
   p1.y = y1;
   p2.x = x2;
   p2.y = y2;
}

Wat is er nu fout aan deze schrijfwijze? De fout heeft te maken met de beveiliging van de private dataleden. De dataleden x en y van de twee Punt objecten zijn niet vrij toegankelijk. Vanuit de Lijn constructor is er alleen maar toegang tot de publieke klassefuncties van p1 en p2. De dataleden x en y zijn privaat binnen de klasse Punt en daarom niet toegankelijk. Wel is de constructor van Punt toegankelijk. Er is echter in C++ geen mogelijkheid om rechtstreeks een constructor te starten als een functieoproep. Daarom kent C++ een speciale schrijfwijze om de dataleden van een klasse te initialiseren met een constructor. Daarom wordt de Punt constructor als volgt geschreven:

Lijn(int x1, int y1, int x2, int y2) : p1(x1,y1),
p2(x2, y2)
{
}

Na de lijst van formele parameters volgt een dubbele punt. Hierna vermelden we de namen van de dataleden die binnen Lijn voorkomen. Dit zijn p1 en p2. Na elk datalid noteren we de naam van de actuele parameters tussen haken. Zo wordt p1 geïnitialiseerd met x1 en y1; p2 wordt geïnitialiseerd met x2 en y2. Vanzelfsprekend moet er binnen de klasse Punt een constructor bestaan die met deze parameters overeen komt.

Met de functie lengte kan de lengte van een object van de klasse Lijn berekend worden.

Klassen afleiden

Als we van plan zijn om een bepaalde klasse uit te breiden met nieuwe dataleden of klassefuncties, dan zouden we rechtstreeks in de klassedefinitie deze dataleden of klassefuncties kunnen bijvoegen. Deze strategie heeft echter nadelen, zeker als de klasse reeds een tijd in gebruik is. Het is veiliger om de klasse ongewijzigd te laten en een afleiding te maken van deze klasse. Dit betekent dat we een nieuwe klasse ontwerpen die alle eigenschappen van een bestaande klasse overerft. Deze strategie heeft twee voordelen:

  • de bestaande klasse hoeft niet gewijzigd te worden

  • de functionaliteit van een bestaande klasse wordt volledig overgenomen in de nieuwe klasse

  • de nieuwe klasse kan extra aangevuld worden met nieuwe dataleden en klassefuncties

Als we een klasse nodig hebben voor de voorstelling van een punt, waarbij ook nog in de klasse een naam opgeslagen wordt, dan is er een nieuwe klasse nodig. Als naam voor de nieuwe klasse kiezen we PuntmetNaam. De originele klasse Punt laten we ongewijzigd. We maken een afleiding van Punt en voegen er een naam aan toe.

De klasse definitie van +PuntmetNaam+ziet er als volgt uit:

#include <iostream.h>
#include "tekst.h"
#include "punt.h"

class PuntmetNaam : public Punt
{
private:
   Tekst naam;

public:
   PuntmetNaam(int ix, int iy, char *nm) : Punt(ix, iy), naam(nm)
   {
   }
   void druk();
};

Op dezelfde regel als de klassenaam schrijven we de naam van de klasse waarvan we willen afleiden. Het woord public geeft aan dat het gaat om een publieke afleiding. Dit betekent dat alle private dataleden en klassefuncties binnen Punt niet toegankelijk zijn vanuit de klassefuncties van PuntmetNaam. De protected leden van Punt zijn wel toegankelijk vanuit PuntmetNaam meer niet van buiten de klasse.

Binnen de klasse PuntmetNaam wordt een extra datalid bijgevoegd: Tekst naam. Hiermee kunnen we een naam opslaan. De constructor voor PuntmetNaam krijgt een extra parameter ten opzicht van die van Punt. De derde parameter is string voor de naam. Door deze nieuwe constructor wordt de oude (overgeërfde) constructor niet meer toegankelijk. Dit is het herdefiniëren van een overgeërfde klassefunctie. De constructor bevat na de parameterlijst een dubbele punt en daarna een lijst van te initialiseren entiteiten: Punt(ix, iy), naam(nm). Met de eerste initialiseren worden de coördinaten ix en iy naar de Punt constructor doorgegeven. Met de tweede wordt de naam geïnitialiseerd. We zien dus twee soorten initialisators. Met een klassenaam geven we aan met welke gegevens de superklasse wordt geïnitialiseerd. Met een naam van een datalid geven we de initialisatie aan van een in de klasse zelf voorkomend klasselid.

void PuntmetNaam::druk()
{
   cout << "<" << naam.str();
   Punt::druk();
   cout << ">";
}

Net zoals de constructor wordt ook de functie druk() opnieuw gedefinieerd. Ook hier is er een verwijzing naar een klassefunctie van de superklasse. Met Punt::druk() wordt een functie uit de superklasse opgeroepen. De naam van de functie wordt voorafgegaan door de klassenaam en twee dubbele punten. Als we dit zouden weglaten, dan ontstaat er ongewild recursie.

In het hoofdprogramma worden twee objecten gedeclareerd. Telkens wordt druk() uitgevoerd.

int main()
{
   Punt p1(12,23);
   PuntmetNaam p2(56, 67, "oorsprong");

   p1.druk();
   cout << "\n";
   p2.druk();
   cout << "\n";

   return 0;
}

Het verband tussen de twee klassen kan grafisch weergegeven worden. Deze diagrammatechniek stamt uit Universal Modelling Language(UML).

!<>

Deze tekening geeft weer dat de klasse PuntmetNaam afgeleid is van de klasse Punt. Elke rechthoek stelt een klasse voor. De naam binnen de rechthoek stelt de klassenaam voor. Eventueel kunnen de dataleden en klassefuncties elk met een aparte rechthoek bijgevoegd worden.

Het diagramma ziet er dan als volgt uit:

!<>

In deze vorm toont het diagramma duidelijk dat door erfenis de klasse PuntmetNaam niet alleen naam als datalid heeft maar ook x en y.

Virtuele klassefuncties

Door het mechanisme van de afleiding is het mogelijk om een bepaalde klasse als basisklasse te gebruiken. Van deze basisklasse worden verschillende afleidingen gemaakt. De afgeleide klassen erven allemaal het gedrag van de basisklasse. De basisklasse bevat het gemeenschappelijk gedrag van de verschillende basisklassen. Dikwijls zijn er in de basisklasse klassefuncties nodig waarvan het gedrag pas definitief in de afgeleide klassen wordt bepaald. Daarom is het nodig dat de taal C++ voorzien is van een mechanisme om de keuze van welke klassefunctie gestart wordt (die uit de basisklasse of die uit de afgeleide klasse) te verschuiven tot bij de uitvoering van het programma. Dit mechanisme heet in C++ virtuele functie. In andere talen worden ook wel de termen dynamische of late binding gebruikt.

Het concept virtuele functie is de kern van de taal C++ die het mogelijk maakt om delen van software te ontwerpen die algemeen is en onafhankelijk van alle later toe te voegen objecttypes.

Om dit concept duidelijk te maken starten we de uitleg van een voorbeeld. In dit voorbeeld maken we een algemene klasse die voor de opslag van een waarde wordt gebruikt. De virtuele functie maakt het mogelijk om specifiek gedrag in een afgeleide klasse te gebruiken vanuit een algemene klasse zonder de details te kennen van de afgeleide klassen en zonder afbreuk te doen aan de algemeenheid van de basisklasse. In het voorbeeld dat volgt willen we gewoon een waarde op het scherm drukken zonder te weten van welk specifiek getaltype de waarde is.

Een abstracte klasse maken

We maken een basisklasse die gaat dienen voor de opslag van een waarde. Als klassenaam kiezen we de naam Waarde. De eerste letter is een hoofdletter, bijgevolg is dit een klassenaam. Deze klasse moet dienen om een waarde van een nog niet gekend type op te slaan. We wensen nu nog niet vast te leggen welk type gebruik zal worden want dan zou de klasse Waarde niet algemeen bruikbaar zijn. De klasse Waarde zou moeten kunnen werken met elk mogelijk getaltype.

class Waarde
{
private:
    // geen datalid

public:
   // hier plaatsen we de vrij toegankelijke klassefuncties
};

De beslissing om geen datalid voor de waarde in de klasse Waarde te plaatsen is een goede beslissing. We kunnen immers het datalid voor de waarde in de afgeleide klassen plaatsen. De klasse Waarde is bedoeld als basisklasse. We zullen van deze klasse nooit objecten maken. Zo komen we meteen tot het begrip abstracte klasse. een abstracte klasse is niet bedoeld om er concrete objecten mee te maken maar wel om een algemeen gedrag te bepalen voor een reeks afgeleide klassen.

Een virtuele functie maken

Bij dit voorbeeld is het gewenste algemeen gedrag van de klasse Waarde de mogelijkheid om de opgeslagen waarde op het scherm te drukken. Daarom plaatsen we een klassefunctie druk() in het publieke gedeelte.

class Waarde   // abstracte klasse
{
public:
   virtual void druk() = 0;
};

De schrijfwijze van het prototype van druk() vertoont twee nieuwe elementen:

  • Voor void staat het woord virtual

  • na de sluitende ronde haak staat = 0

Met het woord virtual geven we aan dat druk() een virtuele functie is. De precieze werking wordt later duidelijk. Na druk() staat er = 0. Hiermee geven we aan dat we voor druk() nog geen implementatie voorzien. Deze implementatie moet ingevuld worden in de verschillende afleidingen van de basisklasse. Deze = 0 is niet nodig om de functie virtueel te maken maar wel om de klasse abstract te maken.

Van een abstracte klasse mogen we geen objecten maken. Wel is het mogelijk om een pointer of een referentie naar een abstracte klasse te maken.

Waarde w1;// FOUT
Waarde *pw1// GOED

Afleidingen maken

We maken twee afleidingen van de basisklasse. Een voor de opslag van een geheel getal en een voor de opslag van een reëel getal.

class IWaarde : public Waarde
{
private:
   int intwaarde;

public:
   IWaarde(int iw) : intwaarde(iw)
   {
   }
   virtual void druk();
};

class FWaarde : public Waarde
{
private:
   double floatwaarde;

public:
   FWaarde(double iw) : floatwaarde(iw)
   {
   }
   virtual void druk();
};

Elke van deze afleidingen krijgt een privaat datalid voor de opslag van de waarde. Het type van de waarde is telkens verschillend. In elke afgeleide klasse is er een constructor voorzien om het object te initialiseren. In elke afgeleide klasse wordt ook het prototype van druk() bijgevoegd. Dit betekent dat de functionaliteit van de functie druk() in de verschillende afgeleide klassen willen invullen. Het woord virtual wordt herhaald voor het prototype, dit is niet verplicht. Wel is het verplicht om bij het herdefiniëren van een virtuele functie in een afgeleide klasse dezelfde formele parameters te voorzien als in de basisklasse.

De twee druk() functies verschillen omdat de te drukken waarden van een ander type zijn:

void IWaarde::druk()
{
   cout << "geheel " << intwaarde;
}

void FWaarde::druk()
{
   cout << "reeel " << floatwaarde;
}

Objectcompatibiliteit

In programmeertalen zoals Pascal, C en C++ is er een strikte typecontrole door de compiler. De omzetting van het ene type naar het andere type is niet altijd toegelaten. Op enkele uitzonderingen na is het verboden om verschillende types te gebruiken in toekenningen. In C en C++ kan deze beperking natuurlijk omzeild worden door de geforceerde omzetting (cast), maar deze programmeertechniek is niet elegant en veroorzaakt veel sneller fouten op. In C++ wordt de cast-bewerking grotendeels overbodig door de mogelijkheid om in beperkte mate toch toekenningen te doen tussen verschillende types.

In C++ is het toegelaten om een toekenning te doen van pointers (dit geldt ook voor het referentietype) van een verschillende type. Er is wel een voorwaarde: de pointer aan de linkerzijde van de toekenning moet van een type zijn dat als superklasse voorkomt van de klasse van de pointer aan de rechterzijde van de toekenning. Deze uitzondering is de enige op de regel die zegt dat de twee types aan beide zijden van een toekenning gelijk moeten zijn.

Een voorbeeld maakt dit duidelijk:

Waarde *pw;
IWaarde *piw;
FWaarde *pfw;

pw = piw;   // ok, Waarde is de basisklasse van IWaarde
pw = pfw;   // ok, Waarde is de basisklasse van FWaarde
piw = pw;   // fout, Iwwaarde is geen basisklasse van Waarde

Deze compatibiliteit tussen verschillende pointertypes is nodig om gebruik te kunnen maken van de virtuele functies. Ter verduidelijking is hier nog het schema dat het verband tussen de verschillende klassen uit het voorbeeld weergeeft.

!<>

Het mechanisme van de virtuele functie

Als we een virtuele klassefunctie oproepen via een pointer naar de basisklasse komt effect van de virtuele functie tot uiting.

IWaarde i1(5);
FWaarde f1(7.9);
Waarde *pw;

pw = &i1;
pw->druk();
pw = &f1;
pw->druk();

De pointer pw is een pointer naar een Waarde object. De declaratie en het gebruik van pw is toegelaten. Objecten van een abstracte klasse mogen niet, pointers naar een abstracte klasse mogen wel. Bij de eerste toekenning wijst pw naar een IWaarde object. Bij de oproep van druk() wordt de IWaarde variant van druk() gestart. Op het scherm verschijnt 5. Bij de tweede toekenning wijst pw naar een FWaarde object. Bij de tweede oproep van druk() wordt de FWaarde variant van druk() gestart. Op het scherm verschijnt 7.9. In elk object van een klasse met tenminste één virtuele functie zit extra informatie opgeslagen over de klasse van het object. Door deze informatie is het mogelijk dat bij de oproep van druk() bepaald wordt welke variant uit een van de afgeleide klassen wordt gestart.

Dit is de volledige tekst van het voorbeeld:

#include <iostream.h>

class Waarde   // abstracte klasse
{
public:
   virtual void druk() = 0;
};

class IWaarde : public Waarde
{
private:
   int intwaarde;

public:
   IWaarde(int iw) : intwaarde(iw)
   {
   }
   virtual void druk();
};

class FWaarde : public Waarde
{
private:
   double floatwaarde;

public:
   FWaarde(double iw) : floatwaarde(iw)
   {
   }
   virtual void druk();
};

void IWaarde::druk()
{
   cout << "geheel " << intwaarde;
}

void FWaarde::druk()
{
   cout << "reeel " << floatwaarde;
}

void toon(Waarde *pw)
{
   pw->druk();
}

int main()
{
   //Waarde ww;    fout Waarde is een abstracte klasse
   IWaarde i1(5);
   FWaarde f1(7.9);
   toon( &i1 );
   toon( &f1 );

   Waarde *pw1 = new IWaarde(256);
   Waarde *pw2 = new FWaarde(1.0/3.0);
   pw1->druk();
   pw2->druk();
}

In het voorbeeld wordt het principe van de virtuele functie tweemaal gedemonstreerd. Eenmaal worden adressen van objecten aan de functie toon() doorgegeven. De functie toon is ontwerpen met het doel de waarde van het doorgegeven object te tonen. Het object wordt doorgegeven via zijn adres. Dit is efficiënter dan de volledige waarde door te geven. De functie toon() verwacht het adres van een Waarde object. Vermits Waarde een abstracte basisklasse is van IWaarde of FWaarde, kan het doorgegeven object een IWaarde of een FWaarde object zijn. Gezien vanuit de functie toon() kan niet op voorhand voorspeld worden of het doorgegeven object een IWaarde of een FWaarde. Dit is de reden waarom we een virtuele functie gebruiken als mechanisme voor de start van druk(). Binnen toon() is er geen kennis nodig over de mogelijke specialisaties van Waarde. We kunnen dit voorbeeld besluiten met te zeggen dat een Waarde object toonbaar is; de waarde van het object kan verschillend zijn in de verschillende afleidingen. Dit verschil in gedrag wordt vastgelegd in de implementatie van de afgeleide klassen.

Constante objecten

Het is mogelijk om binnen de klassedeclaratie voorzieningen te treffen om constante objecten correct te behandelen. Een constant object is een object dat niet wijzigbaar is. Dit geven we aan bij de declaratie met het woord const. Bijvoorbeeld:

const Punt pc(45,67);

Het object pc is niet wijzigbaar; bijgevolg mogen voor dit object alleen klassefuncties gestart worden die garanderen dat er geen van de dataleden gewijzigd wordt. In de klassedeclaratie van Punt zien we na sommige functienamen het woord const staan. Hiermee wordt aangegeven dat de functie gaan dataleden wijzigt. Indien toch een wijziging van een datalid binnen een klassefunctie gebeurt, wordt dit als fout door de compiler gemeld.

#include <iostream.h>

// werken met constante objecten

class Punt
{
private:
   int x;
   int y;

public:
   Punt(int ix = 0, int iy = 0) : x(ix), y(iy)
   {
   }
   void druk() const
   {
      cout << "<" << x <<"," << y <<">" << endl;
   }

   // geen wijzigingen van dataleden toegelaten in const functies
   int haalx() const
   {
      // x++; fout
      return(x);
   }
   int haaly() const
   {
      return(y);
   }

   void zetx(int ix)
   {
      x = ix;
   }
   void zety(int iy)
   {
      y = iy;
   }
};

int main()
{
   Punt p1(2,3);
   const Punt p2(4,5);

   p1.druk();
   p2.druk();
   // p2.zetx(8); fout p2 kan niet gewijzigd worden

   return 0;
}

In main worden twee objecten gedeclareerd: p1 en p2. De laatste is een constante. Dit betekent dat alleen const functies bij dit object gestart kunnen worden.

Statische leden in een klasse

Statische dataleden zijn leden waarvoor slechts éénmaal geheugenruimte wordt gereserveerd. In het volgende voorbeeld bestaat er binnen de klasse Punt een statisch dataveld aantal. De geheugenruimte bestaat slechts éénmaal. Elk object van het type Punt heeft een eigen x en y veld, maar het veld aantal is gemeenschappelijk voor de hele klasse. De toegang tot aantal verloopt niet via een object maar wel via de klasse. Het veld aantal wordt in dit voorbeeld gebruikt om bij te houden hoeveel objecten van de klasse Punt er bestaan. Deze boekhouding wordt met behulp van de constructor en destructor georganiseerd. Telkens als we de constructor of destructor doorlopen wordt aantal met 1 verhoogd of verlaagd. Door het feit dat aantal privaat is, zijn we er zeker van dat aantal niet buiten de klasse gewijzigd kan worden. Daarom zijn er ook toegangsfuncties bijgevoegd. Dit zijn init_aantal() en haal_aantal(). Dit zijn statische functies. Dit betekent dat ze niet in het kader van een object worden gestart, maar wel binnen de klasse.

De geheugenruimte voor een statisch datalid moet expliciet gereserveerd worden.

#include <iostream.h>

// statische leden in een klasse

class Punt
{
private:
   int x;
   int y;
   static int aantal;

public:
   Punt(int ix = 0, int iy = 0) : x(ix), y(iy)
   {
      aantal++;
   }
   ~Punt()
   {
      aantal--;
   } // destructor

   void druk() const;
   int haalx() const
   {
      return(x);
   }
   int haaly() const
   {
      return(y);
   }
   void zetx(int ix)
   {
      x = ix;
   }
   void zety(int iy)
   {
      y = iy;
   }

   // statische klassefuncties worden zonder this opgeroepen
   static void init_aantal()
   {
      aantal = 0;
   }
   static int haal_aantal()
   {
      return( aantal);
   }
};

void druk();

void Punt::druk() const
{
   ::druk();// de twee dubbele punten zijn nodig voor recursie
            // te vermijden
   cout << "<" << x <<"," << y <<">" << endl;
}

void druk()
{
   cout << "cv10 ";
}

// de geheugenruimte voor statische dataleden
// moet expliciet gereserveerd worden

int Punt::aantal;

void fu()
{
   Punt p1(2,3);
   const Punt p2(4,5);

   p1.druk();
   p2.druk();

   cout << "aantal punten " << Punt::haal_aantal() << endl;
}

int main()
{
   // oproep zonder object
   Punt::init_aantal();

   fu();
   cout << "aantal punten " << Punt::haal_aantal() << endl;

   return 0;
}

De [] operator bij reeksen

In dit voorbeeld wordt gedemonstreerd hoe het mogelijk om de index bij arrays te controleren. Het is mogelijk om binnen een klasse een nieuwe betekenis te geven aan de operator\[\]. We kunnen zo een klasse laten werken als een array uit C of Pascal. Dit geeft ons de mogelijkheid om de waarde van de index te controleren.

De klasse Reeks heeft een private pointer naar een array van int’s. Deze array wordt gereserveerd in de constructor en wordt terug vrijgegeven in de destructor. We houden binnen de klassen ook de lengte bij en het datalid lengte. De functie controle_index() wordt gebruikt om de index te controleren en eventueel een foutmelding te geven. Bij fout wordt het programma afgebroken met exit();. De operator\[\] ontvangt de index, controleert die en geeft dan de gewenste dat terug. Het prototype ziet er als volgt uit:

int& operator [] ( unsigned long index )

Merk op dat het terugkeertype een referentietype is. Dit is nodig om de inhoud van de reeks te kunnen wijzigen.

#include <iostreams.h>
#include <stdlib.h>

// een toepassing van de [] operator

class Reeks
{
private:
   int *data;
   unsigned long lengte;

protected:
   void controle_index( unsigned long index, int lijnnr )
   {
      if ( index >= lengte )
      {
         cout << "arrayindex-fout in regel "
              << lijnnr<< " index " << index << endl;
         exit(1);
      }
   }

public:
   Reeks(unsigned long grootte)
   {
      lengte = grootte;// hou de grootte bij
      data = new int[grootte];// reserveer ruimte
      cout << "Reeks constructor\n";
   }
   ~Reeks()
   {
      delete [] data;// geef reeks vrij
      cout << "Reeks destructor\n";
   }

   int& operator [] ( unsigned long index )
   {
      controle_index( index , __LINE__ );// eerst controle
      return data[index];
   }
};

int main()
{
   Reeks lijst(10);

   for (int i=0; i<20; i++)
   {
      lijst[i] = i;
      cout << lijst[i];
   }

   return 0;
}

In main() wordt een reeks van 10 gehele getallen gecreëerd. De lijst wordt opgevuld met getallen en hier ontstaat een fout: bij i = 10 wordt het programma afgebroken.

Sjablonen: algemene reeksen

Het voorgaande voorbeeld was niet flexibel genoeg. Daarom wordt de klasse Reeks algemener gemaakt door sjablonen (templates) te gebruiken. Hierdoor is Reeks onafhankelijk van de opgeslagen soort. Reeks in niet meer een lijst van int’s maar wel van klasse T elementen. Dit zien we aan de naam van de klasse:

template<class T>
class Reeks

De klasse Reeks kent nu een typeparameter T. Hiermee kunnen we aangeven van welke klasse de gegevens zijn. In heel de klasse is int door T vervangen. De klasse Reeks is hierdoor algemener geworden. Voor het overige is Reeks niet gewijzigd.

#include <iostreams.h>
#include <stdlib.h>

// reeks.h een algemene reeks

template<class T>
class Reeks
{
private:
   T *data;
   unsigned long lengte;

protected:
   void controle_index( unsigned long index, int lijnnr )
   {
      if ( index >= lengte )
      {
         cout << "arrayindex-fout in regel "
         << lijnnr<< " index " << index << endl;
         exit(1);
      }
   }

public:
   Reeks(unsigned long grootte)
   {
      lengte = grootte;
      data = new T[grootte];
      cout << "Reeks<T> constructor\n";
   }
   ~Reeks()
   {
      delete [] data;
      cout << "Reeks<T> destructor\n";
   }

   T& operator [] ( unsigned long index )
   {
      controle_index( index , __LINE__ );
      return data[index];
   }
};

In het hoofdprogramma maken we een reeks van int getallen. We moeten het type int als parameter meegeven:

Reeks<int>lijst(10);
Reeks<double>lijst2(100);
Reeks<Punt>lijst3(5);

Het actuele type wordt vermeld tussen kleiner en groter-dan tekens.

#include <iostream.h>
#include <stdlib.h>
#include "reeks.h"

// sjablonen: algemene reeksen

int main()
{
   Reeks<int> lijst(10);

   for (int i=0; i<20; i++)
   {
      lijst[i] = i;
      cout << lijst[i];
   }

   return 0;
}

De reeks in dit voorbeeld is zo algemeen geworden dat we dit type in veel gevallen kunnen toepassen. We hoeven niet meer voor elke soort gegevens een nieuwe reeks te ontwerpen.

Uitzonderingen

Uitzonderingen (exceptions) laten toe om fouten op een gepaste manier af te handelen. In de voorgaande voorbeelden wordt de arrayfout drastisch afgehandeld. Het programma wordt gewoon afgebroken. Door gebruik te maken van uitzonderingen kan de foutafhandeling aangepast worden aan de noden van de gebruiker van een klasse. Binnen de klasse wordt in een foutsituatie de throw bewerking uitgevoerd. Hiermee wordt de fout aan de gebruiker van de klassen gemeld.

throw Fout();

Aan throw wordt een object van een klasse meegegeven. Hiermee wordt de fout geïdentificeerd. Fout() is de oproep van de standaard constructor zonder parameter. Om aan te geven dat we op mogelijke fouten reageren, definiëren we een try-blok.

try
{

}
catch(Fout)
{
   // foutafhandeling
}

Alle acties binnen het try-blok kunnen onderbroken worden door een throw. Alle bestaande objecten worden automatisch afgebroken door hun destructor. Dit geldt niet voor objecten die we dynamisch met new hebben gereserveerd. Het kan daarom nodig zijn om pointers naar een klasse op te nemen binnen een nieuwe klasse zodat de destructor van deze nieuwe klassen voor het vrijgeven van de pointer zorgt.

#include <iostream.h>

// uitzonderingen

class Fout // dit is de foutklasse
{
};

class Gegeven
{
private:
   int getal;

public:
   Gegeven(int geg=0) : getal(geg)
   {
      cout << "Gegeven(" << getal<<") constructor" << endl;
   }
   ~Gegeven()
   {
      cout << "Gegeven(" << getal<<") destructor" << endl;
   }
};

void fu()
{
   Gegeven g = 2;
   Gegeven *pg;

   pg = new Gegeven(3);
   throw Fout();// g wordt afgebroken
   // pg niet
}

void fu2()
{
   Gegeven h = 4;

   fu();// h wordt afgebroken
}

int main()
{
   try// probeer fu2() te starten
   {
      fu2();
   }
   catch(Fout)// van fouten hier op
   {
      cout << "fout" << endl;
   }

   return 0;
}

In de functie fu() wordt een throw uitgevoerd. Hierdoor worden h en g automatisch afgebroken. Dit is niet het geval voor het object dat door pg wordt aangewezen. In main() komen we terecht in het catch-blok en wordt de nodige actie ondernomen.

Een algemene reeks met uitzondering

De algemene reeks is nu met uitzonderingen beveiligd. De foutklasse Arrayfout houdt de foutmelding in tekstvorm bij. Binnen controle_index() kan een throw gestart worden.

// sjabloon Reeks met uitzondering

class Arrayfout
{
private:
   char *melding;

public:
   Arrayfout(char *p) : melding(p)
   {
   }
   char *foutmelding()
   {
      return( melding );
   }
};


template <class T>
class Reeks
{
private:
   T *data;
   unsigned long lengte;

protected:
   void controle_index( unsigned long index, int lijnnr )
   {
      if ( index >= lengte )
      {
         throw Arrayfout("arrayindex-fout");
      }
   }

public:
   Reeks(unsigned long grootte)
   {
      lengte = grootte;
      data = new T[grootte];
      cout << "Reeks constructor\n";
   }
   ~Reeks()
   {
      delete [] data;
      cout << "Reeks destructor\n";
   }
   T& operator [] ( unsigned long index )
   {
      controle_index( index , __LINE__ );
      return data[index];
   }
};

In het hoofdprogramma is een try en catch-blok bijgevoegd. Hiermee kunnen we de arrayfout opvangen.

#include <iostream.h>
#include <string.h>
#include "vreeks.h"

int main()
{
   try// probeer
   {
      Reeks<int> lijst(10);// een lijst van 10 int's

      for (int i=0; i<20; i++)
      {
         lijst[i] = i;
         cout << lijst[i];
      }
   }
   catch(Arrayfout &f)// vang arrayfout
   {
      cout << f.foutmelding();// toon de foutmelding
   }

   return 0;
}

Reeks template met automatische uitbreiding

Het volgende voorbeeld is het bestand nvreeks.h. Deze template klasse is in staat om de capaciteit van de opslag te vergroten zonder dat er gevens verloren gaan.

!<>

In het volgende testprogramma wordt het gebruik van deze reeks gedemonstreerd.

// cppvb17.cpp
#include <stdio.h>
#include "nvreeks.h"

int main()
{
   try
   {
      Reeks<int> tab(10);

      for (int i=0; i<20; i++)
      {
         int sq = i*i;
         tab.voegbij(sq);
      }

      for (int i=0; i<tab.grootte(); i++)
      {
         printf("%d: %d\n", i, tab[i]);
      }
   }
   catch(Arrayfout)
   {
      printf("Arrayfout\n");
   }
   catch(...)
   {
      printf("onbekende fout\n");
   }

   return 0;
}

In het bovenstaand programma zullen de catch blokken nooit bereikt worden omdat de reeks automatisch vergroot wordt. Er wordt gestart met grootte 10 en geëindigd met 20.

Pointers opslaan in een container

In dit voorbeeld is voor het inhoudstype T gekozen voor de pointer Info \*.

#include <stdio.h>
#include "nvreeks.h"

class Info
{
public:
   Info(int g): getal(g)
   {
   }
   void toon()
   {
      printf("%d\n", getal);
   }
private:
   int getal;
};

int main()
{
   try
   {
      Reeks<Info *> tab(10);


      for (int i=0; i<22; i++)
      {
         int sq = i*i;
         tab.voegbij(new Info(sq));
      }

      // alle Info's tonen
      for (int i=0; i<tab.grootte(); i++)
      {
         tab[i]->toon();
      }

      // alle Info's vrijgeven
      for (int i=0; i<tab.grootte(); i++)
      {
         tab[i]->toon();
      }
   }
   catch(Arrayfout)
   {
      printf("Arrayfout\n");
   }
   catch(...)
   {
      printf("onbekende fout\n");
   }
}

Een container als klassevariabele

In dit voorbeeld is de container een klassevariabele. .cppvb19.cpp

#include <stdio.h>
#include "nvreeks.h"

class Info
{
public:
   Info(int g): getal(g)
   {
   }
   void toon()
   {
      printf("%d\n", getal);
   }

private:
   int getal;
};

class Gegevens
{
public:
   Gegevens(int n): lijst(n)
   {
      for (int i=0; i<n; i++)
      {
         lijst.voegbij(new Info(i * i));
      }
   }
   void toon()
   {
      printf("Gegevens:\n");
      for (int i=0; i<lijst.grootte(); i++)
      {
         lijst[i]->toon();
      }
   }
   ~Gegevens()
   {
      for (int i=0; i<lijst.grootte(); i++)
      {
         delete lijst[i];
      }
   }

private:
   Reeks<Info *> lijst;
};


int main()
{
   try
   {
      Gegevens g(22);
      g.toon();
   }
   catch(Arrayfout)
   {
      printf("Arrayfout\n");
   }
   catch(...)
   {
      printf("onbekende fout\n");
   }

   return 0;
}

De STL containerbibliotheek

Een vector voorbeeld

In dit voorbeeld wordt het gebruik van vector getoond.

#include <vector>

using namespace std;

class Punt
{
private:
  int x;
  int y;

public:
  Punt(int ix, int iy) : x(ix), y(iy)
  {
  }

  void toon()
  {
    printf("%d, %d\n", x, y);
  }
};


int main()
{
   vector<Punt *> lijst;

   for (int i=0; i<20; i++)
   {
      Punt *p = new Punt(1+i,2+i);
      lijst.push_back(p);
   }



   for (int i= 0;i<lijst.size(); i++)
   {
      lijst[i]->toon();
   }

   vector<Punt *>::iterator it = lijst.begin();
   while (it != lijst.end())
   {
     Punt *q = *it;
     q->toon();

     it++;
   }


   for (int i= 0;i<lijst.size(); i++)
   {
      delete lijst[i];
   }

   return 0;
}

Een list voorbeeld

Hier is het voorgaande voorbeeld overgenomen en vector door list vervangen. De herhaling waarbij de rechte haken \[\] worden gebruikt voor indexering is geschrapt. Bij list en andere containers is dit niet mogelijk.

#include <list>

using namespace std;

class Punt
{
private:
  int x;
  int y;

public:
  Punt(int ix, int iy) : x(ix), y(iy)
  {
  }
  ~Punt()
  {
    printf("~Punt\n");
  }

  void toon()
  {
    printf("%d, %d\n", x, y);
  }
};


int main()
{
   list<Punt *> lijst;

   for (int i=0; i<20; i++)
   {
      Punt *p = new Punt(1+i,2+i);
      lijst.push_back(p);
   }

   list<Punt *>::iterator it = lijst.begin();
   while (it != lijst.end())
   {
     Punt *q = *it;
     q->toon();

     it++;
   }


   it = lijst.begin();
   while (it != lijst.end())
   {
     Punt *q = *it;
     delete q;

     it++;
   }

   return 0;
}

Java

De programmeertaal Java.

Bash

De programmeertaal Bash.

Python

De programmeertaal Python.

Ruby

De programmeertaal Ruby.

Javascript

De programmeertaal Javascript.

Dart

A Basic Dart Program

The following code uses many of Dart’s most basic features:

// Define a function.
printNumber(num aNumber)
{
  print('The number is $aNumber.'); // Print to the console.
}

// This is where the app starts executing.
main() {
  var number = 42;           // Declare and initialize a variable.
  printNumber(number);       // Call a function.
}

Here’s what this program uses that applies to all (or almost all) Dart apps:

// This is a comment.

Use // to indicate that the rest of the line is a comment. Alternatively, use /* … */. For details, see the section called Comments.

num
A type. Some of the other built-in types are String, int and bool.
42
A number literal. Literals are a kind of compile-time constant.
print()
A handy way to display output.
`` (or "…")
A string literal.

String interpolation: including a variable or expression’s string equivalent inside of a string literal. For more information, see the section called Strings.

main()
The special, required, top-level function where app execution starts.
var
A way to declare a variable without specifying its type.

Important Concepts

As you learn about the Dart language, keep these facts and concepts in mind:

  • Everything you can place in a variable is an object, and every object is an instance of a class. Even numbers, functions, and null are objects. All objects inherit from the Object class.

  • Specifying static types (such as num in the preceding example) clarifies your intent and enables static checking by tools, but it’s optional. (You might notice when you’re debugging your code that objects with no specified type get a special type: dynamic.)

  • Dart parses all your code before running it. You can provide tips to Dart-for example, by using types or compile-time constants-to catch errors or help your code run faster.

  • Dart supports top-level functions (such as main()), as well as functions tied to a class or object (static and instance methods, respectively). You can also create functions within functions (nested or local functions).

  • Similarly, Dart supports top-level variables, as well as variables tied to a class or object (static and instance variables). Instance variables are sometimes known as fields or properties.

  • Unlike Java, Dart doesn’t have the keywords public, protected, and private. If an identifier starts with an underscore (_), it’s private to its library. For details, see the section called Libraries and Visibility.

  • Identifiers can start with a letter or _, followed by any combination of those characters plus digits.

  • Sometimes it matters whether something is an expression or a statement, so we’ll be precise about those two words.

  • Dart tools can report two kinds of errors: warnings and errors. Warnings are just hints that your code might not work, but they don’t prevent your program from executing. Errors can be either compile-time or run-time. A compile-time error prevents the code from executing at all; a run-time error results in an exception being raised while the code executes.

  • Dart has two runtime modes: production and checked. Production is faster, but checked is helpful at development.

Keywords

+————–+————–+————–+————–+————–+————–+
| abstract | continue | factory | import | return | try |
+————–+————–+————–+————–+————–+————–+
| as | default | false | in | set | typedef |
+————–+————–+————–+————–+————–+————–+
| assert | do | final | is | static | var |
+————–+————–+————–+————–+————–+————–+
| break | dynamic | finally | library | super | void |
+————–+————–+————–+————–+————–+————–+
| case | else | for | new | switch | while |
+————–+————–+————–+————–+————–+————–+
| catch | export | get | null | this | with |
+————–+————–+————–+————–+————–+————–+
| class | external | if | operator | throw | const |
+————–+————–+————–+————–+————–+————–+

Dart keywords

Runtime Modes

We recommend that you develop and debug in checked mode, and deploy to production mode.

Production mode is the default runtime mode of a Dart program, optimized for speed. Production mode ignores assert statements and static types.

Checked mode is a developer-friendly mode that helps you catch some type errors during runtime. For example, if you assign a non-number to a variable declared as a num, then checked mode throws an exception.

Variables

Here’s an example of creating a variable and assigning a value to it:

var name = 'Bob';

Variables are references. The variable called name contains a reference to a String object with a value of “Bob”.

Default Value

Uninitialized variables have an initial value of null. Even variables with numeric types are initially null, because numbers are objects.

int lineCount;
assert(lineCount == null);
// Variables (even if they will be numbers) are initially null.

Note

The assert() call is ignored in production mode. In checked mode, assert(condition) throws an exception unless condition is true. For details, see the section called Assert.

Optional Types

You have the option of adding static types to your variable declarations:

String name = 'Bob';

Adding types is a way to clearly express your intent. Tools such as compilers and editors can use these types to help you, by providing early warnings for bugs and code completion.

Note

This chapter follows the style guide recommendation of using var, rather than type annotations, for local variables.

Final and Const

If you never intend to change a variable, use final or const, either instead of var or in addition to a type. A final variable can be set only once; a const variable is a compile-time constant.

A local, top-level, or class variable that’s declared as final is initialized the first time it’s used:

final name = 'Bob';   // Or: final String name = 'Bob';
// name = 'Alice';    // Uncommenting this results in an error

Note

Lazy initialization of final variables helps apps start up faster.

Use const for variables that you want to be compile-time constants. Where you declare the variable, set the value to a compile-time constant such as a literal, a const variable, or the result of an arithmetic operation on constant numbers:

const bar = 1000000;       // Unit of pressure (in dynes/cm2)
const atm = 1.01325 * bar; // Standard atmosphere

Built-in Types

The Dart language has special support for the following types:

  • numbers

  • strings

  • booleans

  • lists (also known as arrays)

  • maps

You can initialize an object of any of these special types using a literal. For example, `` is a string literal, and true is a boolean literal.

Because every variable in Dart refers to an object-an instance of a class-you can usually use constructors to initialize variables. Some of the built-in types have their own constructors. For example, you can use the Map() constructor to create a map, using code such as new Map().

Numbers

Dart numbers come in two flavors:

int
Integers of arbitrary size
double
64-bit (double-precision) floating-point numbers, as specified by the IEEE 754 standard

Both int and double are subtypes of num. The num type includes basic operators such as , -, /, and *, as well as bitwise operators such as >>. The num type is also where you’ll find +abs(), ceil(), and floor(), among other methods. If num and its subtypes don?t have what you?re looking for, the Math library might. (In JavaScript produced from Dart code, big integers currently behave differently than they do when the same Dart code runs in the Dart VM.)

Integers are numbers without a decimal point. Here are some examples of defining integer literals:

var x = 1;
var hex = 0xDEADBEEF;
var bigInt = 346534658346524376592384765923749587398457294759347029438709349347;

If a number includes a decimal, it is a double. Here are some examples of defining double literals:

var y = 1.1;
var exponents = 1.42e5;

Here’s how you turn a string into a number, or vice versa:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

The int type specifies the traditional bitwise shift (<<, >>), AND (&), and OR (|) operators. For example:

assert((3 << 1) == 6);  // 0011 << 1 == 0110
assert((3 >> 1) == 1);  // 0011 >> 1 == 0001
assert((3 | 4)  == 7);  // 0011 | 0100 == 0111

Strings

A Dart string is a sequence of UTF-16 code units. You can use either single or double quotes to create a string:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to just use the other string delimiter.";

To get the string corresponding to an object, Dart calls the object’s toString() method.

var s = 'string interpolation';

assert('Dart has $s, which is very handy.' ==
       'Dart has string interpolation, which is very handy.');
assert('That deserves all caps. ${s.toUpperCase()} is very handy!' ==
       'That deserves all caps. STRING INTERPOLATION is very handy!');

Note

The == operator tests whether two objects are equivalent. Two strings are equivalent if they have the same characters.

You can concatenate strings using adjacent string literals:

var s = 'String ' 'concatenation'
        " works even over line breaks.";
assert(s == 'String concatenation works even over line breaks.');

Another way to create a multi-line string: use a triple quote with either single or double quotation marks:

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

You can create a “raw” string by prefixing it with r:

var s = r"In a raw string, even \n isn't special.";

You can use Unicode escapes inside of strings:

print('Unicode escapes work: \u2665'); // Unicode escapes work: [heart]

For more information on using strings, see the section called Strings and Regular Expressions.

Booleans

To represent boolean values, Dart has a type named bool. Only two objects have type bool: the boolean literals, true and false.

When Dart expects a boolean value, only the value true is treated as true. All other values are treated as false. Unlike in JavaScript, values such as 1, "aString", and someObject are all treated as false.

For example, consider the following code, which is valid both as JavaScript and as Dart code:

var name = 'Bob';
if (name) {
  print('You have a name!'); // Prints in JavaScript, not in Dart.
}

If you run this code as JavaScript, it prints “You have a name!” because name is a non-null object. However, in Dart running in production mode, the above doesn’t print at all because name is converted to false (because name != true). In Dart running in checked mode, the above code throws an exception because the name variable is not a bool.

Here’s another example of code that behaves differently in JavaScript and Dart:

if (1) {
  print('JavaScript prints this line because it thinks 1 is true.');
} else {
  print('Dart in production mode prints this line.');
  // However, in checked mode, if (1) throws an exception.
}

Note

The previous two samples work only in production mode, not checked mode. In checked mode, an exception is thrown if a non-boolean is used when a boolean value is expected.

Dart’s treatment of booleans is designed to avoid the strange behaviors that can arise when many values can be treated as true. What this means for you is that, instead of using code like if (nonbooleanValue), you should instead explicitly check for values. For example:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0/0;
assert(iMeantToDoThis.isNaN);

Lists

Perhaps the most common collection in nearly every programming language is the array, or ordered group of objects. In Dart, arrays are List objects, so we usually just call them lists.

Dart list literals look like JavaScript array literals. Here’s a simple Dart list:

var list = [1,2,3];

Lists use zero-based indexing, where 0 is the index of the first element and list.length - 1 is the index of the last element. You can get a list’s length and refer to list elements just as you would in JavaScript:

var list = [1,2,3];
assert(list.length == 3);
assert(list[1] == 2);

The List type has many handy methods for manipulating lists. For more information about lists, see the section called Generics and the section called Collections.

Maps

In general, a map is an object that associates keys and values. Dart support for maps is provided by map literals and the Map type.

Here’s a simple Dart map:

var gifts = {                         // A map literal
// Keys       Values
  'first'  : 'partridge',
  'second' : 'turtledoves',
  'fifth'  : 'golden rings'
};

In map literals, each key must be a string. If you use a Map constructor, any object can be a key.

var map = new Map();                  // Use a map constructor.
map[1] = 'partridge';                 // Key is 1; value is 'partridge'.
map[2] = 'turtledoves';               // Key is 2; value is 'turtledoves'.
map[5] = 'golden rings';              // Key is 5; value is 'golden rings'.

A map value can be any object, including null.

You add a new key-value pair to an existing map just as you would in JavaScript:

var gifts = { 'first': 'partridge' };
gifts['fourth'] = 'calling birds';    // Add a key-value pair

You retrieve a value from a map the same way you would in JavaScript:

var gifts = { 'first': 'partridge' };
assert(gifts['first'] == 'partridge');

If you look for a key that isn’t in a map, you get a null in return:

var gifts = { 'first': 'partridge' };
assert(gifts['fifth'] == null);

Use .length to get the number of key-value pairs in the map:

var gifts = { 'first': 'partridge' };
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

For more information about maps, see the section called Generics and the section called Maps.

Functions

Here’s an example of implementing a function:

void printNumber(num number) {
  print('The number is $number.');
}

Although the style guide recommends specifying the parameter and return types, you don’t have to:

printNumber(number) {          // Omitting types is OK.
  print('The number is $number.');
}

For functions that contain just one expression, you can use a shorthand syntax:

printNumber(number) => print('The number is $number.');

The ⇒ expr; syntax is a shorthand for { return expr;}. In the printNumber() function above, the expression is the call to the top-level print() function.

Note

Only an expression-not a statement-can appear between the arrow () and the semicolon (;). For example, you can’t put an if statement there, but you can use a conditional (?:) expression.

You can use types with , although the convention is not to do so:

printNumber(num number) => print('The number is $number.'); // Types are OK.

Here’s an example of calling a function:

printNumber(123);

A function can have two types of parameters: required and optional. The required parameters are listed first, followed by any optional parameters.

Optional Parameters

Optional parameters can be either positional or named, but not both.

Both kinds of optional parameter can have default values. The default values must be compile-time constants such as literals. If no default value is provided, the value is null.

If you need to know whether the caller passed in a value for an optional parameter, use the syntax ?param:

if (?device) {    // Returns true if the caller specified the parameter.
  // ...The user set the value. Do something with it...
}

Optional named parameters

When calling a function, you can specify named parameters using paramName: value. For example:

enableFlags(bold: true, hidden: false);

When defining a function, use {param1, param2, …} to specify named parameters:

/// Sets the [bold] and [hidden] flags to the values you specify.
enableFlags({bool bold, bool hidden}) {
  // ...
}

Use a colon (:) to specify default values:

/**
 * Sets the [bold] and [hidden] flags to the values you specify,
 * defaulting to false.
 */
enableFlags({bool bold: false, bool hidden: false}) {
  // ...
}

enableFlags(bold: true); // bold will be true; hidden will be false.

Optional positional parameters

Wrapping a set of function parameters in [] marks them as optional positional parameters:

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

Here’s an example of calling this function without the optional parameter:

assert(say('Bob', 'Howdy') == 'Bob says Howdy');

And here’s an example of calling this function with the third parameter:

assert(say('Bob', 'Howdy', 'smoke signal') ==
  'Bob says Howdy with a smoke signal');

Use = to specify default values:

String say(String from, String msg,
  [String device='carrier pigeon', String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}

assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');

Functions as First-Class Objects

You can pass a function as a parameter to another function. For example:

printElement(element) {
  print(element);
}

var list = [1,2,3];
list.forEach(printElement); // Pass printElement as a parameter.

You can also assign a function to a variable, such as:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

Lexical Scope

Dart is a lexically scoped language, which means that the scope of variables is determined statically, simply by the layout of the code. You can “follow the curly braces outwards” to see if a variable is in scope.

Here is an example of nested functions with variables at each scope level:

var topLevel = true;
main() {
    var insideMain = true;

    myFunction() {
      var insideFunction = true;

      nestedFunction() {
        var insideNestedFunction = true;
        assert(topLevel);
        assert(insideMain);
        assert(insideFunction);
        assert(insideNestedFunction);
      }
    }
}

Notice how nestedFunction() can use variables from every level, all the way up to the top level.

Lexical Closures

A closure is a function object that has access to variables in its lexical scope, even when the function is used outside of its original scope.

Functions can close over variables defined in surrounding scopes. In the following example, adder() captures the variable addBy. Wherever the returned function goes, it remembers addBy.

/// Returns a function that adds [addBy] to a number.
Function makeAdder(num addBy) {
  adder(num i) {
    return addBy + i;
  }
  return adder;
}

main() {
  var add2 = makeAdder(2); // Create a function that adds 2.
  var add4 = makeAdder(4); // Create a function that adds 4.

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}

Testing for Equality

Each time you create a closure, that closure is a new object. This can cause problems when you want to test whether two functions are equivalent. For example, consider this code:

var s = 'some string';       // Create a String object.
var splitClosure1 = s.split; // Get a reference to its split() method.
var splitClosure2 = s.split; // Get another reference to its split() method.

// Because each reference to the method creates a separate closure,
// s.split != s.split, and splitClosure1 != splitClosure2.
assert(s.split != s.split);
assert(splitClosure1 != splitClosure2);

The key to comparing instance methods is to save a reference to the closure:

splitClosure2 = splitClosure1;
assert(splitClosure1 == splitClosure2);

Top-level and static methods are different-you can compare them by name:

foo() {}

class SomeClass {
  static void bar() {}
}

main() {
  assert(foo == foo);
  assert(SomeClass.bar == SomeClass.bar);
}

Return Values

All functions return a value. If no return value is specified, the statement return null; is implicitly appended to the function body.

Operators

Dart defines the operators shown in Table 2.2, Operators and their precedence. You can override many of these operators, as described in the section called Overridable Operators.

+————————————–+————————————–+
| Description | Operator |
+————————————–+————————————–+
| unary postfix and argument | expr++ expr– () [] . ?identifier |
| definition test | |
+————————————–+————————————–+
| unary prefix | -expr !expr ~expr ++expr –expr |
+————————————–+————————————–+
| multiplicative | * / % ~/ |
+————————————–+————————————–+
| additive | + - |
+————————————–+————————————–+
| shift | << >> |
+————————————–+————————————–+
| relational and type test | >= > <= < as is is! |
+————————————–+————————————–+
| equality | == != |
+————————————–+————————————–+
| bitwise AND | & |
+————————————–+————————————–+
| bitwise XOR | ^ |
+————————————–+————————————–+
| bitwise OR | | |
+————————————–+————————————–+
| logical AND | && |
+————————————–+————————————–+
| logical OR | || |
+————————————–+————————————–+
| conditional | expr1 ? expr2 : expr3 |
+————————————–+————————————–+
| cascade | .. |
+————————————–+————————————–+
| assignment | = *= /= ~/= %= += -= <<= >>= |
| | &= ^= |= |
+————————————–+————————————–+

Operators and their precedence

When you use operators, you create expressions. Here are some examples of operator expressions:

a++
a + b
a = b
a == b
a? b: c
a is T

In Table 2.2, Operators and their precedence, each operator has higher precedence than the operators in the rows below it. For example, the multiplicative operator % has higher precedence than (and thus executes before) the equality operator ==, which has higher precedence than the logical AND operator &&. That precedence means that the following two lines of code execute the same way:

if ((n % i == 0) && (d % i == 0)) // Parens improve readability.
if (n % i == 0 && d % i == 0)     // Harder to read, but equivalent.

Warning

For operators that work on two operands, the leftmost operand determines which version of the operator is used. For example, if you have a Vector object and a Point object, aVector + aPoint uses the Vector version of +.

Arithmetic Operators

Dart supports the usual arithmetic operators, as shown in Table 2.3, Arithmetic operators.

+————————————–+————————————–+
| Operator | Meaning |
+————————————–+————————————–+
| + | Add |
+————————————–+————————————–+
| - | Subtract |
+————————————–+————————————–+
| -expr | Unary minus, also known as negation |
| | (reverse the sign of the expression) |
+————————————–+————————————–+
| * | Multiply |
+————————————–+————————————–+
| / | Divide |
+————————————–+————————————–+
| ~/ | Divide, returning an integer result |
+————————————–+————————————–+

Arithmetic operators

Example:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5);   // Result is a double
assert(5 ~/ 2 == 2);    // Result is an integer
assert(5 % 2 == 1);     // Remainder

print('5/2 = ${5~/2} remainder ${5%2}'); // 5/2 = 2 remainder 1

Dart also supports both prefix and postfix increment and decrement operators.

+————————————–+————————————–+
| Operator | Meaning |
+————————————–+————————————–+
| ++var | var = var + 1 (expression value is |
| | var + 1) |
+————————————–+————————————–+
| var++ | var = var + 1 (expression value is |
| | var) |
+————————————–+————————————–+
| –var | var = var - 1 (expression value is |
| | var - 1) |
+————————————–+————————————–+
| var– | var = var - 1 (expression value is |
| | var) |
+————————————–+————————————–+

Increment and decrement operators

Example:

var a, b;

a = 0;
b = ++a;         // Increment a before b gets its value.
assert(a == b);  // 1 == 1

a = 0;
b = a++;         // Increment a AFTER b gets its value.
assert(a != b);  // 1 != 0

a = 0;
b = --a;         // Decrement a before b gets its value.
assert(a == b);  // -1 == -1

a = 0;
b = a--;         // Decrement a AFTER b gets its value.
assert(a != b) ; // -1 != 0

Equality and Relational Operators

+————————————————————————–+
| Operator Meaning |
+————————————————————————–+
| == |
+————————————————————————–+
| Equal; see discussion below |
+————————————————————————–+
| != |
+————————————————————————–+
| Not equal |
+————————————————————————–+
| > |
+————————————————————————–+
| Greater than |
+————————————————————————–+
| < |
+————————————————————————–+
| Less than |
+————————————————————————–+
| >= |
+————————————————————————–+
| Greater than or equal to |
+————————————————————————–+
| <= |
+————————————————————————–+
| Less than or equal to |
+————————————————————————–+

Equality and relational operators

To test whether two objects x and y represent the same thing, use the == operator. Here’s how the == operator works:

  • If x or y is null, return true if both are null, and false if only one is null.

  • Return the result of the method invocation x.==(y). (That’s right, operators such as == are methods that are invoked on their first operand. You can even override many operators, including ==, as you’ll see in the section called Overridable Operators.)

Here’s an example of using each of the equality and relational operators:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);

Type Test Operators

The as, is, and is! operators are handy for checking types at runtime.

+————————————–+————————————–+
| Operator | Meaning |
+————————————–+————————————–+
| —- | as |
+————————————–+————————————–+
| Typecast | is |
+————————————–+————————————–+
| True if the object has the specified | is! |
| type | |
+————————————–+————————————–+

Type test operators

The result of obj is T is true if obj implements the interface specified by T. For example, obj is Object is always true.

Use the as operator to cast an object to a particular type. In general, you should use it as a shorthand for an is test on an object following by an expression using that object. For example, consider the following code:

if (person is Person) {               // Type check
  person.firstName = 'Bob';
}

You can make the code shorter using the as operator:

(person as Person).firstName = 'Bob';

Note

The code isn’t equivalent. If person is null or not a Person, the first example (with is) does nothing; the second (with as) throws an exception.

Assignment Operators

As you’ve already seen, you assign values using the = operator. You can also use compound assignment operators such as +=, which combine an operation with an assignment.

+————–+————–+————–+————–+————–+————–+
| = | ?= | /= | %= | >>= | ^= |
+————–+————–+————–+————–+————–+————–+
| += | *= | ~/= | <<= | &= | |= |
+————–+————–+————–+————–+————–+————–+

Assignment operators

Here’s how compound assignment operators work:

Compound assignment  Equivalent expression
For an operator op:  a op= b  a = a op b
Example:                 a += b   a = a + b

The following example uses both assignment and compound assignment operators:

var a = 2;           // Assign using =
a *= 3;              // Assign and multiply: a = a * 3
assert(a == 6);

Logical Operators

You can invert or combine boolean expressions using the logical operators.

Operator        Meaning
!expr   inverts the following expression (changes false to true, and vice versa)
||      logical OR
&&      logical AND

Here’s an example of using the logical operators:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
}

Bitwise and Shift Operators

You can manipulate the individual bits of numbers in Dart. Usually, you’d use these bitwise and shift operators with integers, as shown in Table 2.9, Bitwise and shift operators.

Operator        Meaning
&       AND
|       OR
^       XOR
~expr   Unary bitwise complement (0s become 1s; 1s become 0s)
<<      Shift left
>>      Shift right

Here’s an example of using bitwise and shift operators:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask)  == 0x02);  // AND
assert((value & ~bitmask) == 0x20);  // AND NOT
assert((value | bitmask)  == 0x2f);  // OR
assert((value ^ bitmask)  == 0x2d);  // XOR
assert((value << 4)       == 0x220); // Shift left
assert((value >> 4)       == 0x02);  // Shift right

Other Operators

A few operators remain, most of which you’ve already seen in other examples.

Table 2.10. Other operators

Operator Name Meaning ():: Function application Represents a function call

[]
List access Refers to the value at the specified index in the list
expr1 ? expr2 : expr3
Conditional If expr1 is true, executes expr2; otherwise, executes expr3
.
Member access Refers to a property of an expression; example: foo.bar selects property bar from expression foo
..
Cascade Allows you to perform multiple operations on the members of a single object; described in the section called Classes
?identifier
Argument definition test Tests whether the caller specified an optional parameter; described in the section called Optional Parameters

Control Flow Statements

You can control the flow of your Dart code using any of the following:

  • if and else

  • for loops

  • while and do-while loops

  • break and continue

  • switch and case

  • assert

You can also affect the control flow using try-catch and throw, as explained in the section called Exceptions.

If and Else

Dart supports if statements with optional else statements, as the next sample shows. Also see conditional expressions (?:), which are covered in the section called Other Operators.

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

Remember, unlike JavaScript, Dart treats all values other than true as false. See the section called Booleans for more information.

For Loops

You can iterate with the standard for loop. For example:

var message = new StringBuffer("Dart is fun");
for (var i = 0; i < 5; i++) {
  message.write('!');
}

Closures inside of Dart’s for loops capture the value of the index, avoiding a common pitfall found in JavaScript. For example, consider:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

The output is 0 and then 1, as expected. In contrast, the example would print 2 and then 2 in JavaScript.

If the object that you are iterating over is an Iterable, you can use the forEach() method. Using forEach() is a good option if you don’t need to know the current iteration counter:

candidates.forEach((candidate) => candidate.interview());

Iterable classes such as List and Set also support the for-in form of iteration, which is described in the section called Iteration:

var collection = [0, 1, 2];
for (var x in collection) {
  print(x);
}

While and Do-While

A while loop evaluates the condition before the loop:

while(!isDone()) {
  doSomething();
}

A do-while loop evaluates the condition after the loop:

do {
  printLine();
} while (!atEndOfPage());

Break and Continue

Use break to stop looping:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

Use continue to skip to the next loop iteration:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

You might write that example differently if you’re using a Iterable such as a list or set:

candidates.where((c) => c.yearsExperience >= 5)
          .forEach((c) => c.interview());

Switch and Case

Switch statements in Dart compare integer, string, or compile-time constants using ==. The compared objects must all be instances of the same class (and not of any of its subtypes), and the class must not override ==.

Each non-empty case clause ends with a break statement, as a rule. Other valid ways to end a non-empty case clause are a continue, throw, or return statement.

Use a default clause to execute code when no case clause matches:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

The following example omits the break statement in the case clause, thus generating an error:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // ERROR: Missing break causes an exception to be thrown!!

  case 'CLOSED':
    executeClosed();
    break;
}

However, Dart does support empty case clauses, allowing a form of fall-through:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':     // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

If you really want fall-through, you can use a continue statement and a label:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed; // Continues executing at the nowClosed label.

nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

A case clause can have local variables, which are visible only inside the scope of that clause.

Assert

Use an assert statement to disrupt normal execution if a boolean condition is false. You can find examples of assert statements throughout this tour. Here are some more:

assert(text != null);  // Make sure the variable has a non-null value.
assert(number < 100);  // Make sure the value is less than 100.
assert(urlString.startsWith('https')); // Make sure this is an HTTPS URL.

Note

Assert statements work only in checked mode. They have no effect in production mode.

Inside the parentheses after assert, you can put any expression that resolves to a boolean value or to a function. If the expression’s value or function’s return value is true, the assertion succeeds and execution continues. Otherwise, the assertion fails and an exception (an AssertionError) is thrown.

Exceptions

Your Dart code can throw and catch exceptions. Exceptions are errors indicating that something unexpected happened. If the exception isn’t caught, the isolate that raised the exception is suspended, and typically the isolate and its program are terminated.

In contrast to Java, all of Dart’s exceptions are unchecked exceptions. Methods do not declare which exceptions they might throw, and you are not required to catch any exceptions.

Dart provides Exception and Error types, as well as numerous predefined subtypes. You can, of course, define your own exceptions. However, Dart programs can throw any non-null object-not just Exception and Error objects-as an exception.

Throw

Here’s an example of throwing, or raising, an exception:

throw new ExpectException('Value must be greater than zero');

You can also throw arbitrary objects:

throw 'Out of llamas!';

Because throwing an exception is an expression, you can throw exceptions in statements, as well as anywhere else that allows expressions:

distanceTo(Point other) => throw new UnimplementedError();

Catch

Catching, or capturing, an exception stops the exception from propagating. Catching an exception gives you a chance to handle it:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

To handle code that can throw more than one type of exception, you can specify multiple catch clauses. The first catch clause that matches the thrown object’s type handles the exception. If the catch clause does not specify a type, that clause can handle any type of thrown object:

try {
  breedMoreLlamas();
} on OutOfLlamasException {           // A specific exception
  buyMoreLlamas();
} on Exception catch(e) {             // Anything else that is an exception
  print('Unknown exception: $e');
} catch(e) {                          // No specified type, handles all
  print('Something really unknown: $e');
}

As the preceding code shows, you can use either on or catch or both. Use on when you need to specify the exception type. Use catch when your exception handler needs the exception object.

Finally

To ensure that some code runs whether or not an exception is thrown, use a finally clause. If no catch clause matches the exception, the exception is propagated after the finally clause runs:

try {
  breedMoreLlamas();
} finally {
  cleanLlamaStalls();  // Always clean up, even if an exception is thrown.
}

The finally clause runs after any matching catch clauses:

try {
  breedMoreLlamas();
} catch(e) {
  print('Error: $e');  // Handle the exception first.
} finally {
  cleanLlamaStalls();  // Then clean up.
}

Learn more by reading the section called Exceptions.

Classes

Dart is an object-oriented language with classes and single inheritance. Every object is an instance of a class, and all classes descend from Object.

To create an object, you can use the new keyword with a constructor for a class. Constructor names can be either ClassName or ClassName.identifier. For example:

var jsonData = json.parse('{"x":1, "y":2}');

var p1 = new Point(2,2);               // Create a Point using Point().
var p2 = new Point.fromJson(jsonData); // Create a Point using Point.fromJson().

Objects have members consisting of functions and data (methods and instance variables, respectively). When you call a method, you invoke it on an object: the method has access to that object’s functions and data.

Use a dot (.) to refer to an instance variable or method:

var p = new Point(2,2);

p.y = 3;             // Set the value of the instance variable y.
assert(p.y == 3);    // Get the value of y.

num distance = p.distanceTo(new Point(4,4)); // Invoke distanceTo() on p.

Use the cascade operator (..) when you want to perform a series of operations on the members of a single object:

query('#button')
    ..text = 'Click to Confirm'                        // Get an object. Use its
    ..classes.add('important')                         // instance variables
    ..onClick.listen((e) => window.alert('Confirmed!')); // and methods.

Some classes provide constant constructors. To create a compile-time constant using a constant constructor, use const instead of new:

var p = const ImmutablePoint(2,2);

Constructing two identical compile-time constants results in a single, canonical instance:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a,b)); // They are the same instance!

The following sections discuss how to implement classes.

Instance Variables

Here’s how you declare instance variables:

class Point {
  num x;      // Declare an instance variable (x), initially null.
  num y;      // Declare y, initially null.
  num z = 0;  // Declare z, initially 0.
}

All uninitialized instance variables have the value null.

All instance variables generate an implicit getter method. Non-final, non-const instance variables also generate an implicit setter method. For details, see the section called Getters and setters.

class Point {
  num x;
  num y;
}

main() {
  var point = new Point();
  point.x = 4;             // Use the setter method for x.
  assert(point.x == 4);    // Use the getter method for x.
  assert(point.y == null); // Values default to null.
}

If you initialize an instance variable where it is declared (instead of in a constructor or method), the value is set when the instance is created, which is before the constructor and its initializer list execute.

Constructors

Declare a constructor by creating a function with the same name as its class (plus, optionally, an additional identifier as described in the section called Named constructors). The most common form of constructor, the generative constructor, creates a new instance of a class:

class Point {
  num x;
  num y;

  Point(num x, num y) {
    // There's a better way to do this, stay tuned.
    this.x = x;
    this.y = y;
  }
}

The this keyword refers to the current instance.

Note

Use this only when there is a name conflict. Otherwise, Dart style omits the this.

The pattern of assigning a constructor argument to an instance variable is so common, Dart has syntactic sugar to make it easy:

class Point {
  num x;
  num y;

  // Syntactic sugar for setting x and y before the constructor body runs.
  Point(this.x, this.y);
}

Default constructors

If you don’t declare a constructor, a default constructor is provided for you. The default constructor has no arguments and invokes the no-argument constructor in the superclass.

====== Constructors aren’t inherited

Subclasses don’t inherit constructors from their superclass. A subclass that declares no constructors has only the default (no argument, no name) constructor.

Named constructors

Use a named constructor to implement multiple constructors for a class or to provide extra clarity:

class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // Named constructor
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}

Remember that constructors are not inherited, which means that a superclass’s named constructor is not inherited by a subclass. If you want a subclass to be created with a named constructor defined in the superclass, you must implement that constructor in the subclass.

Invoking a non-default superclass constructor

By default, a constructor in a subclass calls the superclass’s unnamed, no-argument constructor. If the superclass doesn’t have such a constructor, then you must manually call one of the constructors in the superclass. Specify the superclass constructor after a colon (:), just before the constructor body (if any).

class Person {
  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

main() {
  var emp = new Employee.fromJson({});

  // Prints:
  // in Person
  // in Employee
}

Initializer list

Besides invoking a superclass constructor, you can also initialize instance variables before the constructor body runs. Separate initializers with commas.

class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // Initializer list sets instance variables before the constructor body runs.
  Point.fromJson(Map json) : x = json['x'], y = json['y'] {
    print('In Point.fromJson(): ($x, $y)');
  }
}

Warning

The right-hand side of an initializer does not have access to this.

Redirecting constructors

Sometimes a constructor’s only purpose is to redirect to another constructor in the same class. A redirecting constructor’s body is empty, with the constructor call appearing after a colon (:).

class Point {
  num x;
  num y;

  Point(this.x, this.y);                // The main constructor for this class.
  Point.alongXAxis(num x) : this(x, 0); // Delegates to the main constructor.
}

Constant constructors

If your class produces objects that never change, you can make these objects compile-time constants. To do this, define a const constructor and make sure that all instance variables are final or const.

class ImmutablePoint {
  final num x;
  final num y;
  const ImmutablePoint(this.x, this.y);
  static final ImmutablePoint origin = const ImmutablePoint(0, 0);
}

Factory constructors

Use the factory keyword when implementing a constructor that doesn’t always create a new instance of its class. For example, a factory constructor might return an instance from a cache, or it might return an instance of a subtype.

The following example demonstrates a factory constructor returning objects from a cache:

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to the _ in front of its name.
  static final Map<String, Logger> _cache = <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = new Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) {
      print(msg);
    }
  }
}

Note

Factory constructors have no access to this.

To invoke a factory constructor, you use the new keyword:

var logger = new Logger('UI');
logger.log('Button clicked');

Methods

Methods are functions that provide behavior for an object.

Instance methods

Instance methods on objects can access instance variables and this. The distanceTo() method in the following sample is an example of an instance method:

import 'dart:math';

class Point {
  num x;
  num y;
  Point(this.x, this.y);

  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}

Getters and setters

Getters and setters are special methods that provide read and write access to an object’s properties. Recall that each instance variable has an implicit getter, plus a setter if appropriate. You can create additional properties by implementing getters and setters, using the get and set keywords:

class Rectangle {
  num left;
  num top;
  num width;
  num height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  num get right             => left + width;
      set right(num value)  => left = value - width;
  num get bottom            => top + height;
      set bottom(num value) => top = value - height;
}

main() {
  var rect = new Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

With getters and setters, you can start with instance variables, later wrapping them with methods, all without changing client code.

Note

Operators such as increment (++) work in the expected way, whether or not a getter is explicitly defined. To avoid any unexpected side effects, the operator calls the getter exactly once, saving its value in a temporary variable.

Abstract methods

Instance, getter, and setter methods can be abstract, defining an interface but leaving its implementation up to other classes. To make a method abstract, use a semicolon (;) instead of a method body:

abstract class Doer {
  // ...Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // ...Provide an implementation, so the method is not abstract here...
  }
}

Calling an abstract method results in a run-time error.

Also see the section called Abstract Classes.

Overridable Operators

You can override the operators shown in Table 2.11, Operators that can be overridden. For example, if you define a Vector class, you might define a + method to add two vectors.

Table 2.11. Operators that can be overridden

<       +       |       []
>       /       ^       []=
<=      ~/      &       ~
>=      *       <<      ==
-       %       >>

Here’s an example of a class that overrides the + and - operators:

class Vector {
  final int x;
  final int y;
  const Vector(this.x, this.y);

  Vector operator +(Vector v) { // Overrides + (a + b).
    return new Vector(x + v.x, y + v.y);
  }

  Vector operator -(Vector v) { // Overrides - (a - b).
    return new Vector(x - v.x, y - v.y);
  }
}

main() {
  final v = new Vector(2,3);
  final w = new Vector(2,2);

  assert(v.x == 2 && v.y == 3);         // v   == (2,3)
  assert((v+w).x == 4 && (v+w).y == 5); // v+w == (4,5)
  assert((v-w).x == 0 && (v-w).y == 1); // v-w == (0,1)
}

For an example of overriding ==, see the section called Implementing map keys.

Abstract Classes

Use the abstract modifier to define an abstract class-a class that can’t be instantiated. Abstract classes are useful for defining interfaces, often with some implementation. If you want your abstract class to appear to be instantiable, define a factory constructor.

Abstract classes often have abstract methods. Here’s an example of declaring an abstract class that has an abstract method:

// This class is declared abstract and thus can't be instantiated.
abstract class AbstractContainer {
  // ...Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}

The following class isn’t abstract, and thus can be instantiated even though it defines an abstract method:

class SpecializedContainer extends AbstractContainer {
  // ...Define more constructors, fields, methods...

  void updateChildren() {
    // ...Implement updateChildren()...
  }
// Abstract method causes a warning but doesn't prevent instantiatation.
  void doSomething();
}

Implicit Interfaces

Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements. If you want to create a class A that supports class B’s API without inheriting B’s implementation, class A should implement the B interface.

A class implements one or more interfaces by declaring them in an implements clause and then providing the APIs required by the interfaces. For example:

// A person. The implicit interface contains greet().
class Person {
  final _name;          // In the interface, but visible only in this library,
  Person(this._name);   // Not in the interface, since this is a constructor.
  String greet(who) => 'Hello, $who. I am $_name.'; // In the interface.
}

// An implementation of the Person interface.
class Imposter implements Person {
  final _name = "";      // We have to define this, but we don't use it.
  String greet(who) => 'Hi $who. Do you know who I am?';
}

greetBob(Person person) => person.greet('bob');

main() {
  print(greetBob(new Person('kathy')));
  print(greetBob(new Imposter()));
}

Here’s an example of specifying that a class implements multiple interfaces:

class Point implements Comparable, Location {
  // ...
}

Extending a Class

Use extends to create a subclass, and super to refer to the superclass:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  ...
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  ...
}

Subclasses can override instance methods, getters, and setters. Here’s an example of overriding the Object class’s noSuchMethod() method, which is called whenever code attempts to use a non-existent method or instance variable:

class A {
  // Unless you override noSuchMethod, using a non-existent member
  // results in a NoSuchMethodError.
  void noSuchMethod(Invocation mirror) {
    print('You tried to use a non-existent member: ${mirror.memberName}');
  }
}

Class Variables and Methods

Use the static keyword to implement class-wide variables and methods.

Static variables

Static variables (class variables) are useful for class-wide state and constants:

class Color {
  static const RED = const Color('red'); // A constant static variable.
  final String name;                     // An instance variable.
  const Color(this.name);                // A constant constructor.
}

main() {
  assert(Color.RED.name == 'red');
}

Static variables aren’t initialized until they’re used.

Static methods

Static methods (class methods) do not operate on an instance, and thus do not have access to this. For example:

import 'dart:math';

class Point {
  num x;
  num y;
  Point(this.x, this.y);

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

main() {
  var a = new Point(2, 2);
  var b = new Point(4, 4);
  var distance = Point.distanceBetween(a,b);
  assert(distance < 2.9 && distance > 2.8);
}

Note

Consider using top-level functions, instead of static methods, for common or widely used utilities and functionality.

You can use static methods as compile-time constants. For example, you can pass a static method as a parameter to a constant constructor.

Generics

If you look at the API documentation for the basic array type, List, you’ll see that the type is actually List<E>. The <…> notation marks List as a generic (or parameterized) type-a type that has formal type parameters. By convention, type variables have single-letter names, such as E, T, S, K, and V.

Why Use Generics?

Because types are optional in Dart, you never have to use generics. You might want to, though, for the same reason you might want to use other types in your code: types (generic or not) let you document and annotate your code, making your intent clearer.

For example, if you intend for a list to contain only strings, you can declare it as List<String> (read that as list of string). That way you, your fellow programmers, and your tools (such as Dart Editor and the Dart VM in checked mode) can detect that assigning a non-string to the list is probably a mistake. Here’s an example:

var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
// ...
names.add(42); // Fails in checked mode (succeeds in production mode).

Another reason for using generics is to reduce code duplication. Generics let you share a single interface and implementation between many types, while still taking advantage of checked mode and static analysis early warnings. For example, say you create an interface for caching an object:

abstract class ObjectCache {
  Object getByKey(String key);
  setByKey(String key, Object value);
}

You discover that you want a string-specific version of this interface, so you create another interface:

abstract class StringCache {
  String getByKey(String key);
  setByKey(String key, String value);
}

Later, you decide you want a number-specific version of this interface… You get the idea.

Generic types can save you the trouble of creating all these interfaces. Instead, you can create a single interface that takes a type parameter:

abstract class Cache<T> {
  T getByKey(String key);
  setByKey(String key, T value);
}

In this code, T is the stand-in type. It’s a placeholder that you can think of as a type that a developer will define later.

Using Collection Literals

List and map literals can be parameterized. Parameterized literals are just like the literals you’ve already seen, except that you add <type> (for lists) or <keyType, valueType> (for maps) before the opening bracket. You might use parameterized literals when you want type warnings in checked mode. Here is example of using typed literals:

var names = <String>['Seth', 'Kathy', 'Lars'];
var pages = <String, String>{
    'index.html':'Homepage',
    'robots.txt':'Hints for web robots',
    'humans.txt':'We are people, not machines' };

Note

Map literals always have string keys, so their type is always <String, SomeType>.

Using Constructors

To specify one or more types when using a constructor, put the types in angle brackets (<…>) just after the class name. For example:

var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = new Set<String>.from(names);

The following code creates a map that has integer keys and values of type View:

var views = new Map<int, View>();

Generic Collections and the Types they Contain

Dart generic types are reified, which means that they carry their type information around at runtime. For example, you can test the type of a collection, even in production mode:

var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

However, the is expression checks the type of the collection only-not of the objects inside it. In production mode, a List<String> might have some non-string items in it. The solution is to either check each item’s type or wrap item-manipulation code in an exception handler (see the section called ?Exceptions?).

Note

In contrast, generics in Java use erasure, which means that generic type parameters are removed at runtime. In Java, you can test whether an object is a List, but you can’t test whether it’s a List<String>.

For more information about generics, see Optional Types in Dart.

Libraries and Visibility

The import, part, and library directives can help you create a modular and shareable code base. Libraries not only provide APIs, but are a unit of privacy: identifiers that start with an underscore (_) are visible only inside the library. Every Dart app is a library, even if it doesn’t use a library directive.

Libraries can be distributed using packages. See the section called pub: The Dart Package Manager for information about pub, a package manager included in the SDK.

Using Libraries

Use import to specify how a namespace from one library is used in the scope of another library.

For example, Dart web apps generally use the dart:html library, which they can import like this:

import 'dart:html';

The only required argument to import is a URI[1] specifying the library. For built-in libraries, the URI has the special dart: scheme. For other libraries, you can use a file system path or the package: scheme. The package: scheme specifies libraries provided by a package manager such as the pub tool. For example:

import 'dart:io';
import 'package:mylib/mylib.dart';
import 'package:utils/utils.dart';

Specifying a library prefix

If you import two libraries that have conflicting identifiers, then you can specify a prefix for one or both libraries. For example, if library1 and library2 both have an Element class, then you might have code like this:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
var element1 = new Element();      // Uses Element from lib1.
var element2 = new lib2.Element(); // Uses Element from lib2.

Importing only part of a library

If you want to use only part of a library, you can selectively import the library. For example:

import 'package:lib1/lib1.dart' show foo, bar; // Import only foo and bar.
import 'package:lib2/lib2.dart' hide foo;      // Import all names EXCEPT foo.

Implementing Libraries

Use library to name a library, and part to specify additional files in the library.

Note

You don’t have to use library in an app (a file that has a top-level main() function), but doing so lets you implement the app in multiple files.

Declaring a library

Use library identifier to specify the name of the current library:

library ballgame;   // Declare that this is a library named ballgame.

import 'dart:html'; // This app uses the HTML library.
// ...Code goes here...

Associating a file with a library

To add an implementation file, put part fileUri in the file that has the library statement, where fileUri is the path to the implementation file. Then in the implementation file, put part of identifier, where identifier is the name of the library. The following example uses part and part of to implement a library in three files.

The first file, ballgame.dart, declares the ballgame library, imports other libraries it needs, and specifies that ball.dart and util.dart are parts of this library:

library ballgame;

import 'dart:html';
// ...Other imports go here...

part 'ball.dart';
part 'util.dart';

// ...Code might go here...

The second file, ball.dart, implements part of the ballgame library:

part of ballgame;

// ...Code goes here...

The third file, util.dart, implements the rest of the ballgame library:

part of ballgame;

// ...Code goes here...

Re-exporting libraries

You can combine or repackage libraries by re-exporting part or all of them. For example, you might have a huge library that you implement as a set of smaller libraries. Or you might create a library that provides a subset of methods from another library.

// In french.dart:
library french;
hello() => print('Bonjour!');
goodbye() => print('Au Revoir!');

// In togo.dart:
library togo;
import 'french.dart';
export 'french.dart' show hello;

// In another .dart file:
import 'togo.dart';

void main() {
  hello();   //print bonjour
  goodbye(); //FAIL
}

Isolates

Modern web browsers, even on mobile platforms, run on multi-core CPUs. To take advantage of all those cores, developers traditionally use shared-memory threads running concurrently. However, shared-state concurrency is error prone and can lead to complicated code.

Instead of threads, all Dart code runs inside of isolates. Each isolate has its own memory heap, ensuring that no isolate’s state is accessible from any other isolate.

Learn more about isolates in the section called dart:isolate - Concurrency with Isolates.

Typedefs

In Dart, functions are objects, just like strings and numbers are objects. A typedef, or function-type alias, gives a function type a name that you can use when declaring fields and return types. A typedef retains type information when a function type is assigned to a variable.

Consider the following code, which does not use a typedef:

class SortedCollection {
  Function compare;

  SortedCollection(int f(Object a, Object b)) {
    compare = f;
  }
}

int sort(Object a, Object b) => ... ; // Initial, broken implementation.

main() {
  SortedCollection collection = new SortedCollection(sort);

  // All we know is that compare is a function, but what type of function?
  assert(collection.compare is Function);
}

Type information is lost when assigning f to compare. The type of f is (Object, Object) → int (where means returns), yet the type of compare is Function. If we change the code to use explicit names and retain type information, both developers and tools can use that information.

typedef int Compare(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

int sort(Object a, Object b) => ... ; // Initial, broken implementation.

main() {
  SortedCollection collection = new SortedCollection(sort);
  assert(collection.compare is Function);
  assert(collection.compare is Compare);
}

Note

Currently, typedefs are restricted to function types. We expect this to change.

Because typedefs are simply aliases, they offer a way to check the type of any function. For example:

typedef int Compare(int a, int b);

int sort(int a, int b) => a - b;

main() {
  assert(sort is Compare);  // True!
}

Metadata

Use metadata to give additional information about your code. A metadata annotation begins with the character @, followed by either a reference to a compile-time constant (such as deprecated) or a call to a constant constructor.

The meta package defines the API for two common annotations: @deprecated and @overrides. Here’s an example of using the @deprecated annotation:

import 'package:meta/meta.dart'; // Defines deprecated.

class Television {
  /// _Deprecated: Use [turnOn] instead._
  @deprecated      // Metadata; makes Dart Editor warn about using activate().
  void activate() {
    turnOn();
  }

  /// Turns the TV's power on.
  void turnOn() {
    print('on!');
  }
}

You can define your own metadata annotations. For example, here’s how the meta package defines the deprecated constant:

const deprecated = const _Deprecated();

class _Deprecated {
  const _Deprecated();
}

Here’s an example of defining a @todo annotation that takes two arguments:

library todo;

class todo {
  final String who;
  final String what;

  const todo(this.who, this.what);
}

And here’s an example of using that @todo annotation:

import 'todo.dart';

@todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}

Metadata can appear before a library, class, typedef, type parameter, constructor, factory, function, field, parameter, or variable declaration and before an import or export directive. In the future, you’ll be able to retrieve metadata at runtime using reflection.

Comments

Dart supports single-line comments, multi-line comments, and documentation comments.

Single-Line Comments

A single-line comment begins with //. Everything between // and the end of line is ignored by the Dart compiler.

main() {
  // TODO: refactor into an AbstractLlamaGreetingFactory?
  print('Welcome to my Llama farm!');
}

Multi-Line Comments

A multi-line comment begins with /* and ends with /. Everything between / and */ is ignored by the Dart compiler (unless the comment is a documentation comment; see the next section). Multi-line comments can nest.

main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = new Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

Documentation Comments

Documentation comments are multi-line or single-line comments that begin with /** or ///. Using /// on consecutive lines has the same effect as a multi-line doc comment.

Inside a documentation comment, the Dart compiler ignores all text unless it is enclosed in brackets. Using brackets, you can refer to classes, methods, fields, top-level variables, functions, and parameters. The names in brackets are resolved in the lexical scope of the documented program element.

Here is an example of documentation comments with references to other classes and arguments:

/**
 * The llama (Lama glama) is a domesticated South American
 * camelid, widely used as a meat and pack animal by Andean
 * cultures since pre-Hispanic times.
 */
class Llama {
  String name;

  /**
   * Feeds your llama [Food].
   *
   * The typical llama eats one bale of hay per week.
   */
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

In the generated documentation, [Food] becomes a link to the API docs for the Food class.

To parse Dart code and generate HTML documentation, you can use Dart Editor, which in turn uses the SDK’s dartdoc package. For an example of generated documentation, see the Dart API documentation.

OCaml

De programmeertaal OCaml.

Go

De programmeertaal Go.

Rust

De programmeertaal Rust.

Clojure

De programmeertaal Clojure.

Scheme

De programmeertaal Scheme.

Coloru

De programmeertaal Coloru.

Scala

De programmeertaal Scala.

Haskell

De programmeertaal Haskell.

XSL

De programmeertaal XSL.

Prolog

De programmeertaal Prolog.

Erlang

Inleiding

De programmeertaal Erlang is ontworpen door Ericsson.

De shell

De shell start je met:

erl
Erlang R14B02 (erts-5.8.3) [source] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.3  (abort with ^G)
1>
User switch command
 -->
 --> h
  c [nn]           - connect to job
  i [nn]           - interrupt job
  k [nn]           - kill job
  j                - list all jobs
  s [shell]        - start local shell
  r [node [shell]] - start remote shell
  q                - quit erlang
  ? | h            - this message
 -->

Met ^g kan je onderbreken. Bij de prompt die dan verschijnt kan je user switch commands gebruiken. Met h wordt een overzicht gegeven van de commando’s.

Bij ^C kan jee ook commando’s gebruiken:

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
       (v)ersion (k)ill (D)b-tables (d)istribution

Je kan bij de gewone prompt ook stoppen met q() of init:stop().

erl verstaat het gebruik van TAB. Type li gevolgd door TAB en je ziet dat dit uigebreid wordt tot lists:. Nog een TAB geeft een overzicht van alle methoden in de module lists.

Datatypes

Integer

123               gewone integer
1234567899999999  bigint

Getallen in een andere basis gaat ook:

2#1010
8#377

Karaktercodes krijg je zo:

$9
$A
$\n

Floats

Worden zo geschreven:

1e10

De bewerkingen zijn *, -, * en /. Voor de deling kan je / en div gebruiken. Het laatste is voor gehele getallen. Voor de rest heb je rem.

Bitbewerkingen

Deze zijn:

bsl  shift left
bsr  shift right
band en
bor  of
bxor exclusive of
bnot niet

Binaries

Stellen een aantal unsigned 8 bit waarden voor:

<<1, 2, 3 >>
<<"hallo", 78, 98>>

Atomen

Hebben altijd als eerste teken een kleine letter. De rest mag groot, underscore, cijfer of @ zijn.

punt
vlak
n66

Bij spaties moet je single quotes gebruiken.

'Dit is een atom'

De maximale lengte is 255.

Deze woorden zijn verboden als atom:

after and andalso band begin
bnot bor bsl bsr bxor case catch cond div
end fun if let not of or orelse query
receive rem try when xor

Tuples

{4, 5}
{punt, 6, 7}

Lists

Voorbeelden:

[]
[1, 2, 3]

Een element en een lijst samenvoegen doe je zo:

[1 | [2,3]] geeft [1, 2, 3]

Lijsten samenvoegen gaat ook:

[1,2] ++ [3,4] geeft [1, 2, 3, 4]

Er zijn BIF’s die head en tail van een lijst leveren:

hd([1,2,3]) geeft 1
tl([1,2,3]) geeft [2,3]
length([1,2,3,4]) geeft 4

Strings

Strings worden naar lijsten omgezet.

"ABC" geeft [65, 66, 67]

Pids, Poorten en referenties

Een pid krijg je terug als je een proces maakt.

<0.35.0>

Een poort is zoals een pid maar je kan ermee met niet-Erlang software communiceren.

#Port<1.6>

Referenties zijn unieke labels. Je maakt ze met make_ref().

#Ref<0.0.0.39>

Vergelijken

Doe je met:

==  rekenkundige gelijkheid
/=  rekenkundige ongelijkheid
<
>
=<
>=

Je hebt nog exacte gelijkheid en exacte ongelijkheid:

=:=
=/=

Hierbij worden de waarden als boom vergeleken.

Je kan ook samenstellingen maken:

and
or
not

Om te groeperen gebruik je de haken ( en ). Voor short-circuit testen heb je:

andalso
orelse

Je kan alles met alles vergelijken:

number < atom < reference < fun
< port < pid < tuple < list < bit string

Modules

Roep de functie op met modulenaam:

module:methode(5).

Dit is een module:

%% This is a simple Erlang module
-module(my_module).
-export([pie/0]).
pie() ->
  3.14.

Dit is nog een module.

-module(vb1).
-export([double/1, start/0]).
-vsn([1.0]).
-author("Leo Rutten").

double(X) ->
   2 * X.

start() ->
   G = double(5),
   io:format("getal is ~B~n", [G]).

En dit is de Makfile om te compileren en uit te voeren.

all: run

vb1.beam: vb1.erl
        erlc vb1.erl

run: vb1.beam
        erl -noshell -s vb1 start

clean:
        rm -vf *.dump
        rm -vf *.beam
        rm -vf *~

Variabelen en patronen

Beginnen met een hoofdletter. Als dummy variabele kan je _ gebruiken.

Het = teken werkt als patroonherkenning.

{A,B,C} = {1,2,3}

Functies en clauses

In functies kan je guards inbouwen.

oud_genoeg(X) when X >= 16 -> true;
oud_genoeg(_) -> false.

Meerdere guards mogen ook:

oud_genoeg(X) when X >= 16, X =< 116 -> true;
oud_genoeg(_) -> false.

De komma werkt hier als een andalso samenstelling. En de puntkomma werkt hier als een orelse samenstelling.

niet_oud_genoeg(X) when X < 16; X > 116 -> true;
niet_oud_genoeg(_) -> false.

Ingebouwde functies

Deze bevinden zich in de erlang module. Met de TAB toets kan je ze zichtbaar maken in erl.

1> erlang:
'!'/2                           '*'/2
'+'/1                           '+'/2
'++'/2                          '-'/1
'-'/2                           '--'/2
'/'/2                           '/='/2
'<'/2                           '=/='/2
'=:='/2                         '=<'/2
'=='/2                          '>'/2
'>='/2                          'and'/2
'band'/2                        'bnot'/1
'bor'/2                         'bsl'/2
'bsr'/2                         'bxor'/2
'div'/2                         'not'/1
'or'/2                          'rem'/2
'xor'/2                         abs/1
adler32/1                       adler32/2
adler32_combine/3               append/2
append_element/2                apply/2
apply/3                         atom_to_binary/2
atom_to_list/1                  await_proc_exit/3
binary_part/2                   binary_part/3
binary_to_atom/2                binary_to_existing_atom/2
binary_to_list/1                binary_to_list/3
binary_to_term/1                binary_to_term/2
bit_size/1                      bitstring_to_list/1
bump_reductions/1               byte_size/1
call_on_load_function/1         cancel_timer/1
check_process_code/2            concat_binary/1
crasher/6                       crc32/1
crc32/2                         crc32_combine/3
date/0                          decode_packet/3
delay_trap/2                    delete_module/1
demonitor/1                     demonitor/2
dexit/2                         dgroup_leader/2
disconnect_node/1               display/1
display_nl/0                    display_string/1
dist_exit/3                     dlink/1
dmonitor_node/3                 dmonitor_p/2
dsend/2                         dsend/3
dunlink/1                       element/2
erase/0                         erase/1
error/1                         error/2
exit/1                          exit/2
external_size/1                 finish_after_on_load/2
float/1                         float_to_list/1
flush_monitor_message/2         format_cpu_topology/1
fun_info/1                      fun_info/2
fun_to_list/1                   function_exported/3
garbage_collect/0               garbage_collect/1
garbage_collect_message_area/0  get/0
get/1                           get_cookie/0
get_keys/1                      get_module_info/1
get_module_info/2               get_stacktrace/0
group_leader/0                  group_leader/2
halt/0                          halt/1
hash/2                          hd/1
hibernate/3                     integer_to_list/1
integer_to_list/2               iolist_size/1
iolist_to_binary/1              is_alive/0
is_atom/1                       is_binary/1
is_bitstring/1                  is_boolean/1
is_builtin/3                    is_float/1
is_function/1                   is_function/2
is_integer/1                    is_list/1
is_number/1                     is_pid/1
is_port/1                       is_process_alive/1
is_record/2                     is_record/3
is_reference/1                  is_tuple/1
length/1                        link/1
list_to_atom/1                  list_to_binary/1
list_to_bitstring/1             list_to_existing_atom/1
list_to_float/1                 list_to_integer/1
list_to_integer/2               list_to_pid/1
list_to_tuple/1                 load_module/2
load_nif/2                      loaded/0
localtime/0                     localtime_to_universaltime/1
localtime_to_universaltime/2    make_fun/3
make_ref/0                      make_tuple/2
make_tuple/3                    match_spec_test/3
max/2                           md5/1
md5_final/1                     md5_init/0
md5_update/2                    memory/0
memory/1                        min/2
module_info/0                   module_info/1
module_loaded/1                 monitor/2
monitor_node/2                  monitor_node/3
nif_error/1                     nif_error/2
node/0                          node/1
nodes/0                         nodes/1
now/0                           open_port/2
phash/2                         phash2/1
phash2/2                        pid_to_list/1
port_call/2                     port_call/3
port_close/1                    port_command/2
port_command/3                  port_connect/2
port_control/3                  port_get_data/1
port_info/1                     port_info/2
port_set_data/2                 port_to_list/1
ports/0                         pre_loaded/0
process_display/2               process_flag/2
process_flag/3                  process_info/1
process_info/2                  processes/0
purge_module/1                  put/2
raise/3                         read_timer/1
ref_to_list/1                   register/2
registered/0                    resume_process/1
round/1                         self/0
send/2                          send/3
send_after/3                    send_nosuspend/2
send_nosuspend/3                seq_trace/2
seq_trace_info/1                seq_trace_print/1
seq_trace_print/2               set_cookie/2
set_cpu_topology/1              setelement/3
setnode/2                       setnode/3
size/1                          spawn/1
spawn/2                         spawn/3
spawn/4                         spawn_link/1
spawn_link/2                    spawn_link/3
spawn_link/4                    spawn_monitor/1
spawn_monitor/3                 spawn_opt/1
spawn_opt/2                     spawn_opt/3
spawn_opt/4                     spawn_opt/5
split_binary/2                  start_timer/3
statistics/1                    subtract/2
suspend_process/1               suspend_process/2
system_flag/2                   system_info/1
system_monitor/0                system_monitor/1
system_monitor/2                system_profile/0
system_profile/2                term_to_binary/1
term_to_binary/2                throw/1
time/0                          tl/1
trace/3                         trace_delivered/1
trace_info/2                    trace_pattern/2
trace_pattern/3                 trunc/1
tuple_size/1                    tuple_to_list/1
universaltime/0                 universaltime_to_localtime/1
unlink/1                        unregister/1
whereis/1                       yield/0

Case en if

Bij de if hoort een voorwaarde en aan actie. Een else bestaat niet; je moet een true gebruiken. Bij de voorwaarden mag je een komma of puntkomma gebruiken.

doe_test(X) ->
   if X>0  -> io:format("positief~n");
      true -> io:format("niet positief~n")
   end.

Dit is een voorbeeld met een case.

insert(X,[]) ->
   [X];
insert(X,Set) ->
   case lists:member(X,Set) of
      true  -> Set;
      false -> [X|Set]
   end.

Je kan bij de waarden ook guards bijvoegen.

beach(Temperature) ->
   case Temperature of
      {celsius, N} when N >= 20, N =< 45 ->
         'favorable';
      {kelvin, N} when N >= 293, N =< 318 ->
         'scientifically favorable';
      {fahrenheit, N} when N >= 68, N =< 113 ->
         'favorable in the US';
       _ ->
          'avoid beach'
   end.

Recursie

Het eerste voorbeeld gebruikt geen staartrecursie.

lengte([]) ->
   0;
lengte([_X | Rest]) ->
   1 + lengte(Rest).

Het tweede voorbeeld wel.

lengtetail(L) ->
   lengtetail(L, 0).

lengtetail([], Acc) ->
   Acc;
lengtetail([_X | Rest], Acc) ->
   L = 1 + Acc,
   lengtetail(Rest, L).

Funs

Een anonieme functie schrijf je zo:

F = fun(A, B) -> A + B end.

Hier is een voorbeeld waarin de anonieme functie als parameter wordt meegegeven.

map(_F, []) ->
   [];
map(F, [X|Rest]) ->
   [F(X) | map(F, Rest)].

map(fun(X) -> 2*X end, [1,2,3]).

Er zijn ingebouwde functies die anonymieme functies als parameter verwachten.

map/2
filter/2
foldr/3
foldl/3
all/2
any/2
dropwhile/2
takewhile/2
partition/2
flatten/1
flatlength/1
flatmap/2
merge/1
nth/2
nthtail/2
split/2

Exceptions, try en catch

Errors

Dit zijn de run-time fouten.

function_clause
case_clause
if_clause
badarg
undef
badarith
badfun
badarity

Er zijn 3 soorten exceptions: errors, throws en exits.

Je kan een programma zelf stilleggen met:

erlang:error(badarid).

Exits

exit/1 is een interne exit en exit/2 is een externe exit.

Throws

Dit is zoals Java en C++.

throws(F) ->
   try F() of
      _ -> ok
   catch
      Throw -> {throw, caught, Throw}
   end.

Catch

Dit is een alternatieve manier om exceptions op te vangen.

catch uitdrukking.

List comprehensions

Dit is een verkorte schrijfwijze op lijsten op te bouwen.

Lijst = [2*N || N <- [1,2,3,4]].
Lijst2 = [2*N || N <- [1,2,3,4,5,6,7], N rem 2 =:= 0].
[{A,B}||{A,B}<-[{7,3}, {6,3}, {8,4}, {9,4}],A rem B =:= 0].
[{X,Y}||X<-[1, 2, 3],Y<-[4,5,6]].

Datastructuren

Records

Records worden intern bijgehouden als tuples.

-record(punt, {x=0, y=0}).

doe_test(P = #punt{}) ->
   io:format("P is een punt~n"),
   io:format("   x is ~p~n", [P#punt.x]),
   io:format("   y is ~p~n", [P#punt.y]);
doe_test(P) ->
   io:format("P is geen punt~n").

P1 = #punt{x=5, y=7},
io:format("P1 ~p~n", [P1]),
doe_test(P1),
doe_test(56),

Proplist

Een proplist is een lijst van key/value tuples.

L = proplists:delete(2, [{1,"een"}, {2,"twee"}, {3,"drie"}]),
io:format("L is ~p~n", [L]),
io:format("2 in ~p~n", [proplists:is_defined(2, L)]),
io:format("3 in ~p~n", [proplists:is_defined(3, L)]).

Orddict

Een orddict is gelijkaardig. Een sleutel wordt slechts eenmaal opgeslagen.

O = orddict:new(),
O2 = orddict:append(10,"een", O),
O3 = orddict:store(2,"twee", O2),
io:format("O is ~p~n", [O3]).

General balanced tree

Dit is een gebalanceerde boom.

T = gb_trees:empty(),
io:format("T is ~p~n", [T]),
T2 = gb_trees:enter(10,"een", T),
T3 = gb_trees:enter(2,"twee", T2),
io:format("T3 is ~p~n", [T3]).

Verder bestaan er nog ordsets, sets, gb_sets en sofs. Voor grafen bestaan de modules digraph en digraph_utils. Er zijn ook nog de queue en de array modules.

Bitsyntax en bitstring comprehensions

Preprocessing en include


Processen

Zo start je een proces met een anonieme functie.

F = fun() -> 2+2 end.
spawn(F).

Of rechtstreeks:

spawn(fun()->io:format("hallo~n") end).

Meerdere processen met een list comprehension.

[spawn(fun()->io:format("hallo ~p~n", [X]) end) || X <- lists:seq(1,10)].

self() geeft de id van een proces terug.

Processen kunnen berichten versturen.

self() ! hallo.

Hier wordt een bericht naar zichzelf gestuurd. Met flush() kan je ze allemaal ophalen.

Met receive kan je berichten ontvangen. In het volgende voorbeeld stopt het proces telkens na het ontvangen van een bericht.

-module(conc).
-compile(export_all).

ontvanger() ->
   receive
      hallo ->
         io:format("hallo~n");
      bericht ->
         io:format("bericht~n");
      _ ->
         io:format("onbekend bericht~n")
   end.

main() ->
   io:format("start~n"),
   Pid = spawn(conc, ontvanger, []),
   Pid ! hallo,
   Pid2 = spawn(conc, ontvanger, []),
   Pid2 ! bericht,
   Pid3 = spawn(conc, ontvanger, []),
   Pid3 ! hello,
   io:format("einde~n"),
   erlang:halt().

Met recursie blijft de ontvanger draaien na het ontvangen van berichten. Door de staartrecursie goeit de stack niet.

-module(conc).
-compile(export_all).

ontvanger() ->
   receive
      hallo ->
         io:format("hallo~n");
      bericht ->
         io:format("bericht~n");
      _ ->
         io:format("onbekend bericht~n")
   end,
   ontvanger().

main() ->
   io:format("start~n"),
   Pid = spawn(conc, ontvanger, []),
   Pid ! hallo,
   %%Pid2 = spawn(conc, ontvanger, []),
   Pid ! bericht,
   %%Pid3 = spawn(conc, ontvanger, []),
   Pid ! hello,
   io:format("einde~n"),
   erlang:halt().

De volgende versie van koelkast is aangepast zodat de toestand kan bijgehouden. Dit is een lijst van alle objecten in de koelkast.

-module(koelkast).
-compile(export_all).

koelkast(Voedsellijst) ->
   io:format("koelkast start ~p~n", [Voedsellijst]),
   receive
      {Van, {plaats, Voedsel}} ->
         io:format("plaats ~p~n", [Voedsel]),
         Van ! {self(), ok},
         koelkast([Voedsel | Voedsellijst]);

      {Van, {neem, Voedsel}} ->
         io:format("neem ~p~n", [Voedsel]),
         case lists:member(Voedsel, Voedsellijst) of
            true ->
               Van ! {self(), {ok, Voedsel}},
               koelkast(lists:delete(Voedsel, Voedsellijst));
            false ->
               Van ! {self(), niet_gevonden},
               koelkast(Voedsellijst)
         end;

      stop ->
         io:format("stop~n"),
         ok;

      _ ->
         io:format("onbekend~n"),
         ok
   end.

antwoord() ->
   receive
      A -> io:format("antwoord ~p~n", [A])
   end.

main() ->
   io:format("start~n"),
   Pid = spawn(koelkast, koelkast, [[appel, tomaat]]),
   Pid ! {self(), {plaats, melk}},
   antwoord(),
   Pid ! {self(), {neem, tomaat}},
   antwoord(),
   Pid ! verkeerd,
   Pid ! stop,

   %% Deze sleep van 5 s is nodig omdat anders
   %% de halt te snel gebeurt.
   timer:sleep(5000),

   io:format("einde~n"),
   erlang:halt().

Fouten en processen

Traps

Monitors

Processen met naam

ETS tabellen

Elixir

Introduction

Welcome! In this tutorial we are going to show you how to get started with Elixir. We will start with how to install Elixir, how to use its interactive shell, and basic data types and operators. In later chapters, we are even going to discuss more advanced subjects such as macros, protocols and other features provided by Elixir.

To see Elixir in action, check out these introductory screencasts by Dave Thomas. The first one, Nine Minutes of Elixir, provides a brief tour of the language. The second one is a 30-minute introduction to Elixir that’ll help you get started with writing your first functions and creating your first processes in Elixir. Be sure to follow the next section of this guide to install Elixir on your machine and then follow along with the videos.

PeepCode also has a two hour video with José Valim called Meet Elixir.

Keep in mind that Elixir is still in development so if at any point you receive an error message and you are not sure how to proceed, please let us know in the issues tracker. Having explanative and consistent error messages is one of the many features we aim for Elixir.

Installation

The only prerequisite for Elixir is Erlang, version R16B or later. You can find the source code for Erlang here or use one of the precompiled packages.

For Windows developers, we recommend the precompiled package. Those on the UNIX platform can probably get Erlang installed via one of the many package management tools.

After Erlang is installed, you should be able to open up the command line (or command prompt) and check the Erlang version by typing erl. You will see some information as follows:

Erlang R16B (erts-5.10.1) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Notice that depending on how you installed Erlang, it will not add Erlang binaries to your environment. Be sure to have Erlang binaries in your PATH, otherwise Elixir won’t work!

After Erlang is up and running, it is time to install Elixir. You can do that via Distributions, Precompiled package or Compiling from Source.

Distributions

This tutorial requires Elixir v0.9.0 or later and it may be available in some distributions:

  • Homebrew for Mac OS X

    • Since Elixir requires Erlang R16B, first call brew tap homebrew/versions and then brew install erlang-r16

    • If you have a previous Erlang version installed, unlink it with brew uninstall erlang and link the new one with brew link erlang-r16

    • Update your homebrew to latest: brew update

    • Install Elixir: brew install elixir

  • Fedora 17+ and Fedora Rawhide: sudo yum -y install elixir

  • Arch Linux : Elixir is available on AUR via yaourt -S elixir

If you don’t use any of the distributions above, don’t worry, we also provide a precompiled package!

Compiling from source (Unix and MinGW)

You can download and compile Elixir in few steps. You can get the latest stable release here, unpack it and then run make inside the unpacked directory. After that, you are ready to run the elixir and iex commands from the bin directory. It is recommended that you add Elixir’s bin path to your PATH environment variable to ease development:

$ export PATH="$PATH:/path/to/elixir/bin"

In case you are feeling a bit more adventurous, you can also compile from master:

$ git clone https://github.com/elixir-lang/elixir.git
$ cd elixir
$ make test

If the tests pass, you are ready to go. Otherwise, feel free to open an issue in the issues tracker on Github.

Precompiled package

Elixir provides a precompiled package for every release. Precompiled packages are the best option if you are developing on Windows.

After downloading and unzip-ing the package, you are ready to run the elixir and iex commands from the bin directory. It is recommended that you also add Elixir’s bin path to your PATH environment variable to ease development.

Interactive mode

When you install Elixir, you will have three new executables: iex, elixir and elixirc. If you compiled Elixir from source or you are using a packaged version, you can find these inside the bin directory.

For now, let’s start by running iex which stands for Interactive Elixir. In interactive mode, we can type any Elixir expression and get its result straight away. Let’s warm up with some basic arithmetic expressions:

iex> 1 + 1
2
iex> 10 - 5
5
iex> 10 / 2
5.0

Notice 10 / 2 returned a float 5.0 instead of an integer. This is expected. In Elixir the operator / always returns a float. In case you want to do integer division or get the division remainder, you can invoke the div and rem functions:

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

In the example above, we called two functions called div and rem. Notice that parentheses are not required in order to invoke a function. We are going to discuss more about it later. Let’s move forward and see which other data types we have in Elixir:

Basic types

Some basic types are:

iex> 1          # integer
iex> 0x1F       # integer
iex> 1.0        # float
iex> :atom      # atom / symbol
iex> {1,2,3}    # tuple
iex> [1,2,3]    # list
iex> <<1,2,3>>  # binary

Elixir by default provides many functions to work on those types:

iex> size { 1, 2, 3 }
3

iex> length [ 1, 2, 3 ]
3

Elixir also provides functions (note the dot between the variable and arguments when calling a function):

# function
iex> x = fn(a, b) -> a + b end
#Fun<erl_eval.12.111823515>
iex> x.(1, 2)
3

Elixir also supports strings and they are all encoded in UTF-8:

iex> "hellö"
"hellö"

Strings support interpolation too:

iex> name = "world"
iex> "hello #{name}"
"hello world"

At the end of the day, strings are nothing more than binaries. We can check it using the is_binary function:

iex> is_binary("hello")
true

Note a single-quoted expression in Elixir is a char list and it is not the same as a double-quoted one:

iex> is_binary('hello')
false

We will go into more details about char lists in the next chapter. Finally, Elixir also provides true and false as booleans:

iex> true
true
iex> is_boolean false
true

Booleans are represented internally as atoms:

iex> is_atom(true)
true

Elixir also provides Port, References and PIDs as data types (usually used in process communication) but they are out of the scope of a getting started tutorial. For now, let’s take a look at the basic operators in Elixir before we move on to the next chapter.

Operators

As we saw earlier, Elixir provides +, -, *, / as arithmetic operators.

Elixir also provides ++ and -- to manipulate lists:

iex> [1,2,3] ++ [4,5,6]
[1,2,3,4,5,6]
iex> [1,2,3] -- [2]
[1,3]

String concatenation is done via <>:

iex> "foo" <> "bar"
"foobar"

Elixir also provides three boolean operators: or, and and not. These operators are strict in the sense they expect a boolean (true or false) as their first argument:

iex> true and true
true
iex> false or is_atom(:example)
true

Giving a non-boolean will raise an exception:

iex> 1 and true
** (ArgumentError) argument error

or and and are short-circuit operators. They just execute the right side in case the left side is not enough to determine the result:

iex> false and error("This error will never be raised")
false

iex> true or error("This error will never be raised")
true

Note: If you are an Erlang developer, and and or in Elixir actually map to the andalso and orelse operators in Erlang.

Besides those boolean operators, Elixir also provides ||, && and ! which accept arguments of any type. For these operators, all values except false and nil will evaluate to true:

# or
iex> 1 || true
1
iex> false || 11
11

# and
iex> nil && 13
nil
iex> true && 17
17

# !
iex> !true
false
iex> !1
false
iex> !nil
true

As a rule of thumb, use and, or and not when you are expecting a booleans. If any of the arguments are non-booleans, use &&, || and !.

Elixir also provides ==, !=, ===, !==, <=, >=, < and > as comparison operators:

iex> 1 == 1
true
iex> 1 != 2
true
iex> 1 < 2
true

The difference between == and === is that the latter is more strict when comparing integers and floats:

iex> 1 == 1.0
true
iex> 1 === 1.0
false

In Elixir, we can compare two different data types:

iex> 1 < :atom
true

The reason we can compare different data types is for pragmatism. Sorting algorithms don’t need to worry about different data types in order to sort. The overall sorting order is defined below:

number < atom < reference < functions < port < pid < tuple < list < bit string

You actually don’t need to memorize this ordering, but it is important just to know an order exists.

Well, that is it for the introduction. In the next chapter, we are going to discuss some basic functions, data types conversions and a bit of control-flow.

Diving in

In this chapter we’ll go a bit deeper into the basic data-types, learn some control flow mechanisms and talk about anonymous functions.

Lists and tuples

Elixir provides both lists and tuples:

iex> is_list [1,2,3]
true
iex> is_tuple {1,2,3}
true

While both are used to store items, they differ on how those items are stored in memory. Lists are implemented as linked lists (where each item in the list points to the next item) while tuples are stored contiguously in memory.

This means that accessing a tuple element is very fast (constant time) and can be achieved using the elem function:

iex> elem { :a, :b, :c }, 0
:a

On the other hand, updating a tuple is expensive as it needs to duplicate the tuple contents in memory. Updating a tuple can be done with the set_elem function:

iex> set_elem { :a, :b, :c }, 0, :d
{:d,:b,:c}

Note: If you are an Erlang developer, you will notice that we used the elem and set_elem functions instead of Erlang’s element and setelement. The reason for this choice is that Elixir attempts to normalize Erlang API’s to always receive the subject of the function as the first argument and employ zero-based access.

Since updating a tuple is expensive, when we want to add or remove elements, we use lists. Since lists are linked, it means accessing the first element of the list is very cheap. Accessing the n-th element, however, will require the algorithm to pass through n-1 nodes before reaching the n-th. We can access the head of the list as follows:

# Match the head and tail of the list
iex> [head | tail] = [1,2,3]
[1,2,3]
iex> head
1
iex> tail
[2,3]

# Put the head and the tail back together
iex> [head | tail]
[1,2,3]
iex> length [head | tail]
3

In the example above, we have matched the head of the list to the variable head and the tail of the list to the variable tail. This is called pattern matching. We can also pattern match tuples:

iex> { a, b, c } = { :hello, "world", 42 }
{ :hello, "world", 42 }
iex> a
:hello
iex> b
"world"

A pattern match will error in case the sides can’t match. This is, for example, the case when the tuples have different sizes:

iex> { a, b, c } = { :hello, "world" }
** (MatchError) no match of right hand side value: {:hello,"world"}

And also when comparing different types:

iex> { a, b, c } = [:hello, "world", '!']
** (MatchError) no match of right hand side value: [:hello,"world"]

More interestingly, we can match on specific values. The example below asserts that the left side will only match the right side in case that the right side is a tuple that starts with the atom :ok as a key:

iex> { :ok, result } = { :ok, 13 }
{:ok,13}
iex> result
13

iex> { :ok, result } = { :error, :oops }
** (MatchError) no match of right hand side value: {:error,:oops}

Pattern matching allows developers to easily destruct data types such as tuples and lists. As we will see in following chapters, it is one of the foundations of recursion in Elixir. However, in case you can’t wait to iterate through and manipulate your lists, the Enum module provides several helpers to manipulate lists (and other enumerables in general) while the List module provides several helpers specific to lists:

iex> Enum.at [1,2,3], 0
1
iex> List.flatten [1,[2],3]
[1,2,3]

Keyword lists

Elixir also provides a special syntax to create a list of keywords. They can be created as follows:

iex> [a: 1, b: 2]
[a: 1, b: 2]

Keyword lists are nothing more than a list of two element tuples where the first element of the tuple is an atom:

iex> [head | tail] = [a: 1, b: 2]
[a: 1, b: 2]
iex> head
{ :a, 1 }

The Keyword module contains several functions that allow you to manipulate a keyword list ignoring duplicated entries or not. For example:

iex> keywords = [foo: 1, bar: 2, foo: 3]
iex> Keyword.get keywords, :foo
1
iex> Keyword.get_values keywords, :foo
[1,3]

Since keyword lists are very frequently passed as arguments, they do not require brackets when given as the last argument in a function call. For instance, the examples below are valid and equivalent:

iex> if(2 + 2 == 4, [do: "OK"])
"OK"
iex> if(2 + 2 == 4, do: "OK")
"OK"
iex> if 2 + 2 == 4, do: "OK"
"OK"

Strings and char lists

In Elixir, a double-quoted string is not the same as a single-quoted one:

iex> "hello" == 'hello'
false
iex> is_binary "hello"
true
iex> is_list 'hello'
true

As shown above, double-quoted returns a binary while single-quoted returns a list. In fact, both double-quoted and single-quoted representations are just a shorter representation of binaries and lists respectively. Given that ?a in Elixir returns the ASCII integer for the letter a, we could also write:

iex> <<?a, ?b, ?c>>
"abc"
iex> [?a, ?b, ?c]
'abc'

In such cases, Elixir detects that all characters in the binary and in the list are printable and returns the quoted representation. However, adding a non-printable character forces them to be printed differently:

iex> <<?a, ?b, ?c, 1>>
<<97,98,99,1>>

iex> [?a, ?b, ?c, 1]
[97,98,99,1]

Since lists are implemented as linked lists, it means a char list usually takes a lot of space in memory (one word for each character and another word to point to the next character). For this reason, double-quoted is preferred unless you want to explicitly iterate over a char list (which is sometimes common when interfacing with Erlang code from Elixir).

Although a bit more verbose, it is also possible to do head/tail style pattern matching with binaries. A binary is made up of a number of parts which must be tagged with their type. Most of the time, Elixir will figure out the part’s type and won’t require any work from you:

iex> <<102, "oo">>
"foo"

In the example, we have two parts: the first is an integer and the second is a binary. If we use an Elixir expression, Elixir will default its type to an integer:

iex> rest = "oo"
iex> <<102, rest>>
** (ArgumentError) argument error

In the example above, since we haven’t specified a type for rest, Elixir expected an integer but we passed a binary, resulting in ArgumentError. We can solve this by explicitly tagging it as a binary:

iex> <<102, rest :: binary>>

The type can be integer, float, binary, bytes, bitstring, bits, utf8, utf16 or utf32. We can pass endianness and signedness too, when passing more than one option, we use a list:

iex> <<102, rest :: [binary, signed]>>

Not only that, we can also specify the bit size for each part:

iex> <<102, rest :: size(16)>> = "foo"
"foo"
iex> <<102, rest :: size(32)>> = "foo"
** (MatchError) no match of right hand side value: "foo"

In the example above, the first expression matches because the right string “foo” takes 24 bits and we are matching against a binary of 24 bits as well, 8 of which are taken by the integer 102 and the remaining 16 bits are specified on the rest. On the second example, we expect a rest with size 32, which won’t match.

If at any moment, you would like to match the top of a binary against any other binary without caring about the size of the rest, you can use binary (which has an unspecified size):

iex> <<102, rest :: binary>> = "foobar"
"foobar"
iex> rest
"oobar"

This is equivalent to the head and tail pattern matching we saw in lists. There is much more to binaries and pattern matching in Elixir that allows great flexibility when working with such structures, but they are beyond the scope of a getting started guide.

UTF-8 Strings

A String in Elixir is a binary which is encoded in UTF-8. For example, the string “é” is a UTF-8 binary containing two bytes:

iex> string = "é"
"é"
iex> size(string)
2

In order to easily manipulate strings, Elixir provides a String module:

# returns the number of bytes
iex> size "héllò"
7

# returns the number of characters as perceived by humans
iex> String.length "héllò"
5

Note: to retrieve the number of elements in a data structure, you will use a function named length or size. Their usage is not arbitrary. The first is used when the number of elements needs to be calculated. For example, calling length(list) will iterate the whole list to find the number of elements in that list. size is the opposite, it means the value is pre-calculated and stored somewhere and therefore retrieving its value is a cheap operation. That said, we use size to get the size of a binary, which is cheap, but retrieving the number of unicode characters uses String.length, since the whole string needs to be iterated.

Each character in the string “héllò” above is a Unicode codepoint. One may use String.codepoints to split a string into smaller strings representing its codepoints:

iex> String.codepoints "héllò"
["h", "é", "l", "l", "ó"]

The Unicode standard assigns an integer value to each character. Elixir allows a developer to retrieve or insert a character based on its integer codepoint value as well:

# Gettng the integer codepoint
iex> ?h
104
iex> ?é
233

# Inserting a codepoint based on its hexadecimal value
iex> "h\xE9ll\xF2"
"héllò"

UTF-8 also plays nicely with pattern matching. In the exemple below, we are extracting the first UTF-8 codepoint of a String and assigning the rest of the string to the variable rest:

iex> << eacute :: utf8, rest :: binary >> = "épa"
"épa"
iex> eacute
233
iex> << eacute :: utf8 >>
"é"
iex> rest
"pa"

In general, you will find working with binaries and strings in Elixir a breeze. Whenever you want to work with raw binaries, one can use Erlang’s binary module, and use Elixir’s String module when you want to work on strings, which are simply UTF-8 encoded binaries. Finally, there are a bunch of conversion functions, that allows you to convert a binary to integer, atom and vice-versa in the Kernel module.

Blocks

One of the first control flow constructs we usually learn is the conditional if. In Elixir, we could write if as follow:

iex> if true, do: 1 + 2
3

The if expression can also be written using the block syntax:

iex> if true do
...>   a = 1 + 2
...>   a + 10
...> end
13

You can think of do/end blocks as a convenience for passing a group of expressions to do:. It is exactly the same as:

iex> if true, do: (
...>   a = 1 + 2
...>   a + 10
...> )
13

We can pass an else clause in the block syntax:

if false do
  :this
else
  :that
end

It is important to notice that do/end always binds to the farthest function call. For example, the following expression:

is_number if true do
  1 + 2
end

Would be parsed as:

is_number(if true) do
  1 + 2
end

Which is not what we want since do is binding to the farthest function call, in this case is_number. Adding explicit parenthesis is enough to resolve the ambiguity:

is_number(if true do
  1 + 2
end)

Control flow structures

In this section we’ll describe Elixir’s main control flow structures.

Revisiting pattern matching

At the beginning of this chapter we have seen some pattern matching examples:

iex> [h | t] = [1,2,3]
[1, 2, 3]
iex> h
1
iex> t
[2, 3]

In Elixir, = is not an assignment operator as in programming languages like Java, Ruby, Python, etc. = is actually a match operator which will check if the expressions on both left and right side match.

Many control-flow structures in Elixir rely extensively on pattern matching and the ability for different clauses too match. In some cases, you may want to match against the value of a variable, which can be achieved by with the ^ operator:

iex> x = 1
1
iex> ^x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2
iex> x = 2
2

In Elixir, it is a common practice to assign a value to underscore _ if we don’t intend to use it. For example, if only the head of the list matters to us, we can assign the tail to underscore:

iex> [h | _] = [1,2,3]
[1, 2, 3]
iex> h
1

The variable _ in Elixir is special in that it can never be read from. Trying to read from it gives an unbound variable error:

iex> _
** (ErlangError) erlang error {:unbound_var, :_}

Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:

iex> length([1,[2],3]) = 3
** (ErlangError) erlang error :illegal_pattern

Case

A case allows us to compare a value against many patterns until we find a matching one:

case { 1, 2, 3 } do
  { 4, 5, 6 } ->
    "This won't match"
  { 1, x, 3 } ->
    "This will match and assign x to 2"
  _ ->
    "This will match any value"
end

As in the = operator, any assigned variable will be overridden in the match clause. If you want to pattern match against a variable, you need to use the ^ operator:

x = 1
case 10 do
  ^x -> "Won't match"
  _  -> "Will match"
end

Each match clause also supports special conditions specified via guards:

case { 1, 2, 3 } do
  { 4, 5, 6 } ->
    "This won't match"
  { 1, x, 3 } when x > 0 ->
    "This will match and assign x"
  _ ->
    "No match"
end

In the example above, the second clause will only match when x is positive. The Erlang VM only allows a limited set of expressions as guards:

  • comparison operators (==, !=, ===, !==, >, <, <=, >=);

  • boolean operators (and, or) and negation operators (not, !);

  • arithmetic operators (+, -, *, /);

  • <> and ++ as long as the left side is a literal;

  • the in operator;

  • all the following type check functions:

    • is_atom/1

    • is_binary/1

    • is_bitstring/1

    • is_boolean/1

    • is_float/1

    • is_function/1

    • is_function/2

    • is_integer/1

    • is_list/1

    • is_number/1

    • is_pid/1

    • is_port/1

    • is_record/2

    • is_record/3

    • is_reference/1

    • is_tuple/1

    • is_exception/1

  • plus these functions:

    • abs(Number)

    • bit_size(Bitstring)

    • byte_size(Bitstring)

    • div(Number, Number)

    • elem(Tuple, n)

    • float(Term)

    • hd(List)

    • length(List)

    • node()

    • node(Pid|Ref|Port)

    • rem(Number, Number)

    • round(Number)

    • self()

    • size(Tuple|Bitstring)

    • tl(List)

    • trunc(Number)

    • tuple_size(Tuple)

Many independent guard clauses can also be given at the same time. For example, consider a function that checks if the first element of a tuple or a list is zero. It could be written as:

def first_is_zero?(tuple_or_list) when
  elem(tuple_or_list, 1) == 0 or hd(tuple_or_list) == 0 do
  true
end

However, the example above will always fail. If the argument is a list, calling elem on a list will raise an error. If the element is a tuple, calling hd on a tuple will also raise an error. To fix this, we can rewrite it to become two different clauses:

def first_is_zero?(tuple_or_list)
    when elem(tuple_or_list, 1) == 0
    when hd(tuple_or_list) == 0 do
  true
end

In such cases, if there is an error in one of the guards, it won’t affect the next one.

Functions

In Elixir, anonymous functions can accept many clauses and guards, similar to the case mechanism we have just seen:

f = fn
  x, y when x > 0 -> x + y
  x, y -> x * y
end

f.(1, 3)  #=> 4
f.(-1, 3) #=> -3

As Elixir is an immutable language, the binding of the function is also immutable. This means that setting a variable inside the function does not affect its outer scope:

x = 1
(fn -> x = 2 end).()
x #=> 1

Receive

This next control-flow mechanism is essential to Elixir’s and Erlang’s actor mechanism. In Elixir, the code is run in separate processes that exchange messages between them. Those processes are not Operating System processes (they are actually quite light-weight) but are called so since they do not share state with each other.

In order to exchange messages, each process has a mailbox where the received messages are stored. The receive mechanism allows us to go through this mailbox searching for a message that matches the given pattern. Here is an example that uses the arrow operator <- to send a message to the current process and then collects this message from its mailbox:

# Get the current process id
iex> current_pid = self

# Spawn another process that will send a message to current_pid
iex> spawn fn ->
  current_pid <- { :hello, self }
end
<0.36.0>


# Collect the message
iex> receive do
...>   { :hello, pid } ->
...>     IO.puts "Hello from #{inspect(pid)}"
...> end
Hello from <0.36.0>

You may not see exactly <0.36.0> back, but something similar. If there are no messages in the mailbox, the current process will hang until a matching message arrives unless an after clause is given:

iex> receive do
...>   :waiting ->
...>     IO.puts "This may never come"
...> after
...>   1000 -> # 1 second
...>     IO.puts "Too late"
...> end
Too late

Notice we spawned a new process using the spawn function passing another function as argument. We will talk more about those processes and even how to exchange messages in between different nodes in a later chapter.

Try

try in Elixir is used to catch values that are thrown. Let’s start with an example:

iex> try do
...>   throw 13
...> catch
...>   number -> number
...> end
13

try/catch is a control-flow mechanism, useful in rare situations where your code has complex exit strategies and it is easier to throw a value back up in the stack. try also supports guards in catch and an after clause that is invoked regardless of whether or not the value was caught:

iex> try do
...>   throw 13
...> catch
...>   nan when not is_number(nan) -> nan
...> after
...>   IO.puts "Didn't catch"
...> end
Didn't catch
** throw 13
    erl_eval:expr/3

Notice that a thrown value which hasn’t been caught halts the software. For this reason, Elixir considers such clauses unsafe (since they may or may not fail) and does not allow variables defined inside try/catch/after to be accessed from the outer scope:

iex> try do
...>   new_var = 1
...> catch
...>   value -> value
...> end
1
iex> new_var
** error :undef

The common strategy is to explicitly return all arguments from try:

{ x, y } = try do
  x = calculate_some_value()
  y = some_other_value()
  { x, y }
catch
  _ -> { nil, nil }
end

x #=> returns the value of x or nil for failures

If and Unless

Besides the four main control-flow structures above, Elixir provides some extra control-flow structures to help on our daily work. For example, if and unless:

iex> if true do
iex>   "This works!"
iex> end
"This works!"

iex> unless true do
iex>   "This will never be seen"
iex> end
nil

Remember that do/end blocks in Elixir are simply a shortcut to the keyword notation. So one could also write:

iex> if true, do: "This works!"
"This works!"

Or even more complex examples like:

# This is equivalent...
if false, do: 1 + 2, else: 10 + 3

# ... to this
if false do
  1 + 2
else
  10 + 3
end

In Elixir, all values except false and nil evaluate to true. Therefore there is no need to explicitly convert the if argument to a boolean. If you want to check if one of many conditions are true, you can use the cond macro.

Cond

Whenever you want to check for many conditions at the same time, Elixir allows developers to use cond insted of nesting many if expressions:

cond do
  2 + 2 == 5 ->
    "This will never match"
  2 * 2 == 3 ->
    "Nor this"
  1 + 1 == 2 ->
    "But this will"
end

If none of the conditions return true, an error will be raised. For this reason, it is common to see a last condition equal to true, which will always match:

cond do
  2 + 2 == 5 ->
    "This will never match"
  2 * 2 == 3 ->
    "Nor this"
  true ->
    "This will always match (equivalent to else)"
end

Built-in functions

Elixir ships with many built-in functions automatically available in the current scope. In addition to the control flow expressions seen above, Elixir also adds: elem and set_elem to read and set values in tuples, inspect that returns the representation of a given data type as a binary, and many others. All of these functions imported by default are available in Kernel and Elixir special forms are available in Kernel.SpecialForms.

All of these functions and control flow expressions are essential for building Elixir programs. In some cases though, one may need to use functions available from Erlang, let’s see how.

Calling Erlang functions

One of Elixir’s assets is easy integration with the existing Erlang ecosystem. Erlang ships with a group of libraries called OTP (Open Telecom Platform). Besides being a standard library, OTP provides several facilities to build OTP applications with supervisors that are robust, distributed and fault-tolerant.

Since an Erlang module is nothing more than an atom, invoking those libraries from Elixir is quite straight-forward. For example, we can call the function flatten from the module lists or interact with the math module as follows:

iex> :lists.flatten [1,[2],3]
[1,2,3]
iex> :math.sin :math.pi
1.2246467991473532e-16

Erlang’s OTP is very well documented and we will learn more about building OTP applications in the Mix chapters:

That’s all for now. The next chapter will discuss how to organize our code into modules so it can be easily reused between different applications.

Modules

In Elixir, you can group several functions into a module. In the previous chapter, for example, we invoked functions from the module List:

iex> List.flatten [1,[2],3]
[1, 2, 3]

In order to create our own modules in Elixir, all we have to do is to call the defmodule function and use def to define our functions:

iex> defmodule Math do
...>   def sum(a, b) do
...>     a + b
...>   end
...> end

iex> Math.sum(1, 2)
3

Before diving into modules, let’s first have a brief overview about compilation.

Compilation

Most of the time it is convenient to write modules into files so they can be compiled and reused. Let’s assume we have a file named math.ex with the following contents:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

This file can be compiled using elixirc (remember, if you installed Elixir from a package or compiled it, elixirc will be inside the bin directory):

elixirc math.ex

This will generate a file named Elixir.Math.beam containing the bytecode for the defined module. Now, if we start iex again, our module definition will be available (considering iex is being started in the same directory the bytecode file is):

iex> Math.sum(1, 2)
3

Elixir projects are usually organized into three directories:

  • ebin - contains the compiled bytecode

  • lib - contains elixir code (usually .ex files)

  • test - contains tests (usually .exs files)

Whenever interacting with an existing library, you may need to explicitly tell Elixir to look for bytecode in the ebin directory:

iex -pa ebin

Where -pa stands for path append. The same option can also be passed to elixir and elixirc executables. You can execute elixir and elixirc without arguments to get a list of options.

Scripted mode

In addition to the Elixir file .ex, Elixir also supports .exs files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. .ex files are meant to be compiled while .exs files are used for scripting, without the need for compilation. For instance, one can create a file called math.exs:

defmodule Math do
  def sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)

And execute it as:

elixir math.exs

The file will be compiled in memory and executed, printing 3 as the result. No bytecode file will be created.

Functions and private functions

Inside a module, we can define functions with def and private functions with defp. A function defined with def is available to be invoked from other modules while a private function can only be invoked locally.

defmodule Math do
  def sum(a, b) do
    do_sum(a, b)
  end

  defp do_sum(a, b) do
    a + b
  end
end

Math.sum(1, 2)    #=> 3
Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches. Here is the implementation of a function that checks if the given number is zero or not:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_number(x) do
    false
  end
end

Math.zero?(0)  #=> true
Math.zero?(1)  #=> false

Math.zero?([1,2,3])
#=> ** (FunctionClauseError)

Giving an argument that does not match any of the clauses raises an error.

Named functions also support default arguments:

defmodule Concat do
  def join(a, b, sep // " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

Any expression is allowed to serve as a default value, but it won’t be evaluated during the function definition; it will simply be stored for later use. Every time the function is invoked and any of its default values have to be used, the expression for that default value will be evaluated:

defmodule DefaultTest do
  def dowork(x // IO.puts "hello") do
    x
  end
end

iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
hello
:ok

If a function with default values has multiple clauses, it is recommended to create a separate clause without an actual body, just for declaring defaults:

defmodule Concat do
  def join(a, b // nil, sep // " ")

  def join(a, b, _sep) when nil?(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

When using default values, one must be careful to avoid overlapping function definitions. Consider the following example:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep // " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

If we save the code above in a file named “concat.ex” and compile it, Elixir will emit the following warning:

test_funs.ex:7: this clause cannot match because a previous clause at line 2 always matches

The compiler is telling us that invoking the join function with two arguments will always choose the first definition of join whereas the second one will only be invoked when three arguments are passed:

$ iex test_funs.ex

iex> Concat.join "Hello", "world"
***First join
"Helloworld"

iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

Recursion

Due to immutability, loops in Elixir (and in functional programming languages) are written differently from conventional imperative languages. For example, in an imperative language, one would write:

for(i = 0; i < array.length; i++) {
  array[i] = array[i] * 2
}

In the example above, we are mutating the array which is not possible in Elixir. Therefore, functional languages rely on recursion: a function is called recursively until a condition is reached. Consider the example below that manually sums all the items in the list:

defmodule Math do
  def sum_list([h|t], acc) do
    sum_list(t, h + acc)
  end

  def sum_list([], acc) do
    acc
  end
end

Math.sum_list([1,2,3], 0) #=> 6

In the example above, we invoke sum_list giving a list [1,2,3] and the initial value 0 as arguments. When a function has many clauses, we will try each clause until we find one that matches according to the pattern matching rules. In this case, the list [1,2,3] matches against [h|t] which assigns h = 1 and t = [2,3] while acc is set to 0.

Then, we add the head of the list to the accumulator h + acc and call sum_list again, recursively, passing the tail of the list as argument. The tail will once again match [h|t] until the list is empty, as seen below:

sum_list [1,2,3], 0
sum_list [2,3], 1
sum_list [3], 3
sum_list [], 6

When the list is empty, it will match the final clause which returns the final result of 6. In imperative languages, such implementation would usually fail for large lists because the stack (in which our execution path is kept) would grow until it reaches a limit. Elixir, however, does last call optimization in which the stack does not grow when a function exits by calling another function.

Recursion and last call optimization are an important part of Elixir and are commonly used to create loops, especially in cases where a process needs to wait and respond to messages (using the receive macro we saw in the previous chapter). However, recursion as above is rarely used to manipulate lists, since the Enum module already abstracts such use cases. For instance, the example above could be simply written as:

Enum.reduce([1,2,3], 0, fn(x, acc) -> x + acc end)

Directives

In order to facilitate software reuse, Elixir supports three directives. As we are going to see below, they are called directives because they have lexical scope.

alias

alias allows you to setup aliases for any given module name. Imagine our Math module has a special list for doing math specific operations:

defmodule Math do
  alias Math.List, as: List
end

From now on, any reference to List will automatically expand to Math.List. In case one wants to access the original List, it can be done by accessing the module via Elixir:

List.values             #=> uses Math.List.values
Elixir.List.values      #=> uses List.values
Elixir.Math.List.values #=> uses Math.List.values

Note: All modules defined in Elixir are defined inside a main Elixir namespace. However, for convenience, you can omit the Elixir main namespace.

Calling alias without an as option sets the alias automatically to the last part of the module name, for example:

alias Math.List

Is the same as:

alias Math.List, as: List

Notice that alias is lexically scoped, which allows you to set aliases inside specific functions:

defmodule Math do
  def add(a, b) do
    alias Math.List
    # ...
  end

  def minus(a, b) do
    # ...
  end
end

In the example above, since we are invoking alias inside the function add, the alias will just be valid inside the function add. minus won’t be affected at all.

require

In general, a module does not need to be required before usage, except if we want to use the macros available in that module. For instance, suppose we created our own my_if implementation in a module named MyMacros. If we want to invoke it, we need to first explicitly require MyMacros:

defmodule Math do
  require MyMacros
  MyMacros.my_if do_something, it_works
end

An attempt to call a macro that was not loaded will raise an error. Note that like the alias directive, require is also lexically scoped. We will talk more about macros in chapter 5.

import

We use import whenever we want to easily access functions or macros from other modules without using the qualified name. For instance, if we want to use the duplicate function from List several times in a module and we don’t want to always type List.duplicate, we can simply import it:

defmodule Math do
  import List, only: [duplicate: 2]

  def some_function do
    # call duplicate
  end
end

In this case, we are importing only the function duplicate (with arity 2) from List. Although only: is optional, its usage is recommended. except could also be given as an option.

import also supports selectors, to filter what you want to import. We have four selectors:

  • :default - imports all functions and macros, except the ones starting by underscore;

  • :all - imports all functions and macros;

  • :functions - imports all functions;

  • :macros - imports all macros;

For example, to import all macros, one could write:

import :macros, MyMacros

Note that import is also lexically scoped, this means we can import specific macros inside specific functions:

defmodule Math do
  def some_function do
    import List, only: [duplicate: 2]
    # call duplicate
  end
end

In the example above, the imported List.duplicate is only visible within that specific function. duplicate won’t be available in any other function in that module (or any other module for that matter).

Note that importing a module automatically requires it. Furthermore, import also accepts the as: option which is automatically passed to alias in order to create an alias.

Module attributes

Elixir brings the concept of module attributes from Erlang. For example:

defmodule MyServer do
  @vsn 2
end

In the example above, we are explicitly setting the version attribute for that module. @vsn is used by the code reloading mechanism in the Erlang VM to check if a module has been updated or not. If no version is specified, the version is set to the MD5 checksum of the module functions.

Elixir has a handful of reserved attributes. Here are just a few of them, the most commonly used ones. Take a look at the docs for Module for a complete list of supported attributes.

  • @moduledoc - provides documentation for the current module;

  • @doc - provides documentation for the function or macro that follows the attribute;

  • @behaviour - (notice the British spelling) used for specifying an OTP or user-defined behaviour;

  • @before_compile - provides a hook that will be invoked before the module is compiled. This makes it possible to inject functions inside the module exactly before compilation;

The following attributes are part of typespecs and are also supported by Elixir:

  • @spec - provides a specification for a function;

  • @callback - provides a specification for the behavior callback;

  • @type - defines a type to be used in @spec;

  • @typep - defines a private type to be used in @spec;

  • @opaque - defines an opaque type to be used in @spec;

In addition to the built-in attributes outlined above, custom attributes may also be added:

defmodule MyServer do
  @my_data 13
  IO.inspect @my_data #=> 13
end

Unlike Erlang, user defined attributes are not stored in the module by default since it is common in Elixir to use such attributes to store temporary data. A developer can configure an attribute to behave closer to Erlang by calling Module.register_attribute/3.

Finally, notice that attributes can also be read inside functions:

defmodule MyServer do
  @my_data 11
  def first_data, do: @my_data
  @my_data 13
  def second_data, do: @my_data
end

MyServer.first_data #=> 11
MyServer.second_data #=> 13

Notice that reading an attribute inside a function takes a snapshot of its current value. In other words, the value is read at compilation time and not at runtime. Check the documentation for the module Module documentation for other functions to manipulate module attributes.

Nesting

Modules in Elixir can be nested too:

defmodule Foo do
  defmodule Bar do
  end
end

The example above will define two modules Foo and Foo.Bar. The second can be accessed as Bar inside Foo as long as they are in the same scope. If later the developer decides to move Bar to another file, it will need to be referenced by its full name (Foo.Bar) or an alias needs to be set using the alias directive discussed above.

Aliases

In Erlang (and consequently in the Erlang VM), modules and functions are represented by atoms. For instance, this is valid Erlang code:

Mod = lists,
Mod:flatten([1,[2],3]).

In the example above, we store the atom lists in the variable Mod and then invoke the function flatten in it. In Elixir, the same idiom is allowed:

iex> mod = :lists
:lists
iex> mod.flatten([1,[2],3])
[1,2,3]

In other words, we are simply calling the function flatten on the atom :lists. This mechanism is exactly what empowers Elixir aliases. An alias in Elixir is a capitalized identifier (like List, Keyword, etc) which is converted to an atom representing a module during compilation. For instance, the List alias translates by default to the atom Elixir.List:

iex> is_atom(List)
true
iex> to_binary(List)
"Elixir.List"

Records, Protocols & Exceptions

Elixir provides both records and protocols. This chapter will outline the main features of both and provide some examples. More specifically, we will learn how to use defrecord, defprotocol and defimpl. At the end, we will briefly talk about exceptions in Elixir.

Records

Records are simple structures that hold values. For example, we can define a FileInfo record that is supposed to store information about files as follows:

defrecord FileInfo, atime: nil, accesses: 0

The line above will define a module named FileInfo which contains a function named new that returns a new record and other functions to read and set the values in the record:

file_info = FileInfo.new(atime: { 2010, 4, 17 })
file_info.atime                   #=> Returns the value of atime

file_info = file_info.atime({ 2012, 10, 13 }) #=> Updates the value of atime
file_info

Notice that when we change the atime field of the record, we re-store the record in the file_info variable. This is because as almost everything else in Elixir, records are immutable. So changing the atime field does not update the record in place. Instead, it returns a new record with the new value set.

A record is simply a tuple where the first element is the record module name. We can get the record raw representation as follows:

inspect FileInfo.new, raw: true
#=> "{ FileInfo, nil, 0 }"

Besides defining readers and writers for each attribute, Elixir records also define an update_#{attribute} function to update values. Such functions expect a function as argument that receives the current value and must return the new one. For example, every time the file is accessed, the accesses counter can be incremented with:

file_info = FileInfo.new(accesses: 10)
file_info = file_info.update_accesses(fn(x) -> x + 1 end)
file_info.accesses #=> 11

Pattern matching

Elixir also allows one to pattern match against records. For example, imagine we want to check if a file was accessed or not based on the FileInfo record above, we could implement it as follows:

defmodule FileAccess do
  def was_accessed?(FileInfo[accesses: 0]), do: true
  def was_accessed?(FileInfo[]),            do: false
end

The first clause will only match if a FileInfo record is given and the accesses field is equal to zero. The second clause will match any FileInfo record and nothing more. We can also like the value of accesses to a variable as follows:

def was_accessed?(FileInfo[accesses: accesses]), do: accesses == 0

The pattern matching syntax can also be used to create new records:

file_info = FileInfo[accesses: 0]

Whenever using the bracket syntax above, Elixir expands the record to a tuple at compilation time. That said, the clause above:

def was_accessed?(FileInfo[accesses: 0]), do: true

Is effectively the same as:

def was_accessed?({ FileInfo, _, 0 }), do: true

Using the bracket syntax is a powerful mechanism not only due to pattern matching but also regarding performance, since it provides faster times compared to FileInfo.new and file_info.accesses. The downside is that we hardcode the record name. For this reason, Elixir allows you to mix and match both styles as you may find fit.

For more information on records, check out the documentation for the defrecord macro

Protocols

Protocols is a mechanism to achieve polymorphism in Elixir. Dispatching a protocol is available to any data type as long as it implements the protocol. Let’s consider a practical example.

In Elixir, only false and nil are considered falsy values. Everything else evaluates to true. Depending on the application, it may be important to specify a blank? protocol that returns a boolean for other data types that should be considered blank. For instance, an empty list or an empty binary could be considered blanks.

We could define this protocol as follows:

defprotocol Blank do
  @doc "Returns true if data is considered blank/empty"
  def blank?(data)
end

The protocol expects a function called blank? that receives one argument to be implemented. We can implement this protocol for some Elixir data types in the following way:

# Numbers are never blank
defimpl Blank, for: Number do
  def blank?(number), do: false
end

# Just empty list is blank
defimpl Blank, for: List do
  def blank?([]), do: true
  def blank?(_),  do: false
end

# Just the atoms false and nil are blank
defimpl Blank, for: Atom do
  def blank?(false), do: true
  def blank?(nil),   do: true
  def blank?(_),     do: false
end

And we would do so for all native data types. The types available are:

  • Record

  • Tuple

  • Atom

  • List

  • BitString

  • Number

  • Function

  • PID

  • Port

  • Reference

  • Any

Now, with the protocol defined and implementations in hand, we can invoke it:

Blank.blank?(0)       #=> false
Blank.blank?([])      #=> true
Blank.blank?([1,2,3]) #=> false

Notice however that passing a data type that does not implement the protocol raises an error:

Blank.blank?("hello")
** (UndefinedFunctionError) undefined function: Blank.BitString.blank?/1

Implementing the protocol for all 9 types above can be cumbersome. So Elixir gives us some tools to select which protocols we want to implement explicitly.

Selecting implementations

Implementing the protocol for all types can be a bit of work and in some cases, even unnecessary. Going back to our Blank protocol, the types Number, Function, PID, Port and Reference are never going to be blank. To make things easier, Elixir allows us to declare the fact that we are going to implement the protocol just for some types, as follows:

defprotocol Blank do
  @only [Atom, Record, Tuple, List, BitString, Any]
  def blank?(data)
end

Since we also specified Any as a data type, if the data type is not any of Atom, Record, Tuple, List or BitString, it will automatically fall back to Any:

defimpl Blank, for: Any do
  def blank?(_), do: false
end

Now all data types that we have not specified will be automatically considered non-blank.

Using protocols with records

The power of Elixir extensibility comes when protocols and records are mixed.

For instance, Elixir provides a HashDict implementation that is an efficient data structure to store many keys. Let’s take a look at how it works:

dict = HashDict.new
dict = HashDict.put(dict, :hello, "world")
HashDict.get(dict, :hello) #=> "world"

If we inspect our HashDict, we can see it is a simple tuple:

inspect(dict, raw: true)
#=> { HashDict, 1, [{:hello,"world"}] }

Since HashDict is a data structure that contains values, it would be convenient to implement the Blank protocol for it too:

defimpl Blank, for: HashDict do
  def blank?(dict), do: HashDict.size(dict) == 0
end

And now we can test it:

dict = HashDict.new(hello: "world")
Blank.blank?(dict)         #=> false
Blank.blank?(HashDict.new) #=> true

Excellent! The best of all is that we implemented the Blank protocol for an existing data structure (HashDict) without a need to wrap it or recompile it, which allows developers to easily extend previously defined protocols. Note this only worked because, when we defined the protocol, we have added Record to the list of types supported by the protocol:

@only [Atom, Record, Tuple, List, BitString, Any]

Keep in mind that Record needs to come before Tuple, since all records are tuples (but not all tuples are records). For this reason, in case a record does not implement a given protocol, Elixir will fall back to the tuple implementation of that protocol if one exists. So one can add a default protocol implementation for all records by simply defining a default implementation for tuples.

Built-in protocols

Elixir ships with some built-in protocols, let’s take a look at a couple of those:

  • Access - specifies how to access an element. This is the protocol that empowers bracket access in Elixir, for example:

      iex> x = [a: 1, b: 2]
      [{:a, 1}, {:b, 2}]
      iex> x[:a]
      1
      iex> x[:b]
      2
    
  • Enumerable - any data structured that can be enumerated must implement this protocol. This protocol is consumed by the Enum module which provides functions like map, reduce and others:

      iex> Enum.map [1,2,3], fn(x) -> x * 2 end
      [2,4,6]
      iex> Enum.reduce 1..3, 0, fn(x, acc) -> x + acc end
      6
    
  • Binary.Inspect - this protocol is used to transform any data structure into a readable textual representation. That’s what tools like IEx uses to print results:

      iex> { 1, 2, 3 }
      {1,2,3}
      iex> HashDict.new
      #HashDict<[]>
    

    Keep in mind that whenever the inspected value starts with #, it is representing a data structure in non-valid Elixir syntax. For those, the true representation can be retrieved by calling inspect directly and passing raw as an option:

      iex> inspect HashDict.new, raw: true
      "{HashDict,0,[]}"
    
  • Binary.Chars - specifies how to convert a data structure with characters to binary. It’s exposed via the to_binary function:

      iex> to_binary :hello
      "hello"
    

    Notice that string interpolation in Elixir calls the to_binary function:

      iex> "age: #{25}"
      "age: 25"
    

    The example above only works because numbers implement the Binary.Chars protocol. Passing a tuple, for example, will lead to an error:

      iex> tuple = { 1, 2, 3 }
      {1,2,3}
      iex> "tuple: #{tuple}"
      ** (Protocol.UndefinedError) protocol Binary.Chars not implemented for {1,2,3}
    

    When there is a need to “print” a more complex data structure, one can simply use the inspect function:

      iex> "tuple: #{inspect tuple}"
      "tuple: {1,2,3}"
    

Elixir defines other protocols which can be verified in Elixir’s documentation. Frameworks and libraries that you use may define a couple of specific protocols as well. Use them wisely to write code that is easy to maintain and extend.

Exceptions

The try mechanism in Elixir is also used to handle exceptions. In many languages, exceptions would have its own chapter in a getting started guide but here they play a much lesser role.

An exception can be rescued inside a try block with the rescue keyword:

# rescue only runtime error
try do
  raise "some error"
rescue
  RuntimeError -> "rescued"
end

# rescue runtime and argument errors
try do
  raise "some error"
rescue
  [RuntimeError, ArgumentError] -> "rescued"
end

# rescue and assign to x
try do
  raise "some error"
rescue
  x in [RuntimeError] ->
    # all exceptions have a message
    x.message
end

Notice that rescue works with exception names and it doesn’t allow guards nor pattern matching. This limitation is on purpose: developers should not use exception values to drive their software. In fact, exceptions in Elixir should not be used for control-flow but only under exceptional circumstances.

For example, if you write a software that does log partitioning and log rotation over the network, you may face network issues or an eventual instability when accessing the file system. These scenarios are not exceptional in this particular software and must be handled accordingly. Therefore, a developer can read some file using File.read:

case File.read(file) do
  { :ok, contents }  ->
    # we could access the file
    # proceed as expected
  { :error, reason } ->
    # Oops, something went wrong
    # We need to handle the error accordingly
end

Notice File.read does not raise an exception in case something goes wrong, it returns a tuple containing { :ok, contents } in case of success and { :error, reason } in case of failures. This allows us to use Elixir’s pattern matching constructs to control how our application should behave.

On the other hand, a CLI interface that needs to access or manipulate a file given by the user may necessarily expect a file to be there. If the given file does not exist, there is nothing you can do but fail. Then you may use File.read! which raises an exception:

contents = File.read!(file)

This pattern is common throughout Elixir standard library. Many libraries have both function definitions followed by their “bang!” variation, with exclamation mark. This showcases well Elixir’s philosophy of not using exceptions for control-flow. If you feel like you need to rescue an exception in order to change how your code works, you should probably return an atom or tuple instead, allowing your developers to rely on pattern matching.

Finally, exceptions are simply records and they can be defined with defexception which has a similar API to defrecord. But remember, in Elixir you will use those sparingly. Next, let’s take a look at how Elixir tackles productivity by building some macros using defmacro and defmacrop!

Note: In order to ease integration with Erlang APIs, Elixir also supports “catching errors” coming from Erlang with try/catch, as it works in Erlang:

try do
  :erlang.error(:oops)
catch
  :error, :oops ->
    "Got Erlang error"
end

The first atom can be one of :error, :throw or :exit. Keep in mind catching errors is as discouraged as rescuing exceptions in Elixir.

Macros

An Elixir program can be represented by its own data structures. This chapter will describe what those structures look like and how to manipulate them to create your own macros.

Building blocks of an Elixir program

The building block of Elixir is a tuple with three elements. The function call sum(1,2,3) is represented in Elixir as:

{ :sum, [], [1, 2, 3] }

You can get the representation of any expression by using the quote macro:

iex> quote do: sum(1, 2, 3)
{ :sum, [], [1, 2, 3] }

Operators are also represented as such tuples:

iex> quote do: 1 + 2
{ :+, [], [1, 2] }

Even a tuple is represented as a call to {}:

iex> quote do: { 1, 2, 3 }
{ :{}, [], [1, 2, 3] }

Variables are also represented using tuples, except the last element is an atom, instead of a list:

iex> quote do: x
{ :x, [], Elixir }

When quoting more complex expressions, we can see the representation is composed of such tuples, which are nested on each other resembling a tree where each tuple is a node:

iex> quote do: sum(1, 2 + 3, 4)
{ :sum, [], [1, { :+, [], [2, 3] }, 4] }

In general, each node (tuple) above follows the following format:

{ tuple | atom, list, list | atom }
  • The first element of the tuple is an atom or another tuple in the same representation;

  • The second element of the tuple is an list of metadata, it may hold information like the node line number;

  • The third element of the tuple is either a list of arguments for the function call or an atom. When an atom, it means the tuple represents a variable.

Besides the node defined above, there are also five Elixir literals that when quoted return themselves (and not a tuple). They are:

:sum         #=> Atoms
1.0          #=> Numbers
[1,2]        #=> Lists
"binaries"   #=> Strings
{key, value} #=> Tuples with two elements

With those basic structures in mind, we are ready to define our own macro.

Defining our own macro

A macro can be defined using defmacro. For instance, in just a few lines of code we can define a macro called unless which does the opposite of if:

defmodule MyMacro do
  defmacro unless(clause, options) do
    quote do: if(!unquote(clause), unquote(options))
  end
end

Similarly to if, unless expects two arguments: a clause and options:

require MyMacro
MyMacro.unless var, do: IO.puts "false"

However, since unless is a macro, its arguments are not evaluated when it’s invoked but are instead passed literally. For example, if one calls:

unless 2 + 2 == 5, do: call_function()

Our unless macro will receive the following:

unless({:==, [], [{:+, [], [2, 2]}, 5]}, { :call_function, [], [] })

Then our unless macro will call quote to return a tree representation of the if clause. This means we are transforming our unless into an if!

There is a common mistake when quoting expressions which is that developers usually forget to unquote the proper expression. In order to understand what unquote does, let’s simply remove it:

defmacro unless(clause, options) do
  quote do: if(!clause, options)
end

When called as unless 2 + 2 == 5, do: call_function(), our unless would then literally return:

if(!clause, options)

Which would fail because the clause and options variables are not defined in the current scope. If we add unquote back:

defmacro unless(clause, options) do
  quote do: if(!unquote(clause), unquote(options))
end

unless will then return:

if(!(2 + 2 == 5), do: call_function())

In other words, unquote is a mechanism to inject expressions into the tree being quoted and it is an essential tool for meta-programming. Elixir also provides unquote_splicing allowing us to inject many expressions at once.

We can define any macro we want, including ones that override the built-in macros provided by Elixir. For instance, you can redefine case, receive, +, etc. The only exceptions are Elixir special forms that cannot be overridden, the full list of special forms is available in Kernel.SpecialForms.

Macros hygiene

Elixir macros have late resolution. This guarantees that a variable defined inside a quote won’t conflict with a variable defined in the context where that macro is expanded. For example:

defmodule Hygiene do
  defmacro no_interference do
    quote do: a = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.no_interference
    a
  end
end

HygieneTest.go
# => 13

In the example above, even if the macro injects a = 1, it does not affect the variable a defined by the go function. In case the macro wants to explicitly affect the context, it can use var!:

defmodule Hygiene do
  defmacro interference do
    quote do: var!(a) = 1
  end
end

defmodule HygieneTest do
  def go do
    require Hygiene
    a = 13
    Hygiene.interference
    a
  end
end

HygieneTest.go
# => 1

Variables hygiene only works because Elixir annotates variables with their context. For example, a variable x defined at the line 3 of a module, would be represented as:

{ :x, [line: 3], nil }

However, a quoted variable is represented as:

defmodule Sample do
  def quoted do
    quote do: x
  end
end

Sample.quoted #=> { :x, [line: 3], Sample }

Notice that the third element in the quoted variable is the atom Sample, instead of nil, which marks the variable as coming from the Sample module. Therefore, Elixir considers those two variables come from different contexts and handle them accordingly.

Elixir provides similar mechanisms for imports and aliases too. This guarantees macros will behave as specified by its source module rather than conflicting with the target module.

Private macros

Elixir also supports private macros via defmacrop. As private functions, these macros are only available inside the module that defines them, and only at compilation time. A common use case for private macros is to define guards that are frequently used in the same module:

defmodule MyMacros do
  defmacrop is_even?(x) do
    quote do
      rem(unquote(x), 2) == 0
    end
  end

  def add_even(a, b) when is_even?(a) and is_even?(b) do
    a + b
  end
end

It is important that the macro is defined before its usage. Failing to define a macro before its invocation will raise an error at runtime, since the macro won’t be expanded and will be translated to a function call:

defmodule MyMacros do
  def four, do: two + two
  defmacrop two, do: 2
end

MyMacros.four #=> ** (UndefinedFunctionError) undefined function: two/0

Code execution

To finish our discussion about macros, we are going to briefly discuss how code execution works in Elixir. Code execution in Elixir is done in two steps:

​1) All the macros in the code are expanded recursively;

​2) The expanded code is compiled to Erlang bytecode and executed

This behavior is important to understand because it affects how we think about our code structure. Consider the following code:

defmodule Sample do
  case System.get_env("FULL") do
    "true" ->
      def full?(), do: true
    _ ->
      def full?(), do: false
  end
end

The code above will define a function full? which will return true or false depending on the value of the environment variable FULL at compilation time. In order to execute this code, Elixir will first expand all macros. Considering that defmodule and def are macros, the code will expand to something like:

:elixir_module.store Sample, fn ->
  case System.get_env("FULL") do
    "true" ->
      :elixir_def.store(Foo, :def, :full?, [], true)
    _ ->
      :elixir_def.store(Foo, :def, :full?, [], false)
end

This code will then be executed, define a module Foo and store the appropriate function based on the value of the environment variable FULL. We achieve this by using the modules :elixir_module and :elixir_def, which are Elixir internal modules written in Erlang.

There are two lessons to take away from this example:

​1) a macro is always expanded, regardless if it is inside a case branch that won’t actually match when executed;

​2) we cannot invoke a function or macro just after it is defined in a module. For example, consider:

defmodule Sample do
  def full?, do: true
  IO.puts full?
end

The example above will fail because it translates to:

:elixir_module.store Sample, fn ->
  :elixir_def.store(Foo, :def, :full?, [], true)
  IO.puts full?
end

At the moment the module is being defined, there isn’t yet a function named full? defined in the module, so IO.puts full? will cause the compilation to fail.

Don’t write macros

Although macros are a powerful construct, the first rule of the macro club is don’t write macros. Macros are harder to write than ordinary Elixir functions, and it’s considered to be bad style to use them when they’re not necessary. Elixir already provides elegant mechanisms to write your every day code and macros should be saved as last resort.

With those lessons, we finish our introduction to macros. Next, let’s move to the next chapter which will discuss several topics such as documentation, partial application and others.

Other topics

This chapter contains different small topics that are part of Elixir’s day to day work. We will learn about writing documentation, list and binary comprehensions, partial function application and more!

String sigils

Elixir provides string sigils via the token %:

%b(Binary with escape codes \x26 interpolation)
%B(Binary without escape codes and without #{interpolation})

Sigils starting with an uppercase letter never escape characters or do interpolation. Notice the separators are not necessarily parenthesis, but any non-alphanumeric character:

%b-another binary-

Internally, %b is translated as a function call to __b__. For instance, the docs for %b are available in the function __b__/2 defined in Kernel module.

The sigils defined in Elixir by default are:

  • %b and %B - Returns a binary;

  • %c and %C - Returns a char list;

  • %r and %R - Returns a regular expression;

Heredocs

Elixir supports heredocs as a way to define long strings. Heredocs are delimited by triple double-quotes for binaries or triple single-quotes for char lists:

"""
Binary heredoc
"""

'''
Charlist heredoc
'''

The heredoc ending must be in a line on its own, otherwise it is part of the heredoc:

"""
Binary heredocs in Elixir use """
"""

Notice the sigils discussed in the previous section are also available as heredocs:

%B"""
A heredoc without escaping or interpolation
"""

Documentation

Elixir uses module attributes described in chapter 3 to drive its documentation system. For instance, consider the following example:

defmodule MyModule do
  @moduledoc "It does X"

  @doc "Returns the version"
  def version, do: 1
end

In the example above, we are adding a module documentation to MyModule via @moduledoc and using @doc to document each function. When compiled, we are able to inspect the documentation attributes at runtime (remember to start iex in the same directory in which you compiled the module):

$ elixirc my_module.ex
$ iex
iex> MyModule.__info__(:docs)
[{ { :version, 0 }, 5, :def, [], "Returns the version" }]
iex> MyModule.__info__(:moduledoc)
{1,"It does X"}

__info__(:docs) returns a list of tuples where each tuple contains a function/arity pair, the line the function was defined on, the kind of the function (def or defmacro, private functions cannot be documented), the function arguments and its documentation. The comment will be either a binary or nil (not given) or false (explicit no doc).

Similarly, __info__(:moduledoc) returns a tuple with the line the module was defined on and its documentation.

Elixir promotes the use of markdown with heredocs to write readable documentation:

defmodule Math do
  @moduledoc """
  This module provides mathematical functions
  as sin, cos and constants like pi.

  ## Examples

      Math.pi
      #=> 3.1415...

  """
end

IEx Helpers

Elixir’s interactive console (IEx) ships with many functions to make the developer’s life easier. One of these functions is called h, which shows documentation directly at the command line:

iex> h()
# IEx.Helpers
...
:ok

As you can see, invoking h() prints the documentation of IEx.Helpers. From there, we can navigate to any of the other helpers by giving its name and arity to get more information:

iex> h(c/2)
* def c(files, path // ".")
...
:ok

This functionality can also be used to print the documentation for any Elixir module in the system:

iex> h(Enum)
...
iex> h(Enum.each/2)
...

The documentation for built-in functions can also be accessed directly or indirectly from the Kernel module:

iex> h(is_atom/1)
...
iex> h(Kernel.is_atom/1)
...

Function retrieval and partial application

Elixir supports a convenient syntax for retrieving functions. Let’s suppose we have a list of binaries and we want to calculate the size of each of them. We could do it in the following way:

iex> list = ["foo", "bar", "baz"]
["foo","bar","baz"]
iex> Enum.map list, fn(x) -> size(x) end
[3,3,3]

We could also write this as:

iex> Enum.map list, size(&1)
[3,3,3]

The example above works as if size(&1) translates directly to fn(x) -> size(x) end. Since operators are also function calls, they can also benefit of the same syntax:

iex> Enum.reduce [1,2,3], 1, &1 * &2
6

In this case, &1 * &2 translates to fn(x, y) -> x * y end. The values &1 and &2 map to the argument order in the generated function.

Keep in mind that &N binds only to the closest function call. For example, the following syntax is invalid:

iex> foo(&1, 1 + &2)
** (SyntaxError) iex:1: partial variable &2 cannot be defined without &1

This is because we have two functions calls in the example above, foo and +, and &1 binds to foo while &2 binds to +. Let’s add some parenthesis to make it explicit:

iex> foo(&1, (1 + &2))
** (SyntaxError) iex:1: partial variable &2 cannot be defined without &1

In such cases, you need to use the usual function syntax:

iex> fn(x, y) -> foo(x, 1 + y) end
#Function<erl_eval.12.82930912>

Finally, notice this syntax can also be used to do partial application in Elixir. For instance, if we want to multiply each item in a list per two, we could write it as:

iex> Enum.map [1,2,3], &1 * 2
[2,4,6]

All functions and macros can be retrieved and partially applied, except Elixir’s special forms.

Use

use is a macro that provides a common API for extension. For instance, in order to use the ExUnit test framework that ships with Elixir, you simply need to use ExUnit.Case in your module:

defmodule AssertionTest do
  use ExUnit.Case, async: true

  test "always pass" do
    true = true
  end
end

This allows ExUnit.Case to configure and set up the module for testing, for example, by making the test macro used above available.

The implementation of the use macro is actually quite trivial. When you invoke use with a module, it invokes a hook called __using__ in this module. For example, the use call above is simply a translation to:

defmodule AssertionTest do
  require ExUnit.Case
  ExUnit.Case.__using__(async: true)

  test "always pass" do
    true = true
  end
end

In general, we recommend APIs to provide a __using__ hook in case they want to expose functionality to developers.

Comprehensions

Elixir also provides list and bit comprehensions. List comprehensions allow you to quickly build a list from another list:

iex> lc n inlist [1,2,3,4], do: n * 2
[2,4,6,8]

Or, using keywords blocks:

lc n inlist [1,2,3,4] do
  n * 2
end

A comprehension accepts many generators (given by inlist or inbits operators) as well as filters:

# A comprehension with a generator and a filter
iex> lc n inlist [1,2,3,4,5,6], rem(n, 2) == 0, do: n
[2,4,6]

# A comprehension with two generators
iex> lc x inlist [1,2], y inlist [2,3], do: x*y
[2,3,4,6]

Elixir provides generators for both lists and bitstrings:

# A list generator:
iex> lc n inlist [1,2,3,4], do: n * 2
[2,4,6,8]

# A bit string generator:
iex> lc <<n>> inbits <<1,2,3,4>>, do: n * 2
[2,4,6,8]

Bit string generators are quite useful when you need to organize streams:

iex> pixels = <<213,45,132,64,76,32,76,0,0,234,32,15>>
iex> lc <<r :: size(8), g :: size(8), b :: size(8)>> inbits pixels, do: {r,g,b}
[{213,45,132},{64,76,32},{76,0,0},{234,32,15}]

Remember, as strings are binaries and a binary is a bitstring, we can also use strings in comprehensions. For instance, the example below removes all white space characters from a string via bit comprehensions:

iex> bc <<c>> inbits " hello world ", c != ?\s, do: <<c>>
"helloworld"

Pseudo variables

Elixir provides a set of pseudo-variables. These variables can only be read and never assigned to. They are:

  • __MODULE__ - Returns an atom representing the current module or nil;

  • __FILE__ - Returns a string representing the current file;

  • __DIR__ - Returns a string representing the current directory;

  • __ENV__ - Returns a Macro.Env record with information about the compilation environment. Here we can access the current module, function, line, file and others;

  • __CALLER__ - Also returns a Macro.Env record but with information of the calling site. __CALLER__ is available only inside macros;

Where To Go Next

Applications

In order to get your first project started, Elixir ships with a build tool called Mix. You can get your new project started by simply running:

mix new path/to/new/project

You can learn more about Elixir and other applications in the links below:

A Byte of Erlang

As the main page of this site puts it:

Elixir is a programming language built on top of the Erlang VM.

Sooner than later, an Elixir developer will want to interface with existing Erlang libraries. Here’s a list of online resources that cover Erlang’s fundamentals and its more advanced features:

  • This Erlang Syntax: A Crash Course provides a concise intro to Erlang’s syntax. Each code snippet is accompanied by equivalent code in Elixir. This is an opportunity for you to not only get some exposure to the Erlang’s syntax but also review some of the things you have learned in the present guide.

  • Erlang’s official website has a short tutorial with pictures that briefly describe Erlang’s primitives for concurrent programming.

  • Learn You Some Erlang for Great Good! is an excellent introduction to Erlang, its design principles, standard library, best practices and much more. If you are serious about Elixir, you’ll want to get a solid understanding of Erlang principles. Once you have read through the crash course mentioned above, you’ll be able to safely skip the first couple of chapters in the book that mostly deal with the syntax. When you reach The Hitchhiker’s Guide to Concurrency chapter, that’s where the real fun starts.

Reference Manual

You can also check the source code of Elixir itself, which is mainly written in Elixir (mainly the lib directory), or explore Elixir’s documentation.

Join The Community

Remember that in case of any difficulties, you can always visit the #elixir-lang channel on irc.freenode.net or send a message to the mailing list. You can be sure that there will be someone willing to help. And to keep posted on the latest news and announcements, follow the blog and join elixir-core mailing list.

Introduction to Mix

Elixir ships with a few applications to make building and deploying projects with Elixir easier and Mix is certainly their backbone.

Mix is a build tool that provides tasks for creating, compiling, testing (and soon deploying) Elixir projects. Mix is inspired by the Leiningen build tool for Clojure and was written by one of its contributors.

In this chapter, you will learn how to create projects using mix and install dependencies. In the following sections, we will also learn how to create OTP applications and create custom tasks with Mix.

Bootstrapping

In order to start your first project, simply use the mix new command passing the path to your project. For now, we will create an project called my_project in the current directory:

$ mix new my_project

Mix will create a directory named my_project with few files in it:

.gitignore
README.md
mix.exs
lib/my_project.ex
test/test_helper.exs
test/my_project_test.exs

Let’s take a brief look at some of these.

Note: Mix is an Elixir executable. This means that in order to run mix, you need to have elixir’s executable in your PATH. If not, you can run it by passing the script as argument to elixir:

$ bin/elixir bin/mix new ./my_project

Note that you can also execute any script in your PATH from Elixir via the -S option:

$ bin/elixir -S mix new ./my_project

When using -S, elixir finds the script wherever it is in your PATH and executes it.

mix.exs

This is the file with your projects configuration. It looks like this:

defmodule MyProject.Mixfile do
  use Mix.Project

  def project do
    [ app: :my_project,
      version: "0.0.1",
      deps: deps ]
  end

  # Configuration for the OTP application
  def application do
    []
  end

  # Returns the list of dependencies in the format:
  # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
  defp deps do
    []
  end
end

Our mix.exs is quite straight-forward. It defines two functions: project, which returns project configuration like the project name and version, and application, which is used to generate an Erlang application that is managed by the Erlang Runtime. In this chapter, we will talk about the project function. We will go into detail about what goes in the application function in the next chapter.

lib/my_project.ex

This file contains a simple module definition to lay out our code:

defmodule MyProject do
end

test/my_project_test.exs

This file contains a stub test case for our project:

Code.require_file "test_helper.exs", __DIR__

defmodule MyProjectTest do
  use ExUnit.Case

  test "the truth" do
    assert true
  end
end

It is important to note a couple things:

​1) Notice the file is an Elixir script file (.exs). This is convenient because we don’t need to compile test files before running them;

​2) The first line in our test is simply requiring the test_helper file in the same directory as the current file. As we are going to see, the test/test_helper.exs file is responsible for starting the test framework;

​3) We define a test module named MyProjectTest, using ExUnit.Case to inject default behavior and define a simple test. You can learn more about the test framework in the ExUnit chapter;

test/test_helper.exs

The last file we are going to check is the test_helper.exs, which simply sets up the test framework:

ExUnit.start

And that is it, our project is created. We are ready to move on!

Exploring

Now that we created our new project, what can we do with it? In order to check the commands available to us, just run the help task:

$ mix help

It will print all the available tasks. You can get further information by invoking mix help TASK.

Play around with the available tasks, like mix compile and mix test, and execute them in your project to check how they work.

Compilation

Mix can compile our project for us. The default configurations uses lib/ for source files and ebin/ for compiled beam files. You don’t even have to provide any compilation-specific setup but if you must, some options are available. For instance, if you want to put your compiled files in another directory besides ebin, simply set in :compile_path in your mix.exs file:

def project do
  [compile_path: "ebin"]
end

In general, Mix tries to be smart and compiles only when necessary.

Note that after you compile for the first time, Mix generates a my_project.app file inside your ebin directory. This file defines an Erlang application based on the contents of the application function in your Mix project.

The .app file holds information about the application, what are its dependencies, which modules it defines and so forth. The application is automatically started by Mix every time you run some commands and we will learn how to configure it in the next chapter.

Dependencies

Mix is also able to manage dependencies. Dependencies should be listed in the project settings, as follows:

def project do
  [ app: :my_project,
    version: "0.0.1",
    deps: deps ]
end

defp deps do
  [ { :some_project, "0.3.0", github: "some_project/other" },
    { :another_project, "1.0.2", git: "https://example.com/another/repo.git" } ]
end

Note: Although not required, it is common to split dependencies into their own function.

Source Code Management (SCM)

In the example above, we have used git to specify our dependencies. Mix is designed in a way it can support multiple SCM tools, shipping with :git and :path support by default. The most common options are:

  • :git - the dependency is a git repository that is retrieved and updated by Mix;

  • :path - the dependency is simply a path in the filesystem;

  • :compile - how to compile the dependency;

  • :app - the path of the application expected to be defined by the dependency;

Each SCM may support custom options. :git, for example, supports the following:

  • :ref - an optional reference (a commit) to checkout the git repository;

  • :tag - an optional tag to checkout the git repository;

  • :branch - an optional branch to checkout the git repository;

  • :submodules - when true, initializes submodules recursively in the dependency;

Compiling dependencies

In order to compile a dependency, Mix looks into the repository for the best way to proceed. If the dependency contains one of the files below, it will proceed as follows:

  1. mix.exs - compiles the dependency directly with Mix by invoking the compile task;

  2. rebar.config or rebar.config.script - compiles using rebar compile deps_dir=DEPS, where DEPS is the directory where Mix will install the project dependencies by default;

  3. Makefile - simply invokes make;

If the dependency does not contain any of the above, you can specify a command directly with the :compile option:

    compile: "./configure && make"

If :compile is set to false, nothing is done.

Repeatability

An important feature in any dependency management tool is repeatability. For this reason when you first get your dependencies, Mix will create a file called mix.lock that contains checked out references for each dependency.

When another developer gets a copy of the same project, Mix will checkout exactly the same references, ensuring other developers can “repeat” the same setup.

Locks are automatically updated when deps.update is called and can be removed with deps.unlock.

Dependencies tasks

Elixir has many tasks to manage the project dependencies:

  • mix deps - List all dependencies and their status;

  • mix deps.get - Get all unavailable dependencies;

  • mix deps.compile - Compile dependencies;

  • mix deps.update - Update dependencies;

  • mix deps.clean - Remove dependencies files;

  • mix deps.unlock - Unlock the given dependencies;

Use mix help to get more information.

Dependencies of dependencies

If your dependency is another Mix or rebar project, Mix does the right thing: it will automatically fetch and handle all dependencies of your dependencies. However, if your project have two dependencies that share the same dependency and the SCM information for the shared dependency doesn’t match between the parent dependencies, Mix will mark that dependency as diverged and emit a warning. To solve this issue you can declare the shared dependency in your project and Mix will use that SCM information to fetch the dependency.

Environments

Mix has the concept of environments that allows a developer to customize compilation and other options based on an external setting. By default, Mix understands three environments:

  • dev - the one in which mix tasks are run by default;

  • test - used by mix test;

  • prod - the environment in which dependencies are loaded and compiled;

By default, these environments behave the same and all configuration we have seen so far will affect all three environments. Customization per environment can be done using the env: option:

def project do
  [ env: [
    prod: [compile_path: "prod_ebin"] ] ]
end

Mix will default to the dev environment (except for tests). The environment can be changed via the MIX_ENV environment variable:

$ MIX_ENV=prod mix compile

In the next chapters, we will learn more about building OTP applications with Mix and how to create your own tasks.

Building OTP apps with Mix

In the previous chapter, we have generated a project with Mix and explored a bit how Mix works. In this chapter, we will learn how to build an OTP application. In practice, we don’t need Mix in order to build such applications, however Mix provides some conveniences that we are going to explore throughout this chapter.

The Stacker server

Our application is going to be a simple stack that allow us push and pop items as we wish. Let’s call it stacker:

$ mix new stacker

Our application is going to have one stack which may be accessed by many processes at the same time. To achieve that, we will create a server that is responsible to manage the stack. Clients will send messages to the server whenever they want to push or pop something from the stack.

Since creating such servers is a common pattern when building Erlang and Elixir applications, we have a behavior in OTP that encapsulates common server functionalities called GenServer. Let’s create a file named lib/stacker/server.ex with our first server:

defmodule Stacker.Server do
  use GenServer.Behaviour

  def init(stack) do
    { :ok, stack }
  end

  def handle_call(:pop, _from, [h|stack]) do
    { :reply, h, stack }
  end

  def handle_cast({ :push, new }, stack) do
    { :noreply, [new|stack] }
  end
end

Our server defines three callbacks: init/1, handle_call/3 and handle_cast/2. We never call those functions directly, they are called by OTP whenever we interact with the server. We will go into details about these soon, let’s just ensure it works as expected. To do so, run iex -S mix on your command line to start iex with mix and type the following:

# Let's start the server using Erlang's :gen_server module.
# It expects 3 arguments: the server module, the initial
# stack and some options (if desired):
iex> { :ok, pid } = :gen_server.start_link(Stacker.Server, [], [])
{:ok,<...>}

# Now let's push something onto the stack
iex> :gen_server.cast(pid, { :push, 13 })
:ok

# Now let's get it out from the stack
# Notice we are using *call* instead of *cast*
iex> :gen_server.call(pid, :pop)
13

Excellent, our server works as expected! There are many things happening behind the scenes, so let’s discuss them one by one.

First, we started the server using the :gen_server module from OTP. Notice we have used start_link, which starts the server and links our current process to the server. In this scenario, if the server dies, it will send an exit message to our process, making it crash too. We will see this in action later. The start_link function returns the process identifier (pid) of the newly spawned server.

Later, we have sent a cast message to the pid. The message was { :push, 13 }, written in the same format as we specified in the handle_cast/2 callback in Stacker.Server. Whenever we send a cast message, the handle_cast/2 callback will be invoked to handle the message.

Then we finally read what was on the stack by sending a call message, which will dispatch to the handle_call/3 callback. So, what is the difference between cast and call after all?

cast messages are asynchronous: we simply send a message to the server and don’t expect a reply back. That’s why our handle_cast/2 callback returns { :noreply, [new|stack] }. The first item of the tuple states nothing should be replied and the second contains our updated stack with the new item.

On the other hand, call messages are synchronous. When we send a call message, the client expects a response back. In this case, the handle_call/3 callback returns { :reply, h, stack }, where the second item is the term to be returned and the third is our new stack without its head. Since calls are able to send messages back to the client, it also receives the client information as argument (_from).

Learning more about callbacks

In the GenServer’s case, there are 8 different values a callback such as handle_call or handle_cast can return:

{ :reply, reply, new_state }
{ :reply, reply, new_state, timeout }
{ :reply, reply, new_state, :hibernate }
{ :noreply, new_state }
{ :noreply, new_state, timeout }
{ :noreply, new_state, :hibernate }
{ :stop, reason, new_state }
{ :stop, reason, reply, new_state }

There are 6 callbacks required to be implemented in a GenServer. The GenServer.Behaviour module defines all of them automatically but allows us to customize the ones we need. The list of callbacks are:

  • init(args) - invoked when the server is started;

  • handle_call(msg, from, state) - invoked to handle call messages;

  • handle_cast(msg, state) - invoked to handle cast messages;

  • handle_info(msg, state) - handle all other messages which are normally received by processes;

  • terminate(reason, state) - called when the server is about to terminate, useful for cleaning up;

  • code_change(old_vsn, state, extra) - called when the application code is being upgraded live (hot code swap);

Crashing a server

Of what use is a server if we cannot crash it?

It is actually quite easy to crash our server. Our handle_call/3 callback only works if there is something on the stack (remember [h|t] won’t match an empty list). So let’s simply send a message when the stack is empty:

# Start another server, but with an initial :hello item
iex> { :ok, pid } = :gen_server.start_link(Stacker.Server, [:hello], [])
{:ok,<...>}

# Let's get our initial item:
iex> :gen_server.call(pid, :pop)
:hello

# And now let's call pop again
iex> :gen_server.call(pid, :pop)

=ERROR REPORT==== 6-Dec-2012::19:15:33 ===
...
** (exit) 
...

You can see there are two error reports. The first one is generated by server, due to the crash. Since the server is linked to our process, it also sent an exit message which was printed by IEx as ** (exit) ....

Since our servers may eventually crash, it is common to supervise them, and that’s what we are going to next. There is a bit more to GenServer than what we have seen here. For more information, check GenServer.Behaviour’s documentation.

Supervising our servers

When building applications in Erlang/Elixir, a common philosophy is to “let it crash”. Resources are going to become unavailable, timeout in between services are going to happen and other possible failures exist. That’s why it is important to recover and react to such failures. With this in mind, we are going to write a supervisor for our server.

Create a file at lib/stacker/supervisor.ex with the following:

defmodule Stacker.Supervisor do
  use Supervisor.Behaviour

  # A convenience to start the supervisor
  def start_link(stack) do
    :supervisor.start_link(__MODULE__, stack)
  end

  # The callback invoked when the supervisor starts
  def init(stack) do
    children = [ worker(Stacker.Server, [stack]) ]
    supervise children, strategy: :one_for_one
  end
end

In case of supervisors, the only callback that needs to be implemented is init(args). This callback needs to return a supervisor specification, in this case returned by the helper function supervise/2.

Our supervisor is very simple: it has to supervise one worker, in this case, Stacker.Server and the worker will be started by receiving one argument, which is the default stack. The defined worker is then going to be supervised using the :one_for_one strategy, which restarts each worker after it dies.

Given that our worker is specified by the Stacker.Server module passing the stack as argument, the supervisor will by default invoke the Stacker.Server.start_link(stack) function to start the worker, so let’s implement it:

defmodule Stacker.Server do
  use GenServer.Behaviour

  def start_link(stack) do
    :gen_server.start_link({ :local, :stacker }, __MODULE__, stack, [])
  end

  def init(stack) do
    { :ok, stack }
  end

  def handle_call(:pop, _from, [h|stack]) do
    { :reply, h, stack }
  end

  def handle_cast({ :push, new }, stack) do
    { :noreply, [new|stack] }
  end
end

The start_link function is quite similar to how we were starting our server previously, except that now we passed one extra argument: { :local, :stacker }. This argument registers the server on our local nodes, allowing it to be invoked by the given name (in this case, :stacker), instead of directly using the pid.

With our supervisor in hand, let’s start the console by running iex -S mix once again, which will recompile our files too:

# Now we will start the supervisor with a
# default stack containing :hello
iex> Stacker.Supervisor.start_link([:hello])
{:ok,<...>}

# And we will access the server by name since
# we registered it
iex> :gen_server.call(:stacker, :pop)
:hello

Notice the supervisor started the server for us and we were able to send messages to it via the name :stacker. What happens if we crash our server again?

iex> :gen_server.call(:stacker, :pop)

=ERROR REPORT==== 6-Dec-2012::19:15:33 ===
...
** (exit) 
...

iex> :gen_server.call(:stacker, :pop)
:hello

It crashes exactly as before but it is restarted right away by the supervisor with the default stack, allowing us to retrieve :hello again. Excellent!

By default the supervisor allows a worker to restart at maximum 5 times in a 5 seconds timespan. If the worker crashes more frequently than that, the supervisor gives up on the worker and no longer restarts it. Let’s check it by sending 5 unknown messages one right after the other (be fast!):

iex> :gen_server.call(:stacker, :unknown)
... 5 times ...

iex> :gen_server.call(:stacker, :unknown)
** (exit) {:noproc,{:gen_server,:call,[:stacker,:unknown]}}
    gen_server.erl:180: :gen_server.call/2

The sixth message no longer generates an error report, since our server was no longer started automatically. Elixir returns :noproc (which stands for no process), meaning there isn’t a process named :stacker. The number of restarts allowed and its time interval can be customized by passing options to the supervise function. Different restart strategies, besides the :one_for_one used above, can be chosen for the supervisor as well. For more information on the supported options, check the documentation for Supervisor.Behaviour.

Who supervises the supervisor?

We have built our supervisor but a pertinent question is: who supervisors the supervisor? To answer this question, OTP contains the concept of applications. Applications can be started and stopped as an unit and, when doing so, they are often linked to a supervisor.

In the previous chapter, we have learned how Mix automatically generates an .app file every time we compile our project based on the information contained on the application function in our mix.exs file.

The .app file is called application specification and it must contain our application dependencies, the modules it defines, registered names and many others. Some of this information is filled in automatically by Mix but other data needs to be added manually.

In this particular case, our application has a supervisor and, furthermore, it registers a server with name :stacker. That said, it is useful to add to the application specification all registered names in order to avoid conflicts. If it happens that two applications register the same name, we will be able to find about this conflict sooner. So, let’s open the mix.exs file and edit the application function to the following:

def application do
  [ registered: [:stacker],
    mod: { Stacker, [:hello] } ]
end

In the :registered key we specify all names registered by our application. The :mod key specifies that, as soon as the application is started, it must invoke the application module callback. In this case, the application module callback will be the Stacker module and it will receive the default stack [:hello] as argument. The callback must return the pid of the supervisor which is associated to this application.

With this in mind, let’s open up the lib/stacker.ex file and add the following:

defmodule Stacker do
  use Application.Behaviour

  def start(_type, stack) do
    Stacker.Supervisor.start_link(stack)
  end
end

The Application.Behaviour expects two callbacks, start(type, args) and stop(state). We are required to implement start/2 though we have decided to not bother about stop(state) for now.

After adding the application behavior above, all you need to do is to start iex -S mix once again. Our files are going to be recompiled and the supervisor (and consequently our server) will be automatically started:

iex> :gen_server.call(:stacker, :pop)
:hello

Amazing, it works! As you may have noticed, the application start/2 callback receives a type argument, which we have ignored. The type controls how the VM should behave when the supervisor, and consequently our application, crashes. You can learn more about it by reading the documentation for Application.Behaviour.

Starting applications

Mix will always start the current application and all application dependencies. Notice there is a difference between your project dependencies (the ones defined under the deps key we have discussed in the previous chapter) and the application dependencies.

The project dependencies may contain your test framework or a compile-time only dependency. The application dependency is everything you depend on at runtime. Any application dependency needs to be explicitly added to the application function too:

def application do
  [ registered: [:stacker],
    applications: [:some_dep],
    mod: { Stacker, [:hello] } ]
end

When running tasks on Mix, it will ensure the application and all application dependencies are started. This can also be done via command line with the flag --app:

$ elixir -pa ebin --app my_project

You may also start each application individually via the command line using Erlang’s :application module:

:application.start(:my_project)

Besides registered, applications and mod, there are other keys allowed in the application specification. You can learn more about them in the applications chapter from Learn You Some Erlang.

Finally, notice that mix new supports a --sup option, which tells Mix to generate a supervisor with an application module callback, automating some of the work we have done here (try it!). With this note, we finalize this chapter. We have learned how to create servers, supervise them, and hook them into our application lifecycle. In the next chapter, we will learn how to create custom tasks in Mix.

Creating custom Mix tasks

In Mix, a task is simply an Elixir module inside the Mix.Tasks namespace containing a run/1 function. For example, the compile task is a module named Mix.Tasks.Compile.

Let’s create a simple task:

defmodule Mix.Tasks.Hello do
  use Mix.Task

  @shortdoc "This is short documentation, see"

  @moduledoc """
  A test task.
  """
  def run(_) do
    IO.puts "Hello, World!"
  end
end

Save this module to a file named hello.ex then compile and run it as follows:

$ elixirc hello.ex
$ mix hello
Hello, World!

The module above defines a task named hello. The function run/1 takes a single argument that will be a list of binary strings which are the arguments that were passed to the task on the command line.

When you invoke mix hello, this task will run and print Hello, World!. Mix uses its first argument (hello in this case) to lookup the task module and execute its run function.

You’re probably wondering why we have a @moduledoc and @shortdoc. Both are used by the help task for listing tasks and providing documentation. The former is used when mix help TASK is invoked, the latter in the general listing with mix help.

Besides those two, there is also @hidden attribute that, when set to true, marks the task as hidden so it does not show up on mix help. Any task without @shortdoc also won’t show up.

Common API

When writing tasks, there are some common mix functionality we would like to access. There is a gist:

  • Mix.project - Returns the project configuration under the function project; Notice this function returns an empty configuration if no mix.exs file exists in the current directory, allowing many Mix functions to work even if a mix.exs project is not defined;

  • Mix.Project.current - Access the module for the current project, useful in case you want to access special functions in the project. It raises an exception if no project is defined;

  • Mix.shell - The shell is a simple abstraction for doing IO in Mix. Such abstractions make it easy to test existing mix tasks. In the future, the shell will provide conveniences for colored output and getting user input;

  • Mix.Task.run(task, args) - This is how you invoke a task from another task in Mix; Notice that if the task was already invoked, it works as no-op;

There is more to the Mix API, so feel free to check the documentation, with special attention to Mix.Task and Mix.Project.

Namespaced Tasks

While tasks are simple, they can be used to accomplish complex things. Since they are just Elixir code, anything you can do in normal Elixir you can do in Mix tasks. You can distribute tasks however you want just like normal libraries and thus they can be reused in many projects.

So, what do you do when you have a whole bunch of related tasks? If you name them all like foo, bar, baz, etc, eventually you’ll end up with conflicts with other people’s tasks. To prevent this, Mix allows you to namespace tasks.

Let’s assume you have a bunch of tasks for working with Riak.

defmodule Mix.Tasks.Riak do
  defmodule Dostuff do
    ...
  end

  defmodule Dootherstuff do
    ...
  end
end

Now you’ll have two different tasks under the modules Mix.Tasks.Riak.Dostuff and Mix.Tasks.Riak.Dootherstuff respectively. You can invoke these tasks like so: mix riak.dostuff and mix riak.dootherstuff. Pretty cool, huh?

You should use this feature when you have a bunch of related tasks that would be unwieldy if named completely independently of each other. If you have a few unrelated tasks, go ahead and name them however you like.

OptionParser

Although not a Mix feature, Elixir ships with an OptionParser which is quite useful when creating mix tasks that accepts options. The OptionParser receives a list of arguments and returns a tuple with parsed options and the remaining arguments:

OptionParser.parse(["--debug"])
#=> { [debug: true], [] }

OptionParser.parse(["--source", "lib"])
#=> { [source: "lib"], [] }

OptionParser.parse(["--source", "lib", "test/enum_test.exs", "--verbose"])
#=> { [source: "lib", verbose: true], ["test/enum_test.exs"] }

Check OptionParser documentation for more information.

Sharing tasks

After you create your own tasks, you may want to share them with other developers or re-use them inside existing projects. In this section, we will see different ways to share tasks in Mix.

As a dependency

Imagine you’ve created a Mix project called my_tasks which provides many tasks. By adding the my_tasks project as a dependency to any other project, all the tasks in my_tasks will be available in the parent project. It just works!

As an archive

Mix tasks are useful not only inside projects, but also to create new projects, automate complex tasks and to avoid repetitive work. For such cases, you want a task always available in your workflow, regardless if you are inside a project or not.

For such cases, Mix allows developers to install and uninstall archives locally. You can generate and archive for a current project and install it locally as:

$ mix do archive, local.install

Archives can be installed from a path or any URL:

$ mix local.install http://example.org/path/to/sample/archive.ez

After installing an archive, you can run all tasks contained in the archive, list them via mix local or uninstall the package via mix local.uninstall archive.ez.

MIX_PATH

The last mechanism for sharing tasks is MIX_PATH. By setting up your MIX_PATH, any task available in the MIX_PATH will be automatically visible to Mix. Here is an example:

$ export MIX_PATH="/full/path/to/my/project/ebin"

This is useful for complex projects that must be installed at /usr or /opt but still hook into Mix facilities.

With all those options in mind, you are ready to go out, create and install your own tasks! Enjoy!

Introduction to ExUnit

ExUnit is a unit test framework that ships with Elixir.

Using ExUnit is quite easy, here is a file with the minimum required:

ExUnit.start

defmodule MyTest do
  use ExUnit.Case

  test "the truth" do
    assert true
  end
end

In general, we just need to invoke ExUnit.start, define a test case using ExUnit.Case and our batch of tests. Assuming we saved this file as assertion_test.exs, we can run it directly:

bin/elixir assertion_test.exs

In this chapter, we will discuss the most common features available in ExUnit and how to customize it further.

Starting ExUnit

ExUnit is usually started via ExUnit.start. This function accepts a couple options, so check its documentation for more details. For now, we will just detail the most common ones:

  • :formatter - When you run tests with ExUnit, all the IO is done by the formatter. Developers can define their own formatters and this is the configuration that tells ExUnit to use a custom formatter;

  • :max_cases - As we are going to see soon, ExUnit allows you to easily run tests concurrently. This is very useful to speed up your tests that have no side affects. This option allows us to configure the maximum number of cases ExUnit runs concurrently.

Defining a test case

After ExUnit is started, we can define our own test cases. This is done by using ExUnit.Case in our module:

use ExUnit.Case

ExUnit.Case provides some features, so let’s take a look at them.

The test macro

ExUnit.Case runs all functions whose name start with test and expects one argument:

def test_the_truth(_) do
  assert true
end

As a convenience to define such functions, ExUnit.Case provides a test macro, which allows one to write:

test "the truth" do
  assert true
end

This construct is considered more readable. The test macro accepts either a binary or an atom as name.

Assertions

Another convenience provided by ExUnit.Case is to automatically import a set of assertion macros and functions, available in ExUnit.Assertions.

In the majority of tests, the only assertion macros you will need to use are assert and refute:

assert 1 + 1 == 2
refute 1 + 3 == 3

ExUnit automatically breaks those expressions apart and attempt to provide detailed information in case the assertion fails. For example, the failing assertion:

assert 1 + 1 == 3

Will fail as:

Expected 2 to be equal to (==) 3

However, some extra assertions are convenient to make testing easier for some specific cases. A good example is the assert_raise macro:

assert_raise ArithmeticError, "bad argument in arithmetic expression", fn ->
  1 + "test"
end

So don’t forget to check ExUnit.Assertions' documentation for more examples.

Callbacks

ExUnit.Case defines four callbacks: setup, teardown, setup_all and teardown_all:

defmodule CallbacksTest do
  use ExUnit.Case, async: true

  setup do
    IO.puts "This is a setup callback"
    :ok
  end

  test "the truth" do
    assert true
  end
end

In the example above, the setup callback will be run before each test. In case a setup_all callback is defined, it would run once before all tests in that module.

A callback must return :ok or { :ok, data }. When the latter is returned, the data argument must be a keywords list containing metadata about the test. This metadata can be accessed in any other callback or in the test itself:

defmodule CallbacksTest do
  use ExUnit.Case, async: true

  setup do
    IO.puts "This is a setup callback"
    { :ok, from_setup: :hello }
  end

  test "the truth", meta do
    assert meta[:from_setup] == :hello
  end

  teardown meta do
    assert meta[:from_setup] == :hello
  end
end

Metadata is used when state need to be explicitly passed to tests.

Async

Finally, ExUnit also allows test cases to run concurrently. All you need to do is pass the :async option set to true:

use ExUnit.Case, async: true

This will run this test case concurrently with other test cases which are async too. The tests inside a particular case are still run sequentially.

Lots To Do

ExUnit is still a work in progress. Feel free to visit our issues tracker to add issues for anything you’d like to see in ExUnit and feel free to contribute.

Crystal

The Crystal programming language.


  1. Edsger W. Dijkstra was indertijd een hevig tegenstander van het gebruik van de goto↩︎

  2. Dit soort constructies zou anno 2012 absoluut moeten vermeden worden. ↩︎

  3. Procedure is synoniem van methode, functie of subroutine. ↩︎