In C-programmeren biedt dynamische geheugenallocatie een flexibele en krachtige manier om geheugen toe te wijzen tijdens de uitvoering van een programma. De meest gebruikte functies hiervoor zijn malloc(), calloc() en realloc(). Elk van deze functies heeft zijn eigen kenmerken en toepassingen, afhankelijk van de behoeften van het programma.

Het voorbeeld hieronder toont het gebruik van malloc() voor het toewijzen van geheugen aan een array van 500 drijvende-komma getallen. De geheugenruimte die nodig is voor deze getallen (2.000 bytes) wordt toegewezen aan de pointer ptr. De pointer is in feite een adres dat naar de locatie in het geheugen wijst waar de waarden zijn opgeslagen. Het gebruik van een pointer en een array zijn conceptueel gelijk, omdat het beide manieren zijn om toegang te krijgen tot de geheugenlocatie van gegevens. In dit geval is ptr[i] de manier om het i-de element van de array te benaderen, beginnend bij het geheugenadres dat door ptr wordt vastgehouden. De functie free(ptr) wordt gebruikt om het geheugen dat eerder werd toegewezen te de-alloqueren en de ruimte vrij te geven.

c
#include <stdio.h> #include <stdlib.h> int main() { float *ptr; int i;
ptr = (float*)malloc(500 * sizeof(float));
if (ptr == NULL) { printf("Geen geheugen beschikbaar!\n"); exit(0); } for (i = 0; i < 500; i++) ptr[i] = 1.0 / (i + 1); printf("%f %f\n", ptr[0], ptr[499]); free(ptr); return 0; }

In dit voorbeeld vraagt de malloc(500 * sizeof(float)) functie om voldoende geheugen voor 500 float getallen. Aangezien malloc() een generieke pointer (void*) teruggeeft, wordt het resultaat gecast naar een specifieke pointer van het type float *. Als er niet genoeg geheugen beschikbaar is, zal de functie malloc() een NULL waarde teruggeven. De controle if (ptr == NULL) wordt gebruikt om te bepalen of het geheugen correct is toegewezen.

Naast malloc() zijn er andere belangrijke functies voor dynamische geheugenallocatie, zoals calloc() en realloc(). De calloc() functie wijst geheugen toe en initialiseert alle waarden op nul, terwijl realloc() de grootte van een eerder toegewezen geheugenblok kan aanpassen. Dit laatste kan nuttig zijn als het programma tijdens de uitvoering de grootte van een array moet aanpassen.

Wanneer het programma interactief geheugen toewijst op basis van de invoer van de gebruiker, zoals in het voorbeeld hieronder, kan het controleren of er voldoende geheugen beschikbaar is voor de benodigde grootte van de array. Als er onvoldoende geheugen is, wordt een foutmelding weergegeven en stopt het programma.

c
#include <stdio.h>
#include <stdlib.h> int main() { int i, size; double *vector; printf("Voer de grootte van de array in (double precision) = "); scanf("%d", &size); vector = (double *)malloc(size * sizeof(double)); if (vector == NULL) { printf("Geheugenallocatie mislukt!\n"); return 1; } for (i = 0; i < size; i++) vector[i] = i * 0.5;
printf("Eerste element van de vector: %f\n", vector[0]);
printf("Laatste element van de vector: %f\n", vector[size - 1]); free(vector); printf("Geheugen is succesvol vrijgegeven.\n"); return 0; }

In dit voorbeeld kan het programma tot 100.000.000 elementen verwerken als er voldoende geheugen beschikbaar is. Echter, als de gebruiker probeert om een grotere hoeveelheid geheugen (bijvoorbeeld één miljard elementen) aan te vragen, zal het programma een foutmelding geven door onvoldoende geheugen.

Naast malloc() en calloc() is het belangrijk om te begrijpen dat de dynamische geheugenallocatie bijdraagt aan de efficiëntie van programma's die werken met grote datasets. Programma's kunnen namelijk de hoeveelheid geheugen die ze nodig hebben, aanpassen op basis van de beschikbare systemresources, zonder vooraf vast te leggen hoeveel geheugen er vereist zal zijn.

Er zijn echter belangrijke overwegingen bij het gebruik van dynamische geheugenallocatie, vooral in verband met numerieke fouten. In veel wetenschappelijke en technische programma's, waar nauwkeurigheid van cruciaal belang is, kunnen er numerieke onnauwkeurigheden optreden door het gebruik van verschillende datatypes zoals float en double.

Bijvoorbeeld, in een eenvoudig programma waarin een waarde van 0,1 duizend keer wordt opgeteld, zou men verwachten dat het resultaat precies 1000 is. Echter, door de manier waarop getallen in binaire vorm worden opgeslagen, kan het resultaat in werkelijkheid afwijken van de verwachte waarde. Dit wordt vaak zichtbaar in de uitkomst van het programma als een kleine afwijking (999.902893 in plaats van 1000).

c
#include <stdio.h>
int main() { float s = 0.0; int i; for (i = 0; i < 10000; i++) s = s + 0.1; printf("%f\n", s); return 0; }

Het is belangrijk te begrijpen dat deze afwijkingen voortkomen uit de conversie van decimale getallen naar binaire vorm. Het getal 0,1 kan niet exact worden weergegeven in binaire notatie, waardoor er kleine fouten optreden bij herhaald optellen. Dit probleem is algemeen bekend als het "afkapfout"-probleem.

Een andere veel voorkomende fout is het verlies van significante cijfers wanneer twee bijna gelijkwaardige getallen van elkaar worden afgetrokken. Dit wordt vaak aangeduid als "cancellatie fout". Bijvoorbeeld:

c
#include <stdio.h> int main() { float a, b, c; a = 123.45678; b = 123.45655; printf("%f\n", a - b); return 0; }

In dit geval zou men verwachten dat de uitkomst 0.00023 is, maar het programma zal een iets andere waarde, zoals 0.000229, geven. Deze fout kan onaanvaardbaar zijn wanneer zeer hoge precisie vereist is.

Om de impact van deze numerieke fouten te verminderen, wordt aanbevolen om double in plaats van float te gebruiken voor drijvende-komma getallen. Het type double biedt een grotere nauwkeurigheid, met een bereik van 15 significante cijfers in plaats van de 7 cijfers die door float worden geboden. In veel gevallen is dit voldoende om de impact van kleine fouten te minimaliseren, vooral in wetenschappelijke en technische berekeningen.

Hoe werken de voorwaartse, achterwaartse en centrale verschilmethoden bij numerieke differentiatie?

De numerieke differentiatie is een fundamentele techniek in de numerieke wiskunde, die wordt gebruikt om de afgeleide van een functie te benaderen wanneer de analytische afgeleide moeilijk te verkrijgen is. Een van de meest gangbare benaderingen hiervoor zijn de zogenaamde verschilmethoden: voorwaarts verschil, achterwaarts verschil en centraal verschil. Deze benaderingen zijn gebaseerd op de Taylor-reeksuitbreiding van de functie, en variëren in hun nauwkeurigheid en de vereiste informatie.

In het voorwaartse verschil wordt de afgeleide van een functie f(x)f(x) benaderd door het verschil tussen f(x+h)f(x+h) en f(x)f(x), gedeeld door hh, de stapgrootte. De Taylor-reeks van f(x+h)f(x+h) is:

f(x+h)=f(x)+hf(x)+h22!f(x)+h33!f(x)+f(x+h) = f(x) + h f'(x) + \frac{h^2}{2!} f''(x) + \frac{h^3}{3!} f'''(x) + \cdots

Door alleen de eerste twee termen te behouden, wordt de afgeleide benaderd als:

f(x)f(x+h)f(x)hf'(x) \approx \frac{f(x+h) - f(x)}{h}

Deze benadering is eenvoudig en snel, maar de nauwkeurigheid is beperkt door de grootte van de stap hh, aangezien de truncatiefout van de voorwaartse methode van de orde hh is. In de praktijk kan de voorwaartse verschilmethode bijvoorbeeld de afgeleide van f(2)f'(2) benaderen door:

f(2)f(2.5)f(2.0)0.5=15.25f'(2) \approx \frac{f(2.5) - f(2.0)}{0.5} = 15.25

Het achterwaartse verschil is een andere benadering waarbij de afgeleide wordt benaderd door het verschil tussen f(x)f(x) en f(xh)f(x-h), gedeeld door hh. De Taylor-reeks van f(xh)f(x-h) is:

f(xh)=f(x)hf(x)+h22!f(x)h33!f(x)+f(x-h) = f(x) - h f'(x) + \frac{h^2}{2!} f''(x) - \frac{h^3}{3!} f'''(x) + \cdots

Door de termen van de hogere orde te verwaarlozen, wordt de afgeleide benaderd als:

f(x)f(x)f(xh)hf'(x) \approx \frac{f(x) - f(x-h)}{h}

Net als bij het voorwaartse verschil is de nauwkeurigheid van de achterwaartse verschilmethode van de orde hh. Bijvoorbeeld, de benadering van f(2)f'(2) met de achterwaartse methode wordt als volgt berekend:

f(2)f(2.0)f(1.5)0.5=9.25f'(2) \approx \frac{f(2.0) - f(1.5)}{0.5} = 9.25

De centrale verschilmethode biedt een meer nauwkeurige benadering van de afgeleide door gebruik te maken van zowel f(x+h)f(x+h) als f(xh)f(x-h). Dit komt omdat door het verschil van deze twee uitdrukkingen te nemen, de termen van hogere orde h3h^3 en verder verdwijnen, wat resulteert in een nauwkeuriger resultaat van de afgeleide. De centrale Taylor-reeksen zijn:

f(x+h)=f(x)+hf(x)+h22!f(x)+h33!f(x)+f(x+h) = f(x) + h f'(x) + \frac{h^2}{2!} f''(x) + \frac{h^3}{3!} f'''(x) + \cdots
f(xh)=f(x)hf(x)+h22!f(x)h33!f(x)+f(x-h) = f(x) - h f'(x) + \frac{h^2}{2!} f''(x) - \frac{h^3}{3!} f'''(x) + \cdots

Door deze twee vergelijkingen van elkaar af te trekken, krijgen we:

f(x+h)f(xh)=2hf(x)+2h313!f(x)+f(x+h) - f(x-h) = 2h f'(x) + 2h^3 \frac{1}{3!} f'''(x) + \cdots

Door de hogere termen te negeren, krijgen we de centrale benadering van de afgeleide:

f(x)f(x+h)f(xh)2hf'(x) \approx \frac{f(x+h) - f(x-h)}{2h}

In de praktijk, voor f(2)f'(2), zou dit resulteren in:

f(2)f(2.5)f(1.5)2×0.5=12.25f'(2) \approx \frac{f(2.5) - f(1.5)}{2 \times 0.5} = 12.25

De centrale verschilmethode heeft een hogere nauwkeurigheid omdat de truncatiefout van de orde h2h^2 is. Dit betekent dat de centrale methode bij benadering beter presteert dan de voorwaartse en achterwaartse methoden, waarbij de truncatiefout van de orde hh is.

Echter, de centrale verschilmethode heeft een nadeel: de waarden aan de randen van het domein, zoals f(0)f'(0) en f(1)f'(1), kunnen niet worden berekend zonder toegang te hebben tot de waarden buiten het interval, zoals f(0.1)f(-0.1) en f(1.1)f(1.1). Om dit te omzeilen, kan men kiezen voor een benadering zoals de voorwaartse methode voor het eerste punt en de achterwaartse methode voor het laatste punt. Er bestaat echter een oplossing om de centrale benadering toch te behouden voor de randen, door gebruik te maken van de volgende formule voor de afgeleide bij de randen:

f(x)3f(x)4f(xh)+f(x2h)2hf'(x) \approx \frac{3 f(x) - 4 f(x-h) + f(x-2h)}{2h}

Deze benadering is net zo nauwkeurig als de centrale verschilmethode, maar vereist de waarde van f(x2h)f(x-2h). Dit maakt de methode iets complexer, maar zorgt ervoor dat de afgeleide ook aan de randen van het interval accuraat kan worden berekend.

Wanneer men numerieke differentiatie uitvoert, is het belangrijk om de juiste methode te kiezen op basis van de vereiste nauwkeurigheid en de beschikbare gegevens. De centrale verschilmethode is meestal de beste keuze wanneer de functie op het gehele interval beschikbaar is, maar in gevallen waar we gegevens missen aan de randen, kunnen alternatieven zoals de voorwaartse en achterwaartse methoden worden toegepast. Bovendien moet men zich ervan bewust zijn dat de keuze van de stapgrootte hh een cruciale rol speelt in de nauwkeurigheid van de benadering, aangezien kleinere waarden van hh leiden tot meer nauwkeurige benaderingen, maar ook tot grotere numerieke instabiliteit.

