Στόχοι: μετά από αυτό το κεφάλαιο θα μπορείτε να διαβάζετε αριθμούς από την είσοδο με την
scanfκαι να ελέγχετε την τιμή που επιστρέφει· να δηλώνετε, να αρχικοποιείτε και να χρησιμοποιείτε μονοδιάστατους πίνακες· να υπολογίζετε πόση μνήμη πιάνει ένας πίνακας και σε ποια διεύθυνση βρίσκεται κάθε στοιχείο του· να γράφετε συναρτήσεις που παίρνουν πίνακα (μέγιστο, μέσος όρος, αναζήτηση,atoi)· και να εξηγείτε γιατί η πρόσβαση εκτός ορίων είναι επικίνδυνη.Προαπαιτούμενα: Κεφάλαιο 3, Κεφάλαιο 9
Χρόνος μελέτης: ~2,5 ώρες
Η διάλεξη έχει δύο μέρη. Στο πρώτο γνωρίζουμε τη scanf, τη «συμμετρική» της
printf για διάβασμα: αντί να φτιάχνουμε με το χέρι συναρτήσεις όπως η
getinteger πάνω από την getchar, ζητάμε από τη βιβλιοθήκη να μετατρέψει την
είσοδο σε αριθμούς, αρκεί να της δώσουμε τη διεύθυνση της μεταβλητής και να
ελέγξουμε τι μας επέστρεψε. Στο δεύτερο μέρος εισάγουμε τους πίνακες, τη βασικότερη
δομή δεδομένων σχεδόν κάθε γλώσσας: ένα όνομα για πολλές τιμές ίδιου τύπου,
αποθηκευμένες σε συνεχόμενες θέσεις μνήμης και προσβάσιμες μέσω μιας θέσης (index).
Με το παράδειγμα των 100 αρκουδακιών βλέπουμε γιατί χωρίς πίνακες ο κώδικας δεν
κλιμακώνεται, και έπειτα γράφουμε τις πρώτες συναρτήσεις πάνω σε πίνακες. Οι
πίνακες είναι η βάση για τις συμβολοσειρές, τους δείκτες και σχεδόν όλους τους
αλγορίθμους του υπόλοιπου μαθήματος.
scanfΣτο Κεφάλαιο 9 διαβάζαμε αριθμούς χαρακτήρα-χαρακτήρα με την
getchar και μια δική μας συνάρτηση getinteger (βλ. «Παραδείγματα»). Η πρότυπη
βιβλιοθήκη όμως μας δίνει έτοιμη λύση: τη scanf. Ορίζεται στο header file
stdio.h και διαβάζει δεδομένα εισόδου πολλών τύπων από το stdin του
προγράμματος, αποθηκεύοντας τις τιμές τους σε μεταβλητές.
Η δήλωσή της μοιάζει πολύ με της printf:
int scanf(const char *restrict format, ...);
int printf(const char *restrict format, ...);
"%d" για ακέραιο ή "%d %d" για δύο ακεραίους.... σημαίνουν ότι δέχεται όσα ορίσματα της περάσουμε (πώς γίνεται αυτό θα
το δούμε σε άλλο μάθημα).Η printf μετατρέπει τιμές σε κείμενο, η scanf κείμενο σε τιμές. Για όλες τις
λεπτομέρειες τρέχουμε σε ένα τερματικό man scanf.
scanf θέλει &Στο scanf("%d", &n); δεν περνάμε την τιμή της n αλλά τη διεύθυνσή (address)
της στη μνήμη, με τον τελεστή &. Ο λόγος: τα ορίσματα στην C περνούν με τιμή, άρα
η scanf θα έπαιρνε ένα αντίγραφο του (αρχικοποίητου) περιεχομένου της n και δεν
θα είχε πώς να αλλάξει την ίδια τη μεταβλητή. Με τη διεύθυνση ξέρει πού να γράψει
την τιμή που διάβασε. (Οι διευθύνσεις και οι δείκτες είναι το θέμα του
Κεφαλαίου 11· εδώ αρκεί ο κανόνας.)
Αν ξεχάσουμε το & και γράψουμε scanf("%d", n);, η scanf θα ερμηνεύσει τα
σκουπίδια της n ως διεύθυνση και θα προσπαθήσει να γράψει εκεί. Το αποτέλεσμα
είναι συνήθως:
$ ./scanf
Gimme a number: 3
Segmentation fault
Ο μεταγλωττιστής σάς προειδοποιεί αν μεταγλωττίζετε με -Wall (κάτι σαν «format
‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’»).
scanfΗ scanf επιστρέφει έναν int:
EOF (End-Of-File,
δηλαδή −1)·Ένα πρόγραμμα που αγνοεί την τιμή επιστροφής δεν είναι σωστό: όταν η scanf
αποτύχει, η μεταβλητή μένει με ό,τι είχε πριν (εδώ σκουπίδια) και το πρόγραμμα
συνεχίζει σαν να μην έγινε τίποτα. Ο σωστός τρόπος είναι να ελέγχουμε ότι
επιστράφηκε ακριβώς ο αριθμός τιμών που ζητήσαμε:
if (scanf("%d", &n) != 1) {
printf("Invalid input\n");
return 1;
}
Με μία κλήση μπορούμε να διαβάσουμε πολλές τιμές: scanf("%d %d", &n1, &n2);. Όταν
η scanf ψάχνει για δεκαδικό ψηφίο (%d), αγνοεί τους κενούς χαρακτήρες
(whitespace), δηλαδή κενά, tabs και αλλαγές γραμμής, που προηγούνται του αριθμού.
Γι’ αυτό η είσοδος ` 2 4 διαβάζεται σωστά ως 2 και 4, όπως και το ίδιο ζευγάρι
γραμμένο σε δύο γραμμές. Με δύο τιμές, η σωστή συνθήκη ελέγχου είναι
scanf(…) == 2`.
Έστω ότι έχουμε 100 αρκουδάκια και ψάχνουμε το μεγαλύτερο. Τι κάνουμε; Τα κοιτάμε ένα-ένα, κρατώντας στο μυαλό μας το μεγαλύτερο που έχουμε δει μέχρι τώρα. Ο στόχος είναι η ελαχιστοποίηση του κόπου (ο καλός προγραμματιστής είναι «efficient», aka τεμπέλης): δεν γίνεται να βρούμε το μέγιστο χωρίς να κοιτάξουμε το καθένα, αλλά αρκεί να το κοιτάξουμε μία φορά.
Πώς το κωδικοποιούμε σε C, αν κάθε αρκουδάκι είναι ένας int; Με όσα ξέρουμε ως
τώρα, χρειαζόμαστε 100 μεταβλητές:
int bear0, bear1, bear2, /* ..., */ bear99, max;
bear0 = 42; bear1 = 4; bear2 = 2; /* ... αρχικοποίηση μεταβλητών */
// find the max here: 99 σχεδόν ίδιες εντολές if
Ο κώδικας είναι πολύ επαναληπτικός και δεν γράφεται με βρόχο, γιατί δεν υπάρχει
τρόπος να πούμε «η μεταβλητή bear με αριθμό i». Αυτό λύνουν οι πίνακες.
Ένας πίνακας (array) μας επιτρέπει να χειριζόμαστε ένα σύνολο από δεδομένα ίδιου τύπου με ενιαίο και γενικό τρόπο. Στην C δηλώνεται με τη μορφή:
τύπος όνομα[μέγεθος];
Για παράδειγμα int bears[100];. Η δήλωση έχει τρία μέρη:
Πίνακες ορίζονται για όλους τους τύπους της C: int a[1024];, char b[2048];,
double c[512];.
Στο κάθε στοιχείο αναφερόμαστε με τη θέση (index) του στον πίνακα: για τον
int bears[100]; τα στοιχεία είναι τα bears[0], bears[1], …, bears[99]. Τα
στοιχεία αποθηκεύονται σε συνεχόμενες θέσεις μνήμης. Αν sizeof(int) == 4 και
ο μεταγλωττιστής τοποθετήσει τον πίνακα, ας πούμε, στη διεύθυνση 4, η μνήμη είναι:
| Bytes | Περιεχόμενο |
|---|---|
| 0–3 | (κάτι άλλο) |
| 4–7 | bears[0] |
| 8–11 | bears[1] |
| 12–15 | bears[2] |
| … | … |
| 400–403 | bears[99] |
Ο πίνακας bears καταλαμβάνει \(4 \cdot 100 = 400\) bytes, στις διευθύνσεις 4–403.
Γενικά ένας πίνακας N στοιχείων τύπου T πιάνει N * sizeof(T) bytes.
Επειδή τα στοιχεία είναι συνεχόμενα και ίδιου μεγέθους, η διεύθυνση κάθε στοιχείου υπολογίζεται αμέσως:
\[\text{διεύθυνση}(a[i]) = \text{αρχή του πίνακα} + i \cdot \text{sizeof}(\text{τύπος})\]Για το bears[2]: \(4 + 2 \cdot 4 = 12\), δηλαδή το στοιχείο ξεκινά από το 12ο byte.
Αυτός ο απλός υπολογισμός είναι ο λόγος που η πρόσβαση σε οποιοδήποτε στοιχείο
πίνακα κοστίζει το ίδιο, όσο μεγάλος κι αν είναι ο πίνακας, και ο λόγος που οι
θέσεις ξεκινούν από το 0: η θέση είναι η απόσταση από την αρχή.
Ένας πίνακας N στοιχείων έχει στοιχεία με θέσεις από το 0 μέχρι το N−1. Κάθε
στοιχείο μπορεί να χρησιμοποιηθεί όπως μια μεταβλητή του ίδιου τύπου: σε εκφράσεις,
αναθέσεις, τελεστές και συνθήκες. Η θέση μπορεί να είναι οποιαδήποτε ακέραια
έκφραση, ακόμα και άλλο στοιχείο του πίνακα:
bears[4] = 42;
bears[8] = bears[2] + 42;
bears[bears[4]] = 2; /* bears[42] = 2 */
Η μεγάλη δύναμη είναι ότι η θέση μπορεί να είναι μεταβλητή, οπότε ένας βρόχος
for (i = 0; i < 100; i++) περνά από όλα τα στοιχεία με μία εντολή.
Η σύνταξη είναι παρόμοια με την αρχικοποίηση μεταβλητής, με τις τιμές σε άγκιστρα, χωρισμένες με κόμμα:
int bears[100] = {
11, 25, 26, 31, 14, 13, 19, 3, 2, 19, /* ... άλλες 90 τιμές */
};
Η πρώτη τιμή πάει στο bears[0], η δεύτερη στο bears[1], κ.ο.κ. Αν δεν
αρχικοποιήσουμε έναν τοπικό πίνακα, τα στοιχεία του έχουν σκουπίδια, όπως κάθε
τοπική μεταβλητή· γι’ αυτό συχνά τον μηδενίζουμε με βρόχο πριν τον χρησιμοποιήσουμε.
Ένας πίνακας από χαρακτήρες λέγεται και αλφαριθμητικό ή συμβολοσειρά (string). Επειδή χρησιμοποιούνται πολύ συχνά, η C έχει αρκετές συντομεύσεις για αυτούς (θα τις δούμε στο Κεφάλαιο 14). Οι τρεις παρακάτω δηλώσεις είναι ισοδύναμες:
char hello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd',
'\n', '\0'};
char hello[] = {72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 10, 0};
char hello[] = "Hello World\n";
Η δεύτερη γράφει τους ίδιους χαρακτήρες με τους κωδικούς τους ASCII. Η τρίτη
προσθέτει μόνη της στο τέλος τον null byte '\0': τα string στην C
τερματίζονται πάντα με το null byte, και έτσι μια συνάρτηση ξέρει πού τελειώνει
η συμβολοσειρά χωρίς να της πούμε το μήκος. Όταν παραλείπουμε το μέγεθος ([]), ο
μεταγλωττιστής το υπολογίζει από την αρχικοποίηση· εδώ 13 (12 χαρακτήρες + '\0').
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
'H' |
'e' |
'l' |
'l' |
'o' |
' ' |
'W' |
'o' |
'r' |
'l' |
'd' |
'\n' |
'\0' |
Μπορούμε να αναθέσουμε τα στοιχεία ενός πίνακα σε άλλον με μία εντολή;
int a[3] = {1, 2, 3};
int b[3] = {4, 5, 6};
b = a; // does-not-compile
Όχι, δεν επιτρέπεται στην C. Ο gcc απαντά με
error: assignment to expression with array type. Για να αντιγράψουμε έναν πίνακα
αντιγράφουμε τα στοιχεία ένα-ένα με βρόχο: for (i = 0; i < 3; i++) b[i] = a[i];.
(Το γιατί θα φανεί όταν δούμε τη σχέση πινάκων και δεικτών στο
Κεφάλαιο 12.)
Μια συνάρτηση μπορεί να πάρει πίνακα ως παράμετρο, π.χ.
int find_max(int bears[100]) ή int atoi(char digits[]). Στη δεύτερη μορφή δεν
γράφουμε μέγεθος: η συνάρτηση πρέπει να ξέρει με άλλο τρόπο πού τελειώνει ο
πίνακας, είτε από σταθερό μέγεθος, είτε από ξεχωριστή παράμετρο n, είτε, για
string, από το '\0'. Οι σημειώσεις εξηγούν ότι στην πραγματικότητα η συνάρτηση
παίρνει τη διεύθυνση του πρώτου στοιχείου, άρα δεν γίνεται αντίγραφο του πίνακα και
οι αλλαγές στα στοιχεία του φαίνονται στον καλούντα· θα το δούμε αναλυτικά στα
Κεφάλαια 11 και 12.
Ο πίνακας είναι η βασικότερη δομή δεδομένων (data structure) στην πλειοψηφία των γλωσσών προγραμματισμού: μοντελοποιεί ένα σύνολο τιμών ίδιου τύπου, δεσμεύει μνήμη μαζικά με μία δήλωση και δίνει άμεση αποθήκευση, προσπέλαση και μετατροπή δεδομένων μέσω της θέσης.
Τι θα συμβεί αν προσπελάσουμε θέση έξω από τον πίνακα;
bears[100].bears[-1].Η C δεν ελέγχει τα όρια: ο μεταγλωττιστής απλώς υπολογίζει «αρχή + θέση · μέγεθος»
και διαβάζει ή γράφει εκεί, σε μνήμη που ανήκει σε κάτι άλλο. Το standard της γλώσσας
κατατάσσει αυτή τη χρήση ως απροσδιόριστη συμπεριφορά (undefined behavior): δεν
υπάρχει καμία εγγύηση για το τι θα γίνει. Στην πράξη, το πρόγραμμα θα κρασάρει
(Segmentation fault), θα αλλοιώσει σιωπηλά άλλες μεταβλητές ή, ακόμα χειρότερα,
κάποιος θα το εκμεταλλευτεί για να μας «χακάρει». Είναι ευθύνη του προγραμματιστή να
εξασφαλίσει ότι κάθε θέση i ικανοποιεί \(0 \le i \le N-1\).
getinteger με getcharΗ διάλεξη ξεκινά με το πρόβλημα «θέλω να διαβάσω δύο αριθμούς από την πρότυπη είσοδο και να τους προσθέσω»:
$ ./addnums
Give me a number: 40
Give me another number: 2
Total: 42
Με όσα ξέραμε, η λύση ήταν να διαβάζουμε τους χαρακτήρες έναν-έναν με την getchar
και να τους μετατρέπουμε σε αριθμό (μέθοδος του Horner, βλ.
Κεφάλαιο 9):
#define ERROR -1 // Return value for illegal character
int getinteger(int base) {
int ch; // No need to declare ch as int - no EOF handling
int val = 0; // Initialize return value
while ((ch = getchar()) != '\n') // Read up to new line
if (ch >= '0' && ch <= '0' + base - 1) // Legal character?
val = base * val + (ch - '0'); // Update return value
else
return ERROR; // Illegal character read
return val; // Everything OK - Return value of number read
}
Η συνάρτηση διαβάζει μέχρι την αλλαγή γραμμής έναν ακέραιο γραμμένο στη βάση
base (έως 10): κάθε νόμιμο ψηφίο «σπρώχνει» την τιμή μία θέση αριστερά
(base * val) και προσθέτει το ψηφίο (ch - '0'). Με το πρώτο μη νόμιμο ψηφίο
επιστρέφει ERROR. Προσέξτε ότι το −1 είναι ταυτόχρονα και αποτέλεσμα λάθους, οπότε
δεν ξεχωρίζει από μια έγκυρη τιμή· και ότι δεν χειρίζεται το EOF. «Υπάρχει άλλος
τρόπος;» Ναι: η scanf.
scanfΕφαρμόζει τις ενότητες «Η συνάρτηση scanf» και «Γιατί η scanf θέλει &».
#include <stdio.h>
int main() {
int n;
printf("Gimme a number: ");
scanf("%d", &n);
printf("Square: %d\n", n * n);
return 0;
}
Τυπώνει στο stdout το τετράγωνο του αριθμού που γράψαμε στο stdin. Είναι
σωστό; Όχι, γιατί δεν ελέγχει την τιμή επιστροφής της scanf:
$ ./scanf
Gimme a number: 16
Square: 256
$ ./scanf
Gimme a number: Square:
1068701481
$ ./scanf
Gimme a number: hello
Square: 1072038564
Στη δεύτερη εκτέλεση η είσοδος έκλεισε (Ctrl-D) χωρίς αριθμό, οπότε η scanf
επέστρεψε EOF· στην τρίτη το hello δεν είναι ακέραιος, οπότε επέστρεψε 0. Και
στις δύο η n έμεινε αρχικοποίητη και τυπώθηκε το τετράγωνο των σκουπιδιών της.
scanfΕφαρμόζει την ενότητα «Πολλά ορίσματα και κενοί χαρακτήρες».
#include <stdio.h>
int main() {
int n1, n2;
printf("Gimme two numbers: ");
scanf("%d %d", &n1, &n2);
printf("Result: %d\n", n1 * n2);
return 0;
}
$ ./scanf2
Gimme two numbers: 2 4
Result: 8
Πριν από τον ορισμό των πινάκων, η διάλεξη ρωτά «τι κάνει το παρακάτω πρόγραμμα;»
για ένα απόσπασμα από το histogram.c των σημειώσεων. Εδώ είναι μέσα σε main, με
έναν βρόχο εκτύπωσης στο τέλος που δεν υπάρχει στη διαφάνεια:
#include <stdio.h>
int main() {
int i, ch, total = 0;
int letfr[26]; // Letter occurrences and frequencies array
for (i = 0; i < 26; i++)
letfr[i] = 0;
while ((ch = getchar()) != EOF) {
if (ch >= 'A' && ch <= 'Z') {
letfr[ch - 'A']++; // Found upper case letter
total++;
}
if (ch >= 'a' && ch <= 'z') {
letfr[ch - 'a']++; // Found lower case letter
total++;
}
}
for (i = 0; i < 26; i++) // not on the slide
printf("%c: %d\n", 'a' + i, letfr[i]);
printf("total: %d\n", total);
return 0;
}
Ο letfr έχει έναν μετρητή ανά γράμμα. Η έκφραση ch - 'A' μετατρέπει ένα γράμμα
στη θέση του ('A' → 0, …, 'Z' → 25), οπότε το letfr[ch - 'A']++ αυξάνει τον
σωστό μετρητή χωρίς 26 εντολές if· κεφαλαία και πεζά μετρούν μαζί. Εφαρμόζει τη
«Χρήση στοιχείων πίνακα» και τον μηδενισμό με βρόχο. Το πλήρες πρόγραμμα των
σημειώσεων τυπώνει και ιστόγραμμα.
find_maxΗ λύση του γρίφου με τα αρκουδάκια: μια συνάρτηση που παίρνει πίνακα 100 στοιχείων και επιστρέφει το μέγιστο.
int find_max(int bears[100]) {
int i, max = bears[0];
for (i = 1; i < 100; i++) {
if (bears[i] > max) max = bears[i];
}
return max;
}
flowchart TD
A["max = bears[0], i = 1"] --> B{"i #lt; 100;"}
B -- "ναι" --> C{"bears[i] #gt; max;"}
C -- "ναι" --> D["max = bears[i]"]
C -- "όχι" --> E["i++"]
D --> E
E --> B
B -- "όχι" --> F["return max"]
Σχήμα: ο αλγόριθμος της find_max: μία σάρωση, 99 συγκρίσεις.
Ξεκινάμε με το πρώτο στοιχείο ως «μέγιστο μέχρι τώρα» (όχι με 0, που θα ήταν λάθος αν όλα τα στοιχεία ήταν αρνητικά) και κοιτάμε καθένα από τα υπόλοιπα μία φορά. Εφαρμόζει τις ενότητες «Χρήση στοιχείων πίνακα» και «Πίνακες ως ορίσματα συναρτήσεων».
«Θέλω μια συνάρτηση που να δέχεται έναν πίνακα 100 ακεραίων και να επιστρέφει τον μέσο όρο.»
int average(int grades[100]) {
int i, sum = 0;
for (i = 0; i < 100; i++) {
sum += grades[i];
}
return sum / 100;
}
Το μοτίβο είναι ο συσσωρευτής: μηδενίζουμε το sum και προσθέτουμε κάθε στοιχείο.
Προσέξτε ότι sum / 100 είναι ακέραια διαίρεση, άρα ο μέσος όρος στρογγυλεύεται
προς τα κάτω (για θετικά)· για ακριβές αποτέλεσμα θα επιστρέφαμε double και θα
γράφαμε sum / 100.0.
find«Θέλω μια συνάρτηση που να δέχεται έναν πίνακα 100 ακεραίων και έναν ακέραιο και να γυρνάει τη θέση του στοιχείου αν το βρήκε ή −1.»
int find(int haystack[100], int needle) {
int i;
for (i = 0; i < 100; i++) {
if (haystack[i] == needle) {
return i;
}
}
return -1;
}
Αυτή είναι η σειριακή αναζήτηση (linear search): ψάχνουμε τη «βελόνα» στα «άχυρα». Επιστρέφουμε αμέσως μόλις τη βρούμε, και μόνο αν ο βρόχος τελειώσει χωρίς επιτυχία επιστρέφουμε −1. Το −1 λειτουργεί ως ένδειξη αποτυχίας γιατί δεν είναι ποτέ έγκυρη θέση πίνακα. Πιο γρήγορους τρόπους αναζήτησης θα δούμε στο Κεφάλαιο 17.
atoi: από string σε ακέραιο«Θέλω μια συνάρτηση atoi που να παίρνει έναν πίνακα χαρακτήρων (μόνο ψηφία) και
να επιστρέφει έναν ακέραιο.»
int atoi(char digits[]) {
int result = 0;
for (int i = 0; digits[i]; i++) {
result = 10 * result + digits[i] - '0';
}
return result;
}
Είναι η ίδια ιδέα με την getinteger σε βάση 10, αλλά τώρα οι χαρακτήρες έρχονται
από πίνακα αντί από την είσοδο. Η συνθήκη digits[i] σημαίνει digits[i] != '\0':
ο βρόχος σταματά στο null byte που τερματίζει το string. Για "472":
\(0 \to 4 \to 47 \to 472\).
«Τι μπορεί να πάει στραβά με αυτήν τη συνάρτηση;» Η διάλεξη αφήνει την ερώτηση
ανοιχτή. Μερικές απαντήσεις: δεν ελέγχει ότι κάθε χαρακτήρας είναι ψηφίο (για
"4a" επιστρέφει σκουπίδια χωρίς ένδειξη λάθους)· δεν χειρίζεται πρόσημο· ένα
πολύ μακρύ string υπερχειλίζει τον int· και αν ο πίνακας δεν τελειώνει σε '\0',
ο βρόχος συνεχίζει εκτός ορίων. Επιπλέον, το όνομα atoi υπάρχει ήδη στο
stdlib.h, οπότε σε πραγματικό πρόγραμμα θα διαλέγαμε άλλο όνομα.
Στο Εργαστήριο 6 η άσκηση
judgement.c ζητά max_array, min_array και sum_array πάνω σε πίνακα 10
βαθμολογιών που διαβάζονται με scanf: είναι οι find_max και average αυτού του
κεφαλαίου, με το μέγεθος ως παράμετρο n αντί για σταθερά 100. Η sieve.c (κόσκινο
του Ερατοσθένη) χρησιμοποιεί πίνακα από 0 και 1 όπου η θέση i λέει αν το i είναι
πρώτος, όπως ο letfr χρησιμοποιεί τη θέση για γράμμα.
scanf (από το stdio.h) είναι η συμμετρική της printf: διαβάζει από το
stdin τιμές σύμφωνα με μια συμβολοσειρά μορφοποίησης και τις αποθηκεύει σε
μεταβλητές.scanf περνάμε τη διεύθυνση της μεταβλητής (&n), αλλιώς δεν μπορεί να
γράψει την τιμή· χωρίς & το πρόγραμμα συνήθως κρασάρει με Segmentation fault.scanf επιστρέφει πόσες τιμές διάβασε, ή EOF αν τελείωσε η είσοδος· ένα
πρόγραμμα που δεν ελέγχει αυτήν την τιμή δεν είναι σωστό.scanf αγνοεί κενά, tabs και αλλαγές γραμμής.τύπος όνομα[μέγεθος]; και κρατά μέγεθος τιμές του
ίδιου τύπου· το μέγεθος είναι στατικό.N θέσεων είναι τα a[0] έως a[N-1] και καθένα
χρησιμοποιείται όπως μια απλή μεταβλητή· η θέση μπορεί να είναι οποιαδήποτε ακέραια
έκφραση.N * sizeof(τύπος) bytes και το a[i] βρίσκεται στη διεύθυνση
αρχή + i * sizeof(τύπος).'\0'·
το "Hello World\n" είναι συντομογραφία για τη λίστα των χαρακτήρων του μαζί με
το '\0'.b = a;) δεν επιτρέπεται στην C· αντιγράφουμε
στοιχείο προς στοιχείο.bears[100], bears[-1]) είναι απροσδιόριστη
συμπεριφορά: μπορεί να κρασάρει το πρόγραμμα ή να το κάνει ευάλωτο σε επιθέσεις.| Ελληνικά | English | Σύντομος ορισμός |
|---|---|---|
| πίνακας | array | Σύνολο στοιχείων ίδιου τύπου σε συνεχόμενες θέσεις μνήμης, με ένα όνομα. |
| θέση | index | Ο αριθμός (από 0) που επιλέγει ένα στοιχείο του πίνακα. |
| μέγεθος | size | Πόσα στοιχεία έχει ο πίνακας· στατικό μετά τη δήλωση. |
| συμβολοσειρά, αλφαριθμητικό | string | Πίνακας χαρακτήρων που τερματίζεται με '\0'. |
| null byte | null byte / null terminator | Ο χαρακτήρας '\0' (τιμή 0) που σημειώνει το τέλος ενός string. |
| συμβολοσειρά μορφοποίησης | format string | Το πρώτο όρισμα της printf/scanf, π.χ. "%d %d". |
| διεύθυνση | address | Η θέση μιας μεταβλητής στη μνήμη· την παίρνουμε με &. |
| τέλος αρχείου | end-of-file (EOF) | Η τιμή −1 που επιστρέφουν getchar/scanf όταν τελειώσει η είσοδος. |
| κενοί χαρακτήρες | whitespace | Κενά, tabs και αλλαγές γραμμής. |
| δομή δεδομένων | data structure | Τρόπος οργάνωσης δεδομένων στη μνήμη για αποδοτική χρήση. |
| υπερχείλιση / υποχείλιση | overflow / underflow | Πρόσβαση μετά το τέλος / πριν την αρχή ενός πίνακα. |
| απροσδιόριστη συμπεριφορά | undefined behavior | Χρήση για την οποία το standard δεν εγγυάται κανένα αποτέλεσμα. |
| σειριακή αναζήτηση | linear search | Έλεγχος των στοιχείων ένα-ένα μέχρι να βρεθεί το ζητούμενο. |
scanf σελ. 5–16· getinteger και συχνότητες γραμμάτων σελ. 17–18·
γρίφος με τα αρκουδάκια σελ. 20–27· δήλωση, μνήμη, χρήση και αρχικοποίηση πίνακα
σελ. 28–36· find_max σελ. 37–38· διευθύνσεις και τύποι σελ. 39–40· strings
σελ. 41· ανάθεση, χρησιμότητα, όρια σελ. 42–44· average, find, atoi
σελ. 45–50.sieve.c, judgement.c.man scanf.scanf χωρίς &. Το scanf("%d", n); μεταγλωττίζεται (με προειδοποίηση
στο -Wall) και κρασάρει με Segmentation fault. Γράφετε scanf("%d", &n); και
μεταγλωττίζετε πάντα με -Wall.scanf. Με είσοδο hello ή κενή είσοδο το
πρόγραμμα τυπώνει σκουπίδια (Square: 1072038564). Ελέγχετε
if (scanf("%d", &n) != 1).for (i = 1; i <= 100; i++) χάνει το bears[0]
και διαβάζει το ανύπαρκτο bears[100]. Οι θέσεις είναι 0 έως N-1:
for (i = 0; i < 100; i++).a[N] ή στο a[-1] μπορεί να περάσει
απαρατήρητο και να αλλοιώσει άλλες μεταβλητές, ή να κρασάρει αργότερα. Ελέγχετε
κάθε θέση που προέρχεται από την είσοδο πριν τη χρησιμοποιήσετε.max = 0 η find_max σε πίνακα με μόνο
αρνητικούς επιστρέφει 0. Ξεκινάτε με max = a[0].letfr[i] = 0, οι μετρητές
ξεκινούν από σκουπίδια και τα αποτελέσματα είναι τυχαία.b = a; δίνει error: assignment to expression with array
type. Αντιγράφετε με βρόχο.sum / 100 για άθροισμα 1050 δίνει 10, όχι
10.5. Χρησιμοποιείτε double και 100.0 όταν θέλετε δεκαδικά.Από τα Kahoot των διαλέξεων: οι ερωτήσεις όπου μια λάθος απάντηση μάζεψε πολλές ψήφους, με το ποσοστό σωστών απαντήσεων.
1041, μετρώντας τις θέσεις σαν να ξεκινούσαν από το 1. Αφού το array[0] είναι στο 1000 και κάθε char πιάνει 1 byte, το array[i] είναι στο 1000 + i.hello[5], γιατί η αρίθμηση στους πίνακες ξεκινά από το 0.4096, θεωρώντας ότι ένας int είναι πάντα 4 bytes. Το πρότυπο της C δεν ορίζει το ακριβές μέγεθος του int· εξαρτάται από το σύστημα.scanf("%d %d", &a, &b) αν η είσοδος είναι 5 x, και τι αν η
είσοδος είναι κενή;1scanf("%d", &n) αλλά printf("%d", n);[^q2]double c[512]; και πόσα
bytes πιάνει ο πίνακας;[^q3]int a[50]; ξεκινά από τη διεύθυνση 1000 και sizeof(int) == 4. Σε
ποια διεύθυνση ξεκινά το a[10];[^q4]bears[4] = 42; bears[bears[4]] = 2; ποιο στοιχείο έχει την
τιμή 2;[^q5]char s[] = "abc"; και ποιο είναι το τελευταίο;[^q6]find επιστρέφει −1 όταν δεν βρει το στοιχείο;[^q7]atoi των διαφανειών για το string "0042";[^q8]Ερωτήσεις που παίχτηκαν στις διαλέξεις, με το ποσοστό των φοιτητών που απάντησαν σωστά.
slides-lec10-addnumsslides-lec10-array-assignslides-lec10-array-memoryslides-lec10-averageslides-lec10-findslides-lec10-find-maxslides-lec10-out-of-boundsslides-lec10-scanf-squareslides-lec10-scanf-two-numbersslides-lec10-atoislides-lec10-getintegerslides-lec10-letter-frequencyslides-lec10-scanf-correctslides-lec10-scanf-no-ampersandlab-lab06-judgementlab-lab06-sievelab-lab07-my_progexam-2023-fall-ex5-q1exam-2026-sep-q2exam-2023-fall-ex11-q2exam-2023-fall-ex3-q2exam-2023-fall-ex4-q1exam-2023-fall-ex5-q2exam-2026-jun-q2exam-2023-fall-ex12-q1slides-lec11-atoislides-lec11-averageslides-lec11-findexam-2024-jul-q2exam-2025-jan-q3exam-2023-fall-ex12-q2exam-2023-fall-ex7-q3slides-lec15-complexity-atoiexam-2023-fall-ex12-q4slides-lec16-average-complexityslides-lec16-find-complexityΓια 5 x επιστρέφει 1 (διάβασε μόνο το a)· για κενή είσοδο επιστρέφει
EOF (−1).
[^q2]: Η printf χρειάζεται μόνο την τιμή, ενώ η scanf πρέπει να γράψει στη
μεταβλητή, άρα χρειάζεται τη διεύθυνσή της.
[^q3]: c[0] και c[511]· \(512 \cdot 8 = 4096\) bytes (με sizeof(double) == 8).
[^q4]: \(1000 + 10 \cdot 4 = 1040\).
[^q5]: Το bears[42], γιατί η θέση bears[4] αποτιμάται πρώτα σε 42.
[^q6]: 4 στοιχεία: 'a', 'b', 'c' και το τελευταίο είναι το '\0'.
[^q7]: Γιατί κάθε έγκυρη θέση είναι από 0 έως 99, οπότε το −1 δεν μπερδεύεται με
επιτυχημένο αποτέλεσμα.
[^q8]: Το 42: τα αρχικά μηδενικά αφήνουν το result στο 0. ↩