Cpp luokat tarkemmin

Mureakuha

Loikkaa: valikkoon, hakuun

Sisällysluettelo

Olion sijoitus

Olion arvo voidaan sijoittaa toisen olion arvoksi, mikäli molemmat ovat samaa tyyppiä (saman luokan olioita). Jos olio A sijoitetaan olioon B, kaikki A:han liittyvä data sijoitetaan B:n dataksi vastaaviin datajäseniin.

#include <iostream>
 
class myclass {
  int  a, b;
public:
  void set( int i, int j ) { a = i; b = j; }
  void show( ) { cout << a << ' ' << b << "\n"; }
};
 
main ( )
{
  myclass  o1, o2;
 
  o1.set( 10, 4 );
  o2 = o1;    // sijoitus
  o1.show();  // tulostaa 10 4
  o2.show();  // tulostaa 10 4
  o2.set( 20, 5 );
  o1.show();  // tulostaa 10 4
  o2.show();  // tulostaa 20 5
  o1.set( 15, 3 );
  o1.show();  // tulostaa 15 3
  o2.show();  // tulostaa 20 5
  return 0;
}

On tärkeää havaita, että sijoitusoperaattori kopioi ainoastaan olion datan, oliot ovat edelleen täysin itsenäisiä. Edellisessä esimerkissä olion o1 datalle on jossain päin muistia varattu tilaa 2 kokonaisluvulle ja olion o2 datalle toiset 2 kokonaislukua.

Sijoituslauseen molempien operandien on oltava täsmälleen samaa tyyppiä. Ei riitä, että luokkien datajäsenet ovat samoja, olioiden on kuuluttava täsmälleen samaan luokkaan.

Kaikki data kopioidaan, myös esimerkiksi taulukoiden sisällöt. Seuraavassa pino-esimerkissä pinoon p1 on viety merkkejä. Pinoon p2 ei ole viety mitään push-operaatiolla, mutta sijoitus p2 = p1 aiheuttaa sen, että kaikki data pinosta p1 kopioidaan pinoon p2.

#include <iostream>
#define KOKO  10
 
class pino {
  char  puskuri[ KOKO ];
  int  vapaa;
public:
  pino() { cout << "Konstruktorissa\n"; vapaa = 0; }
  void push( char ch );
  char pop();
};
 
void pino::push( char ch )
{
  if ( vapaa == KOKO ) {
    cout << "Täynnä";
    return;
  }
  puskuri[ vapaa ] = ch;
  vapaa++;
}
 
char pino::pop()
{
  if ( vapaa == 0 ) {
    cout << "Tyhjä";
    return 0;
  }
  vapaa--;
  return  puskuri[ vapaa ];
}
 
main()
{
  pino  p1, p2;
  int  i;
 
  p1.push( 'a' );
  p1.push( 'b' );
  p1.push( 'c' );
 
  p2 = p1  ;  // kloonaus
 
  for ( i = 0; i < 3; i++ ) cout << "pop p1: " << p1.pop() << "\n";
  for ( i = 0; i < 3; i++ ) cout << "pop p2: " << p2.pop() << "\n";
 
  return 0;
}

Sijoitusoperaattoreiden kanssa on oltava varovainen. Jos esimerkiksi luokan jäsenenä on osoitin ja konstruktorissa varataan tilaa ko. osoitteeseen, sijoitusoperaattori kopioi vain kyseisen osoittimen, eli molemmat osoittavat samaan varattuun muistialueeseen.

Seuraavassa on virhe:

#include <iostream>
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <stdlib.h>
 
class mjono{
  char  *p;
  int  len;
public:
  mjono( char  *ptr );
  ~mjono();
  void show();
};
 
mjono::mjono( char  *ptr ) {
  len = strlen( ptr );
  p = (char *) malloc( len + 1 );
  if ( !p ) {
    cout << "Varausvirhe\n";
    exit( 1 );
  }
  printf( "Varattu %d tavua osoitteesta %x\n", len+1, p );
  strcpy( p, ptr );
}
 
mjono::~mjono() {
  printf("Vapautetaan puskuri osoitteesta %x\n", p );
  free( p );
}
 
void mjono::show() {
  cout << p << " - pituus: " << len;
  cout << "\n";
}
 
main() {
  mjono  mj1("Testi"), mj2("C++ on kivaa");
 
  mj1.show();
  mj2.show();
  mj1 = mj2; // tämä aiheuttaa virheen! 
  mj1.show();
  mj2.show();
  return 0;
}

