Muuttujat ja tyypitys

C++:ssa muuttujien käsittelemiseen liittyy paljon enemmän yksityiskohtia kuin Pythonissa. Ei kuitenkaan masennuta, vaikka saattaa tuntua, että alkuun pääseminen on hidasta.

Python vs. C++: muuttujat ja syötteet

Vertaillaan seuraavia ohjelmia (jotka myös toimivat identtisesti):

def main():
    nimi = input("Syötä nimesi: ")
    ikä = int(input("Syötä ikäsi: "))

    print("Hauska tavata", nimi, ikä, "v.")
    print(50 - ikä, "vuoden päästä olet 50 vuotias.")

main()
#include <iostream>
#include <string>

using namespace std;

int main() {
    string nimi = "";
    cout << "Syota nimesi: ";
    getline(cin, nimi);

    int ika = 0;
    cout << "Syota ikasi: ";
    cin >> ika;

    cout << "Hauska tavata " << nimi << " "
         << ika << " v." << endl;
    cout << 50 - ika
         << " vuoden paasta olet 50 vuotias."
         << endl;
}

Huomioita ja johtopäätelmiä:

  • C++-ohjelma on usein pidempi kuin vastaava Python-ohjelma. Yksi esimerkki ei tätä osoita, mutta myös kokemukset puhuvat tämän puolesta.

  • Pythonissa muuttujia saa käyttöönsä antamalla oliolle uuden nimen =-käskyn avulla. Python ei ole nirso siitä, minkä tyyppistä tietoa muuttujaan talletetaan ja tyyppi saattaa vaihdella ohjelman suorituksen kuluessa.

    C++:ssa muuttuja täytyy aina ensin esitellä ennen kuin sitä voi käyttää. Esittely kertoo C++-kääntäjälle, minkä nimistä muuttujaa ohjelmoija haluaisi jatkossa käyttää ja minkä tyyppistä tietoa siihen olisi tarkoitus tallettaa.

  • Pythonissa muuttujien ja funktioiden nimissä oli mahdollista käyttää skandinaavisia kirjaimia, C++:ssa tämä ei ole mahdollista. Siksi C++-esimerkkikoodissa Python-ohjelman muuttuja ikä oli jouduttu nimeämään uudelleen muuttujaksi ika. C++:ssa muuttujien ja funktioiden nimissä voi esiintyää ASCII-kirjaimia, numeroita ja alaviivoja (_). Ensimmäisen merkin pitää olla kirjain.

  • Merkkijonoissa skandinaavisia kirjaimia sen sijaan voi käyttää myös C++:ssa. Kokemus on kuitenkin osoittanut, että siitä voi seurata jossain vaiheessa ongelmia, joten niiden käytön vältteleminen on ihan hyvä käytäntö.

  • Toisin kuin Pythonissa, merkkijonotietotyyppi string ei ole C++:ssa osa kieltä itseään, vaan se on toteutettu kirjastona. Jos ohjelmassa siis haluaa käsitellä merkkijonoja, #include <string> koodin alussa on välttämätön.

  • Toisin kuin Pythonin print-funktio, cout ei lisää automaattisesti välilyöntiä tulostamiensa tietoalkioiden väliin. Se ei myöskään tee rivinvaihtoa, ellei eksplisiittisesti kirjoita endl.

  • Muuttujat cin ja cout ovat tietovirtoja, joka on C++:n tapa mahdollistaa näppäimistöltä tulevan syötteen ja konsolitulosteen käsittely ohjelmakoodissa (syöttö- ja tulostusoperaatiot).

  • Alkeelliset tavat lukea syötteitä cin-tietovirrasta ovat getline-funktio, jolla voi lukea merkkijonoja, ja syöttöoperaattori >>, jolla voi lukea oikeantyyppisen arvon jollekin muuttujalle.

  • Huomaa, että Pythonin input-funktio ja C++:n getline-funktio toimivat hyvin erityylisesti. Pythonin input ottaa parametrina tulostettavan merkkijonon ja funktiokutsun paluuarvoksi evaluoituu luettu merkkijono. Sen sijaan getline ottaa parametrina tietovirran (kurssin alkupuolella aina cin) ja muuttujan, johon syöte luetaan. Huomaa myös, että getline-funktion paluuarvoa ei voida sijoittaa merkkijonomuuttujaan.

  • Syöttöoperaattori >> ohittaa kaikki tyhjät merkit, myös rivinvaihdot. Tämä tarkoittaa, että syötettä luetaan, kunnes käyttäjä syöttää jotain näkyviä merkkejä. Kokeile ohjelman toimintaa siten, että ikää kysyttäessä syötät ensin muutamia rivinvaihtoja ja sitten vasta luvun. Tämä saattaa tuntua kummalliselta, kun Pythonissa olemme tottuneet lukemaan syötettä rivi kerrallaan input-funktiolla.

  • Näillä pääsemme alkuun. Myöhemmässä vaiheessa perehdymme tietovirtojen käsittelemiseen perusteellisemmin ja opettelemme myös syötteiden oikeellisuuden tarkastelemista (jos syöttöoperaattorilla >> luettaessa käyttäjä syöttää vääräntyyppisen arvon).

Muuttujat C++:ssa

C++:ssa muuttuja on määriteltävä (tai ainakin esiteltävä), ennen kuin sitä voi käyttää. Muuttujan määrittely muodostuu kahdesta osasta: esittelystä ja alustuksesta. Esittely jakautuu niin ikään kahteen osaan, joten kokonaisuudessaan muuttujan määrittelyssä on kolme osaa:

  1. muuttujan tietotyyppi, eli siihen talletettavan tiedon tyyppi
  2. muuttujan nimi
  3. alkuarvon antaminen muuttujalle eli muuttujan alustaminen.

Vaiheet 1 ja 2 (eli muuttujan esittely) on pakollista. Esimerkkejä muuttujan määrittelystä:

// Määrittely ilman alustusta eli esitteleminen
int ika;
double lampotila;
string nimi;

// Määrittely kokonaisuudessaan eli esittely ja alustus
int ika = 21;
double lampotila = 16.7;
string nimi = "Teppo";

Esitellyn, mutta alustamattoman muuttujan arvo on epämääräinen, kunnes arvo myöhemmin asetetaan jollain tavoin (sijoitusoperaattorilla = tai syöttöoperaattorilla cin >>):

int ika;
double lampotila;
// Tässä kohdassa seka ika- etta lampotila muuttujien arvo on epämääräinen

ika = 21;
cin >> lampotila;
// Nyt kummallakin on määrätty arvo

Varoitus

Alustamattomat muuttujat ovat yksi syy vaikeasti jäljitettäville ohjelmointivirheille. On hyvää ohjelmointityyliä alustaa muuttuja aina määrittelyn yhteydessä.

C++ on tarkka muuttujan tietotyypistä: muuttujaan ei voi tallettaa muuta kuin sen määrittelyn yhteydessä ilmoitettua tietotyyppiä vastaavaa tietoa.

Muuttujan näkyvyysalue

Lohkon (siis aaltosulkeiden {}) sisällä määritelty paikallinen muuttuja on käytettävissä määrittelykohdastaan lohkon loppusulkuun saakka:

int main() {
    int osallistujia = 0;
    while ( osallistujia < 100 ) {
        int vapaita_paikkoja = 100 - osallistujia;
        ... rivejä poistettu ...
        // Tämä on viimeinen kohta, jossa muuttujaa vapaita_paikkoja voi käyttää.
    }
    ... rivejä poistettu ...
    // Tämä  on viimeinen kohta, jossa muuttujaa osallistujia voi käyttää.
}

Tietotyyppejä

Tietotyypit, joilla C++:ssa pärjää yksinkertaisissa ohjelmissa pitkälle, ovat  int,  double,  bool  ja  string. C++:n double vastaa Pythonin float-tyyppiä (desimaaliluku).

Lisäksi C++:ssa on tietotyyppi char, jonka avulla voidaan käsitellä yhtä 8-bittistä merkkiä. Esimerkiksi seuraava on mahdollista:

char sukupuoli = 'N';

Literaalit ja vakiot

Termi literaali tarkoittaa nimetöntä vakioarvoa ohjelmakoodissa, esimerkiksi: 42,   1.4142,   'x',   "teksti" tai true.

Huomaa seuraava merkittävä ero Pythonin ja C++:n välillä:

  • Pythonissa ei ole erillistä tietotyyppiä merkkien käsittelyyn vaan merkki esitetään merkkijonona, jossa on vain yksi merkki.
  • Merkkijonoliteraalit saa Pythonissa kirjoittaa joko lainaus- tai heittomerkkien sisään. C++:ssa sen sijaan merkkijonoliteraalit (eli string-tyyppiset literaalit) esitetään lainausmerkkien ("abc") sisällä ja heittomerkkien ('x') sisällä pitää olla vain yksi merkki ja kyseinen literaali on tyypiltään char.
  • Siispä esimerkiksi ´X´ on merkki ja "X" on yhden pituinen merkkijonoliteraali, eikä merkkijonoa "X" voi tallentaa muuttujaan, jonka tietotyypiksi on määritelty char.

C++:ssa muuttujiin liittyy vielä yksi lisäominaisuus, jota vastaavaa Pythonissa ei ole laisinkaan: vakiomuuttujat, jotka ilmaistaan lisäämällä apusana const muuttujan määrittelyn alkuun. Ne ovat ikään kuin muuttujia, mutta niiden arvoa ei voi muuttaa määrittelyn jälkeen.

const double PII = 3.14;

... rivejä poistettu ...

// Yritys muuttaa const-vakion arvoa myöhemmin tuottaa virheilmoituksen:
PII = 3.141592653589793; // VIRHE

Vakiot on tarkoitettu sellaisten arvojen nimeämiseen, joiden ei ole tarkoitus muuttua. Luonnonvakiot kuten pii ovat suoraviivaisimpia esimerkkejä, mutta vakioille löytyy muitakin käyttötarkoituksia:

const int LOTTOPALLOJA = 7;

Koska vakion arvoa ei voi muuttaa määrittelyn jälkeen, on aika loogista, että se on alustettava esittelyn yhteydessä.

Ohjelmointityylillisesti vakiot nimetään usein pelkillä isoilla kirjaimilla ja käyttäen sanojen erottimena _-merkkiä. Hyvin valitut vakioiden nimet selkeyttävät ohjelmakoodia.

Dynaaminen vs. staattinen tyypitys

Miten seuraava Python-ohjelma käyttäytyy?

def main():
    print("Alku")
    arvo1 = 5.3
    arvo2 = 7.1
    print(arvo1 + arvo2)
    arvo2 = "Hei"  # Vanhan muuttujan uudelleenkäyttö
    print("Keskiväli")
    print(arvo1 + arvo2)
    print("Loppu")

main()

Kun ohjelma suoritetaan, kaikki toimii normaalisti, kunnes rivillä 8 ohjelman suoritus päättyy virheeseen:

Alku
12.399999999999999
Keskiväli
Traceback (most recent call last):
  File ``koodit/osa-01-04.py'', line 11, in <module>
    main()
  File ``koodit/osa-01-04.py'', line 8, in main
    print(arvo1 + arvo2)
TypeError: unsupported operand type(s) for +:
  'float' and 'str'

Käyttäytyminen johtuu siitä, että Pythonissa on käytössä nk. dynaaminen tyypitys, mikä tarkoittaa sitä, että käsiteltävänä olevan tietoalkion tietotyypin sopivuus tarkastetaan vasta sillä hetkellä, kun tietoalkiota yritetään käyttää.

Käytännössä dynaaminen tyypitys ilmenee siten, että:

  • Sama muuttuja voi ohjelman suorituksen aikana esittää eri tyyppisiä arvoja.
  • Ohjelma toimii normaalisti siihen saakka, kunnes koodissa yritetään käsitellä väärän tyyppistä tietoa väärässä paikassa.

Dynaaminen tyypitys on hyvin yleinen tulkattavissa ohjelmointikielissä.

Vastaava koodi C++:lla näyttäisi seuraavalta:

#include <iostream>
#include <string>

using namespace std;

int main() {
    cout << "Alku" << endl;
    double arvo1 = 5.3;
    double arvo2 = 7.1;
    cout << arvo1 + arvo2 << endl;
    string arvo3 = "Hei";  // Uusi muuttuja
    cout << "Keskivali" << endl;
    cout << arvo1 + arvo3 << endl;
    cout << "Loppu" << endl;
}

Jos tämä ohjelmakoodi yritetään kääntää, saadaan virheilmoitus ja käännös jää kesken (virheilmoitusta yksinkertaistettu):

main.cpp:13: error: no match for 'operator+'
             cout << arvo1 + arvo3 << endl;

