7. týždeň

Strings

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

Video (10'2022)

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 2019\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]) : '#';
    }
}

Unix translate utility: tr

  • Nástroj tr nájdete bežne v každej inštalácii linuxového/unixového systému.
  • Používa sa hlavne na preklad alebo mazanie znakov čítaných zo štandardného vstupu a následný výpis výsledku na štandardný výstup:
$ tr aeiouy AEIOUY
Ahoj, ako sa mas?
AhOj, AkO sA mAs?
Hello, how are you?
HEllO, hOw ArE YOU?

Poznámka

Ak chcete ukončiť zadávanie textu, použite klávesovú skratku Ctrl+d.

  • Nástroj môžeme použiť aj na preklad pomocou Cézarovej šifry. Príklad na preklad s krokom 5:
$ tr a-zA-Z f-za-eF-ZA-E
Ahoj, ako sa mas?
Fmto, fpt xf rfx?
Hello, how are you?
Mjqqt, mtb fwj dtz?
  • Ak sa chcete dozvedieť viac o používaní tohto nástroja, použite manuálovú stránku (man tr). My sa pokúsime o jednoduchú implementáciu tohto nástroja v jazyku C.

Znaky, ktoré je potrebné preložiť

  • Najskôr načítame zo vstupu reťazec, ktorý bude reprezentovať prvú množinu znakov (určenú na preklad).
  • O tejto množine budú platiť tieto pravidlá:
  • Množina môže byť tvorená len znakmi ASCII tabuľky od hodnoty 33 do hodnoty 126 (vrátane). To znamená, že znakmi môžu byť všetky zobraziteľné znaky okrem bielych znakov.
  • Znaky z tejto množiny sa nesmú opakovať, pretože rovnaký znak by tak súčasne mohol byť nahradený niekoľkými inými znakmi.
  • Keďže splnenie prvého pravidla je možné očakávať predvolene, zabezpečíme vo svojej implementácii splnenie druhého pravidla. Ak teda dôjde k porušeniu tohto pravidla, vypíšeme na obrazovku správu Wrong input! a program ukončíme (tr-1.c):
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

void read_set(char set[]);
bool contains_same_letters(const char set[]);


int main() {
    // reading set 1
    char set1[50];
    read_set(set1);

    // check for same letters
    if (contains_same_letters(set1)) {
        printf("Wrong input.\n");
        return -1;
    }

    return 0;
}

void read_set(char set[]) {
    printf("Enter set 1: ");
    scanf("%s", set);
}

bool contains_same_letters(const char set[]) {
    for (int index = 0, len = (int) strlen(set); index < len - 1; index++) {
        for (int index2 = index + 1; index2 < len; index2++) {
            if (set[index] == set[index2]) {
                return true;
            }
        }
    }
    return false;
}

Znaky, ktoré prekladajú

  • Ďalej zo vstupu načítame reťazec, ktorý bude reprezentovať druhú množinu znakov (tie, ktoré prekladajú). Vieme využiť tú istú funkciu na načítanie vstupu, len ju trochu upravíme, aby prompt obsahoval číslo množiny.
  • Jedinou podmienkou, ktorú je potrebné splniť pri načítaní druhej množiny, je, aby sa počet znakov tejto množiny zhodoval s počtom znakov prvej množiny. T.j. ku každému znaku prvej množiny musí existovať znak, ktorým bude nahradený (tr-2.c):
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define SET_LEN 50

void read_set(int set_number, char set[]);
bool contains_same_letters(const char set[]);


int main() {
    // reading set 1
    // ...

    // check for same letters
    // ...

    // reading set 2
    char set2[SET_LEN];
    read_set(2, set2);

    // check for the same length
    if (strlen(set1) != strlen(set2)) {
        printf("Length of sets is different.\n");
        return -1;
    }

    return 0;
}

void read_set(int set_number, char set[]) {
    printf("Enter set %d: ", set_number);
    scanf("%s", set);
}

Text na preklad

  • Teraz načítame text, ktorý je potrebné preložiť. Text bude tvorený reťazcom, ktorý bude na vstupe ukončený novým riadkom.
  • Text sa môžeme pokúsiť načítať niektorou z nasledovných funkcií: scanf(), getchar(), gets() alebo fgets() (pričom miesto uvedenia referencie na súbor použijeme referenciu s názvom stdin (štandardný vstup)).
  • Použitie každej z uvedených funkcií má svoje špecifiká (až na gets(), ktorú prekladač označí za deprecated a odmietne preložiť program). Ak použijeme funkciu scanf(), musíme si byť vedomí toho, že viacnásobné medzery, resp. biele znaky, budú ignorované. Rovnako tak sa musíme rozhodnúť pre vhodnú dĺžku buffra na načítanie vstupu.
  • V našom prípade použijeme funkciu fgets(), keďže tú sme si na prednáškach zatiaľ neukázali (tr-3.c):
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define SET_LEN 50
#define INPUT_LEN 256

void clear_buffer();
void read_set(int set_number, char set[]);
void read_input_text(char text[]);
bool contains_same_letters(const char set[]);

int main() {
    // reading set 1
    // ...

    // check for same letters
    // ...

    // reading set 2
    // ...

    // check for the same length
    // ...

    // clear buffer
    clear_buffer();

    // reading text input
    char text[INPUT_LEN];
    read_input_text(text);

    return 0;
}

void clear_buffer() {
    while (getchar() != '\n');
}

void read_input_text(char text[]) {
    printf("Enter text to translate: ");
    fgets(text, INPUT_LEN, stdin);
}

Preklad načítaného textu

  • Ak sa na vstupe bude nachádzať písmeno, ktoré sa nachádza aj v prvej množine, na výstup sa vypíše také písmeno z druhej množiny, ktoré sa nachádza na rovnakej pozícii ako písmeno z prvej množiny.
  • Ak sa na vstupe bude nachádzať písmeno, ktoré sa v prvej množine nenachádza, rovno sa vypíše na výstup (bez prekladu).
  • Preklad môže vyzerať nasledovne (tr-4.c):
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#define SET_LEN 50
#define INPUT_LEN 256

void clear_buffer();
void read_set(int set_number, char set[]);
void read_input_text(char text[]);
bool contains_same_letters(const char set[]);
int find_position(const char[], char);
void translate(const char set1[], const char set2[], const char text[], char translated[]);

int main() {
    // reading set 1
    // ...

    // check for same letters
    // ...

    // reading set 2
    // ...

    // check for the same length
    // ...

    // clear buffer
    // ...

    // reading text input
    // ...

    // translation
    char translated[strlen(text) + 1];
    translate(set1, set2, text, translated);
    printf("%s", translated);

    return 0;
}

int find_position(const char set1[], const char letter) {
    for (int index = 0, len = (int) strlen(set1); index < len; index++) {
        if (letter == set1[index]) {
            return index;
        }
    }
    return -1;
}

void translate(const char set1[], const char set2[], const char text[], char translated[]) {
    int len = (int) strlen(text);
    for (int index = 0; index < len; index++) {
        int position = find_position(set1, text[index]);
        translated[index] = position != -1 ? set2[position] : text[index];
    }
    translated[len] = '\0';
}
  • Použitie prekladu môže vyzerať nasledovne:
$ ./tr
Enter set1: aeiouy
Enter set2: xxxxxx
Enter text to translate: Hello world!
Hxllx wxrld!

Doplňujúce zdroje

  1. Herout: Učebnica jazyka C
  2. Kernighan, Ritchie: Programovací jazyk C
  3. Unix translate utility
  4. Clearing the input buffer in C

Video