Het gebruik van C-strings.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - TH Rijswijk/Academie voor Engineering.

In de programmeertaal C kun je 1 karakter opslaan in een variabele van het type char. Voor het opslaan van een stukje tekst is in veel programmeertalen een speciaal type (meestal string genoemd) gedefinieerd. C heeft echter niet z'n speciaal type. Een stukje tekst kan in C worden opgeslagen in een char array. In het onderstaande programma chararray.c wordt een character array gevuld met de letters van een woord dat vervolgens wordt afgedrukt en omgekeerd wordt afgedrukt.

#include <stdio.h>

int main() {
    char tekst[5];
    int index;
    tekst[0]='H';
    tekst[1]='a';
    tekst[2]='l';
    tekst[3]='l';
    tekst[4]='o';
    for (index=0; index<5; index++) {
        printf("%c", tekst[index]);
    }
    printf("\nWordt omgekeerd:\n");
    for (index=4; index>=0; index--) {
        printf("%c", tekst[index]);
    }
    getchar();
    return 0;
}

C-string.

Het is niet handig om de karakters in een char array 1 voor 1 te "behandelen". De programmeertaal C heeft ook de mogelijkheid om de inhoud van een char array als 1 geheel te "behandelen". Om dit mogelijk te maken moet de tekst in de array afgesloten worden met een zogenaamd nul-karakter '\0'. Een char array waarbij het einde van de tekst aangegeven wordt door een nul-karakter wordt een C-string genoemd.

Een C-string kan bij de definitie meteen geïnitialiseerd worden:

char tekst[6]="Hallo";

Let op! De character array moet nu minimaal 6 characters groot zijn (1 extra voor het afsluitende nul-karakter). Je mag de character array ook groter maken zonder dat het gedrag van het programma veranderd. Het afsluitende nul-karakter geeft immers het einde van de tekst aan.

Als we de array meteen initialiseren kan de compiler de grootte van de array ook zelf bepalen:

char tekst[]="Hallo";

Om een C-string met behulp van printf af te drukken moet de format specifier %s gebruikt worden:

printf("%s", tekst);

Het onderstaande programma cstring.c doet hetzelfde als het eerste voorbeeldprogramma maar behandeld de tekst zoveel mogelijk als 1 geheel.

#include <stdio.h>

int main() {
    char tekst[]="Hallo";
    int index;
    printf("%s", tekst);
    printf("\nWordt omgekeerd:\n");
    for (index=4; index>=0; index--) {
        printf("%c", tekst[index]);
    }
    getchar();
    return 0;
}

Beperkingen van C-strings.

De mogelijkheden van een C-string zijn echter erg beperkt. Een C-string kan niet worden toegekend met behulp van = en niet worden vergeleken met behulp van ==, !=, <, <=, > en >=. Dit komt omdat de naam van een array in C hetzelfde is als een pointer naar het eerste element van de array. Bij het vergelijken met behulp van de vergelijkingsoperatoren wordt dus het beginadres van de array's vergeleken en niet de inhoud van de array's. Deze beperkingen gelden overigens voor alle array's.

#include <stdio.h>
/* Let op! Dit programma laat zien hoe het NIET moet! */

int main() {
    char tekst[6];
    tekst="Hallo";
    return 0;
}

Het bovenstaande programma cstring_assign.c geeft bij het vertalen met gcc (wxDev-C++) de volgende foutmelding:

incompatible types in assignment

De naam van een array is namelijk een constante pointer naar het eerste element van de array en het beginadres van een array kun je niet wijzigen.

Het onderstaande programma cstring_equal.c geeft niet de uitvoer die je mischien zou verwachten.

#include <stdio.h>
/* Let op! Dit programma laat zien hoe het NIET moet! */

int main() {
    char tekst1[6]="Hallo";
    char tekst2[6]="Hallo";
    if (tekst1!=tekst2) {
        printf("DAT IS GEK! %s != %s\n", tekst1, tekst2);
    }
    getchar();
    return 0;
}

De uitvoer van dit programma is:

DAT IS GEK! Hallo != Hallo

Het beginadres van de eerste array is namelijk altijd ongelijk aan het beginadres van de tweede array.

Functies voor het werken met C-strings.

Om de bovengenoemde beperkingen te "omzeilen" zijn een groot aantal standaard C functies beschikbaar waarmee je C-strings kunt manipuleren. Deze functies zijn gedeclareerd in de include file string.h. Een aantal van deze functies zijn beschreven in de onderstaande tabel:

Voorbeeld Beschrijving
char s[]="Hallo";
size_t size=strlen(s);
De functie strlen bepaald de lengte van de C-string. Het afsluitende nul-karakter wordt daarbij niet meegeteld. Het returntype size_t is een integer type groot genoeg om het resultaat te kunnen bevatten. De variabele size wordt in dit voorbeeld dus gelijk aan 5.
char s[]="Hallo";
char t[]="Harry";
int i=strcmp(s,t);
De fuctie strcmp vergelijkt de inhoud van twee C-strings met elkaar. De returnwaarde is 0 als beide C-strings gelijk zijn. De returnwaarde is >0 als de eerste C-string groter is dan de tweede C-string. Groter dan betekent in dit geval dat de eerste string bij het op alfabetische volgorde zetten achter de tweede string komt te staan. De returnwaarde is <0 als de eerste C-string kleiner is dan de tweede C-string. Kleiner dan betekent in dit geval dat de eerste string bij het op alfabetische volgorde zetten voor de tweede string komt te staan. De variabele i wordt in dit voorbeeld dus <0.
char s[]="Hallo";
char t[6];
strcpy(t,s);
De functie strcpy kopieert de inhoud van de als tweede parameter meegegeven C-string naar de als eerste parameter meegegeven character array. Je moet er daarbij goed op letten dat deze character array groot genoeg is. Vergeet niet om het afsluitende nul-karakter mee te tellen! Zie: De gevaren van buffer overflow! De inhoud van de variabele s wordt in dit voorbeeld dus gekopieerd naar de variabele t.
char s[100]="Hallo";
strcat(s, " daar");
printf("%s\n", s);
De functie strcat "plakt" de inhoud van de als tweede parameter meegegeven C-string achter de C-string die is opgeslagen in de als eerste parameter meegegeven character array. Je moet er daarbij goed op letten dat de character array groot genoeg is. Vergeet niet om het afsluitende nul-karakter mee te tellen! Zie: De gevaren van buffer overflow! De inhoud van de variabele s wordt in dit voorbeeld dus de C-string "Hallo daar"

Een volledig overzicht kun je vinden op: http://www-ccs.ucsd.edu/c/string.html. Een nog uitgebreidere omschrijving is te vinden op: http://www.opengroup.org/onlinepubs/009695399/basedefs/string.h.html.

Lezen en schrijven van C-strings.

Je kunt C-strings met behulp van printf naar het scherm en met behulp van fprintf naar een file schrijven. Als format specifier moet %s gebruikt worden. C-strings kunnen met scanf uit het toetsenbord en met fscanf uit een file gelezen worden. Als format specifier moet ook %s gebruikt worden. De functie's scanf en fscanf lezen bij het gebruik van %s één woord in (gescheiden door spatie, TAB of ENTER). Het gebruik van de format specifier %s bij het inlezen van een C-string is erg gevaarlijk! Zie: De gevaren van buffer overflow! Als je zonder gevaar een C-string wilt inlezen in de array

char woord[20];

Dan kan dat met behulp van de functieaanroep:

scanf("%19s", woord);

De format specifier %19s zorgt ervoor dat er nooit meer dan 19 karakters uit het toetsenbordbuffer worden gelezen. De 20ste plaats in de array woord kan dan gebruikt worden voor het afsluitende nul-karakter.

De functies printf en fprintf geven een integer getal terug die het aantal weggeschreven karakters aangeeft. Deze functies geven een nagatieve waarde terug als er een uitvoerfout optreed. Meer informatie kun je vinden op: http://www.cppreference.com/stdio/printf.html en nog meer op: http://www.opengroup.org/onlinepubs/009695399/functions/printf.html. De functies scanf en fscanf geven een integer getal terug die het aantal juist ingelezen variabelen aangeeft. Deze functies geven de waarde EOF terug als geprobeerd wordt om voorbij het einde van de file te lezen of als er een invoerfout optreed. Meer informatie kun je vinden op: http://www.cppreference.com/stdio/scanf.html en nog meer op: http://www.opengroup.org/onlinepubs/009695399/functions/scanf.html.

Gegeven is het volgende programma scanf.c:

#include <stdio.h>

int main() {
    char woord[20];
    printf("Type een aantal strings (sluit af met Ctrl+Z)\n");
    while (scanf("%19s", woord)==1) {
        printf("De string: \"%s\" is ingelezen!\n", woord);
    }
    return 0;
}

Kun je de uitvoer voorspellen als na de onderstaande invoer op de return toets wordt gedrukt?

Klik hier als je niet zeker bent van je antwoord.


De volgende informatie is niet nodig voor PROS2 maar alleen bedoeld voor studenten die meer willen weten.

Je kunt bij het gebruik van scanf en fscanf ook exact opgeven welke karakters zijn toegestaan om in te lezen in een C-string. Zie: Het inlezen van een string met scanf of fscanf inclusief spaties.

