Syntaksista ja kääntämisestä

Attention

Tässä kohdassa (ja muuallakin materiaalin alkupäässä) Python- ja C++-ohjelmia vertaillaan toisiinsa. Python on valittu vertailukieleksi, koska useimmat opiskelijat osaavat Pythonia Ohjelmointi 1:n pohjalta. Jos et kuitenkaan osaa Pythonia, vertailukohdissa riittää kiinnittää huomiota C++-osuuksiin.

Tässä osiossa katsotaan ensin, miltä pieni C++-ohjelma näyttää ja minkälaisia yksityiskohtia siihen liittyy. Vertaillaan seuraavia Python- ja C++-ohjelmia, jotka ovat toiminnaltaan täysin identtisiä:

#
#  Versio 1: Pythonilla
#

def main():
    print("Hello world!")
    print("Mitä kuuluu?")

main()
//
//  Versio 2: C++:lla
//

#include <iostream>

using namespace std;

int main() {
    cout << "Hello world!" << endl;
    cout << "Mita kuuluu?"
         << endl;
}

Tutkitaan eroja ja yhtäläisyyksiä ohjelmien välillä.

  • C++-ohjelma käynnistyy aina automaattisesti main-funktiosta.

  • Pythonissa pääohjelmafunktio nimettiin main:ksi vain “synergiasyistä”, koska se on monissa muissakin ohjelmointikielissä nimetty niin. Python-kieli itsessään ei tällaista nimeämiskäytäntöä vaadi, mutta C++ vaatii.

  • Suurin osa C++-käskyistä päättyy puolipisteeseen. Valitettavasti tämä sääntö ei kuitenkaan ole niin kattava, että puolipistettä voisi tunkea aivan joka paikkaan. Tämä on merkittävä virheiden ja harmin lähde, kun aloittelevalle C++-ohjelmoijalle ei vielä ole täysin avautunut, minne puolipiste kuuluu ja minne ei.

  • Koska käskyt päättyvät useimmiten puolipisteeseen, sallii C++ käskyjen jakamisen usealle riville ilman erityisnotaatiota. Python vaati yleensä katkaistun rivin loppuun \-merkin osoittamaan, että käsky jatkuu seuraavalla rivillä.

  • Yhteenkuuluvat ohjelman osat (lohkot) merkitään aaltosulkeiden { } sisään, kun Pythonissa ne esitettiin sisennyksen avulla. Tämä ei kuitenkaan tarkoita sitä, että C++-ohjelma kannattaisi jättää sisentämättä, sillä sisennykset kuitenkin auttavat ohjelmakoodin rakenteen hahmottamista.

  • Kommenttimerkki C++-ohjelmassa on merkkiyhdistelmä //. Pythonissa kommentit ilmaistiin #-merkillä.

  • C++:ssa kieli itsessään ei sisällä tulostuskäskyä, vaan tulostaminen on toteutettu kirjaston avulla. Näytölle tulostaminen ja näppäimistöltä lukeminen vaativat iostream-kirjaston käyttöönoton:

    #include <iostream>
    

    Käytetty #include <...> -rakenne on C++:n vastine Pythonin import-käskylle. Kun halutaan käyttää johonkin toiseen tiedostoon toteutettua ohjelmakoodia, käytetään include-rakennetta. Jos halutaan, että kääntäjä etsii sisällytettävää tiedostoa sieltä, minne C++:n kirjastot on talletettu, kirjoitetaan kirjaston nimi kulmasulkeiden sisään. Jos halutaan etsiä itse toteutettuja tiedostoja, kirjoitetaan tiedoston nimi lainausmerkkien ("") sisään.

    Merkillä # alkavat rivit ovat C++:ssa esiprosessorin direktiivejä. Esiprosessori on kääntäjän osa, joka valmistelee ohjelmakooditiedostoja ennen varsinaista käännöstä. Esiprosessori tekee oikeastaan vain tekstikorvausta, eli #include-direktiivin käytännössä kopioi nimetyn tiedoston - tässä tapauksessa iostream-kirjaston - sisällön include-direktiivin paikalle.

  • Kun #include <iostream> on kirjoitettu ohjelmakooditiedoston alkuun, ohjelmasta voidaan tulostaa tietoa näytölle iostream-kirjastosta löytyvän cout-käskyn ja tulostusoperaattorin << avulla. Yhdellä cout-käskyllä voi tulostaa niin monta tietoalkiota kuin haluaa, kunhan muistaa liittää jokaisen eteen tulostusoperaattorin <<. Jos cout:iin tulostaa endl:n, kursori siirtyy seuraavan rivin alkuun.

  • C++-koodissa on vielä yksi käskyrivi, jota Python-versiossa ei näennäisesti ole:

    using namespace std;
    

    Tämä rivi mahdollistaa sen, että iostream-kirjastosta tarvittuja nimiä cout ja endl voidaan käyttää sellaisenaan, ilman että niiden paikalle olisi aina kirjoitettava std::cout ja std::endl.

    Pythonissa on aivan sama mekanismi, joka vaan ei tullut esimerkissä vastaan, koska siinä ei käytetty kirjastoja. Jos Python-ohjelmassa halutaan käyttää kirjastoja, voidaan kirjoittaa:

    import math
    ...
    math.sqrt(2.0)
    

    Tarvitaan siis kirjaston nimi etuliitteenä, kun halutaan kirjaston tarjoama palvelu käyttöön. Tämä voidaan välttää kirjoittamalla:

    from math import *
    ...
    sqrt(2.0)
    

    Käskyllä using namespace siis mahdollistetaan tunnisteiden käyttäminen jostain nimetystä nimiavaruudesta ilman nimiavaruuden määrittävää etuliitettä. Toisaalta etuliitteetkin voivat olla käteviä. Nimiavaruudet ikään kuin määrittävät joukon tunnisteita ja piilottavat tunnisteet näkymästä nimiavaruuden ulkopuolella, jolloin ohjelmakoodissa voidaan hyvin käyttää samaa tunnistetta useassa paikassa, kunhan ne vain on määritetty eri nimiavaruuksiin, jolloin ei ole epäselvyyttä siitä mitä tunnistetta yritetään käyttää. Esimerkiksi tunniste nimi voisi opintorekisteriohjelmassa olla käytössä sekä opiskelijan tiedoissa että opintojakson tiedoissa. Kun molemmat näistä olisi toteutettu omissa nimiavaruuksissaan, tunnisteisiin päästäisiin käsiksi esim. Opiskelija::nimi ja Opintojakso::nimi.

    Tässä vaiheessa kurssia nimiavaruuksista ei tarvitse sen kummemmin murehtia, vaan riittää hoksata, että jos tunniste kuuluu johonkin muuhun kuin globaaliin nimiavaruuteen, se ei löydy, mikäli nimiavaruutta ei ole kirjoitettu koodiin näkyviin joko using namespace-käskyllä tai kirjoittamalla nimiavaruuden näkyviin tunnisteen eteen tähän tapaan:

    nimiavaruus::tunniste
    