Hoe werken de verschillende benaderingsmethoden voor numerieke integratie?

In numerieke integratie zijn er verschillende methoden die kunnen worden gebruikt om een integraal te benaderen. De keuze van de methode heeft invloed op de snelheid van convergentie en de nauwkeurigheid van de resultaten. We bespreken drie populaire methoden: de rechthoekregel, de trapeziumregel en de regel van Simpson, en hoe ze werken in de praktijk.

De rechthoekregel is een eenvoudige benadering waarbij het gebied onder een functie wordt benaderd door rechthoeken. De integraal II wordt benaderd door de som van de functiewaarden op de linkergrenzen van de deelintervallen, vermenigvuldigd met de stapgrootte hh, zoals weergegeven in de formule:

Ih×(f0+f1+f2++fn1)I \sim h \times (f_0 + f_1 + f_2 + \dots + f_{n-1})

waarbij h=banh = \frac{b - a}{n} de stapgrootte is, en f0,f1,,fn1f_0, f_1, \dots, f_{n-1} de functiewaarden zijn op de linkergrenzen van de deelintervallen. Deze methode is eenvoudig te implementeren in C, en het resultaat benadert de werkelijke waarde van de integraal naarmate het aantal partities nn toeneemt. Echter, de rechthoekregel heeft een langzame convergentie, en om een nauwkeurigheid van vijf significante cijfers te bereiken, zijn miljoenen iteraties nodig.

De trapeziumregel is een verbeterde versie van de rechthoekregel. In plaats van rechthoeken worden de gebieden onder de functie benaderd door trapezoïden. Het resultaat wordt gegeven door:

Ih2×(f0+2f1+2f2++2fn1+fn)I \sim \frac{h}{2} \times (f_0 + 2f_1 + 2f_2 + \dots + 2f_{n-1} + f_n)

In de trapeziumregel worden de functies op de tussenliggende punten dubbel geteld, en de uiteinden van het interval worden maar eenmaal geteld. Dit leidt tot een snellere convergentie dan de rechthoekregel, zoals blijkt uit de resultaten van de implementatie in C. De trapeziumregel vereist echter nog steeds veel berekeningen voor een hoge nauwkeurigheid, hoewel de snelheid van convergentie aanzienlijk hoger is dan bij de rechthoekregel.

De regel van Simpson is een verdere verfijning, waarbij een tweede-orde polynoom wordt gebruikt om de segmenten van de functie te benaderen. De benadering is gebaseerd op een gewogen som van de functiewaarden aan de uiteinden van het interval en de tussenliggende punten, zoals in de formule:

Ih3×(f0+4f1+2f2+4f3++2f2n2+4f2n1+f2n)I \sim \frac{h}{3} \times (f_0 + 4f_1 + 2f_2 + 4f_3 + \dots + 2f_{2n-2} + 4f_{2n-1} + f_{2n})

Hierbij wordt nn gedefinieerd als het aantal partities, en de vereiste voor deze methode is dat nn een even getal moet zijn. De regel van Simpson convergeert aanzienlijk sneller dan de rechthoek- en trapeziumregels en bereikt vaak een hoge nauwkeurigheid met slechts een klein aantal iteraties.

De eenvoud van de implementatie van de drie methoden in C laat zien dat de regel van Simpson al met slechts een paar partities zeer nauwkeurige resultaten kan opleveren. De snelheid van convergentie is dus het belangrijkste voordeel van de regel van Simpson. Deze methode is daarom de standaardmethode voor numerieke integratie in veel gevallen, omdat deze met relatief weinig berekeningen een hoge nauwkeurigheid biedt.

Naast de genoemde methoden, is het belangrijk te weten dat de nauwkeurigheid van de numerieke integratie niet alleen afhangt van de grootte van de stapgrootte hh, maar ook van het gedrag van de afgeleiden van de functie f(x)f(x). Functies met snel variërende of singulariteiten kunnen de snelheid van convergentie aanzienlijk vertragen. Bijvoorbeeld, de functie f(x)=1x2f(x) = \sqrt{1 - x^2}, die vaak wordt gebruikt om π\pi te benaderen, vertoont een trage convergentie bij gebruik van de regel van Simpson, omdat de afgeleiden op de grenzen van het interval extreem groot worden.