Cpp taulukot,osoittimet ja referenssit

Mureakuha

Loikkaa: valikkoon, hakuun

Tässä luvussa tarkastellaan jo C:stä tuttuja käsitteitä: taulukot ja osoittimet. Nyt vain kyseessä on oliotaulukot ja olioiden osoitteet. Lisäksi kappaleessa esitellään C++:n eräs keskeinen ominaisuus: referenssi.

Sisällysluettelo

Oliotaulukot

Todennäköisesti olet jo käyttänytkin oliotaulukoita, ainakin sellaisia, joiden konstruktoriin ei välitetä argumentteja. Tällaisissa tilanteissa toimitaan aivan samoin kuin vaikkapa integer-taulukon tapauksessa.

Ohjelmassa luodaan 4 alkioinen samp-taulukko, taulukon alkioiden a-jäsenten arvoiksi asetetaan arvot 0...3 ja lopuksi ne tulostetaan.

#include <iostream>
 
class samp {
  int a;
public:
  samp() { cout << "konstruktorissa\n"; }
  ~samp() { cout << "destruktorissa\n"; }
  void set_a( int n  ) { a = n; }
  int  get_a() { return a; }
};
 
main()
{
  samp  ob[ 4 ];
  int   i;
 
  for ( i = 0; i < 4; i++ ) ob[ i ].set_a( i );
  for ( i = 0; i < 4; i++ ) cout << ob[ i ].get_a() ;
  cout << "\n";
  return 0;
}

Ohjelma tulostaa:

konstruktorissa
konstruktorissa
konstruktorissa
konstruktorissa
0123
destruktorissa
destruktorissa
destruktorissa
destruktorissa

Taulukon alkiot voidaan alustaa, mikäli luokalla on konstruktori.

#include <iostream>
 
class samp {
  int a;
public:
  samp(int n) { a = n; cout << "konstruktorissa\n"; }
  ~samp() { cout << "destruktorissa\n"; }
  int  get_a() { return a; }
};
 
main()
{
  samp  ob[ 4 ] = { -1, -2, -3, -4 };
  int   i;
 
  for ( i = 0; i < 4; i++ ) cout << ob[ i ].get_a() << ' ';
  cout << "\n";
  return 0;
}

Ohjelma tulostaa:

konstruktorissa
konstruktorissa
konstruktorissa
konstruktorissa
-1 -2 -3 -4
destruktorissa
destruktorissa
destruktorissa
destruktorissa

Alustuslauseke samp ob[ 4 ] = { -1, -2, -3, -4 }; on lyhennös pidemmästä muodosta: samp ob[ 4 ] = { samp( -1 ), samp( -2 ), samp( -3 ), samp( -4 ) }; Ensimmäinen tapa on tavallisempi ainakin 1-ulotteisilla taulukoilla. Lyhennysmuoto toimii vain jos konstruktorille välitetään 1 argumentti.

Taulukko voi olla moniulotteinen.

#include <iostream>
 
class samp {
  int a;
public:
  samp(int n) { a = n; cout << "konstruktorissa\n"; }
  ~samp() { cout << "destruktorissa\n"; }
  int  get_a() { return a; }
};
 
main()
{
  samp  ob[ 4 ] [ 2 ] = { 
        { 1, 2 }, 
        { 3, 4 },
        { 5, 6 },
        { 7, 8 }
  };
  int   i, j;
 
  for ( i = 0; i < 4; i++ )  {
    for ( j = 0; j < 2; j++ )
      cout << ob[ i ][ j ].get_a() << ' ';
    cout << "\n";
  }
  return 0;
}

Kun konstruktori haluaa enemmän kuin 1 argumentin, alustuksessa on käytettävä pidempää muotoa.

#include <iostream>
 
class samp {
  int a, b;
public:
  samp( int n, int m ) { a = n; b = m; }
  int  get_a() { return a; }
  int  get_b() { return b; }
};
 
main()
{
  samp  ob[ 4 ] [ 2 ] = { 
        { samp( 0, 0 ), samp( 0, 1 ) }, 
        { samp( 1, 0 ), samp( 1, 1 ) },
        { samp( 2, 0 ), samp( 2, 1 ) },
        { samp( 3, 0 ), samp( 3, 1 ) }
  };
  int   i, j;
 
  for ( i = 0; i < 4; i++ )  {
    for ( j = 0; j < 2; j++ )
    cout << ob[i][j].get_a() << ',' << ob[i][j].get_b() << ' ';
    cout << "\n";
  }
  return 0;
}

Ohjelma tulostaa:

0,0  0,1
1,0  1,1
2,0  2,1
3,0  3.1

Olio-osoittimien käyttö

Olio-osoittimia käytetään samoin kuin struktuuriosoittimia. Olion jäseneen viitataan -> operaattorilla pisteen sijasta. Osoitinaritmetiikkaa suoritetaan oliosta riippuen kuten tavallisilla muuttujillakin. Esimerkiksi jos x on osoitin integeriin ja integer vie kaksi tavua muistia, x++ osoittaa seuraavaan integeriin muistissa eli kaksi tavua eteenpäin. Vastaavasti, jos joku olio vie vaikkapa 5 tavua muistia ja y on osoite ko. olioon, ++y osoittaa 5 tavua eteenpäin ko. oliosta.

#include <stdio.h>
 
class samp {
  int a, b;
public:
  samp( int n, int m ) { a = n; b = m; }
  int get_a() { return a; }
  int get_b() { return b; }
};
 
main()
{
  samp  ob[4] = { samp(1, 2), samp(3, 4), samp(5, 6), samp(7, 8) };
  int  i;
  samp  *p;
 
  p = ob;  // p = &ob[0];
  
  for ( i = 0; i < 4; i++ ) {
    printf("%p: %d %d\n", p, p->get_a(), p->get_b() );
    p++;
  }
  return 0;
}

Ohjelma tulostaa:

3657:21D6: 1 2
3657:21DA: 3 4
3657:21DE: 5 6
3657:21E2: 7 8

this-pointer

C++:ssa on erityisosoitin, jonka nimi on this. this-pointteri välitetään automaattisesti jäsenfunktioon, kun jäsenfunktiota kutsutaan. this-pointter on osoite olioon, joka generoi kutsun. Tässä vaiheessa this-pointteria ei käytetä sen kummemmin mihinkään erityistarkoitukseen, nyt on oleellista ainoastaan ymmärtää, että sellainen on olemassa. Myöhemmin osoittimelle löytyy käteviä käyttötapoja.

Esimerkiksi:

...
samp  x, y;
 
x.get_a();
 // get_a-funktioon välitetään x:n osoite this-pointterin arvona
y.set_a();
 // set_a-funktioon välitetään y:n osoite this-pointterin arvona 
 

Jäsenfunktiossa on mahdollista käyttää this-pointterin arvoa.

Seuraavassa pieni ohjelma sekä kommenteissa vastaavat ohjelman kohdat siten, että käytetään this-pointteria.

#include <stdio.h>
#include <string.h>
 
class inventory {
  char  item[20];
  double  cost;
  int  on_hand;
public:
  inventory( char  *i, double c, int o )
  {
    strcpy( item, i );  // strcpy( this->item, i );
    cost = c;    // this->cost = c;
    on_hand = o;    // this->on_hand = o;
  }
  void show()
  {
    printf("%s: %lf mk varastossa: %d kpl\n",
     item, cost, on_hand );
  /*  printf("%s: %lf mk varastossa: %d kpl\n",
      this->item, this->cost, this->on_hand ); */
  }
};
 
main()
{
  inventory  ob("vasara", 19.50, 6 );
 
  ob.show();
  return 0;
}

new ja delete

Aiemmin dynaamisen muistin varauksen yhteydessä olemme käyttäneet C:n standardifunktioita malloc ja free muistin varaukseen ja vapautukseen. C++ tarjoaa helpomman ja turvallisemmankin tavan: muistia varataan new-operaattorilla ja vapautetaan delete operaattorilla.

pointteri = new  tyyppi;
delete pointteri;

