4. týždeň

Variables & I/O

Premenné - deklarácia, typy (int, float, double, char), rozsah platnosti premennej, práca so vstupom a výstupom (stdio.h, scanf(), printf()), funkcie s návratovou hodnotou, znaky vs čísla, formátovanie vstupu a výstupu, konverzia typov a operátor cast, operátor sizeof()

Video (17.10.2023)

Prezentácia

Zdrojový kód

Poznámky k prednáške

Spočítajme sa

  • Podobne ako v tomto videu, by sme sa mohli spočítať po dvojiciach, trojiciach a pod. Aby bol náš algoritmus čo najefektívnejší, musíme zvážiť koľka-tice ľudí vieme najrýchlejšie nájsť tak, aby sme na konci algoritmu nemali príliš veľa podmienok ohľadne zostávajúceho jedného, dvoch, troch ľuďoch.

Hello world!

  • S Karlom sa rozlúčime príkladom, ktorý sa používa už od roku 1974 a bol adaptovaný do mnohých podôb, resp. mnohých programovacích jazykov. V roku 1974 to bol Brian Kernighan: Programming in C: A Tutorial. Tento príklad vypíše na obrazovku len text: "Hello world!" a slúži ako ukážka najjednoduchšieho programu v danom jazyku (alebo na overenie potrebného softvérového vybavenia pri programovaní). (Zdroj: Wikipedia: Hello world program)
  • Na vypísanie textu na obrazovku potrebujeme poznať funkciu, ktorá vypísanie textu na obrazovku zabezpečí.
  • Kombinácia dvoch znakov '\n' predstavuje špeciálny znak - nový riadok (escape character).
  • To však nestačí - jazyk C nie je skriptovací a jeho príkazy sa nevykonávajú v poradí zhora nadol - od začiatku súboru po jeho koniec. Každý program v jazyku C obsahuje funkciu main(), ktorá označuje štartovacie miesto programu.
  • Ak by sme sa pokúsili program spustiť v tomto momente, neuspeli by sme. Jazyk C totiž funkciu printf() nepozná a musíme mu povedať, kde sa jej definícia nachádza - musí sa ju "naučiť". Na to slúži #include <stdio.h>
  • Výsledná podoba tohto programu bude teda vyzerať takto (hello.c):
#include <stdio.h>

int main(){
    printf("Hello world!\n");

    return 0;
}

Preklad a spustenie programu

  • Práve sme napísali zdrojový kód v jazyku C. Podobne ako u robota Karla, aby sme "ochutnali" výsledok, musíme zo zdrojového kódu vytvoriť spustiteľný kód procesom prekladu (kompilácie).
  • Prekladač je nástroj, ktorý zo zdrojového kódu vytvorí objektový kód. Objektovému kódu rozumie samotný procesor a je možné ho už vykonať (spustiť).
  • Aj naďalej pre preklad budeme používať prekladač GNU C Compiler (gcc). Teraz už nemusíme prilinkovať knižnice pre robota Karla, ale kód prekladáme s nasledovnými parametrami:
    • -std=c11 - budeme používať štandardizovanú verziu jazyka C z roku 2011
    • -Wall - zaujímať nás budú všetky upozornenia (warning), pretože nechceme, aby ste písali "len" kód, ktorý ide, ale aby ste písali "dobrý kód" a prekladač vás na nič špeciálne nemusel upozorňovať (program vie fungovať aj napriek tomu, že vás prekladač niekoľkokrát upozornil)
    • -Werror - aby ste tieto upozornenia neignorovali, táto voľba ich všetky zmení na chyby, čím nedôjde ku vytvoreniu výsledného spustiteľného programu
  • Program môžete prekladať aj použitím nástroja make. Predvolené správanie však funguje bez použitia extra prepínačov. Ak chcete dosiahnuť správne správanie, potrebujete nastaviť minimálne premennú prostredia CFLAGS (v súbore ~/.bashrc, prípadne v súbore ~/.profile):
export CC=gcc
export CFLAGS="-std=c11 -Wall -Werror"
export LDLIBS="-lm"
  • Program následne spustíte napísaním príkazu ./hello z príkazového riadku. Ak budete používať vývojové prostredie, budete mať možnosť spustiť výslednú aplikáciu priamo z neho.

Štandardný vstup a výstup

  • Vytvoríme program, ktorý bude robiť "rodičovskú kontrolu" pre prístup na webové stránky so zaujímavým obsahom. Na začiatku sa návštevníka opýtame na jeho vek a pokiaľ je starší ako 18 rokov, dovolíme mu na stránku vstúpiť. Ak nie, odporučíme mu sledovať aktuálny program na Cartoon Network.
  • Kostra programu bude vyzerať takto:
#include <stdio.h>

int main(){
    printf("Enter your age: ");

    return 0;
}
  • Na to, aby sme tento program vedeli dokončiť, potrebujeme ešte poznať:
    • Ako prečítať hodnotu od používateľa
    • Ako si zapamätať a kam uložiť hodnotu, ktorú používateľ zadal

Premenné

  • Na zapamätanie a uloženie načítanej hodnoty budeme používať premenné.
  • Premenná je pomenované miesto v pamäti pre uloženie hodnoty pre neskoršie použitie.
  • Ak chceme pracovať s premennou, musíme ju deklarovať. "Systém" totiž musí vedieť, koľko pamäte má pre túto premennú vyhradiť. Množstvo pamäte je určené typom premennej.
  • Ak chceme zistiť, koľko bytov zaberajú v pamäti údaje niektorej premennej, môžeme použiť operátor sizeof().
int nr = 10;
printf("int: %ld\n", sizeof(nr));
  • Názov by mal odrážať povahu údajov, ktoré bude premenná uchovávať. Typ hovorí aj o tom, koľko miesta v pamäti bude pre tento údaj vyhradený.
  • Základné typy, s ktorými budeme pracovať sú:
    • int - celé čísla
    • float - reálne čísla
    • double - reálne čísla s dvojnásobnou presnosťou
    • char - znaky (jazyk C nemá typ na uchovávanie reťazcov)
  • Znaky sú v pamäti reprezentované tiež ako čísla. Ak sa na tieto čísla chceme pozerať ako na znaky, musíme vedieť, ktoré číslo predstavuje ktorý znak. Pre toto mapovanie vznikla tabuľka ASCII, ktorá mapuje čísla na znaky.

Celočíselný typ (int)

  • Množina hodnôt predstavuje celé čísla od minimálneho až po maximálne zobraziteľné: [minInt, ..., -1, 0, 1, ..., maxInt]
  • Konštanty minInt a maxInt sú dané počtom bajtov pamäte, ktoré sú vyhradené pre zobrazenie celého čísla a spôsobom kódovania, t.j. ako sú celé čísla reprezentované v pamäti (napr. pri 4 bajtoch minInt = -2 147 483 648 a maxInt = 2 147 483 647)
  • Najdôležitejšie operácie: sčítanie +, odčítanie -, násobenie *, delenie /, ako aj zvyšok po celočíselnom delení modulo %
  • Možnosť pretečenia a podtečenia

Reálny typ (float/double)

  • Hodnoty údajového typu real si môžeme priblížiť nasledovne: [-largeReal, ... , -1.0 , ... , -smallReal, 0.0, +smallReal, ... , +1.0, ...,+largeReal]
  • Zápis: s desatinnou bodkou (3.14) alebo exponenciálny (2.3e-9, resp. -11E21)
  • Niektoré reálne čísla nemôžu byť zobrazené presne, ale dochádza k zaokrúhľovaniu
  • Najdôležitejšie operácie: aritmetické okrem modula
  • Možnosť pretečenia a podtečenia, chyba pri zaokrúhľovaní (t.j. (x / y) * y nemusí byť zhodná s hodnotou x)

Znakový typ (char)

  • Hodnoty znakového typu predstavujú kódy znakov (nezáporné celé čísla), ktoré sú organizované v tzv. znakových sadách (character sets)
  • '0' < '1' (teda kód znaku '0' je pred kódom znaku '1'), 'a' < 'z', 'A' < 'Z' ako aj 'A' < 'a'
  • Príklady znakových sád (8b/16b): ISO-8859-1 (Latin 1), ISO-8859-2 (Latin 2), Windows-1250 , Unicode (16b)
  • Nezobraziteľné znaky, ktoré slúžia napr. ako riadiace pokyny pri práci s tlačiarňou alebo iným vstupno/výstupným zariadením

