Εισαγωγή στη C (#4)

Επικοινωνήστε με: Παναγιώτη Παύλου

e-mail: allos@mail.ntua.gr

Δομές δεδομένων

Οι δομές (structs) είναι ένα εργαλείο που μας παρέχει η C ώστε να ομαδοποιούμε μεταβλητές που σχετίζονται μεταξύ τους νοηματικά. Αυτό αυτόματα μας λύνει και το πρόβλημα της ονοματοδοσίας. Δείτε παρακάτω δύο κώδικες που περιγράφουν ένα τρίγωνο με κορυφές A,B,C χωρίς και με τη χρήση struct.

double A_x, Ay, B_x, B_y, C_x, C_y;
struct Point { double x; double y; };
struct Point A,B,C;
//Χρήση ως A.x A.y B.x κοκ

Το παράδειγμα γίνεται ακόμα πιο ενδιαφέρον όταν χρησιμοποιήσουμε μεταβλητές με διαφορετικούς τύπους δεδομένων. Π.χ.

struct Person {
    char *lastname;
    char *firstname;
    int age;
    int height;
};
struct Person students[10];

Βλέπουμε δηλαδή ότι και τα πεδία της δομής είναι ονοματισμένα, αλλά και οι τύποι δεδομένων δεν είναι ομογενείς όπως στους πίνακες. Επίσης τα στοιχεία στους πίνακες υποδεικνύονται με τον δείκτη μέσα στις αγκύλες, ενώ σε ένα struct, με το όνομα της μεταβλητής και το όνομά τους με τελεία ανάμεσα. Από εκεί και περα τα υπόλοιπα είναι "όπως στους πίνακες". Επειδή όμως αυτό δεν είναι προφανές τι σημαίνει...

  • Τα στοιχεία της είναι αποθηκευμένα διαδοχικά στη μνήμη
  • Το όνομα μιας μεταβλητής τύπου struct είναι pointer στο πρώτο στοιχείο της
  • Αυξάνοντας κατά ένα έναν δείκτη σε μεταβλητή του struct αυξάνεται η τιμή του ώστε να δείχνει στο επόμενο στοιχείο
  • Όπως στους πολυδιάστατους πίνακες ένα στοιχείο ενός πίνακα μπορεί να είναι ένας άλλος πίνακας, έτσι και με ένα struct μπορεί να περιλαμβάνει ένα άλλο struct
#include <stdio.h>
#include <math.h>

struct Point { double x; double y; };

int main() {
    struct Point A, B, C;
    double area;

    A.x = A.y = 0.0;
    B.x = C.y = 1.0;
    B.y = C.x = 0.0;

    area = 0.5 * fabs( ( A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y) ) );

    printf("The area is %lf\n", area);

    return 0;
}
Run online
Ο τελεστής sizeof και το typedef

Επειδή πολλές φορές θα θέλουμε να δημιουργήσουμε δυναμικά μία τέτοια δομή, αλλά δεν είναι εμφανές το μέγεθος της μνήμης που απαιτείται, η C μας παρέχει τον τελεστή sizeof ο οποίος επιστρέφει το μέγεθος της μνήμης σε Bytes που απαιτείται για την αποθήκευση ενός τύπου δεδομένων.

Επίσης επειδή είναι αρκετά εκνευριστικό να πληκτρολογούμε συνεχώς τη λέξη κλειδί struct μαζί με το όνομα της δομής, επίσης μας παρέχεται και η λέξη κλειδί typedef που μας επιτρέπει να ονοματίσουμε τον τύπο δεδομένων με μία ονομασία που να την χρησιμοποιούμε ως έναν δικής μας κατασκευής τύπο δεδομένων.

#include <stdio.h>
#define POINTS_PER_POLYGON 10

typedef struct _point { double x; double y; } Point;

int main () {
    Point A,B,C;
    Point *Polygon = (Point *)malloc(POINTS_PER_POLYGON * sizeof (Point));
    Point *helper = Polygon;

    Polygon[0].x = Polygon[0].y = 0.0;
    helper++;
    (*helper).x = (*helper).y = 1.0;

    printf("{%lf,%lf} , {%lf,%lf}\n", Polygon[0].x,Polygon[0].y,Polygon[1].x,Polygon[1].y);

    free(Polygon);

    return 0;
}
Run online
Συναρτήσεις δημιουργίας

Λόγω του συνήθως μεγάλου αριθμού μεταβλητών, αλλά και της επαναληψιμότητας της διαδικασίας αρχικοποίησης μιας δομής δεδομένων συνηθίζεται να δημιουργούμε τουλάχιστον μία συνάρτηση που να δημιουργεί και να επιστρέφει ένα έτοιμο τέτοιο αντικείμενο.
Σημειώστε ότι επειδή ένας δείκτης σε δομή πριν χρησιμοποιηθεί πρέπει να γίνει dereference, με τη χρήση του *, η γραφή (*a_Point).x γράφεται για λόγους ευκολίας ισοδύναμα και a_Point->x δηλαδή όταν η μεταβλητή είναι pointer, αντί για τελεία χρησιμοποιούμε το ->

#include <stdio.h>
#include <stdlib.h>

typedef struct _mtrx { int M; int N; double **data; } Matrix;


void deleteMx(Matrix *mx) {
    int i;
    if (mx) {
        if (mx->data) {
            for (i=0; i<mx->M; i++) {
                if (mx->data[i])
                    free(mx->data[i]);
            }
            free(mx->data);
        }
        free(mx);
    }
}

Matrix *newEmptyMx(int m, int n) {
    int i, j;
    Matrix *mtrx = (Matrix *)malloc(sizeof(Matrix));
    if (!mtrx) return NULL;

    mtrx->M = m;
    mtrx->N = n;

    mtrx->data = (double **)malloc(sizeof (double *) * m);
    if (!mtrx->data) return NULL;

    for (i=0; i<m; i++) {
        mtrx->data[i] = (double *)malloc(sizeof(double) * n);
        if (!mtrx->data[i]) {
            deleteMx(mtrx);
            return NULL;
        } else {
            for (j=0; j<n; j++)
                mtrx->data[i][j] = 0.0;
        }
    }

    return mtrx;
}

Matrix *newIMx(int n) {
    int i;
    Matrix *m = newEmptyMx(n,n);

    if(!m) return NULL;

    for(i=0; i<n; i++)
        m->data[i][i] = 1.0;

    return m;
}

void ShowMx(Matrix *m) {
    int i,j;

    for(i=0; i<m->M; i++) {
        for(j=0; j<m->N; j++) {
            printf("%2.3lf ", m->data[i][j]);
        }
        printf("\n");
    }
}

void SwapRowsMx(int i, int j, Matrix *mx) {
    double *help;

    help = mx->data[i];
    mx->data[i] = mx->data[j];
    mx->data[j] = help;
}

void SwapColsMx(int i, int j, Matrix *mx) {
    double help;
    int k;

    for (k=0; k<mx->M; k++) {
        help = mx->data[k][i];
        mx->data[k][i] = mx->data[k][j];
        mx->data[k][j] = help;
    }
}

int TransposeMx(Matrix *mx) {
    int L, S;
    int rszCols;
    int i, j;
    double help3, *help, **help2;

    // Check largest dimension
    if (mx->M > mx->N) {
        L = mx->M;
        S = mx->N;
        rszCols = 1;
    } else {
        L = mx->N;
        S = mx->M;
        rszCols = 0;
    }

    // Resize to a big square
    if (rszCols) {
        for(i=0; i<mx->M; i++) {
            help = (double *)realloc(mx->data[i] , sizeof(double ) * L);
            if (!help) {
                deleteMx(mx);
                return 0;
            }
            mx->data[i] = help;
        }
    } else {
        help2 = (double **)realloc(mx->data, sizeof(double *)*L);
        if (!help2) {
            deleteMx(mx);
            return 0;
        }
        mx->data = help2;
        for (i=mx->M; i<L; i++) {
            mx->data[i] = (double *)malloc(sizeof(double) * L);
            if (!mx->data[i]) {
                deleteMx(mx);
                return 0;
            }
        }
    }

    // Do transpose everything (even nonsense data)
    for(i=0; i<L; i++) {
       for(j=0; j<i; j++) {
           help3 = mx->data[i][j];
           mx->data[i][j] = mx->data[j][i];
           mx->data[j][i] = help3;
       }
    }

    // Now crop extra data
    if (rszCols) {
        for (i=S; i<L; i++) {
            if (mx->data[i]) free( mx->data[i] );
        }
        help2 = (double **)realloc(mx->data, sizeof(double *)*S);
        if (!help2) {
            deleteMx(mx);
            return 0;
        }
        mx->data = help2;
    } else {
        for (i=0; i<L; i++) {
            help = (double *)realloc(mx->data[i], sizeof(double)*S);
            if (!help) {
                deleteMx(mx);
                return 0;
            }
            mx->data[i] = help;
        }
    }

    // Fix matrix size
    L = mx->M;
    mx->M = mx->N;
    mx->N = L;

    return 1;
}

int main() {
    Matrix *I = newIMx(5);
printf("Part I\n");
    SwapRowsMx(1,3, I);
    //SwapColsMx(0,2, I);
    ShowMx(I);
    deleteMx(I);

printf("Part II-a\n");
    I = newEmptyMx(3,8);
    I->data[2][5] = 1.23;
    I->data[0][7] = 2.34;
    I->data[1][4] = 8.88;
    I->data[1][2] = 3.45;
    ShowMx(I);
    TransposeMx(I);
printf("Part II-b\n");
    ShowMx(I);
    deleteMx(I);

    return 0;
}
Run online
Διαχείριση Αρχείων

Η χρήση των αρχείων, όπως και πολλών άλλων resources, ξεκινά ανοίγοντάς τα και ολοκληρώνεται με το κλείσιμό τους. Στο ενδιάμεσο η διαχειρισή τους γίνεται με διάφορες εντολές π.χ. ανάγνωσης και εγγραφής. Το άνοιγμά ενός αρχείου μας επιστρέφει (έναν pointer σε) μια δομή (με το όνομα FILE) που το περιγράφει και την οποία τη χρησιμοποιούμε στις κλήσεις όλων των άλλων συναρτήσεων που αφορούν το αρχείο αυτό και φυσικά στο κλείσιμό του.

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

Όταν γράφουμε τα δεδομένα μας σε ένα αρχείο, η εγγραφή τους γίνεται με κάποια σειρά (π.χ. εάν θέλουμε να αποθηκεύσουμε αλγεβρικούς πίνακες όπως στο προηγούμενο παράδειγμα, τότε μπορούμε για κάθε πίνακα να αποθηκεύουμε π.χ πρώτα τις διαστάσεις και μετά τα δεδομένα του κάθε πίνακα). Προφανώς όταν διαβάζουμε με κάποιο άλλο κώδικα, τα περιεχόμενα του αρχείου θα πρέπει να το κάνουμε με την ίδια σειρά. Η συγκεκριμένη σειρά αποθήκευσης των δεδομένων και ο τρόπος που αυτά συνδέεονται μεταξύ τους, ουσιαστικά αποτελεί - αυτό που λέμε ως χρήστες - format του αρχείου ή τύπος αρχείου. Και ακριβώς αυτή η διαφορά στην αποθήκευση των δεδομένων είναι που δεν επιτρέπει σε δύο προγράμματα που έχουν τον ίδιο σκοπό (π.χ. σχεδιαστικά) να ανοίγει το ένα, τα αρχεία του άλλου. Και γι'αυτό όταν βλέπουμε ότι το πρόγραμμα Α μπορεί να ανοίξει (=διαβάσει) ή να γράψει αρχεία για ένα το πρόγραμμα Β, τότε μπορούμε να είμαστε σίγουροι ότι έχει γραφτεί συγκεκριμένος κώδικας που διαβάζει τα δεδομένα από το αρχείο τύπου Α και τα τοποθετεί στη μνήμη με τη μορφή που προβλέπει το πρόγραμμα Β.

Ένα αρχείο μπορούμε να το φαντασούμε εσωτερικά να λειτουργεί παρόμοια με τη μνήμη. Δηλαδή σαν έναν μονοδιάστατο πινακα από Bytes. Μόνο που κατά τη χρήση του υπάρχει και ένας "δείκτης" (όχι με την έννοια του pointer) ή "δρομέας", ο οποίος δείχνει την επόμενη θέση αυτού του πίνακα που είναι προς χρήση (εγγραφή/ανάγνωση). Δηλαδή λειτουργεί ως σελιδοδείκτης ώστε να ξέρουμε μέχρι ποιό σημείο του αρχείου έχουμε φτάσει διαβάζοντας ή γράφοντας. Με το άνοιγμα του αρχείου συνήθως (εκτός αν έχουμε ορίσει αλλιώς) ο δρομέας αυτός δείχνει στην αρχή του αρχείου. Καθώς προχωρά η χρήση του αρχείου αυτός ο δρομέας μετακινείται αυτόματα ανάλογα με τις εντολές διαχείρισης που καλούμε. Πέρα από αυτή τη "φυσική" μετακίνηση του δρομέα μπορούμε να τον μετακινήσουμε με τη χρήση σχετικών εντολών.

Άνοιγμα & κλείσιμο Αρχείων

Για να χρησιμοποιήσουμε ένα αρχείο θα πρέπει πρώτα να το ανοίξουμε επιτυχώς. Αυτό γίνεται με την εντολή fopen η οποία συντάσσεται όπως φαίνεται παρακάτω και επιστρέφει είτε NULL αν υπάρχει πρόβλημα, είτε έναν δείκτη προς μια δομή FILE που περιγράφει το συγκεκριμένο αρχείο, σε αυτή τη χρήση.

FILE *aFile = fopen(char *  path_to_the_file  , char * file_access_mode );

Το path_to_the_file είναι ένα path (διαδρομή) για να βρεθεί το αρχείο στο δίσκο. οπότε λέγεται Οι διαδρομές διαχωρίζονται σε absolute path (δηλαδή απόλυτη διαδρομή) καθώς περιγράφουν επακριβώς το σημείο στον δίσκο (καθώς και τον ίδιο τον δίσκο) στο οποίο είναι αποθηκευμένο το αρχείο π.χ. για Windows C:\myfolder\mysubfolder\myfile.demo ή για Linux /myfolder/mysubfolder/myfile.demo και σε relative path (δηλαδή σχετική διαδρομή) που περιγράφει τη θέση του αρχείου σε σχέση με τον τρέχοντα φάκελο π.χ. εάν είμαι στη γραμμή εντολών στον φάκελο C:\myfolder τότε το αρχείο της προηγούμενης απόλυτης διαδρομής μπορώ να το περιγράψω με σχετική διαδρομή ως mysubfolder\myfile.demo. Γενικά κάθε πρόγραμμα ξεκινά με τρέχοντα φάκελο (current directory) τον φάκελο που βρίσκεται η γραμμή εντολών κατά την έναρξή του. Ενώ εάν ξεκινά μέσα από παραθυρικό περιβάλλον, τότε ο τρέχων κατάλογος είναι αυτός που περιέχει το εκτελέσιμο εκτός ένα στο λειτουργικό σύστημα έχει οριστεί αλλιώς.

Σημείωση: Τα Windows χρησιμοποιούν την ανάποδη κάθετο (\ που ονομάζεται backslash) για να χωρίζουν τα τμήματα (φακέλους) της διαδρομής. Όμως αυτή η κάθετος χρησιμοποιείται και στις συμβολοσειρές για την αναπαράσταση ειδικών χαρακτήρων (\n \t κλπ) θα πρέπει να θυμόμαστε ότι το ίδιο το \ παριστάνεται ως \\, άρα θα την γράφαμε σε κώδικα C ως: char *myPath = "C:\\myfolder\\mysubfolder\\myfile.demo

To file_access_mode είναι ένα από τα ακόλουθα strings και δίπλα στο καθένα παρουσιάζεται η ερμηνία του:

Mode Περιγραφή
rΑνοίγει το αρχείο για ανάγνωση μόνο.
wΑνοίγει το αρχείο για γράψιμο μόνο. Εάν το αρχείο δεν υπάρχει, τότε δημιουργείται. Ο δρομέας βρίσκεται στην αρχή του αρχείου.
aΑνοίγει το αρχείο για γράψιμο μόνο. Εάν το αρχείο δεν υπάρχει, τότε δημιουργείται. Ο δρομέας βρίσκεται στο τέλος του αρχείου.
r+Ανοίγει το αρχείο για ανάγνωση και εγγραφή.
w+Ανοίγει το αρχείο για ανάγνωση και εγγραφή. Εάν το αρχείο δεν υπάρχει τότε δημιουργείται. Εάν υπάρχει το μέχρι τώρα περιεχόμενό του διαγράφεται.
a+Ανοίγει το αρχείο για ανάγνωση και εγγραφή. Εάν το αρχέιο δεν υπάρχει τότε δημιουργείται. Ο δρόμάς βρίσκεται στην αρχή του αρχείου, αλλά ανεξαρτήτρως από τη θέση του, όσα γράφονται προστίθενεται στο τέλος του αρχείου!

Για πλήρη συμβατότητα με όλα τα συστήματα, εάν το περιεχόμενο του αρχείου θα είναι binary καλύτερα να προσθέσετε ένα b στο mode, ως 2ο ή 3ο χαρακτήρα. (π.χ. "rb+" ή "r+b").

Όταν έχετε ολοκληρώση τη χρήση ενός αρχείο που άνοιξε επιτυχώς, θα πρέπει πάντα να καλείτε την εντολή fclose η οποία παίρνει ως μοναδικό όρισμα τον δείκτη προς τη δομή FILE που επέστρεψε η fopen και επιστρέφει μηδενική τιμή αν όλα πάνε καλά.

fclose(FILE *  file_structure );

ΣΗΜΕΙΩΣΗ: Εάν δεν κλείσετε κάποιο αρχείο πριν τον τερματισμό του προγράμματος, τότε υπάρχει κίνδυνος να μην γραφτούν (δηλαδή να χαθούν) τα τελευταία δεδομένα που έχετε "γράψει" σε αυτό. Αυτό οφείλεται στη χρήση βοηθητηκών χώρων μνήμης που ονομάζονται buffers και θα αναφερθούμε σε αυτούς αργότερα.

Εγγραφή και ανάγνωση χαρακτήρων

Για να διαβάσουμε έναν χαρακτήρα από ένα αρχείο (που αναμένουμε να είναι κειμένου) χρησιμοποιούμε την εντολή fgetc που παίρνει μία παράμετρο (τον δείκτη για τη δομή FILE) και επιστρέφει είτε τον χαρακτήρα (ως int), είτε το EOF, είτε κάποιο σφάλμα. Τον χαρακτήρα τον επιστρέφει ως ακέραιο ώστε να είναι δυνατή και η αναπαράσταση και των υπολοιπων τιμών (όπως είναι το EOF και τα σφάλματα). O ακόλουθος κώδικας εκτυπώνει κατακόρυφα το περιεχόμενο ενός αρχείου.

#include <stdio.h>

int main () {
    int c;
    FILE *f1 = fopen("test1.txt","r");
    if (!f1) {
        printf("Cant open file!\n");
        return 0;
    }

    while ((c = fgetc(f1)) != EOF) {
        printf("%c\n",c);
    }

    fclose(f1);

    return 0;
}
Run online

Αντίστροφα, για την εγγραφή σε αρχείο που έχει ανοιχθεί με δυνατότητα εγγραφής, χρησιμοποιούμε την fputc(char c , FILE *f) η οποία, αν όλα πάνε καλά, επιστρέφει μηδενική τιμή, αλλιώς επιστρέφει την ειδική τιμή EOF που είναι τα αρχικά του End Of File και την είδαμε ήδη σε εφαρμογή στο παραπάνω παράδειγμα.

#include <stdio.h>

int main ()
{
   FILE *fp;
   int ch;

   fp = fopen("file.txt", "w+");
   for( ch = 33 ; ch <= 122; ch++ )
   {
      fputc(ch, fp);
   }
   fclose(fp);

   return(0);
}
Run online

Σημειώστε ότι με την εκτέλεση όποιασδήποτε τέτοιας εντολής ο δρομέας του αρχείου αυξάνεται κατά ένα.

Εγγραφή και ανάγνωση συμβολοσειρών

Οι εντολές για ανάγνωση και εγγραφή strings είναι αντίστοιχα οι
fgets ( char * str, int n, FILE * file) και
fputs ( char * str, FILE * file). Και στις δύο περιπτώσεις str είναι ένας πίνακας χαρακτήρων (=συμβολοσειρά) ενώ το file είναι ένας δείκτης στη σχετική δομή FILE. Ειδικά για την ανάγνωση χρειάζεται και η παράμετρος n που θέτει ένα όριο στον αριθμό των χαρακτήρων που θα διαβαστούν από το αρχείο. Συνήθως αυτό είναι και το μέγεθος του πίνακα str, εκτός αν υπάρχει λόγος που ορίζει η λογική του κώδικα, και θα πρέπει να είναι κάποιος μικρότερος.

Προσέξτε ότι ενώ η fputs απλά γράφει στο αρχείο το κείμενο, η fgets διαβάζει το πολύ n χαρακτήρες ή και λιγότερους εάν δεν εμφανιστεί αλλαγή γραμμής. Δηλαδή - εφόσον το όριο n είναι επαρκώς μεγάλο - η fgets διαβάζει το αρχείο γραμμή προς γραμμή.

Όταν η εκτέλεση αυτών των συναρτήσεων είναι επιτυχής η fputs επιστρέφει τον αριθμό των χαρακτήρων που γράφτηκαν ή κάποια αρνητική τιμή ως ένδειξη σφάλματος. Αντίστροφα η fgets επιστρέφει την ίδια την παράμετρο str όταν όλα πάνε καλά ή NULL αν υπάρχει σφάλμα. Και στις δύο περιπτώσεις ο δρομέας του αρχείου αυξάνεται κατά το πλήθος των χαρακτήρων που διαβάστηκαν ή γράφτηκαν σε αυτό.

Στο παρακάτω παράδειγμα αντιγράφεται ένα αρχείο γραμμή προς γραμμή αφήνοντας μία έξτρα κενή γραμμή ανάμεσά τους.

#include <stdio.h>

int main ()
{
   FILE *fin, *fout;
   char str[1024];

   fin = fopen("test1.txt", "r");
   if (!fin) {
       printf("Cant open input file!\n");
       return 1;
   }
   fout = fopen("out1.txt", "w+");
   if (!fout) {
       printf("Cant open output file!\n");
       fclose(fin);
       return 2;
   }

   while(fgets(str, 1024, fin)) {
       fputs(str, fout);
       fputs("\n", fout);   // str Already includes a new line character,
                            // so adding one more is enough for the extra line
   }

   fclose(fout);
   fclose(fin);

   return(0);
}
Run online
Μορφοποιημένη είσοδος & έξοδος κειμένου

Εντελώς αντίστοιχα με όσα έχουμε δει για τις εντολές scanf και printf υπάρχουν για τα αρχεία οι εντολές fscanf και fprintf οι οποίες πριν από τα γνωστά μας ορίσματα παίρνουν και έναν δείκτη προς το αρχείο το οποίο αφορούν. Η 1η επιστρέφει (όπως και η scanf) τον αριθμό τον ορισμάτων στα οποία έβαλε τις τιμές που βρέθηκαν, ενώ η 2η τον αριθμό των χαρακτήρων που γράφτηκαν στο αρχείο.

Μία επίσης ενδιαφέρουσα παραλλαγή τους είναι η sscanf και sprintf, όπου το πρώτο όρισμα που προστίθεται δεν είναι μία δομή FILE, αλλά ένας δείκτης σε συμβολοσειρά η οποία παίζει τον ρόλο της πηγής δεδομένων για την sscanf και του "στόχου" για την sprintf.
Στο παρακάτω παράδειγμα διαβάζονται από ένα αρχείο ανά 3 double χωρισμένα με κόμμα, εκτυπώνονται με νέα μορφή σε μια συμβολοσειρά και η συμβολοσειρά αυτή σε ένα αρχείο.

#include <stdio.h>

int main ()
{
   FILE *fin, *fout;
   char line[1024];
   double a,b,c;
   int n;

   fin = fopen("data2.txt", "r");
   if (!fin) { printf("Cant open input file!\n"); return 1; }
   fout = fopen("out2.txt", "w+");
   if (!fout) { printf("Cant open output file!\n"); fclose(fin); return 2; }

   while((n = fscanf(fin, "%lf, %lf, %lf", &a, &b, &c)) > 0) {
       if (n < 3) c = 0;
       if (n < 2) b = 0;
       sprintf(line, "%03.5lf | %03.5lf | %03.5lf\n", a,b,c);
       fputs(line, fout);
   }

   fclose(fout);
   fclose(fin);

   return(0);
}
Run online
stdin , stdout , stderr

Εκτός από τα τυπικά αρχεία, στους υπολογιστές υπάρχουν και άλλα resources που λειτουργούν μέσα από τον μηχανισμό των αρχείων. Τα πιο τυπικά είναι η εισαγωγή δεδομένων από το πλητρκολόγιο και η έξοδος δεδομένων στην οθόνη. Για τον σκοπό αυτό, χωρίς να κάνουμε κάτι περισσότερο, σε κάθε πρόγραμμα που εκτελείται (από τη γραμμή εντολών τουλάχιστον) υπάρχουν πάντα έτοιμες και ανοιχτές τρείς δομές FILE. Η stdin που συμπεριφέρεται ως αρχείο μόνο για ανάγνωση και το περιεχόμενό της προέρχεται από το πληκτρολόγιο. Και οι stdout και stderr που συμπεριφέρονται ως αρχεία μόνο για εγγραφή. Η διαφορά τους είναι στο πως (πρέπει να) τα χειριζόμαστε. Στο stdout πρέπει να γράφονται τα δεδομένα (π.χ. μηνύματα) που αφορούν την κανονική λειτουργία του προγράμματος ή αποτελούν το αποτέλεσμα της εκτέλεσης. Αντίθετα στο stderr γράφονται μόνο μηνύματα λάθους ή πράγματα που αφορούν δευτερεύουσες λειτουργίες του.

Ο διαχωρισμός αυτός έχει νόημα αν σκεφτούμε τη δυνατότητα που μας δίνει η γραμμή εντολών να ανακατευθύνουμε κάποιο αρχείο στο πρόγραμμά μας ως stdin ή τις εξόδους του stdout και stderr προς άλλα αρχεία. Εκεί για παράδειγμα δεν θα θέλαμε στο αρχείο με τα αποτελέσματα (αυτό που αντιστοιχεί στο stdout) να εμφανίζονται μηνύματα λάθους ή άλλες άσχετες πληροφορίες, τις οποίες θα μπορούσαμε (και αυτό κάνουμε συνήθως) τις αφήνουμε χωρίς ανακατεύθυνση ώστε να εμφανίζονται στην οθόνη.

#include <stdio.h>

int main()
{
    int n, i;
    fscanf(stdin, "%d", &n);
    fprintf(stderr, "Hello! I'll count from 1 to %d\n", n);
    for(i=1; i<=n; i++) {
        fprintf(stdout,"%d ", i);
        if (i%10==0)
            fprintf(stdout,"\n");
    }
    fprintf(stderr,"Ok, done!\n");
    return 0;
}
Run online

Μια άλλη - μη προφανής - χρήση των αρχείων είναι τα pipes και τα named pipes που είναι ένας τρόπος ώστε δύο προγράμματα να ανταλλάσσουν πληροφορίες. Ουσιαστικά ένα pipe είναι δύο αρχεία, ένα εισόδου (μόνο ανάγνωση) και ένα εξόδου (μόνο εγγραφή), όπου ότι γράφεται στο αρχείο εξόδου, είναι άμεσα διαθέσιμο στο αρχείο εισόδου. Προσοχή, αυτό μοιάζει με ένα αρχείο στον δίσκο που θα μπορούσαν να έχουν πρόσβαση και τα δύο προγράμματα ταυτόχρονα, αλλά δεν είναι πραγματικό αρχείο και γι'αυτό είναι δυνατή η ταχύτατη διακίνηση πληροφοριών μέσα από ένα pipe. Τα named pipes ονομάζονται "επίσημα" αρχεία FIFO. Διαβάστε γι'αυτά Online.

Binary I/O

Μέχρι αυτό το σημείο όλα τα δεδομένα που γράφοντας ή διαβάζονταν από τα αρχεία ήταν συμβολοσειρές. Όμως πολύ συχνά θέλουμε να μπορούμε να γράφουμε και να διαβάζουμε καθαρά δυαδική πληροφορία στα αρχεία. Αυτό γίνεται ορίζοντας μια περιοχή της μνήμης ως πηγή της πληροφορίας και ένα αρχείο ως στόχο (εγγραφή), είτε ορίζοντας ένα αρχείο ως πηγή και μια περιοχή της μνήμης ως στόχο (ανάγνωση). Μία περιοχή της μνήμης ορίζεται "κατά τα γνωστά" με τη βοήθεια ενός pointer που δείχνει στην αρχή της και έναν ακέραιο που ορίζει το μέγεθός της.

Οι εντολές είναι οι fread και fwrite που δέχονται τα ίδια ορίσματα. Πρώτα τον δείκτη στην περιοχή της μνήμης, στη συνέχεια δύο ακέραιους (έναν με το μέγεθος του κάθε binary στοιχείου που θα διαβαστεί ή εγγραφεί, και έναν με το πλήθος τέτοιων στοιχείων) και τέλος έναν δείκτη στη δομή FILE. Το αποτέλεσμα και των δύο είναι το πλήθος των στοιχείων που διακινήθηκαν επιτυχώς από ή προς το αρχείο.

#include <stdio.h>

typedef struct _sample {
    int x[5];
    char *y;
} Sample;

int main()
{
    Sample s;
    FILE * fout;

    s.x[0] = 1;
    s.x[1] = 2;
    s.x[2] = 3;
    s.x[3] = 4;
    s.x[4] = 5;

    s.y = "Cha cha cha!";

    if (!(fout = fopen("Testfile.bin","w+b"))) {
        printf("File ERROR!\n");
        return 1;
    }

    fwrite(&s, sizeof(s), 1, fout);
    fclose(fout);

    printf("%x\n",s.x);
    printf("%x\n",s.y);

    return 0;
}
Run online

Το περιεχόμενο ενός τέτοιου αρχείου δεν είναι αναγνώσιμο από έναν άνθρωπο, όπως είναι άμεσα κατανοητό από τον υπολογιστή. Για να βοηθηθούμε ως άνθρωποι στην "ανάγνωση" τέτοιων αρχείων, χρησιμοποιούμε προγράμματα που παρουσιάζουν τη δυαδική πληροφορία ως δεκαεξαδικούς αριθμούς στην οθόνη, ανά 8 ή 16 Bytes. Ένα τέτοιο πρόγραμμα μπορεί να είναι το ακόλουθο.

#include <stdio.h>

int main()
{
    FILE * fin;
    char buffer[1024];
    int i, n;

    if (!(fin = fopen("Testfile.bin","rb"))) {
        printf("File ERROR!\n");
        return 1;
    }

    n = fread(&buffer, 1, 1024, fin);
    fclose(fin);

    for (i=0; i<n; i++) {
        printf("%02x ",buffer[i] & 0xFF);
        if (i%8==7)
            printf("\n");
    }

    printf("\n");

    return 0;
}
Run online

Όπως παρατηρούμε το κείμενο δεν έχει αποθηκευθεί στο αρχείο και αυτό είναι αναμενόμενο, καθώς δεν αποτελεί μέρος της δομής Sample. Αντίθετα ο πίνακας που είναι εξαρχής ορισμένος με συγκεκριμένη διάσταση μέσα στη δομή έχει αποθηκευθεί. Έτσι είναι ευθύνη του προγραμματιστή να φροντίσει να αποθηκεύσει ξεχωριστά το κείμενο αυτό και να το συνδέσει με τη δομή στη συνέχεια. Επίσης στο αρχείο βλέπουμε και κάποια μηδενικά Bytes που "δεν εξηγούνται" βάσει της δομής, αυτά είναι για λόγους ευθυγράμμισης της πληροφορίας και αφορούν τη δομή του συγκεκριμένου υπολογιστή στον οποίο εκτελείται το πρόγραμμά μας.

fseek / ftell

Η θέση του δρομέα στο αρχείο μπορεί να οριστεί με την εντολή fseek που δέχεται τρεις παραμέτρους, πρώτος δίνεται ένας pointer σε δομή FILE, μετά η νέα θέση του δρομέα (ακέραιος προσημασμένος αριθμός) και τέλος μία παράμετρος που ορίζει από που μετράει η θέση του δρομέα. Οι τρεις τιμές που παίρνει αυτή η παράμετρος είναι: SEEK_SET, SEEK_CUR και SEEK_END, που υποδεικνύουν ότι η θέση του δρομέα είναι σε σχέση με την αρχή, την τρέχουσα και το τέλος του αρχείου αντίστοιχα. Αν όλα πάνε καλά επιστρέφεται μηδενική τιμή.

Για να δούμε ποια είναι η θέση του δρομέα χρησιμοποιούμε τη συνάρτηση ftell που παίρνει για μοναδική παράμετρο τον δείκτη στη δομή FILE και επιστρέφει τη θέση του δρομέα σε σχέση με την αρχή του αρχείου ως long. Εάν προκύπτει κάποιο σφάλμα, τότε επιστρέφεται η τιμή -1. Στο παρακάτω παράδειγμα δείτε έναν τρόπο να υπολογίσουμε το μήκος ενός αρχείου σε Bytes.

#include <stdio.h>

int main()
{
    FILE * fin;
    int n;

    if (!(fin = fopen("somefile.txt","r"))) {
        printf("File ERROR!\n");
        return 1;
    }

    fseek(fin, 0, SEEK_END);
    printf("The file length is: %ld\n", ftell(fin));
    fclose(fin);

    return 0;
}
Run online
Buffered I/O

Όταν γράφουμε δεδομένα σε ένα αρχείο (αλλά και όταν διαβάζουμε από αυτό), αυτά μετακινούνται σε ομάδες (blocks) και όλες οι πιο μικρές αλλαγές (πχ εγγραφή ή ανάγνωση ενός χαρακτήρα) γίνονται σε μία περιοχή μνήμης που ορίζει το σύστημα και την ονομάζουμε buffer. Όταν ο buffer αυτός γεμίσει καθώς γράφουμε πληροφορίες ή αδειάσει καθώς διαβάζουμε, τότε μόνο μεταφέρονται πραγματικά πληροφορίες προς ή από το δίσκο. Αυτό έχει ως αποτέλεσμα να δημιουργούνται προβλήματα σε περίπτωση που ενώ έχουμε δώσει εντολή να αποθηκευθούν κάποεις πληροφορίες στο αρχείο, το πρόγραμμα ή και όλη η λειτουργία του υπολογιστή τερματίζεται απρόοπτα, καθώς ο buffer τότε δεν μεταφέρεται στον δίσκο ενώ όλη η πληροφορία του buffer χάνεται αφού είναι στη μνήμη RAM. Για να αποφύγουμε τέτοια προβλήματα κάποεις φορές που ολοκληρώνουμε μία ενότητα του προγράμματος ή μια συγκεκριμένη του δραστηριότητα μπορούμε να καλέσουμε την εντολή fflush που δέχεται ως μοναδικό της όρισμα τον δείκτη στη δομή FILE και όταν όλα πάνε καλά επιστρέφει μηδενική τιμή. Τόσο αυτή η εντολή όσο και το κλείσιμο του αρχείου μεταφέρει άμεσα όλο το περιεχόμενου του buffer στον δίσκο.

Η χρήση του buffer, μας παρέχει πολύ μεγάλη επιτάχυνση στη λειτουργία του προγράμματος, καθώς η καθαυτή πρόσβαση στον δίσκο είναι "ακριβή" από πλευράς χρόνου. Ένα άλλο πλεονέκτημα που μας επιτρέπει η χρήση του buffer κατά την ανάγνωση των δεδομένων είναι η χρήση της εντολής ungetc η οποία επιστρέφει στον buffer ένα χαρακτήρα από αυτούς που έχουν διαβαστεί. Δέχεται δύο ορίσματα, πρώτα τον ίδιο τον χαρακτήρα και μετά τον δείκτη στο FILE για το οποίο θα επιστραφεί το αρχείο στο buffer.

Περισσότερες πληροφορίες για τις δυνατότητες εισόδου και εξόδου από αρχεία μπορείτε να βρείτε στην τεκμηρίωση του stdio.h για παράδειγμα σε αυτή τη σελίδα: http://www.tutorialspoint.com/c_standard_library/stdio_h.htm

Λίγα λογια για τα Αντικείμενα

Όταν έχουμε μία δομή με την οποία περιγράφουμε "κάτι" αυτό ταυτόχρονα σημαίνει οτι η δομή αυτή είναι κατάλληλη να περιγράψει όλα τα "πράγματα" αυτής της κατηγοριας. Έτσι με αυτό τον τρόπο μπορούμε να περιγράψουμε μια ολόκληρη κατηγορία πραγμάτων. Για παράδειγμα για να περιγράψουμε μία ρόδα θα πρέπει να περιγράψουμε την εσωτερική και εξωτερική της διάμετρο, τη διάμετρο του άξονα στον οποίο συνδέεται, τον τρόπο σύνδεσης, το υλικό από το οποίο είναι φτιαγμένη, κλπ. Με αυτό το "σετ" των χαρακτηριστικών μπορεί να μην μπορούμε να περιγράψουμε "όοοοοολες" τις ρόδες, αλλά σίγουρα μπορούμε να περιγράψουμε όσες μας ενδιαφέρουν (θεωρώντας ότι εμείς επιλέξαμε το σύνολο των χαρακτηριστικών).

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

Στην πραγματικότητα η κλάση και το αντικείμενο, έχουν μερικές ιδιότητες ακόμα - ουσιαστικές για την λειτουργία τους. Επειδή κάθε δράση (δηλαδή συνάρτηση - πλέον θα τις αποκαλούμε μεθόδους), που αφορά το αντικειμενο, με βάση όσα ξέρουμε, θα πρέπει να παίρνει ως παράμετρο το ίδιο το αντικείμενο, θα ήταν αρκετά ανόητο στην όψη να χρειάζονταν να γράφουμε: myobject.mymethod( myobject, x,y,z ) έτσι στις πραγματικά αντικειμενοστραφείς γλώσσες θα αρκούσε να γράφαμε: myobject.mymethod( x,y,z )

Η C δεν είναι αντικειμεοστραφής γλώσσα. Τα παραπάνω τα αναφέρουμε για την πληρότητα των σημειώσεων, εξού και είναι τόσο επιδερμικά. Από την άλλη η υλοποιηση της C στον compiler του Arduino έχει κάποια χαρακτηριστικά που λειτουργούν ως αντικείμενα και η αναφορά αυτη έχει σαν σκοπό να επιτρέψει από τη μία να υπάρχει η οικειότητα, από την άλλη να μην τα μπερδέψουμε με τη γλώσσα C.

Συμβολοσειρές στο Arduino

Οι συμβολοσειρές στο arduino - εκτός από τον γνωστό τρόπο που είδαμε οτι έχουμε να τις χειριζόμαστε από τη C - έχουν και κάποια αντικειμενοστραφή χαρακτηριστικά. Για να πάρουμε μια εικόνα θα δούμε εδώ κατηγοριοποιημένες τις μεθοδους που μας παρέχονται και πριν απο αυτό μερικά παραδείγματα για το πως δηλώνουμε και χρησιμοποιούμε τα strings αυτά.


String myStr = "Hello there!";      // String "object" variable
String strFromChar = String('x');
String strFromInt = String(111, HEX);
String strFromNum = String(123.45, 2);  // The desired decimal places is also provided as 2nd arg
String wowString = String(myStr + " How are you?");

Βλέπουμε λοιπόν ότι δηλώνοντας μια μεταβλητή με τύπο String (προσέξτε το κεφαλαίο S) μπορούμε να του κάνουμε απευθείας ανάθεση τιμής από κείμενο σε διπλά εισαγωγικά ή μετατροπή από άλλο τύπο δεδομένων, ακόμα και να προσθέσουμε (= ενώσουμε) δύο τέτοια Strings!! Όλα αυτά μοιάζουν πολύ με τη συμπεριφορά των αντικειμένων σε κώδικα που διαχειρίζεται κείμενα σε μια αντικειμενοστραφή γλώσσα. Στη συνέχεια θα δούμε ποιές μεθόδους (ανά κατηγορία) μπροούμε να εφαρμόσουμε πάνω σε αυτές τις οντότητες.

Μέθοδοι συμβολοσειρών
Δημιουργία Έλεγχοι Πρόσβαση
String s = String('s');
int L = s.length();
int res = s1.compareTo(s2);
s1.equals(s2);
s1.equalsIgnoreCase(s2);
s1.startsWith("Hello");
s1.endsWith(".txt");
s1.reserve(100);
char c = s.charAt(12);
char * internalBuffer = s.c_str();
byte buf[100]; s1.getBytes(buf, 100);
char buf[100]; s1.toCharArray(buf, 100);
s2.setCharAt(whichPosition, 'X');
Τροποποίηση Αναζήτηση Τελεστές
s1.remove(posFrom, count);
s1.remove(posFrom);
s1.replace("this text","another content");
s1.substring(posFrom);
s1.substring(posFrom, count);
long X = s1.toInt();
float F = s1.toFloat();
s2.toLowerCase();
s2.toUpperCase();
s3.trim();
int pos = s1.indexOf(s2, afterThisPos);
int pos = s1.lastIndexOf(s2, beforeThisPos);
String s1 = s2 + s3;
s4 += s5;
s4.concat(s5);

Βλέπουμε στα παραπάνω λειτουργίες που εμφανίζονται στη C++ και άλλες ανώτερου επιπέδου γλώσσες, όπως είναι η υπερφόρτωση συναρτήσεων (δηλαδή την ίδια συνάρτηση να συντάσσεται με διαφορερικά ορίσματα), ή την υπερφόρτωση τελεστών (δηλαδή τη δυνατότητα να ορίσουμε την πρόσθεση μεταξύ συμβολοσειρών) όπως και την χρήση των μεθόδων πάνω στο αντικείμενο.

Ασκήσεις

Για εξάσκηση μπορείτε να δοκιμάσετε να γράψετε τα παρακάτω προγράμματα:

  • Επέκταση του παραδείγματος των πινάκων γράφοντας μία συνάρτηση που υπολογίζει το ίχνος ενός πίνακα.
  • Επέκταση του παραδείγματος των πινάκων ώστε να υπολογίζεται η διακρίνουσα του πίνακα. Η αναδρομή θα βοηθήσει για πιο ξεκάθαρο κώδικα εάν δημιουργήσετε μία συνάρτηση που επιλέγει (=δημιουργεί ως Matrix) τους κατάλληλους υποπίνακες.
  • Δημιουργήστε μία δομή Μαθητής και μία Τάξη και προσθέστε στην τάξη μαθητές με τυχαίους βαθμούς. Δημιουργήστε μια συνάρτηση που υπολογίζει τον μέσο όρο της τάξης και άλλη μία που να την παρουσιάζει με printf.
  • Δημιουργήστε μία συνάρτηση για ένα αρχείο η οποία θα επιστρέφει την επόμενη λέξη από ένα δεδομένο αρχείο. Ως λέξη θεωρούμε όπως σε παλιότερο παράδειγμα, μια ομάδα συνεχόμενων αλφαριθμητικών (όπως τα αντιλαμβάνεται η εντολή isalnum). Το ζητούμενο είναι αρκετά απλό αν χρησιμοποιήσετε την εντολή ungetc.
  • Δημιουργήστε μία συνάρτηση που να διαβάζει έναν αρχείο με 2 στήλες με ακέραιους αριθμούς σε δύο μονοδιάστατους πίνακες. Προσέξτε ότι δεν γνωρίζετε το πλήθος των γραμμών του αρχείου εκ των προτέρων ώστε να ξέρετε το μέγεθος του πίνακα. Πιθανώς θα σας βοηθήσει η εντολή fseek. Αποθηκεύστε τον πίνακα σε δυαδική μορφή σε ένα άλλο αρχείο. Παρατηρείστε τη διαφορά μεγέθους που έχει το αρχικό από το τελικό αρχειο. Πως συγκρίνετε το μέγεθος των δύο αρχείων σε σχέση με το μέγεθος του καθενός από αυτά συμπιεσμένο με μορφή .zip?