Skip to content

Instantly share code, notes, and snippets.

@quvide
Last active October 14, 2016 16:39
Show Gist options
  • Save quvide/fb4f15f1039af1f3a2a4485934cdf7f8 to your computer and use it in GitHub Desktop.
Save quvide/fb4f15f1039af1f3a2a4485934cdf7f8 to your computer and use it in GitHub Desktop.

http://cpp.sh/ on hyödyllinen online-kääntäjä.

Yksinkertaisin ohjelma

// 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

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 */.

Muuttujat

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;

Syöte ja tuloste

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.

Ehtolauseet

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
*/

Toistorakenteet

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)

  1. suoritetaan yhden kerran silmukan alussa (yleensä määritellään indeksimuuttuja)
  2. ehto
  3. 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

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
*/

Nippelitietoa

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 &&.

Harjoitustehtävä

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.

Harjoitustehtävä 2

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.

Luokat ja oliot

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), ... {

}

Harjoitustehtävän 1 esimerkkiratkaisu

#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;
}

Harjoitustehtävän 2 esimerkkiratkaisu

#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;
}

Listat

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.

Muistinhallinta - Viitteet

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.

Muistinhallinta 2 - Muistin allokointi

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".

Muistinhallinta 3 - Lifetime

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.

Nippelitietoa

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.

Viitteet ja oliot

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;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment