Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Letný semester, prednáška č. 1

Z Programovanie
Skočit na navigaci Skočit na vyhledávání

Organizácia predmetu v letnom semestri

Základné črty jazyka Java a programovania v ňom

Prvoradým cieľom tohto predmetu je zvládnutie základov objektovo orientovaného programovania (OOP) prostredníctvom programovacieho jazyka Java. Na úvod je užitočné vedieť o tomto jazyku nasledujúce:

  • Základné syntaktické konštrukcie jazyka Java sú v mnohom veľmi podobné jazykom C a C++. Už tento týždeň by sme mali zvládnuť prepísať do Javy väčšiu časť programov z minulého semestra; budúci týždeň by to už mali byť úplne všetky programy.
  • Jazyk Java je silne (aj keď nie čisto) objektovo orientovaný. Použite tried – ktoré sú popri objektoch základným konceptom OOP – je nutné na napísanie aj toho najjednoduchšieho programu.
  • Na druhej strane je triedu možné použiť aj ako obyčajný „obal” pre niekoľko metód (t.j. funkcií) podobného typu ako v minulom semestri. Takýmto spôsobom budeme s Javou pracovať na dnešnej prednáške. S ozajstným objektovo orientovaným programovaním začneme až budúci týždeň.
  • Programy v jazyku Java sa obvykle nekompilujú do strojového kódu, ale do tzv. javovského bytekódu. Po skompilovaní programu teda nedostávame bežný spustiteľný súbor, ale súbor, ktorý možno spustiť na javovskom virtuálnom stroji (angl. Java Virtual Machine; skr. JVM). Vykonávanie takýchto programov je síce o niečo pomalšie, zato sú však prenositeľné medzi rôznymi operačnými systmémami a architektúrami.

Celkovo ide o jazyk omnoho vyššej úrovne, než jazyk C: v oveľa väčšej miere sa tu abstrahuje od počítačovej architektúry. Java napríklad neumožňuje priamy prístup k pamäťi počítača a o uvoľňovanie alokovanej pamäte sa stará JVM automaticky prostrednictvom mechanizmu tzv. garbage collection. Hoci teda jazyk nie je príliš vhodný na nízkoúrovňové programovanie, tvorba „bežných” používateľských programov je tu podstatne pohodlnejšia, než napríklad v jazyku C. Okrem toho Java disponuje veľkou knižnicou štandardných tried (Java Class Library; skr. JCL), v ktorej je okrem iného implementované aj množštvo algoritmov a dátových štruktúr. Orientáciu v možnostiach ponúkaných touto knižnicou značne uľahčuje dokumentácia k nej.

Rozdiel v úrovni abstrakcie medzi jazykmi C a Java sa premieta aj do typického programátorského štýlu. Od efektívnosti samotnej implementácie sa dôraz obvykle posúva k aspektom softvérového inžinierstva, ako sú napríklad čitateľnosť, rozšíriteľnosť a „spravovateľnosť” kódu. S niektorými elementárnymi princípmi softvérového inžinierstva sa zoznámime aj na tomto predmete.

Prvý program v jazyku Java

Tradične začneme programom, ktorý na konzolu vypíše text Hello, World!.

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • Názov súboru sa musí zhodovať s názvom triedy, ku ktorému sa pridá prípona .java – v našom prípade teda musí byť zdrojový kód uložený v súbore Hello.java.
  • Program je potrebné skompilovať javovským kompilátorom a následne spustiť na javovskom virtuálnom stroji – návod možno nájsť na osobitnej stránke. Po skompilovaní programu získame súbor java.class spustiteľný na JVM.

Rozoberme postupne jednotlivé časti uvedeného programu:

  • Funkcia main, ktorá sa podobne ako v C/C++ začne vykonávať bezprostredne po spustení programu, je „obalená” v triede, ktorú sme nazvali Hello. V jazyku Java musí byť všetok kód súčasťou nejakej triedy (význam tried pre OOP zatiaľ ponechajme bokom).
  • Hlavička funkcie main musí byť vždy tvaru ako vyššie. Modifikátory public a static si vysvetlíme neskôr; nasleduje návratový typ void, názov funkcie main a argumenty funkcie main, ktorými sú argumenty programu z príkazového riadku (v podobe poľa reťazcov).
  • Samotný výpis na konzolu realizuje metóda System.out.println.