Attention

Mitä suurempia ohjelmia toteutat, sitä tärkeämpää on huolehtia, ettei ohjelmassa ole käytössä “turhia” nimiä. Jos käytössä on useita suuria kirjastoja kaikkine nimineen, alkaa käytettyjen tunnisteiden määrä olla niin suuri, että se voi vaikeuttaa uusien tunnisteiden keksimistä.

Siksi on usein parempi vaihtoehto kirjoittaa nimiavaruudet tunnisteiden eteen tähän tapaan: std::cout ja std::endl kuin sisällyttää koko nimiavaruus using namespace std; -käskyllä ja kirjoittaa pelkästään cout ja endl, olkoonkin, että jälkimmäinen muoto on yksinkertaisempi kirjoittaa.

Ilman using namespace -käskyä edellä ollut ohjelma näyttäisikin seuraavalta:

//
//  Versio 3: Ohjelmointityylillisesti paremmin C++:lla
//

#include <iostream>

int main() {
    std::cout << "Hello world!" << std::endl;
    std::cout << "Mita kuuluu?"
              << std::endl;
}

Vaikka yllä oleva koodi onkin tyylillisesti parempi, tulee helposti käytettyä versiota, jossa lukee using namespace std, sillä Qt generoi tämän rivin automaattisesti.

