C(++) programmeren
- Introductie
- Compiler en linker
- Declaraties
- Operators
- Functies
- Flow of control statements
- Invoer en uitvoer
- Arrays
- Structures
- Bestanden (files)
- Pointers
- Assembly code in je programma opnemen
- Aandachtspunten
- Stack frame
- Memory allocation
- Links
Introductie
Compiler en linker
De meest gebruikte IDE (Integrated Development Environment) is waarschijnlijk
Visual Studio
(van Microsoft), en daar heb ik een
aparte pagina aan gewijd.
Hieronder wat summiere informatie over een Borland compiler en linker. Beide zijn overigens gratis, al heb je dan wel een beperkte versie.
BCC32 (Borland)
Je kan deze compiler downloaden.
Na het installeren voeg je de map met de executables
(C:\Borland\BCC55\Bin
) toe aan het pad.
De compiler en linker moeten ook include files en library files kunnen vinden. Die info wil je niet steeds meegeven als je de compiler aanroept, dus zet je het in een configuratiebestand.
De config file heeft extensie CFG
, en kan je het
beste of in de map met executables, of in de map met de te compileren
programma's zetten.
Als je ook de naam van het configuratiebestand niet steeds wil hoeven opgeven
noem je het BCC32.CFG
In dit bestand zet je o.a. de volgende 2 regels:
-Ic:\Borland\BCC32\Include
en
-Lc:\Borland\BCC32\Lib
Foutmeldingen
- Fatal: Unable to open file 'C0W32.OBJ'
- Je moet de map met library files aan de linker opgeven.
- Too many types in declaration in function main
- Ik gebruikte een type dat blijkbaar niet bestaat.
Dat gebeurde in het volgende statement:
printf( "\nA long long is %d bytes\n", sizeof( long long));
- Unable to open include file 'stdio.h'
- Je moet de map met include files aan de linker opgeven.
Declaraties
Variabelen
Bij het declareren van variabelen kan je kijken naar inhoud (teken, geheel getal, etc.) of naar scope (bestaat in het hele programma, alleen binnen een functie, etc.).
Inhoud van een variabele
Van klein naar groot heb je de volgende types voor gehele getallen: (unsigned) char, (unsigned) int, (unsigned) short en (unsigned) long.
Voor niet gehele getallen heb je float en double.
Tenslotte kan je ook getallen van 1, 2 of meer bits definieren, bv. als volgt:
unsigned int a : 1; // getal van 1 bit
unsigned int b : 7; // getal van 7 bits
Scope van een variabele (waar is hij geldig)
Deze indeling is ook gerelateerd aan het gebruik van storage. De opties zijn global, static, local of auto en register. En je hebt ook nog thread local storage.
Een local of automatic variabele is een variabele die gedeclareerd is in een functie, en daarbuiten niet zichtbaar is.
Een global variabele is er 1 die buiten functies gedeclareerd wordt.
Om een static variabele te declareren moet je het keyword
static
voor het type zetten.
Een static variabele wordt 1 keer geïnitialiseerd, maar behoudt ook
buiten de functie waarin hij gedeclareerd is zijn waarde.
Een register variabele zal indien mogelijk in een CPU register gezet worden.
Ook hier moet je het keyword register
voor het
type zetten.
Voorbeelden
Een declaratie ziet er als volgt uit:
type var [= waarde];
Je kan meerdere variabelen op 1 regel declareren, en ze scheiden door een komma.
Voorbeelden:
int teller=0;
double procent = 0.34;
unsigned int a, b, c;
Constantes
Ik heb het hier alleen over symbolische constantes (i.t.t. tot literal constants, constante waardes die in het programma voorkomen). Deze kan je op 2 manieren declareren.
De 1e is met een #define
De algemene structuur is: #define CONSTNAAM
waarde
Voorbeeld: #define PI 3.14
De 2e is met het keyword const
De algemene structuur is: const type
constnaam = waarde
Voorbeeld: const int teller = 100
Operators
Complexe operators
Conditional operator
De syntax is exp1 ? exp2 : exp3
Als exp1
waar is heeft het geheel de waarde
van exp2
, anders die van
exp3
Zo zou je bv. kunnen kijken welke van 2 variabelen de laagste waarde heeft, en
die dan aan een derde kunnen toekennen:
c = (a < b)? a : b;
Dit is equivalent aan
if (a < b)
c = a;
else
c = b;
Komma operator
Functies
Definitie en prototype
Voor je een functie definieert, moet je hem declareren. Dat heet een prototype.
Het prototype ziet er precies zo uit als de 1e regel van de functiedefinitie, met 1 verschil: er staan een ';' aan het eind.
Vbd.: int multiply(int x, int y);
Resultaat van een functie
Als je wilt dat de functie een resultaat teruggeeft zet je het type daarvan voor de naam van de functie (zoals de 1e 'int' hierboven).
In de functie moet in dat geval een return expr
staan, waarbij de expressie hetzelfde type heeft.
De uitwerking van een functie kan natuurlijk ook zijn dat hij bv. iets
afdrukt.
In dat geval kan je als resultaattype void
gebruiken.
Tip: je kan het beste maar 1 return
in een functie
gebruiken.
De functie main
hoeft niet perse aan het begin
van het programma te staan, maar het is wel gebruikelijk.
Parameters en argumenten
Parameters zijn de dingen die tussen haakjes in de functie definitie of prototype staan.
Argumenten zijn de waardes die je daadwerkelijk meegeeft als je een functie aanroept.
Flow of control statements
Je kan een groffe onderverdeling maken in statements om loops mee te maken, en andere statements om de flow of control mee te veranderen.
Loop statements
Het for
statement is het beste
als je in de loop een teller moet veranderen (of iets soortgelijks) en vooraf
iets moet initialiseren.
De do... while
is handig
als je een statement minimaal 1 keer moet uitvoeren.
Je kan de loops wel binnen elkaar gebruiken, maar niet overlappen. Dus dat je loop1 begint, binnen loop1 ook loop2 begint, dan loop1 eindigt, en dan pas loop2 eindigt.
For statement
for (initialisatie; conditie; increment)
{
statements;
}
Eerst wordt de initialisatie uitgevoerd. Vervolgens wordt de conditie getest. Als ie waar is wordt het statement uitgevoerd, en wordt tenslotte increment uitgevoerd. Daarna wordt de conditie opnieuw getest, etc.
Vbd.: het afdrukken van de getallen 1 t/m 10:
for (cnt = 1; cnt <= 10; cnt++)
printf("%d\n", cnt);
While statement
while (conditie)
{
statements;
}
Het while
statement is in feit een simpele vorm
van het for
statement.
Het do... while statement
do
{
statements;
}
while (conditie);
In deze constructie wordt in tegenstelling tot de andere 2 het statement minimaal 1 keer uitgevoerd.
Diversen
Continue statement
Als je in een loop een continue
-statement plaatst,
wordt de rest van de loop overgeslagen voor die ene iteratie van de loop.
Break statement
Met een break
verlaat je de loop.
If statement
if (conditie)
{ statements
}
else if (conditie)
{ statements
}
else
{ statements
}
Switch statement
switch (variable)
{
case value:
statements;
break;
case value:
statements;
break;
case default:
statements;
break;
}
? : statement
(condition) ? (statement1) : (statement2)
statement1 wordt uitgevoerd als de condition waar is, anders wordt statement2 uitgevoerd.
Goto statement
goto label1
statements1
label1:
statements2
De 1e groep statements wordt niet uitgevoerd, omdat het goto-statement zorgt voor een sprong naar label1.
Invoer en uitvoer
De belangrijkste functies zijn printf
(print
formatted) en puts
(put string) voor uitvoer,
en scanf
(scan formatted) en
gets
(get string) voor invoer.
Om deze functies te gebruiken moet je aan het begin van je programma de
regel
#include stdio.h
opnemen.
Dit is het bestand met de prototypes van deze functies.
Format specifiers
In printf
en scanf
is de 1e parameter een string met format specifiers.
In deze string is van elke volgende parameter het type vermeld.
De format specifier begint met %, en je hebt de volgende mogelijkheden:
- %s: Character string (tekenreeks);
- %c: 1 character (char);
- %d: Decimaal getal (int of short);
- %f: Floating point getal (float, double);
- %u: Unsigned getal (unsigned int, unsigned short);
- %ld: Signed decimaal getal (long);
- %lu: Unsigned decimaal getal (unsigned long).
Puts
Deze functie drukt gewoon een string af, met een newline aan het eind.
Gets
Printf
Voorbeeld:
printf("Het product van %d en %d is: %ld\n", x, y, x*y);
Scanf
In tegenstelling tot printf
moeten de 2e en volgende
parameters niet bestaan uit variabelen (of expressies), maar uit adressen
van de variabelen waarin de invoer moet worden opgeslagen.
Voorbeeld:
scanf(Voer 2 getallen in om met elkaar te vermenigvuldigen: %d %d", &x, &y);
De scheiding tussen verschillende in te voeren waardes moet bestaan uit "white space". Alles waar geen tekens staan is white space, dus het gaat spaties maar ook newlines.
2 in te voeren waardes kan je dus scheiden door een willekeurig aantal spaties, maar je kan ze ook op verschillende regels zetten, desnoods met een aantal lege ertussen.
Arrays
Declaratie
Een gewone array (met bv. gehele getallen) declareer je als volgt:
int array[30];
Deze array heeft 30 elementen, genummerd van 0..29
Een meerdimensionale array declareer je als volgt:
int array[12][31];
(als je bv. een element wilt hebben voor elke dag van het jaar, geindexeerd
op maand en dag).
Veel compilers staan niet toe dat je in de definitie een constante als index
gebruikt, tenminste als die met een const
is
gedefiniëerd.
#define
mag wel.
Initialisatie
Een array initialiseren gaat als volgt:
int array[4] = {1, 2, 3, 4};
Een meerdimensionale array kan je zo initialiseren:
int array[2][3] = {1, 2, 3, 4, 5, 6};
of zo:
int array[2][3] = {{1, 2, 3}, {4, 5, 6}};
of zo:
int array[2][3] = {{0}};
Als je te weinig waarden opgeeft blijven er gewoon een aantal elementen van de array in ongedefinieerde staat.
Strings (tekenreeksen)
Strings zijn gewoon een speciaal geval van arrays.
Met het statement
char hallo[]="Hallo wereld!";
maak je een array aan, waarvan de waarden worden
geïnterpreteerd als tekens.
hallo[4]
bevat dus het teken
'o'
.
Een tekenreeks wordt altijd beëindigd met een 0
.
In hallo[13]
zit dus de waarde 0
.
Je kan de hele tekenreeks in 1 keer afdrukken met
printf(stringnaam)
.
Als je individuele tekens als teken wilt afdrukken, kan je iets doen als
printf(%c, hallo[1]);
In dit geval zou het resultaat 'a'
zijn.
Je kan een string als volgt initialiseren met lege waardes:
char hallo[20] = {0};
String functies
Om gebruik te kunnen maken van string functies moet je
#include <string.h>
bovenaan in je programma opnemen.
Sommige van de oorspronkelijke stringfuncties, zoals
scanf
, zijn vanwege security redenen vervangen
door modernere varianten.
Je kan ze nog wel gebruiken, maar dan moet je de controle erop in
je IDE uitschakelen.
String copy (strcpy)
Hiermee kopiëer je een constante string naar een string array, bv.:
char testStr[20] = {0};
strcpy (testStr, "hallo");
String concatenation (strcat)
Hiermee plak je de ene string aan de andere vast. De doelstring staat vooraan, bv.:
char testStr1[20] = "hallo ";
char testStr2[20] = "wereld!";
char testStr3[20] = {0};
strcat(testStr3, testStr1);
strcat(testStr3, testStr2);
printf(testStr3);
De uitvoer is nu "hallo wereld!"
String compare (strcmp)
2 strings worden vergeleken.
Het resultaat is 0 als ze gelijk zijn.
Het resultaat is negatief als het 1e ongelijke teken een lagere waarde heeft in de 1e string dan in de 2e, anders positief.
Structures
Een array is een collectie van gelijksoortige elementen (allemaal gehele getallen, of allemaal tekens), een structure is een verzameling ongelijksoortige elementen (vergelijk het met een handtas waar van alles in zit).
Voorbeeld:
struct testStruct
{
int a;
char b;
float c;
char d[3];
}
Je initialiseert ze net als arrays:
testStruct test1 = {13, 'b', 3.59, "aap"};
Je refereert aan de elementen van een structure m.b.v. de naam van de structure,
dan een punt, en dan de naam van het veld waar je iets mee wilt doen.
In bovenstaand voorbeeld bv. test.b
of
test.d[2]
Bestanden (files)
File functies
Openen van een bestand (fopen)
Lezen van een bestand (fread)
Een voorbeeld van lezen:
FILE * pFile;
char buffer[20] = {0};
pFile = fopen("testfile.txt", 'r');
pFile = fread("testfile.txt", 1, sizeof(buffer), pFile);
fclose(pFile);
Schrijven naar een bestand (fwrite)
Een voorbeeld van schrijven:
FILE * pFile;
char buffer[] = "Hello";
pFile = fopen("testfile.txt", 'w');
pFile = fwrite("testfile.txt", 1, sizeof(buffer), pFile);
fclose(pFile);
Pointers
Een pointer definieer je als volgt:
int *xptr
xptr
is de naam van de pointer,
int
het type.
De compiler is niet erg kieskeurig over waar je de asterisk in de declaratie
precies zet, dus bv. de volgende opties zijn ook OK (zij het verwarrend):
int* xptr
int*xptr
int (*xptr)
En zo geef je hem een waarde:
int x;
xptr = &x;
De variabele (pointer) xptr
bevat nu het
adres van variabele x
In dit stadium refereren &x
en
xptr
beide aan het adres van x
,
en x
en *xptr
beide aan
de waarde van x
Pointers en arrays
Stel je hebt een 2-dimensionale array
int matrix[10][20];
Je benadert een element bv. met
matrix[4][2]
De naam van een array is een pointer naar het begin van de array.
In het geval van bovenstaande meerdimensionale array is er een verschil
tussen int (*matrix)[20]
en
int *matrix[20]
.
Het 2e geval is een array van 20 pointers, het 1e van een pointer naar
een array van 20 integers.
Als je de geheugenruimte dynamisch wilt alloceren, kan je ook
dit doen:
int (*matrix1)[20] = (int (*)[20]) malloc(20*10*sizeof(int))
In C++ typecasting (het stuk tussen haakjes na het '='-teken) is verplicht.
Voorbeeld van invalid address
De volgende code produceert een fout:
int *ptr = 100;
*ptr = 7;
Je probeert de waarde 7 te schrijven naar het adres dat in de variable
ptr
zit, nl. 100.
En dat adres is niet gealloceerd.
Alle pointers zijn 32 bits groot in een 32-bit besturingssysteem. Het verschil merk je wanneer je berekeningen gaat uitvoeren met pointers. Afhankelijk van het datatype waar hij naar wijst kan het verhogen van 1 leiden tot het verhogen met 1, 2, 4, etc.
Dus na
int *xptr = 100;
xptr = xptr+1;
is de waarde van xptr
met 4 verhoogd (omdat een
integer 4 bytes in beslag neemt).
Met xptr + 1
vertel de je de compiler niet dat
hij naar het volgende adres moet gaan, maar naar het adres van de volgende
integer.
Assembly code in je programma opnemen
Dat doe je als volgt:
__asm
{
assembly instructies
}
In het stuk assembly code kan je gewoon refereren aan bv. een variabele
die je ervoor gedeclareerd hebt.
Dus met een instructie als:
MOV a,EAX
(waarbij a
de variabele is).
Aandachtspunten
Expressies hebben een waarde
Een statement als x = 5
kent aan de variabele x
de waarde 5
toe.
Maar de expressie als geheel heeft ook de waarde 5
.
Je kan dus ook het volgende statement maken:
y = x = 5
(y
krijgt ook de waarde 5
)
De expressie x == 5
heeft de waarde
0
of 1
False of true (onwaar of waar)
Elke waarde ongelijk aan 0 is waar, de waarde 0 is onwaar.
Array index
De laagste index in een array is 0.
Als je een array van 500 elementen declareert, loopt de index range van 0 t/m 499.
Calling conventions
In C++ beheert de caller de stack, en moet dus ook de stack weer opruimen na afloop van de call. Vaak voert de callee (de aangeroepen functie) deze taken uit.
Het voordeel van de C-methode is dat je een variabel aantal argumenten mee kan geven aan de call. Het nadeel is de dat de code groter wordt.
Je bepaalt welke conventie gebruikt wordt door te kiezen tussen 1 van deze 2 vormen van declaratie:
int _stdcall funca(...)
int _cdecl funcb(...)
Troubleshooting
Syntax
In geval van syntaxfouten, kan je het beste eerst checken of alle haakjes (zowel de gewone, als de curly braces (zoals '}') in balans zijn.
Een andere, veel voorkomende, fout is het ontbreken van een ';' (semicolon, puntkomma) aan het eind van een statement.
Verder kan je checken of alle variabelen gedeclareerd zijn. Daarbij moet je bedenken dat C(++) case sensitive is!!!
Stack frame
Voor debugging doeleinden is het goed te weten hoe de stack in elkaar zit.
Die bestaat uit (van hoge naar lage adressen) achtereenvolgens:
- De argumenten van een functie-aanroep;
- Het return-adres (het adres van de eerstvolgende instructie na de funtie-aanroep);
- De waarde van het register EBP (base pointer)
Of deze waarde wordt opgeslagen hangt af van de calling convention, etc.; - De lokale variabelen van de aangeroepen functie.
Memory allocation
Global en static variabelen zijn onderdeel van de binary, en worden er tegelijk mee geladen.
Stack variabelen worden tijdens execution time geladen, en bestaan maar kort. Als je een hele grote variabele (array, etc.) lokaal binnen de functie declareert, loopt je kans op een stack overflow. In dat geval kan je hem beter globaal declareren, dan wordt hij gewoon statisch onderdeel van het programma. Op de heap kan ook, zie hieronder.
Dynamic of heap allocation wordt gedaan door C-functies als malloc of calloc. Een heap is een pool of lijst van allocated blokken geheugen. Ze blijven bestaan tot er een free call gedaan wordt.
Memory functies
Malloc
int *xptr = (*int)malloc(2000);
int *yptr = (*int)malloc(2000);
stdlib.h
is nodig voor deze call.
Memcpy
Kopiëert het geheugen van 1 plek naar een andere.
memcpy(yptr, xptr, 500);
Memset
Geeft bepaalde geheugencellen een waarde.
memset(xptr, 'z', 200);
string.h
is nodig voor deze call.
Links
- C/C++ Programming Series 1 Part 11 - Strings in C programming
- Een uitgebreide serie YouTube-filmpjes (sommige meer dan een half uur)
over programmeren in C, waarbij je ook veel leert over
Visual Studio
. - Retrieving the Last-Error Code
- Documentatie op de site van Microsoft over het ophalen van de laatst opgetreden fout.
Reageer via E-mail (dalmolen@xs4all.nl)
Deze pagina is voor het laatst gewijzigd op: 24-01-23 20:37:58