Kirjautuminen

Haku

Tehtävät

Keskustelu: Ohjelmointikysymykset: C: Tietueet ja rekisterin teko

Sivun loppuun

Pekkisp1978 [05.12.2019 22:33:50]

#

Meillä on (tällä kertaa) tehtävänä tehdä oppilasrekisteri, johon voi lisätä oppilaita, selata rekisteriä ja etsiä tietty oppilas (vaikka opiskelijanumeron perusteella). Olen saanut valmiiksi eri mallien ja ohjeiden pohjalta jonkinlaisen rakennelman, mutta haluaisin siitä sellaisen, että voisin lisätä siihen oppilaita yhden kerrallaan ja selata rekisteriä sen mukaan kuinka paljon siihen on syötetty oppilaita. Nyt tämä ohjelma käskee täyttää kerralla koko rekisterin (kuinka paljon paikkoja tietueeseen on määritelty) ja kun sitä selaa tyhjänä niin tulostuu kaikenlaisia merkkejä ruudulle. Ei ole varmasti paljosta kiinni mutta en nyt hoksaa kuinka tuo muutetaan. Tuo for-silmukkahan siihen tietenkin vaikuttaa.

#include <stdio.h>
#include <string.h>

#define ETUNIMEN_PITUUS 10
#define SUKUNIMEN_PITUUS 15
#define OPISKELIJOIDEN_LKM 3

typedef struct {
  char etunimi[ETUNIMEN_PITUUS];
  char sukunimi[SUKUNIMEN_PITUUS];
  int opiskelijanumero;
  int aloitusvuosi;

}OpiskelijaTietue;
void tulostaOpiskelija(OpiskelijaTietue op);
void tulostaValikko(void);
void etsiOpiskelija(OpiskelijaTietue op[], int lkm);
int lueKokonaisluku( void);
void lueRoskat(void);
OpiskelijaTietue lueOpiskelijanTiedot(void);


int main(void){
int i;
int komento;
int forever = 1;
OpiskelijaTietue rekisteri[OPISKELIJOIDEN_LKM];

do{
tulostaValikko();
komento = lueKokonaisluku();

switch( komento ){

      case 1:

         for(i=0; i < OPISKELIJOIDEN_LKM; i++) {
         rekisteri[i] = lueOpiskelijanTiedot();
         }
         break;

      case 2:
         for(i=0; i < OPISKELIJOIDEN_LKM; i++){
         tulostaOpiskelija(rekisteri[i]);
         }

         break;

      case 3:
         etsiOpiskelija(rekisteri,OPISKELIJOIDEN_LKM );
         break;
      case 0:
         return(0);

      default:
         printf("Virheellinen valinta \n\n");
         break;

     }
}while(forever);

return(0);
}

OpiskelijaTietue lueOpiskelijanTiedot(void){

  OpiskelijaTietue op;
  printf("\nAnna opiskelijan etunimi >");
  scanf("%s", op.etunimi);

  printf("\nAnna opiskelijan sukunimi >");
  scanf("%s", op.sukunimi);

  printf("\nAnna opiskelijanumero >");
  scanf("%d", &op.opiskelijanumero);

  printf("\nAnna aloitusvuosi >");
  scanf("%d", &op.aloitusvuosi);

  return(op);

}

void tulostaOpiskelija(OpiskelijaTietue op){

  printf("Etunimi: %s\n", op.etunimi);
  printf("Sukunimi: %s\n", op.sukunimi);
  printf("Opiskelijanumero: %d\n", op.opiskelijanumero);
  printf("Aloitusvuosi: %d\n\n\n", op.aloitusvuosi);

}

void tulostaValikko(void){
    printf("Valitse haluamasi toiminto\n");
    printf("(1) Lisaa opiskelija\n");
    printf("(2) Selaa rekisteria\n");
    printf("(3) Etsi opiskelija\n");
    printf("(0)  Lopeta\n");
    printf("Valinta > ");

}

void etsiOpiskelija(OpiskelijaTietue op[],int lkm){
    int opiskelijanumero;
    int i;
    printf("Anna opiskelijanumero > ");
    scanf("%d", &opiskelijanumero);
    lkm = 3;
    for(i=0;i<lkm;i++){
        if(op[i].opiskelijanumero == opiskelijanumero)
        {

            printf("Opiskelija on %d %s %s\n\n", op[i].opiskelijanumero, op[i].sukunimi, op[i].etunimi);
            break;

    }    else
        printf("Opiskelijaa ei loydy\n\n");
        break;
    }


}

int lueKokonaisluku(void){

   int luku;
   char mki;
   int status;

   while((status = scanf("%d%c", &luku, &mki))==0  || (2 == status && mki != '\n') ){
      lueRoskat();
      printf("Anna kelvollinen valinta > ");
   }

   return luku;
}
void lueRoskat(void){

   while( fgetc(stdin) != '\n');

}

Metabolix [05.12.2019 23:26:42]

#

Haluamasi ominaisuus toteutuu selvimmin sillä, että OPISKELIJOIDEN_LKM vaihtuu kahteen erilliseen tietoon: taulukon koko on vakio REKISTERIN_KOKO_MAX, ja taulukkoon oikeasti syötetty tietomäärä on muuttuja rekisterin_koko, joka on aluksi nolla. Kun rekisterin_koko on pienempi kuin REKISTERIN_KOKO_MAX, taulukkoon voidaan lisätä uusi tieto kohtaan rekisterin_koko ja voidaan sitten korottaa kokolaskuria yhdellä. Vastaavasti rekisteristä voi poistaa tietoja pienentämällä arvoa rekisterin_koko, jos se on suurempi kuin nolla. Tulostuskin toimii oikein, kun tiedetään, montako kohtaa rekisteriin on jo syötetty.

Funktiossa etsiOpiskelija virheellisesti asetat määräksi lkm = 3, vaikka lukumäärä tulee jo parametrina. Lisäksi kyseisessä funktiossa taitaa olla break väärässä paikassa. Sisennä koodi siististi ja tarkasta aaltosulkujen paikat, niin vika löytyy.

Funktio lueRoskat on varsin turha, ja lueKokonaisluku voisi näyttää vaikka tältä:

int lueKokonaisluku(void) {
  int luku;
  while (scanf("%d", &luku) != 1) {
    if (scanf("%*[^\n]") != 0) {
      // TODO: Syöte loppui, ohjelman kannattaa sulkeutua.
    }
    printf("Anna kelvollinen valinta > ");
  }
  return luku;
}

Hassusti kuitenkaan et käytä tätä lueKokonaisluku-funktiota kuin yhdessä kohdassa, vaikka koodissa on monta muutakin kohtaa, joissa luetaan kokonaislukuja (ja joissa ei nyt tarkasteta virhetilannetta).

Muuttuja forever on harhaanjohtava, ja do-while-rakenne on vaikeampi hahmottaa. Jos haluat ikuisen silmukan, selvintä on kirjoittaa silmukan alkuun while(1).

Hyvä tapa on esitellä muuttujat siellä, missä niitä käytetään. Esimerkiksi saman i-muuttujan käyttö useassa kohdassa johtaa helposti virheisiin, ja nykyaikaisemmin muuttuja esiteltäisiin joka silmukassa vain paikallisesti:

for (int i = 0; i < 100; ++i) {}

Pekkisp1978 [08.12.2019 09:52:25]

#

Metabolix kirjoitti:

"Haluamasi ominaisuus toteutuu selvimmin sillä, että OPISKELIJOIDEN_LKM vaihtuu kahteen erilliseen tietoon: taulukon koko on vakio REKISTERIN_KOKO_MAX, ja taulukkoon oikeasti syötetty tietomäärä on muuttuja rekisterin_koko, joka on aluksi nolla. Kun rekisterin_koko on pienempi kuin REKISTERIN_KOKO_MAX, taulukkoon voidaan lisätä uusi tieto kohtaan rekisterin_koko ja voidaan sitten korottaa kokolaskuria yhdellä. Vastaavasti rekisteristä voi poistaa tietoja pienentämällä arvoa rekisterin_koko, jos se on suurempi kuin nolla. Tulostuskin toimii oikein, kun tiedetään, montako kohtaa rekisteriin on jo syötetty."