Načítanie vstupu

  • Na načítanie vstupu od používateľa budeme používať funkciu scanf().
  • Funkcia scanf() má dva parametre:
    • Formátovací reťazec, ktorý hovorí o tom, aký typ údajov má byť prečítaný
    • Adresu premennej, do ktorej má byť táto informácia zapísaná
  • Deklarovanie premennej a načítanie vstupu od používateľa:
int age;
scanf("%d", &age);

Výpis premennej na obrazovku

  • Vypísanie informácie na obrazovku:
printf("You are age years old\n");
  • Očakávame, že ku premennej pristupujeme priamo pomocou jej názvu aj pri výpise. Funkcia printf() obsahuje formátovací reťazec, pomocou ktorého vieme povedať, že sa má na konkrétnom mieste vypísať hodnota takéhoto typu:
printf("You are %d years old\n", age);
  • Nenachádza sa tu už znak '&' - pracujeme priamo s hodnotou.
  • Výsledný kód (age1.c):
#include <stdio.h>

int main(){
    int age;
    printf("How old are you? ");
    scanf("%d", &age);
    printf("You are %d years old\n", age);

    return 0;
}

Rozsah platnosti premennej

  • Rozšírime program tak, aby nebolo možné zadávať iné hodnoty ako tie, ktoré sú v rozsahu (0,120). Používateľ bude hodnoty zadávať dovtedy, pokiaľ nezadá správnu hodnotu.

Bug

  • Vytvoríme spoločne takýto kód (chybný kód) (age2.c):
#include <stdio.h>

int main(){
    do{
        int age;
        printf("How old are you? ");
        scanf("%d", &age);
    } while(age <= 0 || age > 120);
    printf("You are %d years old\n", age);

    return 0;
}

Scope

  • Prekladač hlási, že premenná age nie je deklarovaná. Prečo, keď je deklarovaná na riadku 5?
  • Rozsah platnosti premennej (scope) - premenná platí od jej deklarácie po najbližšie uzatváracie zložené zátvorky "jej úrovne". Vhodným presunutím deklarácie premennej age je možné jej rozsah platnosti rozšíriť (age3.c):
#include <stdio.h>

int main(){
    int age;

    do{
        printf("How old are you? ");
        scanf("%d", &age);
    } while(age <= 0 || age > 120);

    printf("You are %d years old\n", age);

    return 0;
}

Globálne premenné

  • Globálna premenná platí v celom programe. Umiestňuje sa mimo (nad) definíciu funkcie main().
  • Použitiu globálnych premenných sa vyhnite, pretože vedie k zlým návykom!
  • Premenná age použitá ako globálna (age4.c):
#include <stdio.h>

int age;

int main(){
    do{
        printf("How old are you? ");
        scanf("%d", &age);
    } while(age <= 0 || age > 120);

    printf("You are %d years old\n", age);

    return 0;
}

Dokončenie príkladu

  • Výsledné riešenie príkladu je možné stiahnuť tu: age5.c.
#include <stdio.h>

int main(){
    int age;

    do{
        printf("How old are you? ");
        scanf("%d", &age);
    } while(age <= 0 || age > 120);

    printf("You are %d years old\n", age);

    if(age <18){
        printf("Go and watch some Cartoons yourself\n");
    }else{
        printf("Welcome, master!\n");
    }

    return 0;
}

Funkcia s návratovou hodnotou

  • Program ešte upravíme tak, aby používal funkcie s návratovou hodnotou:
#include <stdio.h>

#define MIN 1
#define MAX 120
#define ADULT 18

int how_old(const int, const int);
int is_adult(const int);

int main(){
    int age = how_old(MIN,MAX);

    if(is_adult(age) == 0){
        printf("Go and watch some Cartoons yourself\n");
    }else{
        printf("Welcome, master!\n");
    }

    return 0;
}

