Week 8

Reťazce, string.h, ctype.h, ternárny operátor

Prezentácia

Zdrojový kód

Poznámky k prednáške

Cézarova šifra

#include <stdio.h>

int main() {
    printf("Enter n: ");
    int shift;
    scanf("%d", &shift);

    char text[] = {'H', 'E', 'L', 'L', 'O'};

    for (int index = 0; index < 5; index++) {
        int letter_order = text[index] - 'A';
        int shifted_letter_order = (letter_order + shift) % 26;
        char encrypted = (char) ('A' + shifted_letter_order);
        printf("%c", encrypted);
    }

    printf("\n");

    return 0;
}

  • Výpočet podľa vzťahov na wikipedii
  • Problémy: Nevieme text rozumne načítať (iba po znakoch), zistiť veľkosť, zadefinovať ho ako jeden reťazec.

Úvod do reťazcov

  • Je to jednorozmerné pole znakov ukončené znakom '\0' (NULL Terminator / ukončovač).
  • Dôsledok: Veľkosť poľa musí byť vždy o jeden znak väčšia, ako je dĺžka reťazca, ktorý v ňom chceme uchovať.
  • Ak sa na terminátor zabudne, tak sa za reťazec považuje celá nasledujúca pamäť až po najbližší znak '\0'.

Aktualizácia caesar-encrypt.c

  • Reťazec je možné inicializovať podobne ako jednorozmerné pole - bez uvedenia veľkosti jednorozmerného poľa znakov:
char str[] = "Hello";
  • Tým sme vytvorili jednorozmerné pole znakov o veľkosti 6 znakov, pričom samotný reťazec bude dlhý 5 znakov.
  • Reťazec je možné priamo inicializovať aj nasledovne:
char str[20] = "Hello";
  • Tým sme vytvorili jednorozmerné pole o veľkosti 20 znakov, pričom samotný reťazec bude dlhý 5 znakov.
  • Reťazec je možné inicializovať aj takto:
char* string = "Hello";
  • Tým sme vytvorili jednorozmerné pole znakov o veľkosti 6 znakov, pričom samotný reťazec bude dlhý 5 znakov. Problém však je, že takto vytvorený reťazec sa uloží do špeciálnej časti pamäte, z ktorej je možné len čítať! Ak sa teda pokúsite zmeniť obsah tohto reťazca, skončíte s hláškou Segmentation fault.

Dĺžka reťazca

  • Ak chceme zistiť dĺžku reťazca, môžeme na to využiť funkciu strlen(), ktorá sa nachádza v knižnici string.h (ako aj všetky ostatné funkcie pracujúce s reťazcami).
  • Typ návratovej hodnoty funkcie strlen() je size_t. Je to makro, ktoré definuje bezznamienkový celočíselný typ určený na vyjadrenie veľkosti objektov v pamäti.
  • Pozor na zlú prax:
for (size_t i = 0; i < strlen(string); i++)
  • Funkcia strlen() sa vykoná pri každej iterácii!
  • Ilustrácia problému - vytvoríme si funkciu, ktorá bude náhradou funkcie strlen(), ktorú budeme volať v našom kóde:
#include <unistd.h>

size_t my_strlen(char string[]) {
    printf("counting...\n");
    sleep(1);
    return strlen(string);
}

...
for(size_t index = 0; index < my_strlen(text); index++)
...
  • Je výhodnejšie tento riadok nahradiť za:
for(size_t i = 0, len = my_strlen(string); i < len; i++)
  • V tomto prípade sa funkcia strlen() vykoná len raz.
  • Upravený kód (caesar-encrypt.c):
#include <stdio.h>
#include <string.h>

int main() {
    printf("Enter n: ");
    int shift;
    scanf("%d", &shift);

    char text[] = "HELLO";

    for (size_t index = 0; index < strlen(text); index++) {
        int letter_order = text[index] - 'A';
        int shifted_letter_order = (letter_order + shift) % 26;
        char encrypted = (char) ('A' + shifted_letter_order);
        printf("%c", encrypted);
    }

    printf("\n");

    return 0;
}

Načítanie reťazca z klávesnice

  • Načítavanie sa deje pomocou funkcie scanf() s formátovacím reťazcom "%s".
  • Preskočí biele znaky, napr.: " Hello world! " bude len "Hello"! Načíta práve jeden reťazec a world! zostane vo vstupnom buffri.
  • Pre načítanie celého riadku sa dá použiť napr. funkcia fgets() alebo formátovací reťazec funkcie scanf() je potrebné zapísať v tvare:
"%N[^\n]"
  • kde N predstavuje max. počet znakov, ktoré majú byť načítané a [^\n] reprezentuje regulárny výraz, ktorý hovorí, aby boli načítané všetky znaky okrem nového riadku (znak '\n').
  • Netreba používať operátor &, reťazec je sám adresou.
  • Reťazec, do ktorého sa vstup načítava, musí byť dostatočne dlhý! Funkcia scanf() môže obmedziť množstvo znakov, ktoré načíta vo formátovacom reťazci v tvare "%Ns", kde N udáva počet max. načítaných znakov.
  • Riešenie s načítavaním (caesar-encrypt-scanf.c):
#include <stdio.h>
#include <string.h>

int main(){
    printf("Enter the shift (N): ");
    int shift;
    scanf("%d", &shift);

    printf("Enter the string to encrypt: " );
    char string[11];
    scanf("%10s", string);

    size_t length = strlen(string);
    char encrypted[length + 1];

    for(int index = 0; index < length; index++) {
        encrypted[index] = 'A' + (string[index] -'A' + shift) % 26;
    }

    printf("%s\n", encrypted);

    return 0;
}