Keďže jeden väčší program typicky pozostáva z množstva rôznych tried (v rôznych súboroch), je možné triedy ďalej umiestniť do balíkov; program tak často pozostáva z niekoľkých balíkov navzájom súvisiacich tried. Umiestnenie triedy do balíka možno realizovať príkazom na začiatku zdrojového súboru.

package somepackage;

public class Hello {

    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }

}
  • V našom prípade sme triedu Hello umiestnili do balíka somepackage. Pri práci s IDE ako napr. IntelliJ je často potrebné pridať balík aj do projektu; rozdiel je tiež pri kompilovaní a spúšťaní triedy z príkazového riadku. Viac sa možno dočítať tu.
  • Pokiaľ deklarácia balíka chýba, považuje sa trieda za súčasť nepomenovaného balíka (angl. unnamed package alebo default package). Používanie nepomenovaného balíka vo všeobecnosti nie je dobrá prax, ale je zmysluplné pri menších programoch pre vlastnú potrebu (alebo pre potreby tohto predmetu).
  • Za dobrú prax sa naopak považuje pomenúvanie balíkov tak, aby boli celosvetovo jednoznačne identifikovateľné – za týmto účelom preto boli vybracované špeciálne konvencie, ktoré ale na tomto predmete nebudeme dodržiavať.

Konvencie pomenúvania identifikátorov

Za účelom sprehľadnenia zdrojového kódu a často pomerne rozsiahlych knižníc sa pri pomenúvaní identifikátorov v Jave používajú (okrem iných aj) nasledujúce konvencie:

  • Názvy tried by mali vždy začínať veľkým písmenom.
  • Názvy premenných, metód a balíkov by naopak mali vždy začínať malým písmenom.

Nedodržiavanie týchto konvencií budeme na tomto predmete považovať za chybu. O ďalších konvenciách používaných v tomto smere sa možno dočítať tu.

Príklad o niečo rozsiahlejšieho programu

Nižšie je príklad o niečo rozsiahlejšieho programu v jazyku Java. Pokúste sa s využitím skúseností z minulého semestra uhádnuť, čo tento program robí.

import java.util.*;

public class Program {

    /* Spocita sucet prvkov pola a.
     */
    public static int sum(int[] a) {
        int result = 0;
        for (int i = 0; i <= a.length - 1; i++) {
            result += a[i];
        }
        return result;
    }

    /* Spocita priemer hodnot prvkov pola a.
     */
    public static double average(int[] a) {
        return (double) sum(a) / a.length;
    }

    /* Najde najvacsi prvok pola a.
     */
    public static int max(int[] a) {
        int max = Integer.MIN_VALUE;  // Premenna ma rovnaky nazov ako metoda
        for (int i = 0; i <= a.length - 1; i++) {
            if (a[i] >= max) {
                max = a[i];
            }
        }
        return max;
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int n = scanner.nextInt();
        int[] a = new int[n];

        for (int i = 0; i <= n - 1; i++) {
            a[i] = scanner.nextInt();
        }
        scanner.close();

        System.out.println("Sucet: " + sum(a));
        System.out.println("Priemer: " + average(a));
        System.out.println("Maximum: " + max(a));
    }

}
  • Okamžite vidieť množstvo podobností medzi jazykmi C/C++ a Java, ale aj niekoľko drobných rozdielov.
  • Konštrukcie jazyka Java použité v tomto programe podrobnejšie rozoberieme nižšie.

Základné konštrukcie jazyka Java

Primitívne typy

