Introduction
This text gives an overview for the following programming languages:
- C
- C++
- Java
- Bash
- Python
- Ruby
- Javascript
- Dart
- Ocaml
- Go
- Rust
- Clojure
- Scheme
- Coloru
- Haskell
- XSL
- Prolog
- Erlang
- Elixir
- Crystal
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
orlong long int
L
- type:
long int
orlong long int
LL
- type:
long long int
U
- type:
unsigned int
,unsigned long int
orunsigned long long int
UL
- type:
unsigned long int
orunsigned 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 ofint
l
long
instead ofint
printf("%ld", 0x1234L);
L
long double
in plaats vandouble
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, &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 =&((*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) \* & 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 ditint
.
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 woordvirtual
-
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
andbool
. 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
, andprivate
. 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 unlesscondition
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
ory
isnull
, returntrue
if both arenull
, andfalse
if only one isnull
. -
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 aPerson
, the first example (with is) does nothing; the second (withas
) 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
andelse
-
for
loops -
while
anddo-while
loops -
break
andcontinue
-
switch
andcase
-
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 aList<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
Links
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 thenbrew install erlang-r16
-
If you have a previous Erlang version installed, unlink it with
brew uninstall erlang
and link the new one withbrew 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
, Reference
s and PID
s 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
andor
in Elixir actually map to theandalso
andorelse
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
andset_elem
functions instead of Erlang’selement
andsetelement
. The reason for this choice is that Elixir attempts to normalize Erlang API’s to always receive thesubject
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
orsize
. Their usage is not arbitrary. The first is used when the number of elements needs to be calculated. For example, callinglength(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 usesize
to get the size of a binary, which is cheap, but retrieving the number of unicode characters usesString.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 theEnum
module which provides functions likemap
,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 callinginspect
directly and passingraw
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 theto_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:
-
mix.exs
- compiles the dependency directly with Mix by invoking thecompile
task; -
rebar.config
orrebar.config.script
- compiles usingrebar compile deps_dir=DEPS
, whereDEPS
is the directory where Mix will install the project dependencies by default; -
Makefile
- simply invokesmake
;
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 bymix 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 call
s 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 functionproject
; Notice this function returns an empty configuration if nomix.exs
file exists in the current directory, allowing many Mix functions to work even if amix.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.
-
Edsger W. Dijkstra was indertijd een hevig tegenstander van het gebruik van de
goto
. ↩︎ -
Dit soort constructies zou anno 2012 absoluut moeten vermeden worden. ↩︎
-
Procedure is synoniem van methode, functie of subroutine. ↩︎