Er zijn ook speciale functies voor het lezen en schrijven van C-strings. De functie puts kan gebruikt worden om C-strings naar het scherm te schrijven en fputs om C-strings naar een file te sturen. De functie puts schrijft de als argument meegegeven C-string naar het beeldscherm gevolgd door een '\n' karakter. De functie fputs schrijft de als eerste argument meegegeven C-string naar de als tweede parameter meegegeven FILE*.  De functie fputs voegt in tegenstelling tot puts geen '\n' karakter toe. De functie gets kan gebruikt worden om een C-string van het toetsenbord te lezen maar omdat het maximaal aantal te lezen karakters niet meegegeven kan worden is het gebruik van deze functie erg gevaarlijk. Zie: De gevaren van buffer overflow! De functie fgets kan gebruikt worden om een C-string uit een file te lezen. Deze functie heeft 3 parameters. Als eerste moet de character array waar de C-string wordt opgeslagen worden meegegeven. Als tweede moet het aantal karakters dat in die karakter array kan worden opgeslagen worden meegegeven. Tot slot moet een FILE* worden meegegeven. Het maximaal aantal karakters dat de functie fgets uit de file leest is 1 minder dan het als tweede parameter meegegeven getal. De functie fgets geeft NULL terug als geprobeerd wordt voorbij het einde van de file te lezen of als er een invoerfout optreed. De functie fgets kan ook gebruikt worden om een C-string van het toetsenbord in te lezen door als derde parameter de voorgedefinieerde FILE* stdin mee te geven.

Gegeven is het volgende programma fgets.c:

#include <stdio.h>

int main() {
    char woord[20];
    printf("Type een aantal strings (sluit af met Ctrl+Z)\n");
    while (fgets(woord, 20, stdin)!=NULL) {
        puts(woord);
    }
    return 0;
}

Kun je de uitvoer voorspellen als na de onderstaande invoer op de return toets wordt gedrukt?

Klik hier als je niet zeker bent van je antwoord.

Functies die bedoeld zijn om naar een file te schrijven kunnen we ook gebruiken om naar het beeldscherm te schrijven door de voorgedefinieerde FILE* stdout te gebruiken.

Voorspel de uitvoer (bij ongewijzigde invoer) als in het bovenstaande programma de regel:

    puts(woord);

vervangen wordt door:

    fputs(woord, stdout);

Klik hier als je niet zeker bent van je antwoord.

De gevaren van buffer overflow

Diverse functies zoals strcmp, strcat, gets, fgets, scanf en fscanf leveren als resultaat een C-string. Deze C-string wordt dan opgeslagen in een char array die door een als parameter meegegeven char* wordt aangewezen. Hierbij moet je goed oppassen voor buffer overrun. Dit probleem treedt op als de C-string meer karakters bevat als de char array kan bevatten. Voorbeeld:

#include <stdio.h>
#include <string.h>

int checkpassword() {
   int ok=0;
   char buffer[8];

   printf("Adres van buffer = %p, adres van ok = %p\n\n", buffer, &ok);

   printf("Geef password: ");
   gets(buffer);
   if (strcmp("Geheim!", buffer)==0) {
        ok=1;
   }
   return ok;
}

int main() {
   if (checkpassword()) {
      printf("Toegang tot geheime informatie.\n");
   }
   else {
      printf("Onjuist password!\n");
   }
   fflush(stdin);
   getchar();
   return 0;
}

Je zou denken dat je alleen toegang tot de geheime informatie krijgt als je het password kent1. Als dit programma met DevC++ wordt gecompileerd blijkt dat de variabele ok 12 bytes na de variabele buffer in het geheugen wordt geplaatst. Als we meer dan 8 karakters invoeren worden deze karakters allemaal in het geheugen weggeschreven (buffer overrun). Op deze manier kan ook de variabele ok overschreven worden.

Buffer overrun kun je in dit geval eenvoudig voorkomen door in plaats van de functies gets de functie fgets te gebruiken. In het bovenstaande programma moet de regel:

   gets(buffer);

worden vervangen door:

   gets(buffer, 8, stdin);

De functie zal nu hooguit 7 karakters inlezen (en de achtste plaats in de buffer vullen met het nul karakter).

Het is overigens nog beter om:

   gets(buffer, sizeof buffer, stdin);

te gebruiken. Nu kan de grootte van de buffer zonder verdere consequenties gewijzigd worden.

1 Ik ben er wel vanuit gegaan dat de gebruiker alleen uitvoerrechten heeft voor het programma en geen leesrechten! Anders zou de gebruiker namelijk eenvoudig het geheime password kunnen opsporen door de bufferoverrun2.exe file in notepad te laden. Als de gebruiker ook leesrechten heeft op de executable moeten we het password "encrypted" in de exe file opslaan.

Een ander (soortgelijk) voorbeeld van bufferoverrun kun je hier vinden.