- Leren
- Informatica & ICT
- Programmeren
- Programmeren in C: pointers, geheugenbeheer en debuggen
- Functies en programmastructuur in C
Functies en programmastructuur in C
static en extern. Gebruik de preprocessor voor headerbestanden, macro’s en voorwaardelijke compilatie, verdeel een programma over bronbestanden en headerbestanden en bouw het met make, zodat alleen gewijzigde bestanden opnieuw worden gecompileerd.BeginnerLes 2 van 8 in Cursus Programmeren in C: pointers, geheugenbeheer en debuggen
Wat je leert
- Verdeel een programma in functies die parameters ontvangen en resultaten teruggeven, declareer ze met functieprototypes, begrijp dat C argumenten op waarde doorgeeft en volg hoe aanroepen, ook recursieve, worden uitgevoerd en terugkeren.
- Definieer functies die invoer ontvangen en een resultaat teruggevenSchrijf functiedefinities met een retourtype, parameters en een functieblok, gebruik
returnenvoidcorrect en roep functies aan binnen expressies. - Declareer functies met functieprototypenDeclareer een functie vóór de eerste aanroep met een functieprototype, weet wat de compiler controleert en converteert en vermijd lege haakjes en impliciete declaraties.
- Geef argumenten op waarde door en geef het resultaat terugLeg uit dat elk C-argument naar zijn parameter wordt gekopieerd, voorspel wat een functie wel en niet kan veranderen en gebruik retourwaarden om resultaten terug te geven.
- Volg functieaanroepen en recursieVolg hoe elke aanroep eigen parameters en lokale variabelen krijgt en terugkeert naar de aanroepende functie, en schrijf en volg recursieve functies met een basisgeval.
- Definieer functies die invoer ontvangen en een resultaat teruggeven
- Maak onderscheid tussen het geldigheidsgebied van een naam en de levensduur van een variabele, gebruik statische lokale variabelen om toestand tussen aanroepen te bewaren en weeg de nadelen van globale variabelen af.
- Bepaal waar je een naam kunt gebruiken (geldigheidsgebied)Pas geldigheidsgebieden van blokken en functieprototypes en op bestandsniveau toe, voorspel naar welke variabele een naam verwijst bij naamverberging en voorkom fouten met geldigheidsgebieden.
- Bepaal met opslagklassen hoelang een variabele bestaatMaak onderscheid tussen automatische en statische opslagduur, gebruik statische lokale variabelen om waarden tussen aanroepen te bewaren en weet wat auto, register, static en extern betekenen.
- Deel gegevens zorgvuldig via globale variabelenDefinieer en gebruik variabelen op bestandsniveau (globale variabelen), leg hun zichtbaarheid en initialisatie uit en geef de voorkeur aan parameters en retourwaarden waar een globale variabele verborgen afhankelijkheid zou toevoegen.
- Bepaal waar je een naam kunt gebruiken (geldigheidsgebied)
- Gebruik #include, #define en macro’s veilig en selecteer tijdens het compileren code met voorwaardelijke richtlijnen, voorgedefinieerde macro’s en compileropties.
- Neem bestanden op en definieer macro’sLeg uit wat de preprocessor vóór het compileren doet, neem headerbestanden op, definieer constante en functieachtige macro’s en vermijd de klassieke valkuilen van macro’s.
- Compileer code voorwaardelijkGebruik #if, #ifdef, #ifndef, #elif, #else en #endif met -D-opties en voorgedefinieerde macro’s om tijdens het compileren code op te nemen of uit te sluiten, waaronder beveiligingen tegen herhaalde inclusie.
- Neem bestanden op en definieer macro’s
- Compileer meerdere bronbestanden afzonderlijk en koppel ze, schrijf headerbestanden die declaraties delen en bepaal met static en extern welke namen andere bestanden kunnen zien.
- Compileer en koppel meerdere bronbestandenBeschrijf de stappen van bronbestand tot uitvoerbaar bestand, compileer bestanden afzonderlijk tot objectbestanden, koppel ze en onderscheid compilatiefouten van linkerfouten.
- Schrijf headerbestandenZet gedeelde declaraties in een beveiligd headerbestand, neem het op waar het nodig is, ook in het eigen bronbestand, en zet er geen definities in.
- Regel de koppeling met static en externGebruik extern om één variabele tussen bestanden te delen en static om functies en variabelen privé te houden voor hun bestand, en herstel de linkerfouten die ontstaan als dit verkeerd gaat.
- Compileer en koppel meerdere bronbestanden
- Schrijf makefiles waarvan de regels en afhankelijkheden ervoor zorgen dat alleen gewijzigde onderdelen opnieuw worden gebouwd, en houd ze kort met variabelen, automatische variabelen, patroonregels en schijndoelen.
- Schrijf regels en afhankelijkheden in een makefileSchrijf regels met doelen, afhankelijkheden en met een tab ingesprongen recepten, voorspel welke opdrachten make na een wijziging uitvoert en vermeld afhankelijkheden van headerbestanden correct.
- Vereenvoudig makefiles met variabelen en patroonregelsGebruik variabelen zoals
CCenCFLAGS, de automatische variabelen$@,$<en$^, patroonregels, ingebouwde regels en schijndoelen om een makefile kort en correct te houden.
- Schrijf regels en afhankelijkheden in een makefile
Functies en programmastructuur in C: uitleg
1Schrijf, declareer en roep functies aan
Een functie geeft een taak een naam, een verzameling invoerwaarden en een duidelijke manier om een resultaat op te leveren. Je maakt definities en prototypes en volgt vervolgens waarden naar aanroepen en terug naar de aanroepende functies. Korte uitvoeringssporen maken de regels zichtbaar: een parameter ontvangt een kopie, een retourwaarde moet ergens naartoe en een recursieve aanroep moet uiteindelijk stoppen. De voorbeelden gebruiken C17; expliciete parameterlijsten maken de code ook duidelijk bij compilatie volgens C23.
1.1 Definieer functies die invoer ontvangen en een resultaat teruggeven
Een functie bundelt één taak zodat een ander deel van het programma die kan aanroepen. Geef de functie een naam die de taak beschrijft en kies de invoer en het resultaat voordat je het functieblok schrijft. In een gewoon C-programma met een hostomgeving begint de uitvoering in main.
Maak een definitie
int square(int n)
{
return n * n;
}
De definitie heeft een retourtype (int), een naam (square), een parameterlijst (int n) en een functieblok tussen accolades. De parameter is de lokale variabele die invoer ontvangt. Een argument is de waarde die bij een aanroep wordt opgegeven: in square(6) is die waarde 6. De aanroep levert 36 op. Definieer elke functie op bestandsniveau, buiten andere functies; standaard-C staat geen geneste functiedefinities toe.
Elke parameter heeft een eigen type nodig. Schrijf int max(int a, int b), met een komma tussen de declaraties. Geef bij een aanroep waarden op zonder hun typen: max(8, 3).
Geef een waarde terug aan de aanroepende functie
int max(int a, int b)
{
if (a > b)
return a;
return b;
}
Een return beëindigt de huidige aanroep onmiddellijk. Bij max(8, 3) wordt de eerste return uitgevoerd, zodat de tweede nooit wordt bereikt. Een teruggegeven expressie wordt geconverteerd naar het retourtype van de functie. Kies dat type zo dat het soort resultaat dat je nodig hebt behouden blijft.
Gebruik voor een resultaat met een breuk overal drijvendekommarekenen:
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
De aanroep c_to_f(37.0) geeft 98.6 terug. Een geheeltallige versie met c * 9 / 5 + 32 geeft 98 terug, omdat de deling de breuk weglaat. Als je de volgorde wijzigt in 9 / 5 * c + 32, wordt eerst 9 / 5 geëvalueerd tot 1, wat voor 37 de waarde 69 oplevert. Een drijvendekomma-retourtype alleen kan een geheeltallige berekening die al binnen het functieblok is uitgevoerd niet ongedaan maken.
Gebruik het resultaat
Een aanroep die een waarde teruggeeft, is een expressie. Met de definitie hierboven geeft square(3) + square(4) de waarde 25. In square(square(2)) geeft de binnenste aanroep 4 terug; dat wordt het argument van de buitenste aanroep, die 16 oplevert. Een instructie zoals square(5); is toegestaan, maar gooit de teruggegeven waarde weg.
Afdrukken en teruggeven hebben verschillende taken. printf toont tekst; return levert een resultaat dat de aanroepende functie kan opslaan of in een andere expressie kan gebruiken. Als een gewone functie die niet void is de afsluitende accolade bereikt zonder iets terug te geven en de aanroepende functie het resultaat gebruikt, is het gedrag ongedefinieerd. Compileer met -Wall -Wextra om ontbrekende return-instructies op te sporen.
Functies die geen waarde opleveren
Een void-functie voert een taak uit zonder een waarde terug te geven. Ze kan met return; voortijdig eindigen. Zo kan void print_line(int width) een regel met streepjes afdrukken, maar int x = print_line(5); is ongeldig omdat de aanroep geen waarde oplevert. Om geen invoer te ontvangen schrijf je void in de parameterlijst, zoals in int read_sensor(void).
int main(void) geeft een status terug aan de hostomgeving. Gebruik return 0; voor succes; <stdlib.h> biedt ook EXIT_SUCCESS en EXIT_FAILURE. Als het einde van main wordt bereikt, wordt automatisch nul teruggegeven. Andere functies die een waarde teruggeven krijgen die uitzondering niet.
Belangrijkste punten
- Een definitie vermeldt een retourtype, naam, parameters met hun typen en een functieblok.
- Een parameter ontvangt een argument dat bij een aanroep wordt opgegeven.
returnbeëindigt de aanroep; afdrukken levert het resultaat niet terug.- Gebruik
voidvoor geen retourwaarde en(void)voor geen parameters. - Kies berekeningen en retourtypen die het benodigde resultaat behouden.
1.2 Declareer functies met functieprototypen
Een functieaanroep heeft een declaratie nodig die de compiler al heeft gezien. Als de definitie later komt, zet je een prototype vóór de eerste aanroep. Het prototype vertelt de compiler hoe die de aanroep moet controleren zonder het functieblok te leveren.
Scheid de declaratie van het functieblok
#include <stdio.h>
double c_to_f(double c);
int main(void)
{
printf("%.1f\n", c_to_f(25.0));
return 0;
}
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
Het prototype eindigt met een puntkomma. De definitie heeft een functieblok en geen puntkomma tussen de parameterlijst en de openingsaccolade. Een handige indeling is: headerbestanden, prototypes, main en daarna de overige definities. De uitvoer is hier 77.0.
Parameternamen zijn optioneel in prototypes: double c_to_f(double); declareert dezelfde functie. Namen zoals c kunnen een invoerwaarde verduidelijken, maar hoeven niet overeen te komen met de namen in de definitie. Alle declaraties moeten overeenkomen in retourtype en parametertypen. Een gebruikte functie heeft één definitie nodig; door compatibele prototypes te herhalen ontstaan geen extra functies.
Wat de compiler controleert
Met een prototype controleert de compiler het aantal argumenten en converteert elke opgegeven waarde naar het gedeclareerde parametertype. Bij double c_to_f(double c); hebben zowel c_to_f() als c_to_f(20.0, 5.0) een onjuist aantal argumenten en is een diagnostische melding vereist.
Een verschil tussen numerieke typen kan daarentegen een toegestane conversie zijn:
int half(int n)
{
return n / 2;
}
Bij half(7.9) geeft de conversie de parameter eerst de waarde 7. Geheeltallige deling levert daarna 3 op. Bij half(-7.9) geeft de conversie -7 en levert de deling -3 op. In beide gevallen wordt de breuk afgekapt naar nul. De aanroep behoudt de oorspronkelijke double-waarde niet alleen omdat het argument met een decimale punt is geschreven. Voeg -Wconversion toe als je waarschuwingen over zulke conversies wilt.
Geef expliciet aan wanneer er geen parameters zijn
| Declaratie | Betekenis in C17 | Betekenis in C23 |
|---|---|---|
int read_sensor(void); | Geen parameters | Geen parameters |
int read_sensor(); | Parameterinformatie niet gespecificeerd | Geen parameters |
In C17 verhindert een lege parameterlijst in een declaratie een volledige controle van de argumenten; die staat geen willekeurige argumenten voor de uiteindelijke functie toe. Gebruik consequent (void). GCC 15 heeft het standaard C-dialect gewijzigd in gnu23, dus met -std=c17 maak je bij deze voorbeelden de bedoelde standaard expliciet.
Herstel een declaratieprobleem
Een niet-gedeclareerde functie aanroepen is in strijd met modern C. Neem het standaardheaderbestand op dat de functie declareert, zoals <stdio.h> voor printf, of voeg een eigen passend prototype toe. Huidige versies van GCC en Clang wijzen impliciete declaraties normaal gesproken af.
Een prototype zoals double area(double r); kan niet overeenkomen met een definitie die float teruggeeft en float ontvangt. Neem het headerbestand van een module op in het bestand dat de functies definieert, zodat de compiler ze kan vergelijken. Probeer een typeverschil niet op te lossen door bij de aanroep een expliciete typeconversie toe te voegen: corrigeer de declaratie en definitie zodat ze dezelfde functie beschrijven.
Belangrijkste punten
- Plaats een declaratie vóór de eerste aanroep.
- Een prototype eindigt met een puntkomma; een definitie levert het functieblok.
- Prototypes controleren het aantal argumenten en leggen argumentconversies vast.
- Gebruik
(void)voor geen parameters, zowel in C17 als in C23. - Neem het headerbestand met de declaratie op en zorg dat alle declaraties compatibel zijn.
- Bij een toegestane numerieke conversie kan toch een breuk verloren gaan.
1.3 Geef argumenten op waarde door en geef het resultaat terug
C geeft elk argument op waarde door: elke parameter begint met een kopie van de waarde van het argument. Een waarde toewijzen aan een gewone geheeltallige parameter verandert die kopie, ook als de parameter dezelfde naam heeft als een variabele van de aanroepende functie.
Volg de kopie
Met <stdio.h> opgenomen en deze definitie vóór de aanroep:
void double_it(int n)
{
n *= 2;
printf("inside: %d\n", n);
}
De aanroepende functie voert uit:
int score = 21;
double_it(score);
printf("after: %d\n", score);
De uitvoer is inside: 42, gevolgd door after: 21. De parameter verandert van 21 in 42; geen enkele instructie wijst een waarde toe aan score van de aanroepende functie.
Bewaar een teruggegeven resultaat
int doubled(int n)
{
return n * 2;
}
| Instructie van de aanroepende functie | Effect op een score die aanvankelijk gelijk is aan 21 |
|---|---|
doubled(score); | Gooit 42 weg; score blijft 21 |
score = doubled(score); | Slaat 42 op in score |
De functie berekent een waarde; de toewijzing van de aanroepende functie werkt de eigen variabele bij. De waarde van een lokale variabele teruggeven is veilig: int total = a + b; return total; kopieert de waarde naar buiten voordat de automatische lokale variabele ophoudt te bestaan. Berekeningen die alleen parameters en teruggegeven resultaten gebruiken, kun je testen zonder verborgen gedeelde toestand in te richten.
Bekijk waarom verwisselen met alleen waarden mislukt
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
Na int p = 3, q = 7; swap(p, q); heeft de aanroepende functie nog steeds p == 3 en q == 7. Het functieblok heeft de kopieën verwisseld. Om objecten van de aanroepende functie via parameters te wijzigen zijn hun adressen nodig, die als pointerwaarden worden doorgegeven.
Leg de volgorde van argumenten vast als die ertoe doet
Argumenten kunnen expressies zijn zoals a + 1 of square(b). Ze worden allemaal geëvalueerd voordat het blok van de aangeroepen functie begint, maar hun evaluatievolgorde is niet gespecificeerd. Twee aanroepen van een next_ticket() die toestand wijzigt, kunnen in één afdrukopdracht de ticketnummers in beide volgorden leveren. Gebruik afzonderlijke instructies:
int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);
Vermijd f(i, i++): het niet-geordend lezen en wijzigen van i levert ongedefinieerd gedrag op.
Belangrijkste punten
- Parameters ontvangen kopieën van argumentwaarden.
- Sla een teruggegeven resultaat op met een toewijzing als de aanroepende functie een bijgewerkte waarde nodig heeft.
- Verwisselen met alleen waarden verandert de parameters, niet de variabelen van de aanroepende functie.
- Gebruik afzonderlijke instructies als de evaluatievolgorde van argumenten ertoe doet.
1.4 Volg functieaanroepen en recursie
Een aanroep pauzeert de aanroepende functie totdat een resultaat beschikbaar is. Door de actieve aanroepen te volgen zie je hoe een programma naar de juiste plaats terugkeert en waarom recursie een stopregel nodig heeft.
Houd per actieve aanroep één record bij
De meeste implementaties gebruiken een aanroepstapel (call stack). Elke actieve aanroep heeft een stackframe met de parameterwaarden, automatische lokale variabelen en de plaats waar de aanroepende functie verdergaat. De standaard specificeert het gedrag zonder deze specifieke geheugenindeling te vereisen.
Als main de functie print_table aanroept en print_table de functie c_to_f, keert de omzetfunctie als eerste terug. De uitvoering gaat verder binnen print_table, die later terugkeert naar main. Gewone automatische lokale variabelen horen bij één aanroep. Een statische lokale variabele is anders: alle aanroepen delen de ene opgeslagen waarde.
Volg een recursieve berekening omlaag
Een recursieve functie roept zichzelf aan voor een kleiner probleem. Een basisgeval keert direct terug; een recursief geval komt dichter bij het basisgeval.
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
Bij factorial(4) kan de eerste aanroep de vermenigvuldiging niet voltooien voordat factorial(3) terugkeert. Die aanroep wacht op factorial(2), die op factorial(1) wacht.
| Actieve aanroep | Bewerking die op een resultaat wacht |
|---|---|
factorial(4) | 4 * factorial(3) |
factorial(3) | 3 * factorial(2) |
factorial(2) | 2 * factorial(1) |
factorial(1) | Basisgeval: geef 1 terug |
Op het diepste punt zijn vier aanroepen van factorial actief. Elk heeft een eigen n; het bereiken van n == 1 verandert de waarden van de eerdere aanroepen niet.
Volg de retourwaarden omhoog
De aanroep van het basisgeval geeft 1 terug. De aanroep met n == 2 berekent nu 2 * 1 en geeft 2 terug. De volgende geeft 3 * 2, oftewel 6, terug; de eerste geeft 4 * 6, oftewel 24, terug. Vul retourwaarden bij het terugkeren in het uitvoeringsspoor in, nadat het resultaat van de aangeroepen functie bekend is.
De stopvoorwaarde moet bereikbaar zijn voor de ondersteunde invoerwaarden. Als de test n == 1 zou zijn, zou een start bij nul steeds aanroepen met -1, -2 en lagere waarden opleveren in plaats van één te bereiken. Als de recursie doorgaat, raken de beschikbare middelen uitgeput en kan het programma crashen. De ruimere test n <= 1 stopt bij deze invoerwaarden, hoewel de wiskundige faculteit wordt gebruikt voor niet-negatieve gehele getallen.
Controleer zowel de diepte als het resultaattype
Correct stoppen garandeert niet dat het resultaat past. Op een platform met een 32-bits int is de grootste waarde 2147483647. factorial(12) geeft 479001600, wat past; factorial(13) zou 6227020800 nodig hebben, wat niet past. Overloop bij gehele getallen met teken leidt tot ongedefinieerd gedrag.
Een lus kan de faculteit berekenen met één doorlopend product binnen één functieaanroep. Recursie is nuttig wanneer het volgende probleem dezelfde structuur heeft maar kleiner is. Voor positieve gehele getallen is het optellen van cijfers een ander voorbeeld: sum_digits(472) heeft 2 + sum_digits(47) nodig, daarna 2 + 7 + sum_digits(4), wat 13 oplevert. Een uitvoeringsspoor moet zowel de kleiner wordende invoer als de latere combinatie van retourwaarden tonen.
Belangrijkste punten
- Aanroepen eindigen in omgekeerde volgorde van hun nesting.
- Elke aanroep heeft eigen parameters en automatische lokale variabelen.
- Recursie heeft een bereikbaar basisgeval nodig en moet daar steeds dichterbij komen.
- Volg aanroepen omlaag en retourwaarden omhoog.
- Controleer of recursieve resultaten binnen hun numerieke type passen.
2Bepaal waar variabelen zichtbaar zijn en hoelang ze bestaan
Een naam kan in een klein deel van de broncode zichtbaar zijn terwijl de variabele gedurende het hele programma bestaat. Door die twee eigenschappen uit elkaar te houden worden lokale variabelen, statische lokale variabelen en globale variabelen veel gemakkelijker te begrijpen. Je volgt namen door geneste blokken, vergelijkt nieuwe waarden met bewaarde waarden en kiest hoe een functie de benodigde gegevens ontvangt. Die keuze beïnvloedt hoe gemakkelijk je een verkeerd resultaat kunt achterhalen en een functie afzonderlijk kunt testen.
2.1 Bepaal waar je een naam kunt gebruiken (geldigheidsgebied)
Het geldigheidsgebied is het deel van de broncode waar je een naam kunt gebruiken. Bekijk de accolades en declaraties om te bepalen welke variabele een expressie aanduidt; de volgorde van aanroepen verandert deze regels niet.
Bepaal de grens van het geldigheidsgebied
Een variabele die binnen een blok wordt gedeclareerd, heeft een geldigheidsgebied binnen dat blok. Dit begint direct na de declarator en eindigt bij de afsluitende accolade van het blok. Parameters zijn zichtbaar binnen hun functieblok. Een declaratie in een for-kop, zoals for (int i = 0; i < 10; i++), maakt i beschikbaar binnen die lus, maar niet erna.
Een naam die buiten alle functies is gedeclareerd, heeft een geldigheidsgebied op bestandsniveau, vanaf de declaratie tot het einde van de vertaaleenheid. Functies boven die declaratie hebben een eerdere declaratie nodig om de naam te gebruiken. Een parameternaam in een prototype heeft een geldigheidsgebied van een functieprototype, dat eindigt bij het prototype. Zo kan op double c_to_f(double c); een definitie volgen die de parameter degrees noemt.
Volg naamverberging van binnen naar buiten
#include <stdio.h>
int x = 10;
void show(void)
{
printf("%d\n", x);
}
int main(void)
{
int x = 5;
{
int x = 1;
printf("%d\n", x);
}
printf("%d\n", x);
show();
return 0;
}
De binnenste afdrukopdracht gebruikt 1, de volgende gebruikt 5 en show drukt 10 af. De dichtstbijzijnde zichtbare declaratie van x geldt. Als je het binnenste blok verlaat, wordt de buitenste lokale variabele weer zichtbaar. Door show aan te roepen wordt de lokale variabele van main daarin niet zichtbaar: show is gedefinieerd op een plaats waar de x op bestandsniveau zichtbaar is.
Verschillende functies mogen elk een lokale i, count of result declareren. Dit zijn verschillende variabelen omdat hun declaraties bij verschillende blokken horen.
Declareer een waarde waar later gebruik die kan bereiken
Deze code compileert niet:
if (temp > 30.0) {
int warning = 1;
}
printf("%d", warning);
De afdrukopdracht staat buiten het geldigheidsgebied van warning. Als de waarde daarna moet worden gebruikt, declareer en initialiseer die dan vóór de voorwaardelijke instructie:
int warning = 0;
if (temp > 30.0) {
warning = 1;
}
printf("%d", warning);
De initialisatie levert ook de waarde voor het pad waarop de voorwaarde onwaar is.
Voorkom een onbedoelde nieuwe variabele
Als je int total = 0; binnen een lus declareert, ontstaat elke keer dat het blok wordt binnengegaan een nieuwe lokale variabele. Als je in een buitenste total wilde optellen, gebruik dan een toewijzing zoals total += value;. Schakel -Wshadow in om verborgen namen op te sporen.
Het geldigheidsgebied begint voordat de initialisator wordt geëvalueerd. Binnen een blok leest int x = x + 1; de nieuwe, niet-geïnitialiseerde x, ook als een buitenste x bestaat. Gebruik een andere naam als de initialisator de buitenste waarde nodig heeft.
Het geldigheidsgebied beschrijft waar een naam zichtbaar is in de broncode. De opslagduur beschrijft hoelang het object bestaat terwijl het programma draait. Een statische lokale variabele heeft een geldigheidsgebied binnen een blok terwijl de opgeslagen waarde gedurende de hele uitvoering bestaat.
Belangrijkste punten
- Het geldigheidsgebied van een blok eindigt bij de bijbehorende afsluitende accolade.
- Namen op bestandsniveau zijn zichtbaar onder hun declaratie.
- Een binnenste declaratie verbergt een zichtbare buitenste declaratie.
- Een aangeroepen functie kan de automatische lokale variabelen van de aanroepende functie niet zien.
- Het geldigheidsgebied begint voordat een initialisator wordt geëvalueerd.
- Geldigheidsgebied en opslagduur beantwoorden verschillende vragen.
2.2 Bepaal met opslagklassen hoelang een variabele bestaat
De opslagduur beschrijft hoelang de opslag van een variabele bestaat. Gebruik automatische variabelen voor werk dat bij één aanroep hoort en statische opslag wanneer een waarde tussen aanroepen moet blijven bestaan.
Vergelijk een nieuwe waarde met een bewaarde waarde
Parameters en gewone variabelen met een geldigheidsgebied binnen een blok hebben een automatische opslagduur. Elke functieaanroep krijgt eigen exemplaren. Een declaratie zonder initialisator, zoals int sum;, levert geen bruikbare beginwaarde. Initialiseer een accumulator voordat je die leest: int sum = 0;.
Variabelen op bestandsniveau en variabelen die binnen een blok als static zijn gedeclareerd, hebben een statische opslagduur. Ze bestaan gedurende de hele uitvoering van het programma. Initialisatie gebeurt één keer voordat het programma start; zonder expliciete initialisator beginnen numerieke objecten op nul.
int next_ticket(void)
{
static int n = 0;
n++;
return n;
}
Drie aanroepen in afzonderlijke instructies geven 1, 2 en 3 terug. De initialisator wordt niet opnieuw uitgevoerd wanneer de functie wordt binnengegaan. Als je static verwijdert, ontstaat voor elke aanroep een nieuwe automatische n = 0, zodat alle drie de aanroepen 1 teruggeven.
De naam n heeft nog steeds een geldigheidsgebied binnen het blok. Andere functies kunnen die niet rechtstreeks gebruiken. Ook recursieve aanroepen delen dezelfde statische lokale variabele; ze krijgen geen afzonderlijke statische kopieën zoals bij automatische lokale variabelen.
Initialiseer één keer, wijs toe wanneer nodig
Een statische numerieke initialisator moet in C een constante expressie zijn. static int limit = 12; is geldig; static int limit = read_sensor(); is ongeldig. Voer in uitvoerbare code een toewijzing tijdens de uitvoering uit wanneer een waarde uit een aanroep moet komen. Een instructie zoals n = 0; wordt uitgevoerd wanneer de uitvoering die bereikt. Als je die aan het begin van next_ticket plaatst, wordt de teller dus bij elke aanroep op nul gezet.
Lees de opslagklassespecificaties
| Specificatie | Betekenis in deze C17-code |
|---|---|
auto | Expliciete automatische opslag voor een blokvariabele; meestal weggelaten |
register | Een aanwijzing voor snelle toegang; het adres opvragen is verboden |
static binnen een functie | Blijvende opslag met een lokale naam |
static op bestandsniveau | Interne koppeling, waardoor de naam binnen de vertaaleenheid blijft |
extern | Een declaratie die naar een bestaand object of een bestaande functie verwijst |
Moderne compilers kiezen zelf de registertoewijzing, dus register schrijven verbetert de code zelden. auto int x; betekent in C17 hetzelfde als een gewone lokale int x;. C23 staat ook typeafleiding toe met auto x = 3.5;, wat een double declareert.
C17 heeft ook _Thread_local, in C23 geschreven als thread_local, voor threadopslag. De grammatica deelt typedef in bij de opslagklassespecificaties, hoewel het een type benoemt in plaats van opslag te kiezen. In C17 staat normaal gesproken maar één opslagklassespecificatie in een declaratie; _Thread_local kan ook samengaan met static of extern.
Een extern int count; binnen een blok maakt geen automatische lokale kopie. Die declareert een object met koppeling en statische opslagduur. Kijk altijd waar de declaratie staat voordat je bepaalt wat een opslagklassesleutelwoord doet.
Belangrijkste punten
- Initialiseer automatische variabelen voordat je ze leest.
- Statische opslag duurt de hele uitvoering van het programma en begint standaard op nul.
- Een statische lokale variabele behoudt haar waarde terwijl haar naam lokaal blijft.
- Een statische initialisator wordt één keer uitgevoerd; een toewijzing wordt uitgevoerd wanneer die wordt bereikt.
- De plaats van
staticbepaalt of de belangrijkste rol betrekking heeft op levensduur of koppeling.
2.3 Deel gegevens zorgvuldig via globale variabelen
Een globale variabele wordt op bestandsniveau gedeclareerd, buiten elke functie. Ze behoudt gedurende de hele uitvoering een waarde en kan verschillende functies toegang geven tot gedeelde toestand.
Volg het gedeelde object
#include <stdio.h>
double total_km = 0.0;
void add_trip(double km)
{
total_km += km;
}
int main(void)
{
add_trip(12.5);
add_trip(7.25);
printf("%.2f\n", total_km);
return 0;
}
Beide aanroepen werken hetzelfde object bij en leveren 19.75 op. De parameter km is nog steeds een kopie; daarnaast wijst de functie een waarde toe aan een globale variabele die binnen haar bereik ligt. Doorgifte op waarde verhindert die toewijzing niet.
Globale variabelen hebben statische opslagduur. Numerieke globale variabelen beginnen standaard op nul en vereisen constante expressies als initialisator: int error_count; begint op 0 en double limit = 2.5 * 4; begint op 10.0.
Controleer zichtbaarheid en naamverberging
Een globale naam is alleen zichtbaar onder de declaratie. Een functie boven de definitie heeft een eerdere declaratie nodig, zoals extern double total_km;.
Als add_trip in plaats daarvan het volgende bevat:
double total_km = 0.0;
total_km += km;
maakt de functie een lokaal object aan en werkt ze dat bij. De globale variabele blijft ongewijzigd. Verwijder de extra declaratie om het gedeelde totaal bij te werken; -Wshadow helpt onbedoelde naamverberging te herkennen.
Maak invoer expliciet als delen niet nodig is
Een gedeelde foutenteller kan nuttig zijn wanneer verschillende functies fouten melden. Maar een berekening die een globale variabele leest, heeft invoer die niet in de parameterlijst staat. Tests moeten die toestand instellen en om een verkeerde waarde te achterhalen moet je nagaan welke functies die kunnen wijzigen.
Een alternatief voor de ritberekening is:
double with_trip(double total, double km)
{
return total + km;
}
De aanroepende functie gebruikt total = with_trip(total, 12.5);. Een test levert beide invoerwaarden rechtstreeks, zonder globale variabelen in te stellen.
Beperk gedeelde namen
Gewone variabelen op bestandsniveau hebben externe koppeling. Deel er één via declaraties en één definitie; herhaalde definities in verschillende bestanden verstoren die opzet. static op bestandsniveau geeft daarentegen interne koppeling, zodat een ander bestand een eigen onafhankelijk object met dezelfde naam kan hebben. Beide objecten bestaan nog steeds gedurende de hele uitvoering: het sleutelwoord beperkt hier de koppeling, niet de levensduur.
Belangrijkste punten
- Een globale variabele behoudt één waarde tussen aanroepen en heeft statische opslagduur.
- Doorgifte op waarde verhindert niet dat een functie een zichtbare globale variabele wijzigt.
- Gedeelde toestand voegt invoer en effecten toe die een parameterlijst niet toont.
- Geef de voorkeur aan expliciete parameters en retourwaarden wanneer gedeelde toestand niet nodig is.
3Gebruik de C-preprocessor
Voordat C-code wordt gecompileerd, neemt de preprocessor headerbestanden op, breidt macro’s uit en selecteert voorwaardelijke takken. Deze bewerkingen veranderen de code die de compiler ontvangt, zodat een korte macro een expressie op verrassende manieren kan veranderen. Je bekijkt macro-expansies, groepeert expressies veilig en gebruikt opties en beveiligingen tegen herhaalde inclusie om te bepalen welke tekst behouden blijft. Het belangrijkste onderscheid is dat tussen code kiezen tijdens het bouwen en een uitvoeringspad kiezen terwijl een programma draait.
3.1 Neem bestanden op en definieer macro’s
De preprocessor zet brontekst om voordat de compiler C-expressies en typen controleert. De richtlijnen beginnen met #. Bekijk de uitvoer met gcc -E file.c als een opname of macro-expansie moeilijk te verklaren is.
Neem declaraties op en geef constanten een naam
#include <stdio.h> voegt een standaardheaderbestand in. Gebruik #include "convert.h" voor een projectheaderbestand: GCC doorzoekt eerst de map van het bestand dat het headerbestand opneemt en daarna de ingestelde zoekpaden voor headerbestanden. De optie -Iinclude voegt een zoekmap toe. Opnemen kopieert de tekst van het headerbestand; het koppelt geen gecompileerde functies.
Een objectachtige macro vervangt tokens:
#define MAX_STUDENTS 30
Later gebruik van het token MAX_STUDENTS wordt 30. De preprocessor laat de string "MAX_STUDENTS" en de andere naam MAX_STUDENTS_2 ongemoeid. Schrijf geen = en geen afsluitende puntkomma. Met #define PI 3.14159; wordt return PI * r * r; uitgebreid tot de ongeldige code return 3.14159; * r * r;. Een richtlijn eindigt normaal gesproken bij de nieuwe regel; een afsluitende backslash zet de richtlijn voort.
Breid uit vóór het rekenen
Een functieachtige macro heeft geen spatie tussen de naam en het openingshaakje:
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
SQUARE_BAD(2 + 3) wordt 2 + 3 * 2 + 3, wat 11 oplevert. 100 / SQUARE_BAD(5) wordt 100 / 5 * 5, wat 100 oplevert. De gegroepeerde macro geeft respectievelijk 25 en 4. Zet haakjes om elke parameter en om de volledige vervangingsexpressie, zodat omringende operatoren de groepering niet kunnen veranderen.
Als je in plaats daarvan #define SQUARE (x) ((x) * (x)) schrijft, maak je een objectachtige macro waarvan de vervanging met (x) begint. De spatie verandert de betekenis van de definitie.
Haakjes voorkomen geen herhaalde evaluatie
SQUARE(i++) wordt uitgebreid tot ((i++) * (i++)). Dit wijzigt i tweemaal zonder vastgelegde volgorde, dus het gedrag is ongedefinieerd. Een echte functie zoals square(i++) evalueert het argument één keer voordat het functieblok begint. Geef voor berekeningen de voorkeur aan functies, vooral wanneer een invoerexpressie de toestand verandert.
Een functieachtige macro heeft geen gedeclareerde parameter- of retourtypen. De uitgebreide C-code wordt nog steeds op typen gecontroleerd, maar heeft geen prototype dat argumentconversies vastlegt. Macro’s blijven nuttig voor beveiligingen tegen herhaalde inclusie, voorwaardelijke compilatie en geheeltallige constante expressies. In C17 is een const int-variabele geen geheeltallige constante expressie; een macro of een enum-constante kan een case-waarde leveren. C23 introduceert ook constexpr.
Zet argumenten om in tokens of tekst
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b
SHOW(3 + 4); gebruikt # om het argument in tekst om te zetten en drukt 3 + 4 = 7 af. CAT(temp, 1) gebruikt ## om tokens samen te voegen tot de naam temp1. Bij omzetting naar een stringliteral worden de opgegeven argumenttokens gebruikt zonder macro’s binnen dat voorkomen uit te breiden; een gewoon voorkomen van het argument wordt wel uitgebreid.
Macro’s volgen het geldigheidsgebied van blokken niet. Een definitie blijft actief tot #undef NAME of het einde van de vertaaleenheid, ook als de richtlijn tussen accolades is geschreven. Verwijder een oude definitie met #undef voordat je de macro een andere vervangingstekst geeft.
Belangrijkste punten
- Preprocessing neemt tekst op en breidt tokens uit vóór de compilatie van C-code.
- Een macrodefinitie heeft geen
=of afsluitende puntkomma. - Zet haakjes om macroparameters en de volledige vervangingsexpressie.
- Herhaalde macroargumenten kunnen neveneffecten herhalen; gebruik een functie voor berekeningen.
#zet een argument om in een stringliteral en##voegt tokens samen.- Een macro blijft over blokgrenzen heen gedefinieerd totdat de definitie wordt verwijderd.
3.2 Compileer code voorwaardelijk
Voorwaardelijke compilatie selecteert welke bronregels de compiler bereiken. Daarmee kun je debuguitvoer inschakelen, platformcode kiezen of een headerbestand beschermen tegen herhaalde opname.
Test het bestaan of test een waarde
#define DEBUG 0
#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif
#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif
#ifdef DEBUG vraagt of de macro is gedefinieerd. #if DEBUG evalueert de vervangingswaarde als een geheeltallige constante expressie. Binnen #if telt een niet-gedefinieerde identifier als nul; een verkeerd gespelde macronaam kan ongemerkt een tak verwijderen. De expressies defined DEBUG en defined(DEBUG) testen beide het bestaan.
Gebruik #ifndef NAME om te testen of een macro niet bestaat. Sluit elke voorwaardelijke richtlijn af met #endif. #elif voegt een andere voorwaarde toe en #else biedt het alternatief. C23 voegt #elifdef en #elifndef toe; in C17 kun je die tests uitdrukken met #elif defined(NAME) of #elif !defined(NAME).
Kies de instelling tijdens het bouwen
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif
gcc -std=c17 -DDEBUG main.c definieert DEBUG als 1 en behoudt de afdrukopdracht. Zonder die definitie wordt de regel vóór het compileren verwijderd. -DLEVEL=2 levert een waarde alsof #define LEVEL 2 vóór de tekst van het bestand stond.
Dit verschilt van een gewone if, waarvan de voorwaarde wordt geëvalueerd wanneer het programma draait. Voorwaarden van de preprocessor kunnen geen gewone variabele lezen die tijdens de uitvoering bestaat. #if 0 verwijdert een blok, ook als de code al /* */-commentaar bevat waardoor het onveilig zou zijn om het blok in nog zo’n commentaar te plaatsen.
Beveilig een headerbestand
#ifndef CONVERT_H
#define CONVERT_H
double c_to_f(double c);
#endif
De eerste opname definieert CONVERT_H en behoudt het prototype. Latere opnamen in dezelfde vertaaleenheid slaan de inhoud over. Gebruik een onderscheidende naam voor de beveiliging, gebaseerd op de naam van het headerbestand; vermijd voorloopunderscores die voor de implementatie zijn gereserveerd. Een andere vertaaleenheid begint met een eigen preprocessing en neemt de declaraties voor zichzelf op. Een beveiliging voorkomt niet dat verschillende objectbestanden hetzelfde externe symbool definiëren.
Veel compilers ondersteunen #pragma once, maar dat is geen ISO C-richtlijn. Beveiligingen werken met standaard voorwaardelijke richtlijnen.
Lees de voorgedefinieerde informatie
| Naam | Informatie |
|---|---|
__FILE__ | Huidige naam van het bronbestand als string |
__LINE__ | Huidig regelnummer in de broncode |
__DATE__, __TIME__ | Datum en tijd van vertaling |
__STDC_VERSION__ | Geselecteerde versie van de standaard |
Versiewaarden zijn onder andere 199901L voor C99, 201112L voor C11, 201710L voor C17 en 202311L voor C23. __func__ bevat de huidige functienaam, maar is een voorgedefinieerde identifier, geen macro.
Een platformtak kan defined(_WIN32) of defined(__linux__) testen en daarna #else gebruiken voor het alternatief. Om een niet-ondersteunde bouw expliciet af te wijzen, schrijf je een voorwaarde gevolgd door #error "This program needs C11 or later" en #endif. Een weggelaten #endif levert daarentegen een richtlijnfout op zoals “unterminated conditional directive”.
Belangrijkste punten
#ifdeftest het bestaan;#iftest een waarde.- Niet-gedefinieerde identifiers in
#ifworden als nul geëvalueerd. -DNAME=valuelevert een macrodefinitie voor het bouwen.- Beveiligingen tegen herhaalde inclusie voorkomen herhaalde verwerking binnen één vertaaleenheid.
- Voorgedefinieerde macro’s maken informatie over het bestand, de regel, het bouwtijdstip en de standaard beschikbaar.
4Verdeel een programma over bronbestanden en headerbestanden
Een groter programma kan samenhangende functies in afzonderlijke bronbestanden bewaren en hun declaraties via headerbestanden delen. Elk bronbestand wordt onafhankelijk gecompileerd en de linker verbindt de resulterende objectbestanden. Je bouwt een kleine module, bepaalt welke namen andere bestanden nodig hebben en achterhaalt ontbrekende of dubbele definities. Als zowel aanroepende bestanden als het bestand met de definitie hetzelfde headerbestand opnemen, kan de compiler verschillen opmerken voordat het programma draait.
4.1 Compileer en koppel meerdere bronbestanden
Bij afzonderlijke compilatie wordt elk bronbestand omgezet in een objectbestand voordat het programma wordt gekoppeld. Zo kan een bouw ongewijzigde objectbestanden hergebruiken en krijgen samenhangende functies een duidelijke plaats.
Volg de bouwstappen
Een vertaaleenheid is één .c-bestand na preprocessing, inclusief de inhoud van de opgenomen headerbestanden. Elke eenheid wordt onafhankelijk gecompileerd.
| Stap | Resultaat | GCC-optie die daar stopt |
|---|---|---|
| Preprocessing | Uitgebreide C-tekst | -E |
| Compileren | Assemblytekst | -S |
| Assembleren | Objectbestand | -c |
| Koppelen | Uitvoerbaar bestand | Gewone volledige bouw |
Een objectbestand bevat machinecode en symboolinformatie over namen die het definieert of nodig heeft. Op Unix-achtige systemen heeft het meestal de extensie .o. De linker verbindt het gebruik van externe namen met definities uit objectbestanden en bibliotheken.
Compileer twee bestanden en koppel ze
Stel dat convert.h de functie double c_to_f(double c); declareert, dat convert.c dat headerbestand opneemt en de functie definieert, en dat main.c het headerbestand opneemt en de functie aanroept.
gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp
De eerste opdracht levert main.o op zonder te koppelen. De tweede levert convert.o op. De laatste maakt het uitvoerbare bestand temp uit beide objectbestanden. Je kunt ook in één opdracht compileren en koppelen:
gcc -Wall -Wextra -std=c17 main.c convert.c -o temp
Geef de bronbestanden op voor compilatie en de objectbestanden voor koppeling. Headerbestanden komen binnen via #include; vermeld ze niet als afzonderlijk te compileren programmabronnen. Als alleen convert.c verandert, hergebruik je main.o, bouw je convert.o opnieuw en koppel je opnieuw. Als een gedeeld headerbestand verandert, bouw je elk objectbestand waarvan de bron het opneemt opnieuw.
Achterhaal welke stap is mislukt
Een compilatiefout betreft een vertaaleenheid: ongeldige syntaxis, een verkeerd aantal argumenten of tegenstrijdige zichtbare typen. De linker kan deze fouten niet herstellen.
Een linkerfout betreft het samengevoegde programma. Als je alleen main.c bouwt, kan het prototype ervoor zorgen dat het compileren slaagt, terwijl het koppelen “undefined reference to c_to_f” of “Undefined symbols” meldt. De declaratie belooft een functie; ze levert niet de machinecode ervan. Voeg convert.c toe aan een gecombineerde bouw of convert.o aan de koppelopdracht.
Twee gekoppelde externe definities van dezelfde functie veroorzaken een fout wegens een dubbel symbool. Een gewoon programma heeft één main; het koppelen van twee bronbestanden die beide deze functie definiëren mislukt. Bestandsgebonden static-functies kunnen in verschillende vertaaleenheden dezelfde naam hebben omdat ze interne koppeling hebben.
Ook bibliotheken leveren definities. Op veel Linux-systemen heeft code die functies uit <math.h> gebruikt een koppeloptie voor de wiskundebibliotheek nodig: gcc main.c -o app -lm. Zet die bibliotheekoptie na de code die de bibliotheek nodig heeft.
Neem de interface op, compileer de implementatie
Vermijd #include "convert.c". Dat kopieert functieblokken naar een andere vertaaleenheid. Als convert.c ook wordt gecompileerd en gekoppeld, komen de externe definities tweemaal voor. Het .h-bestand van een module deelt de declaraties; het .c-bestand wordt één keer als bronbestand gecompileerd.
Belangrijkste punten
- Een vertaaleenheid is een gepreprocesset bronbestand met de opgenomen tekst.
-clevert een objectbestand op zonder te koppelen.- Koppel elk benodigd objectbestand aan het uitvoerbare bestand.
- Ontbrekende en dubbele definities zijn koppelproblemen.
- Neem headerbestanden op; compileer bronbestanden afzonderlijk.
4.2 Schrijf headerbestanden
Het headerbestand van een module deelt de interface; het bronbestand levert de implementatie. Zowel aanroepende bestanden als de implementatie nemen het headerbestand op, zodat hun declaraties overeenkomen.
Declareer de interface één keer
Een beveiligd convert.h bevat:
#ifndef CONVERT_H
#define CONVERT_H
double c_to_f(double c);
double f_to_c(double f);
#endif
De implementatie in convert.c neemt het bestand vóór de functieblokken op:
#include "convert.h"
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
double f_to_c(double f)
{
return (f - 32.0) * 5.0 / 9.0;
}
Een aanroepend bestand neemt hetzelfde headerbestand en <stdio.h> op. De aanroep printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); drukt 77.0 100.0 af.
Als de implementatie float c_to_f(float c) definieert, ziet de compiler de tegenstrijdige typen doordat het headerbestand is opgenomen. Bij onafhankelijke compilatie zonder die declaratie kan het verschil onopgemerkt blijven.
Kies de juiste inhoud
| Deel in een headerbestand | Bewaar in een bronbestand |
|---|---|
| Functieprototypes | Functieblokken met externe koppeling |
extern-variabeledeclaraties | Definities van gedeelde objecten |
| Benodigde macro’s en typen | Privé-hulpfuncties en -toestand |
Een headerbestand met int count = 0; definieert een object in elke vertaaleenheid die het opneemt, wat dubbele externe definities veroorzaakt. Zelfs int count; is een voorlopige definitie. De standaardinstelling van GCC sinds versie 10 meldt herhaalde kopieën bij het koppelen, terwijl sommige toolchains gemeenschappelijke symbolen samenvoegen en de programmeerfout verbergen. Gebruik extern int count; in het headerbestand en één definitie in een bronbestand.
Zorg dat opnemen betrouwbaar werkt
Een beveiliging voorkomt herhaalde verwerking binnen een vertaaleenheid, ook bij indirecte opname via andere headerbestanden. Compatibele prototypes mogen worden herhaald, maar typedefinities zoals een enum kunnen fouten door herdefinitie veroorzaken.
Een zelfstandig bruikbaar headerbestand neemt op wat de eigen declaraties nodig hebben. Als een prototype size_t gebruikt, neem daar dan <stddef.h> op. Aanroepende bestanden zouden geen speciale opnamevolgorde nodig moeten hebben.
Bij projectheaderbestanden zoekt GCC met aanhalingstekens eerst in de map van het bestand dat het headerbestand opneemt. Voeg een aparte map voor headerbestanden toe met -Iinclude, zoals in gcc -Iinclude -std=c17 -c src/main.c. Het objectbestand van de implementatie moet nog steeds worden gekoppeld.
Belangrijkste punten
- Headerbestanden delen prototypes,
extern-declaraties, macro’s en typen. - Bewaar definities van gedeelde objecten en functieblokken met externe koppeling in bronbestanden.
- Neem het headerbestand van een module op in de eigen implementatie.
- Gebruik beveiligingen tegen herhaalde inclusie en neem de headerbestanden op die je declaraties nodig hebben.
4.3 Regel de koppeling met static en extern
Koppeling bepaalt of declaraties op verschillende plaatsen naar dezelfde entiteit verwijzen. Dit staat los van het geldigheidsgebied, dat beschrijft waar je een naam kunt gebruiken, en de opslagduur, die beschrijft hoelang een object bestaat.
Verbind één naam met één object
| Koppeling | Betekenis |
|---|---|
| Extern | Declaraties kunnen in verschillende vertaaleenheden naar één entiteit verwijzen |
| Intern | Declaraties verwijzen binnen één vertaaleenheid naar een entiteit |
| Geen | Een lokale naam, zoals een gewone parameter, duidt een eigen entiteit aan |
Functies en gewone variabelen op bestandsniveau hebben standaard externe koppeling. static op bestandsniveau geeft interne koppeling. Gewone automatische lokale variabelen en parameters hebben geen koppeling.
Zet deze declaraties in een beveiligd headerbestand om één foutenteller te delen:
extern int error_count;
void log_error(int code);
Eén bronbestand neemt dat headerbestand op en levert:
int error_count = 0;
void log_error(int code)
{
printf("error: %d\n", code);
error_count++;
}
Dat bronbestand neemt ook <stdio.h> op voor printf. Andere bronbestanden nemen het gedeelde headerbestand op, geen tweede definitie. Na drie aanroepen verwijst elke aanroepende functie die error_count leest naar hetzelfde object en ziet 3.
Maak onderscheid tussen declaratie en definitie
extern int error_count; declareert een bestaand object en reserveert geen nieuwe opslag. int error_count = 0; definieert het object. Een initialisator verandert de situatie: op bestandsniveau is extern int error_count = 0; ondanks het sleutelwoord een definitie. Bewaar de initialisator in het ene bestand met de definitie.
Bij gewone openbare functiedeclaraties is extern optioneel: double c_to_f(double c); declareert al een functie met externe koppeling. Neem de openbare declaratie op in het bestand met de definitie, zodat de compiler de compatibiliteit kan controleren.
Verberg toestand achter functies
static int count = 0;
void counter_increment(void)
{
count++;
}
int counter_value(void)
{
return count;
}
De opgeslagen waarde bestaat gedurende de hele uitvoering, maar alleen deze vertaaleenheid kan er met de naam count naar verwijzen. Zet de openbare functieprototypes in een headerbestand. Een ander bestand kan de teller via die functies verhogen en lezen zonder de privévariabele rechtstreeks te wijzigen. Ook een hulpfunctie kan static zijn wanneer andere bestanden die niet nodig hebben.
Achterhaal een verkeerde keuze van koppeling
Een definitie static int count; in een headerbestand maakt een onafhankelijke privételler in elke vertaaleenheid die het opneemt. Als je de ene bijwerkt, worden de andere niet bijgewerkt. Beveiligingen tegen herhaalde inclusie voegen deze objecten niet samen: elke vertaaleenheid verwerkt een eigen kopie van het headerbestand.
Een functie die in één vertaaleenheid als static is gedefinieerd, kan niet voldoen aan een externe aanroep vanuit een andere; het koppelen mislukt voor die externe naam. Geef alleen functies die andere bestanden nodig hebben externe koppeling en declareer die in het headerbestand.
Binnen een functie geeft static een lokale variabele blijvende opslag zonder het geldigheidsgebied te vergroten. Buiten functies geeft het een object of functie interne koppeling. Variabelen op bestandsniveau hebben met of zonder dat sleutelwoord statische opslagduur.
Belangrijkste punten
- Externe koppeling verbindt declaraties in verschillende vertaaleenheden.
- Deel een variabele met
extern-declaraties en één definitie. staticop bestandsniveau maakt een object of functie privé voor de eigen vertaaleenheid.- Een headerbestand met een statische variabele geeft afzonderlijke objecten aan vertaaleenheden die het opnemen.
- Een initialisator maakt van een
extern-objectdeclaratie op bestandsniveau een definitie. - Het bewaren van een waarde met lokale
staticen de koppeling doorstaticop bestandsniveau hebben verschillende rollen.
5Bouw programma’s uit meerdere bestanden met make
Voor een correcte bouw is meer nodig dan een lijst compileropdrachten: het moet duidelijk zijn welke bestanden van welke andere bestanden afhangen. Een makefile legt die relaties vast, zodat na een wijziging in de broncode de betrokken objectbestanden opnieuw worden gecompileerd en het programma opnieuw wordt gekoppeld. Je volgt die beslissingen, neemt afhankelijkheden van headerbestanden op en verkort herhaalde opdrachten met variabelen en patroonregels. Proefuitvoeringen maken het plan zichtbaar voordat een recept wordt uitgevoerd.
5.1 Schrijf regels en afhankelijkheden in een makefile
make leest Makefile of makefile in de projectmap en werkt het gevraagde doel bij. Zonder doel als argument bouwt het normaal gesproken het eerste gewone doel.
Leg uitvoer en invoer vast
Een regel begint met target: prerequisites. Het recept staat op volgende regels die met een tab inspringen of na een puntkomma op dezelfde regel:
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o
main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c
convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c
.PHONY: clean
clean:; rm -f temp main.o convert.o
Hier is temp het standaarddoel. make clean vraagt om de opruimactie. Als je een recept op de volgende regel schrijft, gebruik dan een echt tabteken: spaties kunnen “missing separator” veroorzaken. Een instelling van je editor om witruimte zichtbaar te maken helpt het verschil te zien.
Volg de beslissingen om opnieuw te bouwen
Make werkt eerst de afhankelijkheden van een doel bij. Daarna bouwt het het doel opnieuw als dat ontbreekt of als een afhankelijkheid een recentere wijzigingstijd heeft.
- Eerste bouw: compileer beide bronbestanden en koppel daarna
temp, drie opdrachten. - Geen wijzigingen: er hoeven geen recepten voor het programma te worden uitgevoerd.
- Wijzig alleen
convert.c: compileer het opnieuw en koppel opnieuw, twee opdrachten; hergebruikmain.o. - Wijzig
convert.h: compileer beide bronbestanden opnieuw en koppel opnieuw, drie opdrachten.
Het headerbestand beïnvloedt beide vertaaleenheden, ook al is geen van beide bronbestanden gewijzigd. Stel je voor dat invoerbestanden objectbestanden opleveren en objectbestanden vervolgens het uitvoerbare bestand. Wijzigingen werken door via die vastgelegde relaties.
Neem afhankelijkheden van headerbestanden op
De onvolledige regel main.o: main.c mist wijzigingen in convert.h. Make leest geen C-broncode om opgenomen bestanden te ontdekken; het volgt de gedeclareerde afhankelijkheden. Vermeld opgenomen headerbestanden of laad door de compiler gegenereerde afhankelijkhedenbestanden om te voorkomen dat je verouderde objectbestanden koppelt.
Houd afhankelijke shellbewerkingen bij elkaar
Afzonderlijke receptregels gebruiken normaal gesproken afzonderlijke shells. Een regel met alleen cd build verandert de map niet voor de volgende regel. Gebruik één opdracht zoals cd build && gcc -c ../main.c wanneer het compileren afhangt van die mapwijziging. Bekijk geplande recepten vooraf met make -n.
Belangrijkste punten
- Regels benoemen doelen, afhankelijkheden en met een tab ingesprongen recepten.
- Make werkt afhankelijkheden bij voordat het beslist of hun doel opnieuw moet worden gebouwd.
- Een ontbrekend doel of een nieuwere afhankelijkheid leidt tot opnieuw bouwen.
- Wijzigingen in headerbestanden moeten ertoe leiden dat elk betrokken objectbestand opnieuw wordt gebouwd.
- Elke receptregel draait normaal gesproken in een eigen shell.
5.2 Vereenvoudig makefiles met variabelen en patroonregels
Variabelen en patroonregels verwijderen herhaalde compileropdrachten terwijl de afhankelijkheidsgraaf behouden blijft. De voorbeelden plaatsen elk recept na een puntkomma op dezelfde regel als het doel en de afhankelijkheden. Verkort een makefile pas nadat de bouwrelaties correct zijn.
Geef gedeelde instellingen een naam
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o
temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^
%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:; rm -f temp $(OBJS)
$(CC) wordt vervangen door de waarde van de compilervariabele; $(CFLAGS) door de bijbehorende opties. CPPFLAGS bevat volgens afspraak preprocessoropties zoals -DDEBUG of -Iinclude. De opties -Wall -Wextra schakelen waarschuwingen in, -g voegt debuginformatie toe en -O2 schakelt optimalisatie in. Niet-gedefinieerde make-variabelen worden vervangen door lege tekst. Schrijf $(CC), niet $CC: het laatste betekent de eenletterige variabele C, gevolgd door een letterlijke C.
Lees automatische variabelen binnen een regel
| Variabele | Waarde in de huidige regel |
|---|---|
$@ | Doelnaam |
$< | Eerste afhankelijkheid |
$^ | Alle afhankelijkheden, zonder dubbele namen |
$? | Afhankelijkheden die nieuwer zijn dan het doel |
Voor main.o is de stam van het patroon main, zodat %.c verandert in main.c. Het compilatierecept gebruikt $< om dat bronbestand aan de compiler door te geven en $@ om main.o te benoemen. Het headerbestand is een afhankelijkheid, maar wordt niet als extra invoer voor compilatie doorgegeven.
In de koppelregel zijn alle afhankelijkheden objectbestanden. $^ wordt uitgebreid tot main.o convert.o en $@ wordt temp. Als je daar $< zou gebruiken, zou alleen main.o worden doorgegeven en zouden de definities in convert.o ontbreken.
GNU make heeft ingebouwde regels die x.c naar x.o kunnen compileren met CC, CPPFLAGS en CFLAGS. Een expliciete afhankelijkheidsregel zonder recept kan dat ingebouwde recept gebruiken. GNU make kan zelfs met make hello een eenvoudig programma uit hello.c bouwen zonder makefile.
Markeer actiedoelen als schijndoelen
clean benoemt een actie, geen bestand dat moet worden gemaakt. .PHONY: clean zorgt ervoor dat de actie wordt uitgevoerd, ook als er een bestand met de naam clean bestaat. rm -f accepteert dat bestanden al ontbreken. Een all-doel dat verschillende programma’s groepeert, is meestal ook een schijndoel.
Genereer headerlijsten en bekijk wat opnieuw wordt gebouwd
Voeg -MMD -MP toe aan de compilatieopties om naast elk objectbestand een .d-bestand te schrijven. -MMD legt afhankelijkheden van gebruikersheaderbestanden vast; -MP voegt lege regels toe die helpen wanneer een vastgelegd headerbestand wordt verwijderd. Laad de lijsten na het gewone standaarddoel met:
-include $(OBJS:.o=.d)
De vervanging zet namen van objectbestanden om in namen van afhankelijkhedenbestanden. Het minteken vooraan staat ontbrekende .d-bestanden bij de eerste bouw toe. Verwijder deze gegenereerde bestanden samen met de objectbestanden in clean.
make -n toont opdrachten vooraf, make -B dwingt opnieuw bouwen af en make -j4 staat maximaal vier recepten tegelijk toe. Correcte afhankelijkheden zijn ook belangrijk bij parallel bouwen. Alleen de tekst van CFLAGS wijzigen maakt bestaande objectbestanden niet ouder dan een afhankelijkheid. Gebruik na het wijzigen van opties make -B of bouw opnieuw na het opschonen, zodat elk objectbestand de nieuwe instellingen gebruikt.
Belangrijkste punten
- Gebruik
$(NAME)om de waarde van een benoemde make-variabele in te voegen. $@,$<en$^duiden het doel, de eerste afhankelijkheid en alle afhankelijkheden aan.- Patroonregels hergebruiken één recept voor bestanden met overeenkomende stammen.
- Schijndoelen voeren acties uit, ook wanneer overeenkomende bestanden bestaan.
- Gegenereerde
.d-bestanden leggen afhankelijkheden van gebruikersheaderbestanden vast. - Dwing na het wijzigen van compileropties een nieuwe bouw af of bouw opnieuw na het opschonen.
Functies en programmastructuur in C: voorbeelden
1Maak en volg een functieaanroep
19 stappen
- 1IntroductieBegin met een kleine taak
Volg een waarde naar een functie en zoek daarna waar het resultaat naartoe gaat. We maken een definitie en een prototype, onderscheiden een gekopieerde parameter van de variabele in de aanroepende functie en volgen recursieve aanroepen naar een basisgeval en terug via hun retourwaarden.
- 2VoorbeeldGeef een taak invoer met een type
We willen een functie die een vaste correctie optelt bij een geheeltallige meetwaarde.
int corrected(int reading) { return reading + 4; }intvóór de naam beschrijft het resultaat.int readingdeclareert een parameter en het functieblok berekent de retourwaarde. Definieer deze functie buiten alle andere functieblokken; standaard-C ondersteunt geen geneste definities. - 3VoorbeeldGebruik een aanroep als expressie
Bij
int result = corrected(9);initialiseert het argument9de parameterreading. De retourexpressie levert13op en de toewijzing van de aanroepende functie slaat die waarde op inresult.De aanroep kan ook binnen een andere expressie staan:
2 * corrected(9)geeft26. Als je alleencorrected(9);schrijft, wordt de aanroep nog steeds uitgevoerd, maar wordt de retourwaarde weggegooid. - 4TipKies tussen teruggeven en afdrukken
Een berekening kan een resultaat teruggeven zonder het af te drukken. Daardoor kan een aanroepende functie het resultaat opslaan, vergelijken of later afdrukken.
Een functie die als
voidis gedeclareerd, kan uitvoer verzorgen maar levert geen waarde voor een toewijzing. Binnen die functie verlaatreturn;de functie zonder waarde. Zorg er bij een functie die een waarde teruggeeft voor dat elk pad dat een resultaat oplevert een passendereturnbereikt; een getal tonen levert het niet aan de aanroepende functie. - 5UitdagingControleer de berekening vóór het teruggeven
Wat geeft deze functie terug voor
average_pair(5, 8)?double average_pair(int a, int b) { return (a + b) / 2; }---reveal---
Ze geeft
6.0terug: geheeltallige optelling geeft13, geheeltallige deling geeft6en conversie naar het retourtype geeft6.0. Gebruik2.0om de deling met drijvendekommagetallen uit te voeren als je6.5nodig hebt. Het retourtype kan een al weggegooide breuk niet herstellen. - 6UitlegLicht de compiler vóór de aanroep in
Als het functieblok later komt, plaats je
int corrected(int reading);vóór de aanroep. Met dit prototype kan de compiler het aantal argumenten en de conversies controleren. De puntkomma geeft een declaratie aan; de definitie levert het functieblok. - 7KernpuntGeef een lege invoerlijst op
Gebruik
int read_sensor(void);voor geen parameters, zowel in C17 als in C23. In C17 laatint read_sensor();de parameterinformatie ongespecificeerd; in C23 betekent die schrijfwijze dat er geen parameters zijn. Selecteer C17 expliciet met-std=c17. - 8UitdagingVolg een toegestane conversie
De compiler heeft
int corrected(int reading);gezien. Wat geeftcorrected(6.8)terug en moet de aanroep worden afgewezen?---reveal---
De aanroep kan worden gecompileerd. Het prototype zorgt ervoor dat
6.8wordt geconverteerd naar het gehele getal6; daarna telt de functie er4bij op en geeft10terug. Een conversiewaarschuwing kan helpen verloren informatie zichtbaar te maken; gebruik-Wconversion. Een prototype controleert een aanroep aan de hand van de gedeclareerde typen, maar niet elk afwijkend numeriek argumenttype is een fout. - 9VoorbeeldTeken twee afzonderlijke waarden
Bekijk een andere functie:
void change(int reading) { reading += 4; }De aanroepende functie voert
int sample = 9; change(sample);uit. Bij het begin bevattensampleen de parameter elk9in afzonderlijke objecten. Het functieblok verandert de parameter in13. Wanneer de aanroep terugkeert, bevatsamplenog steeds9: er is geen waarde aan toegewezen. - 10VerbandLaat de aanroepende functie een resultaat bewaren
Gebruik
sample = corrected(sample);om het resultaat te bewaren. De functie rekent met een kopie; de aanroepende functie slaat daarna de retourwaarde op. Overeenkomende namen zouden hun objecten niet verbinden: de toewijzing werktsamplebij. - 11UitdagingZoek de aanroep waarvan het resultaat wordt weggegooid
Beginnend met
int sample = 7;voert de aanroepende functie uit:corrected(sample); sample = corrected(sample);Wat is
sampledaarna?---reveal---
De eerste aanroep berekent
11en gooit die waarde weg, zodatsampleop7blijft. De tweede ontvangt ook7, geeft11terug en deze keer slaat de aanroepende functie de waarde op. De eindwaarde is11, niet15; een aanroep werkt de argumentvariabele niet bij alleen doordat de functie een gewijzigde waarde teruggeeft. - 12UitlegVoeg een stopregel voor recursie toe
Een recursieve functie roept zichzelf aan voor een kleiner probleem:
int total_to(int n) { if (n <= 0) return 0; return n + total_to(n - 1); }Bij een positieve invoerwaarde kom je door één af te trekken dichter bij het basisgeval. Het basisgeval geeft direct antwoord. Het recursieve geval kan de optelling pas voltooien als de kleinere aanroep terugkeert.
- 13VoorbeeldSchrijf de aanroepen op terwijl je afdaalt
Volg
total_to(3). De eerste aanroep bevatn == 3en wacht om3op te tellen bijtotal_to(2). Die aanroep heeft een eigenn == 2en wacht optotal_to(1).De aanroep met
n == 1wacht optotal_to(0), die onmiddellijk0teruggeeft. Op dat diepste punt zijn vier aanroepen actief. Vervang de eerdere waarden vannniet door nul; ze horen bij verschillende aanroepen. - 14VoorbeeldVul de resultaten in terwijl je terugkeert
Zodra
total_to(0)de waarde0teruggeeft, kan het wachtende werk worden voltooid:- De aanroep met
n == 1geeft1 + 0, oftewel1, terug. - De aanroep met
n == 2geeft2 + 1, oftewel3, terug. - De aanroep met
n == 3geeft3 + 3, oftewel6, terug.
De meeste implementaties bewaren deze actieve aanroepen in stackframes. Elke terugkeer hervat de aanroepende functie na de aanroep en geeft daarna de automatische toestand van die aanroep vrij.
- De aanroep met
- 15UitdagingControleer of een basisgeval bereikbaar is
Stel dat de basistest in
total_towordt gewijzigd inn == 0en de recursieve stap nog steedsn - 1gebruikt. Wat gaat er mis als het eerste argument-2is?---reveal---
De invoerwaarden worden
-2,-3,-4en gaan steeds verder van nul af. De stoptest bestaat, maar is langs dat pad onbereikbaar. Doorgaande recursie kan de stapelruimte uitputten. De versie metn <= 0stopt bij deze invoer onmiddellijk. - 16TipControleer ook de grootte van het antwoord
Een recursieve berekening die stopt, kan nog steeds de grenzen van het retourtype overschrijden. Bij een 32-bits
intpastfactorial(12), terwijl de wiskundige waarde vanfactorial(13)het maximum2147483647overschrijdt.Overloop bij gehele getallen met teken is ongedefinieerd gedrag. Controleer voordat je recursieve invoerwaarden kiest zowel of de aanroepen stoppen als of de tussen- en eindresultaten binnen de gebruikte typen passen. Ook bij een lus moet je de berekeningen controleren.
- 17TipScheid aanroepen wanneer de volgorde ertoe doet
Alle argumentexpressies zijn geëvalueerd voordat het blok van de aangeroepen functie begint, maar C specificeert hun evaluatievolgorde niet. Als twee aanroepen gedeelde toestand wijzigen, bereken hun resultaten dan in afzonderlijke instructies voordat je die resultaten verder doorgeeft.
Vermijd
f(i, i++): dit leest en wijzigtizonder de vereiste volgorde. Doorgifte op waarde maakt een onveilige argumentexpressie niet veilig. - 18UitdagingCombineer declaratie, conversie en aanroepen
Als
total_tovóór gebruik is gedeclareerd, schrijft een aanroepende functieint x = 2; total_to(x); x = total_to(3.9);. Wat is de uiteindelijke waarde vanx?---reveal---
Het eerste resultaat wordt weggegooid, dus
xblijft2. Het tweede argument wordt geconverteerd naar3; het recursieve uitvoeringsspoor geeft6terug en de toewijzing slaat die waarde op inx. De declaratie bepaalt de conversie, recursie berekent de waarde en de aanroepende functie beslist of die wordt bewaard. - 19SamenvattingGebruik drie controlepunten voor een aanroep
Controleer bij een aanroep de declaratie, volg de gekopieerde invoer en zoek waar de retourwaarde wordt gebruikt. Houd bij recursie elke actieve aanroep afzonderlijk bij en combineer de resultaten vanaf het basisgeval naar buiten toe.
2Volg namen en bewaarde waarden
13 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieBegin met een kleine taak
- 2VoorbeeldLees de dichtstbijzijnde zichtbare declaratie
- 3UitlegHoud het geldigheidsgebied aan waarin de functie is geschreven
- 4UitdagingHerstel een waarde die na een blok nodig is
- 5VoorbeeldZie hoe een automatische teller opnieuw begint
- 6VoorbeeldBewaar de teller met static
- 7UitdagingOnderscheid initialisatie van toewijzing
- 8TipLees het sleutelwoord op zijn plaats
- 9VoorbeeldVolg een wijziging van een globale variabele
- 10UitdagingZoek de verbergende lokale variabele
- 11VerbandMaak een berekening gemakkelijker te testen
- 12UitdagingCombineer geldigheidsgebied en bewaarde toestand
- 13SamenvattingNoteer namen en levensduur afzonderlijk
3Bekijk code vóór het compileren
13 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieBegin met een kleine taak
- 2UitlegBekijk wat opname daadwerkelijk levert
- 3VoorbeeldVervang tokens voordat je operatorregels toepast
- 4VoorbeeldBescherm de binnenste en buitenste groepering
- 5UitdagingZoek wat haakjes niet kunnen herstellen
- 6TipHoud macroregels gescheiden van regels voor geldigheidsgebieden
- 7VoorbeeldTest een instelling op bestaan en waarde
- 8UitdagingSelecteer een tak met een compileroptie
- 9VoorbeeldDoorloop de eerste opname van een headerbestand
- 10KernpuntBegin met een nieuwe beveiligingstoestand in een andere eenheid
- 11VerbandZoek een diagnostische melding in de broncode
- 12UitdagingCombineer expansie en selectie van een tak
- 13SamenvattingBekijk de omzetting in de juiste volgorde
4Bouw één programma uit twee bronbestanden
14 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieBegin met een kleine taak
- 2UitlegVolg één vertaaleenheid naar een objectbestand
- 3VoorbeeldSchrijf de gedeelde declaratie
- 4VoorbeeldZet de implementatie in een eigen bronbestand
- 5UitdagingPlaats één gedeelde variabele correct
- 6VoorbeeldCompileer afzonderlijk en koppel daarna
- 7UitdagingHerstel een niet-gedefinieerde verwijzing
- 8TipKopieer de implementatie niet naar het aanroepende bestand
- 9VoorbeeldHoud een teller binnen de module
- 10UitlegOnderscheid drie eigenschappen van de teller
- 11UitdagingLeg twee onafhankelijke tellers uit
- 12TipMaak een headerbestand zelfstandig bruikbaar
- 13UitdagingHerstel afscherming en interface samen
- 14SamenvattingKoppel elke behoefte aan de juiste bouwstap
5Bouw alleen wat is veranderd
15 stappen
Sla deze les op om elke Walkthrough stap voor stap te volgen.
- 1IntroductieBegin met een kleine taak
- 2VoorbeeldLeg de invoer voor het uitvoerbare bestand en de objectbestanden vast
- 3VoorbeeldGeef een objectbestand een recept
- 4VoorbeeldVoer de eerste bouw uit en bouw na een bronwijziging opnieuw
- 5UitdagingVolg een wijziging in een gedeeld headerbestand
- 6TipBekijk het plan voordat je het uitvoert
- 7UitlegVervang herhaalde instellingen door variabelen
- 8VoorbeeldBreid het koppelrecept uit
- 9VoorbeeldGebruik één compilatiepatroon voor beide objectbestanden
- 10UitdagingKies een bronbestand zonder het headerbestand te compileren
- 11TipMaak van opschonen een actie
- 12VerbandLaat de compiler afhankelijkheden van headerbestanden vastleggen
- 13UitdagingZorg dat parallel opnieuw bouwen correct verloopt
- 14UitdagingPas gewijzigde opties toe op het hele programma
- 15SamenvattingControleer de graaf voordat je de uitvoer vertrouwt
Functies en programmastructuur in C: oefenvragen
Set 1: Volg waarden door functieaanroepen10 Oefenvragen
Vraag 1 van 10
int triple(int value) { return value * 3; }?Toon antwoordVerberg antwoord
value, de variabele die met het opgegeven argument wordt geïnitialiseerd. triple benoemt de functie; value * 3 is de teruggegeven expressie, geen parameter.Vraag 2 van 10
void report(int code)?Toon antwoordVerberg antwoord
void-aanroep kan als instructie een taak uitvoeren, maar levert geen resultaat. De parameter int code beschrijft invoer en maakt de aanroep dus niet bruikbaar in een geheeltallige toewijzing.Vraag 3 van 10
double portion(int total) { return total / 4; } terug voor portion(11)?Toon antwoordVerberg antwoord
total / 4 zijn gehele getallen, dus berekent het functieblok 11 / 4 als 2. Dat resultaat wordt daarna geconverteerd naar double, wat 2.0 oplevert. Een retourtype double maakt van de eerdere deling geen drijvendekommadeling; voor 2.75 zou een drijvendekommaoperand nodig zijn.Vraag 4 van 10
int capacity(void) staat onder main. Welke declaratie boven main beschrijft in C17 een aanroep zonder invoer volledig?Toon antwoordVerberg antwoord
int capacity(void); declareert expliciet een geheeltallig resultaat en geen parameters. In C17 laat int capacity(); de parameterinformatie ongespecificeerd in plaats van de aanroep zonder invoer volledig te beschrijven.Vraag 5 van 10
int fifth(int n) { return n / 5; } vóór gebruik is gedeclareerd, wat is dan fifth(-14.8)?Toon antwoordVerberg antwoord
n == -14 geeft. Geheeltallige deling door 5 geeft daarna -2. De keuze -3 zou naar beneden afronden in plaats van de C-regel voor afkappen naar nul bij deze conversies en geheeltallige deling toe te passen.Vraag 6 van 10
double stretch(int n);. Welke definitie is compatibel met die declaratie?Toon antwoordVerberg antwoord
double blijven en het parametertype int; de parameternaam mag veranderen. Als je de naam n behoudt maar het type verandert in double, blijft dat in strijd met het prototype. Compatibiliteit hangt af van de gedeclareerde typen en de parameterlijst, niet van overeenkomende parameternamen.Vraag 7 van 10
Wat drukt het volgende volledige programma af?
#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
int level = 17;
lower(level);
printf("%d\n", level);
}
Toon antwoordVerberg antwoord
17 naar de parameter n en de aftrekking verandert alleen die parameter. level in main bevat nog steeds 17 wanneer die waarde wordt afgedrukt. Voor het voor de hand liggende antwoord 13 zou een retourwaarde aan level moeten worden toegewezen of een andere bewerking het object van de aanroepende functie daadwerkelijk moeten wijzigen.Vraag 8 van 10
Wat is de uiteindelijke waarde van x?
int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
Toon antwoordVerberg antwoord
14 maar gooit die waarde weg, zodat x in de aanroepende functie 4 blijft. De volgende aanroep ontvangt 5, verhoogt de lokale variabele tot 8 en geeft 16 terug, dat aan de variabele van de aanroepende functie wordt toegewezen. Het resultaat 22 houdt ten onrechte de lokale wijziging van de eerste aanroep vast alsof die de variabele van de aanroepende functie vóór de tweede aanroep heeft gewijzigd.Vraag 9 van 10
Wat geeft depth(3) terug?
int depth(int n) {
if (n == 0) return 2;
return 1 + depth(n - 1);
}
Toon antwoordVerberg antwoord
3 naar 0, waar het basisgeval 2 teruggeeft. Elk van de drie wachtende aanroepen telt er één bij op, wat achtereenvolgens 3, 4 en 5 oplevert. Het resultaat 3 telt de recursieve stappen, maar laat de waarde van het basisgeval weg.Vraag 10 van 10
Welke waarde slaat int answer = collect(4.9); op?
int collect(int n) {
if (n <= 1) return 1;
return n + collect(n - 2);
}
Toon antwoordVerberg antwoord
4, zodat de actieve aanroepen 4, 2 en 0 gebruiken. De laatste geeft 1 terug; de eerdere aanroepen geven 2 + 1 == 3 en 4 + 3 == 7 terug. Het antwoord 6 laat de aanroep met invoer nul ten onrechte nul teruggeven, terwijl het daadwerkelijke basisgeval één teruggeeft.Set 2: Volg lokale namen en bewaarde toestand10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
i uit for (int i = 0; i < 6; i++) { } gebruiken?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
Welke waarden drukt dit programma af?
#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
int size = 4;
show();
printf("%d", size);
}
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
Wat wordt afgedrukt?
int total = 0;
for (int i = 1; i <= 3; i++) {
int total = i;
total += 2;
}
printf("%d", total);
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
int amount = amount + 2; binnen een blok niet vanuit een buitenste amount?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
static int calls = 0; geïnitialiseerd tijdens één uitvoering van het programma?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
Welke waarden geven drie afzonderlijke aanroepen van tick terug?
int tick(void) {
static int n = 5;
n += 2;
return n;
}
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
Wat is result na int result = visit(2);?
int visit(int n) {
static int calls = 0;
calls++;
if (n == 0) return calls;
int inner = visit(n - 1);
return inner + calls;
}
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
int permitted(int n) leest een globale limit. Waarom kan hetzelfde argument bij een latere aanroep een ander resultaat opleveren?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
Welke twee gehele getallen worden afgedrukt?
#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
int count = 2;
count = advance(count);
int next = advance(1);
printf("%d %d", count, next);
}
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
read_sensor(). Waarom is static int first = read_sensor(); ongeldig in C17?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 3: Breid macro’s uit en kies takken tijdens het bouwen10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
#define LIMIT 18 in latere broncode?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
Wat is de waarde van 5 * INC(3)?
#define INC(x) x + 1
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
#define LIMIT 12; de instructie int count = LIMIT * 2; ongeldig?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
Wat zijn na dit codefragment de waarden van a en b?
#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
Wat is het gedrag van CUBE(i++) met deze definitie?
#define CUBE(x) ((x) * (x) * (x))
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
#ifdef TRACE bij #define TRACE 0?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
#if VERBOSE ongemerkt een tak overslaan als de bouw alleen VERB0SE definieert?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
Welke waarde voor result bereikt de compiler bij -DLEVEL=2?
#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
Welke tekst drukt dit volledige programma af?
#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
int item2 = 7;
PRINT(JOIN(item, 2));
}
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
int mode = 2;. Twee afzonderlijke bronbestanden nemen het op en worden gecompileerd en daarna gekoppeld. Waarom voorkomt de beveiliging geen fout wegens een dubbele definitie?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 4: Onderzoek headerbestanden, objectbestanden en koppeling10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
gcc -std=c17 -c driver.c normaal gesproken op een Unix-achtig systeem op?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
reading op, maar als alleen het objectbestand daarvan wordt gekoppeld, volgt een niet-gedefinieerde verwijzing naar reading. Wat moet worden toegevoegd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
int classify(double x);, maar het bronbestand definieert double classify(double x). Waardoor wordt het verschil zichtbaar tijdens het compileren van het bronbestand met de definitie?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
static int cached = 3; als die buiten elke functie wordt gedeclareerd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
static int errors; en void record(void);. Het ene bronbestand neemt het op en definieert record om errors te verhogen; een ander neemt het op, roept record tweemaal aan en drukt daarna de eigen errors af. Wat wordt afgedrukt en waarom?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
int failures te laten gebruiken die in één bronbestand is gedefinieerd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
main.c neemt "calc.c" op; calc.c neemt een beveiligd calc.h op en definieert de openbare int calc(int n). Bij het bouwen met gcc main.c calc.c -o run worden twee definities van calc gemeld. Welke wijziging behoudt afzonderlijke compilatie en controleert aanroepen correct?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
static int quota = 9; en int quota_value(void) { return quota; }. Een ander neemt een headerbestand op dat zowel extern int quota; als int quota_value(void); declareert en gebruikt daarna beide namen. Hoe herstel je de interface correct terwijl de opgeslagen quota privé blijft?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
size_t packet_count(void);. Wat maakt het headerbestand zelfstandig bruikbaar?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
extern int jobs = 1; op. Het ene verhoogt jobs en het andere drukt de waarde af. Welke oplossing maakt één gedeeld object terwijl de typen in beide bronbestanden worden gecontroleerd?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Set 5: Plan betrouwbare incrementele bouwprocessen10 Oefenvragen
Sla deze les op om elk antwoord te zien en elke set te oefenen.
Vraag 1 van 10
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 2 van 10
viewer hangt af van main.o en screen.o; elk objectbestand hangt af van het bijbehorende bronbestand. Alleen screen.c wordt nieuwer dan het objectbestand en alle andere invoer blijft ongewijzigd. Welke recepten zijn nodig?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 3 van 10
settings.h op, maar de regel voor het objectbestand vermeldt alleen het bronbestand. Waarom kan een wijziging in het headerbestand ertoe leiden dat het programma na make ongewijzigd blijft?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 4 van 10
$@ en $^ uitgebreid bij de koppelregel tool: main.o engine.o?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 5 van 10
Welke compilerinvoer en -uitvoer worden voor de onderstaande regel gebruikt wanneer make sensor.o bouwt? Ga uit van CPPFLAGS = -DTRACE=1 en CFLAGS = -std=c17.
%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 6 van 10
shared.h < main.o < helper.o < app. De regels zijn app: main.o helper.o, main.o: main.c shared.h en helper.o: helper.c. Daarna wordt alleen shared.h gewijzigd. Beide bronbestanden nemen het op. Wat gebeurt er en wat is er mis?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 7 van 10
clean verhinderen dat een niet-gemarkeerde regel clean: zonder afhankelijkheden objectbestanden verwijdert?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 8 van 10
.d-bestanden na de regel voor het uitvoerbare bestand. main.d vermeldt main.o: main.c a.h; worker.d vermeldt worker.o: worker.c b.h a.h. Alle uitvoerbestanden zijn actueel en daarna wordt alleen a.h gewijzigd. Wat zou make -n moeten tonen?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 9 van 10
app: main.o work.o, main.o: main.c shared.h en work.o: work.c. Alle uitvoerbestanden zijn actueel. CFLAGS verandert van -O0 naar -O2 en daarna wordt alleen shared.h gewijzigd. Beide recepten gebruiken CFLAGS. Wat doet gewone make en hoe zorg je dat de nieuwe opties overal worden toegepast?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Vraag 10 van 10
app hangt af van main.o en codec.o. Elk objectbestand heeft een volledige afhankelijkheidsregel voor bron- en headerbestanden; alleen codec.h wordt door beide bronbestanden gedeeld. Waarom kan make -j4 app na een wijziging in dat headerbestand gelijktijdig compileren en toch correct koppelen?Toon antwoordVerberg antwoord
Sla deze les op om dit antwoord te zien.
Kom je niet uit je eigen opgave? Huiswerkhulp
Kernbegrippen
A
Aanroepende functie
main de functie read_sensor aanroept, is main de aanroepende functie die wacht op de teruggegeven meetwaarde.Aanroepstapel
main.Afhankelijkheid
Argument
cube(5 + 1) wordt het argument geëvalueerd tot 6 voordat het blok van cube begint.Argumentconversie
5.8 aan een int-parameter doorgeeft, wordt die waarde geconverteerd naar 5; er wordt niet afgerond naar het dichtstbijzijnde gehele getal.auto
auto duidt in C17 expliciet automatische opslag aan; C23 gebruikt het ook om een type uit een initialisator af te leiden.auto int level; hetzelfde opslaggedrag als een gewone lokale int level;.Automatische opslagduur
Automatische variabele
$@ benoemt sensor.o terwijl het recept voor dat objectbestand wordt uitgevoerd.B
Basisgeval
1 terug, zodat de wachtende vermenigvuldigingen in eerdere aanroepen kunnen worden voltooid.Beveiliging tegen herhaalde inclusie
D
Declaratie
main maakt controle van een aanroep mogelijk, ook als het functieblok later komt.defined-operator
defined-operator van de preprocessor levert één op als de macronaam als operand is gedefinieerd en nul als dat niet zo is.#if defined(DEBUG) om het bestaan te testen, ook als DEBUG de vervangingstekst 0 heeft.Doel
Doorgifte op waarde
n binnen een functie laat de gewone geheeltallige variabele van de aanroepende functie ongewijzigd.E
extern
extern kan een entiteit met koppeling declareren zonder een object te definiëren, mits geen initialisator er een definitie van maakt.extern int errors;, terwijl één bronbestand de geïnitialiseerde objectdefinitie levert.Externe koppeling
F
Functie
Functieachtige macro
Functiedefinitie
double scale(double factor) moet overeenkomen met het prototype dat zowel in het aanroepende bestand als in de implementatie is opgenomen.Functieprototype
double scale(double factor); vertelt de compiler het argument naar double te converteren.G
Gedeelde toestand
Geen koppeling
Gegenereerd afhankelijkhedenbestand
-MMD kan sensor.d schrijven, waardoor een latere wijziging in een headerbestand sensor.o opnieuw laat bouwen.Geldigheidsgebied
Geldigheidsgebied op bestandsniveau
Geldigheidsgebied van een blok
if-blok is gedeclareerd, kan niet bij naam worden gebruikt in een afdrukopdracht nadat dat blok is afgesloten.Geldigheidsgebied van een functieprototype
value noemen terwijl de definitie dezelfde parameter n noemt.Globale variabele
main.H
Headerbestand
I
Interne koppeling
check definiëren zonder conflict bij het koppelen.K
Koppeling
static de koppeling beperkt tot één vertaaleenheid.L
Linker
M
make-variabele
$(CFLAGS) wordt uitgebreid.CC wijzigt, verandert de compilertekst die recepten gebruiken die naar $(CC) verwijzen.Meervoudige definitie
main definiëren, veroorzaken een fout wegens een meervoudige definitie of een dubbel symbool.Module
N
Naamverberging
total in een lus declareert, kan de bedoelde buitenste accumulator ongewijzigd blijven.Neveneffect
Niet-gedefinieerde verwijzing
sensor.o bij het koppelen ontbreekt, kan een niet-gedefinieerde verwijzing overblijven naar een openbare meetfunctie die alleen door sensor.o wordt gedefinieerd.Niet-gespecificeerde evaluatievolgorde van argumenten
O
Objectachtige macro
#define LIMIT 16 vervangt het token LIMIT, maar laat "LIMIT" ongemoeid.Objectbestand
sensor.c kan de bouw sensor.o vervangen en een ongewijzigd main.o hergebruiken.Omzetting naar een stringliteral
# vóór een macroparameter in vervangingstekst om de opgegeven argumenttokens om te zetten in een stringliteral.3 + 4 omzetten in een stringliteral om de expressie naast de berekende waarde te tonen.Opslagduur
P
Parameter
int cube(int n) ontvangt de parameter n voor elke aanroep een afzonderlijke invoerwaarde.Patroonregel
% om een gemeenschappelijke bestandsnaamstam te herkennen, zodat één afhankelijkheidspatroon en recept veel doelen kunnen bouwen.%.o: %.c koppelt sensor.o aan sensor.c met sensor als gemeenschappelijke stam.Preprocessor
gcc -E sample.c uit om de uitgebreide code te bekijken voordat de compiler C-expressies controleert.Preprocessorrichtlijn
# en stuurt de omzetting van brontekst.#include "sensor.h" voegt de tekst van het headerbestand in tijdens de huidige preprocessing.Proefuitvoering
make -n drukt gewone bouwrecepten af zonder ze uit te voeren en toont zo de geplande compiler- en linkeropdrachten.R
Recept
Recursie
Recursief geval
Regel
main.o vermeldt zowel main.c als de projectheaderbestanden die tijdens de compilatie worden opgenomen.register
register markeert een lokale variabele met een optimalisatieaanwijzing die compilers mogen negeren en verbiedt het opvragen van het adres.register aan een lusteller toevoegt, dwingt dat een moderne compiler niet om een processorregister te gebruiken.Retourwaarde
double result zonder die af te drukken.S
Samenvoegen van tokens
## in een macrovervangingslijst gebruikt om aangrenzende tokens tot één preprocessingtoken te combineren.temp en 2 samenvoegt, kan de identifier temp2 in de uitgebreide broncode vormen.Schijndoel
.PHONY gedeclareerde actie, zodat een bestaand bestand met die naam het recept niet kan onderdrukken.clean als schijndoel te declareren blijft het opschonen werken als er een bestand met de naam clean in de map verschijnt.Stackframe
factorial(4) hebben vier faculteitsaanroepen afzonderlijke actieve stackframes.Standaarddoel
all of het uiteindelijke uitvoerbare bestand vooraan te zetten, voorkom je dat een gewone uitvoering van make na één objectbestand stopt.Statische lokale variabele
static is gedeclareerd en één opgeslagen waarde behoudt tussen herhaalde aanroepen.Statische opslagduur
V
Verborgen afhankelijkheid
Vertaaleenheid
void
void geeft als retourtype aan dat er geen waarde wordt teruggegeven, of, als het alleen in een parameterlijst staat, dat er geen parameters zijn.void-functie voor afdrukken voert de uitvoertaak uit, maar kan niet de rechterkant van een toewijzing leveren.Voorgedefinieerde macro
__FILE__ en __LINE__ kan aangeven waar de melding vandaan komt.Voorlopige definitie
extern die een definitie kan opleveren.int count; in een gedeeld headerbestand zetten is geen overdraagbare vervanging voor een extern-declaratie.Voorwaardelijke compilatie
Z
Zelfstandig bruikbaar headerbestand
size_t declareert, neemt <stddef.h> vóór die declaratie op.#
#error
#error-richtlijn vereist een diagnostische melding wanneer die tijdens preprocessing wordt bereikt, zodat een niet-ondersteunde bouw kan worden afgewezen.#error selecteren wanneer de gekozen C-standaard ouder is dan het programma vereist.Over deze les
Door StudeliFout melden
Maak je eigen Studiemateriaal
Maak van je aantekeningen, slides en documenten Leermodules, Oefenvragen, Flashcards en 6 andere tools.