|
|
|
| Sekalaisia C++ asioita |
| Tässä
luvussa tarkastellaan erilaisia tärkeitä pienempiä
asiakokonaisuuksia. Tämä teksti ei ole ollut mikään manuaali,
sisältäen kaikkea, pienintä yksityiskohtaa myöden. Mikäli
tarvitset lisätietoja, kääntäjäsi referenssimanuaalit lienevät
seuraava tiedonetsintäpaikka. |
| Staattiset luokan jäsenet |
| Luokan
jäsenmuuttuja voidaan määritellä staattisesti static
avainsanan avulla. (Myös jäsenfunktio voidaan määritellä
staattiseksi, mutta tällainen määrittely ei ole lainkaan
tavallista, joten asiaa ei käsitellä tässä) Staattisten jäsenmuuttujien
avulla voidaan selvittää useita melko hankaliakin tilanteita.
Kun jäsenmuuttuja
määritellään staattiseksi, kyseisestä muuttujasta syntyy vain
yksi kopio riippumatta siitä kuinka monta oliota lyseiseen luokkaan
luodaan. Kaikki luokan oliot jakavat yhteisen staattisen muuttujan.
Staattinen jäsenmuuttuja
on olemassa jo ennen kuin yhtään oliota on luotu kyseiseen
luokkaan. Saattinen jäsenmuuttuja on itseasiassa globaali muuttuja,
jonka voimassaoloalue on rajattu siihen luokkaan, johon se on määritelty.
Staattista muuttujaa voidaan jopa käsitellä ilman varsinaista
oliota.
Kun staattinen
muuttuja esitellään luokassa, kyseessä ei ole määrittely vaan
esittely, jolloin varsinainen tilavaraus ja määrittely on tehtävä
muualla kuin luokan sisällä. Määrittely on itseasiassa vain
uudelleen esittely, jossa kerrotaan myös mihin luokkaan ko.
muuttuja kuuluu.
Oletusarvoisesti
saattiset jäsenmuuttujat on alustettu 0:ksi. Muuttujalle voidaan
toki antaa määrittelyn yhteydessä alkuarvo.
HUOM: Useimmissa C++:n toteutuksissa, jos staattisen jäsenmuuttujan
alkuarvo jätetään 0:ksi, määrittelyä ei tarvita lainkaan,
Kuitenkin C++ spesifikaation mukaan näin ei tule menetellä. Lienee
siis turvallisinta käyttää myös määrittelyosaa, vaikkakaan kääntäjäsi
ei sitä vaatisi. (Borlandin C++ 4.x vaatii myös määrittelyn).
Staattisten jäsenmuuttujien
tarkoituksena on poistaa tarve varsinaisten globaalien muuttujien käytöltä.
Luokat, jotka nojaavat globaaleihin muuttujiin, rikkovat lähes aina
kapselointi periaatetta. Kapselointi on OOP:n perusperiaatteita.
#include <iostream.h>
class myClass {
static int i; // esittely
public:
void seti(int n) { i = n; }
int geti() { return i; }
};
int myClass::i; // määrittely
main()
{
myClass o1, o2;
o1.seti(10);
cout << "o1.i: " << o1.geti() << '\n'; // näyttää 10:n
cout << "o2.i: " << o2.geti() << '\n'; // näyttää saman 10:n
return 0;
}
Koska staattinen
jäsenmuuttuja on olemassa, ennenkuin yhtään oliota on luotu
luokkaan, muuttujaan pääsee käsiksi ilman oliota. Seuraavassa
edellisen esimerkin muunnos. Huomaa kuinka i on nyt jouduttu
esittelemään public-puolella.
#include <iostream.h>
class myClass {
public:
static int i; // esittely
void seti(int n) { i = n; }
int geti() { return i; }
};
int myClass::i; // määrittely
main()
{
myClass o1, o2;
myClass::i = 100; // asetetaan suoraan arvoonsa ilman oliota
cout << "o1.i: " << o1.geti() << '\n'; // näyttää 100:n
cout << "o2.i: " << o2.geti() << '\n'; // näyttää saman 100:n
cout << "suoraan: " << myClass::i << '\n'; // näyttää saman 100:n
return 0;
}
Staattisen
muuttujan eräs tavallinen käyttötapa on käyttää sitä yhteisen
resurssin käytön koordinointiin (printteri, levy, verkkopalvelin).
Seuraava ohjelma luo output-nimisen luokan, joka
ylläpitää yhteistä tulostuspuskuria nimeltään outbuf.
Outbuf on staattinen merkkitaulukko. Puskuria käytetään
vastaanottamaan putbuf()-jäsenfunktion lähettämää
dataa. Funktio lähettää str:n sisältämän
datan merkki kerrallaan. Merkit lähetetään, mikäli puskuri on
vapaa. Merkkien lähetyksen ajaksi puskuri lukitaan muilta käyttäjiltä.
#include <iostream.h>
#include <string.h>
class output {
static char outbuf[255]; // yhteinen resurssi
static int inuse; // 0: puskuri vapaa, muuten varattu
static int oindex; // puskurin indeksi
char str[80];
int i; // seuraavan merkin indeksi str:ssä
int who; // olion tunniste ( >0 )
public:
output( int w, char *s ) { strcpy( str, s ); i = 0; who = w; }
void show() { cout << outbuf << '\n'; }
int putbuf()
{
// funktio palauttaa 0:n, jos tulostus onnistui
// palauttaa who:n, jos puskuri itsellä käytössä
// palauttaa -1, jos puskuri toisella käytössä
if (!str[i]) { // tulostus loppu
inuse = 0; // puskurin vapautus
return 0; // tulostus onnistui
}
if (!inuse) inuse = who; // otetaan puskuri haltuun
if (inuse != who) return -1; // puskuri toisen käytössä
if (str[i]) { // vielä tulostettavaa
outbuf[ oindex ] = str[ i ];
i++; oindex++;
outbuf[ oindex ] = '\0';
return who;
}
}
};
char output::outbuf[255];
int output::inuse = 0;
int output::oindex = 0;
main()
{
int status;
output o1( 1, "This is" );
output o2( 2, " a test" );
do {
status = o1.putbuf();
if (status==0)
status = o2.putbuf();
o1.show();
} while ( status );
return 0;
}
|
| Taulukkopohjainen
I/O |
|
Konsoli- ja
tiedosto-I/O:n lisäksi C++ tukee joukkoa toimintoja, jotka käyttävät
merkkitaulukkoja syöttö- tai tulostuslaitteina. C++:n
taulukkopohjainen I/O vastaa C:n taulukkopohjaista I/O:ta, lähinnä
sscanf()- ja sprintf()-funktioita. C++:ssa asia hoidetaan vaan
joustavammin ja se käyttökelpoisempaa, koska käyttäjän määrittelemät
tyypit ovat integroitavissa taulukkopohjaiseen I/O:hon.
Taulukkopohjainen
I/O toimii streamien välityksellä kuten konsoli- ja
tiodosto-I/O:kin. Itseasiassa kaikki mikä aiemmin opittiin I/O:sta
on edelleen käytettävissä, tarvitaan vain muutama lisäfunktio ja
taulukkopohjainen I/O on käytettävissä. Näillä funktioilla
stream kytketään muistialueeseen.
Taulukkopohjainen
I/O vaatii strstream.h tiedoston sisällyttämisen
ohjelmaan. Tämä tiedosto sisältää seuraavien luokkien määrittelyt:
istrstream, ostrstream ja strstream.
Nämä luokat luovat taulukkopohjaiset syöttö, tulostus ja syöttö/tulostus
virrat. Luokat on johdettu ios-luokasta, niinpä
kaikki funktiot ja manipulaattorit, jotka löytyvät istream:sta,
ostream:sta ja iostream:sta ovat käytettävissä istrstream:ssa,
ostrstream:ssa ja strstream:ssa
Kun käytetään
taulukkopohjaista tulostusta, käytetään seuraavaa ostrstream:n
konstruktoria:
ostrstream ostr( char *buf, int size, int mode = ios::out );
ostr on virta,
joka kytketään puskuriin buf. size kertoo puskurin koon.
Tavallisesti mode:sta käytetään oletusarvoa: ios::out, mutta
modessa voidaan käyttää mitä tahansa ios:ssa määriteltyä
moodilippua.
Kun taulukko on
avattu tulostusta varten, taulukkoon lisätään merkkejä, kunnes
se on täysi. Taulukkoa ei ylitetä eikä kirjoiteta päälle.
Ylitysyrityksestä tulee I/O-virhe. pcount()-jäsenfunktiolla
voidaan selvittää puskuriin kirjoitettujen merkkien lukumäärä:
int pcount();
pcount()-funktiota
on kutsutta aina streamin kanssa ja se palauttaa taulukkoon
kirjoitettujen merkkien lukumäärän sisältäen mahdollisen
null-lopetusmerkin.
Kun käytetään
taulukkopohjaista syöttöä, käytetään seuraavaa istrstream:n
konstruktoria:
istrstream istr( const char *buf );
istr on virta,
joka kytketään puskuriin buf. eof() palauttaa
Truen, kun taulukon loppu on saavutettu.
Kun käytetään
taulukkopohjaista syöttöä ja tulostusta, käytetään seuraavaa
strstream:n konstruktoria:
strstream iostr( const char *buf, int size, int mode );
iostr on virta,
joka kytketään puskuriin buf syöttöä ja tulostusta varten. Syöttö-
tulostusoperaatioita varten mode on ios::in | ios::out. Kaikki
aiemmin esitetyt I/O-funktiot mukaanlukien myös binääriset
funktiot ja suorasaantifunktiot toimivat myös taulukkopohjaiseen
I/O:hon.
Seuraavassa
ohjelmassa avataan taulukko
tulostusta varten ja tulostetaan siihen.
#include <iostream.h>
#include <strstream.h>
main()
{
char buf[255];
ostrstream ostr( buf, sizeof( buf ) );
ostr << "Array-based I/O uses streams just like ";
ostr << "'normal' I/O\n" << 100;
ostr << ' ' << 123.45 << '\n';
// myös manipulaattoreita voidaan käyttää
ostr << hex << 100 << " ";
// formaattilippuja voidaan käyttää
ostr << ostr.setf( ios::scientific) << 123.45 << '\n';
ostr << ends;
cout << buf;
return 0;
}
Seuraavassa
esimerkki taulukkopohjaisesta syötteestä. Ohjelma ensin kirjoittaa
taulukkoon normaalisti määrittelyn yhteydessä tapahtuvalla
alustuksella ja sitten lukee taulukon sisällön ja tulostaa sen
konsolille.
#include <iostream.h>
#include <strstream.h>
main()
{
char buf[] = "Hello 100 123.125 a";
istrstream istr( buf );
char str[80];
int i;
float f;
char c;
istr >> str >> i >> f >> c;
cout << str << ' ' << i << ' ' << f;
cout << ' ' << c << '\n';
return 0;
}
Syötetaulukko käyttäytyy
kuten tiedosto, kun se on kytketty streamiin. Esimerkiksi seuraava
ohjelma käyttää binääristä I/O:ta ja eof()-funktiota puskurin
sisällön lukemiseen.
#include <iostream.h>
#include <strstream.h>
main()
{
char buf[] = "Hello 100 123.125 a";
istrstream istr( buf );
char c;
while (!istr.eof()) {
istr.get( c );
cout << c;
}
return 0;
}
Seuraava ohjelma
käyttää taulukkoa sekä syötteessä että tulostuksessa.
#include <iostream.h>
#include <strstream.h>
main()
{
char iobuf[255];
strstream iostr( iobuf, sizeof iobuf, ios::in | ios::out );
iostr << "This is a test\n";
iostr << 100 << hex << 100 << ends;
char str[80];
int i;
iostr.getline( str, 79 );
iostr >> i;
cout << str << ' ' << i << ' ';
iostr >> i;
cout << i;
return 0;
}
|
| Linkitystarkentimet
ja 'asm'-avainsanan käyttö |
| C++:ssa
on kaksi mekanismia, joiden avulla C++ on helpompi linkittää
muihin ohjelmointikieliin. Ensimmäinen on nimeltään linkitystarkennin
(linkage specifier), joka kertoo kääntäjälle, että C++ ohjelman
yksi tai useampi funktio linkitetään ohjelmaan toisesta
ohjelmointikielestä, jonka funktion parametrien välitysmekanismi
tai muu sellainen voi poiketa C++:sta. Toinen on asm
avainsana, jonka avulla C++ koodiin voidaan sisällyttää assembly
käskyjä.
Oletusarvoisesti
kaikki funktiot käännetään ja linkitetään C++ funktioina. Kääntäjää
voidaan pyytää linkittämään funktio siten, että se on
yhteensopiva toiseen kieleen. Kaikki C++ kääntäjät sallivat
funktioiden linkityksen joko C++:na tai C:nä. Jotkin kääntäjät
sallivat myös kieliä kuten Pascal, Ada tai FORTRAN. Jotta funktio
linkitetään toisella kielellä, käytetään linkitystarkentimen
yleistä muotoa:
extern "language" function-prototype;
Edellisessä
"language" on se kieli, johon linkitys halutaan tehdä.
Mikäli useita funktioita linkitetään toiselle kielelle, käytetään
seuraavaa muotoa:
extern "language" {
function-prototypes
}
Kaikkien
linkitystarkentimien on oltava globaaleita eli ne eivät voi sijaita
funktioiden sisällä.
Tavallisin
tilanne linkitystarkentimien käyttöön on silloin, kun ohjelma
linkitetään kolmannen osapuolen aliohjelmapakettiin, joka on käännetty
jollain toisella kielellä. On myös aivan tavallista, ettet joudu
koskaan ohjelmointiurallasi käyttämään linkitystarkentimia.
Vaikka yleensä
on mahdollista linkittää assebly kielisiä rutiineita C++
ohjelmaan, on kuitenkin olemassa usein helpompi tapa käyttää
assebleria. asm avainsanan avulla voit sisällyttää
assembly koodia C++ funktioon. asm avainsanan
yleinen muoto on seuraava:
asm("op-code");
jossa op-code on
assebly kielinen käsky.
Usein
asm-avainsanan käyttö on kääntäjä riippuvaista. Esimerkiksi
Borland C++ hyväksyy seuraavat muodot:
asm op-code;
asm op-code newline
asm {
instruction sequence
}
Seuraava ohjelma
linkittää func()-funktion C:nä ei C++:na:
// osa1.cpp
#include <iostream.h>
extern "C" int func( int x );
int func( int x )
{
return x/3;
}
Nyt edellinen
voidaan ottaa mukaan C-kieliseen ohjelmaan, esimerkiksi seuraavaan:
// osa2.c
#include <stdio.h>
extern int func();
int main(void)
{
int tulos;
tulos = func(9);
printf("%d\n", tulos);
return 0;
}
Seuraavassa
linkitetään useita funktioita samalla kertaa C-kielisiksi:
extern "C" {
void f1();
int f2( int x );
double f3( double x, int *p );
}
Jos käytät
in-line assembleriä, sinun on oltava varma siitä, mitä teet.
Rekisterithän voivat olla ajossa olevan ohjelman käytössä.
Periaatteellinen esimerkki kuitenkin seuraavassa:
// ÄLÄ KOKEILE, ELLET TIEDÄ MITÄ TAPAHTUU
void func()
{
asm ("mov bp, sp");
asm ("push ax");
asm ("mov c1, 4");
// ...
}
|
| Muunnosfunktioiden
luonti |
|
Joskus on tarpeen
muuttaa tietyn tyyppinen olio toisen tyyppiseksi. Muunnos on
mahdollista suorittaa esimerkiksi kuormitetun operaattorifunktion
avulla, mutta on olemassa myös toinen usein helpompi tapa, jota
kutsutaan muunnosfunktioksi. Muunnosfunktio muuttaa olion arvon
yhteensopivaksi toiseen tyyppiin, joka on usein yksi C++:n sisäänrakennetuista
tyypeistä. Muunnosfunktio muuttaa automaattisesti olion arvon
yhteensopivaksi siihen tyyppiin, mitä vaaditaan siinä yhteydessä,
kun oliota käytetään.
Muunnosfunktion
yleinen muoto:
operator tyyppi() { return arvo; }
Tässä tyyppi on
kohdetyyppi eli tyyppi, jota olion tulisi olla lausekkeessa ja arvo
on olion arvo muunnoksen jälkeen. Muunnosfunktiossa ei voi olla
parametreja ja muunnos funktio on sen luokan jäsenfunktio, jolle
muunnos tehdään.
Kuten esimerkeistä
havaitaan, muunnosfunktioiden avulla muunnos tapahtuu siistimmin
kuin muilla C++:n tavoilla suorittaa muunnos, sillä oliota voidaan
suoraan käyttää lausekkeessa kuin lausekkeessa.
Seuraavassa
ohjelmassa coord-luokka sisältää muunnosfunktion integeriin.
Muunosfunktio palauttaa itseisarvon pisteen etäisyydestä origosta.
Mikä tahansa muukin muunnos on mahdollista, mikäli se sopii sinun
sovellukseesi.
#include <iostream.h>
#include <math.h>
class coord {
int x, y;
public:
coord( int i, int j ) { x = i; y = j; }
operator int() { return sqrt( x*x + y*y ); }
};
main()
{
coord o1( 2, 3 ), o2( 4, 3 );
int i;
i = o1; // automaattinen muunnos -> 3
cout << i << '\n';
i = 100 + o2; // automaattinen muunnos -> 5
cout << i << '\n';
return 0;
}
Seuraavassa
esimerkissä mjono-tyyppinen
merkkijono muunnetaan pointteriksi mjonon privaattiin muuttujaan str:
#include <iostream.h>
#include <string.h>
class mjono {
char str[80];
int len;
public:
mjono( char *s ) {
len = strlen( s );
if (len > (sizeof str - 1)) len = sizeof str - 1;
strncpy( str, s, len);
str[len] = '\0';
}
operator char *() { return str; }
};
main()
{
mjono ob1( "Tässä on testi\n" );
char *p, s2[ 80 ];
p = ob1; // muunnos
cout << p << '\n';
strcpy( s2, ob1 ); // muunnos
cout << s2 << '\n';
return 0;
}
|
| C:n ja C++:n erot |
|
Kuten tiedät C++
pohjautuu C-kieleen. Tämä tarkoittaa, että yleensä mikä tahansa
C-ohjelma on automaattisesti (ei-oliopohjainen) C++ ohjelma. C++:n
tuki olio-ohjelmointiin aiheuttaa kuitenkin muutaman pienen eron C:n
ja C++:n välille. Jotkut näistä eroista estävät C-ohjelmaa kääntymästä
C++ kääntäjällä. Seuraavassa käydään erot läpi kohta
kohdalta, vaikkakin ainakin osaa on jo käsitelty aiemminkin.
HUOMAA:
Ei-OOP ominaisuudet C:ssä ja C++:ssa eroavat vain hyvin vähän.
Useimmiten C-ohjelma on kelvollinen C++ ohjelma sellaisenaan.
- C:ssä
Funktion esittely:
int f();
ei kerro mitään
kyseisen funktion parametreista. Funktiolla voi olla yksi tai
useampi parametri tai sillä ei ole parametreja ja mahdolliset
parametrit voivat olla mitä tahansa tyyppiä. C++:ssa edellinen
esittely tarkoittaa, että funktiolla ei ole parametreja.
C++:ssa seuraavat esittelyt tarkoittavat samaa:
int f();
int f(void);
C++:ssa void
on vapaaehtoinen ja teknisesti tarpeeton, mutta useat
ohjelmoijat käyttävät voidia, jottei kenellekään koodin
lukijalle ole epäselvää, onko parametreja vai ei.
- C++:ssa
jokaisella funktiolla on oltava prototyyppi. C:ssä
prototyyppien käyttö on osittain vapaaehtoista, vaikkakin hyvän
ohjelmointitavan mukaisesti myös C:ssä käytetään
prototyyppejä.
- Pieni mutta tärkeä
ero C:n ja C++:n välillä on se, että C:ssä merkkivakio
talletetään automaattisesti integerinä, C++:ssa ei tehdä näin.
- C:ssä voidaan
esitellä globaali muuttuja useaan kertaan, vaikkakin tämä on
hyvän ohjelmointitavan vastaista. C++:ssa useaan kertaan
esittelystä seuraa virhe.
- C:ssä
tunniste voi olla korkeintaan 31 merkkiä pitkä, C++:ssa tällaista
rajoitusta ei ole. Käytännössä ylipitkät tunnisteet ovat
kuitenkin harvinaisia ja hankalia käyttää.
- C:ssä
voidaan, vaikkakin näin tehdään harvoin, kutsua
main()-funktiota keskeltä ohjelmaa. C++:ssa tämä on kiellettyä.
- C:ssä ei
voida ottaa rekisterimuuttujan osoitetta, C++:ssa voidaan. Tästä
seuraa kuitenkin siirrettävyys rajoituksia ohjelmallesi, joten
tuskin haluat käyttää.
|
| Parametrisoidut
tyypit, luokat ja funktiot |
|
Tyyppiparametrisoinnin
avulla voidaan määritellä yleisiä luokkia kuten taulukko, lista
jne, integer taulukon, double taulukon, char taulukon jne sijasta.
Voidaan määritellä yleisiä funktioita kuten max() tai sort()
jne. riippumatta tyypeistä. Suurien luokka- ja algoritmikirjastojen
tekeminen mahdollistuu tyyppiparametrisoinnin avulla.
Parametrisoituja tyyppejä voidaan luoda ns. template
mekanismin avulla.
Jos halutaan tehdä
esimerkiksi funktio, joka palauttaa suuremman kahdesta luvusta, meidän
on tehtävä niin monta erilaista max()-funktiota kuin on erilaisia
tyyppejä, joista maksimi halutaan ottaa:
int max( int i, int j ) {
return ( (i > j) ? i : j ); }
double max( double i, double j ) {
return ( (i > j) ? i : j ); }
complex max( complex i, complex j ) {
return ( (i > j) ? i : j ); }
string max( string i, string j ) {
return ( (i > j) ? i : j ); }
jne.
Olettaen, että kaikille tyypeille on määriteltynä > operaatio.
Ainoa ero
funktioissa on argumenttien tyypeissä ja palautustyypeissä:
tyyppi max( tyyppi i, tyyppi j ) {
return ( (i > j) ? i : j ); }
Asia voidaan
hoitaa template:n avulla:
#include <iostream.h>
template <class Tyyppi> Tyyppi
max( Tyyppi i, Tyyppi j ) {
return ( (i > j) ? i : j );
}
main()
{
int i=1, j=2;
double d=3.0, e=4.0;
cout << "max int 1, 2: " << max( i, j ) << '\n';
cout << "max double 3.0, 4.0: " << max( e, d ) << '\n';
return 0;
}
Jos halutaan
luoda taulukkoluokat IntArray, DoubleArray, jne. Asia hoituu kätevästi
template:n avulla. Esimerkiksi seuraavassa ohjelmassa voidaan luoda
esimerkiksi integer taulukko, char taulukko, double taulukko jne.
Array-nimeä ei nyt enää käytetä yksin (paitsi muutamassa
harvassa paikassa määrittelyn sisällä) vaan Array<Tyyppi>,
kun kyseessä yleinen tilanne tai Jos kyseessä explisiittisesti
esimerkiksi integer-taulukko, käytetään Array<int> jne.
#include <iostream.h>
const int ArraySize = 5; // oletuskoko
template <class Tyyppi>
class Array { // Tässä ei voi olla: Array<Tyyppi>
int size;
Tyyppi *ia;
public:
Array( int sz = ArraySize ); // Voi olla: Array<Tyyppi>
Array() { delete ia; } // Voi olla: Array<Tyyppi>
Tyyppi &operator[] ( int );
};
template <class Tyyppi>
Tyyppi &Array<Tyyppi>::operator[] ( int index)
{
return ia[index];
}
template <class Tyyppi>
Array<Tyyppi>::Array( int sz ) // Jälkimmäinen ei voi olla Array<Tyyppi>
{
size = sz;
ia = new Tyyppi [size];
for (int i=0; i<size; i++) ia[ i ] = 0;
}
main()
{
int i;
Array<int> it( 7 ); // luodaan 7 alkioinen integer taulukko
Array<char> ct( 15 ); // luodaan 15 alkioinen char taulukko
for ( i = 0; i < 7; i++)
it[ i ] = i;
for ( i = 0; i < 15; i++)
ct[ i ] = i+'a';
cout << "\nit: ";
for ( i = 0; i < 7; i++)
cout << it[i] << ' ';
cout << "\nct: ";
for ( i = 0; i < 15; i++)
cout << ct[i] << ' ';
return 0;
}
|
|