Yleisesti ottaen on turvallisempaa käyttää const-vakioita verrattuna #define-makrot. const-vakioiden etuja:
Esimerkki:
#define PLUS 1+1 #define EXT 2 + PLUS /* viittaus edelliseen makroon */ const int plus = 1+1; // ... int a = PLUS * 2; // 1+1*2 = 3 int b = plus * 2; // (1+1)*2 = 4 cout << EXT; // tulostaa 3
Lisätietoja: Ohjelmointi++ luentomoniste, luku 7.5 ja B. Stroustrupin C++ -ohjelmointi, luku 7.8.
Lähes ainoat tilanteet, joissa makroja joudutaan käyttämään ovat yhteensopivuuden säilyttäminen olemassaolevan C-koodin kanssa (C-kielessä ei ole globaaleja const-vakioita) ja makroilla annettavat ohjeet esikääntäjälle projekteissa, jotka sisältävät useita cpp- ja h-tiedostoja (määritellään tarkemmin ohjelmointi++ -kurssilla).
Ohjelmia kirjoitettaessa tulee usein eteen tilanne, jossa kutsuttava funktio vaatii tiettyä tietotyyppiä olevan argumentin - joka ei ole sama kuin ohjelmakoodissa muuten käytettävä. Tällöin tarvitaan muunnosoperaatioita.
Perustyyppejä varten kieli tarjoaa automaattisen tyypin muunnoksen (Ks. moniste, luku 2.2.9. Automaattinen tyypin muunnos takaa, että laskuoperaatioissa olevat erityypiset parametrit ja tulos konvertoidaan eniten tilaa vievään tyyppiin. Konversio suoritetaan lausekkeiden, sijoituslauseiden ja funktiokutsujen yhteydessä tarvittaessa.
Käytettäessä samassa ohjelmassa C- ja C++ -kielen funktioita tai muita aliohjelmakirjastoja (esim. Borlandin tai Microsoftin Windows-kirjastot) tarvitaan muunnosta C-merkkijonojen (char [] tai const char *) ja C++ -merkkijonojen (string) välillä. Muunnos käy helposti stringin metodeita käyttäen:
char c1[] = "Testi"; // C-merkkijonoja
char c2[] = "Toinen";
string s(c1); // Vaihtoehtoisesti myös string s = c1;
s = c2; // sijoitus C-merkkijonosta string-merkkijonoon.
const char * c = s.c_str(); // muunnos stringistä C-vakiomerkkijonoon
printf("%s\n",c); // tulostetaan merkkijono C-tyylillä
Tilanteessa, jossa vaaditaan muunnosta stringistä tavalliseksi C-merkkijonoksi (ei vakio) tarvitaan ylimääräinen merkkijono, johon vakiomerkkijono kopioidaan. Tästä esimerkki conststr.cpp. Toinen (vähemmän turvallinen) tapa on käyttää tyypinmuunnosoperaattoria (ks. alla).
Muunnoksia merkkijonoista perustyyppeihin ja takaisin on melko helppoa tehdä käyttäen C-kielen funktioita atoi (C-merkkijono -> integer), itoa (integer -> C-merkkijono) ja atof (C-merkkijono -> double). Muunnoksissa voi käyttää myös sscanf:ia (C-merkkijonosta perustyyppeihin) tai sprintf:ää (formatoitu tulostus C-merkkijonoon, muuten kuten printf). Esimerkkejä: str2int.cpp ja int2str.cpp
Yleinen C-kielen muunnosoperaattori (engl. typecaston muotoa
(tyyppi)(muuttuja)
Tyypinmuunnosoperaattorilla voidaan muuttaa 'väkisin' muistissa olevan tiedon tulkintatapaa. Tämä voi johtaa vaaratilanteisiin, jos muunnettava tyyppi ei ole 'yhteensopiva' alkuperäisen tyypin kanssa (Ajattele esim. pakotettua muunnosta kokonaisluvusta C-merkkijonoksi). Siksi tyypinmuunnoksia pitäisi yleensä välttää (Taas yksi esimerkki siitä, miten C antaa ohjelmoijalle 'vapaat kädet'). Esimerkki tyyppinmuunnoksesta järkevässä käytössä:
char c = 'a'; cout << c; // tulostaa a int i = (int)(c); cout << i; // tulostaa a:n ascii-koodin 97
C++ -kielen uuden standardin turvallisempi vastine muunnosoperaattorille on static_cast-operaattori (joka ei valitettavasti toimi Borland C++ 3.1:ssä). Se toimii kuten C-kielen muunnosoperaattori, mutta estää esim. muunnokset vakiotyypeistä osoittimiin. Edellinen esimerkki static_cast-operaattorilla:
char c = 'a'; cout << c; // tulostaa a int i = static_cast<int>(c); cout << i; // tulostaa a:n ascii-koodin 97
Const-tyypinmuunnoksia varten uuteen standardiin on määritelty myös operaattori const_cast. Se sallii vain samantyyppisen tiedon muuntamisen const-muodosta muokattavaan muotoon. Edellistä merkkojonoesimerkkiä soveltaen:
void toupper(char c[], int length);
{ // pääohjelma...
string s="Testi";
char* c;
c = const_cast<char*>(s.c_str()); // muunnetaan c-vakiomerkkijono tavalliseksi c-merkkijonoksi
// edellinen c-tyylillä: c = (char*)(s.c_str());
toupper(c,s.length());
cout << c; // tulostaa TESTI
cout << s; // tulostaa TESTI. c-merkkijonon muokkaaminen muutti myös s:n sisältöä!
}
Huomaa, että edellisen esimerkin c-merkkijonolle on varattu muistitilaa vain niin kauan kuin vastaavalle string-merkkijonolle (eli lohkon loppuun asti). Siksi suorien const-muunnosten käyttö ei ole suositeltavaa. Turvallisempaa (mutta myös hitaampaa) on kopioida koko merkkijono kuten esimerkissä conststr.cpp
Monisteessa esitettiin tapa lukea tiedostoa rivi kerrallaan:
ifstream syotto("syotto.txt");
// avaamisen onnistumisen tarkastaminen...
string rivi;
getline(syotto,rivi);
while(!syotto.eof()) {
cout << rivi << endl;
getline(syotto,rivi);
}
Saman asian saa tehtyä yksinkertaisemmin - ilman eof-bitin tutkimista - käyttäen getlinea suoraan silmukan ehtona. getline palauttaa epätoden, kun tiedosto on lopussa ("virtaa ei ole").
ifstream syotto("syotto.txt");
// avaamisen onnistumisen tarkastaminen...
string rivi;
while(getline(syotto,rivi)) {
cout << rivi << endl;
}
Jos tiedoston muoto tiedetään ja se on järjestetty rivi kerrallaan, tiedosto on helppo lukea käyttäen getlinea ja sscanf-funktiota. Esimerkki: henktie3.cpp.
Vaikka osoittimet eivät varsinaisesti kuulu kurssin vaatimuksiin, niiden ymmärtäminen helpottaa taulukoiden ja C-merkkijonojen käyttöä. Lisäksi osoittimilla ja referensseillä on läheinen yhteys.
Osoitin on tietotyyppi, joka sisältää muistiosoitteen (osoittimen arvo). Muistiosoitteessa olevaa arvoa (osoittimen pää) käsitellään osoittimen tyypin mukaisesti. Osoitin esitellään seuraavasti:
tyyppi* nimi;
tyyppi on mikä tahansa C++:n tietotyyppi ja nimi on muuttujan nimi. * on merkkinä siitä, että tyyppi on osoitin.
Osoitinten kanssa käytetään aina seuraavia operaattoreita:
int* a; // esitellään osoitin kokonaislukuun int b=10; a = &b; // otetaan b-muuttujan osoite (&-operaattori). cout << *a; // haetaan a-osoittimen päässä oleva arvo (*-operaattori).
Huomaa, että &- ja *-merkeillä on eri merkitys riippuen niiden paikasta koodissa. Esittelyssä & tarkoittaa referenssiä ja * osoitinta, muuten & hakee muuttujan osoitteen ja * hakee osoittimen päässä olevan arvon ("tähdätään osoitinta pitkin").
Osoitinta voidaan käyttää referenssien tapaan funktion parametreina varmistamaan, että funktiossa tehdyt muutokset näkyvät pääohjelmaan. Sisäisesti mekanismi on identtinen referenssien käytön kanssa, mutta syntaksi eroaa. Osoitinargumentteja käytetään esim. C:n sscanf-funktiossa. Huomaa, että C-funktioissa osoitinargumentit olivat ainoa tapa tuoda funktioon 'muuttuvaa tietoa' argumenttilistassa.
// vaihda-funktio referensseillä
void vaihda(int& a,int& b) {
int c=a;
a=b;
b=c;
}
// ... pääohjelma
int a=1,b=2;
vaihda(a,b);
// vaihda-funktio osoittimilla
void vaihda(int* a,int* b) {
int c=*a; // käsitellään osoittimen päässä olevia arvoja
*a=*b;
*b=c;
}
// ... pääohjelma
int a=1,b=2;
vaihda(&a,&b); // lähetetään funktiolle muuttujien osoitteet
Osoittimen ja referenssin ero on syntaksin lisäksi osoittimen joustavuudessa. Referenssi viittaa aina yhteen ja samaan muuttujaan, mutta osoittimen voi 'ohjata' osoittamaan uuteen muuttujaan (tai jopa tyhjään, ns. 0-osoittimeksi).
int a=1,b=2; int& ref_a = a; // ref_a-viite viittaa a-muuttujaan. int* ptr_a = &a; // ptr_a osoittaa a-muuttujaan. cout << *a; // tulostaa 1 ptr_a = &b; cout << *b; // tulostaa 2 ptr_a = 0; // tyhjä osoitin; cout << *a tuottaisi access violationin...
C-kielen taulukon nimi on osoitin taulukon alkuun. Lisäksi osoittimen paikkaa voi vaihtaa taulukon sisällä:
int c[10]={0,1,2,3,4,5,6,7,8,9};
int* ptr_c1 = c; // osoitin taulukon alkuun
int* ptr_c2 = &(c[0]) // sama kuin ptr_c1
ptr_c1 = c+2; // osoitin taulukon kolmanteen alkioon
cout << *ptr_c1; // tulostaa 2
Tämä selittää, miksi C-kielen merkkijonoja merkitään kahdella tavalla: char [] ja char *. Niiden merkitykset ovat identtiset, kun ne ovat funktion argumentteina:
void char_func(char []c); // samat void char_func(char *c);
Sen sijaan koodilohkon keskellä taulukko- ja osoitinmerkinnät eivät merkitse samaa! Taulukon esittely varaa taulukolle muistitilaa, mutta osoittimen esittely varaa tilaa vain osoittimelle. Osoitinten kanssa ohjelmoitaessa on aina oltava tarkkana, että osoittimen päässä todella on ohjelmalle varattua muistitilaa (suurin osa ohjelman muistivirheistä johtuu nimenomaan virheellisestä osoittimien käytöstä! Osoittimien käyttö on taas yksi esimerkki "vaarallisesta", mutta potentiaalisesti tehokkaasta ohjelmointitavasta). Esimerkki:
{ ...
int* i;
*i = 10; // VIRHE!!! sijoitetaan i-osoittimen päähän 10,
// vaikka i:n päässä ei ole varattua muistia.
... }
Korjattu versio:
{ ...
int* i;
int j;
i = &j; // sijoitetaan i-osoittimeen j:n muistipaikka.
*i = 10; // sijoitetaan i-osoittimen päähän (eli j-muuttujaan) 10.
... }
Kehittyneempiä tietorakenteita ja mielivaltaisen suuria muistialueita varattaessa osoitinten käyttö on välttämätöntä. Yleistietoa osoittimista mm. Ohjelmoinnin kesäkurssin Osoitinkyselyn yhteenvedossa.