C++:ssa on käytössä staattinen tyypitys; jos ohjelmakoodissa yritetään käsitellä tietoa tavalla, jota C++ ei ymmärrä tai salli, tuloksena on virhe kääntäjältä.

Ohjelmoijalle staattinen tyypitys näkyy käytännössä seuraavasti:

  • Muuttujat on määriteltävä ennen käyttöä.
  • Muuttujilla on kiinteästi määrätty tietotyyppi, eikä muuttujiin voi sijoittaa kuin niiden tietotyyppiä vastaavaa tietoa.
  • Jos ohjelmassa on tiedon tyyppeihin liittyvä virhe, ohjelmaa ei voi kääntää konekielelle, eikä siis myöskään suorittaa tai testata.

Staattinen tyypitys on hyvin yleinen käännettävissä ohjelmointikielissä.

Huomaa, kuinka C++-versiossa on rivillä 11 jouduttu määrittelemään uusi string-tyyppinen muuttuja arvo3, koska muuttujaan arvo2 ei voi tallettaa merkkijonoa kuten Pythonissa pystyi tekemään.

Dynaamisesti tyypitettyjen kielien etuna on se, että ohjelmoija pääsee pienemmällä työmäärällä koodia kirjoittaessaan, kun kaikkea tyypitykseen liittyvää (muuttujien määrittely jne.) ei tarvitse mikromanageroida. Monet ohjelmoijat myös pitävät siitä, että muuttuja voi viitata juuri sen tyyppiseen arvoon, kun kulloisellakin hetkellä on tarpeen.

Dynaamisesti tyypitettyjen kielien taakkana taas on se, että ohjelmaan jää helpommin virheitä, aivan kuten esimerkkiohjelmassa tapahtui, kun yritettiin laskea yhteen reaaliluku ja merkkijono. Lisäksi, saman muuttujan käyttäminen useaan eri tarkoitukseen ohjelmakoodissa ei edesauta ohjelman ymmärrettävyyttä.

Tyyppimuunnos

Tyyppimuunnoksessa kääntäjä tulkitsee muuttujan sisältämää dataa erityyppisenä, kuin miksi se on määritelty. Tyyppimuunnos voi olla joko implisiittinen tai eksplisiittinen.

Implisiittisen tyyppimuunnoksen kääntäjä osaa tehdä automaattisesti. Esimerkiksi yksöistarkkuuden liukuluvun float kääntäjä osaa muuntaa automaattisesti kaksoistarkkuuden liukuluvuksi double:

float f = 0.123;
double d = f;

Tai kokonaisluvun int kääntäjä osaa automaattisesti muuntaa liukuluvuksi double:

double d = 3;

Myös kokonaisluvun kääntäjä osaa muuntaa automaattisesti merkkimuuttujaksi:

char merkki = 113;  // merkki on 'q', jonka ASCII-arvo on 113.

Eksplisiittistä tyyppimuunnosta tarvitaan, kun ohjelmoija haluaa itse määrätä, miten kääntäjän pitää tulkita muuttujan sisältöä toisentyyppisenä. Tällöin ohjelmoija suorittaa eksplisiittisen tyyppimuunnoksen operaattorin static_cast avulla.

Esimerkiksi etumerkittömän ja etumerkillisen kokonaisluvun vertaileminen on kääntäjästä epäilyttävää, joten kääntäjä antaa siitä varoituksen:

int i = 0;
unsigned int ui = 0;
...
if( ui == i ) {  // Tältä riviltä tulee varoitus
   ...
}

Jos ohjelmoija on täysin varma, että haluaa kyseisessä tilanteessa kääntäjän tulkitsevan etumerkillistä kokonaislukua etumerkittömänä kokonaislukuna, hän voi tehdä tyyppimuunnoksen eksplisiittisesti ja varoitus poistuu:

if( ui == static_cast< unsigned int >( i ) ) {
   ...
}

Seuraavassa esimerkissä puolestaan halutaan erityisesti korostaa ohjelmakoodin lukijalle, että char-tyyppi tulkitaan kokonaislukuna:

cout << "Syötä jokin merkki: ";
char merkki = ' ';
cin >> merkki;
int merkin_ascii_arvo = static_cast< int >( merkki );
cout << "Merkin " << merkki << " ASCII-arvo on " << merkin_ascii_arvo << endl;