#define ETUNIMEN_PITUUS 10
#define SUKUNIMEN_PITUUS 15
#define REKISTERIN_KOKO_MAX 100

Eli nuin esimerkiksi tuonne?
Ja mainiin tuo muuttuja int rekisterin_koko?

En saa tuota lueopiskelija-funktiota, eli case 1:sta pelaamaan. Tajuan kyllä tuon idean, mutta en saa sitä siirrettyä tuohon ohjelmaan. Esim tuo rekisterin_koko ja REKISTERIN_KOKO_MAX, kuinka ne tuohon asetetaan?

Metabolix [08.12.2019 11:29:55]

#

Ihan täsmälleen kuten kirjoitin:

Tietomäärä on muuttuja rekisterin_koko, joka on aluksi nolla.

int rekisterin_koko = 0;

Kun rekisterin_koko on pienempi kuin REKISTERIN_KOKO_MAX,

if (rekisterin_koko < REKISTERIN_KOKO_MAX) {

... taulukkoon voidaan lisätä uusi tieto kohtaan rekisterin_koko

rekisteri[rekisterin_koko] = lueOpiskelija();

.. ja voidaan sitten korottaa kokolaskuria yhdellä.

rekisterin_koko += 1;

Kaikissa ohjelman kohdissa, joissa käydään läpi rekisteriä (esim. etsiminen, tulostaminen), tulee käyttää muuttujaa rekisterin_koko. Vakio REKISTERIN_KOKO_MAX vaikuttaa vain rekisterin määrittelyyn ohjelman alussa ja tietojen lisäämiseen.

Pekkisp1978 [09.12.2019 10:44:21]

#

Jep, nyt sain pelaamaan tuon opiskelijoiden syötön.
Mutta tuo kun haluan tulostaa rekisterin, tein tällaisen:

if(rekisterin_koko > 0){
  for(i=0; i < rekisterin_koko; i++){
  tulostaOpiskelija(rekisteri[i]);
  }

  }
  else
  printf("Lisaa opiskelijoita!\n");

Nyt se tulostaa ensimmäisen lisätyn opiskelijan tiedot oikein, mutta kun lisää opiskelijoita, alkaa tulla siansaksaa...

Metabolix [09.12.2019 15:05:16]

#

Siansaksa on merkki siitä, että tulostetaan taulukosta kohtia, joihin ei ole laitettu dataa oikein. Eli tarkasta nyt vielä kerran, että tietojen lukemisen yhteydessä laitat ne taulukossa oikeaan kohtaan (rekisterin_koko, ei i), kuten yllä neuvon. Tämäkin virhe olisi ehkä jäänyt tekemättä, jos i-muuttuja määriteltäisiin vain käyttöpaikalla, kuten aiemmin suosittelin.

Siansaksalta sinänsä voi välttyä, kun alustaa muuttujat, mutta tämä ei tietysti korjaa ohjelman toimintaa muilta osin. Tässä tapauksessa rekisterin alustaminen tyhjäksi onnistuisi vaikka näin:

OpiskelijaTietue rekisteri[REKISTERIN_KOKO_MAX] = {{""}};

Pekkisp1978 [14.12.2019 22:12:59]

#

Jep, yhdessä kohtaa oli just tuo rekisterin_koko väärässä paikassa, nyt ohjelma pelittää niin kuin pitää, kiitoksia. Seuraavaksi se pitäs sitten muuntaa sellaiseksi että syöttö, luku ja etsintä tapahtuu tiedostosta...

The Alchemist [15.12.2019 12:08:09]

#

Ei sitä ole mitään järkeä laittaa parsimaan tiedostoa reaaliajassa. Käytä tiedostoa ainoastaan tietojen säilömiseen ns. session yli ja ohjelman käynnistyessä lue se muistiin.

Pekkisp1978 [15.12.2019 12:11:49]

#

Eli? Tehtävänantona on toteuttaa opiskelijarekisteri tiedostoa käyttäen, pohjana edellisen harjoituksen ohjelma (eli tässä ketjussa käsitelty ohjelma).

Metabolix [15.12.2019 12:45:20]

#

Eli ohjelman alussa ennen while-silmukkaa luet rekisterin tiedostosta (rekisterin_koko = lueRekisteri(rekisteri)), ja ohjelman lopussa ennen return-riviä tallennat nämä taas tiedostoon (tallennaRekisteri(rekisteri, rekisterin_koko)). Muuten ohjelma toimii samoin kuin nytkin. Tietysti tallennuksen voi laittaa myös aina tietojen muuttamisen jälkeen, jos haluaa varmistaa tallennuksen siinäkin tilanteessa, että ohjelma kaatuu tai suljetaan rastista tms.

Pekkisp1978 [15.12.2019 19:01:05]

#

Joo, kuulostaa helpolta. Mutta nuissa oppaissa ja ohjeissa näyttää olevan jotenkin sekavia nuo tiedostojen käytön esimerkit, niin en tiedä miten tuo lukeminen ja tallennus tuohon laitetaan. Pointin kyllä ymmärrän, mutta miten se tuohon koodiin asetetaan...

Eli tässä on nyt tuo pääohjelma. Tuo tiedoston käsittely alkaa varmaan nuin kun tuohon olen aloittanut? Ja nuo lueRekisteri ja tallennaRekisteri?

int main(void){
int rekisterin_koko = 0;
int lueRekisteri;
int tallennaRekisteri;
int komento;
int i;
int forever = 1;
FILE *tiedosto;
OpiskelijaTietue rekisteri[REKISTERIN_KOKO_MAX];

tiedosto = fopen("oppilas.txt", "r");
if ( (tiedosto = fopen ("oppilas.txt", "r" ) ) == NULL )
    printf("Tiedoston avaaminen epäonnistui\n");
else


do{
tulostaValikko();
komento = lueKokonaisluku();

switch ( komento ) {

      case 1:

         if(rekisterin_koko < REKISTERIN_KOKO_MAX) {
         rekisteri[rekisterin_koko] = lueOpiskelijanTiedot();
         rekisterin_koko +=1;
         }
         break;

      case 2:

         if(rekisterin_koko > 0){
                 for(i=0; i < rekisterin_koko; i++){
                 tulostaOpiskelija(rekisteri[i]);
                 }

         }
         else
            printf("Lisaa opiskelijoita!\n\n");

         break;
      case 3:
         etsiOpiskelija(rekisteri, rekisterin_koko);
         break;
      case 0:
         return(0);

      default:
         printf("Virheellinen valinta \n\n");
         break;

     }
}while(forever);

return(0);
}

Metabolix [15.12.2019 19:13:28]

#

Pointtini oli, että tekisit sen lukemista ja tallentamista varten uudet funktiot lueRekisteri ja tallennaRekisteri, niin koodia olisi helpompi hahmottaa eikä pääohjelman pituus kasvaisi liikaa.

Koodin lukemista helpottaisi myös, jos sisentäisit sen siististi.

Jos tiedostot on esitetty lukemassasi oppaassa jotenkin epäselvästi, etsi parempi opas. Lyhyesti on kerrottu mm. tässä vanhassa oppaassa, ja funktioiden nimillä (fopen, fclose, fscanf, fprintf) etsimällä löytyy netistä vaikka kuinka paljon materiaalia.

Pekkisp1978 [15.12.2019 20:45:26]

#

Niin tarkoitit nuilla funktioita... joo pitää ruveta kattelemaan parempia oppaita.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta