Skip to content

12. Ο προεπεξεργαστής της C

Σύνοψη Ο ρόλος του προεπεξεργαστή της C, οδηγίες προεπεξεργαστή, μακροεντολές, μακροεντολές με μεταβλητό πλήθος ορισμάτων, stringification, προκαθορισμένες μακροεντολές (__DATE__, __ΤΙΜΕ__ κ.ά.), οδηγίες υπό συνθήκη, φρουροί include,
το #if 0, οι οδηγίες #error, #pragma, #line.

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

12.1 Ο προεπεξεργαστής της C

Η C σε αντίθεση με τις περισσότερες γλώσσες προγραμματισμού διαθέτει προεπεξεργαστή (preprocessor). Ο προεπεξεργαστής της C είναι ένα πρόγραμμα που εκτελείται πριν τη μεταγλώττιση και αναλαμβάνει να επεξεργαστεί ειδικές εντολές που έχουν τη μορφή οδηγιών (directives) και που ξεκινούν με τον χαρακτήρα # στον πηγαίο κώδικα. Ο προεπεξεργαστής μετασχηματίζει τον πηγαίο κώδικα σε μια νέα μορφή κάνοντας αντικαταστάσεις τμημάτων του πηγαίου κώδικα. Αυτή η νέα μορφή κώδικα στη συνέχεια δίνεται στον μεταγλωττιστή που το μετατρέπει σε γλώσσα μηχανής, ακολουθώντας μια σειρά βημάτων.
Οι οδηγίες του προεπεξεργαστή της C ξεκινούν με το σύμβολο # που είναι γνωστό ως δίεση ή pound ή hash ή octothorpe. Οι κύριες οδηγίες είναι το #include και το #define, ενώ συχνά χρησιμοποιούνται και οι οδηγίες υπό συνθήκη #if, #else, #elif και #endif. Μια αναλυτική περι γραφή των δυνατοτήτων του προεπεξεργαστή της C για τον GCC υπάρχει στο “The C Preprocessor” από τους Richard M. Stallman και Zachary Weinberg 1.
Υπάρχουν και άλλες γλώσσες προγραμματισμού που διαθέτουν προεπεξεργαστή όπως η C++, η PL/I και η PHP, ενώ ορισμένες γλώσσες προγραμματισμού όπως η JavaScript χρησιμοποιούν προεπεξεργαστές για να προσθέσουν επιπλέον χαρακτηριστικά στη γλώσσα (π.χ. TypeScript). Στα συστήματα Linux/UNIX υπάρχει η γλώσσα προγραμματισμού M4 που ανήκει στο πρότυπο POSIX και αποτελεί έναν επεξεργαστή μακροεντολών γενικού σκοπού που επιτρέπει την απλοποίηση επαναλαμβανόμενων εργασιών. Η M4 χρησιμοποιείται εκτεταμένα στο build σύστημα GNU Autotools και βοηθά στον εντοπισμό των κατάλληλων ρυθμίσεων που επιτρέπουν την ορθή παραγωγή κώδικα σε συστήματα Linux/UNIX.

12.2 Η οδηγία include

Η οδηγία #include επιτρέπει τη συμπερίληψη των περιεχομένων ενός αρχείου κώδικα σε ένα άλλο αρχείο κώδικα στο οποίο έχει τοποθετηθεί η οδηγία include. Είναι συνηθισμένο να γίνονται include αρχεία επικεφαλίδων συστήματος όπως για παράδειγμα συμβαίνει με την οδηγία #include , οπότε και δίνεται η δυνατότητα χρήσης συναρτήσεων που δηλώνονται στο αρχείο επικεφαλίδας stdio.h όπως η συνάρτηση printf(). Κάθε οδηγία include ακολουθείται από ένα όνομα αρχείου και είτε χρησιμοποιούνται τα σύμβολα < και > για να το περικλείουν είτε τα διπλά εισαγωγικά ". Στην πρώτη περίπτωση το αρχείο αναζητείται σε καταλόγους που η εγκατάσταση της C έχει ορίσει ως καταλόγους συστήματος. Στη λίστα των καταλόγων συστήματος μπορούν να προστεθούν επιπλέον κατάλογοι με την επιλογή ‐I κατά τη μεταγλώττιση του πηγαίου κώδικα. Στην περίπτωση που το όνομα του αρχείου που πρόκειται να συμπεριληφθεί βρίσκεται μέσα σε διπλά εισαγωγικά τότε το αρχείο αναζητείται στον τρέχοντα κατάλογο. Στο παράδειγμα που ακολουθεί το αρχείο ch12_p1.h (κώδικας 12.1) περιέχει τη δήλωση της συνάρτησης foo(). To αρχείο αυτό γίνεται include στο αρχείο ch12_p1.c (κώδικας 12.2), οπότε τα περιεχόμενά του συμπεριλαμβάνονται στη θέση στην οποία υπάρχει η εντολή #include "ch12_p1.h".

Κώδικας 12.1: ch12_p1.h - αρχείο επικεφαλίδας που περιέχει τη δήλωση της συνάρτηση foo().
void foo(int a);
Κώδικας 12.2: ch12_p1.c - ο πηγαίος κώδικας που ορίζει τη συνάρτηση foo() και περιέχει τη main().
1
2
3
4
5
6
7
#include <stdio.h>

#include "ch12_p1.h"

int main(void) { foo(42); }

void foo(int a) { printf("foo function called with argument %d\n", a); }

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

$ gcc ch12_p1.c ‐o ch12_p1
$ ./ch12_p1
foo function called with argument 42
Αν στην οδηγία include έχουν συμπεριληφθεί και πληροφορίες υποκαταλόγων, όπως για παράδειγμα στο #include "myheaders/myheader.h" τότε το αρχείο myheader.h θα αναζητηθεί στον υποκατάλογο myheaders κάτω από τον τρέχοντα κατάλογο προκειμένου τα περιεχόμενά του να συμπεριληφθούν στο σημείο που γίνεται το include.

12.3 Η οδηγία define και η οδηγία undef

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

#define λεκτικό_σύμβολο κείμενο_αντικατάστασης

Αν το κείμενο αντικατάστασης καταλαμβάνει περισσότερες από μία γραμμές τότε τοποθετείται το σύμβολο backslash \ στο τέλος κάθε γραμμής που συνεχίζεται σε επόμενη γραμμή. Οι μακροεντολές χωρίζονται στις μακροεντολές τύπου αντικειμένου και στις μακροεντολές τύπου συνάρτησης. Οι πρώτες αντικαθιστούν απλά το λεκτικό σύμβολο με το κείμενο αντικατάστασης σε όλες τις εμφανίσεις του συμβόλου. Εξαιρούνται εμφανίσεις του συμβόλου μέσα σε διπλά εισαγωγικά, καθώς και εμφανίσεις του συμβόλου ως υποσυμβολοσειρά σε άλλο λεκτικό σύμβολο. Συνεπώς, στον κώδικα 12.3 που ακολουθεί γίνονται μόνο δύο αντικαταστάσεις του λεκτικού συμβόλου FOO, παρά το ότι η ακολουθία χαρακτήρων FOO υπάρχει τέσσερις φορές στον κώδικα.

Κώδικας 12.3: ch12_p2.c - δύο αντικαταστάσεις του λεκτικού συμβόλου FOO από τον προεπεξεργαστή.
1
2
3
4
5
6
7
#define FOO 1729

int main(void) {
  int foo = FOO;
  int FOOBAR = FOO;
  char *bar = "FOO and BAR are placeholder names in computer programming";
}

Χρησιμοποιώντας τον διακόπτη ‐E ο μεταγλωττιστής gcc (και ο clang) επιστρέφει τον πηγαίο κώδικα μετά την εφαρμογή των αντικαταστάσεων που προκαλεί ο προεπεξεργαστής.

$ gcc ‐E ch12_p2.c
int main(){
    int foo = 1729;
    int FOOBAR = 1729;
    char* bar = "FOO and BAR are placeholder names in computer programming";
}

Οι μακροεντολές τύπου συνάρτησης δέχονται ορίσματα με συνέπεια το κείμενο αντικατάστασης να αλλάζει ανάλογα με την κάθε εφαρμογή της μακροεντολής στα διάφορα σημεία του κώδικα. Στο παράδειγμα που ακολουθεί στον κώδικα 12.4 ορίζεται μια μακροεντολή με όνομα SQUARE που μπορεί να χρησιμοποιηθεί για τον υπολογισμό του τετραγώνου της παραμέτρου της. Παρατηρήστε τις επιπλέον παρενθέσεις γύρω από κάθε εμφάνιση της παραμέτρου x, καθώς και τις παρενθέσεις στο σύνολο του κειμένου αντικατάστασης. Η χρήση αυτών των παρενθέσεων είναι καλή πρακτική έτσι ώστε η σειρά των πράξεων να είναι η αναμενόμενη. Για παράδειγμα αν δεν υπήρχαν οι παρενθέσεις γύρω από κάθε εμφάνιση του x, τότε η αντικατάσταση της μακροεντολής στο SQUARE(1+2) θα έδινε το 1+2*1+2 που υπολογίζεται σε 5 και όχι στο τετράγωνο του 3, δηλαδή το 9, που είναι η ορθή τιμή αποτελέσματος.

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

#define SQUARE(x) ((x) * (x))

int main(void) {
  int a = 7;
  printf("%d\n", SQUARE(a));
  printf("%d\n", SQUARE(a + 1));
}
$ gcc ch12_p3.c ‐o ch12_p3
$ ./ch12_p3
49
64

Μια ιδιαιτερότητα της χρήσης μακροεντολών τύπου συναρτήσεων είναι ότι σε περιπτώσεις που ένα όρισμα είναι έκφραση και εντοπίζεται στο κείμενο αντικατάστασης περισσότερες από μία φορές, η έκφραση θα αποτιμηθεί και αυτή περισσότερες από μία φορές. Έτσι στο παράδειγμα με την μακροεντολή SQUARE, αν το όρισμα είναι a+1, τότε η αποτίμηση του αποτελέσματος γίνεται δύο φορές. Αυτό μπορεί σε κάποιες περιπτώσεις να εισάγει περιττούς υπολογισμούς, αλλά συνήθως είναι ένα θέμα που οι μεταγλωττιστές μπορούν να χειριστούν έτσι ώστε να μην παρατηρείται υποβάθμιση της απόδοσης του προγράμματος. Σημαντικότερο είναι το πρόβλημα που δημιουργείται όταν η έκφραση προκαλεί παρενέργειες (side-effects), όπως για παράδειγμα στην περίπτωση του SQUARE(a++). Λόγω του ότι η παράμετρος x χρησιμοποιείται 2 φορές στο κείμενο αντικατάστασης θα προκύψει το κείμενο αντικατάστασης (a++) * (a++) και συνεπώς η τιμή του ορίσματος a θα αυξηθεί 2 φορές κατά ένα, κάτι το οποίο δεν είναι προφανές σε κάποιον που δεν γνωρίζει την υλοποίηση της μακροεντολής. Για να αποφευχθεί αυτή η συμπεριφορά μπορεί η μακροεντολή να γραφεί μέσα σε ένα μπλοκ κώδικα όπως συμβαίνει στον κώδικα 12.5.

Κώδικας 12.5: ch12_p3b.c - αποφυγή πολλαπλής αποτίμησης ορίσματος σε μακροεντολή.
#include <stdio.h>

#define SQUARE(x)                                                              \
  ({                                                                           \
    typeof(x) _x = (x);                                                        \
    _x *_x;                                                                    \
  })

int main(void) {
  int a = 7;
  printf("%d\n", SQUARE(a));
  printf("%d\n", SQUARE(a + 1));
  printf("%d\n", SQUARE(a++));
  printf("%d\n", a);
}

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

12.3.1 Ο ρόλος των συμβόλων # και ## στο κείμενο αντικατάστασης των οδηγιών define

Τα ορίσματα μιας μακροεντολής μπορούν να μετατραπούν σε συμβολοσειρές τοποθετώντας το σύμβολο # πριν το όνομά τους στο κείμενο αντικατάστασης. Η μετατροπή ορισμάτων στα λεκτικά των ονομάτων τους, στις μακροεντολές, ονομάζεται stringification. Ένα σχετικό παράδειγμα παρουσιάζεται στον κώδικα 12.6.

Κώδικας 12.6: ch12_p4.c - παράδειγμα μακροεντολής που εκτελεί stringification.
#include <stdio.h>

#define STRINGIZE_INT(x) printf(#x " = %d\n", x)

int main(void) {
  int a = 42;

  STRINGIZE_INT(a);
  STRINGIZE_INT(a + 1);
}

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

$ gcc ch12_p4.c ‐o ch12_p4
$ ./ch12_p4
a = 42
a+1 = 43

Παρατηρήστε ότι στον κώδικα 12.6, στη δήλωση της μακροεντολής, χρησιμοποιείται το τέχνασμα συνένωσης δύο συμβολοσειρών που βρίσκονται η μια δίπλα στην άλλη (του #x και του " = %d\n"). Ισχύει δηλαδή ότι για τη C, το "HELLO" "WORLD" είναι ισοδύναμο με το "HELLOWORLD".
Ειδικό ρόλο έχουν και τα δύο συνεχόμενα σύμβολα δίεσης ## καθώς μπορούν να χρησιμοποιηθούν για το λεγόμενο pasting ή concatenation. Όταν αντικαθίσταται μια μακροεντολή που έχει στο κείμενο αντικατάστασής της το ## τότε τα δύο σύμβολα που βρίσκονται αριστερά και δεξιά από τα ## συνδυάζονται σε ένα σύμβολο. Στο παράδειγμα του κώδικα 12.7 ορίζεται η μακροεντολή CAT που συνενώνει τα δύο ορίσματά της και χρησιμοποιείται τόσο για τον ορισμό ονομάτων μεταβλητών όσο και για τη δημιουργία αριθμητικών τιμών με επιστημονικό συμβολισμό (1) (π.χ. 2e6).

  1. https://science.howstuffworks.com/math-concepts/scientific-notation.htm
Κώδικας 12.7: ch12_p5.c - παράδειγμα μακροεντολής που συνενώνει τα ορίσματά της.
#include <stdio.h>

#define CAT(x, y) x##y

int main(void) {
  int CAT(x, 0) = CAT(1, e6);
  double CAT(z, 99) = CAT(2, e6);
  printf("%d\n", x0);
  printf("%f\n", z99);
}

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

$ gcc ch12_p5.c ‐o ch12_p5
$ ./ch12_p5
1000000
2000000.000000

Μια πρακτική χρήση του ## παρουσιάζεται στον κώδικα 12.8 όπου η μακροεντολή DECLARE_WITH_COPY δηλώνει και ορίζει μια μεταβλητή και ταυτόχρονα δηλώνει και ορίζει ένα αντίγραφό της με το ίδιο όνομα αλλά με την προσθήκη του _copy στο τέλος του ονόματός της.

Κώδικας 12.8: ch12_p5b.c παράδειγμα μακροεντολής που δηλώνει και ορίζει μια μεταβλητή καθώς και ένα αντίγραφό της.
#include <stdio.h>

#define DECLARE_WITH_COPY(type, name, value)                                   \
  type name = value;                                                           \
  type name##_copy = name;

int main(void) {
  DECLARE_WITH_COPY(double, speed, 60);
  speed *= 1.2;
  printf("SPEED = %.2f\n", speed);
  printf("SPEED (original) = %.2f\n", speed_copy);
}

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

$ gcc ch12_p5b.c ‐o ch12_p5b
$ ./ch12_p5b
SPEED = 72.00
SPEED (original) = 60.00

12.3.2 Μακροεντολές με μεταβλητό πλήθος ορισμάτων

Υπάρχει η δυνατότητα ορισμού μακροεντολής που θα χρησιμοποιεί μεταβλητό πλήθος ορισμάτων. Αυτό γίνεται με τη χρήση των 3 τελειών ως τελευταίο όρισμα της μακροεντολής και του συμβόλου __VA_ARGS__ στο κείμενο αντικατάστασης. Ο κώδικας 12.9 αποτελεί ένα παράδειγμα χρήσης μεταβλητού πλήθους ορισμάτων σε μακροεντολή.

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

#define MACRO_VA_ARGS(x, ...) (x), __VA_ARGS__

int main(void) {
  printf("arg0=%d, arg1=%s\n", MACRO_VA_ARGS(42, "foo"));
  printf("arg0=%d, arg1=%s, arg2=%f\n", MACRO_VA_ARGS(42, "foo", 3.141592));
}

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

$ gcc ch12_p6.c ‐o ch12_p6
$ ./ch12_p6
arg0=42, arg1=foo
arg0=42, arg1=foo, arg2=3.141592

12.4 Προκαθορισμένες μακροεντολές

Το πρότυπο της C ορίζει ένα σύνολο από προκαθορισμένες (built-in) μακροεντολές που μπορούν να χρησιμοποιηθούν απευθείας. Ορισμένες από αυτές είναι οι ακόλουθες.

  • __DATE__, τρέχουσα ημερομηνία σε μορφή μήνας, ημέρα μήνα με 2 ψηφία, έτος με τέσσερα ψηφία (M dd yyyy).
  • __TIME__, τρέχουσα ώρα.
  • __FILE__, όνομα αρχείου στο οποίο βρίσκεται η μακρονεντολή.
  • __LINE__, αριθμός γραμμής που βρίσκεται η μακροεντολή.
  • __STDC__, τιμή 1 αν ο μεταγλωττιστής συμμορφώνεται με το ISO standard της C, αλλιώς τιμή 0.
  • __STDC_VERSION__, μια long ακέραια τιμή της μορφής yyyymmL, όπου yyyy και mm είναι το έτος και ο μήνας της έκδοσης του standard που υποστηρίζεται. Για παράδειγμα η τιμή 201710L υποδηλώνει την αναθεώρηση του 2017 του C standard. Οι νεότερες εκδόσεις είναι πάντα μεγαλύτεροι αριθμοί σε σχέση με τις παλαιότερες, (π.χ. 201710L > 201112L).

Ο κώδικας 12.10 περιέχει εντολές εκτύπωσης τιμών για τις παραπάνω προκαθορισμένες μακροεντολές.

Κώδικας 12.10: ch12_p7.c - εκτύπωση τιμών με προκαθορισμένες μακροεντολές.
#include <stdio.h>

int main(void) {
  printf("Macro __DATE__: %s\n", __DATE__);
  printf("Macro __TIME__: %s\n", __TIME__);
  printf("Macro __FILE__: %s\n", __FILE__);
  printf("Macro __LINE__: %d\n", __LINE__);
  printf("Macro __STDC__: %d\n", __STDC__);
  printf("Macro __STDC_VERSION__: %ld\n", __STDC_VERSION__);
}

Η εκτέλεση του κώδικα εμφανίζει τα ακόλουθα αποτελέσματα.

$ gcc ch12_p7.c ‐o ch12_p7
$ ./ch12_p7
Macro __DATE__: Apr 20 2023
Macro __TIME__: 13:26:29
Macro __FILE__: ch12_p7.c
Macro __LINE__: 7
Macro __STDC__: 1
Macro __STDC_VERSION__: 201710

12.5 Οδηγίες υπό συνθήκη

Μια οδηγία υπό συνθήκη δίνει τη δυνατότητα επιλογής σχετικά με το εάν ένα τμήμα κώδικα θα παραμείνει ή όχι στον κώδικα που θα περάσει στον μεταγλωττιστή. Έτσι μπορεί να επιτευχθεί επιλεκτική συμπερίληψη κώδικα σύμφωνα με συνθήκες που οι τιμές τους υπολογίζονται κατά την προεπεξεργασία. Η οδηγία #if ακολουθείται από μια παράσταση που εάν η τιμή της είναι μη μηδενική, θα συμπεριλάβει στον κώδικα όλες τις εντολές που ακολουθούν μέχρι το επόμενο #endif, #elif ή #else. Μια συχνή χρήση των οδηγιών προεπεξεργαστή υπό συνθήκη είναι όταν ένας κώδικας πρέπει να διαφοροποιείται ανάλογα με το λειτουργικό σύστημα ή την αρχιτεκτονική του υπολογιστή που θα εκτελεστεί. Στο ακόλουθο παράδειγμα, στον κώδικα 12.11, πραγματοποιείται καθαρισμός της οθόνης με διαφορετική εντολή ανάλογα με το εάν πρόκειται για Windows ή για Linux/UNIX ή MacOS λειτουργικό σύστημα.

Κώδικας 12.11: ch12_p8.c - «εκκαθάριση» οθόνης και εμφάνιση του ονόματος του λειτουργικού συστήματος στο οποίο εκτελείται το πρόγραμμα.
#include <stdio.h>
#include <stdlib.h>

int main(void) {
#if defined(_WIN32)
  system("cls");
  printf("Windows\n");
#elif defined(___APPLE__)
  system("clear");
  printf("Apple OS\n");
#elif defined(__linux__)
  system("clear");
  printf("Linux OS\n");
#else
  printf("Unknown OS\n");
#endif
}

Αν για παράδειγμα ο παραπάνω κώδικας εκτελεστεί σε υπολογιστή με Windows, θα «καθαρίσει» την οθόνη και θα εμφανίσει το κείμενο Windows. Παρατηρήστε ότι στον κώδικα χρησιμοποιήθηκε δεξιά από το #if το defined που αν το λεκτικό σύμβολο που το ακολουθεί έχει οριστεί επιστρέφει την τιμή 1, αλλιώς επιστρέφει την τιμή 0.

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

Κώδικας 12.12: ch12_p9.c - επιλεκτική συμπερίληψη τμημάτων κώδικα για μεταγλώττιση.
#include <stdio.h>
#include <stdlib.h>

#define DEBUG 1

int main(void) {
  int x = 0;
  int y = ++x;
#if DEBUG
  fprintf(stderr, "DEBUG: x=%d, y=%d\n", x, y);
#endif
  printf("bye");
}

Η εκτέλεση του παραπάνω κώδικα θα εμφανίσει.

$ gcc ch12_p9.c ‐o ch12_p9
$ ./ch12_p9
DEBUG: x=1, y=1
bye

Αν όμως στη γραμμή 4 η εντολή γίνει #define DEBUG 0 τότε κατά την εκτέλεση θα εμφανιστεί μόνο το κείμενο bye. Επίσης, αν διαγραφεί η εντολή στη γραμμή 4, τότε το λεκτικό σύμβολο DEBUG μπορεί να ορίζεται κατά τη μεταγλώττιση με τον διακόπτη ‐D όπως στη συνέχεια.

$ gcc ch12_p9.c ‐DDEBUG=1 ‐o ch12_p9
$ ./ch12_p9
DEBUG: x=1, y=1
bye
$ gcc ch12_p9.c ‐DDEBUG=0 ‐o ch12_p9
$ ./ch12_p9
bye

Ισοδύναμος κώδικας με το #if defined(FOO) είναι ο #if defined FOO και ο #ifdef FOO. Επίσης, υπάρχει η δυνατότητα να χρησιμοποιηθεί το #ifndef FOO που είναι ισοδύναμο με το #if !defined(FOO) και σημαίνει ότι θα συμπεριληφθούν οι εντολές που ακολουθούν μέχρι το #endif αν το λεκτικό σύμβολο FOO δεν έχει οριστεί στον κώδικα που έχει προηγηθεί.

12.5.1 Φρουροί include

Μια άλλη χρήση των οδηγιών υπό συνθήκη είναι στους λεγόμενους φρουρούς include (include guards). Συνήθως οι δηλώσεις ενός κώδικα απομονώνονται σε ένα αρχείο επικεφαλίδων με κατάληξη .h (π.χ. utility.h) και η υλοποίηση σε άλλο αρχείο με το ίδιο όνομα με κατάληξη .c (π.χ. utility.c). Το πρόγραμμα οδηγός (π.χ. main.c) ή οποιοσδήποτε άλλος κώδικας απαιτεί γνώση των δηλώσεων που περιέχει το utility.h, θα πρέπει να το κάνει include (ή να κάνει include ένα άλλο αρχείο που με τη σειρά του κάνει include το utility.h). Στο απλό παράδειγμα που ακολουθεί χρησιμοποιούνται 3 αρχεία, το ch12_p10_utility.h (κώδικας 12.13), το ch12_p10_utility.c (κώδικας 12.14) και το ch12_p10_main.c (κώδικας 12.15). Στο πρώτο αρχείο δηλώνεται η συνάρτηση add_numbers, στο δεύτερο αρχείο ορίζεται η συνάρτηση και στο τελευταίο η συνάρτηση καλείται από τη main().

Κώδικας 12.13: ch12_p10_utility.h - αρχείο επικεφαλίδας με δήλωση συνάρτησης και include guards.
1
2
3
4
5
6
#ifndef CH12_P10_UTILITY_H
#define CH12_P10_UTILITY_H

int add_numbers(int a, int b);

#endif
Στο αρχείο επικεφαλίδας παρατηρείται το μοτίβο.

#ifndef CH12_P10_UTILITY_H
#define CH12_P10_UTILITY_H
...
#endif

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

Κώδικας 12.14: ch12_p10_utility.c - αρχείο πηγαίου κώδικα με τον ορισμό της συνάρτησης add_numbers().
1
2
3
#include "ch12_p10_utility.h"

int add_numbers(int a, int b) { return a + b; }
Κώδικας 12.15: ch12_p10_main.c - αρχείο πηγαίου κώδικα που περιέχει τη main() και χρησιμοποιεί τη συνάρτηση add_numbers() που δηλώνεται στο αρχείο επικεφαλίδας ch2_p10_utility.h
1
2
3
4
5
6
7
8
#include "ch12_p10_utility.h"
#include <stdio.h>

int main(void) {
  int result = add_numbers(2, 3);
  printf("The result is %d\n", result);
  return 0;
}

Η μεταγλώττιση των δύο αρχείων πηγαίου κώδικα και η εκτέλεση του εκτελέσιμου που προκύπτει γίνεται όπως στη συνέχεια.

$ gcc ch12_p10_utility.c ch12_p10_main.c ‐o ch12_p10_main
$ ./ch12_p10_main
The result is 5

12.5.2 Κώδικας σε σχόλια με #if 0

Για να τοποθετηθούν σε σχόλια πολλές συνεχόμενες γραμμές κώδικα στη C, χρησιμοποιείται ο συνδυασμός συμβόλων /* και */ που σηματοδοτούν την αρχή και το τέλος των γραμμών που θα αποτελέσουν σχόλια. Όμως, αυτός ο τρόπος δεν επιτρέπει εμφωλευμένα σχόλια, δηλαδή να τοποθετηθεί σε σχόλια ένας κώδικας που περιέχει ήδη σχόλια. Δηλαδή δεν επιτρέπεται κάτι όπως το ακόλουθο:

/*
...
        /*
        ...
        */
...
*/

Ωστόσο, μπορεί να χρησιμοποιηθεί η οδηγία υπό συνθήκη #if 0 έτσι ώστε να απενεργοποιηθεί ένα τμήμα κώδικα και να επιτευχθεί επί της ουσίας το επιθυμητό αποτέλεσμα του σχολιασμού κώδικα που περιέχει μέσα του σχόλια /* ... */. Ένα σχετικό παράδειγμα παρουσιάζεται στον κώδικα 12.16.

Κώδικας 12.16: ch12_p11.c - χρήση της #if 0 για να οριστεί ένα εξωτερικό επίπεδο σχολίων μέσα στο οποίο μπορεί να υπάρχουν εμφωλευμένα σχόλια της μορφής /* ... */.
#include <stdio.h>

int main(void) {
  int a = 5, b = 10;

#if 0
    printf("Swap")
    /*
    a ^= b;
    b ^= a;
    a ^= b;
    */
#endif

  printf("a=%d, b=%d\n", a, b);
}

Η εκτέλεση του προγράμματος δεν πρόκειται να εμφανίσει το μήνυμα Swap, ούτε και να εκτελέσει τις εντολές από τη γραμμή 9 μέχρι και τη γραμμή 11 (1) που έτσι και αλλιώς βρίσκονται μέσα σε σχόλια. Αν στη γραμμή 6 αντικατασταθεί το 0 με 1, τότε πλέον κατά την εκτέλεση θα εμφανίζεται το μήνυμα Swap, αλλά πάλι δεν θα εκτελούνται οι εντολές από τη γραμμή 9 μέχρι και γραμμή 11, καθώς θα βρίσκονται εντός σχολίων.

  1. Οι εντολές αυτές πραγματοποιούν αντιμετάθεση των ακεραίων μεταβλητών a και b, με χρήση του τελεστή αποκλειστικής διάζευξης ^

12.6 Άλλες οδηγίες

Πέρα από τις οδηγίες που αναφέρθηκαν υπάρχουν και άλλες οδηγίες όπως η #error, η #pragma και η #line, ενώ υπάρχουν και άλλες οδηγίες που δεν ανήκουν στο standard της C και που υποστηρίζονται από συγκεκριμένους μεταγλωττιστές και μόνο.

12.6.1 Η οδηγία #error

Η οδηγία #error προκαλεί την εμφάνιση ενός λάθους και τον τερματισμό της μεταγλώττισης. Στον κώδικα 12.17 προκαλείται σφάλμα μεταγλώττισης σε περίπτωση που η μεταγλώττιση γίνεται σε λειτουργικό σύστημα που δεν είναι Linux/UNIX.

Κώδικας 12.17: ch12_p12.c - πρόκληση σφάλματος μεταγλώττισης αν το σύστημα στο οποίο γίνεται η μεταγλώττιση δεν είναι Linux/UNIX.
1
2
3
4
5
6
#include <stdio.h>
#ifndef __linux__
#error "works only in linux!"
#endif

int main(void) { printf("This message should appear in Linux only"); }

Αν επιχειρηθεί μεταγλώττιση σε Windows υπολογιστή τότε εμφανίζεται το ακόλουθο σφάλμα μεταγλώττισης:

$ gcc ch12_p12.c ‐o ch12_p12
ch12_p12.c:3:2: error: #error "works only in linux!"
    3 | #error "works only in linux!"
      | ^~~~~

12.6.2 Η οδηγία #pragma

Η οδηγία #pragma συνήθως χρησιμοποιείται σύμφωνα με τις προδιαγραφές που ορίζουν εξωτερικές βιβλιοθήκες. Για παράδειγμα η βιβλιοθήκη OpenMP (1) ορίζει ότι ένας βρόχος επανάληψης θα εκτελεστεί παράλληλα με οδηγίες pragma τοποθετημένες σε κατά τα άλλα συνηθισμένο κώδικα C. Ο κώδικας 12.18 που ακολουθεί ορίζει ότι ο βρόχος επανάληψης της εντολής for θα εκτελεστεί από 2 νήματα (threads) παράλληλα, οπότε οι μισές επαναλήψεις θα γίνουν από το κάθε νήμα.

  1. https://www.openmp.org/
Κώδικας 12.18: ch12_p13.c - κώδικας που χρησιμοποιεί 2 νήματα, με χρήση της βιβλιοθήκης OpenMP.
1
2
3
4
5
6
7
8
9
#include <omp.h>
#include <stdio.h>

int main(void) {
#pragma omp parallel for num_threads(2)
  for (int i = 0; i < 6; i++) {
    printf("i = %d, thread %d\n", i, omp_get_thread_num());
  }
}

Για να μπορεί να δημιουργηθεί το εκτελέσιμο πρόγραμμα για τον παραπάνω κώδικα θα πρέπει ο μεταγλωττιστής να υποστηρίζει το OpenMP και η μεταγλώττιση να γίνει χρησιμοποιώντας τον διακόπτη ‐fopenmp.

$ gcc ch12_p13.c ‐o ch12_p13 ‐fopenmp
$ ./ch12_p13
i = 0, thread 0
i = 1, thread 0
i = 2, thread 0
i = 3, thread 1
i = 4, thread 1
i = 5, thread 1

Μια κοινή χρήση της οδηγίας pragma είναι το #pragma once που δεν ανήκει μεν στο standard της C, αλλά υποστηρίζεται από τους πλέον διαδεδομένους μεταγλωττιστές της C (gcc, clang κ.ά.) και που μπορεί να χρησιμοποιηθεί ως εναλλακτική υλοποίηση για τους φρουρούς include που αναφέρθηκαν στην παράγραφο 12.5.1. Έτσι αντί να υπάρχει στην αρχή και στο τέλος του κάθε αρχείου επικεφαλίδων όπως το hdr.h το

#ifndef HDR_H
#define HDR_H
...
#endif
αρκεί να υπάρχει στην αρχή του μόνο το #pragma once.

12.6.3 Η οδηγία #line

Η οδηγία #line επιτρέπει την αλλαγή των τιμών που αναφέρει ο μεταγλωττιστής σχετικά με τον αριθμό γραμμής και το όνομα του αρχείου πηγαίου κώδικα που εκτελείται. Πρόκειται για τις τιμές που μπορούν να εμφανίζονται σε διαγνωστικά μηνύματα με τις μακροεντολές __FILE__ και __LINE__. Στη συνέχεια παρατίθεται ένα παράδειγμα χρήσης της #line στον κώδικα 12.19, όπου σε συγκεκριμένες θέσεις του κώδικα επαναορίζεται ο αριθμός γραμμής του κώδικα ή και το όνομα του αρχείου.

Κώδικας 12.19: ch12_p14.c - χρήση του #line για επανορισμό αριθμού γραμμών κώδικα και ονόματος αρχείου πηγαίου κώδικα κατά την εκτέλεση του προγράμματος.
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
  printf("Tracing file %s at line %d\n", __FILE__, __LINE__);
#line 100
  printf("Tracing file %s at line %d\n", __FILE__, __LINE__);
#line 1 "dummy.c"
  printf("Tracing file %s at line %d\n", __FILE__, __LINE__);
}

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

$ gcc ch12_p14.c ‐o ch12_p14
$ ./ch12_p14
Tracing file ch12_p14.c at line 4
Tracing file ch12_p14.c at line 100
Tracing file dummy.c at line 1

12.7 Είναι τελικά χρήσιμος ο προεπεξεργαστής της C;

O προεπεξεργαστής της C μπορεί να είναι η αιτία πρόκλησης προβλημάτων με τα οποία έρχονται αντιμέτωποι οι προγραμματιστές 2. Τέτοια προβλήματα είναι η πιθανότητα εισαγωγής σφαλμάτων στον κώδικα, η δημιουργία παραπλανητικού ή δυσνόητου κώδικα που μειώνει την αναγνωσιμότητά του και προκαλούνται σε μεγάλο βαθμό από την έλλειψη αντίληψης της σημασιολογίας της γλώσσας C από τον προεπεξεργαστή. Ωστόσο, οι προγραμματιστές της C, εμπιστεύονται και χρησιμοποιούν τον προεπεξεργαστή καθώς επιτελεί αποτελεσματικά το έργο για το οποίο είναι υπεύθυνος. Για να αποφευχθούν προβλήματα συχνά ακολουθούνται κατευθυντήριες γραμμές (guidelines) στο πνεύμα των αρχών που έχει αποτυπώσει ο David Kieras στο “C Header File Guidelines” 3.

12.8 Ασκήσεις

Άσκηση 1 Γράψτε μακροεντολή με όνομα SWAP που να πραγματοποιεί αντιμετάθεση τιμών στα δύο ορίσματα που θα δέχεται. Εφαρμόστε τη μακροεντολή για αντιμετάθεση δύο ακεραίων τύπου int.

Λύση άσκησης 1
1
2
3
4
5
6
7
8
#include <stdio.h>
#define SWAP(a,b)({a ^= b; b ^= a; a ^= b;})

int main(void) {
  int x = 5, y = 10;
  SWAP(x, y);
  printf("x=%d, y=%d\n", x, y);
}

Άσκηση 2
Γράψτε μακροεντολή με όνομα FOREVER που να υλοποιεί έναν άπειρο βρόχο. Καλέστε την μακροεντολή από κύριο πρόγραμμα.

Λύση άσκησης 2
#include <stdio.h>
#define FOREVER for (;;)

int main(void) {
  int i = 0;
  FOREVER {
    printf("Loop!\n");
    i++;
    if (i == 100)
      break;
  }
}

Άσκηση 3
Γράψτε μακροεντολή με όνομα MALLOC που να δέχεται 2 ορίσματα t και n και να δεσμεύει n θέσεις τύπου t και να επιστρέφει έναν δείκτη τύπου t προς τις θέσεις μνήμης που δεσμεύτηκαν. Αν η δέσμευση μνήμης αποτυγχάνει να εμφανίζει μήνυμα σφάλματος.

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

#define MALLOC(p, t, n) (t *)malloc(sizeof(t) * n);

int main(void) {
  int *a = MALLOC(a, int, 10);
  for (int i = 0; i < 10; i++)
    a[i] = 1;
  free(a);
}

Άσκηση 4
Γράψτε μια μακροεντολή με όνομα FOPEN που να δέχεται 2 ορίσματα fn, m και να ανοίγει ένα αρχείο με όνομα την τιμή του fn σε κατάσταση ανάγνωσης ή εγγραφής, σύμφωνα με την τιμή του m ('r' ή 'w') και να πραγματοποιεί έλεγχο σφαλμάτων. Γράψτε πρόγραμμα που με τη χρήση της FOPEN να διαβάζει να αρχείο κειμένου (π.χ. quotes.txt) και να εμφανίζει τα περιεχόμενά του στη γραμμή εντολών.

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

#define FOPEN(filename, mode)                                                  \
  ({                                                                           \
    FILE *file = fopen(filename, mode);                                        \
    if (file == NULL) {                                                        \
      fprintf(stderr, "Error opening file %s\n", filename);                    \
      exit(EXIT_FAILURE);                                                      \
    }                                                                          \
    file;                                                                      \
  })

int main(void) {
  FILE *file = FOPEN("quotes.txt", "r");
  char buffer[1024];
  while (fgets(buffer, sizeof(buffer), file)) {
    printf("%s", buffer);
  }
  fclose(file);
  return 0;
}

Άσκηση 5
Γράψτε μια μακροεντολή με όνομα IS_GCC που να μπορεί να χρησιμοποιηθεί για να εξεταστεί αν ο μεταγλωττιστής είναι ο GCC. Λάβετε υπόψη ότι ο GCC ορίζει το σύμβολο __GNUC__, αλλά το ίδιο σύμβολο ορίζεται και από τον μεταγλωττιστή clang που όμως ορίζει επιπλέον το σύμβολο __clang__.

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

#define IS_GCC (defined(__GNUC__) && !defined(__clang__))

int main(void) {
#if IS_GCC
  printf("GCC detected");
#else
  printf("GCC is not detected");
#endif
}

  1. Richard M Stallman και Zachary Weinberg. “The C preprocessor: For GCC version 14.0.0 (prerelease)”. Στο: Free Software Foundation (2023). [Online; accessed 2022-05-05], σ. 86. 

  2. Flávio Medeiros, Christian Kästner, Márcio Ribeiro, Sarah Nadi και Rohit Gheyi. “The love/hate relationship with the C preprocessor: An interview study”. Στο: 29th European Conference on Object-Oriented Programming (ECOOP 2015). Schloss Dagstuhl-Leibniz-Zentrum fuer Informatik. 2015. 

  3. David Kieras. C Header File Guidelines. https://websites.umich.edu/~eecs381/handouts/CHeaderFileGuidelines.pdf. [Online; accessed 2022-05-05]. EECS Dept., University of Michigan, 2012.