Konekielisessä ohjelmoinnissa hyödyllisiä standardikirjastofunktioita

TIES448 Kääntäjätekniikka, kevät 2017

Huom! Jos gcc valittaa kirjastofunktioita käytettäessä jotain seuraavanlaista (esim. uudehkoissa Ubuntuissa):

relocation R_X86_64_PC32 against symbol `GC_malloc' can not be used when making a shared object; recompile with -fPIC

Lisää gcc:n komentoriville -no-pie. Se saattaa auttaa.

C-tyyppien tulkintaa

Tyyppi AMD64 SysV ABI ARM EABI
_Bool 1 1
char 1 1
short 2 2
int 4 4
long 8 4
long long 8 8
float 4 4
double 8 8
long double 16 8
size_t unsigned long unsigned int

Taulukossa esitetty luku kertoo kyseisen tyypin koko tavuina; jos luvun sijasta mainitaan toisen tyypin nimi, kyse on synonyymistä. Kunkin taulukossa mainitun tyypin muuttujan osoitteen tulee olla jaollinen tyypin koolla.

Tyyppien char, short, int, long ja long long eteen voidaan kirjoittaa avainsana unsigned tai signed merkitsemään, tulkitaanko luku moduloaritmetiikan vai kahden komplementin mukaisesti. AMD64 SysV ABI määrittelee, että char ilman lisämäärettä tulkitaan kahden komplementin mukaisesti, mutta ARM EABIssa char ilman lisämäärettä tulkitaan moduloaritmetiikan mukaisesti. Kaikki muut tässä kappaleessa mainitut tyypit ilman lisämäärettä käytettynä tulkitaan kahden komplementin mukaisesti.

Osoitin tyypin T muuttujaan on C:ssä tyypiltään T *. Sen koko on 8 (AMD64 SysV ABI) tai 4 (ARM EABI) tavua. Osoittimen osoitteen tulee olla jaollinen tällä kokoluvulla.

Joskus käytetään osoitintyyppiä void *. Tämä tarkoittaa osoitinta, jonka kohteen tyyppiä ei sanota ääneen.

C-standardissa esiintyy tyyppi FILE, mutta koskaan et joudu luomaan sen tyyppistä muuttujaa, sillä aina käsittelet vain tyyppiä FILE * (joka on osoitin).

C-kielessä merkkijonot esitetään perinteisesti osoittimena char-taulukon alkuun (ja ovat siten tyypiltään char *). Merkkijonon viimeisen merkin perässä tulee olla nollatavu.

Merkkien luokittelua

int isalnum(int);
int isalpha(int);
int isblank(int);
int iscntrl(int);
int isdigit(int);
int isgraph(int);
int islower(int);
int isprint(int);
int ispunct(int);
int isspace(int);
int isupper(int);
int isxdigit(int);

Nämä kaikki funktiot ottavat parametrinaan unsigned char-tyypin arvon, joka on konvertoitu int-tyypin arvoksi (lukuarvo säilyttäen), ja palauttavat nollasta eroavan luvun, jos ehto täytyy:

  • isalnum: merkki on kirjain tai numero
  • isalpha: merkki on kirjain
  • isblank: merkki on sanojen erottamiseen rivillä sopiva tyhjä merkki (esim. välilyönti tai tabulaattori) mutta ei rivinvaihto eikä mikään sellainen
  • iscntrl: merkki on kontrollimerkki
  • isdigit: merkki on numeromerkki
  • isgraph: merkin voi tulostaa järkevästi mutta se ei ole välilyönti
  • islower: merkki on pieni kirjain
  • isprint: merkin voi tulostaa järkevästi
  • ispunct: merkki on välimerkki
  • isspace: merkki on tyhjä merkki (esim. välilyönti tai rivinvaihto)
  • isupper: merkki on iso kirjain
  • isxdigit: merkki on heksadesimaalinen numeromerkki

Merkkien muokkausta

int tolower(int);
int toupper(int);

Nämä kaikki funktiot ottavat parametrinaan ja palauttavat unsigned char -tyypin arvon, joka on konvertoitu int-tyypin arvoksi (lukuarvo säilyttäen). Ne muuttavat pienet isoiksi (toupper) tai isot pieniksi (tolower) kirjaimiksi. Jos argumentille ei voi tehdä tällaista muunnosta, se palautetaan sellaisenaan.

Syöttö ja tulostus

Tiedoston avaaminen ja sulkeminen

FILE *fopen(const char *filename, const char *mode);
int fclose (FILE *);

Tiedosto avataan fopen-funktiolla, jolle annetaan parametrina tiedoston nimi. Moodiparametrin tulee alkaa jollakin seuraavista kirjaimista:

  • r - avataan lukemista varten
  • w - avataan tyhjän tiedoston alkuun kirjoittamista varten (tarvittaessa luodaan tiedosto tai tyhjennetään olemassaoleva)
  • a - avataan tiedoston nykysisällön perään kirjoittamista varten (tarvittaessa luodaan tiedosto)

Tämän perään voidaan lisätä yksi tai useampi seuraavista merkeistä (mikäli käytät useampia, laita ne tähän järjestykseen):

  • + - sekä lukeminen että kirjoittaminen on mahdollista
  • b - tiedoston sisältö luetaan sellaisenaan (ilman tätä tiedoston katsotaan olevan tekstitiedosto ja esimerkiksi rivinvaihtomerkkejä käsitellään joissakin järjestelmissä eri tavalla)
  • x - tiedosto luodaan nyt ja lukitaan muita ohjelmia vastaan (jos tiedosto on olemassa tai lukitseminen epäonnistuu, operaatio epäonnistuu)

Linuxissa ja muissa POSIX-yhteensopivissa järjestelmissä b ei tee mitään. Lisäksi x on uusi lisäys eivätkä kaikki järjestelmät ymmärrä sitä.

Funktion fopen paluuarvo on osoitin. Jos se on nolla, operaatio epäonnistui. Muussa tapauksessa osoittimen sisältöä ei tule tarkastella lähemmin, mutta sitä voi käyttää tiedostonkäsittelyssä.

Jokainen fopen-funktiolla avattu tiedosto tulee sulkea fclose-funktiolla. Se palauttaa nollan jos sulkeminen onnistui.

Vakiovirrat

Komentoriviohjelman syöte (esimerkiksi käyttäjän näppäimistö) on valmiiksi avattu lukemista varten ja sitä voi käyttää nimellä stdin. Sitä ei tarvitse sulkea itse.

Komentoriviohjelman tuloste (esimerkiksi terminaali-ikkuna) on valmiiksi avattu kirjoittamista varten ja sitä voi käyttää nimellä stdout. Sitä ei tarvitse sulkea itse.

Komentoriviohjelman virheilmoituksia varten (jotka menevät lähtökohtaisesti siihen terminaali-ikkunaan, jossa ohjelma käynnistettiin, vaikka tuloste olisikin ohjattu tiedostoon) on avattu valmiiksi kirjoittamista varten ja sitä voi käyttää nimellä stderr. Sitä ei tarvitse sulkea itse.

Valitettavasti näiden käyttäminen suoraan assemblystä ei ole useinkaan mahdollista, sillä stdin, stdout ja stderr saavat olla C-kielen makroja. Ainakin Ubuntu 16.10:ssä käyttöyritys aiheuttaa kummallisia virheilmoituksia.

Lukeminen ja kirjoittaminen merkki kerrallaan

int fgetc(FILE *);
int fputc(int, FILE *);
int getchar();
int putchar();

Merkki luetaan annetusta tiedostosta fgetc-funktiolla. Funktio getchar lukee stdinistä.

Merkki kirjoitetaan annettuun tiedostoon fputc-funktiolla. Funktio putchar kirjoittaa stdoutiin.

Kaikissa näissä funktioissa unsigned char -tyyppinen arvo on muutettu int-tyyppiseksi arvo säilyttäen (eli nollabittejä lisäten).

Kaikki nämä funktiot palauttavat virhetilanteessa negatiivisen luvun. Lukufunktiot palauttavat negatiivisen luvun myös silloin, kun tiedoston loppu on saavutettu.

Formatoitu kirjoittaminen (printf)

int printf(char *fmt, ...);
int fprintf(FILE *, char *fmt, ...);

Kumpikin funktio tulostaa fmt-merkkijonon, jossa %:llä alkavat ilmaisut korvataan muiden argumenttien merkkijonotulkinnalla. Funktio printf tulostaa stdiniin ja funktio fprintf annettuun tiedostoon.

Muutamia hyödyllisiä %-ilmaisuja (katso C-kielen dokumentaatiosta lisää):

  • %s - tulosta argumenttina oleva char * -tyyppinen merkkijono sellaisenaan.
  • %d - tulosta argumenttina oleva int-tyyppinen luku
  • %lf - tulosta argumenttina oleva double-tyyppinen luku

Esimerkiksi kutsu

const char * nimi = "Antti-Juhani";
int ika = 39;
printf("Terve %s, ikäsi on %d vuotta\n", nimi, ika);

tulostaa

Terve Antti-Juhani, ikäsi on 39 vuotta

ja rivinvaihdon.

Kumpikin funktio palauttaa tulostettujen merkkien määrän, mutta virhetilanteessa negatiivisen luvun.

Formatoitu luku (scanf)

int scanf(char *fmt, ...);
int fscanf(FILE *, char *fmt, ...);
int sscanf(char *input, char *fmt, ...);

Kumpikin funktio lukee fmt-merkkijonon ohjauksessa syötettä. Funktio scanf lukee stdinistä ja fscanf annetusta tiedostosta. Funktio sscanf lukee annettua input-merkkijonoa.

Merkkijonossa fmt jokainen tyhjä merkki (esim. välilyönti tai rivinvaihto) tarkoittaa, että seuraavat tyhjät merkit syötteestä ohitetaan. Jokainen ei-tyhjä merkki seuraavaan %-merkkiin asti tulee löytyä sellaisenaan syötteestä. Merkki % aloittaa muunnoskäskyn. Muunnoskäskyjä ovat mm. seuraavat (lue C-kielen dokumentaatiosta tarkemmin):

  • %d - lue kokonaisluku ja tallenna se seuraavan argumentin (jonka tulee olla tyyppiä int *) osoittamaan muuttujaan
  • %lf - lue liukuluku ja tallenna se seuraavan argumentin (jonka tulee olla tyyppiä double *) osoittamaan muuttujaan

Funktiot palauttavat, kuinka monen argumentin osoittamaan muuttujaan tallennettiin tavaraa. Mikäli lukemisessa sattui virhe ennen ensimmäistä tallennusta, palautetaan negatiivinen arvo.

Huom! Näitä funktioita on syytä välttää oikeassa ohjelmoinnissa, mutta niillä on varsin helppo tehdä kokeilukäyttöön soveltuvia ohjelmia.

Muistinvaraus

Manuaalisella muistin vapautuksella

void *aligned_alloc(size_t alignment, size_t bytes);
void *malloc(size_t bytes);
void *realloc(void *, size_t bytes);
void free(void *);

Funktio malloc varaa keosta pyydetyn määrän tavuja yhtenäisenä muistialueena ja palauttaa osoittimen sen alkuun.

Funktio aligned_alloc varaa keosta pyydetyn määrän tavuja yhtenäisenä muistialueena, jonka osoite on jaollinen alignment-luvulla. Se palauttaa osoittimen varatun muistialueen alkuun.

Funktio realloc muuttaa aiemmin varatun muistialueen kokoa tuhoamatta sen sisältöä. Huomaa, että muistialueen osoite voi muuttua tämän seurauksena; realloc palauttaa uuden osoitteen. Mikäli reallocille vie nollaosoittimen, se toimii kuten malloc.

Funktiot aligned_alloc, malloc ja realloc palauttavat nollaosoittimen, jos muistinvaraus epäonnistui. Jos realloc palauttaa nollaosoittimen, alkuperäinen muistialue on edelleen käytössä alkuperäisellä koollaan.

Varatun uuden muistin sisältö voi olla mitä vain.

Jokainen aligned_allocin, mallocin tai reallocin palauttama osoite tulee vapauttaa kerran ja vain kerran.

Osoite vapautetaan antamalla se argumenttina freelle. Tämän jälkeen kyseistä osoitetta ei enää saa käyttää.

Mikäli realloc palauttaa muuta kuin nollaosoittimen, on se vapauttanut sille annetun osoitteen ja varannut sen palauttaman osoitteen. Sille annettua osoitetta ei saa uudestaan antaa reallocille eikä freelle, ja sen palauttama osoite tulee vapauttaa.

Osoitteen vapauttaminen kahdesti taikka osoitteen käyttäminen sen jälkeen, kun se on vapautettu, on vakava bugi joka voi johtaa ohjelman arvaamattomaan käytökseen tai ohjelman kaatumiseen. Se voi myös avata tietoturva-aukkoja, jos ohjelma on tietoturvan kannalta merkityksellinen. Vapauttamatta jättäminen puolestaan aiheuttaa muistivuodon, joka voi pahimmillaan estää tietokoneen käytön, kaataa ohjelman tai estää enemmän muistin varaamisen.

Roskienkeruulla

Roskienkeruu (garbage collection) ei kuulu standardikirjastoon, mutta koska siitä on paljon hyötyä, esittelen sen tässä. Tarvittava kirjasto on Boehm–Demers–Weiser, ja sen asentamisesta en kirjoita tässä mitään. Ohjelmaan tulee linkittää kyseinen kirjasto (GCC:llä komentorivioptio -lgc)

void *GC_malloc(size_t);
void *GC_malloc_atomic(size_t);
void *GC_malloc_ignore_off_page(size_t);
void *GC_realloc(void *, size_t);

Nämä kaikki funktiot varaavat roskienkeruun alaisuuteen kuuluvan yhtenäisen muistialueen, jonka koko on pyydetynlainen. Muistialue säilyy hengissä vähintään niin kauan kuin siihen on rekistereissä, pinossa, globaaleissa muuttujissa tai muissa roskienkeruun alaisuuteen kuuluvissa muistialueissa ainakin yksi osoitin. Muistialue tuhotaan automaattisesti joskus sen jälkeen, kun viimeinen siihen osoittava osoite poistuu.

Funktiolla GC_malloc_atomic varatusta muistialueesta ei etsitä osoittimia muihin roskienkeruun alaisiin muistialueisiin. Sitä kannattaa käyttää aina, kun muistialueeseen ei ole tarkoitus tallettaa osoittimia.

Funktiolla GC_malloc_ignore_off_page varattuun muistialueeseen tulee olla ainakin yksi osoitin, joka osoittaa kyseisen muistialueen alkuosaan (ensimmäiset 256 tavua), mikäli muistialueen halutaan säilyvän hengissä. Muualle kyseiseen muistialueeseen osoittavat osoittimet jätetään huomiotta. Tätä kannattaa käyttää isojen muistialueiden varaamiseen, mutta huomaa osoittimen käyttörajoitus.

GC_realloc muuttaa aiemmin roskienkeruun alaisuuteen varatun muistialueen kokoa muuttamatta sen sisältöä. Muistialueen osoite voi sen sijaan vaihtua; uusi osoite palautetaan funktion paluuarvona.

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.