Functies en programmastructuur in C

Leer C-programma’s te organiseren zoals dat in echte projecten gebeurt. Schrijf functies met parameters, retourwaarden en prototypes, ontdek waarom C argumenten op waarde doorgeeft en volg functieaanroepen en recursie. Bepaal waar variabelen zichtbaar zijn en hoelang ze bestaan met geldigheidsgebieden en opslagklassen, waaronder 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

  1. 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 teruggeven
      Schrijf functiedefinities met een retourtype, parameters en een functieblok, gebruik return en void correct en roep functies aan binnen expressies.
    • Declareer functies met functieprototypen
      Declareer 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 terug
      Leg 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 recursie
      Volg hoe elke aanroep eigen parameters en lokale variabelen krijgt en terugkeert naar de aanroepende functie, en schrijf en volg recursieve functies met een basisgeval.
  2. 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 bestaat
      Maak 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 variabelen
      Definieer 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.
  3. 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’s
      Leg 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 voorwaardelijk
      Gebruik #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.
  4. 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 bronbestanden
      Beschrijf de stappen van bronbestand tot uitvoerbaar bestand, compileer bestanden afzonderlijk tot objectbestanden, koppel ze en onderscheid compilatiefouten van linkerfouten.
    • Schrijf headerbestanden
      Zet 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 extern
      Gebruik 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.
  5. 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 makefile
      Schrijf 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 patroonregels
      Gebruik variabelen zoals CC en CFLAGS, de automatische variabelen $@, $< en $^, patroonregels, ingebouwde regels en schijndoelen om een makefile kort en correct te houden.

Functies en programmastructuur in C: uitleg

5 hoofdstukken

1

Schrijf, 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.
  • return beëindigt de aanroep; afdrukken levert het resultaat niet terug.
  • Gebruik void voor 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
DeclaratieBetekenis in C17Betekenis in C23
int read_sensor(void);Geen parametersGeen parameters
int read_sensor();Parameterinformatie niet gespecificeerdGeen 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 functieEffect 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 aanroepBewerking 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.
2

Bepaal 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
SpecificatieBetekenis in deze C17-code
autoExpliciete automatische opslag voor een blokvariabele; meestal weggelaten
registerEen aanwijzing voor snelle toegang; het adres opvragen is verboden
static binnen een functieBlijvende opslag met een lokale naam
static op bestandsniveauInterne koppeling, waardoor de naam binnen de vertaaleenheid blijft
externEen 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 static bepaalt 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.
3

Gebruik 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
NaamInformatie
__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

  • #ifdef test het bestaan; #if test een waarde.
  • Niet-gedefinieerde identifiers in #if worden als nul geëvalueerd.
  • -DNAME=value levert 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.
4

Verdeel 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.

StapResultaatGCC-optie die daar stopt
PreprocessingUitgebreide C-tekst-E
CompilerenAssemblytekst-S
AssemblerenObjectbestand-c
KoppelenUitvoerbaar bestandGewone 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.
  • -c levert 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 headerbestandBewaar in een bronbestand
FunctieprototypesFunctieblokken met externe koppeling
extern-variabeledeclaratiesDefinities van gedeelde objecten
Benodigde macro’s en typenPrivé-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
KoppelingBetekenis
ExternDeclaraties kunnen in verschillende vertaaleenheden naar één entiteit verwijzen
InternDeclaraties verwijzen binnen één vertaaleenheid naar een entiteit
GeenEen 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.
  • static op 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 static en de koppeling door static op bestandsniveau hebben verschillende rollen.
5

Bouw 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; hergebruik main.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
VariabeleWaarde 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

5 Walkthroughs

1

Maak en volg een functieaanroep

19 stappen
Schrijf een functie, declareer die, bewaar het resultaat en volg een recursieve berekening.
  1. 1
    IntroductieBegin 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.

  2. 2
    VoorbeeldGeef 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;
    }
    

    int vóór de naam beschrijft het resultaat. int reading declareert een parameter en het functieblok berekent de retourwaarde. Definieer deze functie buiten alle andere functieblokken; standaard-C ondersteunt geen geneste definities.

  3. 3
    VoorbeeldGebruik een aanroep als expressie

    Bij int result = corrected(9); initialiseert het argument 9 de parameter reading. De retourexpressie levert 13 op en de toewijzing van de aanroepende functie slaat die waarde op in result.

    De aanroep kan ook binnen een andere expressie staan: 2 * corrected(9) geeft 26. Als je alleen corrected(9); schrijft, wordt de aanroep nog steeds uitgevoerd, maar wordt de retourwaarde weggegooid.

  4. 4
    TipKies 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 void is gedeclareerd, kan uitvoer verzorgen maar levert geen waarde voor een toewijzing. Binnen die functie verlaat return; de functie zonder waarde. Zorg er bij een functie die een waarde teruggeeft voor dat elk pad dat een resultaat oplevert een passende return bereikt; een getal tonen levert het niet aan de aanroepende functie.

  5. 5
    UitdagingControleer 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.0 terug: geheeltallige optelling geeft 13, geheeltallige deling geeft 6 en conversie naar het retourtype geeft 6.0. Gebruik 2.0 om de deling met drijvendekommagetallen uit te voeren als je 6.5 nodig hebt. Het retourtype kan een al weggegooide breuk niet herstellen.

  6. 6
    UitlegLicht 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.

  7. 7
    KernpuntGeef een lege invoerlijst op

    Gebruik int read_sensor(void); voor geen parameters, zowel in C17 als in C23. In C17 laat int read_sensor(); de parameterinformatie ongespecificeerd; in C23 betekent die schrijfwijze dat er geen parameters zijn. Selecteer C17 expliciet met -std=c17.

  8. 8
    UitdagingVolg een toegestane conversie

    De compiler heeft int corrected(int reading); gezien. Wat geeft corrected(6.8) terug en moet de aanroep worden afgewezen?

    ---reveal---

    De aanroep kan worden gecompileerd. Het prototype zorgt ervoor dat 6.8 wordt geconverteerd naar het gehele getal 6; daarna telt de functie er 4 bij op en geeft 10 terug. 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.

  9. 9
    VoorbeeldTeken 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 bevatten sample en de parameter elk 9 in afzonderlijke objecten. Het functieblok verandert de parameter in 13. Wanneer de aanroep terugkeert, bevat sample nog steeds 9: er is geen waarde aan toegewezen.

  10. 10
    VerbandLaat 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 werkt sample bij.

  11. 11
    UitdagingZoek de aanroep waarvan het resultaat wordt weggegooid

    Beginnend met int sample = 7; voert de aanroepende functie uit:

    corrected(sample);
    sample = corrected(sample);
    

    Wat is sample daarna?

    ---reveal---

    De eerste aanroep berekent 11 en gooit die waarde weg, zodat sample op 7 blijft. De tweede ontvangt ook 7, geeft 11 terug en deze keer slaat de aanroepende functie de waarde op. De eindwaarde is 11, niet 15; een aanroep werkt de argumentvariabele niet bij alleen doordat de functie een gewijzigde waarde teruggeeft.

  12. 12
    UitlegVoeg 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.

  13. 13
    VoorbeeldSchrijf de aanroepen op terwijl je afdaalt

    Volg total_to(3). De eerste aanroep bevat n == 3 en wacht om 3 op te tellen bij total_to(2). Die aanroep heeft een eigen n == 2 en wacht op total_to(1).

    De aanroep met n == 1 wacht op total_to(0), die onmiddellijk 0 teruggeeft. Op dat diepste punt zijn vier aanroepen actief. Vervang de eerdere waarden van n niet door nul; ze horen bij verschillende aanroepen.

  14. 14
    VoorbeeldVul de resultaten in terwijl je terugkeert

    Zodra total_to(0) de waarde 0 teruggeeft, kan het wachtende werk worden voltooid:

    • De aanroep met n == 1 geeft 1 + 0, oftewel 1, terug.
    • De aanroep met n == 2 geeft 2 + 1, oftewel 3, terug.
    • De aanroep met n == 3 geeft 3 + 3, oftewel 6, 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.

  15. 15
    UitdagingControleer of een basisgeval bereikbaar is

    Stel dat de basistest in total_to wordt gewijzigd in n == 0 en de recursieve stap nog steeds n - 1 gebruikt. Wat gaat er mis als het eerste argument -2 is?

    ---reveal---

    De invoerwaarden worden -2, -3, -4 en gaan steeds verder van nul af. De stoptest bestaat, maar is langs dat pad onbereikbaar. Doorgaande recursie kan de stapelruimte uitputten. De versie met n <= 0 stopt bij deze invoer onmiddellijk.

  16. 16
    TipControleer ook de grootte van het antwoord

    Een recursieve berekening die stopt, kan nog steeds de grenzen van het retourtype overschrijden. Bij een 32-bits int past factorial(12), terwijl de wiskundige waarde van factorial(13) het maximum 2147483647 overschrijdt.

    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.

  17. 17
    TipScheid 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 wijzigt i zonder de vereiste volgorde. Doorgifte op waarde maakt een onveilige argumentexpressie niet veilig.

  18. 18
    UitdagingCombineer declaratie, conversie en aanroepen

    Als total_to vóór gebruik is gedeclareerd, schrijft een aanroepende functie int x = 2; total_to(x); x = total_to(3.9);. Wat is de uiteindelijke waarde van x?

    ---reveal---

    Het eerste resultaat wordt weggegooid, dus x blijft 2. Het tweede argument wordt geconverteerd naar 3; het recursieve uitvoeringsspoor geeft 6 terug en de toewijzing slaat die waarde op in x. De declaratie bepaalt de conversie, recursie berekent de waarde en de aanroepende functie beslist of die wordt bewaard.

  19. 19
    SamenvattingGebruik 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.

2

Volg namen en bewaarde waarden

13 stappen
Volg een blok, een statische teller en een gedeeld totaal zonder de objecten met elkaar te verwarren.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBegin met een kleine taak
  2. 2
    VoorbeeldLees de dichtstbijzijnde zichtbare declaratie
  3. 3
    UitlegHoud het geldigheidsgebied aan waarin de functie is geschreven
  4. 4
    UitdagingHerstel een waarde die na een blok nodig is
  5. 5
    VoorbeeldZie hoe een automatische teller opnieuw begint
  6. 6
    VoorbeeldBewaar de teller met static
  7. 7
    UitdagingOnderscheid initialisatie van toewijzing
  8. 8
    TipLees het sleutelwoord op zijn plaats
  9. 9
    VoorbeeldVolg een wijziging van een globale variabele
  10. 10
    UitdagingZoek de verbergende lokale variabele
  11. 11
    VerbandMaak een berekening gemakkelijker te testen
  12. 12
    UitdagingCombineer geldigheidsgebied en bewaarde toestand
  13. 13
    SamenvattingNoteer namen en levensduur afzonderlijk
3

Bekijk code vóór het compileren

13 stappen
Breid expressies uit, selecteer een debugtak en doorloop een beveiliging van een headerbestand.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBegin met een kleine taak
  2. 2
    UitlegBekijk wat opname daadwerkelijk levert
  3. 3
    VoorbeeldVervang tokens voordat je operatorregels toepast
  4. 4
    VoorbeeldBescherm de binnenste en buitenste groepering
  5. 5
    UitdagingZoek wat haakjes niet kunnen herstellen
  6. 6
    TipHoud macroregels gescheiden van regels voor geldigheidsgebieden
  7. 7
    VoorbeeldTest een instelling op bestaan en waarde
  8. 8
    UitdagingSelecteer een tak met een compileroptie
  9. 9
    VoorbeeldDoorloop de eerste opname van een headerbestand
  10. 10
    KernpuntBegin met een nieuwe beveiligingstoestand in een andere eenheid
  11. 11
    VerbandZoek een diagnostische melding in de broncode
  12. 12
    UitdagingCombineer expansie en selectie van een tak
  13. 13
    SamenvattingBekijk de omzetting in de juiste volgorde
4

Bouw één programma uit twee bronbestanden

14 stappen
Maak een headerbestand en implementatie, koppel hun objectbestanden en bescherm privétoestand.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBegin met een kleine taak
  2. 2
    UitlegVolg één vertaaleenheid naar een objectbestand
  3. 3
    VoorbeeldSchrijf de gedeelde declaratie
  4. 4
    VoorbeeldZet de implementatie in een eigen bronbestand
  5. 5
    UitdagingPlaats één gedeelde variabele correct
  6. 6
    VoorbeeldCompileer afzonderlijk en koppel daarna
  7. 7
    UitdagingHerstel een niet-gedefinieerde verwijzing
  8. 8
    TipKopieer de implementatie niet naar het aanroepende bestand
  9. 9
    VoorbeeldHoud een teller binnen de module
  10. 10
    UitlegOnderscheid drie eigenschappen van de teller
  11. 11
    UitdagingLeg twee onafhankelijke tellers uit
  12. 12
    TipMaak een headerbestand zelfstandig bruikbaar
  13. 13
    UitdagingHerstel afscherming en interface samen
  14. 14
    SamenvattingKoppel elke behoefte aan de juiste bouwstap
5

Bouw alleen wat is veranderd

15 stappen
Schrijf afhankelijkheden, volg wat opnieuw wordt gebouwd en verkort een makefile zonder de betekenis te verliezen.

Sla deze les op om elke Walkthrough stap voor stap te volgen.

  1. 1
    IntroductieBegin met een kleine taak
  2. 2
    VoorbeeldLeg de invoer voor het uitvoerbare bestand en de objectbestanden vast
  3. 3
    VoorbeeldGeef een objectbestand een recept
  4. 4
    VoorbeeldVoer de eerste bouw uit en bouw na een bronwijziging opnieuw
  5. 5
    UitdagingVolg een wijziging in een gedeeld headerbestand
  6. 6
    TipBekijk het plan voordat je het uitvoert
  7. 7
    UitlegVervang herhaalde instellingen door variabelen
  8. 8
    VoorbeeldBreid het koppelrecept uit
  9. 9
    VoorbeeldGebruik één compilatiepatroon voor beide objectbestanden
  10. 10
    UitdagingKies een bronbestand zonder het headerbestand te compileren
  11. 11
    TipMaak van opschonen een actie
  12. 12
    VerbandLaat de compiler afhankelijkheden van headerbestanden vastleggen
  13. 13
    UitdagingZorg dat parallel opnieuw bouwen correct verloopt
  14. 14
    UitdagingPas gewijzigde opties toe op het hele programma
  15. 15
    SamenvattingControleer de graaf voordat je de uitvoer vertrouwt

Functies en programmastructuur in C: oefenvragen

50 Oefenvragen in 5 sets

Set 1: Volg waarden door functieaanroepen10 Oefenvragen

Vraag 1 van 10

Wat is de parameter in int triple(int value) { return value * 3; }?
  1. triple
  2. int
  3. value
  4. value * 3
Toon antwoordVerberg antwoord
Antwoord: C
De parameter is 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

Wat kan een aanroepende functie doen met void report(int code)?
  1. report(4) opslaan in een int.
  2. report(4); aanroepen als instructie.
  3. report(4) optellen bij een geheel getal.
  4. report(4) teruggeven uit een int-functie.
Toon antwoordVerberg antwoord
Antwoord: B
Een 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

Welke waarde geeft double portion(int total) { return total / 4; } terug voor portion(11)?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Toon antwoordVerberg antwoord
Antwoord: A
Beide operanden van 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

Een definitie van int capacity(void) staat onder main. Welke declaratie boven main beschrijft in C17 een aanroep zonder invoer volledig?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Toon antwoordVerberg antwoord
Antwoord: D
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

Als int fifth(int n) { return n / 5; } vóór gebruik is gedeclareerd, wat is dan fifth(-14.8)?
  1. -3
  2. -2
  3. -2.96
  4. -14
Toon antwoordVerberg antwoord
Antwoord: B
De conversie naar het parametertype kapt eerst de breuk af naar nul, wat 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

Een headerbestand declareert double stretch(int n);. Welke definitie is compatibel met die declaratie?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
Toon antwoordVerberg antwoord
Antwoord: D
Het retourtype moet 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);
}
  1. 17
  2. 13
  3. 4
  4. 0
Toon antwoordVerberg antwoord
Antwoord: A
De aanroep kopieert 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);
  1. 22
  2. 28
  3. 16
  4. 14
Toon antwoordVerberg antwoord
Antwoord: C
De eerste aanroep berekent 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);
}
  1. 3
  2. 4
  3. 5
  4. 6
Toon antwoordVerberg antwoord
Antwoord: C
De aanroepen dalen af van 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);
}
  1. 7
  2. 8
  3. 6
  4. 5
Toon antwoordVerberg antwoord
Antwoord: A
De parameter van het type int ontvangt 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

Waar kun je de naam i uit for (int i = 0; i < 6; i++) { } gebruiken?
  1. In de hele functie die de lus bevat.
  2. In de rest van het bronbestand.
  3. In elke functie die door de lus wordt aangeroepen.
  4. Binnen die for-instructie.
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);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
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);
  1. 6
  2. 0
  3. 12
  4. 5
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Waarom initialiseert int amount = amount + 2; binnen een blok niet vanuit een buitenste amount?
  1. De buitenste naam verliest de opgeslagen waarde.
  2. Alle initialisatoren lopen vóór het geldigheidsgebied begint.
  3. De nieuwe naam is geldig in de eigen initialisator.
  4. Beide declaraties duiden hetzelfde lokale object aan.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 5 van 10

Hoe vaak wordt static int calls = 0; geïnitialiseerd tijdens één uitvoering van het programma?
  1. Eén keer, vóór het programma start.
  2. Eén keer per aanroep van de functie.
  3. Eén keer op elk recursieniveau.
  4. Eén keer telkens als de waarde wordt gelezen.
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;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
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;
}
  1. 6
  2. 9
  3. 7
  4. 3
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Een berekening int permitted(int n) leest een globale limit. Waarom kan hetzelfde argument bij een latere aanroep een ander resultaat opleveren?
  1. Het doorgeven van een int-argument verandert de bronvariabele.
  2. Elke aanroep zet alle objecten op bestandsniveau op nul.
  3. Een lokale naam in de aanroepende functie vervangt de globale naam.
  4. Een andere bewerking kan limit hebben gewijzigd.
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);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Een functie moet tijdens de uitvoering een bewaarde numerieke variabele initialiseren met read_sensor(). Waarom is static int first = read_sensor(); ongeldig in C17?
  1. Een statische lokale variabele kan na initialisatie niet worden gelezen.
  2. Een statische numerieke initialisator moet constant zijn.
  3. Een functie kan geen waarde aan een lokale variabele teruggeven.
  4. Een statische lokale variabele wordt bij elke aanroep geïnitialiseerd.
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

Welke tekst vervangt #define LIMIT 18 in latere broncode?
  1. De tekens LIMIT binnen "LIMIT".
  2. Het zelfstandige token LIMIT.
  3. Het achtervoegsel van de naam LOCAL_LIMIT.
  4. De volledige instructie met LIMIT.
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
  1. 16
  2. 20
  3. 19
  4. 15
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

Waarom maakt #define LIMIT 12; de instructie int count = LIMIT * 2; ongeldig?
  1. De macro kan na = geen token vervangen.
  2. Een numerieke macro heeft een gedeclareerd retourtype nodig.
  3. De puntkomma wordt onderdeel van de expressie.
  4. De macro verandert count in een constantenaam.
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);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
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))
  1. Het argument wordt één keer geëvalueerd en die waarde tot de derde macht verheven.
  2. De waarde wordt driemaal verhoogd in een vastgelegde volgorde van links naar rechts.
  3. De aanroep wordt afgewezen omdat een macro geen expressies kan ontvangen.
  4. Het gedrag is ongedefinieerd door herhaalde niet-geordende wijzigingen.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Wat doet #ifdef TRACE bij #define TRACE 0?
  1. Slaat de tak over omdat TRACE tot nul wordt uitgebreid.
  2. Behoudt de tak omdat TRACE is gedefinieerd.
  3. Wijst de definitie af omdat nul geen naam is.
  4. Leest tijdens de uitvoering een variabele met de naam TRACE.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 7 van 10

Waarom kan #if VERBOSE ongemerkt een tak overslaan als de bouw alleen VERB0SE definieert?
  1. De niet-gedefinieerde identifier VERBOSE telt als nul.
  2. De twee namen delen één definitie omdat ze op elkaar lijken.
  3. Elke niet-gedefinieerde preprocessornaam is een fatale compilatiefout.
  4. De richtlijn wacht op een waarde van VERBOSE tijdens de uitvoering.
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
  1. 6
  2. 7
  3. 8
  4. 2
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));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Een beveiligd headerbestand bevat 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?
  1. Het tweede bronbestand hergebruikt de beveiligingstoestand van het eerste.
  2. De beveiliging zet beide definities om in gedeelde declaraties.
  3. Elke vertaaleenheid neemt een eigen externe definitie op.
  4. De linker verwijdert beveiligingsnamen vóór de controle op duplicaten.
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

Wat levert gcc -std=c17 -c driver.c normaal gesproken op een Unix-achtig systeem op?
  1. Een objectbestand met de naam driver.o.
  2. Een uitvoerbaar bestand met de naam driver.
  3. Assemblytekst met de naam driver.s.
  4. Een headerbestand met de naam driver.h.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 2 van 10

Een aanroepend bestand neemt een correct prototype voor reading op, maar als alleen het objectbestand daarvan wordt gekoppeld, volgt een niet-gedefinieerde verwijzing naar reading. Wat moet worden toegevoegd?
  1. Een extra compatibel prototype in het aanroepende bestand.
  2. Een beveiliging rond de bestaande functiedeclaratie.
  3. Het objectbestand dat reading definieert bij de koppelinputs.
  4. Hetzelfde headerbestand als afzonderlijk gecompileerde invoer.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

Een headerbestand declareert 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?
  1. Voeg het bronbestand met de definitie toe aan een latere koppelopdracht.
  2. Zet een tweede prototype alleen in het aanroepende bronbestand.
  3. Voeg alleen een beveiliging tegen herhaalde inclusie rond de aanroepende code toe.
  4. Neem hetzelfde headerbestand op in het bronbestand met de definitie.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Welke koppeling heeft static int cached = 3; als die buiten elke functie wordt gedeclareerd?
  1. Externe koppeling.
  2. Interne koppeling.
  3. Geen koppeling.
  4. Automatische koppeling.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 5 van 10

Een beveiligd headerbestand bevat 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?
  1. 2, omdat beide opnamen dezelfde gedeelde teller aanduiden.
  2. 1, omdat de beveiliging maar één succesvolle aanroep toestaat.
  3. 0, omdat de twee bronbestanden onafhankelijke tellers hebben.
  4. Een linkerfout, omdat beide statische tellers extern zijn.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Wat hoort in een headerbestand om meerdere bronbestanden één int failures te laten gebruiken die in één bronbestand is gedefinieerd?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
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?
  1. Neem calc.h op in main.c en behoud beide bronbestanden in de bouw.
  2. Verwijder calc.c uit de bouw en blijf het opnemen in main.c.
  3. Behoud beide bronbestanden en voeg nog een beveiliging toe aan calc.h.
  4. Neem main.c op in calc.c zodat beide definities overeenkomen.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Eén bronbestand definieert 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?
  1. Verwijder static van de variabele en behoud elk gebruik van quota.
  2. Voeg static int quota; toe aan het headerbestand en blijf rechtstreeks lezen.
  3. Herhaal extern int quota; in het aanroepende bestand vóór elk gebruik.
  4. Verwijder de externe variabeledeclaratie en lees de waarde met quota_value().
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 9 van 10

Een headerbestand declareert size_t packet_count(void);. Wat maakt het headerbestand zelfstandig bruikbaar?
  1. Vraag elk aanroepend bestand om <stddef.h> na het headerbestand op te nemen.
  2. Verplaats de declaratie met een gekopieerde typenaam naar elk bronbestand.
  3. Neem het implementatiebestand vóór het headerbestand op in aanroepende bestanden.
  4. Neem <stddef.h> vóór die declaratie op in het headerbestand.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 10 van 10

Twee bronbestanden nemen elk een headerbestand met 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?
  1. Gebruik extern int jobs; in het headerbestand en één definitie in een bronbestand.
  2. Gebruik static int jobs = 1; in het headerbestand en geen definitie in een bronbestand.
  3. Behoud het headerbestand en beveilig het met een nieuwe macronaam.
  4. Verwijder het headerbestand en initialiseer jobs in beide bronbestanden.
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

Waardoor moet een bestaand gewoon make-doel opnieuw worden gebouwd nadat de afhankelijkheden zijn bijgewerkt?
  1. Het bronbestand heeft meer regels dan voorheen.
  2. Een afhankelijkheid heeft een recentere wijzigingstijd.
  3. Het recept bevat meer dan één opdracht.
  4. Dezelfde opdracht is op een eerdere dag uitgevoerd.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 2 van 10

Het uitvoerbare bestand 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?
  1. Compileer main.c en koppel daarna viewer.
  2. Compileer beide bronbestanden en koppel daarna viewer.
  3. Koppel viewer zonder een bronbestand te compileren.
  4. Compileer screen.c en koppel daarna viewer.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 3 van 10

Een bronbestand neemt 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?
  1. Make heeft geen gedeclareerd pad van het headerbestand naar het objectbestand.
  2. Headerbestanden worden altijd pas tijdens de uitvoering verwerkt.
  3. De linker herstelt automatisch de vorige inhoud van het headerbestand.
  4. Opname in een bronbestand verhindert dat het objectbestand van headerbestanden afhangt.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 4 van 10

Tot wat worden $@ en $^ uitgebreid bij de koppelregel tool: main.o engine.o?
  1. main.o en engine.o
  2. tool en main.o
  3. tool en main.o engine.o
  4. main.o engine.o en tool
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 $@
  1. Bron sensor.c, uitvoer sensor.o, met -DTRACE=1 -std=c17.
  2. Bronnen sensor.c shared.h, uitvoer sensor.o, met -DTRACE=1 -std=c17.
  3. Bron sensor.o, uitvoer sensor.c, met -DTRACE=1 -std=c17.
  4. Bron sensor.c, uitvoer shared.h, met -DTRACE=1 -std=c17.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 6 van 10

Een recente bouw heeft de tijdstempelvolgorde 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?
  1. Beide objectbestanden worden vóór het koppelen opnieuw gebouwd; de afhankelijkheidsgraaf is volledig.
  2. Alleen helper.o wordt vóór het koppelen opnieuw gebouwd; main.o mist een afhankelijkheid.
  3. Geen van beide objectbestanden wordt opnieuw gebouwd; headerbestanden kunnen geen objectrecepten activeren.
  4. Alleen main.o wordt vóór het koppelen opnieuw gebouwd; helper.o mist een afhankelijkheid.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 7 van 10

Waarom kan een bestand met de naam clean verhinderen dat een niet-gemarkeerde regel clean: zonder afhankelijkheden objectbestanden verwijdert?
  1. Make reserveert elk doel met de naam clean voor ingebouwde compilatie.
  2. De shell weigert opdrachten van doelen waarvoor bestanden bestaan.
  3. Make kan dat bestaande doel als al actueel behandelen.
  4. De compiler verwijdert opruimrecepten wanneer optimalisatie is ingeschakeld.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 8 van 10

Een project laadt gegenereerde .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?
  1. Compileer alleen main.c en koppel daarna; voer geen van die recepten uit.
  2. Compileer beide bronbestanden en koppel daarna; voer geen van die recepten uit.
  3. Compileer beide bronbestanden en koppel daarna; voer alle drie de recepten uit.
  4. Koppel alleen; voer geen recepten uit omdat afhankelijkhedenbestanden al bestaan.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Vraag 9 van 10

Een project heeft 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?
  1. Bouw beide objectbestanden opnieuw en koppel; verder opnieuw bouwen is niet nodig voor de nieuwe opties.
  2. Bouw main.o opnieuw en koppel; dwing een volledige nieuwe bouw af om ook work.o bij te werken.
  3. Koppel alleen opnieuw en behoud beide objectbestanden; wijzig het headerbestand opnieuw om de opties toe te passen.
  4. Bouw work.o opnieuw en koppel; dwing een volledige nieuwe bouw af om ook main.o bij te werken.
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?
  1. Het koppelen mag beginnen zodra één objectbestand klaar is en het andere oude objectbestand hergebruiken.
  2. Elk objectbestand wacht op het andere en daarna begint het koppelen met beide oude bestanden.
  3. Beide objectrecepten mogen tegelijk draaien; het koppelen wacht totdat beide klaar zijn.
  4. Het koppelen draait eerst om app te reserveren en daarna vervangen beide objectrecepten het.
Toon antwoordVerberg antwoord

Sla deze les op om dit antwoord te zien.

Kom je niet uit je eigen opgave? Huiswerkhulp

Kernbegrippen

68 Kernbegrippen, elk met een definitie

A

Aanroepende functie
De aanroepende functie is de functie die een andere functie aanroept en verdergaat nadat die terugkeert.
Wanneer main de functie read_sensor aanroept, is main de aanroepende functie die wacht op de teruggegeven meetwaarde.
Aanroepstapel
Een aanroepstapel is de gebruikelijke implementatiestructuur die actieve functieaanroepen bijhoudt en ze in omgekeerde nestingvolgorde laat terugkeren.
Een omzetfunctie die door een tabelafdrukfunctie wordt aangeroepen, keert terug voordat de tabelafdrukfunctie terugkeert naar main.
Afhankelijkheid
Een afhankelijkheid is een gedeclareerde invoer of vereiste die make controleert en bijwerkt voordat het beslist of een doel opnieuw moet worden gebouwd.
Een gedeeld headerbestand is een afhankelijkheid van elk objectbestand waarvan de vertaaleenheid dat headerbestand opneemt.
Argument
Een argument is een expressie die bij een aanroep wordt opgegeven om de beginwaarde van een parameter te leveren.
In cube(5 + 1) wordt het argument geëvalueerd tot 6 voordat het blok van cube begint.
Argumentconversie
Een argumentconversie zet een opgegeven argumentwaarde om naar het parametertype dat een zichtbaar functieprototype voorschrijft.
Als je 5.8 aan een int-parameter doorgeeft, wordt die waarde geconverteerd naar 5; er wordt niet afgerond naar het dichtstbijzijnde gehele getal.
auto
Het sleutelwoord auto duidt in C17 expliciet automatische opslag aan; C23 gebruikt het ook om een type uit een initialisator af te leiden.
In C17 heeft auto int level; hetzelfde opslaggedrag als een gewone lokale int level;.
Automatische opslagduur
Automatische opslagduur geeft gewone lokale variabelen en parameters opslag die is gekoppeld aan hun blokuitvoering of functieaanroep.
Twee actieve recursieve aanroepen hebben afzonderlijke automatische parameters, ook als hun namen overeenkomen.
Automatische variabele
Een automatische variabele wordt door make ingesteld voor de huidige regel en geeft waarden zoals de namen van het doel of de afhankelijkheden.
De automatische variabele $@ benoemt sensor.o terwijl het recept voor dat objectbestand wordt uitgevoerd.

B

Basisgeval
Een basisgeval geeft direct antwoord op een invoerwaarde zonder nog een recursieve aanroep te doen.
Het basisgeval van de faculteitsfunctie geeft 1 terug, zodat de wachtende vermenigvuldigingen in eerdere aanroepen kunnen worden voltooid.
Beveiliging tegen herhaalde inclusie
Een beveiliging tegen herhaalde inclusie voorkomt herhaalde verwerking van een headerbestand binnen één vertaaleenheid.
Twee headerbestanden kunnen een gemeenschappelijk beveiligd headerbestand opnemen zonder de inhoud in dezelfde eenheid te herhalen.

D

Declaratie
Een declaratie introduceert een naam en de type-informatie die nodig is om het gebruik ervan te controleren.
Een declaratie boven main maakt controle van een aanroep mogelijk, ook als het functieblok later komt.
defined-operator
De defined-operator van de preprocessor levert één op als de macronaam als operand is gedefinieerd en nul als dat niet zo is.
Gebruik #if defined(DEBUG) om het bestaan te testen, ook als DEBUG de vervangingstekst 0 heeft.
Doel
Een doel is het uitvoerbestand of de actie die make moet bijwerken.
Het doel voor het uitvoerbare bestand wordt meestal vooraan geplaatst, zodat make zonder argumenten het hele programma bouwt.
Doorgifte op waarde
Doorgifte op waarde is de C-regel voor het doorgeven van argumenten: elke parameter ontvangt een kopie van de waarde die de argumentexpressie berekent.
Een toewijzing aan parameter n binnen een functie laat de gewone geheeltallige variabele van de aanroepende functie ongewijzigd.

E

extern
Het sleutelwoord extern kan een entiteit met koppeling declareren zonder een object te definiëren, mits geen initialisator er een definitie van maakt.
Een gedeeld headerbestand declareert extern int errors;, terwijl één bronbestand de geïnitialiseerde objectdefinitie levert.
Externe koppeling
Externe koppeling maakt het mogelijk dat declaraties in verschillende vertaaleenheden hetzelfde object of dezelfde functie aanduiden.
Een openbare tellerfunctie heeft externe koppeling, zodat aanroepende functies in een ander bronbestand naar de definitie kunnen worden gekoppeld.

F

Functie
Een functie is een benoemd blok uitvoerbare code dat bij aanroep een taak uitvoert.
Met een functie voor temperatuuromrekening kunnen verschillende delen van een programma één geteste berekening hergebruiken.
Functieachtige macro
Een functieachtige macro ontvangt tokenargumenten en voegt die vóór de gewone C-compilatie in de vervangingstekst in.
Haakjes in een functieachtige macro behouden de groepering, maar voorkomen geen herhaalde evaluatie van argumenten.
Functiedefinitie
Een functiedefinitie levert het uitvoerbare blok van een functie samen met de naam, het retourtype en de parameterlijst.
De definitie van double scale(double factor) moet overeenkomen met het prototype dat zowel in het aanroepende bestand als in de implementatie is opgenomen.
Functieprototype
Een functieprototype is een declaratie die het retourtype en de parametertypen van een functie vastlegt om aanroepen te controleren.
Het prototype double scale(double factor); vertelt de compiler het argument naar double te converteren.

G

Gedeelde toestand
Gedeelde toestand bestaat uit bewaarde gegevens die verschillende bewerkingen kunnen lezen of wijzigen.
Een gedeelde foutenteller maakt een later rapport afhankelijk van de eerdere functies die de teller hebben bijgewerkt.
Geen koppeling
Geen koppeling betekent dat een naam een eigen entiteit aanduidt in plaats van declaraties via interne of externe koppeling te verbinden.
Gewone automatische lokale variabelen en parameters hebben geen koppeling, ook als een andere functie dezelfde namen gebruikt.
Gegenereerd afhankelijkhedenbestand
Een gegenereerd afhankelijkhedenbestand legt door de compiler gevonden afhankelijkheden van bron- en headerbestanden vast, zodat make die bij latere bouwprocessen kan laden.
Compileren met -MMD kan sensor.d schrijven, waardoor een latere wijziging in een headerbestand sensor.o opnieuw laat bouwen.
Geldigheidsgebied
Het geldigheidsgebied van een naam is het gebied in de broncode waar de declaratie zichtbaar is.
Een variabele kan nog bestaan terwijl een andere declaratie in een genest blok de naam verbergt.
Geldigheidsgebied op bestandsniveau
Een geldigheidsgebied op bestandsniveau loopt van een declaratie buiten een functie tot het einde van de vertaaleenheid.
Een hulpfunctie die boven een globale variabele is gedefinieerd, heeft een eerdere declaratie nodig om die variabele te gebruiken.
Geldigheidsgebied van een blok
Het geldigheidsgebied van een blok beperkt een lokale naam tot het gebied vanaf direct na de declarator tot het einde van het blok dat die bevat.
Een variabele die binnen een if-blok is gedeclareerd, kan niet bij naam worden gebruikt in een afdrukopdracht nadat dat blok is afgesloten.
Geldigheidsgebied van een functieprototype
Het geldigheidsgebied van een functieprototype beperkt een parameternaam tot het functieprototype dat die naam declareert.
Een prototype mag de parameter value noemen terwijl de definitie dezelfde parameter n noemt.
Globale variabele
Een globale variabele is een object dat op bestandsniveau is gedeclareerd, statische opslagduur heeft en zichtbaar is onder de declaratie.
Twee functies die ritten toevoegen kunnen één globaal afstandstotaal bijwerken dat beschikbaar blijft voor main.

H

Headerbestand
Een headerbestand deelt declaraties, macro’s of typen door tekstuele opname.
Een headerbestand voor omzettingen declareert beide omzettingsrichtingen zonder de gecompileerde functieblokken te leveren.

I

Interne koppeling
Interne koppeling beperkt de gedeelde identiteit van een naam tot één vertaaleenheid en houdt de entiteit privé voor andere eenheden.
Twee bestanden kunnen elk een statische hulpfunctie met de naam check definiëren zonder conflict bij het koppelen.

K

Koppeling
Koppeling bepaalt of declaraties van een naam op verschillende plaatsen naar hetzelfde object of dezelfde functie verwijzen.
Een globale variabele kan statische opslagduur hebben terwijl static de koppeling beperkt tot één vertaaleenheid.

L

Linker
Een linker combineert objectbestanden en bibliotheken en verbindt externe namen met definities.
De linker heeft het objectbestand nodig dat een omzetfunctie definieert, ook als het aanroepende bestand het prototype heeft opgenomen.

M

make-variabele
Een make-variabele geeft herbruikbare tekst een naam, zoals compilerinstellingen of objectlijsten, die met een verwijzing zoals $(CFLAGS) wordt uitgebreid.
Als je één toewijzing aan CC wijzigt, verandert de compilertekst die recepten gebruiken die naar $(CC) verwijzen.
Meervoudige definitie
Een fout door een meervoudige definitie ontstaat wanneer een koppelopdracht tegenstrijdige definities van dezelfde naam met externe koppeling ontvangt.
Twee gekoppelde bronbestanden die beide main definiëren, veroorzaken een fout wegens een meervoudige definitie of een dubbel symbool.
Module
Een module groepeert samenhangende C-implementatiecode in een bronbestand met een bijbehorend headerbestand.
Een tellermodule kan twee openbare functies beschikbaar maken en de opgeslagen tellerstand privé houden.

N

Naamverberging
Naamverberging treedt op wanneer een binnenste declaratie een zichtbare buitenste naam verbergt, zodat gebruik in het binnenste geldigheidsgebied de nieuwe entiteit selecteert.
Als je een tweede total in een lus declareert, kan de bedoelde buitenste accumulator ongewijzigd blijven.
Neveneffect
Een neveneffect is een wijziging van de programmatoestand of een interactie zoals uitvoer, naast het berekenen van de waarde van een expressie.
Het verhogen van een gedeelde teller is een neveneffect, dus de volgorde van afzonderlijke telleraanroepen kan ertoe doen.
Niet-gedefinieerde verwijzing
Een niet-gedefinieerde verwijzing is een koppelingsfout waarbij een benodigde externe naam geen beschikbare definitie heeft in de gekoppelde invoer.
Als 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
Een niet-gespecificeerde evaluatievolgorde van argumenten betekent dat C geen verplichte volgorde kiest voor de evaluatie van de argumentexpressies van één aanroep.
Twee aanroepen die ticketnummers genereren, kunnen hun nummers in één afdrukopdracht in beide volgorden leveren.

O

Objectachtige macro
Een objectachtige macro koppelt een naam aan vervangingstokens die worden ingevoegd wanneer die naam later verschijnt.
De definitie #define LIMIT 16 vervangt het token LIMIT, maar laat "LIMIT" ongemoeid.
Objectbestand
Een objectbestand bevat gecompileerde machinecode en symboolinformatie die de linker met andere objectbestanden kan combineren.
Na een wijziging van alleen sensor.c kan de bouw sensor.o vervangen en een ongewijzigd main.o hergebruiken.
Omzetting naar een stringliteral
Omzetting naar een stringliteral gebruikt # vóór een macroparameter in vervangingstekst om de opgegeven argumenttokens om te zetten in een stringliteral.
Een macro voor diagnostische meldingen kan 3 + 4 omzetten in een stringliteral om de expressie naast de berekende waarde te tonen.
Opslagduur
Opslagduur beschrijft hoelang de opslag van een object tijdens de uitvoering bestaat, onafhankelijk van waar de naam zichtbaar is.
Een statische lokale variabele combineert een geldigheidsgebied binnen een blok met een opslagduur die de hele uitvoering van het programma beslaat.

P

Parameter
Een parameter is een functievariabele die bij het begin van de aanroep wordt geïnitialiseerd met een gekopieerde argumentwaarde.
In int cube(int n) ontvangt de parameter n voor elke aanroep een afzonderlijke invoerwaarde.
Patroonregel
Een patroonregel gebruikt % om een gemeenschappelijke bestandsnaamstam te herkennen, zodat één afhankelijkheidspatroon en recept veel doelen kunnen bouwen.
De regel %.o: %.c koppelt sensor.o aan sensor.c met sensor als gemeenschappelijke stam.
Preprocessor
De preprocessor zet brontekst om met richtlijnen zoals opname, macrovervanging en voorwaardelijke selectie.
Voer gcc -E sample.c uit om de uitgebreide code te bekijken voordat de compiler C-expressies controleert.
Preprocessorrichtlijn
Een preprocessorrichtlijn begint met # en stuurt de omzetting van brontekst.
De richtlijn #include "sensor.h" voegt de tekst van het headerbestand in tijdens de huidige preprocessing.
Proefuitvoering
Een proefuitvoering met make -n drukt gewone bouwrecepten af zonder ze uit te voeren en toont zo de geplande compiler- en linkeropdrachten.
Bekijk een proefuitvoering na een wijziging in een headerbestand om te zien welke objectbestanden make opnieuw wil bouwen.

R

Recept
Een recept is de reeks shellopdrachten die bij een make-regel hoort, waarbij gewone receptregels met echte tabtekens beginnen.
Het recept voor een objectbestand compileert het bronbestand, terwijl het recept voor het uitvoerbare bestand de benodigde objectbestanden koppelt.
Recursie
Recursie treedt op wanneer een functie zichzelf aanroept om een andere instantie van een probleem op te lossen.
Een functie die cijfers optelt kan het laatste cijfer verwerken en de resterende cijfers recursief optellen.
Recursief geval
Een recursief geval is een tak die een deel van een probleem oplost door dezelfde functie aan te roepen met invoer die dichter bij een basisgeval ligt.
Door één af te trekken van een positieve invoer voor de faculteitsfunctie komt het recursieve geval dichter bij de stoptest.
Regel
Een make-regel benoemt een doel, de afhankelijkheden en eventuele receptopdrachten die nodig zijn om dat doel vanuit die invoer bij te werken.
De regel voor main.o vermeldt zowel main.c als de projectheaderbestanden die tijdens de compilatie worden opgenomen.
register
Het sleutelwoord register markeert een lokale variabele met een optimalisatieaanwijzing die compilers mogen negeren en verbiedt het opvragen van het adres.
Als je register aan een lusteller toevoegt, dwingt dat een moderne compiler niet om een processorregister te gebruiken.
Retourwaarde
Een retourwaarde is het resultaat dat een functie aan de aanroepende functie geeft voor gebruik in een expressie.
De retourwaarde van een omzetting kan worden toegewezen aan double result zonder die af te drukken.

S

Samenvoegen van tokens
Bij het samenvoegen van tokens wordt ## in een macrovervangingslijst gebruikt om aangrenzende tokens tot één preprocessingtoken te combineren.
Een macroaanroep die temp en 2 samenvoegt, kan de identifier temp2 in de uitgebreide broncode vormen.
Schijndoel
Een schijndoel benoemt een met .PHONY gedeclareerde actie, zodat een bestaand bestand met die naam het recept niet kan onderdrukken.
Door clean als schijndoel te declareren blijft het opschonen werken als er een bestand met de naam clean in de map verschijnt.
Stackframe
Een stackframe is een record voor één actieve aanroep dat doorgaans de parameters, automatische lokale variabelen en terugkeerlocatie bevat.
Op het diepste punt van factorial(4) hebben vier faculteitsaanroepen afzonderlijke actieve stackframes.
Standaarddoel
Het standaarddoel is het doel dat make kiest zonder expliciet doelargument, normaal gesproken het eerste gewone doel in het bestand.
Door all of het uiteindelijke uitvoerbare bestand vooraan te zetten, voorkom je dat een gewone uitvoering van make na één objectbestand stopt.
Statische lokale variabele
Een statische lokale variabele is een object met een geldigheidsgebied binnen een blok dat als static is gedeclareerd en één opgeslagen waarde behoudt tussen herhaalde aanroepen.
Een statische lokale teller voor ticketnummers loopt bij elke aanroep op in plaats van opnieuw bij de initialisatiewaarde te beginnen.
Statische opslagduur
Statische opslagduur betekent dat een object gedurende de hele uitvoering van het programma bestaat en één keer wordt geïnitialiseerd voordat het programma start.
Een numeriek object op bestandsniveau zonder expliciete initialisator begint op nul en behoudt latere toegewezen waarden.

V

Verborgen afhankelijkheid
Een verborgen afhankelijkheid is een vereiste invoer of toestand die een functie beïnvloedt zonder in de parameterlijst te verschijnen.
Voor een berekening die een globale grens leest, moet die grens worden ingesteld voordat de functie kan worden getest.
Vertaaleenheid
Een vertaaleenheid is één bronbestand na preprocessing, samen met de opgenomen tekst, dat onafhankelijk wordt vertaald.
Als hetzelfde headerbestand in twee bronbestanden wordt opgenomen, draagt het declaraties bij aan twee afzonderlijke vertaaleenheden.
void
Het sleutelwoord void geeft als retourtype aan dat er geen waarde wordt teruggegeven, of, als het alleen in een parameterlijst staat, dat er geen parameters zijn.
Een void-functie voor afdrukken voert de uitvoertaak uit, maar kan niet de rechterkant van een toewijzing leveren.
Voorgedefinieerde macro
Een voorgedefinieerde macro wordt door de implementatie geleverd en geeft informatie zoals de huidige bronregel of taalversie.
Een diagnostische melding met __FILE__ en __LINE__ kan aangeven waar de melding vandaan komt.
Voorlopige definitie
Een voorlopige definitie is een objectdeclaratie op bestandsniveau zonder initialisator of extern die een definitie kan opleveren.
int count; in een gedeeld headerbestand zetten is geen overdraagbare vervanging voor een extern-declaratie.
Voorwaardelijke compilatie
Voorwaardelijke compilatie behoudt of slaat brontakken over op basis van preprocessortests voordat de resulterende code wordt gecompileerd.
Een debugafdruk die door voorwaardelijke compilatie is verwijderd, ontbreekt in het resulterende uitvoerbare bestand.

Z

Zelfstandig bruikbaar headerbestand
Een zelfstandig bruikbaar headerbestand neemt de headerbestanden op die de eigen declaraties nodig hebben, zodat een speciale opnamevolgorde niet nodig is.
Een headerbestand dat een functie met size_t declareert, neemt <stddef.h> vóór die declaratie op.

#

#error
De #error-richtlijn vereist een diagnostische melding wanneer die tijdens preprocessing wordt bereikt, zodat een niet-ondersteunde bouw kan worden afgewezen.
Een versietest kan #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.

Aan de slag