Kun mj1 ja mj2 luodaan, molemmat varaavat muistia merkkijonoilleen. Varattujen alueiden osoittimet on talletettu p-jäseniin. Kun mjono-olio tuhotaan (destruktorissa), varattu muisti vapautetaan. Kun mj2 sijoitetaan mj1:een, molemmat p:t saavat samat arvot, siis molemmat osoittavat muistialuetta, joka alunperin varattiin mj2:lle (C++ on kivaa). Osoitin varatulle muistialueelle, joka sisältää tekstin "Testi", ei ole enää ohjelman käytössä, eli ko. aluetta ei voida enää vapauttaa. Kun mj1 tuhoutuu, se vapauttaa "C++ on kivaa"-puskurin ja kun mj2 tuhoutuu, se yrittää vapauttaa saman puskurin.

Olion välittäminen funktioon argumenttina

Olio voidaan viedä funktioon argumenttina aivan kuten kaikki muutkin muuttujat. Oletusarvoisesti olio välitetään arvoparametrina ( vaikka funktiossa muutettaisiin argumentin arvoa, muutokset eivät näy kutsuvassa kohdassa ohjelmaa).

#include <iostream>
 
class samp {
  int  i;
public:
  samp( int  n ) { cout << "konstruktorissa\n";  i = n; }
  ~samp() { cout << "destruktorissa\n";}
  int  get_i() { return i; }
};
 
int  nelio( samp  o )
{
  return  o.get_i()  * o.get_i();
}
 
int main()
{
  samp  a(10), b(9);
  cout << "1.\n";
  cout << nelio( a ) << "\n";
  cout << "2.\n";
  cout << nelio( b ) << "\n";
  cout << "3.\n\n";
  return 0;
}

Ohjelma tulostaa:

konstruktorissa
konstruktorissa
1.
destruktorissa
100
2.
destruktorissa
81
3.
 
destruktorissa
destruktorissa

Voit ihmetellä jo nyt, miksi destruktoria on kutsuttu 4 kertaa ja konstruktoria vain 2 kertaa. Asia selvitetään hetken kuluttua.

HUOM! Mikäli käytössäsi on Borland C++ 2.x, testitulostukset tulevat hieman eri järjestyksessä: myös funktion sisäiset muuttujat tuhotaan vasta main:n lopussa.

Arvoparametrien yhteydessä kutsuargumentista otetaan kopio, jota käytetään itse funktiossa. Jos funktion sisällä muutetaan argumentin arvoa, muutos ei näy kutsuvassa kohdassa ohjelmaa.

#include <iostream>
 
class samp {
  int  i;
public:
  samp( int  n ) { cout << "konstruktorissa\n";  i = n; }
  ~samp() { cout << "destruktorissa\n";}
  int  get_i() { return i; }
  void set_i( int n ) { i = n; }
};
 
int  nelio( samp  o )
{
  int temp;
 
  temp = o.get_i()  * o.get_i();
  o.set_i( 0 );
  cout << "funktiossa asetetaan kopiolle arvo " << o.get_i() << "\n";
  return temp;
}
 
int main()
{
  samp  a(10);
  cout << "a ennen kutsua: " << a.get_i() << "\n";
  cout << "a:n nelio: " << nelio( a ) << "\n";
  cout << "a kutsun jälkeen: " << a.get_i() << "\n";
  return 0;
}

Tulostaa:

konstruktorissa
a ennen kutsua: 10
funktiossa asetetaan kopiolle arvo 0
destruktorissa
a: nelio: 100
a kutsun jälkeen: 10
destruktorissa

Jos argumenttina välitetään osoite, alkuperäistä arvoa voidaan muuttaa.

#include <iostream>
 
class samp {
  int  i;
public:
  samp( int  n ) { cout << "konstruktorissa\n";  i = n; }
  ~samp() { cout << "destruktorissa\n";}
  int  get_i() { return i; }
  void set_i( int n ) { i = n; }
};
 
int  nelio( samp  *o )
{
  int temp;
 
  temp = o->get_i()  * o->get_i();
  o->set_i( 0 );
  cout << "funktiossa asetetaan arvo " << o->get_i() << "\n";
  return temp;
}
 
int main()
{
  samp  a(10);
  cout << "a ennen kutsua: " << a.get_i() << "\n";
  cout << "a:n nelio: " << nelio( &a ) << "\n";
  cout << "a kutsun jälkeen: " << a.get_i() << "\n";
  return 0;
}

Tulostaa:

konstruktorissa
a ennen kutsua: 10
funktiossa asetetaan kopiolle arvo 0
destruktorissa
a: nelio: 100
a kutsun jälkeen: 0
destruktorissa

Kun funktioon välitetään argumenttina olio, luodaan uusi väliaikainen olio, jonka arvoksi kopioidaan kutsuolion arvo. Kun kyseisen funktion suoritus päättyy, luotu väliaikainen olio tuhotaan.

  1. Kutsutaanko kopion luonnissa konstruktoria ?
  2. Kutsutaanko kopion tuhoamisessa destruktoria ?

VASTAUS: Konstruktoria ei kutsuta mutta destruktoria kutsutaan !!!

Konstruktoria ei kutsuta, sillä funktioon halutaan välittää olion sen hetkisen tilan kopio, ei mitään alustustilaa, mikä syntyisi konstruktorissa. Konstruktoriahan käytetään alustustarkoituksiin.

Katso nyt aiempia ohjelmia, joissa näkyy, että kopiolle ei kutsuta konstruktoria, mutta kutsutaan destruktoria.

Se seikka, että konstruktoria ei kutsuta, mutta destruktoria kutsutaan, voi olla potentiaalinen virheen aiheuttaja: Jos konstruktorissa varataan muistia ja destruktorissa on tarkoitus vapautta kyseinen muisti, päädytään virhetilanteeseen (kääntäjä ei toki huomaa mitään). Kun tällainen olio välitetään funktioon, luodaan kopio, jossa on tn. kerrottuna alkuperäiselle oliolle varatun muistilohkon osoite. Funktiosta poistuttaessa vapautetaan alkuperäiselle oliolle varattu muisti, sillä nyt kutsutaan kopion destruktoria ja kopio viittaa samaan muistilohkoon alkuperäisen kanssa.

Olion palauttaminen funktiosta return lauseella

Olio voidaan palauttaa funktiosta määrittelemällä funktio olion tyyppiseksi ja palauttamalla saman tyyppinen olio return lauseella.

#include <iostream>
#include <string.h>
 
class samp {
  char  s[80];
public:
  samp() { cout << "konstruktorissa\n"; }
  ~samp() { cout << "destruktorissa\n"; }
  void show() { cout << s << "\n"; }
  void set( char *str ) { strcpy( s, str ); }
};
 
samp input()
{
  char   s[80];
  samp  temp;
 
  cout << "2.\n";
  cout << "Anna merkkijono: ";
  cin >> s;
  temp.set( s );
  cout << "3.\n";
  return temp;
}
 
main()
{
  samp  ob;
  cout << "1.\n";
  ob = input();
  cout << "4.\n";
  ob.show();
  return 0;
}

ohjelma tulostaa:

konstruktorissa
1.
konstruktorissa
2.
Anna merkkijono: abc
3.
destruktorissa
destruktorissa
4.
abc
destruktorissa

Huomaa taas yksi ylimääräinen destruktori: tästä myöhemmin

Kun olio on saatu palautettua kutsuvaan kohtaan ohjelmaa, palautetun olion voimassaolo päättyy eli kutsutaan luokan destruktoria. Jos nyt destruktorissa esimerkiksi vapautetaan muistia, tästä seuraa virhetilanne.

// Ohjelmassa on suunnitteluvirhe
 
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
 
class dyna2 {
  char *p;
public:
  dyna2 () { p = 0; cout << "konstruktorissa\n"; }
  ~dyna2();
  void show() { cout << p << "\n"; }
  void set( char *str);
};
 
void dyna2::set( char *str)
{
  p = (char *) malloc( strlen( str ) );
  if (!p) {
    cout << "Varausvirhe\n";
    exit(1);
  }
  cout << "merkkijononasetus\n";
  printf("varataan: %p\n", p );
  strcpy( p, str );
}
 
dyna2::~dyna2()
{
  cout << "destruktorissa\n";
  if (p) {
    printf("vapautetaan: %p\n", p );
    free( p );
  }
}
 
dyna2  input()
{
  char   s[80];
  dyna2  temp;
 
  cout << "2.\n";
  cout << "Anna merkkijono: ";
  cin >> s;
  temp.set( s );
  cout << "3.\n";
  return temp;
}
 
main()
{
  dyna2  ob;
 
  cout << "1.\n";
  ob = input();
  cout << "4.\n";
  return 0;
}

ohjelma tulostaa:

konstruktorissa      (ob)
1.
konstruktorissa      (temp)
2.
Anna merkkijono: abc
merkkijononasetus
varataan: 33DF:00F6
3.
destruktorissa      (temp)
vapautetaan: 33DF:00F6
destruktorissa      (väliaikainen, näkymätön)
vapautetaan: 33DF:00F6
4.
abc
destruktorissa      (ob)
vapautetaan: 33DF:00F6
Null pointer assignment

Ylimääräisistä destruktoreista:

Ensimmäinen konstruktorin kutsu tulee main-funktion alussa ob:n luonnista. Toinen konstruktorin kutsu tulee input-funktiossa temp:n luonnista. Kun funktio palauttaa jotain return-lauseella, ennen funktion päättymistä luodaan väliaikainen käyttäjälle näkymätön muuttuja, jossa palautusarvo välitetään kutsuvaan ohjelman kohtaan. Väliaikaisen muuttujan destruktorifunktiota kutsutaan, kun arvo on saatu välitettyä kutsuvaan ohjelmakohtaan. Kun input funktio päättyy, luodaan väliaikainen näkymätön muuttuja, johon kopioidaan temp:n arvo ja paikalliset muuttujat tuhotaan, eli temp tuhotaan, siis kutsutaan destruktoria temp-muuttujalle. Kun väliaikaisen muuttujan arvo on saatu välitettyä kutsuvaan ohjelman kohtaan, väliaikainen muuttuja tuhotaan eli kutsutaan destruktoria. Ohjelman päättyessä tuhotaan vielä pääohjelmassa luotu ob muuttuja eli kutsutaan sille destruktoria.

Friend-funktiot esittely

Joskus on tarvetta funktiolle, joka ei ole luokan jäsenfunktio, mutta kuitenkin funktiolla on pääsy luokan privaatteihin muuttujiin. Tällainen funktio on ns. friend-funktio. Friend-funktioita käytetään pääasiassa operaattoreiden kuormitustilanteissa ( myöhemmin ) sekä tietyissä I/O-funktioissa (myöhemmin). Friend-funktiot ovat käyttökelpoisia myös tilanteissa, joissa funktion pitää käsitellä kahden tai useamman luokan privaattijäseniä.

Friend-funktio määritellään tavalliseksi ei-jäsenfunktioksi. Sen luokan määrittelyssä, jonka friend-funktiosta on kysymys, kerrotaan kyseinen friend-funktio suhde:

#include <iostream>
 
class myclass {
  int  n, d;
public:
  myclass( int i, int j ) { n = i; d = j; }
  friend  int  isfactor( myclass ob );
};
 
int isfactor( myclass  ob )
{
  if ( !( ob.n % ob.d ) ) return 1;
  else return 0;
}
 
main()
{
  myclass  ob1( 10, 2 ), ob2( 13, 3 );
 
  if  ( isfactor( ob1) ) cout << "2 is a factor of 10\n";
  else  cout << "2 is not a factor of 10\n";
 
  if  ( isfactor( ob2) ) cout << "3 is a factor of 13\n";
  else  cout << "3 is not a factor of 13\n";
 
  return 0;
}

Huomaa, että luokan jäsenfunktio (myclass-konstruktori) viittaa privaatteihin jäseniin suoraan (n ja d) Friend-funktion on viitattava olio.jäsen, sillä kutsuttaessa friend-funktiota siihen liittyvää oliota ei ole mitenkään kiinnitetty kuten jäsenfunktioiden tapauksessa.

Koska friend-funktioita ei ole kiinnitetty mihinkään olioihin, niihin voidaan välittää useita olioita argumentteina.

Friend-funktiot eivät periydy: Jos luokka B perii luokan A ja A:lla on friend-funktio, B ei peri friend-funktiota.

Friend-funktio voi olla usean luokan friend-funktio.

Eräs tilanne friend-funktion käyttöön on se, kun usealla eri luokalla on jokin yhteinen ominaisuus, jota joudutaan vertailemaan eri luokkiin kuuluvien olioiden kesken.

#include <iostream>
 
class kuormuri;  // etukäteen esittely
 
class auto_ {
  int  matkustajat;
  int  nopeus;
public:
  auto_( int m, int n ) { matkustajat = m; nopeus = n; }
  friend int auto_nopeampi( auto_   a, kuormuri   k );
};
 
class kuormuri {
  int  paino;
  int  nopeus;
public:
  kuormuri( int p, int n ) { paino = p; nopeus = n; }
  friend int auto_nopeampi( auto_   a, kuormuri   k );
};
 
int  auto_nopeampi( auto_   a,  kuormuri   k )
{
  return  a.nopeus - k.nopeus;
}
 
main()
{
  int    nopeusero;
  auto_    a1( 6, 110 ), a2( 2, 220 );
  kuormuri  k1( 10000, 100 ), k2( 20000, 140 );
 
  cout << "\nverrataan a1:stä ja k1:stä: ";
  nopeusero = auto_nopeampi( a1, k1 );
 
  if ( nopeusero >= 0 )
    cout << "a1 nopeampi kuin k1\n";
  else 
    cout << "a1 hitaampi kuin k1\n";
 
  cout << "\nverrataan a1:stä ja k2:stä: ";
  nopeusero = auto_nopeampi( a1, k2 );
 
  if ( nopeusero >= 0 )
    cout << "a1 nopeampi kuin k2\n";
  else 
    cout << "a1 hitaampi kuin k2\n";
 
  cout << "\nverrataan a2:stä ja k1:stä: ";
  nopeusero = auto_nopeampi( a2, k1 );
 
  if ( nopeusero >= 0 )
    cout << "a2 nopeampi kuin k1\n";
  else 
    cout << "a2 hitaampi kuin k1\n";
 
  cout << "\nverrataan a2:stä ja k2:stä: ";
  nopeusero = auto_nopeampi( a2, k2 );
 
  if ( nopeusero >= 0 )
    cout << "a2 nopeampi kuin k2\n";
  else 
    cout << "a2 hitaampi kuin k2\n";
 
  return 0;
}

Funktio voi olla jonkin luokan jäsen ja samalla toisen luokan friend. Seuraavassa sama kuin edellä paitsi että auto_nopeampi-funktio on auto_:n jäsen ja kuormurin friend.

#include <iostream>
 
class kuormuri;  // etukäteen esittely
 
class auto_ {
  int  matkustajat;
  int  nopeus;
public:
  auto_( int m, int n ) { matkustajat = m; nopeus = n; }
  int auto_nopeampi( kuormuri   k );
};
 
class kuormuri {
  int  paino;
  int  nopeus;
public:
  kuormuri( int p, int n ) { paino = p; nopeus = n; }
  friend int auto_::auto_nopeampi( kuormuri   k );
};
 
int  auto_::auto_nopeampi( kuormuri   k )
{
  return  nopeus - k.nopeus;
}
 
main()
{
  int    nopeusero;
  auto_    a1( 6, 110 ), a2( 2, 220 );
  kuormuri  k1( 10000, 100 ), k2( 20000, 140 );
 
  cout << "\nverrataan a1:stä ja k1:stä: ";
  nopeusero = a1.auto_nopeampi( k1 );
 
  if ( nopeusero >= 0 )
    cout << "a1 nopeampi kuin k1\n";
  else 
    cout << "a1 hitaampi kuin k1\n";
 
  cout << "\nverrataan a1:stä ja k2:stä: ";
  nopeusero = a1.auto_nopeampi( k2 );
 
  if ( nopeusero >= 0 )
    cout << "a1 nopeampi kuin k2\n";
  else 
    cout << "a1 hitaampi kuin k2\n";
 
  cout << "\nverrataan a2:stä ja k1:stä: ";
  nopeusero = a2.auto_nopeampi( k1 );
 
  if ( nopeusero >= 0 )
    cout << "a2 nopeampi kuin k1\n";
  else 
    cout << "a2 hitaampi kuin k1\n";
 
  cout << "\nverrataan a2:stä ja k2:stä: ";
  nopeusero = a2.auto_nopeampi( k2 );
 
  if ( nopeusero >= 0 )
    cout << "a2 nopeampi kuin k2\n";
  else 
    cout << "a2 hitaampi kuin k2\n";
}

Yleensä ei käytetä täydellisiä luokkamäärittelyitä esimerkiksi funktiokutsuissa, mutta niin voitaisiin tehdä haluttaessa:

nopeampi = a1.auto_nopeampi( k1 );
===
nopeampi = a1.auto_::auto_nopeampi( k1 );
Tämän dokumentin kopiointi, levittäminen sekä muokkaaminen on sallittua GNU Free Documentation Licensen version 1.2 tai uudemman Free Software Foundationin julkaiseman version mukaisesti, ilman muuttumattomuuslauseketta tai kansitekstejä. Tätä koskee vastuuvapaus.
Kopio lisenssistä (englanniksi) löytyy täältä.


Alkuperäinen (c) Petteri Hämäläinen

Henkilökohtaiset työkalut