Hello, world!στην οθόνη του υπολογιστή.
1 #include <stdio.h> 2 3 main() 4 { 5 printf("Hello, world!\n"); 6 }
Οι αριθμοί που εμφανίζονται μπροστά από κάθε γραμμή δεν υπάρχουν στην πραγματικότητα αλλά έχουν προστεθεί για να μας διευκολύνουν να αναφερόμαστε στις διάφορες γραμμές του κώδικα, κατά την ανάλυση που κάνουμε εδώ.
Ότι πούμε παρακάτω ισχύει στο λειτουργικό σύστημα Unix. Σε άλλα λειτουργικά συστήματα (π.χ. MS Windows) και ανάλογα με το software που χρησιμοποιείται, δηλ. ανάλογα με τον ποιόν compiler χρησιμοποιούμε, η διαδικασία μπορεί να είναι από αρκετά δαφορετική έως πολύ όμοια.
Κατ' αρχήν, το C πρόγραμμα (το κείμενο που βλέπετε παραπάνω) φτιάχνεται
και αποθηκεύεται σε ένα αρχείο (file), στο οποίο δίνουμε ένα περιγραφικό
όνομα και την κατάληξη2.2.c
(στο συγκεκριμένο πρόγραμμα δώσαμε το
όνομα hello.c
, για παράδειγμα).
Το κείμενο δημιουργήθηκε χρησιμοποιώντας ένα text editor.
Υπάρχουν πολλοί text editors στο Unix, με αρκετά διαφορετικά χαρακτηριστικά
ο καθένας, και προτείνεται, για αρχή τουλάχιστον, να χρησιμοποιείτε τον
pico
, ο οποίος είναι πολύ εύχρηστος. Για να δημιουργήσετε
λοιπόν το αρχείο hello.c
θα ξεκινήσετε δίνοντας την εντολή
(από δω και πέρα οι γραμμές που αρχίζουν με %
θα παριστάνουν εντολές
που δίνετε στο command line - γραμμή εντολών - του Unix):
% pico hello.cΑφού τελειώσετε με το γράψιμο του κειμένου το αποθηκεύετε και βγαίνετε από το περιβάλλον του
pico
.
Αν τα κάνετε όλα σωστά υπάρχει πλέον στην περιοχή σας ένα αρχείο
με όνομα hello.c
.
Προσέξτε γιατί στο Unix και στη C τα κεφαλαία και τα μικρά γράμματα
θεωρούνται διαφορετικά, για παράδειγμα τα ονόματα hello.c
και Hello.c
θεωρούνται διαφορετικά από το Unix και μπορεί κάλλιστα
να έχετε δυο διαφορετικά αρχεία με αυτά τα ονόματα.
Για να βεβαιωθείτε ότι έχετε όντως το αρχείο στην περιοχή σας δώστε
την εντολή2.3
% cat hello.cκαι, αν όλα έχουν πάει καλά, θα δείτε τα περιεχόμενα του αρχείου να τυπώνονται στην οθόνη σας.
Έχοντας βεβαιωθεί πια ότι το αρχείο hello.c
υπάρχει και έχει τα σωστά περιεχόμενα, μπορούμε να προχωρήσουμε
στη φάση του compilation, ώστε να παραχθεί έτσι ένα εκτελέσιμο
πρόγραμμα.
Δώστε την εντολή2.4
% gcc hello.cΑν δεν εκτυπωθούν error messages (αν έχετε το σωστό αρχείο δε θα εκτυπωθούν) τότε μετά από το πέρας αυτής της εντολής θα υπάρχει στην περιοχή σας ένα αρχείο με όνομα
a.out
,
που είναι κατά παράδοση το όνομα που παίρνουν τα εκτελέσιμα αρχεία
στο Unix, αν ο χρήστης δεν κάνει τον κόπο να δηλώσει ότι επιθυμεί ένα
άλλο όνομα.
Αν ο χρήστης θέλει το εκτελέσιμο αρχείο να μην έχει το
καθιερωμένο όνομα a.out (για παράδειγμα αν θέλει να έχει
παραπάνω από ένα εκτελέσιμα αρχεία στην περιοχή του) τότε
μπορεί να πεί στον compiler τι όνομα να δώσει σε αυτό ως εξής:
% gcc hello.c -o helloΑυτό φτιάχνει ένα εκτελέσιμο αρχείο με όνομα hello (είναι σύνηθες στη C το όνομα του εκτελέσιμοθ να είναι το ίδιο με το όναμα του αρχείου του προγράμματος χωρίς κατάληξη, αν πρόκειται για το Unix, ή με κατάληξη .exe στα MS Windows).
Για να βεβαιωθείτε ότι υπάρχει όντως το αρχείο a.out
στην περιοχή σας δώστε την εντολή
% lsη οποία θα τυπώσει στην οθόνη σας τα ονόματα όλων των υπαρχόντων αρχείων (ανάμεσά τους θα πρέπει να είναι τα
hello.c
και a.out
).
Η εντολή
% ls -Fθα πρέπει να τυπώσει κι ένα αστερίσκο (*) δίπλα στο
a.out
το οποίο υποδηλώνει ότι αυτό είναι ένα εκτελέσιμο αρχείο.
Τέλος, εκτελούμε το πρόγραμμά μας δίνοντας την εντολή
% a.out Hello, world! %(Οι γραμμές παραπάνω που δεν αρχίζουν με % υποδηλώνουν το output του προγράμματος που μόλις εκτελέσαμε.)
hello.c
γραμμή προς γραμμή για να
δούμε πως δουλεύει.
Κατ' αρχήν, στη γραμμή 1, βρίσκεται μια εντολή που απευθύνεται στον
cpp, τον C preprossesor, και λέει στον cpp να τοποθετήσει τα περιεχόμενα
του αρχείου stdio.h στη θέση της γραμμής #include
.
Το αρχείο stdio.h (standard input/output header file) δεν
περιέχει κάποιο εκτελέσιμο κώδικα, αλλά κάποιες δηλώσεις σταθερών
και υποπρογραμμάτων (για παράδειγμα, τι είδους παραμέτρους
περιμένει να λάβει το υποπρόγραμμα printf που χρησιμοποιείται
μέσα στο hello.c.
Για να είμαστε ακριβείς, η πρώτη γραμμή δεν είναι απολύτως απαραίτητη
για την επιτυχή μετάφραση και εκτέλεση του συγκεκριμένου προγράμματος,
κι αν σβήσετε την πρώτη γραμμή και επαναλάβετε τον κύκλο μεταγλώττισης
και εκτέλεσης θα δείτε πως το πρόγραμμα θα εκτελεστεί κανονικά.
Είναι όμως λάθος πρακτική να μην συμπεριλαμβάνονται τα απαραίτητα header files (αυτά που κατά παράδοση έχουν την κατάληξη .h) στο υπό μεταγλώττιση πρόγραμμα, μια και περιέχουν πληροφορίες για τα χρησιμοποιούμενα υποπρογράμματα (πληροφορίες και οδηγίες για τον compiler και όχι για τον προγραμματιστή, ο οποίος σπανίως χρειάζεται να εξετάσει τα περιεχόμενα ενός header file εκτός ίσως από αυτά που ο ίδιος έχει γράψει) που είναι πιο συχνά απαραίτητες για να δουλέψει το πρόγραμμα σωστά παρά όχι.
#include
;
Η πληροφορία αυτή βρίσκεται στο manual της γλώσσας, ή,
για να είμαστε πιο ακριβείς, στο manual της standard library
της C, μέρος της οποίας βιβλιοθήκης είναι και η συνάρτηση
(συνάρτηση, υποποπρόγραμμα και υπορουτίνα είναι συνώνυμες λέξεις
στη C) printf.
Στο Unix το manual αυτό βρίσκεται (ή, καλύτερα, πρέπει να βρίσκεται,
μια και δεν είναι όλα τα Unix συστήματα το ίδιο καλά φτιαγμένα ή,
το σημαντικότερο, συντηρημένα) και σε ηλεκτρονική μορφή.
Αν δώσετε την εντολή
% man 3 printfτο σύστημα θα σας δείξει μερικές σελίδες με πληροφορίες για τη συνάρτηση printf και άλλες σχετικές συναρτήσεις. Ανάμεσα στις πληροφορίες αυτές είναι και η συμβουλή να κάνετε
#include
το stdio.h προκειμένου να
χρησιμοποιείσετε την printf.
Τι είναι ο αριθμός 3 αμέσως μετά το όνομα του προγράμματος man; Είναι το τμήμα του manual του συστήματος όπου βρίσκονται οι πληροφορίες για τη standard C library. Αν είχαμε παραλείψει τον αριθμό 3 και είχαμε απλώς γράψει man printf, το σύστημα θα μας τύπωνε πληροφορίες για το πρόγραμμα printf, που είναι ένα αυτοδύναμο πρόγραμμα που υπάρχει στο Unix και που εκτελεί μια παρόμοια δουλειά με αυτή που κάνει η συνάρτηση printf της standard C library. Και πώς θα καταλαβαίναμε τότε σε ποιο τμήμα του manual είναι η printf που μας ενδιαφέρει εμάς; Υπάρχει το option -k (από το keyword) για το πρόγραμμα man που λέει στο πρόγραμμα να τυπώσει μια γραμμή με λίγη πληροφορία για όλα τα σχετικά προγράμματα. Για παράδειγμα, στο Unix σύστημα που δουλεύω αυτή τη στιγμή ιδού το αποτέλεσμα του man -k printf:
% man -k printf fprintf (3) - formatted output conversion gl_printf (3) - write formatted output in graphic mode printf (1) - format and print data printf (3) - formatted output conversion printftest (6) - tests the vgagl gl_printf function snprintf (3) - formatted output conversion sprintf (3) - formatted output conversion vfprintf (3) - formatted output conversion vprintf (3) - formatted output conversion vsnprintf (3) - formatted output conversion vsprintf (3) - formatted output conversion %Το output έχει από μια γραμμή για κάθε τι που βρήκε το man σχετικό με τη λέξη printf. Η πρώτη λέξη είναι το όνομα της σχετικής ``σελίδας'' του manual και εντός παρενθέσεως βρίσκεται το section του manual. Προσέξτε ότι εμφανίζονται δύο printf, ένα στο section 1 κι ένα στο section 3. Κάνοντας man 1 printf και man 3 printf θα σας δείξει πληροφορίες για δύο διαφορετικά πράγματα. Αντίθετα για το vprintf που εμφανίστηκε μόνο του αρκεί να κάνει κανείς man vprintf για να πάρει όλες τις πληροφορίες που υπάρχουν.
Το συντακτικό της εντολής man μπορεί να διαφέρει από σύστημα σε σύστημα. Για παράδειγμα σε ορισμένα συστήματα της Sun χρειάζεται κανείς να γράψει man -s 3 printf αντί για man 3 printf. Δώστε απλώς
% man manγια να βεβαιωθείτε πώς περιμένει τις παραμέτρους του το δικό σας man.
#include <stdio.h> main() { printf("Hello, world!\n"); }Με λίγα λόγια, οι κενές γραμμές και η στοίχιση δεν επηρεάζουν το πως ο compiler αντιλαμβάνεται το πρόγραμμα και άρα είμαστε ελεύθεροι να στοιχίσουμε το κείμενο όπως εμάς εξυπηρετεί. (Οι εντολές όμως προς τον cpp , τα
#include
δηλ. και
τα #define
, που θα τα δούμε αργότερα, υπακούουν σε αρκετά
αυστηρότερους κανόνες στοίχισης, και θα τα γράφουμε πάντα σε μια
γραμμή το καθένα - αν δε ``χωράνε'' θα δούμε αργότερα τι θα κάνουμε -
αρχίζοντας από τα αριστερά της γραμμής.)
Οι γραμμές 3-6 στο πρόγραμμα παριστάνουν την περιγραφή λειτουργίας μιας συνάρτησης (υπορουτίνα, υποπρόγραμμα). Ένα C πρόγραμμα αποτελείται από ένα σύνολο συναρτήσεων, γραμμένες συνήθως η μια μετά την άλλη (δεν επιτρέπονται, όπως στην Pascal, για παράδειγμα, οι δηλώσεις υπορουτινών μέσα σε υπορουτίνες). Από αυτές τις συναρτήσεις ακριβώς μία φέρει το όνομα main και είναι, κατά σύμβαση, η υπορουτίνα που εκτελείται πρώτη, και που είναι άρα υπεύθυνη για να ``καλέσει'' οποιαδήποτε άλλη συνάρτηση απαιτείται. Μετά το πέρας της main το πρόγραμμα έχει τελειώσει.
Στο πρόγραμμα αυτό εμφανίζονται δυο συναρτήσεις:
η main και η printf.
Η πρώτη από αυτές ορίζεται μέσα στο πρόγραμμα ενώ η δεύτερη
όχι, είναι δηλ. η printf μια συνάρτηση βιβλιοθήκης,
και ο compiler (για την ακρίβεια, ο linker2.5ή loader, όπως
λέγεται στο Unix) ξέρει που θα την αναζητήσει για να συμπεριλάβει
τον κώδικα για την printf στο τελικό εκτελέσιμο
πρόγραμμα2.6.
Η main καλείται από το λειτουργικό σύστημα όταν δώσουμε
εμείς στο command line την εντολή a.out, και με τη σειρά της
καλεί την printf ``περνώντας'' της, όπως λέμε, κάποιες
παραμέτρους. Στη συγκεκριμένη περίπτωση η παράμετρος είναι μία,
η λέξη (string) "Hello, world!\n"
.
Και η printf καλεί κι αυτή με τη σειρά της κάποιες συναρτήσεις
βιβλιοθήκης, αλλά αυτές δεν είμαστε σε θέση να τις γνωρίζουμε,
και δε χρειάζεται συνήθως να τις γνωρίζουμε. Είμαστε απλοί
χρήστες ενός ``μαύρου κουτιού'' που λέγεται printf
και χρειάζεται να ξέρουμε μόνο τη λειτουργική του
συμπεριφορά, δηλ. τι αποτέλεσμα θα έχει αν του δώσουμε το τάδε
όρισμα, κι όχι το πώς είναι φτιαγμένο.
Στο πρόγραμμα αυτό δεν υπάρχουν μεταβλητές.
Υπάρχει μόνο μια σταθερά, το string "Hello, world!\n"
.
Ποιο το νόημα του περίεργου \n
στο τέλος
του string;
Το backslash (\
) παίζει ένα ειδικό ρόλο στη C, και στο Unix γενικότερα.
Χρησιμοποιείται για να υποδηλώσει ότι ο επόμενος χαρακτήρας (δηλ. γράμμα)
που ακολουθεί στο string δεν αντιπροσωπεύει το γράμμα το οποίο φαίνεται,
αλλά κάποιο άλλο χαρακτήρα, για τον οποίο συνήθως δεν υπάρχει σύμβολο
να τον γράψουμε.
Έτσι στη C ο συνδυασμός \n
παριστάνει το χαρακτήρα LF (line feed)
ο οποίος χρησιμοποιείται για να αλλάξει γραμμή το τερματικό στο οποίο
τυπώνουμε.
Αν η σταθερά ήταν η "Hello, world!" (χωρίς το LF στο τέλος)
τότε το output του προγράμματος θα ήταν ως εξής:
% a.out Hello, world!%Δε θα άλλαζε δηλαδή γραμμή το τερματικό μετά την εκτύπωση του κειμένου και το prompt του επόμενου command line θα τυπωνόταν αμέσως μετά το θαυμαστικό.
Για να είμαστε λίγο πιο ακριβείς, ο κώδικας ASCII μόνο αντιστοιχεί χαρακτήρες στα νούμερα από 0 έως 127 (αριθμοί που μπορούν να παρασταθούν με 7 δυαδικά ψηφία). Πολλά σύμβολα, όπως για παράδειγμα αυτά της Ελληνικής αλφαβήτου μένουν απ' έξω, και το κομμάτι από 128 έως 255 χρησιμοποιείται, μεταξύ άλλων, και για την παράσταση αλφαβήτων διαφορετικών από το λατινικό (το οποίο παρίσταται από το standard ASCII κάτω από το 128).
Μπορείτε να δείτε στον παρακάτω πίνακα τους χαρακτήρες για τα ASCII codes από 0 έως 127.
Dec Hx Oct Char Dec Hx Oct Char Dec Hx Oct Char Dec Hx Oct Char --------------- --------------- --------------- --------------- 0 0 000 NUL (null) 32 20 040 SPACE 64 40 100 @ 96 60 140 ` 1 1 001 SOH (start of heading) 33 21 041 ! 65 41 101 A 97 61 141 a 2 2 002 STX (start of text) 34 22 042 " 66 42 102 B 98 62 142 b 3 3 003 ETX (end of text) 35 23 043 # 67 43 103 C 99 63 143 c 4 4 004 EOT (end of transmission) 36 24 044 $ 68 44 104 D 100 64 144 d 5 5 005 ENQ (enquiry) 37 25 045 % 69 45 105 E 101 65 145 e 6 6 006 ACK (acknowledge) 38 26 046 & 70 46 106 F 102 66 146 f 7 7 007 BEL (bell) 39 27 047 ' 71 47 107 G 103 67 147 g 8 8 010 BS (backspace) 40 28 050 ( 72 48 110 H 104 68 150 h 9 9 011 TAB (horizontal tab) 41 29 051 ) 73 49 111 I 105 69 151 i 10 A 012 LF (NL line feed, new line) 42 2A 052 * 74 4A 112 J 106 6A 152 j 11 B 013 VT (vertical tab) 43 2B 053 + 75 4B 113 K 107 6B 153 k 12 C 014 FF (NP form feed, new page) 44 2C 054 , 76 4C 114 L 108 6C 154 l 13 D 015 CR (carriage return) 45 2D 055 - 77 4D 115 M 109 6D 155 m 14 E 016 SO (shift out) 46 2E 056 . 78 4E 116 N 110 6E 156 n 15 F 017 SI (shift in) 47 2F 057 / 79 4F 117 O 111 6F 157 o 16 10 020 DLE (data link escape) 48 30 060 0 80 50 120 P 112 70 160 p 17 11 021 DC1 (device control 1) 49 31 061 1 81 51 121 Q 113 71 161 q 18 12 022 DC2 (device control 2) 50 32 062 2 82 52 122 R 114 72 162 r 19 13 023 DC3 (device control 3) 51 33 063 3 83 53 123 S 115 73 163 s 20 14 024 DC4 (device control 4) 52 34 064 4 84 54 124 T 116 74 164 t 21 15 025 NAK (negative acknowledge) 53 35 065 5 85 55 125 U 117 75 165 u 22 16 026 SYN (synchronous idle) 54 36 066 6 86 56 126 V 118 76 166 v 23 17 027 ETB (end of trans. block) 55 37 067 7 87 57 127 W 119 77 167 w 24 18 030 CAN (cancel) 56 38 070 8 88 58 130 X 120 78 170 x 25 19 031 EM (end of medium) 57 39 071 9 89 59 131 Y 121 79 171 y 26 1A 032 SUB (substitute) 58 3A 072 : 90 5A 132 Z 122 7A 172 z 27 1B 033 ESC (escape) 59 3B 073 ; 91 5B 133 [ 123 7B 173 { 28 1C 034 FS (file separator) 60 3C 074 < 92 5C 134 \ 124 7C 174 | 29 1D 035 GS (group separator) 61 3D 075 = 93 5D 135 ] 125 7D 175 } 30 1E 036 RS (record separator) 62 3E 076 > 94 5E 136 ^ 126 7E 176 ~ 31 1F 037 US (unit separator) 63 3F 077 ? 95 5F 137 _ 127 7F 177 DEL
Καμία σχέση με ... γάτες. Η ονοματολογία προγραμμάτων στο Unix είναι λίγο περίεργη μερικές φορές, πέρα από το ότι είναι και κρυπτική. Σαν παράδειγμα αναφέρω το πρόγραμμα biff, το οποίο ειδοποιεί τον χρήστη κάνοντας beep και τυπώνοντας ένα μικρό μήνυμα στην οθόνη οποτεδήποτε φτάσει ένα νέο μήνυμα e-mail. Η ιστορία είναι ότι ο προγραμματιστής που έφτιαξε αυτό το πρόγραμμα, ο οποίος ζούσε στο Berkeley, CA, είχε ένα σκύλο με το ίδιο όνομα ο οποίος έιχε τη συνήθεια να γαυγίζει οποτεδήποτε ερχόταν ο ταχυδρόμος. Εξ ού το όνομα.
Σε πρώτη προσέγγιση δε βλάπτει ιδιαίτερα όμως το να αγνοεί κανείς το διαχωρισμό αυτό ανάμεσα σε compiler και linker.
Έτσι εξοικονομείται ο χώρος που θα έπιανε, π.χ. η printf, σε όλα τα εκτελέσιμα προγράμματα που την καλούν (πολύ πιθανόν και όλα), η οποία ρουτίνα υπάρχει τώρα στο shared library μόνο. Επίσης εξοικονομείται μνήμη μια και μπορεί ανά πάσα στιγμή να τρέχουν π.χ. 10 προγράμματα που όλα καλούν την printf, και δεν υπάρχει κανένας εγγενής λόγος να υπάρχουν 10 αντίγραφα του κώδικα μηχανής της printf φορτωμένα στη μηχανή, μια και ο κώδικας αυτός είναι ένα στατικό αντικείμενο και δεν το αλλάζει το πρόγραμμα.
Ένα άλλο μεγάλο πλεονέκτημα χρήσης shared libraries vs. archive libraries (δηλ. στατικές βιβλιοθήκες) είναι το ότι δε χρειάζεται να προγράμματα εφαρμογής να αλλάξουν στο παραμικρό αν αντικατασταθεί μια ρουτίνα από ένα shared library. Φανταστείτε για παράδειγμα ότι ο προμηθευτής σας σας δίνει μια νέα standard library της C, που περιέχει μια σαφώς βελτιωμένη έκδοση της printf, χωρίς φυσικά να έχει αλλάξει το interface (δηλ. ο τρόπος με τον οποίο καλείται, το πρωτόκολλο επικοινωνίας ρουτίνας και προγράμματος που την καλεί). Τότε απλά αντικαθιστάτε την παλιά σας βιβλιοθήκη με την καινούργια και οποιαδήποτε εφαρμογή χρησιμοποιούσε την printf μέσα από το shared library την επόμενη φορά που θα τρέξει θα χρησιμοποιήσει αυτόματα την νέα printf. Κάτι τέτοιο θα ήταν αδύνατο αν κάθε πρόγραμμα που χρησιμοποιούσε την printf την είχε συμπεριλάβει μέσα στο εκτελέσιμο κώδικα. Όλα αυτά τα προγράμματα θα έπρεπε να γίνουν compiled ξανά για να ``δουν'' την καινούργια printf.