Perinteinen standardikirjasto

 

Kääntäjän mukana seuraa aina paljon kivaa, kuten erilaisia kirjastoja kaikenlaisten tehtävien hoitamiseen. Kirjastot vaihtelevat kääntäjittäin, mutta standardinmukaiset osat löytyvät jokaisesta. Yleisesti standardikirjastoa voisi kuvailla funktioiksi ja luokiksi, joilla voi tehdä kaikki kakkahommat. Ja sehän meille sopii - emmehän halua haaskata aivokapasiteettiamme joutavanpäiväisyyksiin, kun järjenjuoksu meinaa ontua jo muutenkin.

C++:n perinteinen standardikirjasto on kohtuullisen suppea. Juuri sen vuoksi se löytyy jokaisesta kääntäjästä - kirjaston toteuttaminen ei ole ollut ylivoimainen työmäärä kääntäjän tekijöille. Myöhemmin C++:n on tullut myös laaja ja kattava kirjasto, Standard Template Library (STL). Mutta sellaista ei näpyttelekään illan tai parin aikana, joten ihan jokainen kääntäjä ei STL:ää sisällä. Siksi onkin hyvä tietää myös mitä perinteinen kirjasto pitää sisällään.

Merkkijonot

C:ssä merkkijonot ovat char-taulukoita. Ja niinhän ne pohjimmiltaan ovat C++:ssakin. Merkkijonoja käytettäessä on hyvä muistaa, että niitä ei pidä mennä käyttämään.Vältä niitä kuin ruttoa. Ehkä vähän liioittelin, mutta minä ja monet muut ohjelmoijat olemme huomanneet, että jostain maagisesta syystä merkkijonot onnistuvat houkuttelemaan virheitä kuin "toinen linssi kaupan päälle" -tarjous kyklooppipariskuntia. Nollatavu lopusta saattaa unohtua tai jonoa käsitellessä hukkua, jolloin merkkijonoja käsittelevät funktiot rullaavat ympäri muistia ja sotkevat sen. Ohjelma ei todennäköisesti kaadu siihen paikkaan, vaan virhe esiintyy myöhemmin täysin käsittämättömänä käytöksenä - joku ihan satunnainen muuttuja on voinut muuttaa arvoaan. Siksi kannattaakin merkkijonojen sijasta käyttää merkkijonoluokkaa.

Ihan perinteen vuoksi kerrataan kuitenkin merkkijonojen käsittelyn perusfunktiot: strcmp(..), strcat(..), strcpy(..) ja strlen(..). Kaikkien määrittelyt löytyvät string.h-tiedostosta. strcmp(..) (string compare) vertailee kahta merkkijonoa. Merkkijonoja ei voi vertailla ==-operaattorilla, koska se vain vertailee kahta char-osoitinta, jotka osoittavat eri kohdassa muistia oleviin merkkijonoihin: vertailu on aina epätosi (ellei vertaile merkkijonomuuttujaa itseensä). strcmp(..) palauttaa 0, jos merkkijonot ovat samat. Siis:

if (strcmp(mjono1, mjono2)) cout << "Samat."; 

.. on väärin, koska strcmp(..) palauttaa epätoden mikäli jonot ovat samat. Jos jälkimmäinen on aakkosissa aikaisempi, paluuarvo on suurempi kuin nolla, ja toisinpäin paluuarvo on pienempi kuin nolla. Mikäli strcmp toimisi loogisesti ja palauttaisi toden (nollasta poikkeavan) kun merkkijonot ovat samat, niin aakkosellinen vertailu ei onnistuisi. Tämmöistä ikuista itkuvirttä se on näiden C-merkkijonojen kanssa...

strcat(..) (string concanate) lisää ensimmäisenä parametrina annetun merkkijonon perään toisena annetun. strcpy(..) (string copy) kopioi taas ensimmäiseen merkkijonoon toisen. strlen(..) (string length) palauttaa merkkijonon pituuden. Nyt sitten pistetään nämä merkkihirmut tositoimiin:

#include <string.h>
#include <iostream.h>

int main()
{
	char testi[40] = "Herra Kononen";
	char oikea[20] = "Herra Tossavainen";
	cout << testi << endl;

	if (!strcmp(testi, "Herra Tossavainen haisee kukkasille.")) // eivät ole samat
		cout << "Samat ovat merkkijonot." << endl;
	else 	cout << "Erilaisia ovat." << endl;

	strcpy(testi, oikea);			// kopioidaan oikea alku
	strcat(testi, " haisee kukkasille.");	// lisätään oikea loppu
	cout << testi << endl;

	if (!strcmp(testi, "Herra Tossavainen haisee kukkasille.")) // ovat samat
		cout << "No nyt ovat samat." << endl;
	else 	cout << "Erilaisia ovat vielä." << endl;
	
	return EXIT_SUCCESS;
}

