Jos esittelen ohjelman alussa taulukon kuten alla, varaan silloin 19 merkille tilan(nollatavu loppuun), eikö niin?
char merkitJonossa[20];
Mutta sitten jossain kohdassa ohjelmaa luen käyttäjän kirjoittaman tekstin gets() funktiolla ja sijoitan tekstin tuohon taulukkoon. Esim näin.
printf("Kirjoita jotain, max(19 merkkiä) : "); gets(merkitJonossa);
Mutta miten voi olla mahdollista että käyttäjä saattaa kirjoittaa vaikka 30 merkkiä pitkän tekstin ja seuraavassa tulostuslauseessa koko teksti tulee iloisesti kokonaisuudessaan ruudulle??
printf("Kirjoitit : %s\n", merkitJonossa);
Jäinkin pohtimaan minne taulukon koon yli menevät merkit joutuvat?
Jonnekin random muistipaikkaan?
Jos näin käy, niin onko tässä potentiaalinen mahdollisuus saada aikaan sekaannusta ja sotkua?
Ja jotta haluaisin varmistaa ettei käyttäjä syötä kuin ohjeistuksessa annetun määrän merkkejä niin pitääkö tuo tekstin syöttö suorittaa silmukassa jossa esim. getchar funktiolla haetaan merkki kerrallaan ja lasketaan samalla kierrosten määrää?
Ville kirjoitti:
Jos esittelen ohjelman alussa taulukon kuten alla, varaan silloin 19 merkille tilan(nollatavu loppuun), eikö niin?
char merkitJonossa[20];
Kyllä.
Ville kirjoitti:
Jäinkin pohtimaan minne taulukon koon yli menevät merkit joutuvat?
Jonnekin random muistipaikkaan?
Jos näin käy, niin onko tässä potentiaalinen mahdollisuus saada aikaan sekaannusta ja sotkua?
Ei randomiin, vaan se jatkaa iloisesti seuraavaan muistipaikkaan. Kun seuraava varataan tai on jo varattu jollekin muulle, niin siinä vaiheessa sekaannus ja sotku on taattu. Sinun tapauksessasi missään muualla ei ole vielä kyseisiin muistialueisiin sorkittu. Siksi se toimii vielä.
Ville kirjoitti:
Ja jotta haluaisin varmistaa ettei käyttäjä syötä kuin ohjeistuksessa annetun määrän merkkejä niin pitääkö tuo tekstin syöttö suorittaa silmukassa jossa esim. getchar funktiolla haetaan merkki kerrallaan ja lasketaan samalla kierrosten määrää?
Se on yksi monista vaihtoehdoista. Voit käyttää myös fgetsiä, jolla luet stdiniin.
Torgo kirjoitti:
Se on yksi monista vaihtoehdoista. Voit käyttää myös fgetsiä, jolla luet stdiniin.
Ja pääsen huomattavasti helpommalla, kiitos muistuksesta :)
Yllättävää kuinka nopeasti asiat unohtuvat ...
Tämä kyseinen ongelmasi tunnetaan nimellä Buffer overflow ja tietojen sekoittamisen lisäksi sitä voidaan käyttää myös haavoittuvuutena ohjelmaasi kohtaan.
Tässä yksi esimerkki aiheesta, joka mielestäni havainnollistaa ongelmaa mukavasti. Structin muuttujat on kirjoitettu muistiin peräkkäin, jolloin yli pitkän nimen kirjoittaminen aiheuttaa myös iän muuttumisen.
#include <iostream> using namespace std; struct Hahmo { char nimi[8]; int ika; }; int main() { Hahmo a = {"Matti", 21}; cout << "nimi = " << a.nimi << ", ika = " << a.ika << endl; cout << "Anna uusi nimi: "; cin >> a.nimi; cout << "nimi = " << a.nimi << ", ika = " << a.ika << endl; cin.ignore(); cin.get(); return 0; }
Itse ongelmahan on siinä, että taulukko (tai muukaan muistialue) ei C:ssä tiedä omaa kokoaan. Funktioille välitetään vain osoittimia (muistipaikan järjestysnumeroita), joista alue alkaa, ja on ohjelmoijan vastuulla varmistaa, että alue on riittävän iso suunniteltuun käyttöön tai että funktiolle ilmoitetaan oikea koko (kuten fgets:lle voi ilmoittaa).
Tosin tuo fgetsin käyttö ei poista kokonaan täysin alkuperäistä ongelmaa.
Sanotaan että määrään fgets funktiolle merkkien määräksi vaikkapa 20, jolloin syötteestä luetut merkit menevät mukavasti tuohon taulukkoon(sama kuin ketjun alussa).
fgets(merkitJonossa,20,stdin);
Jos käyttäjä päättää kuitenkin kirjoittaa vaikkapa 30 merkkiä pitkän tekstin, jää taas ylimääräisiä merkkejä "kummittelemaan". Ja jos pyytäisin käyttäjältä seuraavaa merkkijonoa, hyppäisivät muistissa olleet merkit seuraavan merkkijonon lukuun.Esim. näin.
/*Käyttäjä ei pääse edes kirjoittamaan tätä kun muistissa olevat merkit huitaistaan tähän.*/ printf("Anna toinen merkkijono "); gets(toinen);
Eli fgets() funktion kanssakin pitää kuitenkin käyttää jotain väliaikaista taulukkoa johon luetaan mahdolliset ylimäärä merkit.
Mutta olikos sillä rivillä joku maksimimitta?
Jotenkin mielessä pyörii 255 mutta todennäköisesti olen väärässä.
Tai tietysti voisihan ne ylimääräiset ohjata vaikkapa johonkin temp tiedostoon uudella fgets kutsulla, vai??
Ville kirjoitti:
Tai tietysti voisihan ne ylimääräiset ohjata vaikkapa johonkin temp tiedostoon uudella fgets kutsulla, vai??
Voisi, mutta voit myös huuhdella ne ylimääräiset merkit viemäriin.
fflush(stdin);
Tosin edellä mainittu ei ole standardi tapa ja voi joissain ympäristöissä tehdä jotain arvaamatonta. Varmempi huuhtelu olisi:
while ((ch = getchar()) != '\n' && ch != EOF);
Tässä jälkimmäisessä tavassa taas on se huono puoli, että sitä voi käyttää vain kun stdin streamissa on oikeasti jotain ylimääräistä. Sama ongelma on ehdottamallasi ylimääräisellä fgets -kutsulla.
Tällainen tapa tuntui toimivalta:
void lue_merkkijono(char * mjono, int max_pituus) { char * rivinvaihto; // Luetaan merkkejä (1 -- (max_pituus - 1)) kpl. fgets(mjono, max_pituus, stdin); // Haetaan rivinvaihdon paikka rivinvaihto = strchr(mjono, '\n'); if (!rivinvaihto) { // Jos rivinvaihtoa ei löytynyt, niin jotain jäi puskuriin. // Luetaan ne pois. while (getchar() != '\n'); } else { // Poistetaan rivin lopusta '\n' *rivinvaihto = '\0'; } }
Ja vielä esimerkki, jolla voi testata:
int main(void) { char mjono[20]; printf("Anna jotain: "); lue_merkkijono(mjono, 20); printf("Annoit \"%s\"\n", mjono); printf("Anna jotain lisää: "); lue_merkkijono(mjono, 20); printf("Nyt annoit \"%s\".\n", mjono); return 0; }
Tuo strchr() olikin uusi tuttavuus :)
Funktio fgets sisällyttää rivinvaihdon luettuun tekstiin. Käsittelyä varten tekstin lopusta täytyy joka tapauksessa poistaa \n tai \r\n, ja samalla vaivalla selviää, saatiinko koko rivi luettua.
Kummittelevat merkit voi helposti ohittaa scanf-funktiolla. Seuraava koodi lukee rivin loppuun (mutta ei lue rivinvaihtomerkkejä):
scanf("%*[^\r\n]");
Toisaalta koko lukemisen voi hoitaa scanf:llä. Jos tietää etukäteen, mitä haluaa, yksikin lauseke yleensä riittää. Joustavampiin ratkaisuihin tarvitsee luonnollisesti enemmän koodia.
Seuraava funktio osaa sopeutua puskurin kokoon ja valinnaisesti ohittaa tai jättää ohittamatta rivin loppuosan, ja lisäksi se kertoo paluuarvollaan, jäikö rivi kesken.
#include <stdio.h> int lue_rivi(FILE* file, char* teksti, int koko, int ohita) { char formaatti[32]; int pituus = 0, kesken; if (koko <= 0) { return -1; } /* Luodaan formatti: "%123[^\r\n]%n" */ sprintf(formaatti, "%%%d[^\r\n]%%n", koko - 1); if (fscanf(file, formaatti, teksti, &pituus) != 1) { return -1; } teksti[pituus] = 0; /* Tarkistetaan loppuminen. */ ungetc(kesken = fgetc(file), file); kesken = (kesken != '\r' && kesken != '\n'); /* Ohitetaan loput merkit ja rivinvaihto. */ if (ohita) { fscanf(file, "%*[^\r\n]"); } fscanf(file, "%*1[\r]"); fscanf(file, "%*1[\n]"); if (kesken) return 1; return 0; } int main() { char teksti[4] = {0}; int paluu; do { /* Luetaan rivi, ohitetaan loppuosa. Kokeile myös 0:lla! */ paluu = lue_rivi(stdin, teksti, 4, 1); printf("%d, '%s'\n", paluu, teksti); } while (teksti[0] != 'q' && paluu != -1); return 0; }
Juuri kun luulin ymmärtäväni jotain c kielestä ja scanf funktion käytöstä, tulee Metabolix ja sammuttaa valot ja vetää maton jalkojen alta...noin kuvainnollisesti ainakin ;)
Metabolix kirjoitti:
Kummittelevat merkit voi helposti ohittaa scanf-funktiolla. Seuraava koodi lukee rivin loppuun (mutta ei lue rivinvaihtomerkkejä):
scanf("%*[^\r\n]");
Voisitko Metabolix(tai joku muukin) selventää mitä tuossa scanf funktion kutsussa tapahtuu ...
Enkä myöskään kapinoi jos joku selventäisi mitä tuossa sprintf kutsussa tapahtuu.
Sprintf funktio on kyllä jonkin verran tuttu, eli olen sen avulla taulukoihin tekstiä kirjoitellut. Ja sitähän tuossa tehdään, tuo kirjoitettava "teksti" kun ei meinaa nyt aueta :(
Metabolix kirjoitti:
/* Luodaan formatti: "%123[^\r\n]%n" */ sprintf(formaatti, "%%%d[^\r\n]%%n", koko - 1);
Pahoittelut että tähän viestiketjuun tuli näinkin pitkä väli.
Mutta kun pitää sinkoilla sinne sun tänne ja työkin haittaa harrastustoimintaa.
Nämä voi ihan C:n standardista lukea. Formaattien hyvä tunteminen on jopa suotavaa: kun tällainen tehokas työkalu on valmiiksi tarjolla, on tyhmää ruveta turhaan itse keksimään samaa uudestaan.
Lukemisessa %-merkin jälkeen *-merkki tarkoittaa, että kyseistä juttua ei tallenneta mihinkään.
Hakasulkumerkintä toimii %s:n tapaan mutta lukee vain määrättyjä merkkejä (tai ^:n kanssa kaikkia paitsi määrättyjä) eikä pysähdy välilyöntiin eikä lisää tekstin loppuun nollamerkkiä. Viimeksi mainitun vuoksi käytetään sitten %n:ää, joka ei lue mitään vaan tallentaa tähän mennessä luettujen merkkien määrän, jonka avulla voidaan lisätä nollamerkki jälkikäteen.
Tulostuksessa %% tarkoittaa yhtä %-merkkiä, %d tulostaa luvun, hakasulkujen sisältö tulostuu sellaisenaan, %% tulostaa taas %-merkin ja n tulostuu sellaisenaan.
Seuraavassa ovat vielä koodini scanf- ja printf-rivit selitettyinä:
sprintf( formaatti, "%%" /* % */ "%d" /* koko - 1, esimerkiksi 123 */ "[^\r\n]" /* [^\r\n] */ "%%" /* % */ "n" /* n */ koko - 1 ); /* Yhteensä siis %123[^\r\n]%n. */ /* %123[^\r\n] = enintään 123 merkkiä, jotka eivät ole \r tai \n. * %n = yllä luettujen merkkien todellinen määrä. */ fscanf(file, formaatti, teksti, &pituus); /* %*[^\r\n] = ohitetaan rajattomasti merkkejä, jotka eivät ole \r tai \n. */ fscanf(file, "%*[^\r\n]"); /* Ohitetaan yksi \r ja sitten yksi \n, jos sellaiset ovat. * Linuxin rivinvaihto on \n, vanhan Macin \r, Windowsin \r\n, * joten tämä koodi selviää näistä kaikista kunnialla. */ fscanf(file, "%*1[\r]"); fscanf(file, "%*1[\n]");
Korjasin muuten edellisestä viestistäni pari scanf:ää fscanf:ksi, olin ensin tehnyt funktion vain standardivirtaa varten mutta päätinkin sitten muuttaa sen geneeriseen muotoon.
Metabolix kirjoitti:
Nämä voi ihan C:n standardista lukea. Formaattien hyvä tunteminen on jopa suotavaa: kun tällainen tehokas työkalu on valmiiksi tarjolla, on tyhmää ruveta turhaan itse keksimään samaa uudestaan.
Ensiksi.
Vähän hävettää etten ollut kunnolla tutkinut tuolta standardista. Mielessä oli kuva että muistin mitä siellä lukee mutta tekipä meikäläiselle tepposet. Löysin nyt osan kirjoittamistasi asioista sieltä.
Toiseksi.
Kunnon standardi kuvauksen löytämisen ongelma(jos tuo nyt tuli oikein ilmaistua?).
Netin syövereistä olen muutaman sivun löytänyt mutta monasti on tullut ikävä java + sun tasoista/laajuista kuvausta.
Onko heittää kattavan sivuston osoitetta?
Enkä ole oikein saanut kelpo kirjaakaan käsiini :(
Vai onko ylipäätään olemassa jotakin kirjaa johon olisi kerätty kattava standardi kuvaus?
Eikä tietysti haittaa jos kirja sattuisi olemaan saatavilla vielä tänäkin päivänä.
Yleensä niissä on melko pintapuolinen "sutaisu" asiasta.
Metabolix kirjoitti:
Lukemisessa %-merkin jälkeen *-merkki tarkoittaa, että kyseistä juttua ei tallenneta mihinkään.
Nyt en oikein tavoita ajatusta. Merkit luetaan mutta niille ei tehdä mitään?
Metabolix kirjoitti:
Hakasulkumerkintä toimii %s:n tapaan mutta lukee vain määrättyjä merkkejä (tai ^:n kanssa kaikkia paitsi määrättyjä) eikä pysähdy välilyöntiin eikä lisää tekstin loppuun nollamerkkiä. Viimeksi mainitun vuoksi käytetään sitten %n:ää, joka ei lue mitään vaan tallentaa tähän mennessä luettujen merkkien määrän, jonka avulla voidaan lisätä nollamerkki jälkikäteen.
No hakasulkeet tuli kerrattua, kiitos siitä ;)
Mutta mistä löytyy selitys ^ merkin vaikutukselle, sitä en löytämistäni tietolähteistä löytänyt :(
Ja taas kannatti herätä, tuo %n:hän on mainio.
Kyselen varmaan tosi tylsiä perusteisiin liittyviä juttuja mutta kun ei omin avuin selviä niin siinä sitä sitten ollaan.
Kiitoksia paljon kaikille!
Ville kirjoitti:
Onko heittää kattavan sivuston osoitetta?
Googlesta hakusanoilla C standard löytyvät standardityöryhmän sivut, joilta löytyy mm. C99-standardin vuonna 2005 päivitetty versio ja ennakkotietoa kehitteillä olevista standardeista.
Näin yksityiskohtainen ja teknisesti esitetty materiaali ei tietenkään korvaa hyvää opasta perusasioiden opettelussa, mutta kun esimerkiksi printf-funktio on yleisellä tasolla tuttu, standardi on minusta erinomainen lähde yksityiskohtien opetteluun. Kaikkea ei tarvitse osata ulkoa, vaan arvokasta on tietää, mistä tieto löytyy ja mikä kaikki suunnilleen on mahdollista. Toinen standardien heikkous on, että niissä kerrotaan vain, miten asioiden pitäisi olla: todellisuudessa esimerkiksi Microsoftin C-standardikirjasto ei (kai vieläkään?) tue %z-formaattia, ja juuri mikään kääntäjä ei tue C++:n export-sanaa.
Ville kirjoitti:
Mutta mistä löytyy selitys ^ merkin vaikutukselle, sitä en löytämistäni tietolähteistä löytänyt :(
^-merkki kääntää ryhmän vastakkaiseksi: %[abcd] hyväksyy vain mainitut merkit (abcd), %[^abcd] hyväksyy aivan kaikki muut merkit (mm. 012, efg, \r\n). Standardissa ainakin kerrotaan tämä kaikki. :)
Ville kirjoitti:
Kyselen varmaan tosi tylsiä perusteisiin liittyviä juttuja mutta kun ei omin avuin selviä niin siinä sitä sitten ollaan.
Ei hätää: En usko, että läheskään jokainen tämän foorumin C-koodari tuntisi formaattien kiemuroita, ja harva on oikeasti edes kiinnostunut asiasta. Myöskään alkuperäisen muistikysymyksesi vastaus ei ole kaikille ilmeinen. (Ohjelmointiputkaan on suunnitteilla tietoa prosessorin ja muistin toiminnasta ym. ohjelmoijan yleissivistykseen kuuluvista aiheista, mutta koska aika on rajallista, näitä saa varmasti odottaa vielä hyvän aikaa.)
Aihe on jo aika vanha, joten et voi enää vastata siihen.