Skip to content

11. Είσοδος/Έξοδος

Σύνοψη Ορίσματα γραμμής εντολών, η συνάρτηση getopt(), αρχεία κειμένου και δυαδικά αρχεία, ο τύπος δεδομένων FILE, ανάγνωση δεδομένων από αρχεία κειμένου με τις fgetc(), fgets(), fscanf(), αποθήκευση δεδομένων σε αρχεία κειμένου με τις fputc(), fprintf(), ανάγνωση δεδομένων από δυαδικά αρχεία με την fread(), εγγραφή δεδομένων σε δυαδικά αρχεία με την fwrite(), μετακίνηση σε δυαδικά αρχεία με την fseek().

Προαπαιτούμενη γνώση Τύποι δεδομένων, είσοδος/έξοδος, δομές επιλογής και επανάληψης, συναρτήσεις, πίνακες, δομές, δείκτες, αλφαριθμητικά.

11.1 Ορίσματα γραμμής εντολών

Η main() είναι ειδικός τύπος συνάρτησης καθώς αποτελεί το σημείο εισόδου (entry point) για την εκτέλεση του προγράμματος. Ο προκαθορισμένος τύπος επιστροφής της είναι int και υπάρχουν δύο τύποι της, ένας που δεν δέχεται παραμέτρους και ένας που δέχεται. Στα παραδείγματα κώδικα των προηγούμενων κεφαλαίων η main() δεν δεχόταν παραμέτρους και για αυτόν το λόγο συμπεριλαμβανόταν στον κώδικα με τη μορφή:

int main(void) { ... }

Στη συνέχεια θα παρουσιαστούν παραδείγματα όπου η main() θα δέχεται ορίσματα κατά την κλήση της, τα λεγόμενα ορίσματα γραμμής εντολών (command line arguments).

11.1.1 Παραδείγματα με ορίσματα γραμμής εντολών

Ένα παράδειγμα παρουσιάζεται στον κώδικα 11.1 όπου εντός των παρενθέσεων της main() υπάρχουν δύο παράμετροι:

  1. Στην πρώτη θέση μια ακέραια παράμετρος με όνομα argc που περιέχει το πλήθος των ορισμάτων που δίνονται από τη γραμμή εντολών κατά την κλήση του προγράμματος. Στο πλήθος αυτό προσμετράται πάντα και το όνομα του εκτελέσιμου προγράμματος.
  2. Μια παράμετρος με όνομα argv που είναι ένας πίνακας αλφαριθμητικών. Τα στοιχεία του πίνακα είναι τα ορίσματα της γραμμής εντολών συμπεριλαμβανομένου του ονόματος του εκτελέσιμου προγράμματος.

Τα ονόματα των παραμέτρων δεν είναι κατά ανάγκη argc και argv, αλλά έχει καθιερωθεί να χρησιμοποιούνται αυτά. Αν ο κώδικας 11.1 μεταγλωττιστεί και εκτελεστεί όπως στη συνέχεια, τα αποτελέσματα θα είναι:

Κώδικας 11.1: ch11_p1.c - ένα απλό παράδειγμα εμφάνισης του πλήθους των ορισμάτων που δέχεται κατά την εκτέλεσή του ένα πρόγραμμα.
1
2
3
4
5
6
#include <stdio.h>

int main(int argc, char **argv) {
  printf("argc=%d\n", argc);
  return 0;
}
$ gcc ch11_p1.c ‐o ch11_p1
$ ./ch11_p1
argc=1

Το πλήθος των ορισμάτων που εκτυπώνεται είναι 1, διότι όπως αναφέρθηκε προσμετράται και το ίδιο το εκτελέσιμο πρόγραμμα ως πρώτο. Αν στη συνέχεια εκτελέσουμε το ίδιο πρόγραμμα με περισσότερα ορίσματα θα έχουμε την ακόλουθη έξοδο:

$ ./ch11_p1 alfa bravo charlie
argc=4

Σε αυτήν την περίπτωση τα ορίσματα είναι το όνομα του εκτελέσιμου και τα alfa, bravo, charlie και συνεπώς το πλήθος είναι 4. Ένας νέος κώδικας που εκτυπώνει όλα τα ορίσματα είναι ο κώδικας 11.2 όπου κάθε όρισμα κατά την κλήση του εκτελέσιμου καταλαμβάνει και μια θέση στον πίνακα argv.

Κώδικας 11.2: ch11_p2.c - εμφάνιση των ορισμάτων που δέχεται κατά την εκτέλεσή του το πρόγραμμα.
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char **argv) {
  for (int i = 0; i < argc; i++) {
    printf("argv[%d]=%10s\n", i, argv[i]);
  }
  return 0;
}

Η μεταγλώττιση και εκτέλεση του προγράμματος θα δώσει τα ακόλουθα αποτελέσματα:

$ gcc ch11_p2.c ‐o ch11_p2
$ ./ch11_p2 alfa bravo charlie
argv[0]= ./ch11_p2
argv[1]= alpha
argv[2]= bravo
argv[3]= charlie

Στο επόμενο παράδειγμα, στον κώδικα 11.3, τα ορίσματα της γραμμής εντολών χρησιμοποιούνται για την εκτέλεση αριθμητικών πράξεων. Ο χρήστης εκτελεί το πρόγραμμα εισάγοντας την επιθυμητή πράξη π.χ. ADD για πρόσθεση (SUB, MUL, DIV για αφαίρεση, πολλαπλασιασμό και διαίρεση αντίστοιχα) και τους τελεστέους της πράξης. Ο κώδικας ελέγχει αρχικά ότι ο αριθμός των ορισμάτων είναι σωστός και αν δεν έχουν εισαχθεί τα 3 αναμενόμενα ορίσματα (πράξη, τελεστέος 1, τελεστέος 2) τότε εμφανίζεται μήνυμα που πληροφορεί για τον ορθό τρόπο εκτέλεσης του προγράμματος και η εκτέλεση τερματίζει. Αν από την άλλη μεριά έχει εισαχθεί το σωστό πλήθος ορισμάτων, τότε εκτελείται η πράξη και το αποτέλεσμα εμφανίζεται στην οθόνη. Για τη μετατροπή των ορισμάτων από αλφαριθμητικά σε πραγματικούς αριθμούς χρησιμοποιείται η συνάρτηση atof() που ορίζεται στο stdlib.h.

Κώδικας 11.3: ch11_p3.c - παράδειγμα με χρήση ορισμάτων για εκτέλεση αριθμητικών πράξεων.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv) {
  if (argc != 4) {
    printf("Usage: %s <operator> <operand1> <operand2>\n", argv[0]);
    printf("Example: %s ADD 1.4 2.7\n", argv[0]);
    printf("Result: %lf\n", 1.4 + 2.7);
    return EXIT_FAILURE;
  }
  double v1 = atof(argv[2]);
  double v2 = atof(argv[3]);
  double result = .0;
  if (!strcmp(argv[1], "ADD")) {
    result = v1 + v2;
  } else if (!strcmp(argv[1], "SUB")) {
    result = v1 - v2;
  } else if (!strcmp(argv[1], "MUL")) {
    result = v1 * v2;
  } else if (!strcmp(argv[1], "DIV")) {
    result = v1 / v2;
  }
  printf("Result: %lf \n", result);
  return 0;
}

Ένα παράδειγμα μεταγλώττισης και εκτέλεσης του παραπάνω προγράμματος παρουσιάζεται στη συνέχεια:

$ gcc ch11_p3.c ‐o ch11_p3
$ ./ch11_p3 ADD 4.5 8.1
Result: 12.600000

11.1.2 Η συνάρτηση getopt()

Για διάφορες εντολές που μπορούν να δοθούν στη γραμμή εντολών υπάρχουν διακόπτες (switches) που τροποποιούν τη συμπεριφορά τους. Για παράδειγμα η εντολή tail σε περιβάλλον UNIX μπορεί να κληθεί με τον διακόπτη ‐n όπως στη συνέχεια:

$ tail ‐n 3 file.txt

Το αποτέλεσμα της εντολής θα είναι η εκτύπωση των 3 τελευταίων γραμμών του αρχείου file.txt. Αν η κλήση της tail γινόταν χωρίς τον διακόπτη ‐n και την τιμή 3, τότε θα εμφάνιζε τις τελευταίες 10 γραμμές του αρχείου καθώς αυτή είναι η προκαθορισμένη συμπεριφορά που έχει οριστεί για το συγκεκριμένο μικρό πρόγραμμα. Συνεπώς, για την tail ο διακόπτης ‐n καθορίζει το πλήθος των γραμμών που εμφανίζονται. Παρόμοια λειτουργικότητα μπορεί να επιτευχθεί και σε προγράμματα C με χρήση της συνάρτησης getopt(int argc, char **argv, char *options) που ορίζεται στο unistd.h. Οι παράμετροι που δέχεται η συνάρτηση getopt() χρησιμοποιούνται ως εξής:

  1. Η argc είναι το πλήθος των ορισμάτων της γραμμής εντολών.
  2. Η argv είναι ο πίνακας των ορισμάτων της γραμμής εντολών.
  3. Η options χρησιμοποιείται για να ορίσει ποια ορίσματα της γραμμής εντολών είναι διακόπτες και ποια απαιτούν επιπλέον και τιμή.

Ένα παράδειγμα χρήσης της συνάρτησης getopt() παρουσιάζεται στον κώδικα 11.4, με σχόλια που εξηγούν τον ρόλο της μεταβλητής options, της συνάρτησης getopt() και της μεταβλητής optarg που ορίζεται στο unistd.h.

Κώδικας 11.4: ch11_p4.c - κώδικας που χρησιμοποιεί τη συνάρτηση getopt().
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // Για την getopt και την optarg

int main(int argc, char **argv) {
  int aflag = 0, bvalue = 0, option;
  double dvalue = 0.0;
  // Η μεταβλητή options περιέχει τα σύμβολα των παραμέτρων που αναμένεται να
  // δεχθεί το πρόγραμμα, το ':' μετά από ένα σύμβολο υποδηλώνει ότι η
  // παράμετρος αναμένει μια τιμή
  char *options = "ab:d:";
  // Χρησιμοποιούμε την getopt για να αναλύσουμε τις παραμέτρους της γραμμής εντολών
  while ((option = getopt(argc, argv, options)) != -1) {
    switch (option) {
    case 'a': // Σε περίπτωση που η παράμετρος είναι -a
      aflag = 1;
      break;
    case 'b': // Σε περίπτωση που η παράμετρος είναι -b
      bvalue = atoi(optarg); // optarg περιέχει τη συμβολοσειρά της τιμής που
                             // ακολουθεί το -b
      break;
    case 'd': // Σε περίπτωση που η παράμετρος είναι -d
      dvalue = atof(optarg); // optarg περιέχει τη συμβολοσειρά της τιμής που
                             // ακολουθεί το -d
      break;
    case '?': // Σε περίπτωση άγνωστης παραμέτρου
      printf("Error in parsing\n");
      return EXIT_FAILURE;
    }
  }
  printf("Command line arguments: a=%d  b=%d d=%lf\n", aflag, bvalue, dvalue);
  return 0;
}

Η συνάρτηση getopt() πρέπει να καλείται επαναληπτικά μέχρι να επιστρέψει -1. Σε κάθε επανάληψη επιστρέφει το επόμενο όρισμα στη σειρά των ορισμάτων. Τα αποδεκτά ορίσματα περιγράφονται στη μεταβλητή options. Αν μετά το όνομα του ορίσματος ακολουθεί το σύμβολο :, τότε το όρισμα αυτό πρέπει να ακολουθείται από κάποια τιμή. Η λήψη αυτής της τιμής γίνεται μέσω της μεταβλητής optarg. Στην περίπτωση του κώδικα 11.4 επιτρέπονται τα ακόλουθα τρία ορίσματα:

  1. Το όρισμα ‐a, που είναι ένας απλός διακόπτης και συνεπώς δεν ακολουθείται από κάποια τιμή. Αν η εκτέλεση του προγράμματος συμπεριλάβει το όρισμα αυτό, η μεταβλητή aflag θα λάβει την τιμή 1, αλλιώς θα λάβει την τιμή 0.
  2. Το όρισμα ‐b, που δέχεται αμέσως μετά μια ακέραια τιμή. Αυτή η τιμή ανατίθεται στην τοπική μεταβλητή bvalue.
  3. Το όρισμα ‐d, που δέχεται μια δεκαδική τιμή μετά από αυτό. Αυτή η δεκαδική τιμή θα ανατεθεί στην τοπική μεταβλητή dvalue.
Πίνακας 11.1: Τιμές κατάστασης ανοίγματος αρχείων.
Κατάσταση (mode) Σημασία
"r" Ανοίγει το αρχείο μόνο για ανάγνωση. Αν το αρχείο δεν υπάρχει, επιστρέφεται σφάλμα.
"w" Ανοίγει το αρχείο για εγγραφή. Αν το αρχείο υπάρχει, τα περιεχόμενά του διαγράφονται και αν δεν υπάρχει, δημιουργείται ένα νέο αρχείο.
"a" Ανοίγει το αρχείο για προσάρτηση (append). Αν το αρχείο υπάρχει, η εγγραφή γίνεται στο τέλος του αρχείου. Αν δεν υπάρχει, δημιουργείται ένα νέο.
"r+" Ανοίγει το αρχείο για ανάγνωση και εγγραφή. Δεν δημιουργεί νέο αρχείο αν αυτό δεν υπάρχει.
"w+" Παρόμοιο με το "w", αλλά επιτρέπει και την ανάγνωση του αρχείου. Εάν το αρχείο υπάρχει, τα περιεχόμενά του διαγράφονται.
"a+" Παρόμοιο με το "a", αλλά επιτρέπει και την ανάγνωση του αρχείου. Η εγγραφή γίνεται στο τέλος του αρχείου. Δηλαδή, ανεξάρτητα από την τρέχουσα θέση του δείκτη στο αρχείο κατά την ανάγνωση, η εγγραφή προσθέτει πάντα τα δεδομένα στο τέλος του αρχείου.

Ένα παράδειγμα εκτέλεσης παρουσιάζεται στη συνέχεια:

$ gcc ch11_p4.c ‐o ch11_p4
$ ./ch11_p4 ‐a ‐b 42 ‐d 3.14
Command line arguments: a=1 b=42 d=3.140000

11.2 Ο τύπος δεδομένων FILE

Υπάρχουν δύο κατηγορίες αρχείων, τα αρχεία κειμένου και τα δυαδικά αρχεία. Τα αρχεία κειμένου περιέχουν δεδομένα που μπορούν να διαβαστούν από οποιονδήποτε κειμενογράφο απλού κειμένου. Σε αυτήν την κατηγορία ανήκουν για παράδειγμα τα αρχεία κώδικα. Στη δεύτερη κατηγορία είναι αρχεία που απαιτούν ειδική εφαρμογή έτσι ώστε να διαβαστούν τα περιεχόμενά τους. Τέτοια αρχεία είναι για παράδειγμα τα αρχεία .docx από το Microsoft Word, τα αρχεία .xlsx από το Microsoft Excel, τα αρχεία εικόνας .png κ.λπ.
Στη γλώσσα C τα αρχεία ανοίγουν με τη συνάρτηση fopen(filepath, mode). Το πρώτο όρισμα είναι το όνομα του αρχείου που πρέπει να ανοίξει (συμπληρωμένο με τη διαδρομή του στο σύστημα αρχείων, αν το αρχείο δεν βρίσκεται στον ίδιο κατάλογο με το αρχείο του κώδικα). Το δεύτερο όρισμα ορίζει την κατάσταση (mode) στην οποία θα ανοίξει το αρχείο με αποδεκτές τιμές αυτές που παρουσιάζονται στον Πίνακα 11.1. Η συνάρτηση fopen() επιστρέφει NULL αν δεν μπορεί να ανοίξει το συγκεκριμένο αρχείο με το συγκεκριμένο mode. Αν τα καταφέρει επιστρέφει έναν δείκτη προς μια ειδική δομή FILE.
Το παράδειγμα του κώδικα 11.5 επιχειρεί να ανοίξει το αρχείο test.txt για ανάγνωση υποθέτοντας ότι βρίσκεται στον ίδιο τοπικό φάκελο. Αν το αρχείο ανοίξει επιτυχώς, τότε εμφανίζεται ένα μήνυμα που πληροφορεί για την επιτυχία ανοίγματος του αρχείου αλλιώς εμφανίζεται μήνυμα λάθους.

Κώδικας 11.5: ch11_p5.c - άνοιγμα αρχείου κειμένου.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *fn = "test.txt";
  FILE *fp = fopen(fn, "r");
  if (fp == NULL) {
    printf("An error occured on an attempt to open %s\n", fn);
    return EXIT_FAILURE;
  }
  printf("File %s opened successfully (for read)\n", fn);
  fclose(fp);
  return 0;
}

11.3 Είσοδος από αρχεία κειμένου

11.3.1 Ανάγνωση χωρίς διαμόρφωση

Για την ανάγνωση αρχείων απλού κειμένου υπάρχουν δύο τρόποι: ο πρώτος είναι να γίνει ανάγνωση του αρχείου χαρακτήρα προς χαρακτήρα, ενώ ο δεύτερος είναι να γίνει ανάγνωση ανά γραμμή. Ο πρώτος τρόπος παρουσιάζεται στον κώδικα 11.6, όπου γίνεται ανάγνωση όλων των χαρακτήρων που βρίσκονται σε ένα αρχείο και εμφανίζεται ο συνολικός αριθμός τους καθώς και ο συνολικός αριθμός γραμμών. Για την ανάγνωση κάθε χαρακτήρα χρησιμοποιείται η συνάρτηση fgetc() που κάθε κλήση της επιστρέφει τον επόμενο χαρακτήρα από το αρχείο εισόδου και αν δεν υπάρχουν άλλοι χαρακτήρες, επιστρέφει τη σταθερά EOF (End Of File).

Κώδικας 11.6: ch11_p6.c - ανάγνωση χαρακτήρα προς χαρακτήρα των περιεχομένων ενός αρχείου κειμένου.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *fn = "test.txt";
  FILE *fp = fopen(fn, "r");
  char ch = ' ';
  int charcount = 0, linecount = 0;
  if (fp == NULL) {
    printf("An error occured on an attempt to open %s\n", fn);
    return EXIT_FAILURE;
  }
  do {
    ch = fgetc(fp);
    if (ch != EOF) {
      printf("%c", ch);
      charcount++;
      if (ch == '\n') {
        linecount++;
      }
    }
  } while (ch != EOF);
  // Αν η τελευταία γραμμή δεν τελειώνει με αλλαγή γραμμής
  if (ch != '\n') {
    linecount++;
  }
  // Αν το αρχείο είναι κενό
  if (charcount == 0) {
    linecount = 0;
  }
  printf("\nCharacters: %d\n", charcount);
  printf("Lines: %d\n", linecount);
  fclose(fp);
  return 0;
}

Ο τρόπος ανάγνωσης αρχείου του κώδικα 11.6 είναι σχετικά αργός καθώς πρέπει να κάνει τόσες αναγνώσεις από το αρχείο όσοι είναι και οι χαρακτήρες του αρχείου. Εναλλακτικά, μπορεί να γίνει ανάγνωση γραμμή προς γραμμή με τη συνάρτηση fgets() όπως παρουσιάζεται στον κώδικα 11.7.

Κώδικας 11.7: ch11_p7.c - ανάγνωση γραμμή προς γραμμή των περιεχομένων ενός αρχείου κειμένου.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
  char *fn = "test.txt";
  FILE *fp = fopen(fn, "r");
  char line[256];
  int linecount = 0;
  int charcount = 0;
  if (fp == NULL) {
    printf("An error occured on an attempt to open %s\n", fn);
    return EXIT_FAILURE;
  }
  while (fgets(line, 256, fp)) {
    if (line[strlen(line) - 1] == '\n') {
      line[strlen(line) - 1] = '\0'; // αφαίρεση του χαρακτήρα αλλαγής
                                     // γραμμής απο το line
      charcount++; // προσμέτρηση χαρακτήρα αλλαγής γραμμής στο πλήθος
                   // χαρακτήρων
    }
    printf("%s\n", line);
    linecount++;
    charcount += strlen(line);
  }
  printf("Characters: %d\n", charcount);
  printf("Lines: %d\n", linecount);
  fclose(fp);
  return 0;
}

Η συνάρτηση fgets() δέχεται τρία ορίσματα:

  1. Έναν δείκτη προς το αλφαριθμητικό στο οποίο θα γίνει αποθήκευση της εισόδου.
  2. Τον μέγιστο αριθμό χαρακτήρων που θα διαβαστούν από το αρχείο. Αν η ανάγνωση φτάσει στο τέλος της γραμμής και το πλήθος των γραμμάτων είναι μικρότερο από τον μέγιστο αριθμό, τότε η fgets() τερματίζει και επιστρέφει τη γραμμή που διάβασε, διαφορετικά διαβάζει τόσους χαρακτήρες όσος είναι ο μέγιστος αριθμός χαρακτήρων.
  3. Ένα δείκτη προς το αρχείο από το οποίο γίνεται η ανάγνωση.

Επίσης, η συνάρτηση fgets() επιστρέφει έναν δείκτη προς τη γραμμή που διάβασε από το αρχείο. Αν τα δεδομένα του αρχείου τελειώσουν, τότε η συνάρτηση επιστρέφει NULL.
Έστω ένα αρχείο test.txt, στον ίδιο κατάλογο με τον κώδικα, με το ακόλουθο περιεχόμενο:

Excerpt from the Tao of programming (https://www.mit.edu/~xela/tao.html)
If the Tao is great, then the operating system is great.
If the operating system is great, then the compiler is great.
If the compiler is great, then the application is great.
The user is pleased , and there is harmony in the world.

Η εκτέλεση του κώδικα 11.6, όπως και του κώδικα 11.7 για το συγκεκριμένο αρχείο (test.txt) εμφανίζει τα ακόλουθα αποτελέσματα:

Excerpt from the Tao of programming (https://www.mit.edu/~xela/tao.html)
If the Tao is great, then the operating system is great.
If the operating system is great, then the compiler is great.
If the compiler is great, then the application is great.
The user is pleased , and there is harmony in the world.
Characters: 304
Lines: 5

Αξίζει να σημειωθεί ότι η fgets() διαβάζοντας το αρχείο κειμένου αντιγράφει στο αλφαριθμητικό και την αλλαγή γραμμής. Οι γραμμές 16-21 στον κώδικα 11.7 αντικαθιστούν τον τελευταίο χαρακτήρα, αν είναι αλλαγή γραμμής, με τον χαρακτήρα τερματισμού αλφαριθμητικών.

11.3.2 Ανάγνωση με διαμόρφωση

Αν τα δεδομένα που πρέπει να διαβαστούν δεν είναι κείμενο, αλλά είναι για παράδειγμα αριθμοί, τότε η ανάγνωση των τιμών από το αρχείο μπορεί να γίνει με τη χρήση της fscanf(). Στο παράδειγμα του κώδικα 11.8, έστω ότι στο αρχείο grades.txt υπάρχουν σε κάθε γραμμή του τρεις αριθμοί, ένας ακέραιος που είναι ο αριθμός μητρώου φοιτητή και δύο δεκαδικοί που είναι η βαθμολογία του φοιτητή σε διάλεξη και εργαστήριο αντίστοιχα. Το πρόγραμμα εμφανίζει τον αριθμό μητρώου του φοιτητή με τη μεγαλύτερη μέση βαθμολογία. Ο κώδικας θεωρεί ότι το πλήθος των φοιτητών είναι γνωστό και ίσο με 5.

Κώδικας 11.8: ch11_p8.c - ανάγνωση διαμορφωμένου αρχείου κειμένου με 5 εγγραφές.
#include <stdio.h>
#include <stdlib.h>
#define SIZE 5

int main(void) {
  int code, maxcode;
  double lab, lecture, average, maxaverage;
  FILE *fp = fopen("grades.txt", "r");
  if (fp == NULL) {
    printf("File could not be opened\n");
    return EXIT_FAILURE;
  }
  for (int i = 0; i < SIZE; i++) {
    fscanf(fp, "%d %lf %lf", &code, &lecture, &lab);
    average = (lecture + lab) / 2.;
    if (i == 0 || average > maxaverage) {
      maxcode = code;
      maxaverage = average;
    }
  }
  fclose(fp);
  printf("The student with the best performance is: %d\n", maxcode);
  printf("The best average grade is               : %lf\n", maxaverage);
  return 0;
}

Αν θεωρηθεί ότι τα περιεχόμενα του αρχείου εισόδου (grades.txt) είναι τα ακόλουθα:

100 10 5
101 8 7
102 9 6
103 7 5
104 8 8

τότε το αποτέλεσμα εκτέλεσης του κώδικα 11.8 θα είναι:

The student with the best performance is: 104
The best performance is                 : 8.000000

Ωστόσο, συνήθως το πλήθος των εγγραφών σε ένα αρχείο δεν είναι γνωστό και επομένως θα πρέπει να χρησιμοποιηθεί διαφορετικός τρόπος για την ανάγνωση όλων των στοιχείων αρχείου δεδομένων. Για να επιτευχθεί αυτό μπορεί να χρησιμοποιηθεί η τιμή επιστροφής της συνάρτησης fscanf() καθώς επιστρέφει το πλήθος των ορισμάτων που μπόρεσε να διαβάσει με επιτυχία. Για παράδειγμα η ανάθεση:

int n = fscanf(fp,"%d %d",&x,&y);

θα επιστρέψει στη μεταβλητή n την τιμή 2 αν μπόρεσε να διαβάσει με επιτυχία δύο αριθμούς. Αν δεν υπάρχουν όμως άλλοι αριθμοί να διαβάσει, τότε η συνάρτηση θα επιστρέψει την τιμή 0. Το χαρακτηριστικό αυτό χρησιμοποιείται στον κώδικα 11.9, προκειμένου να εντοπιστεί και πάλι η καλύτερη βαθμολογία φοιτητή.

Κώδικας 11.9: ch11_p9.c - ανάγνωση διαμορφωμένου αρχείου κειμένου με άγνωστο πλήθος εγγραφών.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int code, maxcode;
  double lab, lecture, average, maxaverage;
  int count = 0;
  FILE *fp = fopen("grades.txt", "r");
  if (fp == NULL) {
    printf("File could not be opened\n");
    return EXIT_FAILURE;
  }
  while (fscanf(fp, "%d %lf %lf", &code, &lecture, &lab) > 0) {
    average = (lecture + lab) / 2.;
    if (count == 0 || average > maxaverage) {
      maxcode = code;
      maxaverage = average;
    }
    count++;
  }
  fclose(fp);
  printf("Number of students: %d\n", count);
  printf("The student with the best performance is: %d\n", maxcode);
  printf("The best average grade is               : %lf\n", maxaverage);
  return 0;
}

Πολλές φορές τα αρχεία δεδομένων ενδέχεται να χρησιμοποιούν διαχωριστές για τα δεδομένα. Επίσης, εφαρμογές όπως το Microsoft Excel ή το Libreoffice Calc εξάγουν τα δεδομένα σε μορφή CSV (Comma Separated Values), δηλαδή κάθε στήλη χωρίζεται από την επόμενη με κάποιον διαχωριστή όπως είναι το κόμμα. Έτσι το προηγούμενο αρχείο βαθμολογίας θα μπορούσε να έχει την εξής διαμόρφωση:

100,10,5
101,8,7
102,9,6
103,7,5
104,8,8

Για να μπορέσει η εφαρμογή να διαβάσει αρχεία αυτής της μορφής, θα πρέπει να γίνει ανάγνωση καθεμιάς γραμμής του αρχείου ξεχωριστά και στη συνέχεια να διαχωριστεί η γραμμή στα επιμέρους στοιχεία της, δηλαδή κωδικό σπουδαστή και βαθμολογίες. Ο κώδικας 11.10 πραγματοποιεί ακριβώς αυτό, ενώ για τον διαχωρισμό των στοιχείων της κάθε γραμμής χρησιμοποιείται η συνάρτηση strtok().

Κώδικας 11.10: ch11_p10.c - ανάγνωση δεδομένων από αρχείο csv.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void get_elements(char *line, int *code, double *lec, double *lab) {
  int icount = 0;
  char *token;
  token = strtok(line, ",");
  while (token != NULL) {
    icount++;
    if (icount == 1) {
      *code = atoi(token);
    } else if (icount == 2) {
      *lec = atof(token);
    } else {
      *lab = atof(token);
    }
    token = strtok(NULL, ",");
  }
}

int main(void) {
  int code, maxcode, count = 0;
  double lab, lecture, average, maxaverage;
  char line[256];
  FILE *fp = fopen("grades.csv", "r");
  if (fp == NULL) {
    printf("File could not be opened\n");
    return EXIT_FAILURE;
  }
  while (fgets(line, 256, fp)) {
    get_elements(line, &code, &lecture, &lab);
    average = (lecture + lab) / 2.;
    if (count == 0 || average > maxaverage) {
      maxcode = code;
      maxaverage = average;
    }
    count++;
  }
  fclose(fp);
  printf("Number of students: %d\n", count);
  printf("The student with the best performance is: %d\n", maxcode);
  printf("The best average grade is               : %lf\n", maxaverage);
  return 0;
}

11.4 Έξοδος σε αρχεία κειμένου

Για την εγγραφή χαρακτήρων σε αρχεία κειμένου μπορεί να χρησιμοποιηθεί η συνάρτηση fputc(). Η συνάρτηση αυτή δέχεται ως πρώτο όρισμα τον χαρακτήρα που θα γραφεί στο αρχείο και ως δεύτερο όρισμα έναν δείκτη προς το αρχείο που θα γίνει η εγγραφή. Στο παράδειγμα του κώδικα 11.11, παρουσιάζεται η εγγραφή τυχαίων γραμμάτων του αγγλικού αλφαβήτου σε ένα αρχείο κειμένου. Ο χρήστης εισάγει το επιθυμητό όνομα αρχείου και το πλήθος των τυχαίων γραμμάτων και παράγονται αντίστοιχα κεφαλαία γράμματα που γράφονται σε αυτό το αρχείο.

Κώδικας 11.11: ch11_p11.c - εγγραφή αποτελεσμάτων σε αρχείο κειμένου.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  FILE *fp;
  int count = 0;
  char filename[100];
  printf("Input the name of the file: ");
  fgets(filename, 100, stdin);
  fp = fopen(filename, "w");
  if (fp == NULL) {
    printf("Write to file %s failed!\n", filename);
    return EXIT_FAILURE;
  }
  printf("Input a number of characters: ");
  scanf("%d", &count);
  for (int i = 0; i < count; i++) {
    char c = (char)('A' + rand() % 26);
    printf("%c", c);
    fputc(c, fp);
  }
  fclose(fp);
  return 0;
}

Ένα παράδειγμα εκτέλεσης παρουσιάζεται στη συνέχεια:

Input the name of the file: a.txt
Input a number of characters: 10
LRFKQYUQFJ

Για την εγγραφή άλλων τιμών πέρα από χαρακτήρες σε αρχεία μπορεί να χρησιμοποιηθεί η συνάρτηση fprintf() που έχει παρόμοια σύνταξη με τη συνάρτηση printf() αλλά με τη διαφορά πως η έξοδος γίνεται σε αρχείο, αντί για την οθόνη. Στο παράδειγμα του κώδικα 11.12 αποθηκεύονται στο αρχείο fibonacci.txt οι 10 πρώτοι όροι της ακολουθίας Fibonacci (1).

  1. https://r-knott.surrey.ac.uk/Fibonacci/fib.html
Κώδικας 11.12: ch11_p12.c - αποθήκευση των 10 πρώτων όρων της ακολουθίας Fibonacci σε αρχείο κειμένου.
#include <stdio.h>
#include <stdlib.h>

// αναδρομική συνάρτηση υπολογισμού του n-οστού όρου
// της ακολουθίας fibonacci (προσοχή στην απόδοση της συνάρτησης).
int fib(int n) {
  if (n <= 1)
    return 1;
  else
    return fib(n - 1) + fib(n - 2);
}

int main(void) {
  FILE *fp = fopen("fibonacci20.txt", "w");
  if (fp == NULL) {
    printf("Write failed!\n");
    return EXIT_FAILURE;
  }
  for (int i = 0; i < 10; i++) {
    fprintf(fp, "%4d %4d\n", i + 1, fib(i));
  }
  fclose(fp);
  return 0;
}

Μετά την εκτέλεση του προγράμματος θα έχει δημιουργηθεί το αρχείο fibonacci.txt στον ίδιο κατάλογο με το εκτελέσιμο πρόγραμμα και θα έχει τα ακόλουθα περιεχόμενα:

 1   1
 2   1
 3   2
 4   3
 5   5
 6   8
 7  13
 8  21
 9  34
10  55

Τα πεδία δομών μπορούν επίσης να εγγραφούν σε αρχεία κειμένου. Για παράδειγμα ο κώδικας 11.13, αποθηκεύει δύο εγγραφές της δομής student στο αρχείο students.txt.

Κώδικας 11.13: ch11_p13.c - αποθήκευση περιεχομένου δομών σε αρχείο κειμένου.
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  char name[100];
  char lastname[100];
  int id;
  int semester;
} student;

int main(void) {
  FILE *fp = fopen("students.txt", "w");
  if (fp == NULL) {
    printf("Write failed!\n");
    return EXIT_FAILURE;
  }
  student student1 = {"John", "Doe", 100, 5};
  fprintf(fp, "%s %s %d %d\n", student1.name, student1.lastname, student1.id,
          student1.semester);
  student student2 = {"Jane", "Doe", 101, 5};
  fprintf(fp, "%s %s %d %d\n", student2.name, student2.lastname, student2.id,
          student2.semester);
  fclose(fp);
  return 0;
}

Στο αρχείο students.txt η πληροφορία εγγράφεται με την ακόλουθη μορφή:

John Doe 100 5
Jane Doe 101 5

11.5 Δυαδικά αρχεία

Τα αρχεία κειμένου, αν και είναι εύκολα στον χειρισμό τους και άμεσα αναγνώσιμα, αποτελούν τη συντριπτική μειοψηφία των αρχείων που υπάρχουν σε έναν ηλεκτρονικό υπολογιστή. Για παράδειγμα τα αρχεία ήχου, τα αρχεία εικόνας, τα φύλλα εργασίας, οι παρουσιάσεις κ.λπ. δεν είναι αρχεία κειμένου αλλά ειδικού τύπου αρχεία που συχνά αποκαλούνται δυαδικά αρχεία (binary files). Αρχεία τέτοιου είδους απαιτούν ειδικές εφαρμογές για τον χειρισμό τους, αλλά η πρόσβαση στα δεδομένα τους είναι πολύ πιο γρήγορη από ότι σε αρχεία κειμένου. Αυτό συμβαίνει διότι τα δεδομένα γράφονται και διαβάζονται στο αρχείο απευθείας ως bytes και όχι με διαμόρφωση κειμένου. Στη C συνήθως τα δυαδικά αρχεία χρησιμοποιούνται για εγγραφή και ανάγνωση δομών (structs).

11.5.1 Έξοδος σε δυαδικό αρχείο

Η εγγραφή δεδομένων σε δυαδικά αρχεία δεν γίνεται με τη συνάρτηση fprint(), καθώς αυτή γράφει μορφοποιημένο κείμενο και όχι απευθείας bytes. Η εγγραφή δυαδικών δεδομένων γίνεται με τη χρήση της συνάρτησης fwrite(x, size, count, fp) με τα ορίσματά της να έχουν την ακόλουθη σημασία:

  1. x είναι ένας δείκτης προς τα δεδομένα που πρόκειται να εγγραφούν στο αρχείο. Συνήθως πρόκειται για έναν δείκτη προς μια δομή ή για έναν πίνακα δομών.
  2. size είναι το μέγεθος σε bytes καθεμίας εγγραφής που θα αποθηκευτεί στο αρχείο.
  3. count είναι το πλήθος των εγγραφών που θα αποθηκευτούν στο αρχείο.
  4. fp είναι δείκτης προς το αρχείο.

Στο παράδειγμα του κώδικα 11.14 παρουσιάζεται η αποθήκευση δυο εγγραφών της δομής person στο αρχείο persons.txt.

Κώδικας 11.14: ch11_p14.c - αποθήκευση εγγραφών της δομής student σε δυαδικό αρχείο.
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  char name[100];
  char lastname[100];
  int age;
} person;

int main(void) {
  person p = {"John", "Doe", 46};
  person x = {"Richard", "Roe", 33};
  FILE *fp = fopen("persons.bin", "w");
  if (fp == NULL) {
    printf("Write failed!\n");
    return EXIT_FAILURE;
  }
  fwrite(&p, sizeof(person), 1, fp);
  fwrite(&x, sizeof(person), 1, fp);
  fclose(fp);
  return 0;
}

Η εμφάνιση των περιεχομένων του αρχείου persons.bin δεν είναι εφικτή με έναν απλό επεξεργαστή κειμένου (αν και οι χαρακτήρες στα δεδομένα μπορεί να είναι ορατοί). Ωστόσο, αν χρησιμοποιηθεί μια εντολή όπως η xxd (1) ενός UNIX συστήματος θα ληφθούν αποτελέσματα παρόμοια με τα ακόλουθα:

  1. https://linuxhandbook.com/xxd-command/
$ xxd persons.bin
00000000: 4a6f 686e 0000 0000 0000 0000 0000 0000 John............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 446f 6500 0000 0000 0000 0000 ....Doe.........
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000c0: 0000 0000 0000 0000 2e00 0000 5269 6368 ............Rich
000000d0: 6172 6400 0000 0000 0000 0000 0000 0000 ard.............
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000100: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000110: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000120: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000130: 526f 6500 0000 0000 0000 0000 0000 0000 Roe.............
00000140: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000150: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000160: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000170: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000180: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000190: 0000 0000 2100 0000                     ....!...

Ο κώδικας 11.14 γράφει στο αρχείο persons.bin με δύο εντολές fwrite, αλλά αυτό θα μπορούσε να συμβεί και με μόνο μία εντολή για μεγαλύτερη ταχύτητα, όπως φαίνεται στον κώδικα 11.15.

Κώδικας 11.15: ch11_p15.c - αποθήκευση 2 εγγραφών της δομής person σε δυαδικό αρχείο με την fwrite().
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  char name[100];
  char lastname[100];
  int age;
} person;

int main(void) {
  FILE *fp = fopen("persons.bin", "w");
  if (fp == NULL) {
    printf("Write failed!\n");
    return EXIT_FAILURE;
  }
  person p[2] = {{"John", "Doe", 46}, {"Richard", "Roe", 33}};
  fwrite(p, sizeof(person), 2, fp);
  fclose(fp);
  return 0;
}

11.5.2 Είσοδος από δυαδικό αρχείο

Η συνάρτηση fread() μπορεί να χρησιμοποιηθεί για ανάγνωση δεδομένων από δυαδικά αρχεία και η σύνταξή της είναι παρόμοια με τη σύνταξη της fwrite(). Στο παράδειγμα του κώδικα 11.16 γίνεται ανάγνωση των δύο εγγραφών της δομής person που αποθηκεύτηκαν στο αρχείο persons.bin με τον κώδικα 11.15.

Κώδικας 11.16: ch11_p16.c - ανάγνωση εγγραφών της δομής person από δυαδικό αρχείο με την fread().
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  char name[100];
  char lastname[100];
  int age;
} person; 

void print_person(person p) {
  printf("Firstname: %s, Lastname: %s, Age: %4d\n", p.name, p.lastname, p.age);
}

int main(void) {
  FILE *fp = fopen("persons.bin", "r"); 
  if (fp == NULL) {
    printf("Read failed!\n");
    return EXIT_FAILURE;
  }
  person data[2];
  fread(data, sizeof(person), 2, fp); 
  for (int i = 0; i < 2; i++) {
    print_person(data[i]);
  }
  fclose(fp);
  return 0;
}

11.5.3 Μετακίνηση με την fseek()

Μια ευκολία που παρέχουν τα δυαδικά αρχεία είναι η δυνατότητα μετακίνησης σε οποιοδήποτε σημείο τους.
Για να γίνει αυτό χρησιμοποιείται η συνάρτηση:

int fseek(fp, offset, whence);
όπου οι παράμετροί της έχουν την ακόλουθη σημασία:

  1. fp είναι ο δείκτης προς το αρχείο.
  2. offset είναι ο αριθμός των bytes σε σχέση με το whence.
  3. whence μπορεί να λάβει τρεις συμβολικές τιμές:

    1. SEEK_SET, συμβολίζει την αρχή του αρχείου.
    2. SEEK_CUR, συμβολίζει την τρέχουσα θέση στο αρχείο.
    3. SEEK_END, συμβολίζει το τέλος του αρχείου

Μια συνηθισμένη χρήση της fseek() είναι για τον υπολογισμό του συνολικού μεγέθους ενός αρχείου σε bytes. Αυτό μπορεί να γίνει σε συνδυασμό με τη συνάρτηση ftell(), που επιστρέφει την τρέχουσα θέση σε bytes σε ένα αρχείο. Ένα παράδειγμα υπολογισμού του μεγέθους ενός αρχείου παρουσιάζεται στον κώδικα 11.17 όπου γίνεται μετακίνηση στο τέλος του αρχείου και στη συνέχεια υπολογίζεται το συνολικό μέγεθος σε bytes με χρήση της ftell().

Κώδικας 11.17: ch11_p17.c - υπολογισμός του μεγέθους ενός αρχείου σε bytes με τις fseek() και ftell().
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  char *fn = "persons.bin";
  FILE *fp;
  int len;
  fp = fopen(fn, "r");
  if (fp == NULL) {
    printf("Read failed!\n");
    return EXIT_FAILURE;
  }
  fseek(fp, 0, SEEK_END);
  len = ftell(fp);
  fclose(fp);
  printf("Size of %s = %d bytes\n", fn, len);
  return 0;
}

Θεωρώντας ότι το αρχείο persons.bin που δημιουργήθηκε με τον κώδικα 11.15 υπάρχει, η εκτέλεση του παραπάνω προγράμματος θα εμφανίσει:

Size of persons.bin = 408 bytes

Ένα ακόμα παράδειγμα χρήσης της fseek() παρουσιάζεται στον κώδικα 11.18. Ο κώδικας ανοίγει το δυαδικό αρχείο εγγραφών persons.bin, μετακινείται στην τελευταία εγγραφή, διαβάζει τα περιεχόμενά της και τα εμφανίζει.

Κώδικας 11.18: ch11_p18.c - μετακίνηση στην τελευταία εγγραφή ενός δυαδικού αρχείου και εμφάνισή της.
#include <stdio.h>
#include <stdlib.h>

typedef struct {
  char name[100];
  char lastname[100];
  int age;
} person;

int main(void) {
  FILE *fp = fopen("persons.bin", "r");
  if (fp == NULL) {
    printf("Read failed!\n");
    return EXIT_FAILURE;
  }
  person a_person;
  fseek(fp, -sizeof(person), SEEK_END);
  fread(&a_person, sizeof(person), 1, fp);
  printf("Person details: %s %s %d \n", a_person.name, a_person.lastname,
         a_person.age);
  fclose(fp);
  return 0;
}

Η εκτέλεση του προγράμματος, εφόσον υπάρχει το αρχείο persons.bin θα εμφανίσει:

Person details: Richard Roe 33

11.6 Ασκήσεις

Άσκηση 1
Να γραφεί πρόγραμμα που ο χρήστης να εισάγει το όνομα ενός αρχείου κειμένου και το πρόγραμμα να εμφανίζει το ποσοστό των χαρακτήρων του αρχείου που είναι κεφαλαία γράμματα του αγγλικού αλφαβήτου.

Λύση άσκησης 1
#include <ctype.h>
#include <stdio.h>

int main() {
  char filename[100];
  printf("Enter the name of the text file: ");
  scanf("%99s", filename);

  FILE *file = fopen(filename, "r");
  if (file == NULL) {
    perror("Error opening file");
    return 1;
  }

  int total_chars = 0;
  int uppercase_chars = 0;
  char ch;

  while ((ch = fgetc(file)) != EOF) {
    if (isalpha(ch)) {
      total_chars++;
      if (isupper(ch)) {
        uppercase_chars++;
      }
    }
  }

  if (total_chars == 0) {
    printf("No alphabetic characters found in the file.\n");
  } else {
    double percentage = (double)uppercase_chars / total_chars * 100;
    printf("Percentage of uppercase characters: %.2f%%\n", percentage);
  }

  fclose(file);
  return 0;
}

Άσκηση 2
Nα γραφεί πρόγραμμα που ο χρήστης να εισάγει το όνομα ενός αρχείου κειμένου και το πρόγραμμα να εμφανίζει τις γραμμές του αρχείου που ξεκινούν με κεφαλαίο γράμμα του αγγλικού αλφαβήτου.

Λύση άσκησης 2
#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>

int main() {
  char filename[100];
  printf("Enter the name of the text file: ");
  scanf("%99s", filename);

  FILE *file = fopen(filename, "r");
  if (file == NULL) {
    perror("Error opening file");
    return 1;
  }

  char
      line[1024]; // Θεωρούμε ότι κάθε γραμμή έχει μέγιστο μήκος 1023 χαρακτήρων
  while (fgets(line, sizeof(line), file)) {
    if (isupper(line[0])) {
      printf("%s", line); // Εκτυπώνει τη γραμμή αν ξεκινάει με κεφαλαίο γράμμα
    }
  }

  fclose(file);
  return 0;
}

Άσκηση 3
Nα γραφεί πρόγραμμα που ο χρήστης να εισάγει ακέραιους αριθμούς επαναληπτικά μέχρι να δώσει την τιμή 0. Για κάθε αριθμό που εισάγει ο χρήστης, αν είναι πρώτος, να εισάγεται στο αρχείο primes.txt και να εμφανίζεται σχετικό μήνυμα. Το πρόγραμμα πριν τον τερματισμό του να εμφανίζει τα περιεχόμενα του primes.txt.

Λύση άσκησης 3
#include <stdbool.h>
#include <stdio.h>

bool is_prime(int num) {
  if (num <= 1)
    return false;
  if (num % 2 == 0 && num > 2)
    return false;
  for (int i = 3; i * i <= num; i += 2) {
    if (num % i == 0)
      return false;
  }
  return true;
}

void display_file_contents(const char *filename) {
  FILE *file = fopen(filename, "r");
  int num;

  if (file == NULL) {
    perror("Error opening file");
    return;
  }

  printf("Contents of %s:\n", filename);
  while (fscanf(file, "%d", &num) != EOF) {
    printf("%d\n", num);
  }

  fclose(file);
}

int main(void) {
  int num;
  FILE *file = fopen("primes.txt", "w");
  if (file == NULL) {
    perror("Error opening file");
    return 1;
  }

  printf("Enter numbers (0 to stop): ");
  while (1) {
    scanf("%d", &num);
    if (num == 0)
      break;

    if (is_prime(num)) {
      fprintf(file, "%d\n", num);
      printf("%d is a prime number and has been added to the file.\n", num);
    }
  }

  fclose(file);

  // Display the contents of primes.txt before exiting
  display_file_contents("primes.txt");

  return 0;
}

Άσκηση 4
Γράψτε ένα πρόγραμμα που θα εγγράφει μια ακολουθία ακεραίων τιμών σε ένα αρχείο κειμένου με τις τιμές μεταξύ τους διαχωρισμένες με κόμματα. Οι τιμές να επιλέγονται τυχαία στο διάστημα [0,100) και το πλήθος τους να καθορίζεται από τον χρήστη κατά την εκτέλεση του προγράμματος. Στη συνέχεια να διαβάζει τις τιμές από το αρχείο και να υπολογίζει και να εμφανίζει τον μέσο όρο τους. Να υπολογίζει και να εμφανίζει τους χρόνους που χρειάστηκε για να γίνει τόσο η εγγραφή των ακεραίων τιμών στο αρχείο, όσο και η ανάγνωση των τιμών. Επαναλάβετε τη διαδικασία, αλλά αυτήν τη φορά για εγγραφή και ανάγνωση των ακεραίων τιμών σε δυαδικό αρχείο. Συγκρίνετε τους χρόνους.

Παρατήρηση: Η μέτρηση χρόνου εκτέλεσης από τη γραμμή εντολών μπορεί να γίνει με το βοηθητικό πρόγραμμα time σε Linux και MacOS. Στα Windows μπορεί να εγκατασταθεί το ptime https://www.pc-tools.net/win32/ptime/ για να προσομοιωθεί παρόμοια λειτουργικότητα με το time του Linux.

Λύση άσκησης 4
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NUMBERS_COUNT 1000000
#define FILENAME_TEXT "numbers.txt"
#define FILENAME_BINARY "numbers.bin"

void write_numbers_to_text_file(int *numbers, const char *filename) {
  clock_t start = clock();
  FILE *file = fopen(filename, "w");
  if (file == NULL) {
    perror("Error opening file for writing");
    exit(EXIT_FAILURE);
  }

  for (int i = 0; i < NUMBERS_COUNT; i++) {
    fprintf(file, "%d%s", numbers[i], (i < NUMBERS_COUNT - 1) ? "," : "\n");
  }

  fclose(file);
  clock_t end = clock();
  printf("Time taken to write to text file: %.6f seconds\n",
         (double)(end - start) / CLOCKS_PER_SEC);
}

void write_numbers_to_binary_file(int *numbers, const char *filename) {
  clock_t start = clock();
  FILE *file = fopen(filename, "wb");
  if (file == NULL) {
    perror("Error opening file for writing");
    exit(EXIT_FAILURE);
  }

  fwrite(numbers, sizeof(int), NUMBERS_COUNT, file);

  fclose(file);
  clock_t end = clock();
  printf("Time taken to write to binary file: %.6f seconds\n",
         (double)(end - start) / CLOCKS_PER_SEC);
}

void read_numbers_from_text_file(const char *filename) {
  clock_t start = clock();
  FILE *file = fopen(filename, "r");
  if (file == NULL) {
    perror("Error opening file for reading");
    exit(EXIT_FAILURE);
  }

  int number;
  char comma;
  long sum = 0;
  while (fscanf(file, "%d%c", &number, &comma) == 2) {
    sum += number;
  }
  printf("Average: %.2f\n", (double)sum / NUMBERS_COUNT);

  fclose(file);
  clock_t end = clock();
  printf("Time taken to read from text file: %.6f seconds\n",
         (double)(end - start) / CLOCKS_PER_SEC);
}

void read_numbers_from_binary_file(const char *filename) {
  clock_t start = clock();
  FILE *file = fopen(filename, "rb");
  if (file == NULL) {
    perror("Error opening file for reading");
    exit(EXIT_FAILURE);
  }

  int numbers[NUMBERS_COUNT];
  fread(numbers, sizeof(int), NUMBERS_COUNT, file);
  long sum = 0;
  for (int i = 0; i < NUMBERS_COUNT; i++) {
    sum += numbers[i];
  }
  printf("Average: %.2f\n", (double)sum / NUMBERS_COUNT);

  fclose(file);
  clock_t end = clock();
  printf("Time taken to read from binary file: %.6f seconds\n",
         (double)(end - start) / CLOCKS_PER_SEC);
}

int main(void) {
  int numbers[NUMBERS_COUNT];
  for (int i = 0; i < NUMBERS_COUNT; i++) {
    numbers[i] = rand() % 100;
  }

  write_numbers_to_text_file(numbers, FILENAME_TEXT);
  read_numbers_from_text_file(FILENAME_TEXT);

  write_numbers_to_binary_file(numbers, FILENAME_BINARY);
  read_numbers_from_binary_file(FILENAME_BINARY);

  return 0;
}