Στόχοι: μετά από αυτό το κεφάλαιο θα μπορείτε να
- εξηγείτε τι είναι bit, byte και διεύθυνση μνήμης, και πώς διαφέρει η κύρια από τη δευτερεύουσα μνήμη·
- μετατρέπετε αριθμούς ανάμεσα στο δυαδικό, το δεκαδικό και το δεκαεξαδικό σύστημα·
- δηλώνετε μεταβλητές, να τους αναθέτετε τιμές και να εξηγείτε τι σημαίνει αυτό για τη μνήμη·
- επιλέγετε τύπο (
char,int,unsigned,float,double, …) με βάση το εύρος τιμών του·- αναπαριστάτε αρνητικούς ακεραίους σε συμπλήρωμα ως προς 2 και να αναγνωρίζετε την υπερχείλιση ακεραίων·
- τυπώνετε μεταβλητές με την
printf, χρησιμοποιώντας ακολουθίες διαφυγής και προσδιοριστικά μορφοποίησης.Προαπαιτούμενα: Κεφάλαιο 0, Κεφάλαιο 1
Χρόνος μελέτης: ~2 ώρες
Ένα πρόγραμμα δουλεύει με δεδομένα, και τα δεδομένα ζουν στη μνήμη του υπολογιστή.
Σε αυτή τη διάλεξη βλέπουμε πώς οργανώνεται η μνήμη (bits, bytes, διευθύνσεις), πώς
γράφουμε αριθμούς στο δυαδικό και στο δεκαεξαδικό σύστημα, και πώς η C μάς δίνει
πρόσβαση στη μνήμη μέσα από μεταβλητές: ονόματα για κομμάτια μνήμης με
συγκεκριμένο τύπο. Ο τύπος καθορίζει πόσα bytes πιάνει μια μεταβλητή και πώς
ερμηνεύονται τα bits της (ως χαρακτήρας ASCII, ως ακέραιος με ή χωρίς πρόσημο, ως
πραγματικός). Επειδή τα bytes είναι πεπερασμένα, και οι τιμές που χωράνε είναι
πεπερασμένες: έτσι προκύπτει η υπερχείλιση ακεραίων, πηγή πραγματικών καταστροφών.
Κλείνουμε με την printf, το βασικό μας εργαλείο για να βλέπουμε τις τιμές των
μεταβλητών.
Το bit (binary digit, δυαδικό ψηφίο) είναι η μικρότερη μονάδα πληροφορίας σε
έναν υπολογιστή: παίρνει μία από δύο τιμές, 0 ή 1. Ένα μόνο bit λέει πολύ λίγα,
γι’ αυτό ο υπολογιστής ομαδοποιεί τα bits. Byte είναι μια ομάδα από bits που
αποθηκεύονται μαζί σε ένα κελί μνήμης. Σχεδόν πάντα ένα byte έχει 8 bits (λέγεται
και octet, οκτάδα), π.χ. 00101010.
Κάθε ένα από τα 8 bits είναι ανεξάρτητα 0 ή 1, άρα υπάρχουν \(2^8 = 256\)
διαφορετικά bytes. Γενικά, με \(n\) bits φτιάχνουμε \(2^n\) διαφορετικούς συνδυασμούς.
Αυτός ο απλός κανόνας εξηγεί όλα τα εύρη τιμών που θα δούμε παρακάτω.
Η μνήμη είναι μια μεγάλη σειρά από bytes, το ένα μετά το άλλο: Byte 0, Byte 1,
Byte 2, … . Το μέγεθός της μετριέται σε bytes: 1 KB (KiloByte) = 1.000 bytes,
1 MB (MegaByte) = 1.000.000 bytes, 1 GB (GigaByte) = 1.000.000.000 bytes. Οι
σημειώσεις του μαθήματος χρησιμοποιούν τις δυνάμεις του 2 (1 KB = \(2^{10} = 1024\)
bytes κ.ο.κ.)· συναντώνται και οι δύο ορισμοί.
Η θέση ενός κελιού στη μνήμη λέγεται διεύθυνση (address). Μια μνήμη με \(N\) bytes
έχει διευθύνσεις από \(0\) έως \(N-1\). Για παράδειγμα, αν στο Byte 2 είναι
αποθηκευμένο το 11100011, λέμε ότι «στη διεύθυνση 2 υπάρχει το byte
\(11100011_{(2)}\)». Ο επεξεργαστής διαβάζει και γράφει bytes δίνοντας τη διεύθυνσή
τους. Οι διευθύνσεις θα γίνουν κεντρικές όταν φτάσουμε στους δείκτες
(Κεφάλαιο 11).
Η δευτερεύουσα μνήμη (secondary memory) αποθηκεύει δεδομένα μόνιμα, σε υλικό που τα κρατάει ακόμα και χωρίς ρεύμα: σκληροί δίσκοι, flash drives (USB sticks), DVD. Η κύρια μνήμη (primary memory, η RAM) αποθηκεύει δεδομένα προσωρινά (χάνονται όταν σβήσει ο υπολογιστής), αλλά ο επεξεργαστής έχει άμεση πρόσβαση σε αυτά και μπορεί να τα επεξεργαστεί. Εκεί ζουν οι μεταβλητές ενός προγράμματος όσο τρέχει.
Όταν λέμε σκέτο «μνήμη», εννοούμε την κύρια μνήμη, εκτός αν διευκρινίσουμε κάτι άλλο.
Ένας δυαδικός αριθμός γράφεται με δύο σύμβολα, το 0 και το 1· λέμε ότι
εκφράζεται με βάση το 2, ή στο δυαδικό σύστημα, ή ότι έχει δυαδική αναπαράσταση.
Όπως στο δεκαδικό, κάθε ψηφίο έχει βάρος μια δύναμη της βάσης, με το δεξιότερο ψηφίο
να έχει βάρος \(2^0\). Έτσι
Ο δείκτης \((2)\) ή \((10)\) δείχνει τη βάση. Αντιστρέφοντας τη διαδικασία βρίσκουμε τη δυαδική αναπαράσταση οποιουδήποτε δεκαδικού αριθμού (δείτε το παράδειγμα «Μετατροπές ανάμεσα σε βάσεις»).
Ένας δεκαεξαδικός αριθμός (hexadecimal) χρησιμοποιεί 16 σύμβολα: 0–9 και
A–F, όπου A = 10, …, F = 15. Η μετατροπή γίνεται με τον ίδιο τρόπο:
Γιατί μας ενδιαφέρει το δεκαεξαδικό; Επειδή \(16 = 2^4\), κάθε δεκαεξαδικό ψηφίο
αντιστοιχεί ακριβώς σε 4 bits, οπότε ένα byte γράφεται πάντα με δύο δεκαεξαδικά
ψηφία (00 έως FF). Στο δεκαδικό χρειάζονται έως τρία ψηφία (0 έως 255), και
τα ψηφία δεν αντιστοιχούν σε συγκεκριμένα bits. Για παράδειγμα, το ίδιο byte γράφεται:
| Βάση | Αναπαράσταση |
|---|---|
| Δυαδικό | 0101 1111 |
| Δεκαεξαδικό | 5F (0101 = 5, 1111 = F) |
| Δεκαδικό | 95 (\(64 + 16 + 8 + 4 + 2 + 1\)) |
Υπάρχει και το οκταδικό σύστημα (octal, βάση 8, ψηφία 0–7), όπου κάθε ψηφίο
αντιστοιχεί σε 3 bits. Στη C και τα δύο εμφανίζονται στις σταθερές: 0x2A είναι
δεκαεξαδικό, 052 (με μηδενικό μπροστά) είναι οκταδικό.
Μεταβλητή (variable) είναι ένα τμήμα της μνήμης με συγκεκριμένο όνομα. Για να χρησιμοποιηθεί μια μεταβλητή στη C πρέπει πρώτα να δηλωθεί (declaration) με κάποιον τύπο (type). Η μορφή της δήλωσης είναι
τύπος όνομα;
Κάθε μέρος της δήλωσης λέει κάτι στον μεταγλωττιστή:
Για παράδειγμα, με int x; ο μεταγλωττιστής μπορεί να δεσμεύσει 4 bytes για το x
ξεκινώντας από το Byte 0, ενώ με char c; 1 byte, π.χ. το Byte N-1. Ποια
ακριβώς διεύθυνση θα επιλεγεί δεν το ελέγχουμε εμείς· το σημαντικό είναι ότι το όνομα
και ο τύπος αρκούν για να βρεθούν τα σωστά bytes.
Τα bits στη μνήμη δεν έχουν από μόνα τους νόημα· το νόημα το δίνει ο τύπος. Τα 8 bits
ενός char μπορούν να ερμηνευθούν ως χαρακτήρας κειμένου, με την αντιστοίχιση
του πίνακα ASCII. Για παράδειγμα, ο αριθμός \(67_{(10)} = 43_{(16)}\) αντιστοιχεί
στο γράμμα 'C', το \(65\) στο 'A', το \(10\) στην αλλαγή γραμμής '\n' και το \(0\)
στον «κενό» χαρακτήρα '\0'.
Ο πίνακας ASCII έχει 128 χαρακτήρες (κωδικοί 0–127), επειδή είναι κώδικας 7 bits:
\(2^7 = 128\). Οι πρώτοι 32 είναι χαρακτήρες ελέγχου (αλλαγή γραμμής, tab, «καμπανάκι»
κ.ά.) και οι υπόλοιποι γράμματα, ψηφία και σύμβολα. Ολόκληρο τον πίνακα τον βλέπετε
στο τερματικό με την εντολή man ascii: η man είναι η εντολή του Linux που μας
δίνει το εγχειρίδιο (manual) για ένα πρόγραμμα ή θέμα του συστήματος.
Ο τύπος int, που αναπαριστά ακεραίους, πιάνει συνήθως 4 bytes, δηλαδή 32 bits. Ο
αριθμός 42 αποθηκεύεται ως
00000000000000000000000000101010 (4 bytes = 32 bit)
και η μετατροπή στο δεκαδικό γίνεται όπως πριν, από το δεξιότερο bit: \(0 \cdot 2^0 + 1 \cdot 2^1 + 0 \cdot 2^2 + 1 \cdot 2^3 + \dots = 42\).
Με 32 bits αναπαριστούμε \(2^{32} = 4.294.967.296\) διαφορετικούς ακεραίους. Αυτό φέρνει δύο προβλήματα:
Στο συμπλήρωμα ως προς 2 (two’s complement) το πρόσημο φαίνεται από το
σημαντικότερο (αριστερότερο) bit του αριθμού: 0 σημαίνει θετικό (ή μηδέν), 1
σημαίνει αρνητικό. Για να πάρουμε την αναπαράσταση του \(-N\) από έναν θετικό δυαδικό
\(N\):
flowchart LR
A["N = 11 = 00001011"] -->|"flip όλα τα bits"| B["11110100"]
B -->|"+ 1"| C["-N = 11110101"]
Σχήμα: τα δύο βήματα για το \(-11\) σε 8 bits.
Γιατί αυτή η επιλογή; Επειδή με αυτήν η πρόσθεση δουλεύει με το ίδιο κύκλωμα για
θετικούς και αρνητικούς: αν προσθέσετε 00001011 και 11110101, παίρνετε
1 00000000, και το κρατούμενο που περισσεύει από τα 8 bits χάνεται, οπότε μένει \(0\).
Συνέπεια για τα εύρη: με \(n\) bits ένας προσημασμένος ακέραιος παίρνει τιμές από
\(-2^{n-1}\) έως \(2^{n-1}-1\) (για 8 bits: \(-128\) έως \(127\)), ενώ ένας μη προσημασμένος
(unsigned) από \(0\) έως \(2^n - 1\) (για 8 bits: \(0\) έως \(255\)).
Ο πίνακας συνοψίζει τους βασικούς τύπους, με τα συνηθισμένα μεγέθη που δίνουν οι διαφάνειες. Τα μεγέθη δεν είναι εγγυημένα: εξαρτώνται από τον μεταγλωττιστή και το σύστημα.
| Τύπος | Συνήθως (bytes) | Εύρος τιμών | Παράδειγμα τιμής |
|---|---|---|---|
char |
1 | \(-128\) … \(127\) | 'B', 0x42 |
short int |
2 | \(-32.768\) … \(32.767\) | 42 |
int |
4 | \(-2.147.483.648\) … \(2.147.483.647\) | 42 |
long int |
4 | όπως το int |
42L |
float |
4 | θετικές από \(1.17 \cdot 10^{-38}\) έως \(3.4 \cdot 10^{38}\) | 42.42F |
double |
8 | θετικές από \(2.2 \cdot 10^{-308}\) έως \(1.8 \cdot 10^{308}\) | 42.42 |
long double |
8, 10, 12, 16 | 42.42L |
|
unsigned char |
1 | \(0\) … \(255\) | 'B', 0x42 |
unsigned short int |
2 | \(0\) … \(65.535\) | 42 |
unsigned int |
4 | \(0\) … \(4.294.967.295\) | 42 |
unsigned long int |
4 | \(0\) … \(4.294.967.295\) | 42LU |
Παρατηρήστε ότι:
unsigned εκδοχή με το ίδιο μέγεθος, που δεν
αναπαριστά αρνητικούς και φτάνει στο διπλάσιο θετικό όριο·L για long, U για
unsigned, F για float· ένας πραγματικός χωρίς κατάληξη (42.42) είναι
double·long int είναι 4 bytes σε κάποια συστήματα (π.χ. Windows) αλλά 8 bytes στο
Linux 64 bit που χρησιμοποιούμε στο εργαστήριο. Ο long long int (δεν είναι στον
πίνακα των διαφανειών) είναι συνήθως 8 bytes σε όλα.Ανάθεση (assignment) είναι η αποθήκευση μιας τιμής σε μια μεταβλητή. Μπορεί να γίνει κατά τον ορισμό της (αυτό λέγεται αρχικοποίηση, initialization) ή αργότερα:
int x = 42; // ανάθεση κατά τον ορισμό
int y;
y = 42; // ανάθεση μετά τον ορισμό
int z = 0x2A; // η ίδια τιμή σε δεκαεξαδικό
int w = 052; // και σε οκταδικό
Και οι τέσσερις μεταβλητές έχουν την ίδια τιμή: \(42\). Η βάση στην οποία γράφουμε μια σταθερά αφορά μόνο τον πηγαίο κώδικα· στη μνήμη αποθηκεύονται πάντα τα ίδια bits.
Τι υπάρχει σε μια μεταβλητή πριν την ανάθεση; Ό,τι έτυχε να βρίσκεται σε εκείνα τα
bytes: ο μεταγλωττιστής δεσμεύει τη μνήμη, αλλά δεν τη «καθαρίζει». Γι’ αυτό μια
μεταβλητή χωρίς ανάθεση έχει απρόβλεπτη τιμή («σκουπίδια») και δεν πρέπει να τη
διαβάζουμε. Μετά την x = 42;, τα 4 bytes της x γίνονται:
| Byte | Πριν την ανάθεση | Μετά την ανάθεση |
|---|---|---|
Byte 0 |
00101010 |
00000000 |
Byte 1 |
01000010 |
00000000 |
Byte 2 |
11100011 |
00000000 |
Byte 3 |
11100011 |
00101010 |
Η διάταξη αυτή (το λιγότερο σημαντικό byte στο τέλος) είναι η σχηματική των
διαφανειών. Στους επεξεργαστές x86 που χρησιμοποιούμε, τα bytes ενός int
αποθηκεύονται με την αντίστροφη σειρά (little endian)· το θέμα επανέρχεται στο
Κεφάλαιο 12.
Αφού ένας int χωράει τιμές έως \(2.147.483.647\), τι γίνεται αν ένας υπολογισμός
βγάλει μεγαλύτερο αποτέλεσμα; Τα bits που δεν χωράνε χάνονται και το αποτέλεσμα
«τυλίγεται» γύρω από το εύρος: αυτό λέγεται υπερχείλιση ακεραίων (integer
overflow). Για παράδειγμα, \(2.000.000.000 + 2.000.000.000 = 4.000.000.000\) δεν χωράει
σε int, και το πρόγραμμα τυπώνει \(4.000.000.000 - 2^{32} = -294.967.296\) (δείτε το
παράδειγμα «Το πρόγραμμα overflow»). Όπως στα γνωστά
σκίτσα με τα προβατάκια: μετράμε 32767 και το επόμενο είναι -32768.
Για προσημασμένους ακεραίους, η υπερχείλιση είναι στην πραγματικότητα
απροσδιόριστη συμπεριφορά (undefined behavior) στη C: το «τύλιγμα» είναι αυτό που
συνήθως βλέπουμε, αλλά ο μεταγλωττιστής δεν το εγγυάται. Για unsigned τύπους το
τύλιγμα (modulo \(2^n\)) είναι εγγυημένο. Η διόρθωση είναι πάντα η ίδια: επιλέξτε
τύπο με αρκετό εύρος για τις τιμές που θα χρειαστείτε.
Η υπερχείλιση δεν είναι θεωρητικό πρόβλημα. Η πρώτη πτήση του πυραύλου Ariane 5 (1996) κατέληξε σε καταστροφή, επειδή μια πραγματική τιμή μετατράπηκε σε ακέραιο 16 bit που δεν τη χωρούσε. Το πρόβλημα του 2000 (αποθήκευση του έτους με δύο ψηφία) και το πρόβλημα του 2038 είναι συγγενικά προβλήματα αναπαράστασης.
Είπαμε ότι ο int είναι συνήθως 4 bytes. Υπάρχουν δύο τρόποι να είμαστε βέβαιοι:
sizeof δίνει το μέγεθος ενός τύπου ή μιας μεταβλητής σε bytes,
π.χ. sizeof(int). Θα τον δούμε αναλυτικά σε επόμενες διαλέξεις.<stdint.h> ορίζει ακέραιους τύπους με εγγυημένο πλήθος bits:
int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t,
uint64_t. Ο αριθμός είναι τα bits, και το u σημαίνει unsigned.Η συνάρτηση printf() τυπώνει δεδομένα στο αρχείο εξόδου stdout (standard
output), που συνήθως είναι το τερματικό. Έχει μεταβλητή λίστα παραμέτρων:
" ", που καθορίζει πώς θα
τυπωθούν τα δεδομένα.printf() χρησιμοποιεί
τις τιμές τους.Το format string μπορεί να περιέχει τρία είδη πραγμάτων: απλούς χαρακτήρες, που
τυπώνονται όπως είναι· ακολουθίες διαφυγής (escape sequences)· και
προσδιοριστικά μορφοποίησης (format specifiers). Για παράδειγμα, στο
printf("x = %d\n", x); το x = τυπώνεται αυτούσιο, το %d αντικαθίσταται από
την τιμή του x ως ακέραιος και το \n αλλάζει γραμμή.
Μια ακολουθία διαφυγής αποτελείται από μια ανάστροφη κεκλιμένη \ (backslash) και
έναν χαρακτήρα. Χρησιμεύει για χαρακτήρες που δεν γράφονται εύκολα μέσα σε
εισαγωγικά: αλλαγή γραμμής, tab, ή τα ίδια τα " και \.
| Ακολουθία | Σημασία |
|---|---|
\n |
Αλλαγή γραμμής, σαν το πλήκτρο Enter |
\r |
Επαναφορά του δρομέα (cursor) στην αρχή της τρέχουσας γραμμής |
\t |
Κενό ίσο με ένα tab, σαν το πλήκτρο Tab |
\\ |
Ανάστροφη κεκλιμένη (backslash) |
\" |
Διπλά εισαγωγικά (double quotes) |
\xNN |
Ο χαρακτήρας με κωδικό NN σε δεκαεξαδικό |
\a |
Ηχητικό σήμα (bell) |
Οι ακολουθίες διαφυγής είναι χαρακτήρες ASCII: στον πίνακα της man ascii θα δείτε
το '\n' με κωδικό 10, το '\t' με 9, το '\a' με 7. Έτσι το "\x43" τυπώνει C.
Ένα προσδιοριστικό μορφοποίησης αποτελείται από τον χαρακτήρα % και έναν ή
περισσότερους χαρακτήρες που λένε πώς να γίνει η μορφοποίηση. Κάθε προσδιοριστικό
«καταναλώνει» με τη σειρά την επόμενη παράμετρο της printf.
| Προσδιοριστικό | Σημασία |
|---|---|
%c |
Χαρακτήρας ASCII |
%d ή %i |
Ακέραιος (int) |
%u |
Μη προσημασμένος (unsigned) ακέραιος |
%f |
Πραγματικός (float ή double) |
%llu |
unsigned long long int |
%% |
Ο ίδιος ο χαρακτήρας % |
Τα προσδιοριστικά πρέπει να ταιριάζουν με τον τύπο της τιμής: το %d περιμένει
int, το %lld long long, το %ld long. Η πλήρης λίστα είναι στο
εγχειρίδιο της printf· το
Εργαστήριο 3 έχει έναν
συνοπτικό πίνακα. Το ίδιο byte μπορεί να τυπωθεί με διαφορετικούς τρόπους: με %c
ένας char που έχει τιμή 67 τυπώνεται C, με %d τυπώνεται 67.
Μεταβλητές του ίδιου τύπου μπορούν να δηλωθούν στην ίδια γραμμή, χωρισμένες με κόμμα. Οι δύο παρακάτω μορφές είναι ισοδύναμες:
int a;
int b;
int c;
int a, b, c;
Τα ονόματα μεταβλητών αποτελούνται από γράμματα, ψηφία και _, και δεν ξεκινούν με
ψηφίο. Οι δεσμευμένες λέξεις (reserved keywords) της C έχουν προκαθορισμένο
νόημα για τον μεταγλωττιστή και δεν επιτρέπεται να χρησιμοποιηθούν ως ονόματα
μεταβλητών ή συναρτήσεων:
auto do goto signed unsigned
break double if sizeof void
case else int static volatile
char enum long struct while
const extern register switch
continue for return typedef
default float short union
Πολλές από αυτές τις γνωρίσατε ήδη (int, char, return)· οι υπόλοιπες θα
εμφανιστούν στα επόμενα κεφάλαια.
Εφαρμόζει: «Δυαδικό, δεκαεξαδικό και οκταδικό σύστημα».
Από δυαδικό σε δεκαδικό: πολλαπλασιάζουμε κάθε ψηφίο με τη δύναμη του 2 της θέσης
του και προσθέτουμε: \(101010_{(2)} = 32 + 8 + 2 = 42\). Με τα βάρη
128 64 32 16 8 4 2 1 γραμμένα πάνω από τα bits, το 01011111 δίνει
\(64 + 16 + 8 + 4 + 2 + 1 = 95\).
Από δεκαδικό σε δυαδικό: αντιστρέφουμε τη διαδικασία. Διαιρούμε διαδοχικά με το 2 και κρατάμε τα υπόλοιπα· διαβασμένα από το τελευταίο προς το πρώτο, δίνουν τα bits:
42 / 2 = 21 υπόλοιπο 0
21 / 2 = 10 υπόλοιπο 1
10 / 2 = 5 υπόλοιπο 0
5 / 2 = 2 υπόλοιπο 1
2 / 2 = 1 υπόλοιπο 0
1 / 2 = 0 υπόλοιπο 1 -> 42 = 101010 (2)
Από δυαδικό σε δεκαεξαδικό και πίσω: χωρίζουμε σε τετράδες από δεξιά και
μετατρέπουμε κάθε τετράδα σε ένα ψηφίο: 0101 1111 → 5F, 0010 1010 → 2A.
Αντίστροφα, 2A → 0010 1010. Έτσι τα 42, 0x2A και 052 στη C είναι ο ίδιος
αριθμός.
Εφαρμόζει: «Ερμηνεία δυαδικών ψηφίων: χαρακτήρες ASCII».
Η διάλεξη ρώτησε: ποια εντολή του Linux μάς επιτρέπει να μάθουμε για ένα πρόγραμμα;
Η απάντηση είναι η man, που δεν περιορίζεται σε προγράμματα:
man ascii
Ανοίγει τη σελίδα ascii(7) με στήλες Oct, Dec, Hex, Char. Εκεί διαβάζετε,
για παράδειγμα:
Oct Dec Hex Char
103 67 43 C
012 10 0A LF '\n' (new line)
Βγαίνετε με q. Το ίδιο κάνετε και για τις συναρτήσεις της C: man 3 printf.
Εφαρμόζει: «Συμπλήρωμα ως προς 2».
Έστω \(N = 11_{(10)} = 00001011_{(2)}\) σε 8 bits. Για το \(-N\):
N 00001011
flip 11110100
+ 1 + 00000001
-N 11110101
Επαλήθευση: το σημαντικότερο bit είναι 1, άρα ο αριθμός είναι αρνητικός· και
00001011 + 11110101 = 1 00000000, όπου το 9ο bit χάνεται και μένει \(0 = 11 + (-11)\).
Αν διαβάσουμε τα ίδια bits ως unsigned char, παίρνουμε \(245 = 256 - 11\): τα bits
είναι ίδια, αλλάζει μόνο η ερμηνεία.
Εφαρμόζει: «Υπερχείλιση ακεραίων», «Οι τύποι μεταβλητών της C».
Τι θα τυπώσει το παρακάτω πρόγραμμα;
#include <stdio.h>
int main() {
printf("%d\n", 2000000000 + 2000000000);
return 0;
}
$ ./overflow
-294967296
Τι συνέβη; Και οι δύο σταθερές είναι int, άρα και το άθροισμα υπολογίζεται ως
int. Το σωστό αποτέλεσμα, \(4.000.000.000\), ξεπερνάει το μέγιστο \(2.147.483.647\), και
τα 32 bits που μένουν ερμηνεύονται σε συμπλήρωμα ως προς 2 ως
\(4.000.000.000 - 2^{32} = -294.967.296\). Ο gcc συνήθως προειδοποιεί ήδη στη
μεταγλώττιση (integer overflow in expression of type 'int')· μην αγνοείτε τις
προειδοποιήσεις.
Πώς το διορθώνουμε; Κάνουμε τον υπολογισμό σε τύπο 64 bit και τυπώνουμε με το αντίστοιχο προσδιοριστικό:
#include <stdio.h>
int main() {
printf("%lld\n", 2000000000LL + 2000000000LL);
return 0;
}
$ ./overflow
4000000000
Η κατάληξη LL κάνει τις σταθερές long long. Αφού το αποτέλεσμα είναι θετικό, θα
μπορούσαμε να χρησιμοποιήσουμε και unsigned int με %u (χωράει έως
\(4.294.967.295\)), αλλά με πολύ μικρό περιθώριο.
Εφαρμόζει: «Ερμηνεία δυαδικών ψηφίων», «Ακολουθίες διαφυγής», «Προσδιοριστικά μορφοποίησης». Το πρόγραμμα δεν είναι από τις διαφάνειες· συνδυάζει τους πίνακές τους για να τους δοκιμάσετε:
#include <stdio.h>
int main() {
char c = 'C';
unsigned int u = 4000000000U;
printf("c = %c (%d), x = %d\n", c, c, 0x2A);
printf("u = %u, sizeof(int) = %zu\n", u, sizeof(int));
printf("100%% \"done\"\tbye\\\n");
return 0;
}
$ ./types
c = C (67), x = 42
u = 4000000000, sizeof(int) = 4
100% "done" bye\
Ο ίδιος char τυπώνεται ως γράμμα με %c και ως κωδικός ASCII με %d· το
4000000000 χωράει σε unsigned int αλλά όχι σε int· το αποτέλεσμα της sizeof
τυπώνεται με %zu· στην τελευταία γραμμή, %% δίνει %, \" δίνει ", \t ένα
tab και \\ μια \.
number του readint.c πριν το διαβάσετε: θα δείτε ό,τι είχε η μνήμη. Η
επέκταση «Υψηλή ακρίβεια» του calc.c ζητάει αριθμούς έως \(10^{18}\), που δεν
χωράνε σε int· χρειάζεστε long long με %lld.%d, %ld, %lld, %u, %lu, %llu, %f,
%Lf). Παράξενα αρνητικά αποτελέσματα για μεγάλους αριθμούς σημαίνουν σχεδόν
πάντα υπερχείλιση.man ascii), ακέραιος ή πραγματικός.int είναι συνήθως 4 bytes (32 bits), άρα έχει πεπερασμένο εύρος· τα μεγέθη
των τύπων είναι «συνήθη» και όχι εγγυημένα, και κάθε ακέραιος έχει unsigned
εκδοχή.0x2A) ή οκταδικό (052)· πριν την πρώτη ανάθεση περιέχει ό,τι
υπήρχε στη μνήμη.2000000000 + 2000000000 σε int τυπώνει -294967296· η διόρθωση είναι τύπος με
μεγαλύτερο εύρος.sizeof μαθαίνουμε το μέγεθος ενός τύπου, και με το <stdint.h> δηλώνουμε
ακεραίους με ακριβές πλήθος bits (int32_t, uint64_t κ.ά.).printf τυπώνει στο stdout· το format string περιέχει απλούς χαρακτήρες,
ακολουθίες διαφυγής (\n, \t, \\, \") και προσδιοριστικά (%c, %d, %u,
%f, %llu, %%) που πρέπει να ταιριάζουν με τους τύπους των τιμών.int a, b, c;), και οι
δεσμευμένες λέξεις δεν μπορούν να γίνουν ονόματα.| Ελληνικά | English | Σύντομος ορισμός |
|---|---|---|
| δυαδικό ψηφίο | bit (binary digit) | Η μικρότερη μονάδα πληροφορίας: 0 ή 1 |
| byte, οκτάδα | byte, octet | Ομάδα (συνήθως 8) bits σε ένα κελί μνήμης |
| διεύθυνση | address | Η θέση ενός κελιού (byte) στη μνήμη |
| κύρια μνήμη | primary memory (RAM) | Προσωρινή μνήμη με άμεση πρόσβαση από τον επεξεργαστή |
| δευτερεύουσα μνήμη | secondary memory | Μόνιμη αποθήκευση: δίσκοι, flash, DVD |
| δυαδικό σύστημα | binary | Αρίθμηση με βάση το 2 |
| δεκαεξαδικό σύστημα | hexadecimal | Αρίθμηση με βάση το 16 (0–9, A–F) |
| οκταδικό σύστημα | octal | Αρίθμηση με βάση το 8 |
| μεταβλητή | variable | Τμήμα της μνήμης με όνομα και τύπο |
| δήλωση | declaration | Εισαγωγή μεταβλητής με τύπο και όνομα |
| τύπος | type | Πόση μνήμη πιάνει μια τιμή και πώς ερμηνεύεται |
| ανάθεση | assignment | Αποθήκευση τιμής σε μεταβλητή (x = 42;) |
| αρχικοποίηση | initialization | Ανάθεση κατά τον ορισμό (int x = 42;) |
| πίνακας ASCII | ASCII table | Αντιστοίχιση κωδικών 0–127 σε χαρακτήρες |
| συμπλήρωμα ως προς 2 | two’s complement | Αναπαράσταση αρνητικών: flip όλα τα bits και +1 |
| μη προσημασμένος | unsigned | Ακέραιος τύπος χωρίς αρνητικές τιμές |
| υπερχείλιση ακεραίων | integer overflow | Αποτέλεσμα που δεν χωράει στον τύπο του |
| αλφαριθμητικό μορφοποίησης | format string | Η πρώτη παράμετρος της printf |
| ακολουθία διαφυγής | escape sequence | \ και ένας χαρακτήρας, π.χ. \n |
| προσδιοριστικό μορφοποίησης | format specifier | % και χαρακτήρες, π.χ. %d |
| τυπική έξοδος | standard output (stdout) | Το αρχείο όπου γράφει η printf |
| δεσμευμένη λέξη | reserved keyword | Λέξη της C που δεν γίνεται όνομα (int, if, …) |
<stdint.h>: σελ. 26–27· printf: σελ. 28–30· δηλώσεις πολλών μεταβλητών και δεσμευμένες λέξεις: σελ. 31–32.readint.c), ασκήσεις calc.c (με την επέκταση «Υψηλή ακρίβεια») και pyth.cprintf και η άσκηση seq.cprintf: tips και referenceman ascii, man 3 printfint. Γινόμενα ή αθροίσματα μεγάλων αριθμών βγαίνουν ξαφνικά
αρνητικά (2000000000 + 2000000000 → -294967296). Διόρθωση: long long με
%lld, ή unsigned long long με %llu, και σταθερές με κατάληξη LL.long long r = 2000000000 + 2000000000;
εξακολουθεί να υπερχειλίζει, γιατί το άθροισμα υπολογίζεται σε int πριν την
ανάθεση. Διόρθωση: 2000000000LL + 2000000000.printf. printf("%d\n", x) με long long x ή
printf("%d\n", 3.14) τυπώνει σκουπίδια· ο gcc με -Wall προειδοποιεί
(format '%d' expects argument of type 'int'). Διόρθωση: το προσδιοριστικό του
τύπου (%lld, %f, %zu για sizeof).int n; printf("%d\n", n); τυπώνει ό,τι
υπήρχε στη μνήμη. Διόρθωση: αρχικοποιείτε πάντα πριν διαβάσετε.int x = 052; δεν είναι 52 αλλά 42, γιατί είναι
οκταδικό. Διόρθωση: μη γράφετε αρχικά μηδενικά σε δεκαδικές σταθερές.% ή \ χωρίς διαφυγή. printf("100%\n") δεν τυπώνει 100%. Διόρθωση:
%% και \\.int int = 5; ή int new, for; δεν μεταγλωττίζεται
(expected identifier). Διόρθωση: άλλο όνομα.Από τα Kahoot των διαλέξεων: οι ερωτήσεις όπου μια λάθος απάντηση μάζεψε πολλές ψήφους, με το ποσοστό σωστών απαντήσεων.
0x50, κάνοντας την πρόσθεση σαν να ήταν δεκαδικό (49 + 1 = 50)· στο δεκαεξαδικό μετά το 9 έρχεται το A.0xFF ως unsigned είναι 255)· σε signed char με συμπλήρωμα ως προς 2 όλοι οι άσοι σημαίνουν αρνητικό αριθμό.int μπορεί να είναι 2 bytes.int64_t, που είναι ο τύπος στον οποίο μετέβη η Google μετά· ένας 64-bit ακέραιος δεν θα κόντευε ποτέ να υπερχειλίσει στα 2 δις.int (όπου η υπερχείλιση δίνει αρνητικό) στους unsigned· ένας unsigned int αναδιπλώνεται modulo \(2^{32}\) και δεν γίνεται ποτέ αρνητικός (αυστηρά, μπορεί να βγει 0 ή μικρότερος από τους προσθετέους).int x;;3char έχει εύρος \(-128\) έως \(127\) ενώ ο unsigned char \(0\) έως \(255\);5int y = 0x10 + 010;;6printf("%c%c\n", 72, 0x69);;7long long r = 2000000000 + 2000000000; δεν δίνει 4000000000;8unsigned int και ποιο για να τυπώσετε το σύμβολο %;9Ερωτήσεις που παίχτηκαν στις διαλέξεις, με το ποσοστό των φοιτητών που απάντησαν σωστά.
slides-lec02-ascii-countslides-lec02-byte-valuesslides-lec02-hex-digitsslides-lec02-int-sizeslides-lec02-int32-countslides-lec02-manslides-lec02-twos-complementslides-lec02-overflowlab-lab00-aboutlab-lab00-overflowlab-lab04-printcharexam-2023-fall-ex4-q3exam-2024-sep-q1slides-lec03-power-outageexam-2023-fall-ex15-q2exam-2023-fall-ex6-q1exam-2025-sep-q1exam-2026-sep-q1hw-2023-hw0-collatzhw-2025-hw0-aliquotslides-lec08-product-overflowexam-2023-fall-ex1-q1exam-2024-jul-q1hw-2024-hw0-trolleylab-lab02-calclab-lab02-readintlab-lab04-encode-decodeexam-2026-jun-q2exam-2025-sep-q3lab-lab08-legolasexam-2025-jan-q2hw-2023-hw1-mirrorhw-2024-hw1-factorexam-2023-fall-ex0-q4exam-2023-fall-ex11-q4exam-2023-fall-ex13-q4exam-2024-dec-q2hw-2025-bonus0-stergioslab-lab05-ladderslides-lec24-binary-literal\(2^{16} = 65.536\), όσες και οι τιμές του short ή του unsigned short. ↩
\(64 + 32 + 4 = 100_{(10)}\)· σε τετράδες 0110 0100, δηλαδή \(64_{(16)}\). ↩
Από τον τύπο πόσα bytes θα δεσμεύσει (και πώς θα τα ερμηνεύει)· από το όνομα, σε ποια διεύθυνση θα αποθηκεύσει τη μεταβλητή, ώστε να τη βρίσκει κάθε φορά που τη χρησιμοποιούμε. ↩
00000001 → flip 11111110 → +1 11111111. ↩
Και οι δύο έχουν 8 bits, δηλαδή 256 τιμές. Ο προσημασμένος τις μοιράζει σε αρνητικές και μη αρνητικές (\(-2^7\) έως \(2^7 - 1\)), ο unsigned τις κρατάει όλες για μη αρνητικές (\(0\) έως \(2^8 - 1\)). ↩
\(16 + 8 = 24\): το 0x10 είναι δεκαεξαδικό και το 010 οκταδικό. ↩
Hi: 72 είναι ο κωδικός ASCII του H και \(69_{(16)} = 105\) του i. ↩
Οι δύο σταθερές είναι int, οπότε το άθροισμα υπολογίζεται σε int και υπερχειλίζει πριν ανατεθεί στο r. Χρειάζεται 2000000000LL. ↩
%u για unsigned int και %% για το %. ↩