Formátované čítanie z a zápis do reťazca

  • Funkcie scanf() a printf() sa používajú na formátované čítanie zo štandardného vstupu a zápis na štandardný výstup.
  • Funkcie sscanf() a sprintf() sa používajú na formátované čítanie a zápis do reťazcov (bez výpisu na štandardný výstup).
  • Výhoda: príprava reťazca na neskoršie použitie (lepšia prax ako ho "skladať" postupným vypisovaním alebo v niektorých jazykoch operátorom +).
  • Príklad použitia:
char buffer[256];
sprintf(
    buffer,
    "Input string: %s\nShift: %d\nEncrypted string: %s\nCreated by ZAP 2017\n",
    string, shift, encrypted
);
printf("%s", buffer);
  • Tieto funkcie je možné použiť aj na konverziu medzi číslami a reťazcami.
  • Príklad:
#include <stdio.h>

int main() {
    printf("Enter number: ");
    char string[10];
    scanf("%s", string);

    int number;
    sscanf(string, "%d", &number);
    printf("The number you entered is: %d (%#x, %#o)\n", number, number, number);

    return 0;
}
  • Výstup daného príkladu:
    $ ./try
    Enter number: 5
    The number you entered is: 5 (0x5, 05)
  • Odbočka: Príklad uvedený v printf.c obsahuje rôzne príklady formátovania výstupu pomocou funkcie printf().

Bežné operácie so znakmi

  • Upravme si najskôr príklad s Cézarovou šifrou tak, aby sme používali vlastnú funkciu s parametrom (caesar-function.c):
#include <stdio.h>
#include <string.h>

void caesar_encrypt(const int shift, char text[]);

int main() {
    printf("Enter the shift (N): ");
    int shift;
    scanf("%d", &shift);

    printf("Enter the string to encrypt: ");
    char string[11];
    scanf("%10s", string);

    char encrypted[strlen(string) + 1];
    strcpy(encrypted, string);

    caesar_encrypt(shift, encrypted);

    printf("%s\n", encrypted);

    return 0;
}

void caesar_encrypt(const int shift, char text[]) {
    for (size_t index = 0, length = strlen(text); index < length; index++) {
        text[index] = 'A' + (text[index] - 'A' + shift) % 26;
    }
}
  • Cézarova šifra nám funguje, avšak iba na veľkých písmenách abecedy. Upravme ju tak, aby znaky, ktoré nie sú písmená abecedy, šifrované neboli. Šifrované budú iba písmená abecedy - malé aj veľké:
void caesar_encrypt(const int shift, char text[]) {
    for (int index = 0, length = strlen(text); index < length; index++) {
        if (text[index] > = 'A' && text[index] <= 'Z') {
            text[index] = 'A' + (text[index] - 'A' + shift) % 26;
        }
        if (text[index] > = 'a' && text[index] <= 'z') {
            text[index] = 'a' + (text[index] - 'a' + shift) % 26;
        }
    }

    text[strlen(text)] = '\0';
}
  • Aby sme nemuseli robiť veľké výpočty pri práci so znakmi, veľkou výhodou môže byť použitie funkcií isupper() a islower() z knižnice ctype.h. Celé riešenie sa nachádza v caesar-ctype.c:
#include <ctype.h>

void caesar_encrypt(const int shift, char text[]) {
    for (int index = 0, length = strlen(text); index < length; index++) {
        if (isupper(text[index])) {
            text[index] = 'A' + (text[index] - 'A' + shift) % 26;
        }
        if (islower(text[index])) {
            text[index] = 'a' + (text[index] - 'a' + shift) % 26;
        }
    }

    text[strlen(text)] = '\0';
}
  • Nasledujúci príklad zmení veľké znaky reťazca na malé a ostatné znaky nahradí znakom # (magic.c):
#include <ctype.h>
#include <stdio.h>
#include <string.h>

void magic(char text[]);

int main() {
    printf("Enter string: ");
    char text[20];
    scanf("%19s", text);

    printf("Original string: %s\n", text);
    magic(text);
    printf("Modified string: %s\n", text);

    return 0;
}

void magic(char text[]) {
    for (size_t index = 0, length = strlen(text); index < length; index++) {
        if (isupper(text[index])) {
            text[index] = tolower(text[index]);
        } else {
            text[index] = '#';
        }
    }
}
  • Je častou chybou iba zavolať tolower(text[index]);. Táto funkcia daný znak nezmení, ale vytvorí nový znak, ktorý vráti ako návratovú hodnotu funkcie. Tento nový znak je potrebné niekam uložiť: text[index] = tolower(text[index]);

Ternárny operátor

  • Náš posledný príklad je možné upraviť tak, aby namiesto if-else používal ternárny operátor. Výsledok je v programe magic-ternary.c:
#include <ctype.h>
#include <stdio.h>
#include <string.h>

void magic(char text[]);

int main() {
    printf("Enter string: ");
    char text[20];
    scanf("%19s", text);

    printf("Original string: %s\n", text);
    magic(text);
    printf("Modified string: %s\n", text);

    return 0;
}

void magic(char text[]) {
    for (size_t index = 0, length = strlen(text); index < length; index++) {
        text[index] = isupper(text[index]) ? tolower(text[index]) : '#';
    }
}

Doplňujúce zdroje

  1. Herout: Učebnica jazyka C
  2. Kernighan, Ritchie: Programovací jazyk C

Video