Harjoittelen hieman olio-ohjelmointia C++:lla ja keräilen tähän pieniä kysymyksiä, jotka ovat hieman hämärän peitossa.
Kuinka paljon kannattaa ohjelmaa pilkkoa eri tiedostoihin? Onko hyvä tapa tehdä jokaiselle luokalle oma .h ja .cpp tiedosto? Vai enemminkin laittaa yhden aihepiirin luokat samaan .h ja .cpp tiedostoihin?
Onko mahdollista tai järkevää tehdä kaksi luokkaa, joilla on toisiinsa osoittimia/viittauksia?
Esim: on luokat ihminen ja talo. Talolla on asukkaina ihmisiä ja ihmiset omistavat taloja. Oman järjen mukaan molemmilla ei voi olla toisiaan oikeina jäseninä, mutta luulisi että osoittimet olisivat mahdollisia.
Jiffy kirjoitti:
Kuinka paljon kannattaa ohjelmaa pilkkoa eri tiedostoihin? Onko hyvä tapa tehdä jokaiselle luokalle oma .h ja .cpp tiedosto? Vai enemminkin laittaa yhden aihepiirin luokat samaan .h ja .cpp tiedostoihin?
Jokaista luokkaa kohti vähintään headeri, johon voi laittaa toteutuksetkin Java-tyyliin (en tiedä, mitä C++ standardi sanoo tästä), tai sitten jokaista luokkaa kohti yksi headeri ja yksi .cpp-tiedosto
Lisäys:
Jiffy kirjoitti:
Onko mahdollista tai järkevää tehdä kaksi luokkaa, joilla on toisiinsa osoittimia/viittauksia?
Itse miellän niin, että suunnittelu pitää olla sen verran hyvässä kunnossa, ettei tule tarvetta tuollaiselle :)
Vielä yksi lisäys :P
Jiffy kirjoitti:
Esim: on luokat ihminen ja talo. Talolla on asukkaina ihmisiä ja ihmiset omistavat taloja.
Tuollainen kannattaisi varmaan tehdä niin, että talo säilöö asukkaidensa lisäksi omistajansa jäsenmuuttujiin.
Tässäpä tiedostoihin jakoon liittyvää asiaa on selitetty melko mukavasti.
Jyrkkien mielipiteiden anto tästä asiasta on mielestäni ehdottomasti väärin, ellei jokin ohjelmointityyli erityisesti vaadi jotain tiettyä tapaa.
On fiksua laittaa ihmiset ja talot luokat vaikka asuminen otsikko ja kooditiedostoihin, mikäli ne ovat pieniä ja kerralla hallittavia kokonaisuuksia SEKÄ kuuluvat selkeästi yhteen. Tällä tarkoitan sitä, että jos ohjelmassa on koira ja sen sukupuu niin ne kuuluvat taas toiseen osastoon. Jos taas ohjelma koostuu ihmisestä, talosta ja autotallista, laitetaan jokainen erikseen omaan tiedostoonsa.
Lisäksi asioiden niputtamiseen kannattaa ajatella myös miten niitä käytetään. Jos luokkaa ruuvi ja meisseli käytetään yhdessä aina, tunge ne samaan tiedostoon.
Tiedostoihin hajauttamisella on kaksi tehtävää. Nopeuttaa kääntöä ja helpottaa ymmärtämistä. Jos saat itse päättää tee kuinka haluat, mutta lue jaskan linkki ja muut mielipiteet, ettet opettele mitään todella kummallista tapaa.
Ja on aivan mahdollista, että esittelee luokan ja käyttää osoittimen läpi sitä.
Viittaus ei voi olla jäsenenä.
class foo{ int& a; };
ei nyt vain ole mahdollinen.
class foo{ void bar( int& a); };
taas on aivan mahdollinen. Intin tilalla voi olla mikä tahansa tyyppi, luokkakin.
Mutta on huonoa suunnittelua, ja itkemisen aiheen etsimistä, jos kaksi luokkaa sekaantuvat toisiinsa voimakkaasti. Totuushan tästä on, että periyttäminen ja virtuaalifunktiot voivat ihan oikeasti vaatia tuota.
temppuhan menee jotenkin näin:
//minunluokka.hh class luovaLuokka; //Tärkeä taika. Esittelee kääntäjälle, että tämmönen tulee. //vt. funktion esittely ennen main()ia ja toteutus main()in jälkeen. class minunLuokka{ public: minunLuokka(luovaLuokka * luoja); void teeTemppu(); /* Tämä ei toimisi: void teeTemppu(){luojani->varmasti()?foo():bar();}; luojaLuokasta ei olisi tietoa tässä vaiheessa. ja teeTemppu() olisi inline funktio. */ private: luovaLuokka * luojani; } //minunluokka.cc #include "minunluokka.hh" #include "luovaluokka.hh" minunLuokka::minunLuokka(luovaLuokka * luoja){ luojani = luoja; } void minunLuokka::teeTemppu(){ if(luoja->varmasti()){ //mielikuvitusta tähän }else{ // toista mielikuvitusta } } //luovaLuokka.hh #include "minunluokka.hh" class luovaLuokka{ public: luovaLuokka(); bool varmasti(); void pyydaTemppua(); private: minunLuokka * luotu; } //luovaluokka.cc #include "luovaluokka.hh" luovaLuokka::luovaLuokka(){ luotu = new minunLuokka(this); } void luovaLuokka::pyydaTemppua(){ luotu->teeTempu(); }
Tuosta puuttuu #ifndef #define #endif litanniat.
Mutta on mahdollista ja tekee melko sotkuiseksi. Toivottavasti en johdattanut pimeälle puolelle. Jos tuollaista tuntuu kaipaavan, niin kysy miksi tuo tarvitaan ja voiko sen tehdä jotenkin toisin ilman sitä ja kenties paremmin.
vuokkosetae kirjoitti:
Viittaus ei voi olla jäsenenä.
Viittaus voi olla jäsenenä, mutta se pitää alustaa heti konstruktorissa.
Jiffy kirjoitti:
Harjoittelen hieman olio-ohjelmointia C++:lla ja keräilen tähän pieniä kysymyksiä, jotka ovat hieman hämärän peitossa.
Kuinka paljon kannattaa ohjelmaa pilkkoa eri tiedostoihin? Onko hyvä tapa tehdä jokaiselle luokalle oma .h ja .cpp tiedosto? Vai enemminkin laittaa yhden aihepiirin luokat samaan .h ja .cpp tiedostoihin?
Onko mahdollista tai järkevää tehdä kaksi luokkaa, joilla on toisiinsa osoittimia/viittauksia?
Esim: on luokat ihminen ja talo. Talolla on asukkaina ihmisiä ja ihmiset omistavat taloja. Oman järjen mukaan molemmilla ei voi olla toisiaan oikeina jäseninä, mutta luulisi että osoittimet olisivat mahdollisia.
Valinta luokkien jakamisesta tiedostoihin riippuu niiden koosta ja keskinäisistä riippuvuuksista. Jos sinulla on paljon pieniä luokkia, jotka kuuluvat samaan perintäshierarkiaan tai johonkin muuhun loogiseen kokonaisuuteen, ei ole mielestäni tarpeellista laittaa jokaista omaan tiedostoonsa.
Toisessa ääripäässä on tapaus, jossa yhden luokan voi jakaa useampaan tiedostoon ns. pointer to implementation -tekniikan avulla. Kun teet laajempaa C++-projektia, havaitset, että yhden luokan muuttaminen vaatii yleensä kaiken siitä riippuvan koodin kääntämistä uudelleen. Tämä ei ole enää hauskaa, kun se vie paria minuuttia suuremman ajan, mitä se helposti voi tehdä varsinkin malleja paljon käyttävän koodin kanssa.
Voit välttää muutoksista johtuvaa uudelleenkääntämistä siirtämällä luokan private-osan omaan luokkaansa, jonka määrittelet vain tätä tarkoitusta varten. Alkuperäisessä luokassa on sen sijasta osoitin tähän luokkaan, ja tämän takia yksityisen luokan muutokset eivät vaadi alkuperäistä luokkaa käyttävän koodin muuttamista. Mikään muu koodi ei käytä tätä implementation-luokkaa suoraan.
Tästä tekniikasta käytetään lyhyttä nimeä PIMPL, joka tulee sanoista pointer to implementation (tai private implementation joidenkin Google-osumien mukana). Se on hyvä tekniikka pitää mielessä, mutta sitä ei tietenkään tule käyttää joka kerta. Google-haulla löydät lisää tietoa ja konkreettisia esimerkkejä.
Ihan hyvä yleinen tapa on tehdä yksi luokka tiedostoa kohti ja poiketa tästä vain perustellusta syystä. Lisäksi kannattaa tarpeen mukaan käyttää eri hakemistoja, etteivät kaikki tiedostot ole sekaisin yhdessä. Muista myös nimiavaruudet.
Keskinäiset osoittimet eri luokissa ovat mahdollisia ja joskus pakollisia ratkaistavan ongelman luonteen takia. C++ FAQ -ohjeessa on oma kappaleensa erilaisten osoittimia käyttävien hierarkioiden serialisoinnista. Kannattaa tutustua aiheeseen, ettei monimutkaisen luokkarakenteen pohjalta rakennetun tiedon serialisointi osoittaudu sitten käytännössä yllättävän vaikeaksi.
http://www.parashift.com/c -faq-lite/serialization.html
(Esim. ohjelman tietojen tallentaminen tiedostoon on yksi esimerkki tiedon serialisoinnista. Yleisesti se tarkoittaa sen muuttamista joksikin ulkoiseksi esitystavaksi, josta ohjelman pitää myös voida lukea se takaisin, kun sitä ajetaan myöhemmin uudestaan tai eri koneessa.)
Pekka Karjalainen kirjoitti:
Voit välttää muutoksista johtuvaa uudelleenkääntämistä siirtämällä luokan private-osan omaan luokkaansa, jonka määrittelet vain tätä tarkoitusta varten. Alkuperäisessä luokassa on sen sijasta osoitin tähän luokkaan, ja tämän takia yksityisen luokan muutokset eivät vaadi alkuperäistä luokkaa käyttävän koodin muuttamista. Mikään muu koodi ei käytä tätä implementation-luokkaa suoraan.
Kuulostaa taas hommalta joka sopisi paremmin kääntäjälle ja se että ohjelmoija joutuu itse tuon tekemään taas vaikuttaa lähinnä ohjelmoijan kiusaamiselta. (Toki joitain erikoistapauksia varten voisi olla flagi ettei näin saa tehdä automaattisesti)
Grez kirjoitti:
Pekka Karjalainen kirjoitti:
Voit välttää muutoksista johtuvaa uudelleenkääntämistä siirtämällä luokan private-osan omaan luokkaansa, jonka määrittelet vain tätä tarkoitusta varten. Alkuperäisessä luokassa on sen sijasta osoitin tähän luokkaan, ja tämän takia yksityisen luokan muutokset eivät vaadi alkuperäistä luokkaa käyttävän koodin muuttamista. Mikään muu koodi ei käytä tätä implementation-luokkaa suoraan.
Kuulostaa taas hommalta joka sopisi paremmin kääntäjälle ja se että ohjelmoija joutuu itse tuon tekemään taas vaikuttaa lähinnä ohjelmoijan kiusaamiselta. (Toki joitain erikoistapauksia varten voisi olla flagi ettei näin saa tehdä automaattisesti)
Kieli joka tekisi tämän automaattisesti olisi jokin muu kieli kuin C++. Vetoan tässä tunnettuun C++-auktoriteettiin hieman :)
Scott Meyers kirjoittaa Effective C++ 3rd ed. -kirjassa näin (s. 140 Item 31)
Scott Meyers kirjoitti:
The problem is that C++ doesn't do a very good job of separating interfaces from implementations. A class definition specifies not only a class interface but also a fair number of implementation details.
Meyers jatkaa kertomalla, että yksi syy tähän on se, että C++:ssa halutaan kääntäjän voivan määrittää luokan instanssin koon muistin varausta varten (ja varmaan näin on tarpeen tehdä useissa muissakin kielissä). Tämä tieto riippuu luokan jäsenistä ja niiden sijoittelusta muistiin, jonka määrittäminen vaatii tarkan tiedon luokan rakenteesta. Siksi private-sana ei C++-kielessä kapseloi kaikkea tietoa luokan sisällöstä. Se estää sääntöjä noudattavaa ohjelmoijaa käsittelemästä näitä kenttiä suoraan ja kertoo koodin lukijalle tärkeän asian, mutta kääntäjä käsittelee myös private-määreiden tietoa eri tavoin käännöksen aikana (jonka takia muutokset private-osaan vaativat siitä riippuvan koodin kääntämistä uudelleen). Toisin sanoen: Luokan toteutus on C++-kielessä kytketty oletuksena läheisesti sitä käyttävän koodin rakenteeseen, ainakin kääntäjän näkökulmasta.
Tämän ongelman voi ratkaista sijoittamalla luokan datajäsenet osoittimien taakse, koska osoittimilla on tunnettu koko ja muut ominaisuudet, vaikka kaikkea tietoa niiden osoittamisen kohteena olevasta datasta ei olisikaan käsillä. Osoittimen käyttötapa tai rakenne ei yleensä muutu, vaikka sen osoittaman datan rakenne muuttuisikin. Tämä olisi hieman kuin se, mitä Java tekee oletuksena. Luokkien instansseja käsitellään Javassa aina viittauksien kautta, jotka ovat eräänlaisia rajoitettuja osoittimia. (Unohdetaan nyt ne perustason int- ym. tyypit, esimerkin vuoksi.)
C++:ssa on kuitenkin tarkoitus antaa ohjelmoijan valita käsitteleekö hän dataansa suoraan esim. varaamalla sille muistin pinosta, toisen luokan instanssin sisästä tai muulla tavoilla, vai käsitteleekö hän sitä osoittimien tai viittauksien kautta. Ja siksi C++-kielessä kiusataan ohjelmoijaa tällaisilla asioilla ja valinnoilla, jotka pitää itse tehdä tarpeen mukaan. Jos kääntäjä tekisi jotain tällaista automaattisesti, ei se olisi C++:n suunnitteluperiaatteiden mukaista. Meillä olisi joku toinen kieli silloin.
It's C++. Deal with it.
Kertauksen vuoksi lisäisin vielä lopuksi jotakin. En sano, että tätä PIMPL-tekniikkaa tulisi käyttää aina, tai edes usein. Sillä on myös omat puutteensa, esimerkiksi siitä joutuu maksamaan usein pienen hinnan suorituskyvyssä, ja se voi vaikeuttaa debuggerin käyttöä.
Se vain on tunnettu tapa ratkaista mahdollinen ongelma, nimittäin liialliset riippuvuudet eri luokkien tai ohjelman osien välillä, sekä tästä johtuvat pitkät käännösajat ym. ongelmat. Se löytyy myös alan kirjallisuudesta (ym. kirjassa se on mainitsemani Item 31:n varsinainen aihe) ja luultavasti siten myös kirjansa lukeneiden koodista, joten se kannattaa tuntea ainakin nimeltä ja yleisellä tasolla. Tai sitten täytyy vain käyttää muita kieliä, joissa tätä asiaa ei tarvitse koskaan (eikä voi) pitää mielessä.
Aihe on jo aika vanha, joten et voi enää vastata siihen.