Mutta ei näitä C-kielen aikuisia merkkijonoräpsyjä kannata käyttää, jos vaan vaihtoehto löytyy. Juuri nollatavuun päättyminen tekee merkkijonoista yhden pahimmista ongelmapesäkkeistä C:ssa. Merkkijonojen kanssa taistellessa olen minäkin pahimmat kolhuni hankkinut ohjelmoijan kimmeltävään sotisopaan. Onhan siinä opittu, vaan olisi tuon voinut uskoa vähemmälläkin. Ja nyt uskon: paljon parempi vaihtoehto on tehdä luokka, joka hoitaa merkkijonojen käsittelyn. Kun kaikki merkkijonotemppuilu on yhden pirun hyvin testatun luokan sisällä, ei mahdollisuuksia merkkijonojen kanssa niin yleisiin inhimillisiin virheisiin enää ole. Luokan tekemisessä on vaan oma vaivansa. Niinpä onkin mukavaa ja mieltä ylentävää, että tuo maallinen puurtaminen on hoidettu meidän puolestamme - C++:n STL-standardikirjastosta löytyy näpsäkkä merkkijonoluokka string. Sitä käyttäen voidaan merkkijonoja, kiitos uudelleenmääriteltyjen operaattoreiden, käyttää kuin mitä tahansa muuttujia. Tällä tavalla yllä oleva tehtäisiin standardikirjaston string-luokan avulla:

#include <string>
#include <iostream>

using std::string;
using std::cout;
using std::endl;

int main()
{
	string testi = "Herra Kononen";
	string oikea = "Herra Tossavainen";
	cout << testi << endl;

	if (testi == "Herra Tossavainen haisee kukkasille.")
		cout << "Samat ovat merkkijonot." << endl;
	else 	cout << "Erilaisia ovat." << endl;

	testi = oikea;
	testi += " haisee kukkasille.";
	cout << testi << endl;

	if (testi == "Herra Tossavainen haisee kukkasille.")
		cout << "No nyt ovat samat." << endl;
	else cout << "Erilaisia ovat vielä." << endl;
	
	return EXIT_SUCCESS;
}

Itseasiassa standardikirjaston toteutus on hoidettu niin kätevästi, että luokan avulla ei pelkästään voi käsitellä merkkijonoja, vaan mitä tahansa merkkijonon kaltaisia tietorakenteita: vaikka numerojonoja tai Tilaus-oliojonoja. Tämän joustavuuden hyödyntäminen vaatii kuitenkin mallien hallintaa, mihin emme tässä voi pureutua. Koska merkkijonot ovat ylivoimaisesti yleisimpiä tarvittuja jonoja, on tyyppimäärittelyn (typedef) avulla määritelty tyyppi string, joka on siis ihan perinteinen merkkijono. Taustalla piilee ovela mallitoteutus, mutta se on niin hyvin piiloitettu että siitä ei käyttäjän tarvitse välittää.

Vanhempien kääntäjien kirjastot eivät sisällä STL:ää - eivätkä siis string-luokkaa. Kuitenkin lähes poikkeuksetta löytyy kääntäjän oma samantyyppinen merkkijonoluokka, nimeltään yleensä String. Sellaisen löytää otsikkotiedostosta string.hpp, joissain kääntäjissä (Turbo C++) strng.h. Luokkien käyttäminen on yleensä samanlaista kuin standardin string-luokan. Toki aina tulee käyttää standardia vaihtoehtoa, mikäli mahdollista.

 

atoi ja itoa

atoi tulee sanoista alphabet to integer, kirjaimet numeroiksi. atoi() siis heittää merkkijonon int-muotoon. Sehän onnistuu myös pelkällä tyyppimuunnoksella, mutta silloin int luku muutetaan tavuksi - siis kaikki ylimääräiset bitit heitetään mäkeen ja tulos ei ole ollenkaan sitä mitä haluttiin. Jos haluamme että merkkijono "222" muuttuu int-luvuksi, jonka arvo on 222, pitää se tekemän atoi():lla. Toisinpäin taas käytetään itoa():aa.

#include <iostream.h>

int main()
{
	char luku1[] = "222";
	char luku2[5];
	int x = atoi(luku);
	itoa(x, luku2, 10); // kymmenlukumuunnos
	cout << "Ja muunnosten tulos on " << luku2 << endl;
	
	return EXIT_SUCCESS;
}

Siis atoi():lle annetaan osoitin merkkijonoon ja saadaan muunnettu int-numero. itoa() syö vähän enemmän argumentteja, ensin annetaan muunnettava int-luku, sitten osoitin kohdemerkkijonoon ja lopuksi minkä kantainen muunnos tehdään. Matti Meikäläinen käyttää kymmenlukuja, joten viimeinen parametri on siinä tapauksessa kymmenen. C++-tyylinen tapa hoitaa muunnokset on käyttää STL:n stringstream-luokkaa.

 

Sattumia soppaan

Seuraava aihe sattuu olemaan satunnaisuus. Filosofisesta näkökulmasta kurkistaen aihe on enemmänkin kuin mielenkiintoinen. Satunnaisuus on metafysiikan perustavanlaatuisia kysymyksiä, johon liittyy mm. kiistakysymys siitä, onko maailma deterministinen vai ei. Myös modernilla kvanttifysiikalla on kantansa asiassa - satunnaisuutta puolustava kanta, keskimäärin.

Kuitenkaan kovin syvällistä filosofista katsantoa ei tarvita noppapelin ohjelmoimiseen, joten tartumme taas käytäntöä kädestä ja talutamme sen toiselle puolelle tietä kuin Sudenpojat ikään. Koska tietokone on deterministinen vehje, siis syöte määrää aina tarkasti tulosteen, ei "todellista satunnaisuutta" saada aikaan. Moneen tarkoitukseen ihan riittävän lähelle päästään kuitenkin käyttämällä ns. kaaottisia kaavoja. Hauska sana kaaos tarkoittaa sitä, että kaava on puhtaan matemaattinen ja samoilla alkuarvoilla saadaan aina sama tulos, mutta riippuvuus syötteen ja tuloksen välillä on niin monimutkainen, että käytännössä mitään riippuvuutta ei ole havaittavissa. Tulokset heittelehtivät villisti kuin villiintyneet viidakkoapinat.

Kaikkien kääntäjien mukana toimitetaan satunnaislukujen luomiseen tarvittava ohjelmanpätkä, satunnaislukugeneraattori. Generaattori laskee uuden satunnaisluvun aina edellisen pohjalta. Ensimmäinen luku on nimeltään siemenluku, joka kannattaa ottaa kellosta, jotta syntyvä satunnaislukusarja olisi aina erilainen. Siemenluku asetetaan näin (menoon mukaan tarvitaan stdlib.h ja time.h):

srand(time(0)); // jossain kääntäjissä (DJGPP) srandom(time(0))

Satunnaisluku väliltä 0 - max:

luku = rand() % max;

Satunnaisluku väliltä min - max:

luku = min + rand() % (max - min);

Tämä kääntäjän standardi satunnaislukugeneraattori ei kuitenkaan ole kovin satunnainen. Jotkut algoritmit vaativat kunnollista satunnaisuutta, johon kääntäjän mukana tuleva ANSI C -standardin mukainen valitettavan köykäinen generaattori ei kelpaa. Kuten mainitsinkin, aitoa satunnaisuutta deterministisellä ja loogisella tietokoneella ei saavuteta - ilman pieniä, tai itseasiassa aivan hemmetin suureellisia, jippoja. Näitä kaikkia kiinnostavia kysymyksiä pohditaan Matematiikka-sektion kappaleessa Sattuu ja tapahtuu...

 

qsort

Sorttia löytyy monenlaista sorttia. Viittaamme nyt lajittelualgoritmeihin, siis "sort algorithms". Ne järjestävät kasan tietoa tietyn säännön mukaan, esimerkiksi numerojärjestykseen. Saatan joskus luennoida erilaisista lajittelualgoritmeistä, mutta en nyt. Lätkäisen vaan kääntäjän mukana tulevan valmiin ja hyvän qsortin käyttöohjeet. Lähes kaikissa C++-kääntäjissä on mukana yleispätevä ja hyvä quicksort-algoritmin toteutus. Se on nopea ja joustava, niinpä sitä voi käyttää melkein mihin tarkoitukseen vain.

qsort(..) nielaisee neljä parametria. Ensin pistetaan lajiteltavan taulukon osoite, sitten tieto kuinka monta alkiota lajitellaan, sitten yhden alkion koko tavuina ja lopuksi annetaan osoitin vertailufunktioon. Vertailufunktio tehdään itse. Sen tulee ottaa kaksi void*-tyyppistä parametria, siis tyypittömät osoittimet vertailtaviin arvoihin. Osoittimet kannattaa aluksi muuntaa oikeaan tyyppiin. Sen jälkeen vertailtaessa pitää muistaa, että vertailee osoittimien osoittamaa dataa - ei itse osoittimia. Mikäli sen unohtaa, niin ainakin Watcom-kääntäjän qsort(..) kiemurtelee umpisolmulle ja tukehtuu lopulta. Vertailufunktion pitää palauttaa int-arvo, joka on pienempi kuin nolla jos ensimmäinen on pienempi, 0 jos vertailtavat ovat yhtäsuuret ja suurempi kuin 0 jos ensimmäinen on suurempi. Alla lepää sitten int-taulukon lajitteleva esimerkki.

#include <stdlib.h>
#include <iostream.h>

int compare(const void *op1, const void *op2)
{
	const int *p1 = (const int *) op1; // typecast void pointers to int
	const int *p2 = (const int *) op2; // void pointer is type insensitive, it can point to anything

	if ((*p1) < (*p2)) return -1; // first is less, so return is less than 0
 	else if ((*p1) > (*p2)) return 1; // first is more, so return is more than 0
	return 0; // equal
}

void main()
{
	const int cVal = 20;
	int values[cVal];
	for (int a=0; a < cVal; a++) values[a] = rand() % 200;

	cout << "Before: "; for (a=0; a < cVal; a++) cout << values[a] << " ";

	qsort((void*)values, cVal, sizeof(int), compare);

	cout << endl << "After: "; for (a=0; a < cVal; a++) cout << values[a] << " ";
	
	return EXIT_SUCCESS;
}

Aidosti C++-tyylinen tapa hoitaa tämä juttu olisi käyttää STL:ää (Standard Template Library). Ongelmahan on siinä, että yleispätevän lajittelualgoritmin pitäisi pystyä lajittelemaan mitä vain tietoa. Alla olevassa esimerkissä se on ratkaistu käyttämällä tyypitöntä void*-osoitinta, joka kuitenkin on hyvin vaarallinen lelu. STL:ssä ongelma on elegantisti ratkaistu malleilla. Palaamme asiaan siis mallien yhteydessä.

 

Trigonometria

Kyllä niitä sinejä ja kosinejä voi C++:llakin laskea. Funktiot ovat sin(..), cos(..) ja tan(..), otsikkotiedosto math.h, katso kääntäjän helpeistä loput.

No, mikäli trigonometrologia ei ole niin hirveän tuttua, niin yhden kompastuskiveyksen voisin nostaa tässä esille. Tosi insinöörit eivät käytä asteita kulmien mittaamiseen, vaan tupsulakkien keskuudessa pinnalle noussut ilmiö on radiaanien käyttö perinteisten asteiden sijasta. Kun täysi kulma on 360 astetta, niin radiaaneina se on 2*PII. Siis kaksi kertaa pii. Pii on se loppumaton luku, joka alkaa 3.141... ja jonka desimaalien opettelu on tapa päästä Guinnesin kirjan sivuille - mikäli ei jaksa syödä kilokaupalla kuukiaisenmunia. Pii radiaania on 180 astetta, siis oikokulma. Mitä ideaa piillä pilailemisessa sitten on? No, se on ympyrän, tässä tapauksessa yksikköympyrän, perustavanlaatuisiin ominaisuuksiin liittyvä, eikä mitenkään vaikea juttu. En minä rupea sitä kuitenkaan nyt selostamaan tässä, ei se ole mitenkään kiinnostavaa - sen voi siis opetella vaikka koulussa. Lapset, olkaa tarkkaavaisia laskento-opin tunnilla.

Harvoin kyllä tulee vajottua näin alas, kun yritän keksiä jotain sisältöä aiheesta, jossa mitään erityistä selitettävää ei juurikaan ole. Taidanpa nyt tipauttaa leikepöydältä esimerkin tähän. Vielä ennen kuin ryntäät paheellisiin harrastuksiisi, niin mainitsen että edellä esiteltyjen funktioiden käänteisfunktiot (arkusfunktiot) ovat acos(..), asin(..) ja atan(..). Niillä saa laskettua kulmia, kun tietää suorakulmaisen kolmion kahden sivun pituudet. Esimerkki on englanniksi, älä suotta peljästy.

#include <math.h>
#include <iostream.h>

const double pi = 3.141; // far from accurate value

double DegToRad(double deg) { return (deg/180.0) * pi; }
double RadToDeg(double rad) { return (rad/pi) * 180.0; }


int main()
{
	double n, number;
	cout << "Give me an angle in degrees: ";
	cin >> n; number = DegToRad(n);
	cout << "Thank you." << endl;

	cout << " Sine of " << " radians is " << sin(number) << endl;
	
	return EXIT_SUCCESS;
}

 

Takaisin