Skip to content

2. Εισαγωγικές έννοιες στον προγραμματισμό με τη C

2.1 Εισαγωγή

Η C είναι μια μικρή γλώσσα και αυτό αποτελεί μια στρατηγική σχεδιαστική απόφαση των δημιουργών της. Σε αυτό το πνεύμα οι δεσμευμένες λέξεις στη C είναι σχετικά λίγες. Οι βασικές δεσμευμένες λέξεις περιλαμβάνονται στον Πίνακα 2.1(1).

  1. Στη C υπάρχουν και άλλες δεσμευμένες λέξεις που έχουν προστεθεί κατά την εξέλιξη των προτύπων της γλώσσας (π.χ.inline και restrict στη C99, _StaticAssert στη C11 κ.ά.)

auto

double

int

struct

break

else

long

switch

case

enum

register

typedef

char

extern

return

union

const

float

short

unsigned

continue

for

signed

void

default

goto

sizeof

volatile

do

if

static

while

Βασικό συστατικό τμήμα κάθε προγράμματος στη C είναι η συνάρτηση main() που λειτουργεί ως σημείο εισόδου (entry point) για την εκτέλεση του προγράμματος. Η συνάρτηση main() συνήθως γράφεται όπως στη συνέχεια.

int main(void) {
    // Ο κώδικας του προγράμματος τοποθετείται εδώ
    return 0;
}
To void εντός των παρενθέσεων της main(), στη γραμμή 1, υποδηλώνει ότι η main() δεν δέχεται παραμέτρους. Ωστόσο, η main() μπορεί να γραφεί και ως int main(int argc, char *argv[]) έτσι ώστε να δέχεται ορίσματα γραμμής εντολών όπως θα δούμε στο Κεφάλαιο 6. Το σώμα της συνάρτησης main(), δηλαδή ότι περιέχεται ανάμεσα στις αγκύλες μετά το main(), περιέχει τον κώδικα που θα εκτελεστεί, άρα εκείκαθορίζεται η ροή εκτέλεσης του προγράμματος. Στη γραμμή 2 του κώδικα υπάρχει ένα σχόλιο που η αρχήτου υποδηλώνεται με το //. Η εντολή return στο τέλος της main() χρησιμοποιείται για να καθορίσει την κατάσταση του προγράμματος κατά την έξοδο, δηλαδή αν το πρόγραμμα επιτέλεσε σωστά τις λειτουργίες του ή αν προκλήθηκε κατά την εκτέλεση κάποια «μη φυσιολογική» κατάσταση. Η επιστροφή της τιμής 0 συμβατικά υποδηλώνει την επιτυχή εκτέλεση, ενώ μη μηδενικές τιμές υποδηλώνουν ότι το πρόγραμμα τερματίστηκε ανώμαλα.

2.1.1 Εισαγωγή

Μέσα στον κώδικα μπορούν να προστίθενται επεξηγηματικά σχόλια που θα αγνοηθούν κατά τη μεταγλώττιση και εκτέλεση του προγράμματος. Τα σχόλια μπορούν να τεκμηριώνουν τον σκοπό, τη λειτουργικότητα ή το σκεπτικό με το οποίο έχει γραφεί ο κώδικας. Εφόσον πράγματι κομίζουν κάποια χρήσιμη πληροφορία, διευκολύνουν την κατανόηση και συντήρηση του κώδικα από τον ίδιο τον προγραμματιστή ή από άλλους προγραμματιστές. Στη C υπάρχουν δύο είδη σχολίων, τα σχόλια μιας γραμμής και τα σχόλια πολλαπλών γραμμών. Όπως ήδη αναφέρθηκε, τα σχόλια μιας γραμμής ξεκινούν με το // και συνεχίζουν μέχρι το τέλος της γραμμής. Τα σχόλια πολλαπλών γραμμών περικλείονται ανάμεσα σε / και / όπως στο ακόλουθο παράδειγμα.

/* Αυτό είναι 
ένα σχόλιο
πολλαπλών γραμμών */  

2.2 Τύποι δεδομένων και μεταβλητές

Μια μεταβλητή είναι μια περιοχή συνεχόμενων θέσεων στη μνήμη του υπολογιστή στην οποία αποδίδεται από τον προγραμματιστή ένα όνομα. Κατά τη διάρκεια εκτέλεσης ενός προγράμματος οι μεταβλητές αποθηκεύουν δεδομένα που τροποποιούνται έτσι ώστε να εξυπηρετούν την επιδιωκόμενη κατά περίπτωση αλγοριθμική συμπεριφορά. Τυπικά, μια μεταβλητή αποτελείται από όνομα, διεύθυνση μνήμης, τύπο δεδομένων, τιμή, διάρκεια ζωής (life cycle) και εμβέλεια (scope). Το όνομα είναι το αναγνωριστικό της μεταβλητής που χρησιμοποιεί ο προγραμματιστής για να αναφερθεί σε αυτή. Η διεύθυνση της μεταβλητής είναι η διεύθυνση μνήμης όπου βρίσκονται τα περιεχόμενα της μεταβλητής. Ο τύπος δεδομένων της (π.χ. ακέραιος, πραγματικός κ.λπ.) καθορίζει το είδος δεδομένων που μπορεί να αποθηκεύσει και τις λειτουργίες που μπορούν να εκτελεστούν σε αυτά τα δεδομένα. Η τιμή της μεταβλητής έχει να κάνει με την ερμηνεία των δυαδικών ψηφίων που υπάρχουν στις θέσεις μνήμης που καταλαμβάνει η μεταβλητή στη μνήμη και εξαρτάται από τον τύπο δεδομένων της. Η διάρκεια ζωής της είναι η χρονική περίοδος που η μεταβλητή υφίσταται στη μνήμη και διατηρεί την τιμή της. Τέλος, η εμβέλεια αναφέρεται στο τμήμα του προγράμματος που η μεταβλητή είναι προσπελάσιμη και μπορεί να χρησιμοποιηθεί. Η δήλωση μιας μεταβλητής έχει την ακόλουθη μορφή:

τύπος_δεδομένων όνομα_μεταβλητής;

2.2.1 Τύποι δεδομένων στη C

Οι βασικοί τύποι δεδομένων στην C είναι ο char, ο int, ο float και ο double, όπως φαίνεται και στον Πίνακα 2.2. Επιπλέον, υπάρχουν οι προσδιοριστές (qualifiers) short (ακέραια αριθμητική τιμή μικρού εύρους), long (αριθμητική τιμή μεγάλου εύρους), signed (προσημασμένος) και unsigned (μη προσημασμένος, περιέχει μόνο μη αρνητικές τιμές) που μπορούν να εφαρμοστούν σε ορισμένους, κατά περίπτωση, βασικούς τύπους και να αλλάξουν το μέγεθος που καταλαμβάνουν στη μνήμη και συνεπώς και το εύρος τιμών που μπορούν να αναπαραστήσουν ή και την ακρίβεια αν πρόκειται για πραγματικούς αριθμούς. Για παράδειγμα ο προσδιοριστής short μπορεί να εφαρμοστεί μόνο σε ακεραίους και εγγυάται μέγεθος τουλάχιστον 2 bytes = 16 bits, άρα καλύπτει εύρος ακεραίων τιμών τουλάχιστον
από −215 = −32768 έως 215 − 1 = 32767. Για να δηλωθεί μια μεταβλητή x ως τύπου δεδομένων short αυτό μπορεί να γίνει ως signed short int x; ή ως short int x; ή ως short x; με το τελευταίο και συντομότερο να προτιμάται.

Πίνακας 2.2: Οι βασικοί τύποι δεδομένων της C.
Βασικός τύπος δεδομένων Περιγραφή
char χαρακτήρας, μπορεί να χρησιμοποιηθεί και ως μικρός ακέραιος
int ακέραιος αριθμός
float πραγματικός αριθμός κινητής υποδιαστολής απλής ακρίβειας
double πραγματικός αριθμός κινητής υποδιαστολής εκτεταμένης ακρίβειας

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

Στη συνέχεια παρουσιάζονται δύο παραδείγματα εκτέλεσης, ένα σε υπολογιστή 64 bits, με λειτουργικό σύστημα Linux και μεταγλώττιση με τον GCC 13.1.0 και ένα σε σύστημα 32 bits με λειτουργικό σύστημα Linux και μεταγλώττιση με μια σχετικά παλιά έκδοση του clang την 3.8.0 (κυκλοφόρησε το 2016). Τα αποτελέσματα είναι διαφορετικά για τους long αριθμούς, τόσο τους signed όσο και τους unsigned, καθώς στη μια περίπτωση το μέγεθος είναι 8 bytes ενώ στην άλλη είναι 4 bytes. Επίσης στους long double, αν και το εύρος τους είναι ίδιο, το μέγεθος είναι 16 bytes στην πρώτη περίπτωση αλλά μόνο 12 bytes στη δεύτερη περίπτωση. Οι διαφορές θα ήταν περισσότερες αν η σύγκριση αφορούσε ένα σύστημα 16 bits. Για παράδειγμα οι μεταγλωττιστές της C για συστήματα 16 bits συνήθως θεωρούν ότι οι τιμές τύπου int καταλαμβάνουν 2 bytes, γεγονός που περιορίζει το εύρος τους από -32768 μέχρι και 32767. Υπάρχουν πολλά τέτοια συστήματα στον χώρο των ενσωματωμένων συστημάτων όπως είναι o μικροελεγκτής MSP430 της Texas Instruments που έχει αρχιτεκτονική 16 bit.

GCC 13.1.0 detected
Platform: 64 bits
Data Type(Storage Size)                 Value Range
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
char (1 byte)                           ‐128 to 127
unsigned char (1 byte)                  0 to 255
signed char (1 byte)                    ‐128 to 127
int (4 bytes)                           ‐2147483648 to 2147483647
unsigned int (4 bytes)                  0 to 4294967295
short (2 bytes)                         ‐32768 to 32767
unsigned short (2 bytes)                0 to 65535
long (8 bytes)                          ‐9223372036854775808 to 9223372036854775807
unsigned long (8 bytes)                 0 to 18446744073709551615
long long 8 bytes                       ‐9223372036854775808 to 9223372036854775807
unsigned long long (8 bytes)            0 to 18446744073709551615
float (4 bytes)                         1.175494e‐38 to 3.402823e+38
double (8 bytes)                        2.225074e‐308 to 1.797693e+308
long double (16 bytes)                  3.362103e‐4932 to 1.189731e+4932
Linux 64 bits, GCC 13.1.0 με ημερομηνία κυκλοφορίας την 26/4/2023.
clang 3.8.0 detected
Platform: 32 bits
Data Type(Storage Size)                 Value Range
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
char (1 byte)                           ‐128 to 127
unsigned char (1 byte)                   0 to 255
signed char (1 byte)                    ‐128 to 127
int (4 bytes)                           ‐2147483648 to 2147483647
unsigned int (4 bytes)                   0 to 4294967295
short (2 bytes)                         ‐32768 to 32767
unsigned short (2 bytes)                 0 to 65535
long ( 4 bytes )                        ‐2147483648 to 2147483647
unsigned long ( 4 bytes )                0 to 4294967295
long long (8 bytes)                     ‐9223372036854775808 to 9223372036854775807
unsigned long long (8 bytes)             0 to 18446744073709551615
float (4 bytes)                          1.175494e‐38 to 3.402823e+38
double (8 bytes)                         2.225074e‐308 to 1.797693e+308
long double ( 12 bytes )                 3.362103e‐4932 to 1.189731e+4932
Linux 32 bits, clang 3.8.0 με ημερομηνία κυκλοφορίας την 8/3/2016.

Για την εκτύπωση των παραπάνω τιμών χρησιμοποιείται o τελεστής sizeof για το μέγεθος που καταλαμβάνει σε μνήμη η κάθε τιμή, ενώ για τα εύρη τιμών των τύπων δεδομένων χρησιμοποιούνται σταθερές όπως η INT_MAX (μέγιστη τιμή για τον ακέραιο τύπο δεδομένων) που ορίζονται στις επικεφαλίδες limits.h και float.h. O σύντομος κώδικας 2.1 που ακολουθεί, εμφανίζει τιμές μόνο για τους τύπους δεδομένων int και double.

ch2_p1.c
1
2
3
4
5
6
7
8
9
#include <float.h>
#include <limits.h>
#include <stdio.h>

int main(void) {
  printf("int (%zu bytes)\t\t%d to %d\n", sizeof(int), INT_MIN, INT_MAX);
  printf("double (%zu bytes)\t%e to %e\n", sizeof(double), DBL_MIN, DBL_MAX);
  return 0;
}
Κώδικας 2.1: ch2_p1.c - εμφάνιση μεγέθους σε bytes και εύρους τιμών για μεταβλητές τύπου δεδομένων int και τύπου δεδομένων double.

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

int (4 bytes)           ‐2147483648 to 2147483647
double (8 bytes)         2.225074e‐308 to 1.797693e+308

Οι εντολές στις γραμμές 6 και 7 του κώδικα θα εξηγηθούν στη συνέχεια του κεφαλαίου.

Φορητοί τύποι δεδομένων Καθώς βασικοί τύποι δεδομένων όπως ο int μπορεί να έχουν διαφορετικό μέγεθος και εύρος τιμών ανάλογα με τον μεταγλωττιστή και την αρχιτεκτονική του συστήματος, προέκυψε η ανάγκη για φορητούς τύπους δεδομένων, δηλαδή τύπους που η συμπεριφορά τους να είναι η ίδια ανεξάρτητα από μεταγλωττιστή και υπολογιστικό σύστημα. Έτσι, στο πρότυπο C99 εισήχθησαν για τους ακεραίους νέοι τύποι όπως ο uint32_t που σημαίνει ότι πρόκειται για μη προσημασμένο (unsigned) ακέραιο των 32 bits, οπότε είναι εγγυημένο ότι το εύρος τιμών του είναι από 0 έως και 232 − 1 = 4294967295. Ο τύπος uint32_t και οι αντίστοιχοι τύποι για 8, 16 και 64 bits ορίζονται στο stdint.h μαζί και με άλλους τύπους δεδομένων όπως για παράδειγμα τύπους που δίνουν έμφαση στην ταχύτητα εκτέλεσης (π.χ. int_fast32_t) ή στο μέγεθος που καταλαμβάνουν στη μνήμη (π.χ. int_least64_t).

Ένας ακόμα φορητός τύπος δεδομένων είναι ο size_t που ορίζεται στην stdef.h, αλλά και σε άλλες επικεφαλίδες (π.χ. στην stdlib.h), και εγγυάται ότι μπορεί να αναπαραστήσει το μέγεθος του μεγαλύτερου αντικειμένου στη μνήμη που υποστηρίζεται από την υλοποίηση. Πρόκειται για τον τύπο επιστροφής του τελεστή sizeof που θα συναντήσουμε και στη συνέχεια. Οι τιμές του size_t είναι πάντα μη αρνητικές και χρησιμοποιείται συχνά ως τύπος δεδομένων μεταβλητών επανάληψης, αλλά και για την αποθήκευση αποτελεσμάτων που είναι μη αρνητικά (π.χ. μήκος ενός λεκτικού).

2.3 Σταθερές

Τα προγράμματα C συνήθως περιέχουν κάποιες σταθερές τιμές. Κάθε σταθερή τιμή έχει τύπο δεδομένων και υπάρχουν ειδικοί συμβολισμοί για τη διευκρίνιση του τύπου δεδομένων που επιθυμούμε να αποδώσουμε. Έτσι:

  • Οι χαρακτήρες τοποθετούνται σε απλά εισαγωγικά, π.χ. 'a'.
  • Οι τιμές unsigned υποδηλώνονται με το επίθεμα u ή το U, π.χ. 42U.
  • Οι τιμές long έχουν ως επίθεμα το l ή το L, π.χ. 120L.
  • Οι τιμές long long έχουν ως επίθεμα το ll ή το LL, π.χ. 120LL.
  • Οι τιμές unsigned long και unsigned long long γράφονται με ul (ή με UL) και με ull (ή με ULL) αντίστοιχα, π.χ. 42ULL.
  • Οι τιμές τύπου float έχουν ως επίθεμα το f ή το F, π.χ. 3.14159F.

Στη C υπάρχουν και οι «μεταβλητές μόνο για ανάγνωση» που λαμβάνουν μια αρχική τιμή που δεν επιτρέπεται να αλλάξει στη συνέχεια του προγράμματος. Αυτό γίνεται με τη δεσμευμένη λέξη const. Για παράδειγμα:

const float PI=3.14159; // μεταβλητή μόνο για ανάγνωση

Εναλλακτικά προς τις μεταβλητές μόνο για ανάγνωση μπορούν να οριστούν οι λεγόμενες συμβολικές σταθερές που ορίζονται με την οδηγία define όπως στο ακόλουθο παράδειγμα.

#define PI 3.14159 // συμβολική σταθερά

Παρατηρήστε ότι δεν υπάρχει ; στο τέλος της #define PI 3.14159. Η define είναι οδηγία προς τον προεπεξεργαστή, ένα τμήμα του μεταγλωττιστή της C, που πραγματοποιεί αντικαταστάσεις κειμένου πριν την πραγματική μεταγλώττιση του κώδικα. Περισσότερα για τον προεπεξεργαστή αναφέρονται στο Κεφάλαιο 12.

2.4 Δηλώσεις και ορισμοί μεταβλητών

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

int x; // δήλωση ακέραιας μεταβλητής
x = 42; // ορισμός τιμής για τη μεταβλητή x
int y = 1729; // δήλωση και ορισμός τιμής για τη μεταβλητή y

Τα ονόματα των μεταβλητών σχηματίζονται με γράμματα της αγγλικής αλφαβήτου, ψηφία και την κάτω παύλα, ενώ δεν πρέπει να ξεκινούν με ψηφίο ή να περιέχουν κενά. Δεν επιτρέπεται η χρήση δεσμευμένων λέξεων ως ονόματα μεταβλητών και υπάρχει διάκριση ανάμεσα σε κεφαλαία και πεζά γράμματα, δηλαδή η C είναι case sensitive (π.χ. τα αναγνωριστικά myvar και myvaR είναι διαφορετικά). Τα ονόματα των μεταβλητών πρέπει να επιλέγονται με συνέπεια, να έχουν νόημα στα πλαίσια του γενικότερου κώδικα και να διευκολύνουν την ανάγνωσή του. Πολύ μικρά ή πολύ μεγάλα ονόματα μεταβλητών μπορεί να δυσχεραίνουν την κατανόηση του κώδικα και τη συντήρησή του.

2.5 Είσοδος/Έξοδος

Η εμφάνιση αποτελεσμάτων στην οθόνη και η είσοδος τιμών από το πληκτρολόγιο αποτελούν βασικό τρόπο αλληλεπίδρασης του χρήστη με τον υπολογιστή. Στη συνέχεια θα εξεταστούν οι συναρτήσεις printf() και scanf() που οι δηλώσεις τους υπάρχουν στο stdio.h. Υπάρχουν και άλλες συναρτήσεις εισόδου/εξόδου της C όπως είναι οι getc(), putc(), fgets(), fprintf() για τις οποίες θα γίνουν σε επόμενες παραγράφους σύντομες αναφορές.

2.5.1 Η συνάρτηση printf()

Η printf() επιτρέπει τη μορφοποιημένη εμφάνιση ορισμάτων της στο τερματικό. Η δήλωση της συνάρτησης στο stdio.h είναι η ακόλουθη:

int printf(const char* format, ...);
Η συνάρτηση επιστρέφει μια ακέραια τιμή που είναι το πλήθος των χαρακτήρων που εκτυπώνει ή μια αρνητική τιμή αν συμβεί κάποιο σφάλμα. Η πρώτη παράμετρος που δέχεται είναι το λεγόμενο «αλφαριθμητικό μορφοποίησης» (format string) που συνήθως περιέχει προσδιοριστές μορφοποίησης (format specifiers) που ξεκινούν με το σύμβολο % και που αντικαθίστανται από τις τιμές των επόμενων παραμέτρων. Οι τρεις τελείες που ακολουθούν στη λίστα παραμέτρων στην παραπάνω δήλωση της συνάρτησης printf() σημαίνουν ότι η συνάρτηση μπορεί να δεχθεί μεταβλητό πλήθος ορισμάτων. Πρέπει να υπάρχει συμφωνία στο πλήθος των προσδιοριστών μορφοποίησης που υπάρχουν στο λεκτικό του πρώτου ορίσματος με το πλήθος των ορισμάτων που ακολουθούν. Στο αλφαριθμητικό μορφοποίησης μπορούν να χρησιμοποιηθούν ακολουθίες διαφυγής (escape sequences ή escape characters) όπως το \n, που προκαλεί αλλαγή γραμμής. Η σημασία των κυριότερων ακολουθιών διαφυγής παρουσιάζεται στον Πίνακα 2.3.

Πίνακας 2.3:Χαρακτήρες διαφυγής που μπορούν να χρησιμοποιηθούν στο αλφαριθμητικό μορφοποίησης της printf().

Ακολουθία διαφυγής Περιγραφή
\"
Διπλά εισαγωγικά
\'
Απλά εισαγωγικά
\
Ανάστροφο σλας (backslash)
\0
Χαρακτήρας NULL (τερματισμός στα αλφαριθμητικά)
\a
Ήχος (καμπανάκι)
\b
Οπισθοχώρηση μιας θέσης (backspace)
\t
Στηλοθέτης (tab)
\n
Αλλαγή γραμμής
\xHH
Δεκαεξαδική αναπαράσταση χαρακτήρα (HH είναι η δεκαεξαδική τιμή)
Ο κώδικας 2.2 αποτελεί ένα παράδειγμα επίδειξης λειτουργίας ακολουθιών διαφυγής με την printf().
Κώδικας 2.2: ch2_p2.c - παραδείγματα χρήσης ακολουθιών διαφυγής στο αλφαριθμητικό μορφοποίησης της printf().
#include <stdio.h>

int main(void) {
  printf("This string contains newline characters \nNew Line\n");
  printf("This string contains a tab character \tTabbed Text\n");
  printf("This string contains a backslash character \\ \n");
  printf("This string contains \"Double Quotes\"\n");
  printf("This string contains \'Single Quotes\'\n");
  printf("This string contains backspace characters:Learn C++\b\b\bplain C\n");
  printf("This string makes a beep sound \a \n");
  printf("This string contains a hexadecimally encoded symbol \xFB \n");
  printf("This string contains the NULL character\0 unreached text");
  return 0;
}
Η έξοδος του προγράμματος είναι η ακόλουθη:

$ gcc ch2_p2.c -o ch2_p2 -Wall -Wextra -pedantic -std=c17
$ ./ch2_p2
This string contains newline characters 
New Line
This string contains a tab character    Tabbed Text
This string contains a backslash character \
This string contains "Double Quotes"
This string contains 'Single Quotes'
This string contains backspace characters:Learn plain C
This string makes a beep sound
This string contains a hexadecimally encoded symbol √
This string contains the NULL character
Βέβαια, ο κύριος λόγος χρήσης της printf() είναι η εμφάνιση αποτελεσμάτων. Στο πρόγραμμα που ακολουθεί (κώδικας 2.3) παρουσιάζονται ορισμένες από τις σημαντικότερες δυνατότητες που παρέχει η printf() για μορφοποιημένη έξοδο τιμών.

Κώδικας 2.3: ch2_p3.c - παραδείγματα μορφοποιημένης εξόδου με την printf.
#include <stdio.h>

int main(void) {
  int num = 42;
  float pi = 3.14159f;
  char ch = 'A';
  char str[] = "Hello, World!"; // Αλφαριθμητικό
  printf("Integer: %d\n", num); // Εκτύπωση ενός ακεραίου
  printf("Float: %f\n", pi);    // Εκτύπωση ενός float
  printf("Character and its int value: %c %d\n", ch,
         ch); // Εκτύπωση χαρακτήρα και της αριθμητικής του τιμής
  printf("String: %s\n", str); // Εκτύπωση ενός λεκτικού
  printf("Octal: %o Hexadecimal: %x\n", num,
         num); // Εκτύπωση ακεραίου σε οκταδική και δεκαεξαδική μορφή
  printf("Width and Padding : %8d\n", num); // Εκτύπωση με καθορισμό πλάτους
  printf("Padding with zeros: %08d\n", num); // Εκτύπωση με καθορισμό πλάτους
  printf("Precision: %.2f\n", pi); // Εκτύπωση με καθορισμό ακρίβειας
  printf("Left-alignment : |%-20s|\n", str); // Αριστερή στοίχιση
  printf("Right-alignment: |%20s|\n", str);  // Δεξιά στοίχιση
  printf("Print %% character: %%\n"); // Εκτύπωση του χαρακτήρα %
  return 0;
}
Integer: 42
Float: 3.141590
Character and its int value : A 65
String: Hello, World!
Octal: 52 Hexadecimal: 2a
Width and Padding :             42
Padding with zeros:       00000042
Precision: 3.14
Left‐alignment : |Hello, World!          |
Right‐alignment: |          Hello, World!|
Print % character: %

Ο πίνακας 2.4 δείχνει προσδιοριστές μορφοποίησης που χρησιμοποιούνται συχνά.

Πίνακας 2.4: Προσδιοριστές μορφοποίησης που χρησιμοποιούνται συχνά στην printf().
Προσδιοριστής μορφοποίησης
Τύπος δεδομένων
Περιγραφή
%d
int
Ακέραιος
%o
int
Ακέραιος στο οκταδικό σύστημα
%x ή %X
int
Ακέραιος στο δεκαεξαδικό σύστημα
%u
unsigned int
Μη προσημασμένος ακέραιος
%ld
long
Ακέραιος long
%lld
long long
Ακέραιος long long
%f
float ή double
Πραγματικός απλής ή διπλής ακρίβειας
%e ή %E
float ή double
Επιστημονικός συμβολισμός
%g ή %G
float ή double
Εμφάνιση είτε με δεκαδικά ψηφία είτε με επιστημονικό συμβολισμό ανάλογα με την τιμή
%Lf
long double
Πραγματικός εκτεταμένης ακρίβειας
%c
char
Χαρακτήρας
%s
char*
Αλφαριθμητικό
%p
void*
Διεύθυνση δείκτη
%zu
long long
Για εκτύπωση μεταβλητών size_t

Η συνάρτηση printf κατευθύνει τα μηνύματα στην οθόνη. Ωστόσο, η οθόνη είναι μια μόνο από τις διαθέσιμες επιλογές. Η C λειτουργεί με τα λεγόμενα ρεύματα εξόδου (output streams). Αν επιθυμούμε την κατεύθυνση μηνυμάτων σε κάποιο άλλο ρεύμα εξόδου, όπως για παράδειγμα ένα αρχείο, τότε μπορεί να χρησιμοποιηθεί η συνάρτηση fprintf() που δέχεται ως επιπλέον πρώτο όρισμα σε σχέση με την printf(), το ρεύμα εξόδου. Στο παράδειγμα που ακολουθεί (κώδικας 2.4) χρησιμοποιούνται τα ρεύματα εξόδου stdout και stderr.

Κώδικας 2.4: ch2_p4.c - χρήση των ρευμάτων stdin και stderr για την κατεύθυνση της εξόδου.
#include <stdio.h>

int main(void) {
  int dividend = 42, divisor1 = 2, divisor2 = 0;
  fprintf(stdout, "Info: %d/%d = %d\n", dividend, divisor1,
          dividend / divisor1);
  fprintf(stderr, "Error: division by zero if %d/%d is to be attempted\n",
          dividend, divisor2);
  return 0;
}
Η εκτέλεση του προγράμματος εμφανίζει:
Info: 42/2 = 21
Error: division by zero if 42/0 is to be attempted

Το stdout είναι το ρεύμα εξόδου όπου κατευθύνεται η «κανονική» έξοδος προγραμμάτων και τυπικά αντιστοιχεί στην κονσόλα, οπότε εμφανίζει μηνύματα στην οθόνη. Το ρεύμα εξόδου stderr επίσης τυπικά αντιστοιχεί στην κονσόλα, αλλά χρησιμοποιείται για εμφάνιση σφαλμάτων, πέρα από τη φυσιολογική πορεία εκτέλεσης του προγράμματος. Πρακτικά, η χρήση του stderr αποτελεί έναν τυποποιημένο τρόπο χειρισμού των σφαλμάτων που καθιστά ευκολότερο τον εντοπισμό και την επίλυση προβλημάτων. Συμπερασματικά, η printf() είναι μια συνάρτηση με πολλές δυνατότητες. Μια καλή περιγραφή των «μυστικών» της printf() βρίσκεται στο 1.

2.5.2 Η συνάρτηση scanf()

Η συνάρτηση scanf() επιτρέπει την εισαγωγή τιμών που πληκτρολογεί ο χρήστης σε μεταβλητές. Η δήλωσή της στο stdio.h είναι η ακόλουθη:

int scanf(const char* format, ...);
Η συνάρτηση επιστρέφει μια ακέραια τιμή που είναι το πλήθος των τιμών που διάβασε επιτυχώς. Η πρώτη παράμετρος της scanf() είναι αλφαριθμητικό που περιέχει προσδιοριστές για τις τιμές που πληκτρολογεί ο χρήστης. Για κάθε προσδιοριστή πρέπει να αντιστοιχεί μια μεταβλητή στη λίστα ορισμάτων. H scanf() απαιτεί τα ορίσματα να δίνονται ως διευθύνσεις μνήμης των μεταβλητών. Στη C η διεύθυνση μιας απλής μεταβλητής λαμβάνεται με τον τελεστή &. Στο ακόλουθο πρόγραμμα (κώδικας 2.5) ο χρήστης εισάγει τιμές διαφόρων τύπων δεδομένων με την scanf().

Κώδικας 2.5: ch2_p5.c - ανάγνωση ακεραίων τιμών, πραγματικών τιμών και αλφαριθμητικών με τη scanf().
#include <stdio.h>

int main(void) {
  int i1;
  long i2;
  float f1;
  double f2;
  char s[20];
  printf("Enter 2 int values : ");
  scanf("%d", &i1);
  scanf("%ld", &i2);
  printf("i1=%d, i2=%ld\n", i1, i2);
  printf("Enter 2 float values : ");
  scanf("%f %lf", &f1, &f2);
  printf("f1=%f, f2=%lf\n", f1, f2);
  printf("Enter a string: ");
  scanf("%s", s);
  printf("s=%s\n", s);
  return 0;
}

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

Enter 2 int values : 1
2
i1=1, i2=2
Enter 2 float values : 1.4 3.7
f1=1.400000, f2=3.700000
Enter a string: Hello
s=Hello

2.5.3 Ιδιαιτερότητες της scanf() και άλλες συναρτήσεις εισόδου/εξόδου

Η scanf() δίνει στον προγραμματιστή τη δυνατότητα να διαβάσει οποιαδήποτε δεδομένα (π.χ. αριθμούς, αλφαριθμητικά) από το πληκτρολόγιο. Ωστόσο, υπάρχουν μερικές λεπτομέρειες χρήσης της αλλά και άλλες συναρτήσεις εισόδου που είναι χρήσιμο να γνωρίζει κανείς έτσι ώστε να αποφύγει γνωστά και συνηθισμένα προβλήματα. Καθώς η εισαγωγή αλφαριθμητικών από το χρήστη είναι μια πολύ συνηθισμένη λειτουργία, παρά το ότι στο Κεφάλαιο 8 θα αναφερθούμε αναλυτικά στα αλφαριθμητικά, θα επισημάνουμε εδώ ότι ένα αλφαριθμητικό στη C με δυνατότητα αποθήκευσης κειμένου 10 χαρακτήρων(1) διαβάζεται και εκτυπώνεται με τον κώδικα 2.6.

  1. Ένα αλφαριθμητικό 10 χαρακτήρων χρειάζεται χώρο 11 χαρακτήρων για αποθήκευση, διότι τα αλφαριθμητικά στη C έχουν ως τελευταίο χαρακτήρα το λεγόμενο NULL χαρακτήρα (ή NULL terminator) που σηματοδοτεί το τέλος του αλφαριθμητικού
Κώδικας 2.6: ch2_p6.c - είσοδος αλφαριθμητικού με τη scanf().
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
  char string[11]; // buffer αποθήκευσης χαρακτήρων εισόδου (μαζί με το '\0')
  printf("Input text:");
  scanf("%s", string);
  printf("You entered:|%s|\n", string);
  return 0;
}

Η εκτέλεση του κώδικα με είσοδο χρήστη το “Caesar” (αλφαριθμητικό 6 χαρακτήρων) θα εμφανίσει:

Input text:Caesar
You entered:|Caesar|
Ωστόσο, αν δοκιμάσουμε άλλες εισόδους θα λάβουμε κάποια «περίεργα» αποτελέσματα. Δύο τέτοιες περιπτώσεις θα εξετάσουμε στη συνέχεια.

Αποφυγή υπερχείλισης αλφαριθμητικού Αν εισαχθεί ένα κείμενο όπως το “JuliusCaesar” (12 χαρακτήρων), τότε θα συμβεί υπερχείλιση στη μεταβλητή s, καθώς δεν θα επαρκεί ο δεσμευμένος χώρος για αποθήκευση της εισόδου. Η υπερχείλιση δημιουργεί διάφορα προβλήματα καθώς δεδομένα γράφονται σε θέσεις που δεν θα έπρεπε. Στο συγκεκριμένο παράδειγμα απλά θα εμφανιστούν όλοι οι χαρακτήρες που εισήγαγε ο χρήστης, όμως σε άλλες περιπτώσεις οι συνέπειες θα μπορούσαν να είναι δραματικές. Η αποφυγή υπερχείλισης με αποκοπή των επιπλέον χαρακτήρων μπορεί να γίνει εύκολα αντικαθιστώντας την scanf("%s", s) με scanf("10%s", s). Τότε θα αποθηκευτούν και θα εμφανιστούν οι 10 πρώτοι χαρακτήρες της εισόδου, δηλαδή η έξοδος θα είναι You entered:|JuliusCaes|.

Είσοδος με κενούς χαρακτήρες Ένα δεύτερο πρόβλημα εμφανίζεται όταν το κείμενο της εισόδου έχει κενά. Για παράδειγμα για είσοδο “Mea Culpa” η έξοδος είναι
You entered:|Mea|. Το πιθανότερο είναι να θέλαμε να αποθηκευτεί το σύνολο της φράσης ως αλφαριθμητικό. Για να συμβεί αυτό η κλήση της συνάρτησης scanf() θα πρέπει να γίνει scanf("%10[^\n]s", s). Τότε, η έξοδος θα είναι You entered:|Mea Culpa|. Ένας άλλος τρόπος να επιτευχθεί το ίδιο αποτέλεσμα είναι να χρησιμοποιηθεί η συνάρτηση fgets() όπως στον κώδικα 2.7 που ακολουθεί:

Κώδικας 2.7: ch2_p7.c, είσοδος αλφαριθμητικού με την fgets().
#include <stdio.h>

int main(void) {
  char string[11]; // buffer αποθήκευσης χαρακτήρων εισόδου (μαζί με το '\0')
  printf("Input text:");
  fgets(string, 11, stdin);
  printf("You entered:|%s|\n", string);

  return 0;
}

Η κλήση της συνάρτησης fgets() στη γραμμή 6 δέχεται ως ορίσματα τη μεταβλητή όπου θα αποθηκευθεί η είσοδος του χρήστη, το πλήθος των χαρακτήρων που θα περιέχει η μεταβλητή (συμπεριλαμβανομένου του '\0') και το stdin που είναι το προκαθορισμένο ρεύμα εισόδου, που τυπικά είναι συνδεδεμένο με το πληκτρολόγιο, δηλαδή δέχεται ότι πληκτρολογεί ο χρήστης.

Διαδοχικές scanf()

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

Κώδικας 2.8: ch2_p8.c - προβλήματα στην είσοδο δεδομένων με τη scanf().
#include <stdio.h>

int main(void) {
  int age;
  char grade;
  printf("Enter age and grade:");
  scanf("%d", &age);
  scanf("%c", &grade);
  printf("Age=%d Grade=%c\n", age, grade);
  return 0;
}

Ακολουθεί η έξοδος του προγράμματος για ένα παράδειγμα δεδομένων εισόδου (π.χ. ηλικία 7, τάξη A).

Enter age and grade:7
Age=7 Grade=
Η επιδιόρθωση του κώδικα γίνεται απλά αντικαθιστώντας την κλήση της συνάρτησης scanf() με την κλήση scanf(" %c", &grade). Δώστε προσοχή στο κενό πριν το %c που αποτελεί οδηγία προς τη scanf() να αγνοήσει κενούς χαρακτήρες αν υπάρχουν πριν την τιμή που θα χρησιμοποιηθεί ως είσοδος. Με την αλλαγή αυτή ο κώδικας λειτουργεί όπως θα θέλαμε.
Enter age and grade:7
A
Age=7 Grade=A

Οι συναρτήσεις getc() και putc()

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

Κώδικας 2.9: ch2_p9.c - ανάγνωση και εμφάνιση ενός χαρακτήρα.
#include <stdio.h>

int main(void) {
  printf("Enter a character: ");
  int ch = getc(stdin); // Ανάγνωση ενός χαρακτήρα από το stdin
  printf("You entered: ");
  putc(ch, stdout); // Εγγραφή ενός χαρακτήρα στο stdout
  printf("\nYou entered '%c' which is at ASCII position %d (display using "
         "printf)\n",
         ch, ch); // Μορφοποιημένο μήνυμα με την printf
  return 0;
}

Υποθέτοντας ότι ο χρήστης εισάγει τον χαρακτήρα a, το πρόγραμμα θα εμφανίσει τα ακόλουθα.

Enter a character: a
You entered: a
You entered 'a' which is at ASCII position 97 (display using printf)

2.6 Τελεστές

Οι τελεστές (operators) είναι πράξεις που εφαρμόζονται πάνω σε δεδομένα που αναφέρονται ως τελεστέοι (operands). Αν ο τελεστής εφαρμόζεται σε έναν μόνο τελεστέο τότε είναι μοναδιαίος (unary), αν εφαρμόζεται σε δύο τελεστέους είναι δυαδικός (binary) και αν εφαρμόζεται σε τρεις τελεστέους είναι τριαδικός (ternary). Οι τελεστές επίσης διακρίνονται σε σχέση με το είδος της πράξης που εκτελούν σε αριθμητικούς, συγκριτικούς, λογικούς, χειρισμού δυαδικών ψηφίων κ.λπ. Θα ξεκινήσουμε την περιήγηση στους τελεστές με τον τελεστή ανάθεσης τιμής =.

2.6.1 Τελεστής αν'αθεσης τιμής

Μια εντολή ανάθεσης τιμής έχει την ακόλουθη σύνταξη:

όνομα_μεταβλητής = έκφραση;

Στο αριστερό μέρος του τελεστή = βρίσκεται μια μεταβλητή και στο δεξί μέρος μια έκφραση που υπολογίζεται και η τιμή της ανατίθεται στη μεταβλητή. Ένα ενδιαφέρον χαρακτηριστικό του τελεστή ανάθεσης τιμής στη C είναι ότι μπορεί να χρησιμοποιηθεί για να ανατεθεί τιμή σε πολλές μεταβλητές με μια μόνο εντολή όπως στο ακόλουθο απόσπασμα κώδικα, όπου και οι τρεις μεταβλητές λαμβάνουν την τιμή 10.

x = y = z = 10;
Αυτό συμβαίνει διότι ο τελεστής ανάθεσης τιμής έχει προσεταιριστικότητα από δεξιά προς τα αριστερά (right-to-left associativity). Έτσι στο παράδειγμα γίνεται μια σειρά από αλυσιδωτές αναθέσεις τιμών, όπου η τιμή 10 ανατίθεται πρώτα στο z και στη συνέχεια η τιμή του z ανατίθεται στο y και μετά η τιμή του y ανατίθεται στο x, έτσι ώστε στο τέλος και οι τρεις μεταβλητές να έχουν την ίδια τιμή.

2.6.2 Αριθμητικοί τελεστές

Οι αριθμητικοί τελεστές της C είναι οι +, , *, / και %. Ο τελεστής / όταν εφαρμόζεται σε ακεραίους πραγματοποιεί ακέραια διαίρεση, και ο τελεστής % υπολογίζει το υπόλοιπο της ακέραιας διαίρεσης. Οι υπόλοιποι τελεστές έχουν την αναμενόμενη λειτουργία που περιμένουμε από την αριθμητική. Ο τελεστής μπορεί να λειτουργήσει, είτε ως δυαδικός τελεστής (π.χ. x = 1 ‐ 2;) είτε ως μονομελής για να δηλώσει το αρνητικό πρόσημο μιας έκφρασης (π.χ. x = ‐3;). Στη συνέχεια ακολουθεί ένα πρόγραμμα (κώδικας 2.10), που παρουσιάζει παραδείγματα πράξεων με αριθμητικούς τελεστές.

Κώδικας 2.10: ch2_p10.c - πράξεις με τους αριθμητικούς τελεστές.
#include <stdio.h>

int main(void) {
  printf("15 / 7 = %d, 15 %% 7 = %d\n", 15 / 7, 15 % 7);
  printf("7 / 15 = %d, 7 %% 15 = %d\n", 7 / 15, 7 % 15);
  printf("15.0 / 7.0 = %.2f\n", 15.0 / 7.0);
  printf("15 / 7.0 = %.2f\n", 15 / 7.0);
  printf("15.0 / 7 = %.2f\n", 15.0 / 7);
  return 0;
}

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

15 / 7 = 2, 15 % 7 = 1
7 / 15 = 0, 7 % 15 = 7
15.0 / 7.0 = 2.14
15 / 7.0 = 2.14
15.0 / 7 = 2.14

Ο τελεστής += και άλλοι τελεστές πράξης ανάθεσης Οι αριθμητικοί τελεστές συνδυάζονται στη C με τον τελεστή ανάθεσης τιμής προκειμένου να αποτελέσουν συντομότερους τρόπους συγγραφής εντολών. Έτσι, για παράδειγμα η εντολή x = x + 5; μπορεί να γραφεί ως x += 5;. Εκτός από τον τελεστή += (addition assignment), υπάρχουν και οι τελεστές *=, /=, ‐= και %= με αντίστοιχες λειτουργίες.

Τελεστές μοναδιαίας αύξησης και μοναδιαίας μείωσης Η μοναδιαία αύξηση και η μοναδιαία μείωση της τιμής μιας μεταβλητής είναι μια πολύ κοινή πράξη στον προγραμματισμό, σε βαθμό που οι σχεδιαστές της C θεώρησαν ότι πρέπει να υπάρχει ένας συντομότερος τρόπος από το x = x + 1; ή το x += 1; για την εντολή αυτή, οπότε και επιλέχθηκε ο συμβολισμός x++; και αντίστοιχα x‐‐; για τη μείωση κατά ένα. Μια ιδιαιτερότητα των τελεστών αυτών είναι ότι μπορούν να χρησιμοποιηθούν, είτε προθεματικά (prefix) σε μια μεταβλητή (π.χ. ++x;), είτε επιθεματικά (postfix), όπως στο παράδειγμα που αναφέρθηκε. Η διαφορά ανάμεσα στους δύο τρόπους εφαρμογής εμφανίζεται όταν μια έκφραση με τον τελεστή ++ ή τον ‐‐ εμφανίζεται σε μια παράσταση. Αν ο τελεστής χρησιμοποιείται προθεματικά όπως για παράδειγμα στο x = 10; y = ++x + 5; τότε εκτελείται η πράξη της μοναδιαίας αύξησης και με αυτήν την τιμή η μεταβλητή συμμετέχει στον υπολογισμό της παράστασης, οπότε το αποτέλεσμα στο παράδειγμα θα είναι x = 11, y = 16;. Αν από την άλλη μεριά ο κώδικας χρησιμοποιούσε τον τελεστή ++ επιθεματικά, όπως στο x = 10; y = x++ + 5; τότε στην έκφραση υπολογισμού του y θα χρησιμοποιούνταν η τιμή 10 για το x και τα αποτελέσματα θα ήταν x = 11, y = 15;. Ακολουθεί ο σχετικός κώδικας (κώδικας 2.11) και τα αποτελέσματα εκτέλεσής του.

Κώδικας 2.11: ch2_p11.c - παράδειγμα με τον τελεστή μοναδιαίας αύξησης.
#include <stdio.h>

int main(void) {
  int x = 10, y = ++x + 5;
  printf("Prefix increment example: x=%d, y=%d\n", x, y);
  x = 10;
  y = x++ + 5;
  printf("Postfix increment example: x=%d, y=%d\n", x, y);
  return 0;
}
Prefix increment example: x=11, y=16
Postfix increment example: x=11, y=15

2.6.3 Συγκριτικοί τελεστές

Οι συγκριτικοί τελεστές είναι οι <, <=, >, >=, (έλεγχος ισότητας) και != (έλεγχος ανισότητας). Είναι εύκολο για κάποιον αρχάριο προγραμματιστή να μπερδέψει τον τελεστή ανάθεσης τιμής = με τον συγκριτικό τελεστή ελέγχου ισότητας , καθώς ο μεταγλωττιστής δεν θα εμφανίσει μήνυμα σφάλματος αν γραφεί για παράδειγμα x = 1 αντί για x == 1. Ένας τρόπος για να αποφευχθεί αυτό είναι σε περίπτωση που μια μεταβλητή συγκρίνεται με μια σταθερά ή με μια έκφραση να γράφεται στο δεξί μέρος του ελέγχου η μεταβλητή
(π.χ. 1 == x, αντί για x == 1). Καθώς η C δεν διέθετε πριν την έκδοση C99 λογικό τύπο δεδομένων, υπάρχει η εξής σύμβαση. Η τιμή 0 θεωρείται ψευδής και η τιμή 1 αληθής, όπως και όλες οι ακέραιες τιμές πλην του μηδενός. Για παράδειγμα αν ανατεθεί μια λογική έκφραση σε μια ακέραια μεταβλητή, αν η λογική έκφραση είναι ψευδής τότε η μεταβλητή θα λάβει τιμή 0, ενώ αν είναι αληθής θα λάβει την τιμή 1. Αυτό φαίνεται στο ακόλουθο πρόγραμμα (κώδικας 2.12).

Κώδικας 2.12: ch2_p12.c - το 0 ως ψευδές και το 1 ως αληθές.
1
2
3
4
5
6
7
#include <stdio.h>

int main(void) {
  printf("1 > 2 is evaluated as %d\n", 1 > 2);
  printf("2 <= 2 is evaluated as %d\n", 2 <= 2);
  return 0;
}
1 > 2 is evaluated as 0
2 <= 2 is evaluated as 1

Ο τύπος bool Ο τύπος bool ενσωματώθηκε επίσημα στη C με το πρότυπο C99. Έτσι, μπορούν να χρησιμοποιηθούν οι συμβολικές σταθερές true και false σε προγράμματα C. Σε αυτήν την περίπτωση πρέπει να γίνει include το stdbool.h

2.6.4 Λογικοί τελεστές

Οι λογικοί τελεστές είναι οι &&, ||, ! και είναι το λογικό ΚΑΙ (AND), το λογικό Ή (OR) και το λογικό ΌΧΙ (NOT) αντίστοιχα. Οι λογικοί τελεστές επιτρέπουν τη σύνταξη σύνθετων λογικών εκφράσεων όπως για παράδειγμα η (x > 5 && x <=10) || x == 20 που αποτιμάται ως αληθής για οποιαδήποτε τιμή του x μεγαλύτερη του 5 και μικρότερη ή ίση του 10 ή αν το x έχει την τιμή 20. Ο πίνακας αληθείας για τους λογικούς τελεστές παρουσιάζεται στον Πίνακα 2.5.

Πίνακας 2.5: Λογικοί τελεστές - πίνακας αληθείας.
p q p && q p || q !p
ΨΕΥΔΗΣ (0) ΨΕΥΔΗΣ (0) ΨΕΥΔΗΣ (0) ΨΕΥΔΗΣ (0) ΑΛΗΘΗΣ (1)
ΨΕΥΔΗΣ (0) ΑΛΗΘΗΣ (1) ΨΕΥΔΗΣ (0) ΑΛΗΘΗΣ (1) ΑΛΗΘΗΣ (1)
ΑΛΗΘΗΣ (1) ΨΕΥΔΗΣ (0) ΨΕΥΔΗΣ (0) ΑΛΗΘΗΣ (1) ΨΕΥΔΗΣ (0)
ΑΛΗΘΗΣ (1) ΑΛΗΘΗΣ (1) ΑΛΗΘΗΣ (1) ΑΛΗΘΗΣ (1) ΨΕΥΔΗΣ (0)

Εσπευσμένη αποτίμηση λογικών εκφράσεων Υπάρχει περίπτωση μια σύνθετη λογική έκφραση να μπορεί να αποτιμηθεί χωρίς να εξεταστούν όλοι οι όροι της. Για παράδειγμα αν πολλές λογικές εκφράσεις συνδέονται με τον τελεστή && τότε αν ξεκινώντας από αριστερά εντοπιστεί μια λογική έκφραση που είναι ψευδής, δεν χρειάζεται να αποτιμηθούν οι υπόλοιπες εκφράσεις καθώς γνωρίζουμε ότι η σύνθετη έκφραση θα είναι ψευδής. Ομοίως, αν μια σύνθετη λογική έκφραση αποτελείται από λογικές εκφράσεις που συνδέονται με το || τότε αν κατά την αποτίμηση της σύνθετης έκφρασης εντοπιστεί μια συνθήκη που είναι αληθής, τότε οι συνθήκες που απομένουν δεν αποτιμώνται καθώς η τιμή τους δεν πρόκειται να επηρεάσει την αληθή τιμή της σύνθετης λογικής έκφρασης που θα είναι σε κάθε περίπτωση αληθής. Ο τρόπος αυτός αποτίμησης σύνθετων λογικών εκφράσεων στη C αναφέρεται και ως short circuit (βραχυκύκλωμα) και μερικές φορές μπορεί να έχει μη επιθυμητά αποτελέσματα. Ένα παράδειγμα δίνεται στη συνέχεια στον κώδικα 2.13, όπου παρά το ότι υπάρχει στη γραμμή 6 πράξη διαίρεσης ακεραίου με μηδέν, που θα δημιουργούσε σφάλμα εκτέλεσης, ο κώδικας ολοκληρώνει την εκτέλεσή του κανονικά καθώς η αποτίμηση της σύνθετης λογικής έκφρασης γίνεται χωρίς να αποτιμάται ποτέ η έκφραση (10/x) > 1 μιας και η έκφραση x == 0 είναι αληθής.

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

int main(void) {
  int x = 0;
  bool f = (x == 0 || (10 / x) > 1);
  printf("The logic expression's value is %d\n", f);
  return 0;
}

Η έξοδος του προγράμματος είναι η ακόλουθη

The logic expression 's value is 1.

2.6.5 Τελεστές χειρισμού δυαδικών ψηφίων

Οι τελεστές χειρισμού δυαδικών ψηφίων της C είναι οι & (bitwise and), | (bitwise or), ^ (bitwise xor), << (ολίσθηση προς τα αριστερά), >> (ολίσθηση προς τα δεξιά) και ~ (συμπλήρωμα ως προς ένα). Οι τελεστές αυτοί εκτελούν πράξεις σε επίπεδο δυαδικού ψηφίου. O Πίνακας 2.6 παρουσιάζει τη λειτουργία των τελεστών &,|,^,~. Ο τελεστής του συμπληρώματος ως προς ένα αντιστρέφει τα bits του τελεστέου.

Πίνακας 2.6: Τελεστές χειρισμού δυαδικών ψηφίων.
x y x & y x | y x^y ~x
0 0 0 0 0 1
0 1 0 1 1 1
1 0 0 1 1 0
1 1 1 1 0 0

Οι τελεστές ολίσθησης μετατοπίζουν τα bits του αριστερού τελεστέου τους ένα πλήθος θέσεων (που είναι η τιμή του δεξιού τελεστέου), είτε προς τα αριστερά είτε προς τα δεξιά. Για παράδειγμα ο τελεστής αριστερής ολίσθησης << αν εφαρμοστεί στην τιμή 12 που έχει τη δυαδική αναπαράσταση 00001100 με δεξί τελεστέο την τιμή 2, θα μετατοπίσει όλα τα ψηφία του 00001100 κατά 2 θέσεις προς τα αριστερά δίνοντας την τιμή 48 που αναπαρίσταται δυαδικά ως 00110000. Αξίζει να σημειωθεί ότι η μετατόπιση των ψηφίων ενός δυαδικού αριθμού κατά ένα ψηφίο προς τα αριστερά ισοδυναμεί με διπλασιασμό του, ενώ η μετατόπιση προς τα δεξιά με υποδιπλασιασμό του (π.χ. η αριστερή ολίσθηση 7 << 1 θα επιστρέψει 14, ενώ η δεξιά ολίσθηση 7 >> 1 θα επιστρέψει 3).

Ο κώδικας 2.14 δείχνει παραδείγματα λειτουργίας των bitwise τελεστών.

Κώδικας 2.14: ch2_p14.c - παράδειγμα χρήσης τελεστών χειρισμού δυαδικών ψηφίων.
#include <stdio.h>

int main(void) {
  unsigned int a = 12; // Δυαδικός: 1100
  unsigned int b = 10; // Δυαδικός: 1010
  unsigned int resultAnd = a & b;
  printf("Bitwise AND: %u\n", resultAnd); // Αποτέλεσμα: 8 (1000)
  unsigned int resultOr = a | b;
  printf("Bitwise OR: %u\n", resultOr); // Αποτέλεσμα: 14 (1110)
  unsigned int resultXor = a ^ b;
  printf("Bitwise XOR: %u\n", resultXor); // Αποτέλεσμα: 6 (110)
  unsigned int resultNotA = ~a;
  printf(
      "Bitwise NOT on %u: %u\n", a,
      resultNotA); // Αποτέλεσμα: 4294967283 (11111111111111111111111111110011)
  unsigned int resultLeftShift = a << 2;
  printf("Left Shift: %u\n", resultLeftShift); // Αποτέλεσμα: 48 (110000)
  unsigned int resultRightShift = b >> 1;
  printf("Right Shift: %u\n", resultRightShift); // Αποτέλεσμα: 5 (101)
  return 0;
}

Λάβετε υπόψη ότι μια unsigned int τιμή έχει 32 bits. Τα αποτελέσματα εκτέλεσης είναι τα ακόλουθα.

Bitwise AND: 8
Bitwise OR: 14
Bitwise XOR: 6
Bitwise NOT on 12: 4294967283
Left Shift: 48
Right Shift: 5

2.6.5.1 Μάσκες δυαδικών ψηφίων

Μια μάσκα δυαδικών ψηφίων είναι ένα δυαδικό πρότυπο (bit pattern) που χρησιμοποιείται για την επιλογή ή την κατάργηση επιλογής συγκεκριμένων δυαδικών ψηφίων σε ακεραίους. Στην πράξη εφαρμόζονται τελεστές bitwise με τελεστέους την τιμή που επιθυμούμε να τροποποιήσουμε και τη μάσκα. Με αυτόν τον τρόπο μπορούμε να ορίσουμε ότι συγκεκριμένα bits θα λάβουν τιμή 1 ή τιμή 0 ή θα εναλλάξουν την τιμή τους.

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

void print_binary(unsigned int num) {
  int bits = sizeof(unsigned int) * 8;
  for (int i = bits - 1; i >= 0; i--) {
    unsigned int mask = 1u << i; // Δημιουργία μάσκας με 1 στην τρέχουσα θέση
    unsigned int bit = (num & mask) >> i; // Εξαγωγή της τιμής του τρέχοντος bit
    printf("%u", bit);
  }
  printf("\n");
}

int main(void) {
  unsigned int flags = 0;
  print_binary(flags);     // ...0000
  unsigned int flagA = 1; // Bit 0
  unsigned int flagB = 2; // Bit 1
  unsigned int flagC = 4; // Bit 2
  unsigned int flagD = 8; // Bit 3
  // Ορισμός flags με bitwise OR και μάσκας bits
  flags |= flagA;
  flags |= flagB;
  flags |= flagD;
  print_binary(flags); // ...1011
  // Έλεγχος αν ένα flag έχει τεθεί με bitwise AND και μάσκα bits
  printf((flags & flagB) ? "Flag B is set.\n" : "Flag B is not set.\n");
  // Εκκαθάριση ενός flag με bitwise AND και μάσκα bits
  flags &= ~flagA;
  print_binary(flags); // ...1010
  // Αλλαγή κατάστασης ενός flag με bitwise XOR και μάσκα bits
  flags ^= flagC;
  print_binary(flags); // ...1110
  // Έλεγχος αν ορισμένα flags έχουν τεθεί
  printf((flags & (flagB | flagC)) == (flagB | flagC)
             ? "Flag B, and flag C are set.\n"
             : "Either flag B or flag C are not set.\n");
  return 0;
}

Το παράδειγμα του κώδικα 2.15 περιλαμβάνει ορισμό της συνάρτησης print_binary() που δέχεται ως όρισμα έναν μη προσημασμένο ακέραιο αριθμό και τον εμφανίζει ως δυαδικό αριθμό 32 ψηφίων. Καθώς στις συναρτήσεις θα αναφερθούμε στο Κεφάλαιο 4, θα θεωρήσουμε προς το παρόν τη συγκεκριμένη συνάρτηση ως ένα «μαύρο κουτί» που απλά κάνει την εργασία που επιθυμούμε. Η εκτέλεση του προγράμματος δίνει τα ακόλουθα αποτελέσματα.

00000000000000000000000000000000
00000000000000000000000000001011
Flag B is set.
00000000000000000000000000001010
00000000000000000000000000001110
Flag B, and flag C are set.
Οι δυνατότητες των τελεστών δυαδικών ψηφίων είναι πάρα πολλές για να αναπτυχθούν εδώ. Δεκάδες παραδείγματα έξυπνης εφαρμογής τους υπάρχουν στο 2.

2.6.6 Τελεστές μετατροπής τύπων

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

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

Μετατροπές τύπων όπως αυτές που αναφέρθηκαν είναι γνωστές ως υπονοούμενες μετατροπές τύπων (implicit type conversions ή coercions). Από την άλλη μεριά, υπάρχουν οι ρητές μετατροπές τύπων (explicit type conversions) που είναι γνωστές και ως casts. Αυτό επιτυγχάνεται γράφοντας (τύπος) έκφραση έτσι ώστε η έκφραση να μετατραπεί στον ζητούμενο τύπο. Ένα παράδειγμα “casting” δίνεται στον κώδικα 2.16.

Κώδικας 2.16: ch2_p16.c - μετατροπή ακεραίων τιμών σε πραγματικές τιμές.
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
  float x = 5 / 2; // Ακέραια διαίρεση
  printf("%.1f\n", x);
  x = (float)5 / (float)2; // Διαίρεση με πραγματικό αποτέλεσμα
  printf("%.1f\n", x);
  return 0;
}
Ακολουθεί το αποτέλεσμα της εκτέλεσης του κώδικα:

2.0
2.5
2.5

2.6.7 Άλλοι τελεστές

Η C διαθέτει επιπλέον τελεστές όπως ο sizeof που ήδη χρησιμοποιήσαμε στο παράδειγμα ch2_p1.c, ο τελεστής διεύθυνσης μνήμης &, ο τριαδικός τελεστής ?: και άλλους όπως τον τελεστή τελεία . και τον τελεστή βελάκι ‐> που θα δούμε στα Κεφάλαια 6 και 7. Στη συνέχεια δίνεται μια σύντομη περιγραφή του sizeof, του & ως τελεστή διεύθυνσης μνήμης και του ?:.

Ο τελεστής sizeof Ο τελεστής sizeof χρησιμοποιείται για να ληφθεί το μέγεθος σε bytes ενός τύπου δεδομένων, μιας μεταβλητής ή μιας έκφρασης. Ο υπολογισμός αυτός γίνεται κατά τον χρόνο μεταγλώττισης και όχι κατά την εκτέλεση. Το αποτέλεσμα που επιστρέφει ο sizeof είναι ένας μη προσημασμένος ακέραιος τύπου size_t. Ο κώδικας 2.17 δείχνει ότι ο τελεστής μπορεί να εφαρμοστεί και σε εκφράσεις. Στην περίπτωση των μεταβλητών ο τελεστής sizeof μπορεί να χρησιμοποιηθεί και χωρίς τις παρενθέσεις.

Κώδικας 2.17: ch2_p17.c - παράδειγμα χρήσης του τελεστή sizeof.
#include <stdio.h>

int main(void) {
  int x = 42;
  printf("The size of data type int is %zu bytes\n", sizeof(int));
  printf("The size of variable x is %zu bytes\n", sizeof x);
  printf("The size of the value of the expression x + 1.5 is %zu bytes\n",
         sizeof(x + 1.5));
  return 0;
}
Η κλήση της συνάρτησης printf() στις γραμμές 7 και 8 θα εμφανίσει ότι το μέγεθος του αποτελέσματος της έκφρασης x + 1.5 είναι 8 bytes. Αυτό συμβαίνει διότι για να υπολογιστεί η έκφραση θα πρέπει να μετατραπεί η τιμή της ακέραιας μεταβλητής x σε double και το αποτέλεσμα θα είναι τύπου double, που καταλαμβάνει στη μνήμη 8 bytes.

The size of data type int is 4 bytes
The size of variable x is 4 bytes
The size of the value of the expression x + 1.5 is 8 bytes
Ο τελεστής διεύθυνσης μνήμης & Ο τελεστής & αναφέρθηκε ήδη ως bitwise and, αλλά έχει διαφορετικό ρόλο αν τοποθετηθεί πριν από από μια μεταβλητή οπότε επιστρέφει τη διεύθυνση μνήμης της μεταβλητής. Δείτε το ακόλουθο παράδειγμα (κώδικας 2.18):

Κώδικας 2.18: ch2_p18.c - παράδειγμα χρήσης του τελεστή διεύθυνση μνήμης.
#include <stdio.h>

int main(void) {
  int x = 42;
  int y = 1729;
  printf("Variable x has value %d and is at address %p \n", x, (void *)&x);
  printf("Variable y has value %d and is at address %p \n", y, (void *)&y);
  int d = (&x - &y) * sizeof(int); // διαφορά σε bytes ανάμεσα σε 2 διευθύνσεις
  printf("Space in bytes between memory locations of x and y is %d\n", d);
  return 0;
}
Ακολουθεί ένα παράδειγμα εκτέλεσης του κώδικα:

Variable x has value 42 and is at address 0000006EDF3FFB78
Variable y has value 1729 and is at address 0000006EDF3FFB74
Space in bytes between memory locations of x and y is 4

Ο τριαδικός τελεστής ?: Ο τριαδικός τελεστής επιτρέπει τη συγγραφή εκφράσεων επιλογής με την ακόλουθη συνοπτική σύνταξη:

(συνθήκη) ? (έκφραση_αν_είναι_αληθής) : (έκφραση_αν_είναι_ψευδής)
Η συνθήκη είναι μια λογική έκφραση που μπορεί να αποτιμηθεί είτε αληθής είτε ψευδής. Αν η συνθήκη είναι αληθής, τότε αποτιμάται η έκφραση μετά το ? και επιστρέφει ως αποτέλεσμα του τριαδικού τελεστή. Αν η συνθήκη είναι ψευδής, τότε αποτιμάται η έκφραση μετά το : και επιστρέφει αυτή ως αποτέλεσμα του τελεστή. Πρόκειται για έναν σύντομο τρόπο γραφής κώδικα που θα μπορούσε να γραφεί με την εντολή if που θα δούμε στο επόμενο κεφάλαιο. Ένα παράδειγμα χρήσης του τριαδικού τελεστή που εντοπίζει τον μεγαλύτερο από δύο αριθμούς φαίνεται στη συνέχεια στον κώδικα 2.19

Κώδικας 2.19: ch2_p19.c - παράδειγμα χρήσης του τριαδικού τελεστή.
1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void) {
  int num1 = 10, num2 = 7;
  int max = (num1 > num2) ? num1 : num2;
  printf("num1=%d, num2=%d, max=%d\n", num1, num2, max);
  return 0;
}

Ακολουθούν τα αποτελέσματα εκτέλεσης.

num1=10, num2=7, max=10

2.7 Προτεραιότητα τελεστών

Στη C οι τελεστές έχουν προτεραιότητες που καθορίζουν τη σειρά αποτίμησης εκφράσεων που περιέχουν διάφορους τελεστές. Πέντε κύρια σημεία σχετικά με την προτεραιότητα των τελεστών στη C είναι τα ακόλουθα:

  1. Οι εκφράσεις σε παρενθέσεις εκτελούνται πρώτες. Συνεπώς, κάνοντας χρήση παρενθέσεων μπορεί να παρακαμφθεί η σειρά πράξεων που επιβάλλει η προτεραιότητα των τελεστών.
  2. Για τους αριθμητικούς τελεστές ισχύει η γνωστή από τα μαθηματικά σειρά αποτίμησης. Για παράδειγμα ο τελεστής * έχει μεγαλύτερη προτεραιότητα από τον τελεστή +.
  3. Οι μοναδιαίοι τελεστές (π.χ. ++, !) έχουν υψηλότερη προτεραιότητα από τους δυαδικούς τελεστές.
  4. Οι τελεστής ανάθεσης = (καθώς και οι τελεστές πράξης ανάθεσης όπως ο +=) έχουν μικρότερη προτεραιότητα από σχεδόν όλους τους άλλους τελεστές. Αποτιμώνται μετά την αποτίμηση της έκφρασης που βρίσκεται στο δεξί μέρος του τελεστή ανάθεσης.
  5. Τελεστές με την ίδια προτεραιότητα εκτελούνται βάσει προσεταιριστικότητας του τελεστή. Για παράδειγμα τελεστές όπως ο +,,* και / έχουν προσεταιριστικότητα από αριστερά προς τα δεξιά, οπότε μια πράξη όπως η 10 ‐ 4 + 3 θα επιστρέψει το σωστό αποτέλεσμα 9 και όχι το 3 που θα προέκυπτε αν το 4 + 3 γινόταν πρώτο. Παράδειγμα τελεστή που έχει προσεταιριστικότητα από δεξιά προς τα αριστερά είναι ο τελεστής ανάθεσης =. Ωστόσο, αξίζει να σημειωθεί ότι η πλειονότητα των τελεστών έχει προσεταιριστικότητα από αριστερά προς τα δεξιά.

Αναλυτικές πληροφορίες για την προτεραιότητα και την προσεταιριστικότητα των τελεστών στη C μπορούν να εντοπιστούν στο 3.

2.8 Κύκλος μεταγλώττισης, σύνδεσης και εκτέλεσης

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

  1. Προεπεξεργασία
  2. Μεταγλώττιση
  3. Συμβολομετάφραση
  4. Σύνδεση

Για την επίδειξη της μεταγλώττισης θα χρησιμοποιηθεί το ακόλουθο παράδειγμα πηγαίου κώδικα (κώδικας 2.20).

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

int main(void) {
  double x = 2.0;
  double irrational = sqrt(x);
  printf("The square root of %.2f is %.8f\n", x, irrational);
  return 0;
}

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

$ gcc ‐o ch2_p20 ch2_p20.c ‐lm

Στο παρασκήνιο εκτελούνται και τα 4 στάδια μεταγλώττισης και σύνδεσης και παράγεται το εκτελέσιμο αρχείο ch2_p20. Ο διακόπτης ‐o ακολουθείται από το όνομα του εκτελέσιμου αρχείου, ενώ στο συγκεκριμένο παράδειγμα υπάρχει επιπλέον και ο διακόπτης ‐lm που υποδηλώνει ότι ήδη μεταγλωττισμένες εκδόσεις συναρτήσεων όπως στο παράδειγμα αυτό η sqrt() (τετραγωνική ρίζα) θα ληφθούν από κάποιο αρχείο βιβλιοθήκης (π.χ. libm.a ή libm.so για Linux) και θα συμπληρώσουν τον δυαδικό κώδικα που θα αποτελέσει το εκτελέσιμο. Η εκτέλεση του προγράμματος θα εμφανίσει την τετραγωνική ρίζα του αριθμού 2.

$ ./ch2_p20
The square root of 2.00 is 1.41421356
Στη συνέχεια θα παρουσιαστούν τα στάδια μεταγλώττισης και το αποτέλεσμα που παράγει το κάθε στάδιο, όπως φαίνεται και στο Σχήμα 2.1.

graph TD
    A["πηγαίος κώδικας (*.c, *.h)"] -->|Μεταγλώττιση και σύνδεση| B

    subgraph " "
        B["προεπεξεργασία (gcc -E)"]
        B -->|"*.i"| C["μεταγλώττιση (gcc -c)"]
        C -->|"συμβολικός κώδικας (*.s)"| D["συμβολομετάφραση (as)"]
        D -->|"κώδικας αντικειμένου (*.o)"| E["σύνδεση (ld)"]
    end

    E -->|"εκτελέσιμο"| F[" "]
    G["βιβλιοθήκες"] --> E
Σχήμα 2.1: Στάδια μεταγλώττισης και σύνδεσης.

1. Προεπεξεργασία Η προεπεξεργασία προηγείται της πραγματικής μεταγλώττισης. Στο στάδιο αυτό αρχικά αφαιρούνται τυχόν σχόλια και στη συνέχεια ο προεπεξεργαστής ερμηνεύει οδηγίες όπως η #include και η #define έτσι ώστε να παραχθεί μια νέα μορφή κώδικα, κατάλληλη για τον μεταγλωττιστή. Για να απομονωθεί το στάδιο της προεπεξεργασίας μπορεί να χρησιμοποιηθεί ο διακόπτης ‐E όπως στη συνέχεια:

$ gcc ‐o ch2_p20.i ch2_p20.c ‐E
Το αρχείο ch2_p20.i που προκύπτει είναι πολύ μεγαλύτερο σε σχέση με το αρχείο πηγαίου κώδικα (1661 γραμμές κώδικα για το συγκεκριμένο παράδειγμα). Ένα απόσπασμα του ch2_p20.i που δείχνει την αρχή και το τέλος του φαίνεται στη συνέχεια:

# 0 "ch2_p20.c"
# 0 "<built‐in>"
# 0 "<command ‐line>"
# 1 "/usr/include/stdc‐predef.h" 1 3 4
...
# 4 "ch2_p20.c"
int main() {
    double x = 2.0;
    double irrational = sqrt(x);
    printf("The square root of %.2f is %.8f\n", x, irrational);
    return 0;
}

Η χρήση του διακόπτη ‐E προκαλεί την κλήση του προγράμματος cpp (C PreProcessor), οπότε το ίδιο αποτέλεσμα θα είχε προκύψει και με την εντολή:

$ cpp ‐o ch2_p20.i ch2_p20.c

2. Μεταγλώττιση Στο στάδιο της μεταγλώττισης μετατρέπονται τα περιεχόμενα του .i αρχείου σε συμβολικό κώδικα (assembly), δηλαδή κώδικα χαμηλού επιπέδου. Στο στάδιο αυτό εντοπίζονται τυχόν συντακτικά σφάλματα (syntax errors) και παράγονται μηνύματα λαθών καθώς και προειδοποιήσεις (warnings). Η εντολή που ακολουθεί χρησιμοποιεί τον διακόπτη ‐S και παράγει το αρχείο συμβολικού κώδικα ch2_p20.s.

$ gcc ‐o ch2_p20 ‐S ch2_p20.i
Ο συμβολικός κώδικας που δημιουργείται εξαρτάται από το σύστημα για τον οποίο δημιουργείται. Συνεπώς θα είναι διαφορετικός για τον ίδιο κώδικα C ακόμα και αν χρησιμοποιηθεί ο ίδιος μεταγλωττιστής (π.χ. gcc) σε διαφορετικά συστήματα (π.χ. Linux ή Windows ή OSX λειτουργικό σύστημα, x86 ή x86-64 ή ARM αρχιτεκτονική CPU). Η κατανόηση του κώδικα assembly είναι χρήσιμη δεξιότητα καθώς βοηθά στην κατανόηση της απόδοσης των προγραμμάτων και σε ορισμένες περιπτώσεις επιτρέπει την περαιτέρω εκμετάλλευση των δυνατοτήτων του υλικού.

Μια ενδιαφέρουσα σελίδα που επιτρέπει τη δημιουργία συμβολικού κώδικα για πολλά και διαφορετικά συστήματα και στη συνέχεια τον πειραματισμό με τον κώδικα που δημιουργείται είναι η Complexity Explorer(1) από τον Matt Godbolt.

  1. https://godbolt.org/
Κώδικας 2.21: Συμβολικός κώδικας για το ch2_p20.c σε σύστημα x86-64 gcc 13.1.
.LC1:
    .string "The square root of %.2f is %.8f\n"      

main:
    push    rbp                              
    mov     rbp, rsp                         
    sub     rsp, 16                           

    movsd   xmm0, QWORD PTR .LC0[rip]         
    movsd   QWORD PTR [rbp-8], xmm0           

    mov     rax, QWORD PTR [rbp-8]            
    movq    xmm0, rax                        
    call    sqrt                             

    movq    rax, xmm0                        
    mov     QWORD PTR [rbp-16], rax           

    movsd   xmm0, QWORD PTR [rbp-16]          
    mov     rax, QWORD PTR [rbp-8]            
    movapd  xmm1, xmm0                       
    movq    xmm0, rax                        

    mov     edi, OFFSET FLAT:.LC1             
    mov     eax, 2                            
    call    printf                           

    mov     eax, 0                            
    leave                                     
    ret                                       

.LC0:
    .long 0                                  
    .long 1073741824                         

3. Συμβολομετάφραση Ο κώδικας assembly μετατρέπεται σε εντολές μηχανής από τον συμβολομεταφραστή (assembler). Οι εντολές συμβολικού κώδικα μετατρέπονται σε δυαδικό κώδικα που είναι γνωστός ως κώδικας αντικείμενο (object code). Με την ακόλουθη εντολή παράγεται το αρχείο αντικείμενο που έχει επέκταση .o. Το πρόγραμμα που αναλαμβάνει τη διαδικασία είναι το as.

$ as ‐o ch2_p20.o ch2_p20.s

4. Σύνδεση Στη σύνδεση συνδέεται κώδικας που υπάρχει σε βιβλιοθήκες με το πρόγραμμά μας. Το πρόγραμμα μπορεί να αποτελείται από επιμέρους αρχεία πηγαίου κώδικα, οπότε η σύνδεση αναλαμβάνει να συνδυάσει τα επιμέρους αρχεία σε ένα τελικό εκτελέσιμο. Το θέμα αυτό θα αναλυθεί στο Κεφάλαιο 13 που εξετάζει το θέμα της διαμέρισης κώδικα. Το πρόγραμμα που αναλαμβάνει τη σύνδεση είναι το ld, αλλά η αλληλεπίδραση μαζί του είναι ευκολότερη μέσω του gcc όπως στο ακόλουθο παράδειγμα που χρησιμοποιεί το αρχείο αντικείμενο για να παράξει το τελικό εκτελέσιμο ch2_p20.

$ gcc ‐o ch2_p20 ch2_p20.o

Ο μεταγλωττιστής gcc δίνει τη δυνατότητα δημιουργίας όλων των ενδιάμεσων αρχείων που παράγονται από τα στάδια μεταγλώττισης χρησιμοποιώντας τον διακόπτη ‐save‐temps. Έτσι με την ακόλουθη εντολή δημιουργούνται και τα 4 αρχεία που πριν δημιουργήθηκαν με ξεχωριστές εντολές.

$ gcc ‐o ch2_p20 ‐save‐temps ch2_p20.c 
Τα αρχεία που δημιουργούνται είναι τα ch2_p20.i, ch2_p20.s, ch2_p20.o και ch2_p20.

2.9 Ασκήσεις

Άσκηση 1
Γράψτε ένα πρόγραμμα που να δέχεται ένα χρονικό διάστημα σε δευτερόλεπτα και να εμφανίζει το χρονικό διάστημα στη μορφή ΩΩ:ΛΛ:ΔΔ όπου ΩΩ είναι οι ώρες, ΛΛ είναι τα λεπτά και ΔΔ είναι τα δευτερόλεπτα (π.χ. για είσοδο 64326 να εμφανίζει 17:52:06).

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

int main(void) {
  int x, h, m, s;
  printf("Enter duration in seconds:");
  scanf("%d", &x);
  h = x / 3600;
  x %= 3600;
  m = x / 60;
  s = x % 60;
  printf("%02d:%02d:%02d\n", h, m, s);
  return 0;
}

Άσκηση 2
Γράψτε πρόγραμμα που να δημιουργεί έναν μη προσημασμένο ακέραιο (unsigned int) με όλα τα δυαδικά του ψηφία να έχουν την τιμή 1. Εκτυπώστε την τιμή του στο δεκαδικό σύστημα και επιβεβαιώστε ότι η τιμή αυτή ταυτίζεται με τη σταθερά UINT_MAX από το limits.h.

Λύση άσκησης 2
#include <limits.h>
#include <stdint.h>
#include <stdio.h>

int main(void) {
  unsigned int x = 0;
  x = ~x;
  printf("x=%u UINT_MAX=%u\n", x, UINT_MAX);
  return 0;
}

Άσκηση 3
Eντοπίστε τις διαφορές των προσδιοριστών μορφοποίησης %i και %d κατά τη χρήση τους στις printf() και scanf().

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

int main(void) {
  const char s[] = "Learn C";
  for (size_t i = 1; i <= strlen(s); i++) {
    printf("|%*s|\n", (int)(i + strlen(s)), s);
  }
  for (size_t i = 1; i <= strlen(s); i++) {
    printf("|%.*s|\n", (int)i, "Learn C");
  }
}

Άσκηση 4
Γράψτε ένα απλό πρόγραμμα που να δημιουργεί μια μεταβλητή τύπου int και μια μεταβλητή τύπου unsigned int. Αναθέστε και στις δύο από το limits.h τις μεγαλύτερες τιμές που μπορούν να λάβουν (ΜΑΧ_ΙΝΤ και UINT_MAX αντίστοιχα) και εκτυπώστε τις τιμές αυτές. Αυξήστε κατά ένα τις τιμές και των δύο μεταβλητών και εκτυπώστε τις τιμές εκ νέου. Τι παρατηρείτε;

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

int main(void) {
  int a = INT_MAX;
  unsigned int b = UINT_MAX;
  printf("a=%d, b=%u\n", a, b);
  a++;
  b++;
  printf("a=%d, b=%u\n", a, b);
  return 0;
}

Άσκηση 5
Εκτυπώστε τις τιμές των εκφράσεων 0.1 + 0.2, 0.1 + 0.3, 0.1 + 0.2 == 0.3, 0.1 + 0.3 == 0.4. Τι παρατηρείτε; Αναζητήστε τους λόγους της «περίεργης» συμπεριφοράς στο 4.

Λύση άσκησης 5
1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
  printf("0.1 + 0.2 = %f\n", 0.1 + 0.2);
  printf("0.1 + 0.3 = %f\n", 0.1 + 0.3);
  printf("0.1 + 0.2 == 0.3 is %d\n", 0.1 + 0.2 == 0.3); // false
  printf("0.1 + 0.3 == 0.4 is %d\n", 0.1 + 0.3 == 0.4); // true
  return 0;
}

  1. Don Colton. Secrets of ”printf”. https://inst.eecs.berkeley.edu/~cs61c/fa19/hw/hw2/printf_colton.pdf Accessed: 2023-06-01. 2015 

  2. Sean Eron Anderson. Bit Twiddling Hacks. http://graphics.stanford.edu/~seander/bithacks.html. Accessed: 2023-06-01 

  3. C Operator Precedence. https://en.cppreference.com/w/c/language/operator_precedence. Accessed: 2023-06-01. 

  4. Sean Eron Anderson. Floating Point Numbers. Why floating-point numbers are needed? https://floating‐point‐gui.de/formats/fp/. Accessed: 2023-06-01.