Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C++: Aloittelevalle ohjelmoijalle esimerkkikoodia

Sivun loppuun

themos [24.03.2009 09:20:46]

#

Olen tässä jonkin aikaa harjoitellut ohjelmointia c kielellä ja siirtynyt vähän aikaa sitten c++ pariin. Sen verran pitälle olen päässyt, että olen opetellut käyttämään luokkia. Tässä kohtaa olen jäänyt jumiin. Olen ohjelmoinut yksinkertaista peliohjelmaa, jolla arvotaan lottonumeroita ja vikinglottonumeroita. Monella tavalla olen sen saanut toimimaan aina opettelemalla uuden asian ja sitä mukaa muuttamalla koodia. Nyt olen sitten viikon jauhanut ja yrittänyt ohjelmoida kyseistä ohjelmaa luokilla, mutta ei onnistu ei. Jos jollakulla olisi ylimääräistä aikaa niin tekisi esimerkkikoodin, miten ohjelma pitäisi tehdä. Saisi edes jonkin käsityksen miten luokat toimivat ohjelmissa. Netti on oppaita pullollaan ja kirjojakin on, mutta eipä ole juuri apua ollut, kun niissä ei mitään oikeita esimerkkiohjelmia ole. Jotain koiraa ja kissa kyllä löytyy luokilla jotka ei mitään tee. Kiitos jo etukäteen.

Teuro [24.03.2009 09:37:49]

#

#include <vector>
#include <iostream>

class lottoNumero{
  private:
    int alaRaja;
    int ylaRaja;
    int numero;
  public:
    lottoNumero(int ala, int yla){
      this->alaRaja = ala;
      this->ylaRaja = yla;
    }

    int annaNumero(){
      return this->numero;
    }

   void asetaNumero(int n){
     this->numero = n;
   }
};

class lottoRivi{
  private:
    int maara;
    std::vector<lottoNumero>arvotut;
  public:
    lottoRivi(int n){
      this->maara = n;
    }

    void lisaaNumero(lottoNumero n){
      this->arvotut.push_back(n);
    }

    void tulostaRivi(){
      for(unsigned int a = 0; a < this->arvotut.size(); a++){
        std::cout << this->arvotut[a].annaNumero() << std::endl;
      }
    }
};

Jotenkin tuosta lähteä rakentelemaan? Muokattu hieman koodia.

goala [24.03.2009 15:38:22]

#

Töissä väänsin, ei välttämättä käänny, mutta.

Esim. näin:

#include <iostream>
#include <vector>
#include <ctime>
#include <cassert>
#include <stdlib.h>

class LottoNumero
{
        public:
                explicit LottoNumero(int numero, bool arvottu = false)
                : _numero(numero)
                , _arvottu(arvottu)
                {
                        // Sinänsä turhaa.. mutta joo.

                        assert(numero > 0 && numero < 38);
                }

        public:
                bool    _arvottu;       //<! Onko numero jo arvottu?
                int     _numero;        //<! Itse lotto-pallo.
};

class Lotto
{
        public:
                Lotto()
                {
                        // Alustetaan lottopallot..

                        for (int i = 1; i < 38; i++) {
                                _arvotutNumerot.push_back(LottoNumero(i));
                        }

                        /** Alustetaan satunnaislukugeneraattori.
                          * Suosittelen tutustumaan myös esim. boostin
                          * mersenne twisteriin.
                          */

                        srand(time(NULL));
                }

                ~Lotto()
                {
                }

                std::vector<int> Arvo()
                {
                        // Pistetään pallot pyörimään..

                        for (std::vector<LottoNumero>::iterator i = _arvotutNumerot.begin(); i != _arvotutNumerot.end(); i++) {
                                *i->_arvottu = false;
                        }

                        int arvotutNumerot = 0;
                        std::vector<int> numerot;

                        // ..ja arvotaan seitsemän numeroa!

                        while (arvotutNumerot < 7) {
                                int numero = rand() % 37;

                                while (_arvotutNumerot[numero]._arvottu) {
                                        numero = rand() % 37;
                                }

                                _arvotutNumerot[numero]._arvottu = true;
                                numerot.push_back(_arvotutNumerot[numero]._numero);

                                ++arvotutNumerot;
                        }

                        return numerot;
                }

        private:
                std::vector<LottoNumero> _arvotutNumerot;
};

