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.