int how_old(const int min, const int max){
    int age;

    do{
        printf("How old are you? ");
        scanf("%d", &age);
    } while(age < min || age > max);

    printf("You are %d years old\n", age);

    return age;
}

int is_adult(const int age){
    if(age < ADULT){
        return 0;
    }
    return 1;
}
  • Výsledné riešenie príkladu je možné stiahnuť tu: age6.c.

Unit Converter

  • Vytvoríme program na prevod teploty zo stupňov celzia do kelvinov a fahrenheitov. Vzťahy na prevod teploty sú nasledovné:
fahrenheit = 9/5 * celsius + 32;
kelvin = celsius + 273.15;
  • Prvá verzia po priamom použití týchto vzťahov bude vyzerať nasledovne (converter-bad.c):
#include <stdio.h>

int main(){
    printf("Enter the temperature in Celsius: ");
    float celsius;
    scanf("%f", &celsius);

    float fahrenheit = 9/5 * celsius + 32;
    float kelvin = celsius + 273.15;

    printf("%.2f in Celsius is %.2f in Fahrenheit and %.2f in Kelvin\n", celsius, fahrenheit, kelvin);

    return 0;
}
  • Presnosť výpočtu závisí od presnosti čiastkových výsledkov: Aký bude výsledok operácie 9/5? Je potrebné s ním pracovať ako s typom float. Teda 9.0/5.0.
  • Formátovanie výsledku na dve desatinné miesta pomocou zápisu %.2f.

Odbočka: Presnosť desatinných čísiel

  • Vytvorme jednoduchý program (float1.c):
#include <stdio.h>

int main(){
    float f = 1/10;
    printf("%.2f\n", f);

    return 0;
}
  • Po preklade a spustení je však miesto výsledku 0.10 výsledkom 0.00.
  • Problém je, že delíme jeden integer druhým. Výsledkom operácie podielu dvoch integerov je ďalší integer.

Pretypovanie a cast operátor

  • Riešením problému je spraviť z hodnoty typu int typ float (float2.c):
#include <stdio.h>

int main(){
    float f = 1.0/10.0;
    printf("%.2f\n", f);

    return 0;
}
  • Rovnako tak môžeme explicitne pretypovať typ int na typ float pomocou cast operátora (float3.c):
#include <stdio.h>

int main(){
    float f = (float)1/(float)10;
    printf("%.2f\n", f);

    return 0;
}
  • Aká hodnota by sa nachádzala v premennej f, ak by sme výraz upravili nasledovne:
float f = (float)1/10;
float f = 1/(float)10;
float f = (float)(1/10);

Presnosť desatinných miest

  • Pokiaľ budete pri výpise zväčšovať počet čísiel nachádzajúcich sa za desatinnou čiarkou, výsledkom už nebude hodnota 0.1, ale na konci budú pribúdať rozličné iné hodnoty. Je to dané tým, že typ float má konečnú veľkosť (je v pamäti reprezentovaný konečným počtom bytov). To znamená, že v istom momente môžu byť výsledky vašich výpočtov nepresné.
printf("%.20f\n", 1.0/10.0);

Dokončenie

#include <stdio.h>

int main(){
    printf("Enter the temperature in Celsius: ");
    float celsius;
    scanf("%f", &celsius);

    float fahrenheit = 9.0/5.0 * celsius + 32;
    float kelvin = celsius + 273.15;

    printf("%.2f in Celsius is %.2f in Fahrenheit and %.2f in Kelvin\n", celsius, fahrenheit, kelvin);

    return 0;
}

Doplňujúce zdroje

  1. W. Wulf, M. Shaw: Global variable considered harmful, 1973
  2. Proč počítačům dělají problémy desetinná čísla
  3. What Every Computer Scientist Should Know About Floating-Point Arithmetic
  4. Functions vs parameters
  5. Funkcia printf(): c-reference, cplusplus.com - Loads the data from the given locations, converts them to character string equivalents and writes the results to stdout.
  6. Funkcia scanf(): c-reference, cplusplus.com - Reads data from stdin, interprets it according to format and stores the results into given locations.
  7. Operátor sizeof(): c-reference - Queries size of the object or type. Used when actual size of the object must be known.

Video