http://cpp.sh/ on hyödyllinen online-kääntäjä.
// sisällytetään iostream-otsake, joka sisältää
// syötteeseen ja tulostamiseen liittyviä asioita
#include <iostream>
// määritellään main-niminen funktio, jonka palautusarvo
// on tyyppiä int eli kokonaisluku
int main() {
// tulostetaan rivi "Hello, world!";
// std::endl tarkoittaa rivinvaihtoa
std::cout << "Hello, world!" << std::endl;
// palautetaan 0, joka kertoo käyttöjärjestelmälle,
// että ohjelma suoritettiin onnistuneesti
return 0;
}
/*
Ohjelman tuloste on:
Hello, world!
*/
Kommentit ovat osioita, joita kääntäjä ei ota huomioon. //
määrittelee kommentin rivin loppuun asti ja /*
määrittelee ns. monirivisen kommentin, joka päättyy merkkijonoon */
.
Muuttujia on monia erilaisia, mutta niiden määrittely toimii aina samalla tavalla:
tyyppi muuttujan_nimi;
Muuttujalle voidaan asettaa arvo joko määrittelyn yhteydessä tai jälkeenpäin. Molemmat seuraavista toimivat samalla tavalla:
tyyppi muuttujan_nimi = arvo;
tyyppi muuttujan_nimi;
muuttujan_nimi = arvo;
Tekstin tulostaminen ja käyttäjän syötteen lukeminen tapahtuu yleensä cout
ja cin
-virtojen kautta. Ne on määritelty iostream
-otsakkeessa. cout
-virtaan tulostetaan ja cin
-virrasta luetaan syötettä. Huomaa, että virtaan voi heittää muutakin kuin tekstiä:
int i;
std::cin >> i;
std::cout << "Muuttujan i arvo on " << i << "." << std::endl;
// Muuttujan i arvo on 42.
Voimme päättää mitä koodia suoritamme ehtolauseiden totuusarvon perusteella. Käytetyimpiä ehtolauseita ovat if
, else if
ja else
.
int i;
std::cin >> i;
if (i % 3 == 0) {
std::cout << "Luku " << i << " on jaollinen kolmella.";
} else {
std::cout << "Luku " << i << " ei ole jaollinen kolmella.";
}
Kun ketjutamme useita ehtoja peräkkäin, muodostamme ehtoketjuja. Ehtoketju alkaa aina if
-lauseesta ja jatkuu siitä joko else if
tai else
-lauseella. Useammat if
-lauseet peräkkäin eivät siis muodosta ehtoketjua. Ehtoketjun tarkastelu loppuu aina ensimmäiseen suoritettavaan lauseeseen. Esimerkki havainnollistaa toivottavasti paremmin kuin teksti:
if (false) {
std::cout << "totta";
} else if (true) {
std::cout << "totta2";
} else {
std::cout << "totta3";
}
/*
tuloste:
totta2
*/
Haluamme etsiä alkulukuja helpolla tavalla. Muistutuksena: alkuluku on luku, joka on jaollinen vain ykkösellä ja itsellään. Tietokoneilla voimme helposti kokeilla jaollisuutta monilla luvuilla peräkkäin, jotain tämän tapaista: (%
-operaattori tarkoittaa jakojäännöstä!)
int luku = 5;
if (luku % 2 == 0) {
std::cout << "Luku ei ole alkuluku.";
} else if (luku % 3 == 0) {
std::cout << "Luku ei ole alkuluku.";
} else if (luku % 4 == 0) {
std::cout << "Luku ei ole alkuluku.";
} else {
std::cout << "Luku on alkuluku!";
}
Hmm. Entä jos luku on 123?
int luku = 5;
if (luku % 2 == 0) {
std::cout << "Luku ei ole alkuluku.";
} else if (luku % 3 == 0) {
std::cout << "Luku ei ole alkuluku.";
} else if (luku % 4 == 0) {
std::cout << "Luku ei ole alkuluku.";
// ...
} else if (luku % 122) {
std::cout << "Luku ei ole alkuluku.";
} else {
std::cout << "Luku on alkuluku!";
}
Tähän on paljon parempia tapoja sekä tilankäytön kannalta ja nopeuden kannalta. Yksinkertaisin parannus nopeuden kannalta on pieni matemaattinen oivallus: on järjetöntä tutkia jaollisuutta arvoilla, jotka ovat suurempia kuin tutkittavan luvun neliöjuuri.
Tilankäytöllisesti meitä auttavat toistorakenteet while
ja for
. Huomaa, että i++
on lyhennysmerkintä seuraavalle: i = i + 1
.
int i = 0;
while (i < 5) {
std::cout << i << std::endl;
i++;
}
/*
tuloste:
0
1
2
3
4
*/
for
-lauseella sama:
for (int i = 0; i < 5; i++) {
std::cout << i << std::endl;
}
/*
tuloste:
0
1
2
3
4
*/
while
-lause toistaa blockia niin kauan kuin määritelty ehto on totta. for
-lause koostuu kolmesta osasta: for (1; 2; 3)
- suoritetaan yhden kerran silmukan alussa (yleensä määritellään indeksimuuttuja)
- ehto
- mitä tehdään "kierrosten" välissä
Palataan takaisin alkulukuesimerkin pariin. Ohjelma häviää paljon tehokkuudessa, mutta esimerkin on tarkoitus olla selkeä.
#include <iostream>
#include <cmath>
// sqrt() eli neliöjuurifunktio on määritelty tässä otsakkeessa
int main() {
std::cout << "Syötä luku: ";
int n;
std::cin >> n;
if (n == 1) {
std::cout << "Luku ei ole alkuluku." << std::endl;
return 0;
}
if (n == 2) {
std::cout << "Luku on alkuluku." << std::endl;
return 0;
}
for (int i = 2; i <= sqrt(n); i++) {
if (n % i == 0) {
std::cout << "Luku ei ole alkuluku." << std::endl;
// kun palautusarvo on annettu, alempana olevaa koodia ei enää suoriteta
return 0;
}
}
std::cout << "Luku on alkuluku." << std::endl;
return 0;
}
Funktiot ovat toinen tapa koodin toistuvuuden vähentämiseen. Jos on jokin tietty asia, jota halutaan tehdä monta kertaa, on järkevää laittaa se funktion sisään. Ne määritellään aina samalla rakentella, joka muistuttaa lievästi muuttujien määrittelyä.
palautustyyppi funktion_nimi(parametrin_tyyppi parametrin_nimi, ...) {
// koodia!
return jotain;
}
Muuttujan jotain
tyyppi pitää olla tyypiltään palautustyyppi. Jos funktiolle ei haluta palautusarvoa, palautustyypiksi määritellään void
. Funktiota kutsutaan rakenteella funktion_nimi(parametri1, parametri2, ...)
:
void tulostajotain() {
std::cout << "jotain" << std::endl;
}
tulostajotain();
/*
tuloste:
jotain
*/
Tässä on esimerkkiohjelma, joka laskee ilmanpaineen annetulla korkeudella.
#include <iostream>
#include <cmath> // exp(x) = e^x
using std::cout;
using std::cin;
using std::endl;
float pressure(float altitude, float p0) {
return p0 * exp(altitude*-1/8421);
}
int main() {
while (true) {
float altitude;
float p0;
cout << "Syötä ilmanpaine maanpinnan tasolla kilopascaleissa: ";
cin >> p0;
cout << "Syötä korkeus metreissä (0 lopettaa): ";
cin >> altitude;
if (altitude == 0) {
// break-avainsana lopettaa silmukan suorituksen
break;
}
if (altitude < 0 || pressure < 0) {
cout << "Korkeuden ja nollapaineen on oltava positiivisia!" << endl << endl;
// continue-avainsana lopettaa tämän kierroksen suorituksen ja siirtyy suoraan seuraavaan
continue;
}
cout << "Ilmanpaine " << altitude << " metrin korkeudessa on " << pressure(altitude, p0) << "kPa" << endl << endl;
}
}
Huomaat muutaman asian, jota ei vielä olla käsitelty. using
-avainsanalla voidaan määritellä käytössä olevat nimiavaruudet tai osia niistä. Kun alussa määritellään using std::cout
, tarvitsee meidän lopussa koodissa kirjoittaa enää cout
, kun haluamme tulostaa. Ehtolauseessa on käytetty tai-operaattoria, jota merkitään ||
. Ja-operaattoria merkitään &&
.
Tee ohjelma, joka kysyy käyttäjältä luvun. Jos luku on jaollinen kolmella, tulota Fizz. Jos luku on jaollinen viidellä, tulosta Buzz. Jos luku on jaollinen molemmilla, tulosta FizzBuzz.
Tee ohjelma, joka kysyy käyttäjältä luvun. Tulosta luvun verran Fibonaccin lukujonoa. Fibonaccin lukujonon kaksi ensimmäistä lukua ovat ykkösiä. Seuraava luku on aina kahden edellisen summma. Lukujono on siis 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
Esimerkkiratkaisut harjoitustehtäviin tulevat seuraavan luvun jälkeen. Yritä kuitenkin ratkaista tehtäbät ensin itse.
C++ on oliopohjainen ohjelmointikieli. Olioiden olemusta on vaikea selittää ilman esimerkkejä, mutta ne ovat luokkien ilmentymiä. Luokat taas ovat "kokoelmia" muuttujista ja funktioista. Luokat on tapana nimetä isolla alkukirjaimella. Seuraavasti voidaan määritellä luokka Ihminen:
class Ihminen {
public:
int age;
float height;
float weight;
};
Ihminen juho;
juho.age = 18;
juho.height = 180.2;
juho.weight = 75.5;
Avainsana public
tarkoittaa, että sitä seuraavat muuttujat ja funktiot ovat julkisia, eli näkyvissä olion ulkopuolella. private
-muuttujat näkyvät vain luokan sisällä ja protected
-muuttujat luokan ja alaluokkien sisällä. Luokan määrittelyn jälkeen luomme olion. Se tapahtuu samalla tavalla kuin muuttujan määritteleminen, nyt tyyppi on vain luokan nimi. Voimme kirjoittaa ja lukea juho
-olion muuttujia syntaksilla muuttuja.oliomuuttuja
.
Luokalle voidaan myös määritellä funktioita, joista erityisasemassa ovat konstruktori ja destruktori. Niillä ei ole tyyppiä. Konstruktoria kutsutaan, kun olio luodaan, ja destruktoria, kun olio on poistumassa. Konstruktori on huomattavasti enemmän käytetty kuin destruktori. Luokan funktioiden nimien eteen tulee määriteltäessä Luokka::
.
#include <iostream>
using std::cout;
using std::endl;
class Ihminen {
public:
int age;
float height;
float weight;
Ihminen(int, float, float);
float bmi();
};
Ihminen::Ihminen(int age, float height, float weight) : age(age), height(height), weight(weight) {
}
float Ihminen::bmi() {
return weight/(height*height);
}
int main() {
Ihminen juho(18, 1.802, 75.5);
cout << "Juhon painoindeksi on " << juho.bmi() << endl;
}
Esimerkissä parametrien nimi ja nimi2 arvot kopioituvat suoraan oliomuuttujien oliomuuttuja1 ja oliomuuttuja2 arvoiksi.
Luokka::Luokka(tyyppi nimi, tyyppi2 nimi2, ...) : oliomuuttuja1(nimi), oliomuuttuja2(nimi2), ... {
}
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main() {
int luku;
cout << "Syötä luku: ";
cin >> luku;
if (luku % 15 == 0) {
cout << "FizzBuzz"
} else if (luku % 3 == 0) {
cout << "Fizz";
} else if (luku % 5 == 0) {
cout << "Buzz";
}
cout << endl;
return 0;
}
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int fibo(int n) {
if (n == 2 || n == 1) {
return 1;
} else {
return fibo(n-1) + fibo(n-2);
}
}
int main() {
int luku;
cout << "Syötä luku: ";
cin >> luku;
for (int i = 1; i <= luku; i++) {
cout << fibo(i) << endl;
}
return 0;
}
Listoissa voidaan säilyttää monta saman tyypin muuttujaa.
int lista[5] = {1, 2, 3, 4, 5};
cout << lista[0] << endl;
// tulostaa "1"
Listat on helppo käydä läpi for-silmukalla:
int lista[10];
for (int i = 0; i < 10; i++) {
lista[i] = i+1;
}
cout << lista[9] << endl;
// tulostaa "10"
Listoissa on se huono puoli, että niiden koko pitää tietää etukäteen. Käyttäjän syötteen kokoisen listan voi tehdä, mutta on suositeltavampaa käyttää vektoreita. Vektorit ovat dynaamisen kokoisia listoja; ne kasvavat sitä mukaa kun niihin lisätään uusia alkioita.
#include <iostream>
#include <vector>
using std::cout;
using std::cin;
using std::endl;
using std::vector;
float mean(vector<int> v) {
float s = 0;
for (auto el : v) {
s += el;
}
return s/v.size();
}
int main() {
vector<int> v;
while (true) {
cout << "Syötä positiivinen kokonaisluku (0 tai pienempi lopettaa): ";
int luku;
cin >> luku;
if (luku <= 0) {
break;
}
v.push_back(luku);
}
cout << "Syöttämiesi lukujen keskiarvo on " << mean(v) << endl;
return 0;
}
Vektori määritellään syntaksilla std::vector<tyyppi>
, ja siihen lisätään alkio syntaksilla v.push_back(alkio)
. For-silmukka käy läpi kaikki vektorin v
elementit. Muuttuja el
saa silmukan aikana kaikki alkioiden arvot. auto
on erikoistyyppi, joka osaa arvata mikä sen kuuluu olla. Tässä tapauksessa muuttujan el
tyypiksi tulee int
, koska vektorissa säilytetään int
-tyyppisiä alkioita.
Olet ehkä huomannut, että funktiossa muuttamamme arvot eivät muuta alkuperäisiä arvoja:
void muokkaa(int luku) {
luku = luku*2;
}
int luku = 1;
muokkaa(luku);
cout << luku << endl;
// tulostaa 1
Tämä johtuu siitä, että funktiota kutsuessa parametrit kopioituvat. Jos haluamme muokata alkuperäistä muuttujaa, meidän pitää antaa funktiolle parametriksi muuttujan muistiosoite. Funktiossa muutamme annetussa muistiosoitteessa annettua arvoa.
void muokkaa(int* luku) {
*luku = *luku*2;
}
int luku = 1;
muokkaa(&luku);
cout << luku << endl;
// tulostaa 2
Viite-tyyppi määritellään syntaksilla tyyppi*
. Viite-tyypissä säilytetään muistiosoitteita. Muutujan muistiosoitteen saa syntaksilla &muuttuja
. Edellisessä esimerkissä funktion muokkaa parametri luku on tyyppiä int-muistiosoite. Kun haluamme käsitellä muuttujaa jonka muistiosoitteen tiedämme, käytämme syntaksia *muuttuja
. C++:ssa on lyhentävä merkintätapa edellisen esimerkin kaltaisille tilanteille. Merkintää kutsutaan nimellä "passing by reference". Seuraava esimerkki tekee käytännössä saman kuin edellinen, mutta yhdellä erikoismerkinnällä neljän sijaan.
void muokkaa(int& luku) {
luku = 2*luku;
}
int luku = 1;
muokkaa(luku);
cout << luku << endl;
// tulostaa 2
Viitteet ja passing by reference ovat hyödyllisiä isoa määrää dataa käsitellessä, sillä kopiointi voi viedä paljon aikaa.
Vektoreita käsitellessä mainittiin, että on mahdollista luoda käyttäjän syötteen kokoinen lista. Se tapahtuu näin:
int koko;
cout << "Syötä listan koko: ";
cin >> koko;
int* lista = new int[koko];
lista[0] = 1;
cout << lista[0] << endl;
delete [] lista;
Mitä ihmettä oikein tapahtuu? Yksinkertaistettuna ohjelmallamme on kaytössä kahdenlaista muistityyppiä: stack-muisti ja heap-muisti. Stack-muisti on nopeaa, mutta sitä on vähän käytössä. Heap on hitaampaa, mutta sitä on paljon. Vakiomäärän tilaa vievät muuttujat säilytetään automaattisesti stackissa, mutta emme voi varata sieltä satunnaisen kokoisia paloja muistia ajon aikana.
Heapista varataan muistia new
-avainsanalla. Esimerkissä pyydämme tietokonetta antamaan meille viiden int
-tyypin kokoisen palan heap-muistia. new
-palauttaa muistiosoitteen varatun tilan alkuun, ja asetamme sen lista-muuttujaan. lista
-muuttujan tyyppi on viite int
-tyyppiin. Lista toimii nyt samalla tavalla kuin stack-muistissa oleva lista.
Toisin kuin stackissa, heapista varattu muisti pysyy varattuna kunnes kerromme tietokoneelle, että sen voi vapauttaa. Listan vapauttaminen toimii delete []
-avainsanalla. Jos muistia ei vapauteta, syntyy muistivuoto. Muistivuoto tarkoittaa sitä, että ohjelma tarvitsee koko ajan enemmän ja enemmän muistia ajon aikana.
Osaatko keksiä mitä seuraava esimerkki tulostaa?
void muokkaa(int lista[]) {
lista[0] = 2;
}
int lista[1];
lista[0] = 1;
muokkaa(lista);
cout << lista[0] << endl;
Edellisen selityksen perusteella voisi luulla, että lista kopioituu parametriksi, ja alkuperäinen arvo ei muutu. Listat ovat kuitenkin mielenkiintoinen poikkeustapaus. Kun luomme listan normaalilla tavalla (int lista[1]
), muuttuja on erikoista array-tyyppiä. Array-tyyppi toimii yleensä samalla tavalla kuin vastaava viite, sillä arrayn tyyppi muuttuu lähes aina käsiteltäessä viitteeksi. Yksi ero on tärkeää pitää mielessä: voimme tietää listan koon vain jos se on array-tyypin takana. Emme siis voi mitenkään tietää, mihin viitteen takana oleva lista päättyy, ellemme tallenna sen kokoa luodessa johonkin.
Olennaista on muistaa, että kun lista annetaan parametrina funktiolle, parametria ei kopioida, joten sen muokkaaminen muokkaa alkuperäistä listaa. Yllä oleva esimerkki tulostaa siis "2".
Muuttujat elävät C++:ssa vain siinä scopessa kuin ne on määritelty. Scopen määrittelevät aaltosulut {}
.
{ int a = 0; }
a++;
Yllä oleva esimerkki ei suostu kääntymään, koska a ei ole enää määritelty toisella rivillä. Samoin käy funktioiden kanssa:
void maarittele_muuttuja() {
int a = 0
}
maarittele_muuttuja();
a++;
Tässä kohtaa pointtereiden oikea käyttötarkoitus astuu kuvioihin. Voimme määritellä muuttujia heap-muistiin funktion sisällä ja palauttaa viitteen, joka osoittaa luotuun muuttujaan.
#include <iostream>
using std::cout;
using std::endl;
int* maarittele_muuttuja() {
int* a = new int;
*a = 10;
return a;
}
int main() {
int* a = maarittele_muuttuja();
(*a)++;
cout << *a << endl;
delete a;
return 0;
}
Jos olet ymmärtänyt viitteet ja muistin toiminnan, osaat sanoa että yllä oleva esimerkki tulostaa "11". (*a)
on suluissa, koska muuten ++
-operaattori kasvattaisi viitteen sisältämää muistiosoitetta yhdellä. Haluamme kasvattaa itse arvoa.
Mitä tapahtuu, jos tulostamme viitteen, emmekä sen viittaaman osoitteen arvoa? Katsotaan.
int* a = new int;
cout << a;
delete a;
Tulostuu heksamuodossa oleva muistiosoite, esimerkiksi 0x7ffe09cab4a0
. Tuloste on lähes joka kerta erilainen, sillä heapista saadaan yleensä aina eri muistialue käyttöömme.
Oliot toimivat perusmuuttujatyyppien kanssa samalla tavalla. Ainoa ero on se, että olion muuttujia ja funktioita kutsutaan nuolioperaattorilla pisteen sijasta:
class Circle {
public:
int radius;
};
Circle p;
p.radius = 1;
Circle* q = new Circle;
q->radius = 1;
delete q;