Tulkkaus vs. kääntäminen

Python on tulkattava ohjelmointikieli ja C++ on käännettävä ohjelmointikieli.

Tulkattavan ohjelmointikielen ohjelmia suoritettaessa jokaisella suorituskerralla tarvitaan erillinen apuohjelma, jota kutsutaan kyseisen ohjelmointikielen tulkiksi (esim. Python-tulkki). Jos tulkkiohjelma poistetaan tai se tuhoutuu, kyseisellä ohjelmointikielellä kirjoitettuja ohjelmia ei voida enää suorittaa ennen kuin tulkki asennetaan uudelleen. Tulkin tehtävänä on muuttaa lähdekoodin käskyjä konekielelle sitä mukaa kun ohjelman suoritus etenee, jotta tietokoneen keskusyksikkö (prosessori) pystyy suorittamaan ne.

Käännettävällä ohjelmointikielellä toteutettujen ohjelmien suorittaminen eroaa tulkattavasta ohjelmasta siinä, että ohjelma täytyy ennen suorittamista kokonaisuudessaan kääntää eli muuntaa konekieliseksi koodiksi. Kääntämiseen tarvitaan apuohjelma, jota kutsutaan ohjelmointikielen kääntäjäksi (esim. C++-kääntäjä).

Käännöksen yhteydessä kääntäjä suorittaa koko lähdekooditiedostolle suuren määrän virhetarkistuksia ja tuottaa siitä kokonaisen konekielisen ohjelman, joka yleensä talletetaan kovalevylle erilliseen tiedostoon (esim. Windows:issa .exe-päätteiseen tiedostoon). Kun käännösprosessi on valmis, kääntäjää ei enää tarvita, koska kovalevylle talletettua konekielistä ohjelmaa voidaan suorittaa yhä uudelleen ilman kääntäjäohjelman apua. Toki, jos lähdekoodiin tehdään muutoksia, se on käännettävä uudelleen konekieliseksi tiedostoksi, jolloin kääntäjää tarvitaan uudelleen.

On olemassa monia ohjelmointikieliä, jotka sijoittuvat jonnekin tulkattavien ja käännettävien kielien välimaastoon. Tällaisilla kielillä kirjoitetut ohjelmat voidaan kääntää nk. välikoodiksi, jota sitten voidaan suorittaa nopeasti ja vähemmillä virhetarkasteluilla.

Jos haluamme olla aivan täsmällisiä, käännös on (ainakin) kaksivaiheinen. Ensin ohjelma käännetään niin sanotuksi objektitiedostoksi. Tässä muodossa kirjoitettu ohjelma on jo konekoodia, mutta sitä ei voi vielä suorittaa tietokoneella. Syy on se, että lopulliseen ohjelmaan tarvitaan osia myös kääntäjän kirjastosta, jonka takia ohjelma pitää vielä linkittää.

Linkityksessä ohjelman erikseen käännetyt osat liitetään yhteen ja niistä muodostetaan yksi ajettava (executable) ohjelma. Kääntäjät yleensä kätkevät ohjelmoijalta linkitysvaiheen, mutta suurissa ohjelmistossa käännös on tehtävä kahtena vaiheena, jotta koko ohjelmaa ei tarvitsisi kääntää jokaisen pienen muutoksen jälkeen. Hyöty on melkoinen, kun ohjelmiston koko ylittää 100000 tai miljoona riviä, mutta havaittavissa se on jo paljon pienemmilläkin ohjelmilla.