Werken met pointers

advertisement
Werken met pointers, structuren en bestanden.
”Het .BMP bestandsformaat”
CVO/KHBO, progr C/C++
H. OPSTAELE
Pointers
Pointers worden gebruikt voor:
1. Dynamische gegevensstructuren, waar de ene structuur verwijst naar een andere
structuur (bvb linked-list, B-trees, bestandsformaten, etc. )
2. Doorgeven van parameters in een functie
a. “Call-by-reference”
b. Snelheidsredenen bij het doorgeven van grote structuren
void func(int *a, int b)
{
*a = 10;
b = 11;
}
void main()
{ int x=1,y=2;
func( &x, y ) ;
cout << x << ‘ ‘ << y << endl ;
return 0 ;
//uitvoer : 10 2
}
3. De definitie van een array bevat ALTIJD een verborgen pointer
int a[10]; // de variabele “a” is een POINTER naar een
tabel met 10
integers
a[0] = 99; //zet eerste element van array a op 99
*a = 99;
//idem
a[2] = 123; //zet derde element van a op 123
*(a+2) = 123; //idem
4. Bij dynamische creatie (ie. alloceren van geheugen) [zie later!]
Wat is een pointer?
Een pointer is niet meer dan een geheugenadres (32 bits of 4 bytes op een Pentium
machine). Het kan gewoon uitgeschreven worden met via het cout object (de uitvoer is
by default in hexadecimaal notatie.) Pointers zijn eigenlijk gewoon integer getallen (int),
maar mogen nooit als gewone integers gebruikt worden in berekeningen (zoals
vermenigvuldiging ed.) !!! Een pointer is verbonden aan een bepaald type (C++ is een
“strongly typed” programmeertaal). Dit in tegenstelling tot VB die bvb een “Variant”
type bevat (VB is ‘loosly typed’).
int *p;
cout << p;
Een point wijst altijd naar een bepaald type: een int, een double, een structuur,... Bij
declaratie moet een pointer-variabele áltijd voorafgegaan worden door een asterix ( ‘*’ ).
struct rec_t {
int a ; //veld ‘a’ van type integer
int b; //veld ‘b’
} ;
double dbl, * pdbl=dbl;
rec_t *pRec=NULL ;
int *px,x,y,*py ;
Noot : Eén uitzondering. het is mogelijk een pointer te hebben zonder type. Dit is dan
een void-pointer. Conversie van een bepaald type pointer naar een ander type pointer is
mogelijk maar gevaarlijk! (er bestaan speciale operatoren voor, maar dit is buiten het
bereik van deze les).
char buf[10];
void *ptr=(void*)(buf+5);
Het is een goede gewoonte een pointer ALTIJD een waarde te geven. Veel fouten in
programma’s komen net door niet-geinitialiseerde pointers. Als de pointer (nog) naar
niets wijst zet je die op 0 (of op NULL).
int *p; //niet geinitialiseerd!!!!
int *p=NULL; //ok
cout << p ;
Werken met pointers
Belangrijk is hier om een grafisch schema bij te houden waar een pointer naartoe wijst.
“Adres-Van”-operator &
Geeft het adres van een variabele (bij assignatie aan een pointer)
int *px;
int x;
px = &x;
“Inhoud-van”-operator *
Geeft de data terug waar een pointer naartoe wijst, om deze te zetten (assigneren) en op te
vragen.
int x=9;
int *px=&x;
cout << x << *px <<endl;
*px=10 ;
cout << x << *px <<endl;
Belangrijk: De compiler zal nooit een “inhoud-van” (of dereference-) -operator in
vraag stellen, maar als je verwijst naar geheugen dat niet het u is zal je een “crash” (of
‘access violation’) krijgen in het beste geval.
char *p=NULL ;
*p=123;
=> ACCESS VIOLATION!
Opmerking
Verwar de volgende code niet :
int a=1, b=2;
int *p1=&a,*p2=&b;
(1) p1=p2 ;
(2) *p1 = *p2 ;
(1) Zet pointer p1 gelijk aan pointer p2. Vanaf nu wijzen p1 en p2 naar dezelfde
geheugenlocatie (di. variabele b).
(2) Zet de inhoud van p1 gelijk aan de inhoud van p2 (p1 wijst nog altijd naar ‘a’ en p2
naar ‘b’). De inhoud van ‘a’ is dus veranderd.
Werken met struct’s en class’es
Als je de members van een struct of class wilt aanspreken via een pointer, dan kan dit
gemakkelijk met de -> operator.
struct rec_t {
int a ; //veld ‘a’ van type integer
int b; //veld ‘b’
} ;
class base_t {
int a;
public:
int getA() { return a; }
};
rec_t
rec_t
pr->a
pr->b
r;
*pr ;
= 10 ;
= 11 ;
cout << pr->a
cout << r.a ;
//idem
base_t b, *pb =&b;
cout << b->getA() ;
Dynamische creatie van geheugen
Geheugen wordt ge”alloceert” op de heap. De heap bevat het volledige geheugenbereik
van 4Gb dus met inbegrip van virtueel geheugen. Door zeer veel geheugen in te nemen
kan je dus effectief andere processen zónder resources zetten of vertragen. Geheugen moet
op tijd worden vrijgegeven.
Arrays worden aangemaakt met de [ ] operator.
Initialisatie van het type kan door de waarde tussen haakjes te geven
char *p=new char[-1L];
//alloceer 4gigabyte aan data
int
*pi=new int(2);
//alloceer geheugen en zet int waarde op 2
cout << *pi; //uitvoer: 2
Als je meer geheugen vraagt dan er voor handen is, zal de new-operator een NULL ( 0 )
teruggeven.
int *pi=new int;
if (pi==NULL) {
cout << “onvoldoende geheugen..”;
}
Geef het geheugen terug vrij.
Als je dit vergeet dan wordt dit een “geheugenlek” genoemd.
Voor arrays:
int *pa=new int[123];
delete []pa;
pa = NULL;
//pa wijst naar niets
Voor basistypes, structs en klassen
rec_t *p=new rec_t(1,1);
delete p;
p=NULL;
//p wijst naar niets
Pointer berekeningen
Enkel het optellen is een zinnige bewerking op pointers. Pointers met elkaar aftrekken is
evenwel ook mogelijk.
int *p = new int[10];
int *q=p;
*p = 1; //p[0]=1
p++;
*p = 2;
//p[1]=2
p = p+2;
*p = 4 ; //p[3]=4
int delta=q-p; //delta == 3
sizeof-operator
sizeof geeft het aantal bytes terug dat een variabele of een type in het geheugen
inneemt.
int x;
double *p=0;
struct rec_t {
char c ;
int a ;
short b ;
} ;
sizeof(x) ; //4 op Pe
sizeof(p) //4 op Pe
sizeof(double) //8 op Pe
sizoef(rec_t)
//7 op Pe AFH. van MEMORY ALIGNMENT
// (zie onder)!!!
Drie effecten moeten in acht genomen worden:
(A) De groote van int, pointers, ed. zijn STERK processor afhankelijk (soms 16-, soms
24-, soms 32- en soms 64bits). Vooral met de komst van de 64bits machines is dit
belangrijk!!
(B) Alhoewel een array eigenlijk niet meer is dan een pointer naar geheugen, zal de
sizeof-operator toch te grootte van de array zelf geven
char buf[99];
cout << sizeof(buf); //uitvoer: 99
(C) De compiler herschikt het geheugen zodat die efficiënt naar de processor toe is (zie
artikel onderaan). Dit is vooral van belang voor structuren en klassen; deze techniek
wordt dan “padding” genoemd.
Noot: Deze memory-alignment van de compiler kan afgezet worden.
In
Visual
C++
onder
Settings/Code
Generation,
selecteer
je 1 byte onder ‘structure alignment’.
In Cygwin (GNU compiler) geef de vlag –fpack-struct mee.
5. ALIGNMENT
============
http://www.gamedev.net
All data in RAM should be aligned to adresses divisible by 2, 4, or 8
according to this scheme:
operand size
alignment
--------------------------1 (byte)
1
2 (word)
2
(or adress mod 4 >< 3. other proc. require align by 2)
4 (dword)
4
6 (fword)
4
(Pentium Pro requires alignment by 8)
8 (qword)
8
10 (tbyte)
8
(Pentium Pro requires alignment by 16)
Misaligned data will take at least 3 clock cycles extra to access. Aligning code is
not necessary when you run on the Pentium, but for optimal performance on other
processors you may align subroutine entries and loop entries by 8 or 16.
Bestanden Schrijven en Lezen
Verreweg de eenvoudigste manier om een bestand te lezen of te schrijven is door gebruik te
maken van de ‘buffered file access’ van C.
Men dient:
0. De header file <stdio.h> includeren
1. Een bestand openen
2. Schrijven en/of lezen
3. Het bestand sluiten
#include
In de code de header-file <stdio.h> te includeren
#include <stdio.h>
Bestand openen
Een bestand te openen met de functie fopen(…).
Als de functie succesvol is, geeft deze een file descriptor terug (een pointer naar de
structuur FILE). Als het openen niet lukt, krijg je 0 (= de constante NULL) terug.
Voorbeeld
FILE *f;
//Open een bestand (testje.txt) om er naar te schrijven (mode “wb+”)
//Als het bestand al bestond wordt die overschreven..
f = fopen(“testje.txt”,”wb+”);
// f is de file descriptor
if (NULL==f) {
printf(“niet gelukt!\n”);
} else {
printf(“bestand aangemaakt!\n”);
}
De modus is een korte string die weergeeft wat je met het bestand zal doen.
Voorbeelden:
Modus
“wb+”
“rb”
“rb+”
Omschrijving
Binair data schrijven en lezen. Als het bestand
al bestond wordt die overschreven..
Binaire data lezen (NIET schrijven)
Binaire info lezen en schrijven. Als het
bestand al bestond wordt die niet vernietigd.
Belangrijk bij bestandsnamen:
Als men een bestandsnaam gebruikt zonder pad aanduiding, dan zoekt het systeem
altijd vanuit de werkdirectory (di. de plaats van het project of ook aan te passen via de
Project Settings-dialoog in VC).
Wil men toch een ander pad gebruiken dan kan di, maar onthou dat de backslash (\)
een speciale betekenis heeft in C; Gebruik de dubbele backslash in de plaats (\\) of
UNIX-stijl paden is ook mogelijk (voorwaarts slash (/) ipv. backslash…)
bvb. fopen(“c:/Mijn Documenten/testje.txt”,”r”); //UNIX stijl
of fopen(“c:\\Mijn Documenten\\testje.txt”,”r”); //UNIX stijl
Lezen & Schrijven
Naar het bestand lezen of schrijven is afhankelijk van de modus waarin je het
bestand geopend hebt.
Informatie wordt steeds aan het eind van een bestand toegevoegd (kan ook anders,
met de fseek-functie ).
Tekst schrijven kan je met de varianten van printf en scanf: fprintf en fscanf
Normaal, naar scherm…
Naar bestand met file descriptor f
printf(“hallo wereld!”);
fprintf( f , “hallo wereld!”);
printf(“test, integer: %d float:
%f!\n”,11,3.14);
fprintf( f, “test, integer: %d
float: %f!\n”,11,3.14);
scanf(“%d”, & a);
fscanf(
f , " %d ", &a) ;
Wil men geen tekst maar binaire informatie in een bestand schrijven (zoals structuren,
variabelen ed.) dan kan je gebruik maken van de functies fwrite (schrijven) en fread
(lezen).
fread( <adres van variabele> , <grootte van de variabele> , <aantal variabelen> , <file descriptor>);
fwrite(<adres van variabele> , <grootte van de variabele> , <aantal variabelen> , <file descriptor> );
Noot: als je variabele geen array is, dan de parameter <aantal variabelen> gelijk aan 1.
Voorbeeld.
#include <stdio.h>
struct Punt {
int x,y,z ;
} ;
int main()
{
FILE *f = fopen(“test.bin”,”rb+”);
int
a, b[100];
float c;
Punt d;
fwrite(&a, sizeof(int) , 1 ,f);
fwrite(b, sizeof(int) , 100 ,f); // de array (b) is autom. een
pointer!!
fwrite(&c , sizeof(float) , 1 ,f);
fwrite(&d , sizeof(Punt) , 1 ,f);
fseek( f , SEEK_SET, 0); //ga naar begin van het bestand.
fread(&d , sizeof(Punt) , 1 ,f);
fclose(f);
}
//sluit bestand
Het bestand sluiten
Als men gedaan heeft , dan moet je het besturingssysteem daarvan op de hoogte
stellen. Dit gebeurt met de fclose( ) functie.
Waarom? wel, verschillende redenen.
1/ Als men schrijft naar een bestand houdt het OS daarmee rekening zodat geen
twee programma’s naar hetzelfde bestand kunnen schrijven (zou onoverzichtelijke
gevolgen kunnen hebben…) Dit wordt een “file lock” genoemd.
2/ In oudere besturingsystemen (DOS, Win3.11, Win95(?)) kan men maar een
beperkt aantal file descriptors tegelijk openstaan hebben. Met fclose geeft men zijn file
descriptor terug op. Bovendien geef je terug een stukje geheugen vrij en is je systeem
terug een beetje efficienter…
3/ Bij gebufferde files, wordt –om snelheidsredenen- niet onmiddellijk naar
harde schrijf geschreven maar blijft de geschreven data in het geheugen “hangen” (di.
de buffer), Er wordt enkel naar Harde Schijf geschreven als het besturingssysteem
beslist dat het écht nodig is. Men kan met functies aan het systeem eisen dat data
effectief wordt weggeschreven:
fflush( < file descriptor> );//alle data totnutoe effectief dumpen op de HD
en
fclose( < file descriptor> );//alle data effectief naar HD schrijven en sluiten
Noot: Bij het (normaal) verlaten van een programma worden de bestanden ook
automatisch mooi gesloten. Als het programma crasht dan gebeurt dit niet…
fclose( <file descriptor> );
Voorbeeld
fclose( f );
Windows Bitmap (.BMP/.DIB)
Hieronder wordt het 24 bits formaat van de Windows bitmap beschreven (ie. extensie .BMP
of .DIB).
Een 24-bits bitmap is een ‘2-dimensionele matrix’ van H hoog en B breed, waarvan iedere
pixel wordt voorgesteld door 3 bytes ( 1 byte roodwaarde, 1 byte groenwaarde en 1 byte
blauwwaarde). De totale array grootte is dus 3*H*B.
struct RGB {
char r,g,b;
};
RGB bmp[H][B];
Het BMP-bestandsformaat wordt voorafgegaan door twee vaste header’s die direct na elkaar
komen). De structuren worden hieronder gegeven. Achter deze twee headers komt de data.
BMP-HEADER
BMP-HEADERINFO
BMP-24BIT-RAW-DATA
Bekijk ook de hex-dump van een eenvoudige 50x50 witte bitmap.
Meer info op http://members.fortunecity.com/shetzl/bmpffrmt.html
/* BMPHeader
2 bfType 19778 must always be set to 'BM' to declare that this is a .bmp-file.
4 bfSize ??
specifies the size of the entire file in bytes.
2 bfReserved1 0 must always be set to zero.
2 bfReserved2 0 must always be set to zero.
4 bfOffBits 0x36 specifies the offset from the beginning of the file to the bitmap data.
*/
struct BMPHeader /*STRCUTURES MUST BE PACKED*/
{
char type[2];
unsigned sz;
short resv1,resv2;
unsigned offset;
};
/*
4 biSize 40 specifies the size of the BITMAPINFOHEADER structure, in bytes.
4 biWidth ?? specifies the width of the image, in pixels.
4 biHeight ?? specifies the height of the image, in pixels.
2 biPlanes 1 specifies the number of planes of the target device, must be set to zero.
2 biBitCount 24 specifies the number of bits per pixel (2/8/16/24).
4 biCompression 0 Specifies the type of compression, usually set to zero (no compression).
4 biSizeImage 0 specifies the size of the image data, in bytes. If there is no compression, it
is valid to set this member to zero.
4 biXPelsPerMeter 0 specifies the the horizontal pixels per meter on the designated targer
device, usually set to zero.
4 biYPelsPerMeter 0 specifies the the vertical pixels per meter on the designated targer
device, usually set to zero.
4 biClrUsed 0 specifies the number of colors used in the bitmap, if set to zero the number of
colors is calculated using the biBitCount member.
4 biClrImportant 0 specifies the number of color that are 'important' for the bitmap, if set
to zero, all colors are important.
*/
struct BMPHeaderInfo /*STRCUTURES MUST BE PACKED*/
{
unsigned size;
unsigned width;
unsigned height;
unsigned short planes;
unsigned short bitcount;
unsigned compression;
unsigned sizecompression;
unsigned x_pxls_p_m;
unsigned y_pxls_p_m;
unsigned clrused;
unsigned clrimportant;
};
Opdracht 1
Noot: Voor deze opdrachten moet men altijd de grootte van de bitmap
steeds vooraf kennen.
Maak een programma waarbij het mogelijk is om een Windows Bitmap weg te schrijven.
1/ In het programma moet het mogelijk zijn om een lege bitmap te maken in het groen,
wit of zwart.
2/ Het moet mogelijk zijn om een cirkel te tekenen in een gegeven met een radius.
Menu
---------------------1:
Maak Wit vlak
2:
Maak Zwart vlak
3:
Maak Groen vlak
4:
Teken cirkel
5:
Beveilig figuur
0:
Einde
Keuze: 4
Geef radius: 40
Cirkel OK.
Keuze: 5
Geef bestandsnaam: prentje.bmp
Bestand opgeslagen.
Keuze: 0
Byebye...
Opdracht 2
Idem als vorige opdracht, maar maak nu een functie bij zodat men enkel de rood, groen of
blauwcomponenten van een BMP kan zien.
IMAGE FILTER v1.0
Geef bestandsnaam:
prentje.bmp
Inlezen ok...
Bezig met verwerken...
Rood component: rood.bmp
Groen component: groen.bmp
Blauw component: blauw.bmp
Einde.
Download