Syöte- ja tulostus C-kielessä¶
Osaamistavoitteet: Tämän materiaalin luettuasi osaat tuottaa muotoiltua tulostusta ja tiedät mihin tarkoitukseen se on hyödyllistä sulautetuissa järjestelmissä.
Aiemmassa materiaalissa olemme jo onnistuneet tulostamaan tekstiä tietokoneen ruudulle C-kielellä
printf-funktion avulla. Seuraavaksi paneudutaan asiaan hieman syvemmälti ja katsotaan miten tehdään muotoiltua tulostusta. Tässä yhteydessä esitellään C:n standardikirjaston stdio muotoiltuun tulostukseen ja syötteen lukemiseen tehdyt valmiit funktiot. Koska nämä funktiot kuuluvat C:n standardikirjastoon, niiden rajapinta ja perustoiminta on standardoitu. Varsinainen toteutus ja tuettujen ominaisuuksien määrä voivat kuitenkin vaihdella järjestelmien välillä, erityisesti sulautetuissa ympäristöissä.Sulautetuissa järjestelmissä esiintyy yleisesti erilaisia standardikirjastojen toteutuksia, joissa kaikkea toiminnallisuutta ei ole mukana. Resurssirajoitusten vuoksi esimerkiksi liukulukujen käsittely, tulostus ruudulle, syötteiden kysely näppäimistöltä, jne, on jätetty pois. Standardikirjaston sijaan usein tarjotaan oheislaitteille omia kirjastoja, jotka toteuttavat vastaavan toiminnallisuuden, esimerkiksi funktioita merkkijonon tulostukseen LCD-näytölle.
Emme vielä tässä vaiheessa tiedä kaikkea C-kielen erikoisuuksista, mutta pärjäämme silti tämän materiaalin funktioiden kanssa aivan mainiosti.
Muotoiltu tulostus¶
C-kielessä on useita funktioita, joilla voidaan tuottaa muotoiltua tulostusta hieman eri tarkoituksiin. Käsittelemme tässä kaksi yleistä tapausta, joista jälkimmäinen on tärkeä tietää tehokkaassa sulautettujen ohjelmoinnissa.
Funktio printf¶
Funktio
printf tulostaa työasemissa merkkijonon valitulle oheislaitteelle (näytölle) sen tulostusvuohon, josta käyttöjärjestelmä sitten poimii tuotoksemme ja toimittaa sen oheislaitteelle. Katsotaanpa esimerkkiä printf:n käytössä..
uint8_t a=97;
printf("Luku on %d ja se plus yksi on %d\n", a, a+1);
// Tämä tulostaa näytölle
Luku on 97 ja se plus yksi on 98
Tarkemmin funktion
printf syntaksi on:printf("muotoilumerkkijono", argumentti1, argumentti2, ..., argumenttiN);
Muotoilumerkkijono kertoo, millaisen merkkijonon haluamme tulostaa. Merkkijono voi sisältää valmista tekstiä, ohjausmerkkejä (rivinvaihto jne) tekstin tulostamiseen ja sekä muuttujien arvoja (ja vakioiden arvoja). Muuttujien osalta muotoilumerkkijono kertoo miten muuttujien arvot muokataan tulostukseen osana merkkijonoa.
Yllä esimerkissä muotoilumerkkijono on
"Luku on %d ja plus yksi on %d\n". Nyt on siis sekaisin tekstiä ja muuttujan (tässä a ja a+1) arvoja. Paikkamerkki % kertoo, että tähän kohti tulostusta tulee muuttujan arvo ja prosenttimerkin perässä olevat lisätiedot sen miten arvo tulostetaan. Muodollisesti prosenttimerkin ja siihen liittyvien merkkien yhdistelmää kutsutaan muotoilumääreeksi (engl. format specifier). Esimerkissä paikkamerkillä %d kerromme että halutaan tulostaa muuttujan arvo kokonaislukuna. Näitä paikkamerkkejä (muotoilumääreitä) C-kielessä koko onkin joukko, joista tässä alla yleisimmät.
| Paikkamerkki | Muutujatyyppi |
| d,i | kokonaisluku (int) |
| u | etumerkitön kokonaisluku (unsigned int) |
| f | liukuluku (float, double) |
| x | heksadesimaaliluku |
| c | merkki (ASCII-koodin mukaan) |
| s | merkkijono(char-taulukko) |
| p | osoitin, muistiosoite (palataan tähän) |
| % | prosenttimerkki |
Hox! Binääriluvun tulostamiseen ei siis ole standardissa suoraa muotoilua. Tässä onkin oiva harjoitustehtävä itseopiskeluun..
Esimerkki.
uint8_t x=97;
printf("x on kokonaislukuna %d\n",x);
printf("x on heksalukuna %x\n",x);
printf("x on liukulukuna %f\n",(float)x);
// Näytölle tulostuu..
x on kokonaislukuna 97
x on heksalukuna 61
x on liukulukuna 97.000000
Muotoilumääre kertoo
printf-funktiolle, miten vastaava argumentti tulkitaan. Siksi argumentin tyypin täytyy olla yhteensopiva muotoilumääreen kanssa. Esimerkiksi %d odottaa kokonaislukuarvoa, kun taas %f odottaa liukulukuarvoa.printf ei automaattisesti muunna vääräntyyppistä argumenttia muotoilumääreen perusteella. Jos haluamme tulostaa muuttujan x liukulukuna, voimme muuntaa sen eksplisiittisesti ennen funktiokutsua:printf("x on liukulukuna %f\n", (float)x);
Ja kun käytämme johdettuja muuttujatyyppejä, saattaa kääntäjä joskus edellyttää tyyppimmuunnosta tai käännösvaiheessa tulee varoitus.
Muotoilun tarkentaminen voidaan tehdä lisätiedoilla:
Muotoilumäärettä voidaan tarkentaa lisätiedoilla:
Muotoilumäärettä voidaan tarkentaa lisätiedoilla:
| Määre | Lisämuotoilu |
| n | koko tulostuskentän vähimmäisleveys; oletuksena oikealle tasattu |
| -n | koko tulostuskentän vähimmäisleveys; vasemmalle tasattu |
| 0n | kentän vähimmäisleveys, joka täytetään tarvittaessa etunollilla |
| .n | tarkkuus; %f-muotoilussa desimaalipilkun jälkeen tulostetaan täsmälleen n numeroa |
| l | pituusmääre, jota käytetään long-kokonaislukutyyppien kanssa |
| h | pituusmääre, jota käytetään short-kokonaislukutyyppien kanssa |
| + | näytetään aina etumerkki (+ tai -) |
Esimerkiksi
%05d käyttää vähintään viiden merkin levyistä kenttää, kun taas %.2f tulostaa liukuluvun kahdella desimaalilla.Esimerkki.
uint8_t x=97;
printf("x on kokonaislukuna %05d\n",x);
printf("x on liukulukuna %.2f\n",(float)x);
float y = 97.126;
printf("y on liukulukuna %.2f\n",x);
// Näytölle tulostuu..
x on kokonaislukuna 00097
x on liukulukuna 97.00
y on liukulukuna 97.13
Ohjausmerkkejä käytetään merkki- ja merkkijonoliteraaleissa erikoismerkkien esittämiseen. Ne alkavat kenoviivalla
\. Esimerkiksi \n tarkoittaa rivinvaihtoa ja \t tabulaattoria.Alla on joitakin yleisiä ohjausmerkkejä:
Alla on joitakin yleisiä ohjaus- ja erikoismerkkejä:
| Merkkivakio | Tarkoitus |
| \n | rivinvahto |
| \t | sarkain (tabulaattori) |
| \\ | takakeno |
| \? | kysymysmerkki |
| \" | lainausmerkki |
Funktio sprintf¶
Alussa lupasimme käsitellä kaksi eri standardikirjaston tulostusfunktion versiota, joten esittelemme niistä nyt toisen eli
sprintf-funktion. Tälle funktioille on erittäin hyödyllisiä käyttötarkoituksia sulautettuja järjestelmiä ohjelmoidessa. Nyt, tämä funktio toimii muuten samoin kuin
printf, mutta siinä argumentiksi annetaan lisäksi merkkijono, johon tulostus menee talteen. Funktiolla siis muotoillaan tekstiä toiseen merkkijonoon.. ok, mutta miksi? Sulautetuissa järjestelmissä tulostukset ohjataan jollekin oheislaitteelle sen oman (ajuri)kirjaston kautta. Usein näihin ajurikirjastoihin ei ole toteutettu standardikirjaston läheskään kaikkia tulostusominaisuuksia, vaikka siellä ehkä onkin printf-niminen funktio. Kuten yllä todettiin, esimerkiksi liukulukujen tulostustoiminnallisuus saattaa puuttua kokonaan. Näin ollen
sprintf on erittäin kätevä siksi, että voimme sen avulla muotoilla valmiiksi merkkijonon oheislaitteelle tulostusta varten. Oheislaitteen tulostusfunktio saa sitten valmiin merkkijonon argumentiksi, eikä sen tarvitse muokata tulostusta. Helppoa ja kätevää!Nyt syntaksissa
sprintf eroaa siltäosin, että muotoilumerkkijonon eteen annetaan argumentiksi merkkijono, johon tulostus tallentuu. Esimerkki. Tulostetaan pi:n likiarvo LCD-näytölle. Oletetaan LCD:n kirjastossa olevan
lcd_printf-funktio merkkijonon tulostamista varten. Määritellään myös LCD-näytön leveys merkkeinä vakioon LCD_MAX_WIDTH. Koska C-merkkijono tarvitsee lisäksi nollaterminointimerkin, varaamme puskurista yhden merkin lisää. Joten nyt muuttujaan lcd_str mahtuu juuri näytön rivin verran merkkejä.#define LCD_MAX_WIDTH 16
float pi = 3.14159;
char lcd_str[LCD_MAX_WIDTH+1];
sprintf(lcd_str,"%f\n",pi);
lcd_printf(lcd_str);
Tämä esimerkki kannattaa sisäistää, koska juuri näin tulemme toimimaan myöhemmin kurssilla. Tästä lisää myöhemmin..
Hox! Ole erityisen tarkkana, että muotoiltu tulostus mahtuu kohdemerkkijonolle varattuun tilaan. Lopullinen tulostus voi olla huomattavasti pidempi kuin itse muotoilumääre. Esimerkiksi
%08d tuottaa vähintään kahdeksan merkkiä. Jos kohdepuskuri on liian pieni, sprintf voi kirjoittaa sen lopun yli ja ylikirjoittaa muuta muistissa olevaa dataa. Tällainen puskurin ylivuoto voi olla vaikea jäljittää ja aiheuttaa ohjelman virheellistä toimintaa tai jopa järjestelmän kaatumisen.Ylivuodon Välttäminen: snprintf¶
Vaikka
sprintf on erittäin hyödyllinen, siihen liittyy merkittävä riski sulautetuissa järjestelmissä: puskurovervuotojen mahdollisuus. Tämän riskin minimoimiseksi voimme käyttää turvallisempaa vaihtoehtoa, snprintf-funktiota. Tämä funktio antaa meille mahdollisuuden määritellä maksimimäärä merkkejä, jotka kirjoitetaan puskuriin, mukaan lukien nollaterminointimerkki, estäen mahdollisuuden kirjoittaa puskurin yli.Palataanpa aiempaan esimerkkiimme, jossa muotoilimme pi:n arvon LCD-näytölle:
#define LCD_MAX_WIDTH 16
float pi = 3.14159;
char lcd_str[LCD_MAX_WIDTH];
snprintf(lcd_str, sizeof(lcd_str), "%f\n", pi);
lcd_printf(lcd_str);
Tässä esimerkissä
snprintf-funktion toinen argumentti on sizeof(lcd_str), mikä varmistaa, että tulostus ei ylitä lcd_str-puskuriin varattua tilaa. Jos muotoiltu merkkijono (mukaan lukien nollaterminointimerkki) on liian pitkä, snprintf katkaisee sen sopimaan puskuriin, varmistaen silti, että merkkijono on asianmukaisesti nollaterminoitettu.Hox! On äärimmäisen tärkeää käyttää
sizeof-operaatiota, kun määritellään puskurin koko snprintf-funktiolle, sillä koon kovakoodaaminen voi johtaa virheisiin, jos puskurin koko muuttuu tulevaisuudessa. Tämä käytäntö auttaa välttämään hankalia virheitä, jotka liittyvät puskurovervuotoihin, ja jotka voivat olla erityisen haastavia debugata sulautetuissa järjestelmissä.Sulautettua asiaa¶
Katsotaanpas seuraavaksi mitä muuta, ja varsin oleellista, hyötyä muotoillusta tulostuksesta on sulautettuja järjestelmiä ohjelmoidessa.
Ohjelman debuggaus¶
Funktio
sprintf on yllämainitun käyttötarkoituksen lisäksi korvaamaton apu sulautetun järjestelmän toiminnan debuggaamisessa, eli toiminnan ajonaikaisessa seuraamisessa ja virhetilanteiden selvittelyissä. Muistetaan ensimmäiseltä luennolta, että sulautetun ohjelman suorittamista laitteessa voitiin kontrolloida debuggeri-laitteen avulla. Verrattuna työasemaohjelmointiin, on sulautetun ohjelman suoritusta ja fyysisen laitteen sielunelämää vaikea seurata "etänä" työasemalta. Näinollen debuggerit yleensä tarjoaa (erilaisia) keinoja lähettää viestejä laitteelta debuggerin kautta ohjelmointiympäristölle. Esimerkiksi merkkijonoja voidaan laitteessa pyörivästä ohjelmasta tulostaa ohjelmointiympäristön konsoli-ikkunaan.
Esimerkki. Nyt vaikkapa
for-silmukassa voidaan tarkastella lähes reaaliajassa keräämiämme anturidatan arvoja. Oletetaan, että kiihtyvyysanturin eri akselien arvot on jo haettu muuttujiin acc_x, acc_y ja acc_z, joista ne tulostetaan merkkijonoon debug_str. Funktio System_printf tulostaa SensorTagilta CCS-kehitysympäristön konsoli-ikkunaan, kuten tulemme myöhemmässä materiaalissa näkemään.char debug_str[80];
...
sprintf(debug_str,"%.2f %.2f %.2f",acc_x,acc_y,acc_z);
System_printf(debug_str);
...
Pico SDK:ssä sama tulos voidaan saavuttaa käyttämällä
printf-funktiota System_printf-funktion sijasta. Tässä tapauksessa sprintf-funktiota ei oikeastaan tarvita, koska printf voi muotoilla numeeriset arvot ja kirjoittaa ne suoraan stdout-virtaan. Käytämme sprintf-funktiota tässä vain vertailun vuoksi. Pico SDK käyttää omaa printf-funktiotaan, joka on rajoitettu versio funktiosta, mutta kuluttaa huomattavasti vähemmän resursseja. char debug_str[80];
...
sprintf(debug_str,"%.2f %.2f %.2f",acc_x,acc_y,acc_z);
printf("%s", debug_str);
...
Hox! Ette arvaakaan kuinka vuosien aikana moni harjoitustehtävissä ja -työssä ollut bugi on selvinnyt debug-välitulostusten avulla. Tässä monesti aloitteleva ohjelmoija voi ajatella, että eipäs kun minä selvitän tämän nyt päässäni, johon menee sitten aikaa. Näin voi kurssilla toki tehdä, mutta sitten siellä työelämässä ammattilaisetkin mieluummin turvautuvat ohjelman toiminnan lokitukseen, josta ongelma voi olla huomattavasti helpompi bongata. Isommissa ohjelmistoissa siellä työelämässä lokitus usein on ainoa vaihtoehto selvittää bugeja, varsinkin jos ohjelma koostuu useasta yhtaikaa ajettavasta modulista.
Kuitenkin
printf saattaa aiheuttaa merkittävää resurssikuormitusta (esim. suurta muistin käyttöä) ja tehdä järjestelmästä epäluotettavan. Siksi on tärkeää ymmärtää rajoitukset ja mahdolliset virheet. Sulautetuissa järjestelmissä printf-funktiolla kirjoitettu tulostus voidaan puskuroida ennen kuin se todella lähetetään UARTin, USB:n tai muun tulostusrajapinnan kautta. Jos puskuria ei tyhjennetä tai järjestelmä pysähtyy, nollautuu tai jatkaa suoritusta ennen kuin data ehditään lähettää, osa tulostuksesta voi jäädä näkymättä. Tarvittaessa puskuroitu tulostus voidaan pakottaa lähetettäväksi esimerkiksi funktiolla fflush(stdout).Pilkku on tärkeä¶
Sulautetussa maailmassa (ja muuallakin) toisiinsa liittyvää informaatiota, kuten vaikkapa kiihtyvyysanturin eri akselien arvot samalla ajan hetkellä, on hyödyllistä käsitellä yhtenä pakettina (tietorakenteena) ohjelmissa. Tietorakenteihin mennään tarkemmin myöhemmässä materiaalissa, mutta katsotaanpa tässä miten
sprintf-funktio liittyy asiaan. Yleisesti toisiinsa liittyvän tiedon välittämiseen on tietotekniikassa esitelty merkkijonopohjainen Comma Separated Value (CSV)-formaatti, jossa tietorakenteen eri kenttien erottimena toimii pilkku. CSV:stä on esitetty eeri käyttöön soveltuvia standardeja, mutta niitä ei tarvitse kurssilla tietää. Selkeitä käyttötarkoituksia CSV-formaatille sulautetuissa järjestelmissä on merkkijonopohjainen kommunikointi oheislaitteiden kanssa sekä langaton viestintä.
Esimerkki. Yllämainitun kiihtyvyysanturin eri akselien arvot voitaisiin esittää CSV-muodossa seuraavasti, missä pilkulla erotetut numeroarvot vastaavat akselien arvoja.
0.342,0.665,0.734
Huomataan, että tällaista CSV-muotoisia tiedonvälitystä on C-kielessä helppo toteuttaa
sprintf-funktion avulla. Esimerkki. Jälleen kiihtyvyysanturin 3:n akselin arvot on kerätty talteen muuttujiin
acc_x, acc_y ja acc_z ja tehdäänpäs niistä CSV-muotoinen merkkijono ylläolevan esimerkin mukaan. char message[80];
sprintf(message,"%.3f,%.3f,%.3f\n",acc_x,acc_y,acc_z);
Hox! Kurssin harjoitustyössä tullaan tarvitsemaan
sprintf-funktiota tässä käyttötarkoituksessa ihan varmasti. Syötteen käsittely¶
No, vielä asiasta toiseen, eli
stdio-kirjaston funktioilla voimme myös kysyä näppäimistösyötettä käyttäjältä (tai itseasiassa muultakin oheislaitteelta). Laiteläheisessä ohjelmoinnissa hyvin harvoin, jos koskaan, käytetään näitä funktiota joten tämä materiaali vain tiedoksi..Tässä funktio
getchar tarjoaa keinon kysyä käyttäjältä syötettä merkki kerrallaan. Esimerkki. Seuraavalla
do-while-rakenteella käyttäjältä kysytyt merkit tallentuvat muuttujaan c yksi kerrallaan, kunnes käyttäjä painaa Enter-näppäintä (suom. palautusnäppäin..).uint8_t c = 0;
do {
c = getchar();
} while (c != '\n');
..eli on
do-while-rakenteella ne kätevät puolensakin. Jos tässä haluaisimme talteen koko syötteen, pitäisi se tallentaa merkkijonoon yksittäisen merkkimuuttujan sijasta. Huomataan, että kun työasemalla testaamme ohjelmaa, näppäimistö kaiuttaa näppäinpainallukset ruudulle näkyviin. Eri käyttöjärjestelmissä voimme toki ohjata miten konsoli-ikkunan syöte näkyy ruudulla ja kaiutus saadaan tarvittaessa pois päältäkin, esimerkiksi kun kysymme käyttäjältä salasanaa.
Muotoiltu syöte¶
Funktio
scanf on standardikirjaston funktio, jolla voidaan kysyä käyttäjältä muotoiltua syötettä ja tallentaa se muuttujiin halutussa muodossa. Asia vain tiedoksi, kurssilla emme käytä tätä funktiota.Funktion esittely on samankaltainen kuin
printf:n, pienillä, mutta oleellisilla eroilla, kun käytössä on samat muotoilut. Lisäksi muuttujien eteen funktiokutsussa tulee &-operaattori, mutta palataan siihen myöhemmin. scanf(muotoilumerkkijono, &muuttuja1, &muuttuja2, ...);
Esimerkki. Kysytään käyttäjältä työasemassa kokonaisluku ja tulostetaan se näytölle.
int luku = 0;
scanf("%d", &luku);
printf("%d\n", luku);
Funktiolla
scanf voidaan kysyä muotoiltuja syötteitäkin, mutta muotoilu on helppo rikkoa, koska funktio itsessään ei voi kontrolloida mitä käyttäjä oikeasti näppäilee. Alla, jos käyttäjä ei syötteessä noudattaisi funktiolle annettuja lisämääreitä, esim. numeron pituus suurempin kuin kaksi merkkiä, menisi funktio sekaisin.int paiva = 0;
char kuukausi[12];
int vuosi = 0;
printf("Anna paivamaara muodossa: 24.Joulukuuta 2017? ");
scanf("%2d.%11s %4d", &paiva, kuukausi, &vuosi);
Vähemmän yllättäen löytyy myös
sscanf-funktio, joka lukee muotoiltua syötettä merkkijonosta standardisyötteen sijasta.Muita tärkeitä stdout-funktioita¶
stdout on vakio tulostusvirta, eli tiedostovirta, jota ohjelmat käyttävät tulostuksensa (yleensä tekstin) lähettämiseen. Oletuksena virta on kytketty terminaaliin / konsoliin ja näin ollen näkyy näytöllä. Mutta se voidaan ohjata myös tiedostoihin (esim. lokitiedostoihin) tai uudelleenohjata toiseen oheislaitteeseen (esim. sarjaliikenne USB:n yli Raspberry Pi Picolla). stdout-kohde on konfiguroitavissa. printf-funktio lähettää muotoillun tulostuksen stdout-virtaan, kun taas sprintf ja snprintf tallentavat muotoillun tulostuksen merkkijonoon. stdout-virtaan kirjoittamiseen on myös muita hyödyllisiä funktioita:puts (const char s[])-> Lähettää merkkijonon ilman muotoiluvalintojastdout-virtaan. Lisää automaattisesti rivinvaihdon\nmerkkijonon loppuun.fwrite(const void source[] source, size_t size, size_t amount, stdout)-> Lähettää binääridataa muistilohkosta (puskuritaulukosta)stdout-virtaan. Puskuri voi sisältää mitä tahansa tyyppiä. Funktio palauttaa onnistuneesti kirjoitettujen alkioiden määrän.
{{{hightlight=c
//Oletetaan, että vastaanotamme ulkoisesta lähteestä dataa oheislaitteelta ja tallennamme sen sample_bufferiin
//Jossain kohtaa koodia jokin täyttää tämän puskurin:
//Oletetaan, että vastaanotamme ulkoisesta lähteestä dataa oheislaitteelta ja tallennamme sen sample_bufferiin
//Jossain kohtaa koodia jokin täyttää tämän puskurin:
- define MEMS_BUFFER_SIZE 256
int16_t sample_buffer[MEMS_BUFFER_SIZE];
//Myöhemmin voimme lähettää koko taulukon stdoutiin tavuina. Esim. lähettää sen toiseen laitteeseen USB-sarjaliikenteen kautta, jota käytetään stdoutina.
int16_t temp_sample_buffer[MEMS_BUFFER_SIZE];
int sent_bytes = 0;
//Kopioi sisältö väliaikaiseen puskuriin
memcpy(temp_sample_buffer, sample_buffer, (size_t)MEMS_BUFFER_SIZE * sizeof(sample_buffer[0]));
//Kirjoita stdoutiin
size_t sample_sent = fwrite(temp_sample_buffer,sizeof(temp_sample_buffer[0]),MEMS_BUFFER_SIZE,stdout);
sent_bytes += sizeof(temp_sample_buffer[0]) * sample_sent;
fflush(stdout); // Varmista, että puskurin sisältö tyhjennetään stdout-virtaan.
int sent_bytes = 0;
//Kopioi sisältö väliaikaiseen puskuriin
memcpy(temp_sample_buffer, sample_buffer, (size_t)MEMS_BUFFER_SIZE * sizeof(sample_buffer[0]));
//Kirjoita stdoutiin
size_t sample_sent = fwrite(temp_sample_buffer,sizeof(temp_sample_buffer[0]),MEMS_BUFFER_SIZE,stdout);
sent_bytes += sizeof(temp_sample_buffer[0]) * sample_sent;
fflush(stdout); // Varmista, että puskurin sisältö tyhjennetään stdout-virtaan.
putchar(int c)lähettää yhden merkinstdout-virtaan. Jotkin kirjastot voivat sisältää oman toteutuksensa. Esimerkiksi Pico SDK tarjoaaputchar_raw-funktion, joka kirjoittaa merkin ilman rivinvaihtojen muunnosta.
Lopuksi¶
Enemmissä määrin
printf:n muotoilun salaisuuksista kiinnostuneille lisätietoa löytyy mm. dokumentista Secrets of printf.Anna palautetta
Kommentteja materiaalista?