tyyppi on sen tyypin nimi, jonka tyyppiselle muuttujalle halutaan varata muistia ja pointteri on osoitin muuttujaan, joka on tyyppi-tyyppinen.

Esimerkki: halutaan varata muistia integerille:

...        // #include <stdlib.h>
int  *p;
 
p = new int;      // p = (int *)malloc( sizeof( int ) );
if (!p)        
  return;
*p = 5;
cout << "\n" << *p;
delete p;      // free( p );
...

Kuten malloc:ssa, jos muistia ei ole tarpeeksi new palauttaa nul-pointterin.

New:n ja deleten käytössä mallociin ja freehin nähden on joitain etuja:

  • new varaa automaattisesti tarpeeksi muistia, ei tarvita sizeof..., eli yksi mahdollinen virhepaikka vähemmän.
  • new palauttaa automaattisesti oikean tyyppisen osoitteen, ei tarvita tyyppimuunnosta (int *)
  • new:ta ja deleteä voidaan kuormittaa, jolloin voit rakentaa oman muistinvarausjärjestelmän.
  • new:n avulla varattu muisti voidaan alustaa.
  • ei tarvita enää #include malloc.h tai stdlib.h lauseita
#include <iostream>
 
class samp {
  int i, j;
public:
  samp(){ cout << "\nKonstruktorissa"; }
  ~samp() { cout << "\nDestruktorissa"; }
  void   set_ij( int a, int b ) { i = a; j = b; }
  int  get_product() { return i*j; }
};
 
main()
{
  samp  *p;
 
  p = new samp;
  if (!p) {
    cout << "\nVarausvirhe";
    return 1;
  }
  p->set_ij( 4, 5 );
  cout << "\nTulo on " << p->get_product() << "\n";
  delete p;
  return 0;
}

Ohjelma tulostaa:

Konstruktorissa
Tulo on 20
 
Destruktorissa

Lisää new- ja delete-operaattoreista

  1. Dynaamisesti varatuille olioille voidaan antaa alkuarvot.
  2. Voidaan luoda dynaamisesti taulukoita, joille ei kuitenkaan voi antaa alkuarvoja esittelyn yhteydessä.

Alkuarvon asetus luonnin yhteydessa: p-var = new type ( alkuarvo );

Esim. luodaan integer, jolle annetaan alkuarvo 5:

int  *x;
x = new integer (5 );

Varataan dynaaminen 1-ulotteinen taulukko: p-var = new type [ koko ]; Nyt p-var osoittaa koko-kokoisen 1-ulotteisen taulukon 1. alkiota

Taulukolle varatun muistin vapautus: delete [ koko ] p-var; HUOM! HUOM!

Edellä esitetty delete aiheuttaa sen, että jokaiselle taulukon alkiolle kutsutaan destruktori-funktiota. Itse varattu muisti vapautetaan vain kertaalleen. Jos koko-jätetään pois, muisti kyllä vapautetaan, mutta destruktoreita ei kutsuta (jos destruktoreita ei ole, koko on turha).

Ohjelma luo luokan koe olion ja taulukollisen olioita.

#include <iostream>
#include <stdio.h>
 
class koe {
  int i, j;
public:
  koe( int a, int b ) { cout << "\nkonstr."; i = a; j = b; }
  koe() { cout << "\ndef. konstr."; i = 0; j = 0; }
  ~koe() { cout << "\ndestr"; }
  void set( int a, int b ) { i=a; j = b; }
  void show() { cout << "\ni=" << i << " j=" << j; }
};
 
main()
{
  koe  *muuttuja, *taulu;
  int  i;
 
  muuttuja = new koe ( 3,4 );
  if (!muuttuja) {
    cout << "\nvarausvirhe";
    return 1;
  }
  taulu = new koe[3];
  if (!taulu) {
    cout << "\nvarausvirhe";
    return 1;
  }
  muuttuja->show();
  for (i = 0; i < 3; i++)
    taulu[i].show();  // Huomaa piste, ei nuoli
 
//  delete muuttuja;
//  delete [3] taulu;  
 
  return 0;
}

Ohjelma tulostaa:

konstr.
def. konstr.
def. konstr.
def. konstr.
i=3 j=4
i=0 j=0
i=0 j=0
i=0 j=0
Jos lopussa lisäksi delete lauseet ->
destr.
destr.
destr.
destr.

Jos jälkimmäisestä delete-lauseesta olisi jätetty koko [3] pois, destruktoria oltaisiin kutsuttu yhteensä vain 2 kertaa.

Referenssi

C++:ssa on referenssi-niminen ominaisuus, joka on "sukua" osoittimelle. Referenssi käyttäytyy kuten muuttujan toinen nimi, se on viittaus tiettyyn muuttujaan. Referenssiä voidaan käyttää:

  1. funktion argumenttina (tärkein käyttötapa)
  2. funktion palautusarvona (return)
  3. riippumattomana referenssinä

Seuraavassa esimerkissä käytetään vasemmalla puolella tavallista osoitinta ja oikealla puolella referenssiä funktion argumenttina:

#include <iostream>                     #include <iostream>
 
void  f ( int  *n );                      void f ( int  &n );
 
main()                                    main()
{                                         {
  int  luku = 0;                             int  luku = 0;
 
  f ( &luku );                               f ( luku );
  cout << "luku=" << luku << "\n";          cout << "luku=" << luku << "\n";
  
  return 0;                                 return 0;
}                                         }
 
void f ( int *n )                         void f ( int  &n )
{                                         {
  *n = 100;                                 n = 100;
}                                         }

Referenssin hyödyt osoittimeen nähden:

   * Funktion kutsussa ei tarvitse muistaa ottaa muuttujan osoitinta (&), muuttujan osoitin välitetään automaattisesti funktioon.
   * Funktioliittymä on siistimpi, elegantimpi (mielipide?).
   * Kun funktioon välitetään olio referenssinä, ei tehdä kopiota.

Olioiden referenssien välittäminen

Kuten muistanet aiemmasta, kun olio välitetään funktioon arvoparametrina, oliosta tehdään kopio. Vaikka kopiolle ei kutsuta konstruktoria, destruktoria kutsutaan. Tästä saattoi olla vakavia seurauksia tilanteissa, joissa destruktori sisälsi esimerkiksi muistin vapautuksia.

Edellä olleen ongelman eräs ratkaisu on käyttää referenssejä arvoparametrien sijasta: kopiota ei tehdä, eikä näin ollen myöskään destruktoria kutsuta.

HUOMAA: Referenssi ei ole osoitin, siis käytetään pistettä ( . ) ei nuolta ( -> ).

Seuraavassa rinnakkain sama ohjelma muuten, mutta vasemmalla funktiota kutsutaan tavallisella arvoparametrilla ja oikealla käytetään referenssiä.

#include <iostream>                   #include <iostream>
 
class myclass {                         class myclass {
  int  who;                                int  who;
public:                                 public:
  myclass ( int n ) {                     myclass( int n ) {
    who = n;                                who = n;
    cout << "konstr: " << who << "\n";      cout << "konstr: " << who
  }                                       }
  ~myclass()                              ~myclass()
  {                                       {
    cout << "destr: " << who;               cout << "destr: " << who;
    cout << "\n";                           cout << "\n";
  }                                       }
  int id() { return who; }                int id() { return who; }
};                                      };
 
void  f ( myclass o )                   void  f ( myclass  &o )
{                                       {
  cout << "f:ssä " << o.id();             cout << "f:ssä " << o.id();
  cout << "\n";                           cout << "\n";
}                                       }
 
main()                                  main()
{                                       {
  myclass  x( 1 );                         myclass  x ( 1 );
 
  f( x );                                 f( x );
 
  return 0;                               return 0;
}                                       }
konstr: 1          konstr: 1
f:ssä 1            f:ssä 1
destr: 1          destr: 1
destr: 1

Referenssin palauttaminen funktiosta

Funktio voi palauttaa referenssin. Tämä on erittäin käyttökelpoinen tietyissä operaattoreiden kuormitustilanteissa. Näistä kuitenkin valitettavasti vasta myöhemmin. Referenssiä voidaan kuitenkin käyttää siten, että funktio voi olla sijoituslausekkeen vasemmalla puolella. f() = 5; Omituista vai mitä.