int main()
{
        Lotto *lotto = new Lotto();
        std::vector<int> oikeaRivi = lotto->Arvo();

        std::cout << "Iltaa, arvomme loton numerot: ";

        for (std::vector<int>::iterator i = oikeaRivi.begin(); i != oikeaRivi.end(); i++) {
                std::cout << *i << " ";
        }

        delete lotto;
}

Teuro [24.03.2009 18:09:45]

#

Toi mun eka vastaus oli verraten huono, mutta seuraavasta esimerkistä saattaapi olla jopa hiukan hyötyä aloittajalle. Saa kommentoida vapaasti. Lyhyt kuvaus toiminnasta:

Aluksi luodaan todella yksinkertainen poikkeusluokka erilaisille virheille, jotta koodi pysyy hieman siistimpänä. Seuraavaksi luodaan lottoNumeroille luokka, jotka siis lisätään varsinaiselle lottoriville.

Lottorivi tarkistaa lisäyksen yhteydessä syötteen oikeellisuuden. Syöte voi olla liian iso, jolloin generoituu poikkeusolio, samoin kuin liian pienellä syötteellä, sekä listalla jo olevasta numerostakin.

Lottonumeroluokkassa on ylikuormitettuna vertailuoperaattorit < ja == operaattoreille, joita käyttävät std::sort() ja std::find() algoritmit. Koodi on testattu ja tuntuu toimivan oikein.

#include <vector>
#include <iostream>
#include <algorithm>
#include <ctime>

class poikkeus{
	private:
		std::string viesti;
	public:
		poikkeus(std::string v){
			this->viesti = v;
		}

		std::string annaViesti(){
			return this->viesti;
		}
};

class lottoNumero{
	private:
		int numero;
	public:
		int annaNumero(){
			return this->numero;
    	}

		void asetaNumero(int n){
			this->numero = n;
		}

		bool operator <(const lottoNumero &toinen)const{
			return (numero < toinen.numero);
		}

		bool operator ==(const lottoNumero &toinen)const{
			return (numero == toinen.numero);
		}
};

class lottoRivi{
	private:
		int maara;
		int alaRaja;
		int ylaRaja;

		std::vector<lottoNumero>arvotut;
	public:
		lottoRivi(int n, int a, int y){
			this->maara = n;
			this->alaRaja = a;
			this->ylaRaja = y;
    	}

		void lisaaNumero(lottoNumero n){
			if(n.annaNumero() < this->alaRaja){
				throw poikkeus("Annettu numero on liian pieni!");
			}

			if(n.annaNumero() > this->ylaRaja){
				throw poikkeus("Annettu numero on liian suuri!");
			}

			std::vector<lottoNumero>::iterator haku;
			haku = std::find(this->arvotut.begin(), this->arvotut.end(), n);

			if(haku != this->arvotut.end()){
				throw poikkeus("Annettu numero on jo listalla! ");
			}else{
				this->arvotut.push_back(n);
			}
    	}

    	int annaMaara(){
			return this->maara;
		}

		int annaYlaRaja(){
			return this->ylaRaja;
		}

		int annaAlaRaja(){
			return this->alaRaja;
		}

		void tulostaRivi(){
			std::sort(this->arvotut.begin(), this->arvotut.end());

			for(unsigned int a = 0; a < this->arvotut.size(); a++){
				std::cout << this->arvotut[a].annaNumero() << std::endl;
      		}
    	}
};

int main(){
	lottoRivi eka(7, 1, 39);

	std::srand ( time(NULL) );

	for(unsigned int a = 0; a < eka.annaMaara(); a++){
		int satu = rand() % eka.annaYlaRaja() + eka.annaAlaRaja();
		lottoNumero num;
		num.asetaNumero(satu);
		try{
			eka.lisaaNumero(num);
		}catch(poikkeus p){
			std::cout << p.annaViesti() << std::endl;
			a--;
		}
	}

	eka.tulostaRivi();

	system("pause");
	return EXIT_SUCCESS;
}

koo [24.03.2009 20:04:27]

#

Minusta luokkien käytön idea ihan kielestä riippumatta on siinä, että asiat voidaan jäsentää kokonaisuuksiksi, jotka eivät mene niin helposti rikki ja sotkuun. Luokista tehdyillä olioilla on tehtävänä pitää itsensä järkevässä kunnossa ja hoitaa omat asiansa - tai jos tämä ei onnistu, heittää poikkeus eli exception.

Tämän puolesta en oikein ymmärrä, miksi esimerkiksi lottonumeron pitäisi tietää, onko hänet arvottu vai ei. Luulisi sen olevan enemmän lottokoneen vastuulla.

Aloittelijalla on ihan tarpeeksi tekemistä C++:n syntaksin ja kirjaston kanssa. Tavallisesti tulee tehtyä aika paljon epäolennaista koodia. Minusta näissä esimerkeissäkin on vähän jotain sen suuntaista.

Järkevässä koodissa luokat ovat yleensä aika pieniä ja funktiot melko lyhyitä. Yleensä luokkien periytymisketjut ovat aika lyhyitä. Template-funktioita ja -luokkia kyllä käytetään, monet perustekniikat ovat ihan ok, mutta kovin edistykselliset rakennelmat ovat ärsyttävän vaikeaselkoisia.

Kovat hemmot eivät näytä kisaavan varsinaisesti siitä, kuka eniten kirjoittelee koodia, vaan ennemminkin siitä, miten vähällä koodilla taaskin selvittiin. Tai mistäs minä sitä oikeasti tietäisin.

Jotta kysyjän pää nyt saataisiin varmasti pyörälle, niin loihdinpa tähän oman mallini siitä, miten lottoa voisi pyöritellä.

Ensinnäkin, tietokoneen laskenta ei tavallisissa yhteyksissä ole sattumanvaraista, vaan sattumaakin pitää simuloida. Jotta lottopallot saadaan sekaisin, tehdään erillinen Sekoittaja-luokka. Tämä on järkevää siksi, että satunnaislukujen tuottaminen vaatii oikeasti laajaa tietoa ja ymmärrystä ja kun sellaista karttuu, voidaan Sekoittaja-komponentti yksinkertaisesti vaihtaa ilman, että täytyy koko ohjelma käydä muuttamassa. Sekoittajan vastuulla on vain osata tulla alustetuksi jollakin tavalla ja antaa pyydettäessä satunnainen numero. Tässä tiedosto Sekoittaja.hpp:

#ifndef SEKOITTAJA_HPP
#define SEKOITTAJA_HPP

class Sekoittaja
{
public:
    Sekoittaja();
    explicit Sekoittaja(int siemenluku);
    int operator()(int ylaraja);
};

#endif

Tuossa on vähän erikoinen operaattorimäärittely. Se voisi olla kyllä vaikka joku nimetty funktio, mutta tuo kutsuoperaattori () nyt vain sattuu sopimaan parhaiten tähän ja muuhunkin käyttöön tosi mukavasti. Joka tapauksessa, sen avulla saadaan satunnainen luku siten, että 0 <= luku < ylaraja.

Vähänkään suuremmissa kokonaisuuksissa luokat ja niihin läheisesti liittyvät funktiot kannattaa yleensä esitellä omissa header-tiedostoissaan. Vastavasti niiden toteutukset kannattaa panna omiin kooditiedostoihinsa. Tässä siis Sekoittaja.cpp:

#include "Sekoittaja.hpp"
#include <cstdlib>
#include <ctime>

Sekoittaja::Sekoittaja()
{
    // alustetaan systeemikellosta
    int siemenluku = std::time(0);
    std::srand(siemenluku);
}

Sekoittaja::Sekoittaja(int siemenluku)
{
    std::srand(siemenluku);
}

int Sekoittaja::operator()(int ylaraja)
{
    return std::rand() % ylaraja;
}

Sekoittajan ytimessä on standardikirjastosta löytyvät satunnaislukuvehkeet, jotka ovat aika huonoja. Surkeampia ovat vain omakeksimät viritykset. Tai jos ne ovat parempia, niiden tekijöillä on yleensä plakkarissaan tohtorin paperit jostakin yliopistosta.

Lottokone puolestaan on vehje, jossa on moottorina Sekoittaja ja jossa on haluttu määrä palloja. Sekoittaja ja pallot annetaan, kun konetta kasataan eli konstruktorissa. Lottokone osaa arpoa aina kerrallaan niin monta palloa kuin pyydetään. Esittelyt Lottokone.hpp:

#ifndef LOTTOKONE_HPP
#define LOTTOKONE_HPP
#include "Sekoittaja.hpp"
#include <vector>

class Lottokone
{
	Sekoittaja& sekoittaja;
	int palloja;
public:
	Lottokone(Sekoittaja& s, int pallolkm);
	std::vector<int> Arvo(int lkm);
};