Jazyk Java podporuje celkovo osem primitívnych dátových typov:

  • int: 32-bitové celé čísla so znamienkom, v rozsahu od -231 po 231 - 1; ďalšie celočíselné typy sú byte, short a long (na rozdiel od C/C++ už teda long a short nie sú modifikátory, ale priamo celočíselné typy).
  • double: 64-bitové desatinné čísla s pohyblivou desatinnou čiarkou; ďalší typ desatinných čísel je 32-bitový float.
  • boolean: logické hodnoty true alebo false.
  • char: 16-bitové znaky v kódovaní Unicode (UTF-16); typ teda napríklad podporuje slovenskú diakritiku.

Deklarácie premenných a priradenia zapisujeme rovnako ako v C/C++. Na rozdiel od C/C++ možno napríklad definovať aj lokálnu premennú s rovnakým názvom ako niektorá metóda.

int x = 0;

Kompilátor pritom nedovolí použitie neinicializovanej lokálnej premennej.

int x;
System.out.println(x); // variable x might not have been initialized

Neskôr uvidíme, že prvky polí a premenné tried a ich inštancií sa inicializujú automaticky.

Pretypovanie tiež funguje podobne ako v C/C++, avšak implicitné (automatické) pretypovanie je možné pre menší počet dvojíc typov, než v C/C++. Číselné typy možno usporiadať ako byte ≺ short ≺ int ≺ long ≺ float ≺ double a implicitné pretypovanie je možné iba v smere tohto usporiadania. Ďalej je možné implicitne pretypovať char na int, long, float a double (nie však naopak). Zvyšné pretypovania je nutné robiť explicitne, rovnako ako v C/C++.

int x = 65;
char c = (char) x;
System.out.println(c);

Celočíselné konštanty sa na char pretypujú automaticky; možno napríklad priamo písať char c = 65.

Operátory

Jazyk Java podporuje podobnú sadu operátorov ako jazyky C a C++; ich kompletný zoznam vrátanie precedencií možno nájsť tu. Spomeňme predovšetkým:

  • Operátory priradenia =, +=, *=, atď.
  • Aritmetické operátory +, -, *, /, %. Na celočíselných typoch sa operátor / správa, rovnako ako v C/C++, ako celočíselné delenie.
  • Prefixové a sufixové operátory ++, --.
  • Relačné a porovnávacie operátory ==, !=, <, >, <=, >=.
  • Logické operátory ||, &&, !.

Cykly a podmienky

Nasledujúce konštrukcie sú v Jave veľmi podobné ako v C/C++:

  • Podmienky: if, else a switch.
  • Cykly: for, while, do ... while.
  • Príkazy break a continue.

Drobný rozdiel pri ich použití vyplýva zo skutočnosti, že v Jave nedochádza k automatickému pretypovaniu z napr. číselných typov na boolean. Napríklad nasledujúci kus kódu, ktorý by bol v C/C++ korektný, vyústi v Jave chybu.

int n = 1;
if (n) {  // incompatible types: int cannot be converted to boolean
    // ...           
}

Nápravu dosiahneme nahradením celočíselného výrazu n výrazom logickým.

int n = 1;
if (n != 0) {
    // ...           
}

Jednorozmerné polia

V Jave sa oproti C/C++ používa trochu iná syntax pre deklaráciu poľa.

int[] a; // Deklaracia premennej a reprezentujucej pole celych cisel

Samotné vytvorenie poľa a priradenie referencie naň do premennej a následne možno realizovať podobne ako pri dynamicky alokovaných poliach v C++.

a = new int[10];

Pamäť alokovanú pri vytvorení poľa nie je potrebné manuálne uvoľňovať; stará sa o to automatický „smetiarsky” mechanizmus (angl. garbage collection). Prípadne tiež možno spojiť deklaráciu premennej s vytvorením poľa do jediného príkazu.

int[] a = new int[rozsah];

Namiesto int[] a možno písať aj int a[]; prvý spôsob je ale prirodzenejší, keďže typom premennej a je v oboch prípadoch int[].

Pri vytvorení poľa sa všetky jeho prvky automaticky inicializujú – a to na 0 pri poliach čísel resp. znakov (kde ide o znak s kódom 0, nie o znak '0'), na false pri poliach booleovských hodnôt a na null pri poliach objektov a polí (poľami takéhoto typu sa budeme zaoberať až neskôr). V prípade potreby inicializovať pole vopred známej dĺžky na iné hodnoty možno použiť nasledujúcu skratku.

int[] a = {0, 1, 2, 3, 4, 5};

Týmto príkazom sa do premennej a priradí referencia na šesťprvkové pole obsahujúce postupne hodnoty 0, 1, 2, 3, 4, 5. Takéto priradenie možno realizovať iba na riadku, na ktorom je príslušná premenná deklarovaná. Neskôr možno do premennej a priradiť takto „vymenované” pole nasledovne.

int[] a;

// ...

a = new int[]{0, 1, 2, 3, 4, 5};

Súčasťou poľa je v Jave aj informácia o jeho dĺžke, ku ktorej možno pre pole a pristúpiť cez a.length. Nasledujúci kus kódu tak napríklad do stoprvkového poľa a uloží postupnosť čísel 0, 1, 2, ..., 99.

int[] a = new int[100];
for (int i = 0; i <= a.length - 1; i++) {
    a[i] = i;
}

V prípade pokusu o prístup k prvku poľa mimo jeho rozsah Java za behu programu vyhodí výnimku java.lang.ArrayIndexOutOfBoundsException. Mechanizmom výnimiek a ich spracovaním sa na tomto predmete budeme zaoberať neskôr; v tomto momente je podstatná skutočnosť, že v Jave nie je možné pomocou prístupu k danému poľu prepísať pamäť mimo jeho rozsah (čo je jedna z najnepríjemnejších chýb v C/C++).

Hodnoty vs. referencie v jazyku Java

Napriek tomu, že v jazyku Java neexistuje mechanizmus smerníkov (ani žiadna obdoba smerníkovej aritmetiky), sú všetky premenné okrem premenných primitívnych typov v skutočnosti referenciami predstavujúcimi adresy v pamäti. Presnejšie:

  • Premenné primitívnych typov obsahujú hodnoty týchto primitívnych typov.
  • Premenné všetkých zvyšných typov obsahujú referencie (na pole alebo na objekt).

Premenné obsahujúce referencie – zatiaľ vieme pracovať iba s premennými ukazujúcimi na pole – sa tak správajú veľmi podobne ako smerníky v C/C++. Operátor = aplikovaný na takéto premenné nekopíruje hodnoty, ale referencie; podobne operátor == neporovnáva hodnoty, ale referencie.

int[] a = {1, 2, 3, 4, 5};
int[] b = a;                   // Premenne a, b ukazuju na to iste patprvkove pole
b[0] = 10;
System.out.println(a[0]);      // Vypise 10
b = new int[a.length];         // Premenne a, b uz teraz ukazuju na dve rozne polia
for (int i = 0; i <= b.length - 1; i++) {
    b[i] = a[i];               // Hodnoty v oboch poliach su odteraz rovnake, polia ako take su rozne
}
System.out.println(a == b);    // Vypise false

Premenná obsahujúca referenciu môže nadobúdať špeciálnu hodnotu null; v takom prípade referencia neukazuje na žiadnu pamäť.

Cyklus for each

V Jave existuje špeciálny variant cyklu for, tzv. cyklus for each, umožňujúci postupne prejsť cez všetky hodnoty prvkov poľa a aj bez indexovej premennej. Napríklad kus kódu

int[] a = {6, 5, 4, 3, 2, 1};
for (int x : a) {
    System.out.println(x);
}

je ekvivalentný nasledujúcemu kódu.

int[] a = {6, 5, 4, 3, 2, 1};
for (int i = 0; i <= a.length - 1; i++) {
    System.out.println(a[i]);
}

Vo všeobecnosti možno povedať, že cyklus for each s iteračnou premennou x pracuje nasledovne:

  • Prechádza postupne pole od začiatku po jeho koniec.
  • V každej iterácii najprv skopíruje hodnotu na danej pozícii poľa do premennej x a následne vykoná príkazy v tele cyklu.

Z toho vyplýva, že pomocou cyklu for each nemožno meniť hodnoty prechádzaného poľa. Napríklad v cykle

for (int x : a) {
    x = 0;
}

sa nemenia jednotlivé prvky poľa, ale iba lokálna premenná x. Avšak v prípade, že sú prvkami poľa referencie, možno pomocou cyklu for each meniť hodnoty, na ktoré tieto referencie ukazujú (to je vlastnosť, ktorú oceníme až neskôr).

Viacrozmerné polia

Popri jednorozmerných poliach možno v Jave pracovať aj s dvojrozmernými poľami, ktoré sa správajú podobne ako polia smerníkov resp. smerníky na smerníky v C/C++. V javovskej terminológii môžeme povedať, že dvojrozmerné pole je poľom polí. Obdĺžnikové pole vytvoríme napríklad nasledovne.

int[][] a = new int[3][4];  // Vytvori pole s troma riadkami a troma stĺpcami

Takéto pole si možno predstaviť ako trojprvkové pole jednorozmerných polí dĺžky 4. Napríklad a[0] je teda jednorozmerné pole zodpovedajúce nultému riadku dvojrozmerného poľa a. S dvojrozmerným poľom potom pracujeme prirodzeným spôsobom.

for (int i = 0; i <= a.length - 1; i++) {
    for (int j = 0; j <= a[i].length - 1; j++) {
        a[i][j] = i + j;
    }
}

Výsledné pole je znázornené na nasledujúcom obrázku.

JPamat1.png

Alternatívne možno najprv inicializovať iba pole jednotlivých riadkov a následne každý z riadkov zvlášť. Takto môžeme vytvoriť aj iné ako obdĺžnikové polia, napríklad „trojuholník” z nasledujúceho príkladu.

int[][] a = new int[3][]; // V tomto momente su a[0], a[1] aj a[2] rovne null (vdaka automatickej inicializacii prvkov pola)
for (int i = 0; i <= a.length - 1; i++) {
    a[i] = new int[i + 1];
}
for (int i = 0; i <= a.length - 1; i++) {
    for (int j = 0; j <= a[i].length - 1; j++) {
        a[i][j] = j;
    }
}

Výsledné pole je znázornené na nasledujúcom obrázku.

JPamat2.png

Z tejto predstavy o reprezentácii dvojrozmerných polí by malo byť zrejmé, že napríklad inicializácia int[][] a = new int[][10]; nie je korektná.

Rovnako ako s dvojrozmernými poľami možno v Jave pracovať aj s poľami o ľubovoľnom konečnom počte rozmerov. Pri inicializácii takýchto polí platí, že je potrebné určiť prvých niekoľko rozmerov (kde „niekoľko” v tomto prípade znamená „aspoň jeden”).

int[][][] a = new int[3][4][5]; // OK
int[][][] b = new int[3][4][];  // OK
int[][][] c = new int[3][][];   // OK
int[][][] d = new int[][4][5];  // Chyba
int[][][] e = new int[3][][5];  // Chyba
int[][][] f = new int[][][];    // Chyba

Statické metódy

Obdobou funkcií, ako ich poznáme z minulého semestra, sú v jazyku Java statické metódy triedy.

  • Definujú sa vždy vo vnútri nejakej triedy s použitím podobnej syntaxe ako v C/C++. Pred návratový typ je potrebné napísať kľúčové slovo static, vďaka ktorému pôjde o statickú metódu triedy (čiže zjednodušene povedané „obyčajnú funkciu”) a nie o metódu inštancie triedy, čo je koncept objektovo orientovaného programovania, s ktorým sa zoznámime na budúcej prednáške.
  • Pred klúčové slovo static ešte možno pridať modifikátor prístupu ako napr. public alebo private hovoriaci o viditeľnosti metódy z iných tried a balíkov. Modifikátormi prístupu sa budeme detailnejšie zaoberať neskôr.
  • Statické metódy voláme rovnako ako funkcie v C/C++ (pri statickej metóde metoda z inej triedy Trieda pri jej volaní píšeme Trieda.metoda). Kľúčové slovo return sa tiež správa podobne ako v C/C++, avšak program obsahujúci metódu s návratovým typom rôznym od void a chýbajúcim príkazom return nie je možné skompilovať. Rovnako ako v C/C++ funguje aj rekurzia.

Príklad:

public class Trieda {

    static long faktorial(int n) {
        if (n == 0) {
            return 1;
        } else {
            return n * faktorial(n - 1);
        }
    }

    public static void main(String[] args) {
        System.out.println(faktorial(18));
    }

}

Metóda main

  • Špeciálnou statickou metódou je metóda main, ktorá sa vykoná bezprostredne po spustení programu. Tá musí mať návratový typ void, modifikátor prístupu public a jediný argument typu String[] reprezentujúci pole argumentov programu z príkazového riadku (prípadné metódy main s inou hlavičkou sa považujú za „obyčajné” metódy a po spustení programu sa nevykonávajú).
public class Trieda {

    public static void main() {
        System.out.println("Tento text sa nikdy nevypise.");
    }

    public static void main(String[] args) {
        System.out.println("Pocet argumentov: " + args.length);
    }

}
  • Každá trieda, od ktorej po skompilovaní očakávame spustiteľnosť na JVM, musí mať definovanú hlavnú metódu main; často však píšeme aj triedy, ktoré slúžia len na použitie z iných tried.
  • V IDE ako napr. IntelliJ je zvyčajne potrebné vybrať hlavnú triedu projektu, čo je trieda, ktorú sa prostredie bude snažiť spúšťať po spustení celého projektu. Táto trieda tak musí mať definovanú metódu main.

Predávanie argumentov v Jave

Argumenty metód sa v jazyku Java vždy predávajú hodnotou.

  • Pre argumenty sa tedy vždy vytvoria nové lokálne premenné, do ktorých sa skopírujú hodnoty argumentov, s ktorými bola metóda volaná.
  • Nie je teda možné napísať metódu, ktorá pozmení premenné z volajúcej metódy. Ak sú ale argumentmi metódy referencie (polia alebo objekty), je možné pozmeniť hodnoty, na ktoré táto referencia ukazuje (t.j. napríklad zmeniť obsah poľa alebo premenné objektu).

Príklad:

public class Trieda {

    static void f(int n, int[] a) {
        n = 6;
        a[0] = 7;
        a = new int[]{8, 9, 10, 11, 12};
    }

    public static void main(String[] args) {
        int n = 0;
        int[] a = {1, 2, 3, 4, 5};
        f(n, a);
        System.out.println(n);     // Vypise 0
        System.out.println(a[0]);  // Vypise 7
    }

}
  • Špeciálne napríklad v Jave nie je možné napísať funkciu swap, ktorá vymení hodnoty dvoch premenných primitívnych typov.

Práca s reťazcami

Zvyšok tejto prednášky sčasti presahuje jej rámec tým, že začneme pracovať s niektorými špeciálnymi objektmi bez toho, aby sme si vysvetlili mechanizmus objektov vo všeobecnosti. Už pomerne elementárne úkony, ako práca s reťazcami alebo so vstupom a výstupom, sa totiž v Jave realizujú s využitím objektov. Zvyšok tejto prednášky je motivovaný praktickou potrebou zvládnutia týchto úkonov; hlbšie pochopenie nasledujúceho materiálu nadobudneme budúci týždeň.

Trieda String

Reťazce znakov sú v Jave objekty triedy String. Premenné typu String majú teda charakter referencií na samotný objekt. Objekty triedy String sú konštantné reťazce – po vytvorení ich už teda nemožno meniť. Premennú typu String ale samozrejme môžeme meniť tak, že do nej priradíme referenciu na iný objekt typu String.

Trieda StringBuilder

Spracovanie vstupu a výstupu

Knižnica tried jazyka Java

Trieda Math

Trieda Random

Trieda Arrays