Het gebruik van C-strings.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de Haagse Hogeschool - Academie voor Technology, Innovation & Society Delft.

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(void) {
    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]);
    }
    fflush(stdin);
    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(void) {
    char tekst[] = "Hallo";
    int index;
    printf("%s", tekst);
    printf("\nWordt omgekeerd:\n");
    for (index = 4; index >= 0; index--) {
        printf("%c", tekst[index]);
    }
    fflush(stdin);
    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(void) {
    char tekst[6];
    tekst = "Hallo";
    return 0;
}

Het bovenstaande programma cstring_assign.c geeft bij het vertalen met Microsoft Visual Studio de volgende foutmelding:

error: '=' : left operand must be l-value

De naam van een array is namelijk een (constante) pointer naar het eerste element van de array en dit beginadres van de array kun je niet wijzigen. Dingen die links van een = teken mogen staan worden l-values genoemd (met de l van links) en de naam van een array is dus geen l-value.

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(void) {
    char tekst1[6] = "Hallo";
    char tekst2[6] = "Hallo";
    if (tekst1 != tekst2) {
        printf("DAT IS GEK! %s != %s\n", tekst1, tekst2);
    }
    fflush(stdin);
    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 s[] = "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 overrun! De inhoud van de variabele s wordt in dit voorbeeld dus gekopieerd naar de variabele t. Na afloop bevat t de C-string "Hallo" die wordt afgesloten met een nul-karakter.
char s[] = "Hallo";
char t[4];
strncpy(t, s, sizeof t);
De functie strncpy is een "veilige" versie van strcpy. Bij deze functie wordt een derde argument meegegeven die het aantal characters aangeeft dat in de als eerste parameter meegegeven character array kan worden weggeschreven. Deze functie beschermt je dus wel tegen buffer overrun maar het probleem van deze functie is dat de de als eerste parameter meegegeven character array niet met een nul-karakter wordt afgesloten als strlen(s) >= sizeof t. Zie: http://blog.liw.fi/posts/strncpy/.
char s[] = "Hallo";
char t[4];
strcpy_s(t, sizeof t, s);
De functie strcpy_s is een veilige (secure) versie van strcpy. Deze functie is opgenomen in de C11 standaard. De functie veroorzaakt een run-time assert als de als laatste parameter meegegeven C-string meer karakters bevat dan het als tweede parameter meegegeven aantal.
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 overrun! De inhoud van de variabele s wordt in dit voorbeeld dus de C-string "Hallo daar".
char s[10] = "Hallo";
strcat_s(s, sizeof s, " daar");
printf("%s\n", s);
De functie strcat_s is een veilige (secure) versie van strcat. Deze functie is opgenomen in de C11 standaard. De functie veroorzaakt een run-time assert als de als de resulterende C-string meer karakters bevat dan het als tweede parameter meegegeven aantal. In het gegeven voorbeeld zal dus een run-time assert optreden omdat de resultaat C-string "Hallo daar" niet past in de character array s (er is geen ruimte voor het afsluitende nul-karakter). Zie secure string functies in C11.

Een volledig overzicht kun je vinden op: http://en.cppreference.com/w/c/string/byte. Een nog uitgebreidere omschrijving is te vinden op: http://pubs.opengroup.org/onlinepubs/9699919799/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 overrun! 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://en.cppreference.com/w/c/io/fprintf en nog meer op: http://www.opengroup.org/onlinepubs/9699919799/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://en.cppreference.com/w/c/io/fscanf en nog meer op: http://www.opengroup.org/onlinepubs/9699919799/functions/scanf.html.

Gegeven is het volgende programma scanf.c :

#include <stdio.h>

int main(void) {
    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 MICPRG 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 overrun! 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(void) {
    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 overrun.

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(void) {
   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(void) {
   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 Microsoft Visual Studio wordt gecompileerd blijkt dat de variabele ok 16 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:

   fgets(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:

   fgets(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 buffer overrun kun je hier vinden.

Secure string functies in C11.

In de C11 standaard zijn veilige versies van verschillende string functies opgenomen. Deze functies zijn bedoeld om buffer overrun te voorkomen. In Microsoft Visual Studio kun je gebruik maken van deze functies (zie: http://msdn.microsoft.com/en-us/library/8ef0s5kh%28v=vs.100%29.aspx).

Voorbeeld strcat_s.c :

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

int main(void) {
    char s[10] = "Hallo";
    strcat_s(s, sizeof s, " daar");
    printf("%s\n", s);
    getchar();
    return 0;
}

Als dit programma uitgevoerd wordt in de debugger van Microsoft Visual Studio dan verschijnt de volgende run-time assert:

Als dit programma gecompileerd wordt in Release mode (in tegenstelling tot Debug mode) en uitgevoerd wordt buiten de debugger dan wordt het programma afgesloten met de volgende foutmelding:

Dit komt omdat de resultaat C-string "Hallo daar" niet past in de character array s (er is geen ruimte voor het afsluitende nul-karakter). Om dit probleem op te lossen moet de regel:

    char s[10] = "Hallo";

vervangen worden door:

    char s[11] = "Hallo";

Het gebruik van de functie strcat_s zorgt er dus voor dat het programma wordt afgesloten zodra er een buffer overrun optreed.

Als in het bovenstaande programma gebruik gemaakt wordt van strcat in plaats van strcat_s dan lijkt het programma correct te werken terwijl er sprake is van een buffer overrun Zie: De gevaren van buffer overrun! Als het programma in de Release mode gecompileerd en uitgevoerd wordt dan merk je helemaal niets van deze buffer overrun. Als het programma in Debug mode gecompileerrd en uitgevoerd wordt in Visual Studio dan verschijnt bij het afsluiten van het programma nog wel de foutmelding:

Maar dat komt als mosterd naar de maaltijd omdat het programma gewoon verder gaat nadat de buffer overrun is opgetreden.