#include <iostream>
 
int  x;
 
int  &funktio()
{
  return x;
}
 
main()
{
  funktio( ) = 100;
 
  cout << x << "\n";
  return 0;
}

Tässä funktio palauttaa referenssin kokonaislukuun, tarkemmin sanoen globaaliin muuttujaan x. funktion sisällä lauseke return x; ei palauta x:n arvoa vaan oikeastaan x:n osoitteen referenssin muodossa. Tällöin main:ssa lauseke funktio() = 100; sijoittaa arvon 100 kyseiseen osoitteeseen eli muistipaikkaan x.

Kun palautetaan referenssi johonkin, on oltava tarkkana, että kyseisen muuttujan voimassaoloalue yltää myös kutsukohtaan. Esimerkiksi, jos x olisi ollut funktion sisällä määritelty muuttuja, sen voimassaoloalue olisi päättynyt funktion suorituksen loppuun, eli sijoitus main:ssa olisi tapahtunut muistialueelle, joka ei enää olisi ollut käytettävissä. Siis seuraava olisi väärin:

W  int  &funktio()
W  {
W    int  x;
W    return x;
W  }

Ainakin Borland C++ 4.5 havaitsee asian jo käännösvaiheessa: Attempting to return a reference to local variable ...

Eräs tapa käyttää funktiota, joka palauttaa referenssin, löytyy esimerkiksi indekstit tarkistavasta taulukkotyypistä. C:ssä ja C++:ssa ei tehdä automaattisesti mitään indeksien järkevyystarkistuksia käsiteltäessä taulukoita.

Esim.

...
int  i;
int   x[5]; 
 
for (i=-100; i<100; i++) x[i] = 0;
...

menee nätisti kääntäjästä läpi.

C++:ssa voidaan luoda oma luokka: taulukko, jossa suoritetaan indeksien raja-arvotarkistukset. Taulukko-luokka sisältää kaksi ydinfunktiota: funktio joka tallettaa arvon taulukon alkiolle ja funktio, joka lukee arvon taulukon alkiosta. Nämä funktiot voivat tarkistaa ajonaikana, ettei raja-arvoja rikota.

// raja-arvot tarkistava char-taulukko
#include <iostream>
#include <stdlib.h>
 
class charArray {
  int  size;
  char  *p;
public:
  charArray( int num );
  char   &put( int  i ); // ei vie taulukkoon mitään palauttaa,
        //referenssin haluttuun alkioon
  char  get( int  i ); // palauttaa alkion halutusta kohdasta
};
 
charArray::charArray( int  num )
{
  p = new char [  num ];
  if (!p) {
    cout << "\nVarausvirhe\n";
    exit(1);
  }
  size = num;
}
 
char  &charArray::put( int i )
{
  if ( i < 0 || i >= size ) {
    cout << "\nraja-arvo virhe\n";
    exit(1);
  }
  return p[ i ];
}
 
char  charArray::get( int i )
{
  if ( i < 0 || i >= size ) {
    cout << "\nraja-arvo virhe\n";
    exit(1);
  }
  return p[ i ];
}
 
main( )
{
  charArray  a( 10 );
 
  a.put( 3 ) = 'X';
  a.put( 2 ) = 'R';
 
  cout << a.get( 3 ) << "  " << a.get( 2 ) << "\n";
 
  a.put( 10 ) = 'S'; // Raja-arvovirhe
  return 0;
}

Raja-arvojen tarkistusta on syytä käyttää, mikäli on olemassa todellinen mahdollisuus raja-arvojen rikkomiseen, johtuen jostain ulkoisesta seikasta, jota ei olla voitu etukäteen ohjelmoida koodiin. Aina raja-arvotarkistuksia ei kannata käyttää, sillä ne tietenkin hidastavat ohjelman suoritusta, kun jokainen taulukkoviittaus tarkoittaa erillisen funktion kutsumista. Riippumattomat referenssit ja referenssien rajoitukset Vaikkakin riippumatonta referenssiä ei yleensä käytetä ja itse asiassa sen käyttöä tulee välttää, sellaisen luominen on mahdollista. Riippumaton referenssi on kaikin tavoin toinen nimi muuttujalle. Koska referenssiin ei voi sijoittaa arvoa, referenssi on aina alustettava esittelyn yhteydessä (kerrottava, minkä muuttujan toinen nimi se on ).

Kaikkiin referensseihin liittyy joukko rajoituksia:

  1. Referenssi ei voi viitata toiseen referenssiin
  2. Referenssistä ei voi ottaa osoitetta
  3. Ei voida luoda referenssi-taulukoita
  4. Referenssi on alustettava esittelyn yhteydessä, paitsi jos referenssi on luokan jäsen tai referenssi palautetaan funktiosta tai välitetään funktioon argumenttina Seuraavassa esimerkki riippumattomasta referenssistä:
#include <iostream>
 
main()
{
  int  x;
  int  &ref = x;
  int  &ref2 = 99;
  x = 10;
  ref = 100;
 
  cout << x << "  "<< ref << "\n";
 
  x = ref2;
  cout << x << "  "<< ref << "\n";
 
  return 0;
}

Tulostaa:

100  100
99  99

copy-konstruktori

Aikaisemmin havaittiin, että välitettäessä funktioon olio tai palautettaessa funktiosta olio ja yleisesti sellaisissa tilanteissa, joissa järjestelmä luo automaattisesti väliaikaisen olion, kutsuttiin luokan destruktoria muttei konstruktoria. Tästä aiheutui ongelmia, mikäli destruktorissa oli esimerkiksi muistin vapautuksia.

Eräs ratkaisu ongelmaan on se, että suunnitellaan luokka siten, ettei destruktorissa tarvitse vapauttaa muistia. Usein tällainen toteutus on kuitenkin vaikea.

Parempi ratkaisu asiaan on kuormittaa konstruktorifunktiota ns. copy-konstruktorilla. Copy konstruktori on aina tiettyä muotoa ja järjestelmä tunnistaa sen siten, että väliaikaisille olioille kutsutaan automaattisesti kyseistä konstruktoria.

Copy-konstruktori on muotoa: X( const X& )

#include <iostream>
#include <stdio.h>
 
class  samp {
  int  a;
public:
  samp( int x ) {
     a = x;
    printf("konstr: %p  %d\n", this, a );
  }
// copy konstruktori
//  samp( const samp &z ) {
//    a = z.a;
//    printf("copy-konstr: %p  %d\n", this, a );
//  }
 
  ~samp() {
    printf("destr: %p  %d\n", this, a );
  }
  int get_a() { return a; }
  void set_a(int x) { a = x; }
};
 
samp  nelio( samp  x )
{
  samp  tulos(-1);
  tulos.set_a ( x.get_a() * x.get_a() );
  return tulos;
}
 
main ()
{
  samp  ob(5);
  samp  tulos( 0 );
 
  tulos = nelio( ob );
     printf("%p: tulos.a = %d\n", &tulos, tulos.get_a() );
  return 0;
}

Testitulokset vasemmalla ilman copy-konstruktoria ja oikealla copy-konstruktorin kanssa

Ohjelma tulostaa:                Ohjelma tulostaa:
konstr: 09CF : 2278  5           konstr: 34F7:22A2  5
konstr: 09CF : 2276  0           konstr: 34F7:22A0  0
konstr: 09CF : 2246  -1          copy-konstr: 34F7:227E 5 (argum.)
destr: 09CF : 2246  25           konstr: 34F7:2270  -1
destr: 09CF : 2254  5  (argum.)  copy-konstr: 34F7:229E  25 (return)
destr: 09CF : 2274  25 (return)  destr:  34F7:2270  25
09CF: 2276: tulos.a = 25         destr:  34F7:227E  5 (argum.)
destr: 09CF : 2276  25           destr:  34F7:229E  25 (return)
destr: 09CF : 2278  5            34F7:22A0: tulos.a = 25
                                 destr: 34F7:22A0  25
                                 destr:34F7:22A2  5
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