#endif

Arvottujen numeroiden esittämiseen käytetään kirjaston vector-luokkaa, joka on tässä esimerkissä ihan sopiva. Siinä voi olla tarvittaessa vaihteleva määrä alkioita.

Lottokone.cpp:

#include "Lottokone.hpp"
#include <algorithm>
#include <stdexcept>

Lottokone::Lottokone(Sekoittaja& s, int pallolkm)
: sekoittaja(s), palloja(pallolkm)
{
    if (palloja < 0)
        throw std::runtime_error("outo maara palloja");
}

std::vector<int> Lottokone::Arvo(int lkm)
{
    if (lkm <= 0 || palloja < lkm)
        throw std::runtime_error("outo määrä arvottavia");

    std::vector<int> pallot;
    // täytetään kone
    for (int i = 0; i < palloja; ) pallot.push_back(++i);
    // sekoitetaan pallot
    std::random_shuffle(pallot.begin(), pallot.end(), sekoittaja);
    // otetaan tarvittava määrä
    pallot.resize(lkm);
    // ja annetaan ne nätisti
    std::sort(pallot.begin(), pallot.end());
    return pallot;
}

Lottokone tekee joitakin järkevyystarkasteluja. Jos se havaitsee virheen, se heittää exceptionin, joka on ihan peruskirjastosta löytyvää tyyppiä.

Verrattuna joihinkin muihin mahdollisiin tapoihin, numeroiden arvonnassa on aika vähän koodia. Homma tapahtuu Lottokoneen sisällä, joten voidaan aika hyvin olettaa, että kaikki numerot/pallot ovat kelvollisia. Sitten vain sekoitetaan kaikki pallot ja annetaan niitä haluttu määrä järjestettynä. Standardikirjastosta löytyy näppärät vehkeet noihinkin hommiin.

Simppelillä/kököllä pääohjelmalla voi sitten demota, kuinka homma pelaa:

#include "Lottokone.hpp"
#include <iostream>

void TulostaRivi(std::vector<int> const& rivi)
{
    for (int i = 0; i < rivi.size(); ++i)
        std::cout << " " << rivi[i];
    std::cout << "\n";
}

int main()
{
    Sekoittaja seko;

    Lottokone suomilotto(seko, 39);
    std::vector<int> rivi = suomilotto.Arvo(7);
    TulostaRivi(rivi);
    rivi = suomilotto.Arvo(7);
    TulostaRivi(rivi);

    Lottokone vikinglotto(seko, 48);
    TulostaRivi(vikinglotto.Arvo(6));
}

Luokkien kanssa puuhataan aika tavalla samoin kuin perinteisten funktioittenkin. Luokkien avulla homman jäsennys ja tehtävänjako on selkeämpää. Ja turvallisempaa, kun ihan mitä tahansa ei pysty ihan missä tahansa käpälöimään ilman erityisiä kepulikonsteja.

Jos tämä oli jokin harjoitustehtävä, niin kerrotko sitten, että mitä saatiin arvosanaksi.

Janezki [24.03.2009 20:22:19]

#

Heh, väännän itsekin tässä C++ olioita vanhojen Delphi-olioiden pohjalta. Esimerkkejä näyttää olevan jo riittämiin asti joten tyydyn toteamaan että olio-ohjelman ymmärtäminen on hieman hankalaa. Kyllähän tyhmempikin ymmärtää miten olio rakentuu sisältä päin mutta se että miten oliot saadaan toimimaan harmoniassa keskenään. Minulle Java oli vähän liian iso pala purtavaksi koska en ymmärtänyt miten sievästä proseduraalisesta pohjarakenteesta siirrytään puhtaaseen oliomaailmaan.

themos [24.03.2009 22:34:50]

#

Jep
Kiitoksia kovasti esimerkeistä. Alkoivat lotot pyörähtelemään ja pyörähtipä jo muukin ohjelma. Tässä juuri nyt tajusin miten siis tämä luokka juttu toimii. Eli mikä tekee mitäkin ja mihin se laitetaan koodissa. Onkohan noista kirjastoista olemassa mitään tietoa esim.listamuodossa tai jotain, mistä näkisi mikä sisältää mitäkin. Vastauksissakin on jo joitakin, mistä en ole kuullutkaan. Jatkossa pääsisi paljon helpommalla niinkuin Koo aikaisemmin totesi. Ja ei ole harjoitustehtävä. Sattuneesta syystä on vain aikaa ainakin tämän vuoden loppuun makoilla kotona ja koittaa keksiä jotain järkevää. Niinpä ajattelin opetella ohjelmointia mutta annetaan nyt suoraan vaikka kymppi

koo [24.03.2009 23:11:14]

#

Opiskeluvaiheessa ei ole aivan hirveätä, vaikka koodaisikin sellaista, mikä löytyykin jo peruskirjastosta. En kyllä tiedä, mikä olisi alkuun hyvä kirja tai lähde C++:n opiskeluun. Itse selailin aikoinaan itsensä Stroustrupin kirjaa (sitä saa kai suomeksikin). Standardikirjaston juttuja voi katsoa vaikka Dinkumwarelta tai C++ Referencestä, jälkimmäisessä on paremmin käyttöesimerkkejä (myös huonoja).

Tästä oliotouhusta sen verran, että monet proseduraaliset jutut ovat oikeastaan jo aika oliomaisia. Jos meillä on esimerkiksi C-meininkiä

FILE *filu = fopen("filu.txt", "w");
fprintf(filu, "heippa\n");

niin siinähän sitä jo on file-olio, johon liittyy tiettyjä funktioita. Oliommalla kielellä homma vain voidaan kasata niin, että toiminnallisuus ja vastuut voidaan jakaa järkevämmin. Palikkaesimerkissä tämä ei välttämättä ole niin ilmeistä, mutta suurisssa ohjelmistoissa edut ovat selviä.

Ei ole ihan oma keksintöni, että luokan olioiden tärkein tehtävä on pitää itsensä ehjässä ja järjellisessä kunnossa. Luokkaan tai sen lähimaastoon kerätään sen edustamaan asiaan liittyvät toiminnot. Näillä periaatteilla rajapinnoista tulee turvallisempia kuin perinteisellä proseduraalisella tyylillä.

Selkeyttä voi tulla muutenkin:

// Näinkö?
PiirraKolmio(5, -3, 42, 66, -17, 2);

// Vai näin?
Piste p1(5, -3);
Piste p2(42, 66);
Piste p3(-17, 2);
Kolmio k(p1, p2, p3);
Piirra(k);

Juu, oliomaisemmassa koodissa voi olla enemmän koodirivejä, niin kuin tässä. Olioisempi voi kuitenkin olla luettavampaa. Ja muokattavampaa, ajatellaanpa vaikka tilanteita, joissa ihan kaikki koordinaatit eivät olekaan sallittuja. Minne lisätään tarkastukset? Tai jos pitääkin piirtää jollekin eri laitteelle, pinnalle tms.?

Luokkien periyttäminen vastaa aika kivasti ihmisten tapaa hahmottaa ja luokitella asioita. Ei sitä silti joka paikassa tarvitse käyttää. Periyttämisen idea on rajapintojen uudelleenkäyttö: Samantapaiset otukset tekevät samantapaisia asioita, joten niitä voi komentaa samaan tapaan.

Jos ei oikein tahdo aueta, että miten proseduraalisesta ajatusmallista mennään oliomaiseen, voi olla, että kuvittelee oliotouhun olevat jotenkin turhan hienoa ja vaikeata. Kyllä se aika pienestäkin jo lähtee liikkeelle. Eikä niitä olioita ihan vaan olioiden takia olla tekemässä.

Suunnittelussa on kyllä paljon rajatapauksia. Milloin esimerkiksi suomilottokone on eri luokka kuin vikinglottokone, vai onko kyseessä vain lottokone, joka käyttäytyy asetettavien parametrien mukaan? Onko lottonumeroilla ja lottoriveillä jotakin sellaisia ominaisuuksia, joita ei ole ihan vaan tavallisilla numeroilla ja numeroiden joukoilla. Tällaisiin kysymyksiin ei ole aina yhtä ainoaa selvää vastausta, asia riippuu (käyttö-) tilanteesta, joka sekin voi muuttua. Oliomaailmassa muutosten teko on luontaisesti helpompaa, sitten kun tilanne muuttuu.

Yksi hankaluus on kyllä se, että C++ on aika raju vehje näiden juttujen opiskeluun. Syntaksia, ominaisuuksia, peukalosääntöjä ja kirjastojuttuja on niin helkutisti.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta