(Kannattaa ensin lukea Johdannosta kappale Tietokone ohjelman toiminta )
Muuttujia tarvitaan ohjelmoinnissa aina kun halutaan tallentaa jotain arvoja, joita voi myös muuttaa myöhemmin. Jos esimerkiksi haluat tehdä ohjelman, joka kysyy käyttäjän iän ja tulostaa sen ruudulle, tarvitset muuttujan.
Muuttuja luodaan ilmoittamalla muuttujan tietotyyppi ja nimi. Tietotyyppi ilmaisee millaista tietoa haluat tallentaa. Tietotyyppi voi olla esimerkiksi:
Muuttujatyyppiä valittaessa pitää tarkistaa tietotyypin lisäksi lukualueen riittävyys ja muuttujatyypin vaatima muisti-tilantarve. Esimerkiksi kokonaislukutyyppejä on useita ja ne vievät eri määrän muistia. Muistia kannattaa yrittää säästää, mutta kannattaa varmistaa, että lukualue riittää.
Muuttujatyyppejä on esitelty mm. seuraavalla sivulla https://en.wikipedia.org/wiki/C_data_types
Muuttujan nimen kannattaa kuvata selkeästi, mitä tietoa muuttuja sisältää. Esimerkiksi nimet student_age ja total_price kertovat muuttujien tarkoituksesta paremmin kuin nimet x ja value. Lyhyet nimet, kuten i, sopivat kuitenkin esimerkiksi pienen silmukan laskuriksi, kun niiden merkitys käy asiayhteydestä selvästi ilmi.
Muuttujille on yleensä suositeltavaa antaa englanninkieliset nimet. Englanti on yleinen ohjelmoinnin kieli, ja englanninkieliset nimet sopivat yhteen ohjelmointikielten, kirjastojen ja dokumentaation kanssa. Ne myös helpottavat koodin jakamista eri kieliä puhuvien ohjelmoijien kesken. Samassa ohjelmassa kannattaa käyttää yhtä kieltä ja yhtenäistä nimeämistapaa.
int, for tai return, ei voi käyttää muuttujien niminä.age ja Age ovat eri muuttujia.first_name ja maximum_speed.Muuttuja voidaan luoda esimerkiksi seuraavilla lauseilla
Muuttujan alustamisella tarkoitetaan arvon sijoittamista muuttujaan sen luonnin yhteydessä. Esimerkiksi näin:
Muuttujan näkyvyysalue tarkoittaa sitä osaa koodista, josta muuttujan voidaan viitata. Jos muuttujaan viitataan tämän näkyvyysalueen ulkopuolelta, kääntäjä antaa virheilmoituksen joka ilmoittaa "ettei muuttujaa ole".
Muuttujan näkyvyysalue on se lohko, jossa muuttuja on määritelty. Lohko tarkoittaa yleensä alkusulun { ja loppusulun } välistä aluetta.
Esimerkki
int main(){
int i;
for (i=1; i<=5; i++) {
printf("%d: Hello inside loop!\n",i);
}
printf("%d: Hello outside loop!\n",i);
return 0;
}
Edellinen koodi toimii, koska muuttujan i näkyvyysalue on koko main-funktio.
Jos esimerkkiä muutetaan näin:
int main(){
for (int i=1; i<=5; i++) {
printf("%d: Hello inside loop!\n",i);
}
printf("%d: Hello outside loop!\n",i);
return 0;
}
Koodi ei käänny, koska for-lauseessa määritellyn muuttujan i näkyvyysalue rajoittuu kyseiseen for-silmukkaan. Muuttujaan ei siis voida viitata silmukan jälkeen.
Muuttuja kannattaa yleensä määritellä mahdollisimman lähellä sen käyttöpaikkaa. Jos muuttujaa i käytetään vain silmukan laskurina, se kannattaa määritellä for-lauseessa. Tällöin muuttuja ei ole tarpeettomasti käytettävissä muualla ohjelmassa, ja koodista käy selvemmin ilmi sen käyttötarkoitus. Muuttuja tulee määritellä ennen silmukkaa vain silloin, kun sen arvoa tarvitaan myös silmukan jälkeen.
Erityisesti scanf-funktiota käytettäessä on hyvä ymmärtää mitä tarkoittaa muuttujan osoite. Muuttujan osoite tarkoittaa sen muistipaikan numeroa, johon muuttujan arvo on tallennettu. Voidaan ajatella, että tietokoneen muisti on jaettu samankokoisiin lokeroihin, jotka on numeroitu. Kukin lokero on yhden tavun suuruinen eli siihen voidaan tallentaa 8 bittiä.
Muuttujan osoitteeseen viitataan c-kielessä laittamalla &-merkki muuttujan nimen eteen. Esimerkiksi muuttujan age osoite on &age.
char tyyppiselle muuttujalle varataan muistia vain yksi muistipaikka, joten muuttujan osoite on selkeästi tuon muistipaikan osoite. Sen sijaan esimerkiksi int tyyppiselle muuttujalle varataan yleensä 4 muistipaikkaa ja muuttujan osoitteella tarkoitetaan näistä ensimmäistä. Jos esimerkiksi muuttujalle age on varattu muistipaikat 101,102,103 ja 104, niin &age on 101.
Typecasting tarkoittaa muuttujatyypin vaihtoa ohjelman suorituksen aikana. Se taphtuu kirjoittamalla uusi tyyppi suluissa muuttujan nimen eteen. Tällaista tarvitaan, jos halutaan esimerkiksi laskea kokonaislukujen osamäärä, niin että tulos on liukuluku. Esimerkiksi näin:
int a, b;
float result;
result = (float)a / (float)b;
Jos edellä ei käytetä typecasting:ia, on myös result tyypiltään int ja silloin desimaaliosa menetetään.
<stdio.h>-otsikkotiedostossa esitellään printf-funktio. Sen avulla voidaan tulostaa ruudulle tekstiä ja muuttujien arvoja.
Esimerkiksi seuraava lause tulostaa lainausmerkkien sisällä olevan tekstin ruudulle. Merkit \n aiheuttavat rivinvaihdon.
printf("This is first line \n and this is second line\n");
Muuttujan arvoa varten tulostettavaan merkkijonoon kirjoitetaan muunnosmääre eli formaattimääre. Määre kertoo printf-funktiolle, minkä tyyppinen arvo kyseiseen kohtaan tulostetaan. Tulostettavat arvot annetaan merkkijonon jälkeen samassa järjestyksessä kuin niitä vastaavat muunnosmääreet.
| Muunnosmääre | Tulostettava arvo | Esimerkki |
|---|---|---|
%d | int-kokonaisluku | printf("%d", age); |
%f | float- tai double-liukuluku | printf("%f", weight); |
%c | yksittäinen merkki | printf("%c", grade); |
%s | merkkijono | printf("%s", name); |
Esimerkiksi int tyyppisen muuttujan arvo voidaan tulostaa seuraavasti:
int age=23;
printf("Jim is %d years old", age);
Edellä %d ilmoittaa, että tähän kohtaan tulostetaan kokonaisluku ja pilkun jälkeen ilmoitetaan, että em. kokonaisluku on muuttujan age arvo. Esimerkki siis tulostaa ruudulle tekstin Jim is 23 years old
Voidaan tulostaa myös useita muuttujan arvoja. Esimerkiksi näin:
int age=23;
float height=181.5f;
printf("Jim is %d years old and his height is %f cm ", age, height);
Ja edellä %f ilmoittaa, että tähän tulostetaan liukuluku. Ilman lisäohjeita, printf tulostaa melkoisen määrän desimaaleja. Jos halutaan rajata tulostettavien desimaalien määrä vaikka kahteen voidaan edellisen esimerkin tulostuslause kirjoittaa näin
printf("Jim is %d years old and his height is %.2f cm ", age, height);
Seuraavassa esimerkissä käytetään kaikkia edellä esiteltyjä muunnosmääreitä:
char name[] = "Teppo";
int age = 23;
float height = 181.5f;
char grade = 'A';
printf("Name: %s, age: %d, height: %.1f, grade: %c\n",
name, age, height, grade);
Voit lukea lisätietoja printf-funktiosta sivulta https://www.tutorialspoint.com/c_standard_library/c_function_printf.htm
<stdio.h>-otsikkotiedostossa esitellään myös scanf-funktio. Sen avulla voidaan lukea käyttäjän näppäimistöltä antamia arvoja. scanf-funktiolle ilmoitetaan luettavan tiedon tyyppi muunnosmääreellä sekä sen muuttujan osoite, johon arvo tallennetaan.
| Muunnosmääre | Muuttujan tyyppi | Esimerkki |
|---|---|---|
%d | int | scanf("%d", &age); |
%f | float | scanf("%f", &weight); |
%c | char | scanf(" %c", &grade); |
%s | merkkitaulukko, esimerkiksi char name[30] | scanf("%29s", name); |
" %c" ennen %c-määrettä oleva välilyönti ohittaa syötteestä mahdolliset tyhjemerkit, kuten edellisen syötteen rivinvaihdon.name eteen ei kirjoiteta &-merkkiä.%29s pituusraja estää 30 merkin taulukon ylivuodon ja jättää tilaa merkkijonon lopetusmerkille '\0'. %s lukee vain ensimmäiseen tyhjemerkkiin asti.double-muuttuja luetaan scanf-funktiolla käyttäen määrettä %lf. printf-funktiossa sekä float- että double-arvo tulostetaan määreellä %f.
double distance;
scanf("%lf", &distance);
printf("%f\n", distance);
Huomautus: Tämän oppaan ensimmäisissä esimerkeissä scanf-funktiota käytetään yksinkertaisten arvojen lukemiseen, ja käyttäjän oletetaan antavan pyydetyn tyyppinen arvo. Oikeassa ohjelmassa scanf-funktion paluuarvo kannattaa tarkistaa, sillä virheellinen syöte voi jäädä syötepuskuriin ja vaikeuttaa seuraavien arvojen lukemista. Kokonaisen rivin tai luotettavammin tarkistettavan syötteen lukemiseen voidaan käyttää fgets-funktiota ja muuntaa luettu teksti tarvittaessa esimerkiksi strtol-funktiolla. Tämä tapa esitellään myöhemmin merkkijonojen yhteydessä.
Esimerkiksi käyttäjältä kysytään ikä ja hänen antamansa tieto halutaan tallentaa kokonaislukuna muuttujan age arvoksi (eli siis age-muuttujalle varattuun muistipaikkaan). Nyt voidaan käyttää seuraavanlaista funktiokutsua:
scanf("%d", &age);
Alla olevassa esimerkissä on kokonaisuudessaan c-lähdekoodi sovellukselle, jossa kysytään käyttäjältä ikä ja se tulostetaan ruudulle
#include <stdio.h>
int main(){
int age;
printf("How old are you?\n");
scanf("%d", &age);
printf("You said that you are %d years old\n", age);
return 0;
}
Funktiolla scanf voidaan lukea myös useampi arvo kerralla, kunhan kerrotaan mikä välimerkki muuttujien arvojen väliin tulee sijoittaa. Seuraavassa esimerkissä luetaan kaksi kokonaislukua ja käyttäjän on syötettävä ne pilkulla erotettuna.
int x;
int y;
scanf("%d, %d", &x, &y);
Voit lukea lisätietoja scanf-funktiosta sivulta https://www.tutorialspoint.com/c_standard_library/c_function_scanf.htm
Kommentti tarkoittaa tekstinosaa (sana, rivi, kappale), jota kääntäjä ei huomioi. Kommenttien tarkoituksena on antaa informaatiota koodia lukevalle ohjelmoijalle. Kommentit muodostetaan c-kielessä kahdella tavalla:
// tarkoittavat, että näiden jälkeen loppuosa rivistä tulkitaan kommentiksi/* ja */ väliin jäävä osa tulkitaan kommentiksiTähän mennessä tekstissä on jo esiintynyt funktiot main, printf ja scanf.
Funktiota voidaan sanoa myös aliohjelmaksi, sillä se on itsenäinen osa sovellusta. Edellä mainitut printf ja scanf ovat funktioita, jotka on määritetty c-ohjelmointiympäristön mukana toimitettavassa stdio-kirjastossa. Aiemmissa esimerkeissä on hyödynnetty niitä kutsumalla mainittuja funktioita.
Ohjelmoijat kirjoittavat myös itse funktioita. Niiden tarkoitus on selkeyttää koodia jakamalla sitä pienempiin kokonaisuuksiin. Ne voivat myöskin vähentää koodirivien määrää, koska samaa funtiota voidaan hyödyntää useita kertoja.
Funktio muodostuu kahdesta osasta:
Funktion palautusarvo tarkoittaa tietoa, jonka funktio lopuksi palauttaa. Voidaan esimerkiksi kirjoittaa funktio, joka suorittaa jonkin laskutoimituksen ja palauttaa tuon laskutoimituksen tuloksen. Palauttaminen määritetään käyttäen sanaa return.
Funktion parametrit tarkoittavat funktiolle annettavia lähtötietoja. Jos esimerkiksi kirjoitat funktion, joka laskee kahden kokonaisluvun summan, niin nuo kaksi kokonaislukua ovat funktion parametrit.
Esimerkki yhteenlasku-funktio voidaan kirjoittaa näin:
int sum(int a, int b)
{
int answer=a+b;
return answer;
}
Edellä rivi int sum(int a, int b) on siis funktion otsikko. Siinä on määritetty, että
Sulkumerkkien {} sisällä oleva osa muodostaa funktion rungon. Sen sisällä suoritetaan yhteenlasku ja lopuksi palautetaan laskun tulos.
void-palautusarvo tarkoittaa, että funktio ei palauta mitään. Esimerkiksi yhteenlaskufunktio voidaan toteuttaa niin, että se tulostaa vastauksen, eikä palauta mitään:
void second_sum(int a, int b)
{
int answer=a+b;
printf("The sum is %d\n", answer);
}
Koska eo. funktio ei palauta mitään (paluuarvo on void), ei funktiossa ole return-lausetta.
Funktiota ei suoriteta, jollei sitä kutsuta. Funktion kutsu siis ilmoittaa, että tässä kohdassa halutaan suorittaa mainittu funktio.
Funktiota kutsutaan "mainitsemalla" sen nimi. Nimen perään tulee aina sulut (). Sulkujen sisään tulee argumentit eli siis ne arvot, jotka funktion parametreille annetaan. Jos, funktio palauttaa jotain, yleensä tämä palautusarvo tallennetaan johonkin (tai tulostetaan).
Esimerkiksi em. funktiota, joka ei palauta mitään voidaan kutsua näin:
second_sum(7,8);Jolloin funktio tulostaa lukujen 7 ja 8 summan.
Sen sijaan funktiota joka palauttaa annettujen lukujen summan voitaisiin kutsua näin:
int calc=sum(5,6);jolloin calc nimiseen muuttujaan tallennetaan lukujen 5 ja 6 summa.
Funktion prototyypin tarkoitus on kertoa kääntäjälle mikä on funktion palautusarvon tyyppi ja mikä on funktion parametrien lukumäärä ja niiden tyypit. Prototyypit kirjoitetaan yleensä ohjelman alkuun, koska prototyyppi on esitettävä ennen kuin funktiota kutsutaan.
Funktion prototyypiksi kelpaa funktion otsikko eli esimerkiksi aiemmista esimerkeistä rivi int sum(int a, int b). Parametrien nimiä ei ole prototyypissä välttämätön mainita eli voitaisiin kirjoittaa edellinen prototyyppi myös muodossa int sum(int , int )
Alla olevassa esimerkissä prototyyppien poisjättäminen aiheuttaa virheilmoituksen tai ainakin varoituksen.
int sum(int , int );
void second_sum(int , int );
int main(){
int sum(int a, int b);
void second_sum(int a, int b);
int calc=sum(5,6);
printf("The sum is %d\n",calc);
second_sum(7,8);
return 0;
}
int sum(int a, int b){
int answer=a+b;
return answer;
}
void second_sum(int a, int b){
int answer=a+b;
printf("The sum is %d\n", answer);
}
Jokaisessa c-ohjelmassa tulee olla main-funktio eli pääohjelma. Muuten sovellusta ei suoriteta ollenkaan. main-funktiolle ei kirjoiteta kutsua, vaan käyttöjärjestelmä kutsuu sitä.
Nykyisessä c-standardissa suositellaan, että main-funktion palautusarvoksi määritetään int. Siksi siis funktion lopussa on lause return 0;
Mikäli main-funktiolle määritetään parametreja on kyseessä ns. komentoriviargumentit, jotka siis annetaan komentoriviltä.
Esimerkiksi ping ohjelmaa käynnistetään komentoriviltä antamalla sille argumenttina ip-osoite tai dns-nimi näin:
ping google.com
Osoitin on muuttuja, jonka arvoksi voidaan asettaa muistiosoite. Siitä on kerrottu tarkemmin omassa kappaleessaan. Tässä kuitenkin yksi esimerkki siitä, milloin se on tarpeellinen.
Kun funktiolle annetaan argumenttina muuttuja, funktio luo siitä paikallisen kopion. Se on siis eri muuttuja vaikka sen nimi olisi sama kuin alkuperäisen muuttujan. Funktio ei siis pysty muuttamaan alkuperäisen muuttujan arvoa. Jos tälläinen tarve ilmenee, voidaan funktiolle antaa argumenttina muuttuja osoite (alla esimerkissä &x) ja funktion parametriksi laitetaan osoitin muuttuja (alla esimerkissä int *x).
Esimerkin suorittamalla voit havaita, että
#include <stdio.h>
void valueTest(int x);
void pointerTest(int *x);
int main()
{
int x=5;
printf("Aluksi arvo=%d\n",x);
//kutsutaan valueTest-funtiota
valueTest(x);
printf("valueTest kutsuttu ja mainissa arvo=%d\n",x);
printf("\n");
//kutsutaan pointerTest-funtiota
pointerTest(&x);
printf("pointerTest kutsuttu ja mainissa arvo=%d\n",x);
printf("\n");
return 0;
}
void valueTest(int x){
x=x+2;
printf("\tvalueTest funktiossa arvo=%d\n",x);
}
void pointerTest(int *x){
*x = *x+2;
printf("\tpointerTest funktiossa arvo=%d\n",*x);
}
Isommat sovellukset pyritään rakentamaan modulaarisiksi eli jakamaan pienempiin, selkeistä tehtävistä vastaaviin osiin. Tällöin myös koodia jaetaan useampaan tiedostoon, mikä helpottaa ohjelman ymmärtämistä, ylläpitoa ja osien uudelleenkäyttöä.
C-kielessä moduulille luodaan usein erillinen header-tiedosto (.h) ja lähdekooditiedosto (.c). Header-tiedosto kuvaa moduulin rajapinnan: siinä esitellään esimerkiksi funktioiden prototyypit, joiden avulla ohjelman muut osat voivat käyttää moduulia. C-tiedostoon kirjoitetaan funktioiden toteutukset eli varsinainen toiminta. Muut lähdekooditiedostot sisällyttävät tarvitsemansa header-tiedoston #include-direktiivillä, ja moduulin .c-tiedosto käännetään ja linkitetään mukaan ohjelmaan.
Miksi sisällytetään .h-tiedosto eikä suoraan .c-tiedostoa? #include tuo tiedoston sisällön sellaisenaan mukaan sitä käyttävään lähdekoodiin ennen kääntämistä. Funktion kutsumista varten kääntäjä tarvitsee tiedon funktion nimestä, parametrien tyypeistä ja paluuarvon tyypistä. Header-tiedostossa oleva prototyyppi antaa nämä tiedot ilman funktion toteutusta.
Myös .c-tiedoston sisällyttäminen on teknisesti mahdollista, mutta silloin mukaan tulevat funktioiden toteutukset. Jos sama .c-tiedosto sisällytetään useaan erikseen käännettävään lähdekooditiedostoon tai lisäksi käännetään omana tiedostonaan, esimerkiksi alla olevan multiply-funktion toteutus tulee ohjelmaan useaan kertaan ja linkitys epäonnistuu. Erillisen header-tiedoston avulla useat lähdekooditiedostot voivat käyttää samaa rajapintaa, kun toteutus käännetään mukaan vain kerran. Header-tiedoston sisällytyssuoja ei estä eri käännösyksiköissä syntyviä päällekkäisiä toteutuksia.
Header-tiedostot tunnistaa tarkentimesta .h. Ne voivat olla ohjelmoijan itsensä kirjoittamia tai esimerkiksi C:n standardikirjaston otsikkotiedostoja, kuten stdio.h. Header-tiedosto ei itsessään ole kirjasto, vaan se esittelee kirjaston tai moduulin tarjoaman rajapinnan.
Header tiedostot liitetään koodiin include-lauseella esimerkiksi näin:
#include <stdio.h> #include "myfunctions.h"Nyt ensimmäinen tiedosto eli stdio.h on kääntäjän mukana toimitettu tiedosto, joten kulmasuluilla
<> ilmoitetaan kääntäjälle, että sitä etsitään systeemihakemistosta. Sen sijaan toinen eli myfunctions.h on käyttäjän itse kirjoittama, joten lainausmerkeillä ilmoitetaan, että sitä etsitään projektihakemistosta.
Esimerkki voisi sisältää tiedostot main.c ja myfunctions.h ja myfunctions.c. Tiedoston myfunctions.h sisältö voisi olla seuraava:
#ifndef MYFUNCTIONS_H #define MYFUNCTIONS_H int multiply(int, int); #endifTiedostossa on funktion
multiply prototyyppi sekä sisällytyssuoja (include guard). Suoja estää header-tiedoston sisällön käsittelyn useaan kertaan saman käännösyksikön sisällä eli silloin, kun käsitellään yhtä .c-tiedostoa ja kaikkia sen sisällyttämiä header-tiedostoja.
#ifndef MYFUNCTIONS_H tarkistaa, ettei makroa MYFUNCTIONS_H ole vielä määritelty. Jos se on jo määritelty, sisältö ohitetaan vastaavaan #endif-direktiiviin asti.#define MYFUNCTIONS_H määrittelee makron, jolloin saman header-tiedoston myöhemmät sisällytykset ohittavat suojatun sisällön.#endif päättää #ifndef-direktiivillä aloitetun ehdollisen osuuden.Nämä ovat esikäsittelijän direktiivejä. Suojamakron nimi valitaan jokaiselle header-tiedostolle yksilölliseksi, yleensä tiedoston nimen perusteella. Pelkän samanlaisen funktion prototyypin toistaminen on sallittua, mutta sisällytyssuoja on hyvä käytäntö: se estää esimerkiksi header-tiedostossa olevan struct-määrittelyn toistumisesta aiheutuvat virheet.
Tiedoston myfunctions.c sisältö voisi olla seuraava:
#include "myfunctions.h"
int multiply(int a, int b){
return a*b;
}
Siinä on siis funtion multiply toteutus. Ja main.c tiedoston sisältö on seuraava:
#include <stdio.h>
#include "myfunctions.h"
int main(){
int calc=multiply(5,6);
printf("The result is %d\n",calc);
return 0;
}
Ohjelman voi kääntää ja linkittää esimerkiksi GCC-kääntäjällä seuraavasti:
gcc main.c myfunctions.c -o ohjelma
Molemmat .c-tiedostot käännetään ja niiden tuottama koodi linkitetään yhdeksi ohjelmaksi. Header-tiedostoa ei anneta komentoon erikseen, sillä #include "myfunctions.h" tuo sen esittelyt kummankin lähdekooditiedoston käyttöön. Linkityksessä main.c-tiedoston multiply-kutsu yhdistetään myfunctions.c-tiedostossa olevaan toteutukseen.
C-kielessä ei ole varsinaista muuttujatyppiä merkkijonoja varten, vaan merkkijonoja käsitellään merkkitaulukoina. Esimerkiksi, jos halutaan tallentaa sana Hello, voidaan se tallentaa taulukkoon, johon mahtuu 6 merkkiä. Tarvitaan 6 merkkiä, koska sanassa on 5 merkkiä ja jonon loppumerkille tarvitaan yksi tavu. Loppumerkki on \0.
Seuraavana on kaksi tapaa luoda merkkitaulukko ja alustaa se sanalla Hello
char myWord[]="Hello";
char myWord[6]={'H','e','l','l','o','\0'};
Käytettiinpä kumpaa vain edellisistä, niin on luotu merkkitaulukko, jossa on 6 alkiota ja nämä alkiot ovat:
myWord[0], myWord[1], myWord[2], myWord[3], myWord[4], myWord[5].
Huomaa, että edellä alkiota myWord[6] ei ole olemassa!
printf- ja scanf-funktioissa merkkijonon muunnosmääre on %s.
Seuraavassa esimerkissä käyttäjältä kysytään nimi ja hänelle tulostetaan tervehdys:
#include <stdio.h>
int main(void)
{
char fname[30];
printf("What is your name?\n");
scanf("%29s", fname);
printf("Hello %s\n", fname);
return 0;
}
fname-niminen 30 merkin taulukko. Siihen mahtuu enintään 29 varsinaista merkkiä, koska viimeinen paikka tarvitaan merkkijonon lopetusmerkille '\0'.%29s estää scanf-funktiota kirjoittamasta taulukon rajojen ulkopuolelle.scanf-kutsussa käytetään nimeä fname ilman &-merkkiä. Tässä yhteydessä taulukon nimi muuntuu osoittimeksi sen ensimmäiseen alkioon eli arvoon &fname[0].
Ilmaus &fname tarkoittaa osoitinta koko taulukkoon. Sen osoitearvo on sama kuin ensimmäisen alkion osoite, mutta sen tyyppi on eri, joten sitä ei pidä antaa scanf-funktion %s-muunnokselle.
scanf-funktio lopettaa %s-muunnoksella lukemisen ensimmäiseen tyhjemerkkiin. Siksi sillä voidaan lukea vain yksi sana kerrallaan. Kokonainen rivi, joka voi sisältää välilyöntejä, voidaan lukea fgets-funktiolla:
char fname[100]; fgets(fname, sizeof(fname), stdin);
Kun scanf-funktiolla luetaan syötettä, se jättää usein rivinvaihtomerkin \n syötepuskuriin. Jos tämän jälkeen käytetään fgets-funktiota, se voi lukea jäljelle jääneen rivinvaihdon ja palauttaa tyhjän rivin. Ennen fgets-kutsua rivin loput voidaan lukea ja hylätä seuraavasti:
int c;
while ((c = getchar()) != '\n' && c != EOF) {
}
Muuttujan c tyyppi on int, koska getchar voi palauttaa joko luetun merkin tai erityisen EOF-arvon. Seuraavassa esimerkissä ikä luetaan scanf-funktiolla ja koko nimi fgets-funktiolla:
#include <stdio.h>
#include <string.h>
int main(void)
{
int age;
char fullname[100];
printf("Enter your age: ");
scanf("%d", &age);
/* Luetaan ja hylätään nykyisen syöttörivin loput. */
int c;
while ((c = getchar()) != '\n' && c != EOF) {
}
printf("Enter your full name: ");
fgets(fullname, sizeof(fullname), stdin);
fullname[strcspn(fullname, "\n")] = '\0';
printf("Age: %d\n", age);
printf("Full name: %s\n", fullname);
return 0;
}
Tyhjentämättä syöttöpuskuria fgets lukisi scanf-funktion jättämän rivinvaihdon ja palauttaisi tyhjän merkkijonon sen sijaan, että odottaisi käyttäjän nimeä.
Syötepuskuria ei tarvitse tyhjentää, jos kaikki käyttäjän syötteet luetaan fgets-funktiolla. fgets-funktiolla luettu ikä voidaan muuntaa kokonaisluvuksi esimerkiksi strtol-funktiolla. Seuraava yksinkertaistettu esimerkki olettaa, että käyttäjä syöttää kelvollisen kokonaisluvun ja että syötteiden lukeminen onnistuu:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
char input[100];
char fullname[100];
int age;
printf("Enter your age: ");
fgets(input, sizeof(input), stdin);
age = (int)strtol(input, NULL, 10);
printf("Enter your full name: ");
fgets(fullname, sizeof(fullname), stdin);
fullname[strcspn(fullname, "\n")] = '\0';
printf("Age: %d\n", age);
printf("Full name: %s\n", fullname);
return 0;
}
Huomautus: Oikeassa ohjelmassa kannattaa tarkistaa, onnistuiko syötteen lukeminen. Katso tarkistukset sisältävä esimerkki.
strcspn-funktiolla etsitään nimen lopusta fgets-funktion tallentama rivinvaihto ja korvataan se merkkijonon lopetusmerkillä '\0'.
Huomautus: Jos käyttäjä kirjoittaa nimeksi Teppo Testi ja painaa Enter-näppäintä, fgets tallentaa merkkijonon tavallisesti muodossa "Teppo Testi\n\0". Jos rivinvaihtoa \n ei poisteta, se voi aiheuttaa tulostukseen ylimääräisen rivinvaihdon. Myös esimerkiksi vertailu strcmp(fullname, "Teppo Testi") epäonnistuu, koska fullname sisältää nimen lisäksi rivinvaihdon.
Taulukko eli taulukkomuuttuja tarkoittaa muuttujaa johon voidaan tallentaa useita samantyyppisiä arvoja. Edellä käsiteltiin jo merkkitaulukoita, mutta taulukoihin voidaan tallentaa myös kokonaislukuja tai liukulukuja.
Esimerkiksi seuraava lause luo kokonaislukutaulukon, johon voidaan tallentaa 5 kokonaislukua
int ages[5];
Taas on syytä muistaa, että ensimmäinen alkio on ages[0] ja viimeinen on ages[4]
Pari esimerkkiä kokonaislukutaukoista:
int ages[5]={6,3,12,22,31};
printf("The age of the second person is %d\n",ages[1]);
printf("What is the age of the third person?\n");
scanf("%d",&ages[2]);
Kaksiulotteinen taulukko määritellään antamalla kaksi indeksiä eli ikäänkuin rivien ja sarakkaiden lukumäärä. Esimerkiksi seuraavassa määritellään kaksiulotteinen kokonaislukutaulukko.
int main()
{
int tableOfNumbers[2][3]={
{1,2,3},
{4,5,6}
};
printf("The last number in the table is %d\n",tableOfNumbers[1][2]);
return 0;
}
Ja sovellus tulostaa siis tekstin The last number in the table is 6
Ehtorakenteiden avulla ohjelma voi suorittaa eri toimenpiteitä tilanteen mukaan. Päätös voi perustua esimerkiksi käyttäjän antamaan arvoon, muuttujien vertailuun tai usean ehdon yhdistelmään.
Yksinkertaisimmillaan ehtorakenne koostuu if-lauseesta ja lohkosta, joka suoritetaan vain ehdon ollessa tosi:
int test = 5;
printf("Before the test\n");
if (test == 5) {
printf("The value of test is 5\n");
}
printf("After the test\n");
Ehto test == 5 on tosi, kun muuttujan test arvo on 5. Tällöin aaltosulkeiden { } sisällä oleva lohko suoritetaan. Jos ehto on epätosi, lohko ohitetaan. Vertailuoperaattori == tutkii arvojen yhtäsuuruutta, kun taas sijoitusoperaattori = antaa muuttujalle arvon.
Vertailuoperaattorit vertaavat kahta arvoa. Vertailun tulos on joko tosi tai epätosi.
| Operaattori | Merkitys | Esimerkki |
|---|---|---|
== | yhtä suuri kuin | age == 18 |
!= | eri suuri kuin | age != 18 |
> | suurempi kuin | age > 18 |
< | pienempi kuin | age < 18 |
>= | suurempi tai yhtä suuri kuin | age >= 18 |
<= | pienempi tai yhtä suuri kuin | age <= 18 |
Loogisilla operaattoreilla voidaan yhdistää ehtoja tai kääntää ehdon totuusarvo.
| Operaattori | Merkitys | Ehto on tosi, kun |
|---|---|---|
&& | ja | molemmat ehdot ovat tosia |
|| | tai | ainakin toinen ehto on tosi |
! | ei | sen jäljessä oleva ehto on epätosi |
if (age >= 13 && age <= 19) {
printf("Teenager\n");
}
if (day == 6 || day == 7) {
printf("Weekend\n");
}
if (!is_logged_in) {
printf("Please log in\n");
}
Operaattorit && ja || käyttävät oikosulkuarviointia. Operaattorin && jälkimmäistä ehtoa ei tutkita, jos ensimmäinen ehto on epätosi. Operaattorin || jälkimmäistä ehtoa ei tutkita, jos ensimmäinen ehto on tosi.
Huomautus: Loogiset operaattorit && ja || ovat eri operaattoreita kuin bittitason operaattorit & ja |.
C-kielessä <stdbool.h>-otsikkotiedosto määrittelee tyypin bool sekä arvot true ja false.
#include <stdbool.h> bool is_ready = true;
Totuusarvoa ei tarvitse verrata erikseen arvoon true tai false. Seuraavat muodot ovat lyhyitä ja selkeitä:
if (is_ready) {
printf("Ready\n");
}
if (!is_ready) {
printf("Not ready\n");
}
C-kielen ehdoissa arvo 0 tulkitaan epätodeksi ja kaikki nollasta poikkeavat arvot tosiksi.
if–else-rakenteessa suoritetaan kahdesta vaihtoehdosta täsmälleen toinen:
if (is_ready) {
printf("The program is ready\n");
} else {
printf("The program is not ready\n");
}
Jos ehto on tosi, suoritetaan if-lohko. Muussa tapauksessa suoritetaan else-lohko.
Ehdollinen operaattori valitsee ehdon perusteella kahdesta arvosta toisen. Sitä kutsutaan myös ternääriseksi operaattoriksi, koska siinä on kolme osaa:
ehto ? arvo_jos_tosi : arvo_jos_epätosi
Esimerkiksi seuraava if–else-rakenne valitsee pääsymaksun asiakkaan iän perusteella:
int age = 16;
int fee;
if (age < 18) {
fee = 5;
} else {
fee = 10;
}
Sama valinta voidaan kirjoittaa ehdollisella operaattorilla näin:
int age = 16; int fee = (age < 18) ? 5 : 10;
Ehdollisen operaattorin etuna on, että kahdesta arvosta valitseminen voidaan kirjoittaa tiiviisti yhtenä lausekkeena. Sitä kannattaa käyttää, kun ehto ja molemmat vaihtoehdot ovat lyhyitä ja rakenne säilyy helposti luettavana.
Tavallinen if–else on parempi valinta, jos vaihtoehdoissa suoritetaan useita toimenpiteitä, kutsutaan eri funktioita tai käytetään monimutkaisia ehtoja. Sisäkkäisiä ehdollisia operaattoreita kannattaa yleensä välttää, koska ne vaikeuttavat koodin lukemista.
Kun vaihtoehtoja on enemmän kuin kaksi, voidaan käyttää yhtä tai useampaa else if-haaraa:
int test = 1;
if (test == 1) {
printf("Option A\n");
} else if (test == 2) {
printf("Option B\n");
} else if (test == 3) {
printf("Option C\n");
} else {
printf("Option D\n");
}
Ehdot tutkitaan ylhäältä alaspäin. Vain ensimmäinen haara, jonka ehto on tosi, suoritetaan. Jos mikään ehto ei ole tosi, suoritetaan mahdollinen else-haara.
Seuraavassa esimerkissä henkilö luokitellaan iän perusteella lapseksi, teini-ikäiseksi tai aikuiseksi:
int age = 16;
if (age < 13) {
printf("Child\n");
} else if (age <= 19) {
printf("Teenager\n");
} else {
printf("Adult\n");
}
Kun ohjelma pääsee toiseen ehtoon, ensimmäisen ehdon tiedetään jo olevan epätosi. Siksi ehtoa ei tarvitse kirjoittaa muodossa age >= 13 && age <= 19, vaan pelkkä ylärajan tarkistus age <= 19 riittää.
switch-rakenne sopii tilanteeseen, jossa yhden muuttujan arvoa verrataan useisiin kiinteisiin arvoihin. Aiemman test-esimerkin voi kirjoittaa näin:
switch (test) {
case 1:
printf("Option A\n");
break;
case 2:
printf("Option B\n");
break;
case 3:
printf("Option C\n");
break;
default:
printf("Option D\n");
}
Jokainen case määrittää yhden mahdollisen arvon. break lopettaa switch-rakenteen suorittamisen, jotta suoritus ei jatku seuraavaan haaraan. default suoritetaan, jos mikään case-arvo ei vastaa muuttujan arvoa.
switch ei sovellu suoraan vaihteluvälien tai operaattoreilla && ja || yhdistettyjen ehtojen tutkimiseen. Tällaisissa tilanteissa käytetään tavallisesti if–else if -rakennetta.
Yksittäisiä merkkejä voidaan verrata tavallisilla vertailuoperaattoreilla. Välilyönti ennen %c-määrettä ohittaa syötteeseen mahdollisesti jääneet tyhjemerkit:
char character;
scanf(" %c", &character);
if (character == 'a') {
printf("The character is a\n");
}
Merkkijonojen sisältöjä ei verrata operaattorilla ==, sillä se vertaisi merkkijonojen osoitteita. <string.h>-otsikkotiedostossa esitelty strcmp-funktio vertaa merkkijonojen sisältöjä:
#include <string.h>
char word[20];
scanf("%19s", word);
if (strcmp(word, "Terve") == 0) {
printf("The strings are equal\n");
}
strcmp palauttaa arvon 0, kun sille annetut merkkijonot ovat sisällöltään samat. Muodon %19s pituusraja jättää 20 merkin taulukkoon tilaa myös merkkijonon lopetusmerkille.
Toistorakenteet ovat tärkeä osa ohjelmointia. C-kielessä on kolme tavallista silmukkarakennetta: for, while ja do ... while.
for-silmukkaa käytetään yleensä silloin, kun toistojen lukumäärä on ennalta tiedossa, esimerkiksi kun kysytään viisi lukua.
while-silmukka sopii tilanteisiin, joissa toistetaan toimenpiteitä niin kauan kuin ehto on voimassa, esimerkiksi kun kysytään lukuja niin kauan kuin annetut luvut ovat positiivisia.
Esimerkki for-silmukasta:
int main()
{
int number;
int sum = 0;
for (int x = 1; x <= 5; x++) {
printf("Anna luku %d/5: ", x);
if (scanf("%d", &number) != 1) {
printf("Syöte ei ollut kokonaisluku.\n");
return 1;
}
sum += number;
}
printf("Lukujen summa on %d\n", sum);
return 0;
}
Ehto scanf("%d", &number) != 1 tarkistaa, onnistuiko kokonaisluvun lukeminen. scanf palauttaa onnistuneesti luettujen arvojen lukumäärän, joten tässä tapauksessa onnistunut luku palauttaa arvon 1. Jos käyttäjä antaa esimerkiksi kirjaimen, palautusarvo on jokin muu kuin 1 ja ohjelma tulostaa virheilmoituksen.
Edellä for-silmukan kolmella osalla on seuraavat tehtävät:
x = 1 antaa muuttujalle alkuarvon.x <= 5 määrittää ehdon, jonka on oltava tosi, jotta silmukan runko suoritetaan.x++ kasvattaa muuttujan arvoa yhdellä jokaisen kierroksen jälkeen.
Lauseke sum += number; on lyhyempi muoto lausekkeelle sum = sum + number;.
while-silmukan ehto tarkistetaan ennen jokaista kierrosta. Seuraavassa esimerkissä negatiivinen luku tai nolla lopettaa syötteen, eikä lopetusarvoa lisätä summaan:
int main()
{
int number;
int sum = 0;
printf("Anna positiivisia lukuja (lopeta nollalla tai negatiivisella luvulla):\n");
while (scanf("%d", &number) == 1 && number > 0) {
sum += number;
}
printf("Lukujen summa on %d\n", sum);
return 0;
}
do ... while -silmukan runko suoritetaan vähintään kerran, koska ehto tarkistetaan vasta rungon jälkeen.
int number;
do {
printf("Anna positiivinen luku: ");
if (scanf("%d", &number) != 1) {
printf("Syöte ei ollut kokonaisluku.\n");
return 1;
}
} while (number <= 0);
Joskus tarvitaan toistorakenne toistorakenteen sisälle. Seuraavassa esimerkissä kaksiulotteisen kokonaislukutaulukon alkiot tulostetaan rivi kerrallaan kahdella sisäkkäisellä for-silmukalla.
int main()
{
int tableOfNumbers[3][4] = {
{5, 2, 9, 11},
{4, 6, 8, 10},
{12, 8, 4, 3}
};
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 4; column++) {
printf("%d, ", tableOfNumbers[row][column]);
}
printf("\n");
}
return 0;
}
Edellisen ohjelman tulostus on:
5, 2, 9, 11, 4, 6, 8, 10, 12, 8, 4, 3,
Operaattorien avulla voidaan manipuloida muuttujien arvoja, kuten kasvattaa tai vähentää.
Matemaattiset operaattorit ovat:
Incrementointi tarkoittaa tässä yhteydessä muuttujan arvon kasvattamista yhdellä ja decrementointi taas vähentämistä yhdellä. C-kielessä nämä ovat
Yksinkertaisin sijoitusoperaattori on =, joka siis sijoittaa merkin oikealla puolella olevan arvon(muuttuja tai vakio) merkin vasemmalla puolella olevaan muuttujaan.
Muita sijoitusoperaattoreita ovat :
| Operaattori | Esimerkki | Kuvaus |
|---|---|---|
| = | a = b | a = b |
| += | a += b | a = a+b |
| -= | a -= b | a = a-b |
| *= | a *= b | a = a*b |
| /= | a /= b | a = a/b |
| %= | a %= b | a = a%b |
Osoitin (pointer) eli osoitinmuuttuja tarkoittaa muuttujaa, johon voidaan sijoittaa muistiosoite. Osoitin luodaan ilmoittamalla minkä tyyppisen muuttujan osoite halutaan sijoittaa ja osoittimen nimen eteen kirjoitetaan *-merkki. Esimerkiksi mypointer-niminen osoitinmuuttuuja, johon voidaan sijoittaa kokonaislukumuuttuja osoite voidaan luoda seuraavasti
int *mypointer;Tutkitaan vielä osoitinta seuraavan esimerkin avulla
int x=5;
int *mypointer;
mypointer=&x;
printf("x:n arvo eli *mypointer on %d\n",*mypointer);
printf("x:n osoite eli mypointer on %p\n",mypointer);
Edellä siis lauseella mypointer=&x; mypointer osoittimeen sijoitettiin muuttujan x osoite (eli &x).
Osoitinta tarvitaan, jos funktion pitää muuttaa sille annettujen muuttujien arvoja.
Esimerkiksi seuraavassa sovelluksessa funktiolle doubleValueByValue annetaan muuttujan x arvo argumenttina. Tällöin funktio ei muokkaa alkuperäisen muuttujan x arvoa, koska funktion parametrina määritelty muuttuja x ei ole sama muuttuja kuin mainissa määritelty x.
void doubleValueByValue(int x);
int main()
{
int x=5;
doubleValueByValue(x);
printf("Test1: mainissa x:n arvo=%d\n",x);
return 0;
}
void doubleValueByValue(int x){
x=2*x;
printf("funktiossa doubleValueByValue x:n arvo=%d\n",x);
}
Ohjelma tulostaa seuraavaa:
funktiossa doubleValueByValue x:n arvo=10 Test1: mainissa x:n arvo=5Jos halutaan, että funktio muuttaa sille argumenttina annetun muuttujan arvoa, määritetään funktion parametri osoittimeksi ja argumenttina viedään muuttujan x osoite eli seuraavasti
void doubleValueByPointer(int *x);
int main()
{
int x=5;
doubleValueByPointer(&x);
printf("Test2: mainissa x:n arvo=%d\n",x);
return 0;
}
void doubleValueByPointer(int *x){
*x=2**x;
printf("funktiossa doubleValueByPointer x:n arvo=%d\n",*x);
}
Jolloin ohjelma tulostaa
funktiossa doubleValueByPointer x:n arvo=10 Test2: mainissa x:n arvo=10
Tietue (struct) on käyttäjän määrittelemä tietotyyppi, jonka avulla samaan kokonaisuuteen liittyviä arvoja voidaan ryhmitellä. Tietueen kentät voivat olla keskenään eri tyyppisiä. Seuraava määrittely luo tietuetyypin struct Person:
struct Person {
int number;
char name[20];
};
Määrittely ei vielä luo muuttujaa. Tietueen sisällä määriteltyjä muuttujia number ja name kutsutaan kentiksi eli jäseniksi (member). Tietuetyypistä voidaan luoda tietuemuuttujia seuraavasti:
struct Person p1; struct Person p2;
Avainsanalla typedef tietuetyypille voidaan antaa lyhyempi vaihtoehtoinen nimi. Seuraavassa Person toimii tyypin nimenä, joten muuttujan määrittelyssä ei tarvitse kirjoittaa avainsanaa struct:
typedef struct Person {
int number;
char name[20];
} Person;
Person p1; Person p2;
Tietuemuuttujan kenttään viitataan pisteoperaattorilla (.). Tietueeseen osoittavan osoittimen kautta kenttään viitataan nuolioperaattorilla (->). Seuraava esimerkki havainnollistaa niiden eroa:
#include <stdio.h>
#include <string.h>
typedef struct Person {
int number;
char name[20];
} Person;
int main(void) {
Person p1;
p1.number = 1;
strcpy(p1.name, "Jim Morrison");
printf("p1: nimi = %s, numero = %d\n", p1.name, p1.number);
Person *p2 = &p1;
printf("p2:n osoittama tietue: nimi = %s, numero = %d\n",
p2->name, p2->number);
return 0;
}
Osoitin p2 osoittaa samaan tietueeseen kuin muuttuja p1, joten esimerkissä ei luoda toista henkilöä. strcpy-funktio on esitelty otsikkotiedostossa <string.h>, ja sillä voidaan kopioida merkkijono toiseen merkkijonoon. Kohdetaulukossa on oltava riittävästi tilaa kopioitavalle merkkijonolle.
Samasta tietuetyypistä voidaan muodostaa taulukko. Seuraava esimerkki tallentaa kolmen joukkueen jäsenen numerot ja nimet:
Joukkuetta varten luodaan team-niminen kolmen alkion tietuetaulukko lauseella Person team[3] = {0};. Person on tietuetyypin nimi, team on taulukkomuuttujan nimi ja [3] ilmoittaa taulukon alkioiden määrän. Alustus {0} alustaa taulukon kaikkien tietueiden kentät nolliksi: jokaisen number-kentän arvoksi tulee 0 ja name-kentän merkit alustetaan nollamerkeiksi, jolloin nimi on aluksi tyhjä merkkijono.
#include <stdio.h>
#include <string.h>
typedef struct Person {
int number;
char name[20];
} Person;
Person team[3] = {0};
for (int x = 0; x < 3; x++) {
printf("Anna %d. numero\n", x + 1);
scanf("%d", &team[x].number);
int c;
while ((c = getchar()) != '\n' && c != EOF) {
}
printf("Anna %d. nimi\n", x + 1);
if (fgets(team[x].name, sizeof(team[x].name), stdin) != NULL) {
team[x].name[strcspn(team[x].name, "\n")] = '\0';
}
}
printf("Joukkueen jäsenet\n");
for (int x = 0; x < 3; x++) {
printf("Nimi: %s, numero: %d\n", team[x].name, team[x].number);
}
scanf-funktion jälkeen syötepuskuriin jäävä rivinvaihto poistetaan getchar-silmukalla ennen nimen lukemista. Nimi luetaan fgets-funktiolla, jotta se voi sisältää välilyöntejä. fgets tallentaa yleensä myös rivinvaihdon merkkijonoon, joten se etsitään strcspn-funktiolla ja korvataan merkkijonon lopetusmerkillä '\0'.
C-kielinen sovellus voi kirjoittaa tai lukea tiedostoja. Käsiteltävä tiedosto voi olla joko tekstitiedosto tai binääritiedosto. Tässä tutkitaan vain tekstitiedoston käsittelyä.
Tiedoston käsittelyssä tarvitaan osoitin johon tallennetaan käsiteltävän tiedoston muistiosoite.
Tiedosto-osoitin voidaan määritellä seuraavasti
FILE *filepointer;Sen jälkeen funktiolla fopen-voidaan avata tiedosto. fopen-funktiolle annetaan kaksi argumenttia
FILE *filepointer;
filepointer=fopen("C:/temp/test2.txt","w");
Lopuksi tiedosto suljetaan fclose-lauseella.
Tiedostosta voidaan lukea merkkejä fscanf-funktiolla. Sen syntaksi on muuten sama kuin scanf-funtkiolla, mutta ensimmäinen parametri on tiedosto osoitin. Esimerkiksi kaksi lausetta, joista ensimmäinen lukee ruudulta annetun luvun ja jälkimmäinen tiedostosta
scanf("%d", &age);
fscanf(filepointer, "%d", &age);
Tiedostota voidaan lukea myös fgetc ja fgets funktioilla. Niistä on esimerkkejä alla.
Tiedostoon voidaan kirjoittaa fprintf-funktiolla. Sen syntaksi on muuten sama kuin printf-funktiolla, mutta ensimmäinen parametri on tiedosto osoitin. Esimerkiksi kaksi lausetta, joista ensimmäinen tulostaa ruudulle ja jälkimmäinen tiedostoon
printf("Name of the student = %s and age = %d",name, age);
fprintf(filepointer,"Name of the student = %s and age = %d",name, age);
Tiedostoon voidaan kirjoittaa yksi merkki funktiolla fputc. Esimerkiksi näin
char x='a'; fputc(x, filepointer);
Esimerkiksi seuraava sovellus kirjoittaa tiedostoon rivin "Hello world"
int main()
{
FILE *filepointer;
filepointer=fopen("C:/temp/test2.txt","w");
fprintf(filepointer,"Hello world\n");
fclose(filepointer);
return 0;
}
Huomaa, että tiedostopolussa on käytetty kauttaviivaa(/) ei kenoviivaa(\). Kenoviivaa ei voida käyttää, koska sillä on esityismerkityksiä C-kielessä kuten
"C:\\temp\\test2.txt"
Seuraavassa esimerkissä luetaan tiedostoa merkki kerrallaan ja tulostetaan merkit ruudulle
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *filepointer;
char ch;
filepointer=fopen("C:/temp/test2.txt","r");
if (filepointer == NULL)
{
perror("Error while opening the file.\n");
exit(EXIT_FAILURE);
}
printf("The contents of file is:\n");
while((ch = fgetc(filepointer)) != EOF)
printf("%c", ch);
fclose(filepointer);
return 0;
}
Esimerkissä if-lauseella tarkistetaan, onko tiedosto olemassa. Jos ei ole niin lopetetaan, eikä yritetä jatkaa. while-loopilla merkkejä luetaan, kunnes saavutetaan tiedoston loppu.
EOF ei ole tiedoston loppumerkki, vaan funktio fgetc palauttaa EOF arvon luettuaan tiedoston loppumerkin.
Edellä siis lukemiseen käytettiin funktiota fgetc, joka lukee yhden merkin. Lukemiseen voidaan käyttää myös funktioita
Seuraavassa esimerkissä luetaan tiedostoa rivi kerrallaan ja tulostetaan sisältö ruudulle.
char row[255];
while (fgets(row, sizeof(row), filepointer) != NULL) {
printf("%s", row);
}
Ja sama voidaan tehdä sana kerrallaan seuraavasti.
char word[50];
while (fscanf(filepointer, "%49s", word) == 1) {
printf("%s ", word);
}
Edellä silmukka jatkuu niin kauan kuin fscanf onnistuu lukemaan tiedostosta yhden sanan. Paluuarvo 1 tarkoittaa, että yksi merkkijono (sana) on luettu onnistuneesti. Kun tiedoston loppu saavutetaan tai lukeminen epäonnistuu, fscanf ei enää palauta arvoa 1 ja silmukka päättyy.
%49s tarkoittaa, että scanf lukee enintään 49 merkkiä merkkijonoon. Tämä estää puskuriylivuodon, koska taulukkoon jää tilaa myös lopetusmerkille '\0' (esim. char word[50]).
C-ohjelman näkökulmasta tietokoneen keskusmuisti voidaan jakaa kahteen pääalueeseen:
malloc-, calloc- tai realloc-funktioiden avulla.
Ohjelmoijan vastuulla on vapauttaa tämä muisti myöhemmin free-funktiolla.
Automaattinen muuttuja on muuttuja, joka varataan automaattisesti pinomuistiin silloin, kun funktio kutsutaan, ja vapautetaan automaattisesti funktiosta poistuttaessa.
Dynaamista muistia käytetään, jos esimerkiksi tarvitaan niin iso taulukko, että pinomuisti ei riitä sille. Windowsissa pinomuistin koko on yleensä 2 MB ja int muuttuja vie 4 tavua, niin maksimi kokonaisluku taulukon koko on noin 500 000 int-alkiota.
Esimerkiksi seuraava sovellus kaatuu Windowsissa, koska pinomuisti ei riitä.#include <stdio.h>
int main()
{
int myarray[1000000];
printf("Sovellus kaatui varmaankin\n");
return 0;
}
Tehdään sama sovellus käyttäen dynaamista muistia taulukolle. Dynaaminen muistinvaraus voidaan tehdä mm. funktioiden malloc tai calloc avulla. Alla olevassa esimerkissä käytetään malloc-funktiota.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *myarray = (int*)malloc(1000000 * sizeof(int));
if (myarray == NULL) {
printf("Muistin varaus epäonnistui\n");
return 1;
}
printf("Taulukon luonti onnistui\n");
free(myarray);
return 0;
}
Dynaamista muistia tarvitaan myös tilanteissa, joissa tarvittavan muistimäärän koko ei ole tiedossa käännösaikana. Esimerkiksi kun käyttäjältä kysytään, kuinka monta lukua hän haluaa syöttää, eikä lukumäärää tiedetä etukäteen. Tällöin voidaan varata taulukko dynaamisesti kekomuistista.
Dynaaminen muisti on hyödyllinen myös silloin, kun tarvitaan rakenteeltaan muuttuvia tai suuria tietorakenteita, kuten linkitettyjä listoja, puita tai dynaamisesti kasvavia taulukoita.
Kun sovellus käännetään c-kääntäjän on tiedettävä kuinka paljon muistia sen tulee varata sovellukselle. Täten esimerkiksi kokonaisluku taulukon kokoa ei voida asettaa sovelluksen suorituksen aikana, jos ei käytetä dynaamista muistinvarausta.
Alla olevassa esimerkissä käytetään malloc-funktiota dynaamiseen muistinvaraukseen.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *array_pointer;
int array_size;
int sum=0;
printf("Montako lukua haluat antaa?\n");
scanf("%d",&array_size);
//Varataan taulukolle tilaa dynaamisesta muistista
array_pointer = (int*)malloc(array_size * sizeof(int));
if(array_pointer == NULL){
printf("Taulukon luonti ei onnistu\n");
}
else {
for(int x=0; x<array_size; x++){
printf("Anna %d.luku\n",x+1);
scanf("%d",&array_pointer[x]);
sum+=array_pointer[x];
}
printf("Annoit luvut\n");
for(int x=0; x<array_size; x++){
printf("%d ,",array_pointer[x]);
}
printf("\nAnnettujen lukujen summa=%d\n",sum);
//Vapautetaan varattu muisti
free(array_pointer);
}
return 0;
}