Διάλεξη ΙΙ — Προγραμματισμός στη C II
Αναδρομή (Recursion)  |  Τελεστές και Εκφράσεις

📚 Κεφάλαιο 2: Αναδρομή (Recursion)

Πώς λειτουργεί η αναδρομή βήμα-βήμα — με animated οπτικοποίηση της στοίβας κλήσεων

2.1 Τι είναι η Αναδρομή;

Η αναδρομή είναι μια τεχνική στον προγραμματισμό όπου μια συνάρτηση καλεί τον εαυτό της. Αυτό ακούγεται παράξενο στην αρχή — πώς μπορεί μια συνάρτηση να καλεί τον εαυτό της;

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

🔁 Παράδειγμα από την καθημερινή ζωή: Στέκεστε σε μια ουρά και θέλετε να ξέρετε πόσοι είστε. Ρωτάτε τον μπροστινό σας. Εκείνος ρωτάει τον δικό του μπροστινό. Το ίδιο γίνεται μέχρι τον πρώτο της ουράς, που λέει «εγώ είμαι ο 1ος». Τότε ο επόμενος λέει «εγώ είμαι ο 2ος», κ.ο.κ., μέχρι να φτάσει η απάντηση σε εσάς. Έτσι δουλεύει η αναδρομή.

2.1.1 Τα δύο μέρη κάθε αναδρομικής συνάρτησης

Κάθε αναδρομική συνάρτηση έχει υποχρεωτικά δύο μέρη. Αν λείπει το ένα, το πρόγραμμα δεν θα δουλέψει σωστά:

🛑 1. Βασική Περίπτωση (Base Case)

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

Π.χ.: if (n == 0) return 1;

🔁 2. Αναδρομική Περίπτωση (Recursive Case)

Είναι το σημείο όπου η συνάρτηση καλεί τον εαυτό της με ένα μικρότερο πρόβλημα, πλησιάζοντας κάθε φορά τη βασική περίπτωση.

Π.χ.: return n * factorial(n-1);


2.2 Παράδειγμα: Παραγοντικό n!

Το παραγοντικό είναι το πιο κλασικό παράδειγμα για να καταλάβουμε την αναδρομή, γιατί ο ίδιος ο μαθηματικός ορισμός του είναι αναδρομικός:

n! = n × (n−1) × (n−2) × ... × 2 × 1 5! = 5 × 4 × 3 × 2 × 1 = 120 Αναδρομικά: 0! = 1 ← βασική περίπτωση n! = n × (n−1)! ← αναδρομική περίπτωση

2.2.1 Κώδικας C

#include <stdio.h> /* ───────────────────────────────────────── Αναδρομική συνάρτηση παραγοντικού Δέχεται: ακέραιο n >= 0 Επιστρέφει: n! (παραγοντικό) ───────────────────────────────────────── */ int factorial(int n) { /* ΒΑΣΙΚΗ ΠΕΡΙΠΤΩΣΗ: σταματάμε εδώ */ if (n == 0) { return 1; /* 0! = 1 εξ ορισμού */ } /* ΑΝΑΔΡΟΜΙΚΗ ΠΕΡΙΠΤΩΣΗ: καλούμε τον εαυτό μας */ return n * factorial(n - 1); } int main() { printf("5! = %d\n", factorial(5)); /* 120 */ printf("4! = %d\n", factorial(4)); /* 24 */ printf("0! = %d\n", factorial(0)); /* 1 */ return 0; }
5! = 120 4! = 24 0! = 1

2.3 Πώς υπολογίζεται το 5! — Βήμα-Βήμα

Αυτό που συχνά δυσκολεύει είναι το εξής: όταν η factorial(5) καλεί την factorial(4), τι γίνεται με την τιμή n=5; Χάνεται; Αντικαθίσταται; Η απάντηση είναι ότι αποθηκεύεται αυτόματα στη στοίβα (stack) και περιμένει.

⚠️ Σημαντικό: Κάθε κλήση της factorial δημιουργεί ένα νέο αντίγραφο της μεταβλητής n στη μνήμη. Τα αντίγραφα αυτά είναι εντελώς ανεξάρτητα το ένα από το άλλο.

2.3.1 Εικόνα εκτέλεσης

factorial(5) = 5 × factorial(4) factorial(4) = 4 × factorial(3) factorial(3) = 3 × factorial(2) factorial(2) = 2 × factorial(1) factorial(1) = 1 × factorial(0) factorial(0) = 1 BASE CASE 🛑 ← επιστρέφει 5 × 24 = 120 ← επιστρέφει 4 × 6 = 24 ← επιστρέφει 3 × 2 = 6 ← επιστρέφει 2 × 1 = 2 ← επιστρέφει 1 × 1 = 1 📉 ΚΑΘΟΔΟΣ (Κλήσεις) 📈 ΑΝΟΔΟΣ (Επιστροφές)

2.4 Animated Βήμα-Βήμα: factorial(5)

Χρησιμοποιήστε τα κουμπιά για να δείτε ακριβώς πώς χτίζεται και αποχτίζεται η στοίβα κλήσεων:

🔍 Εκτέλεση του factorial(5)
Η στοίβα κλήσεων (call stack) — κάθε πλαίσιο κρατά την τιμή n
Βήμα 0 / 10
Πατήστε ▶ Επόμενο για να ξεκινήσετε, ή ⚡ Αυτόματο για αυτόματη αναπαραγωγή.
● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○

2.5 Πώς Αποθηκεύεται στη Μνήμη (Stack)

Κάθε κλήση της factorial δημιουργεί ένα νέο πλαίσιο στοίβας (stack frame). Σκεφτείτε τη στοίβα σαν στοίβα από πιάτα — βάζετε ένα-ένα και παίρνετε από πάνω.

📦 Τι αποθηκεύεται σε κάθε πλαίσιο;
  • Η τιμή της παραμέτρου n (π.χ. n=5, n=4, …)
  • Η διεύθυνση επιστροφής — δηλαδή πού θα συνεχίσει ο κώδικας όταν τελειώσει
  • Το αποτέλεσμα που αναμένεται από την επόμενη κλήση
▲ ΚΟΡΥΦΗ ΣΤΟΙΒΑΣ (το πιο πρόσφατο frame)
🛑 factorial(0) → return 1 ← ΒΑΣΙΚΗ ΠΕΡΙΠΤΩΣΗ — επιστρέφει τώρα!
↓ επιστρέφει στο...
⏳ factorial(1) → return 1 × ? ← περιμένει factorial(0)
↓ επιστρέφει στο...
⏳ factorial(2) → return 2 × ? ← περιμένει factorial(1)
↓ επιστρέφει στο...
⏳ factorial(3) → return 3 × ? ← περιμένει factorial(2)
↓ επιστρέφει στο...
⏳ factorial(4) → return 4 × ? ← περιμένει factorial(3)
↓ επιστρέφει στο...
⏳ factorial(5) → return 5 × ? ← περιμένει factorial(4)
▼ ΒΑΣΗ ΣΤΟΙΒΑΣ (η πρώτη κλήση από την main)
✅ Άρα η απάντηση είναι: Η C δεν "ξεχνά" τις ενδιάμεσες τιμές. Κάθε κλήση έχει το δικό της ξεχωριστό πλαίσιο στη μνήμη με τη δική της τιμή n. Όταν επιστρέψει το αποτέλεσμα, το πλαίσιο αφαιρείται από τη στοίβα και ο έλεγχος επιστρέφει στο προηγούμενο, που συνεχίζει από εκεί που σταμάτησε.

2.6 Άλλα Παραδείγματα Αναδρομής

2.6.1 Δύναμη αριθμού xy

Το xy σημαίνει ότι πολλαπλασιάζουμε το x με τον εαυτό του y φορές. Αναδρομικά, αυτό γράφεται ως: xy = x × xy-1. Βασική περίπτωση: οποιοσδήποτε αριθμός υψωμένος στο 0 ισούται με 1.

#include <stdio.h> /* x^y = x * x^(y-1) Βασική περίπτωση: x^0 = 1 */ int power(int x, int y) { if (y == 0) return 1; /* βασική: x^0 = 1 */ return x * power(x, y - 1); /* x^y = x * x^(y-1) */ } int main() { printf("2^8 = %d\n", power(2, 8)); printf("3^4 = %d\n", power(3, 4)); printf("5^0 = %d\n", power(5, 0)); printf("7^1 = %d\n", power(7, 1)); return 0; }
2^8 = 256 3^4 = 81 5^0 = 1 7^1 = 7

Ας δούμε βήμα-βήμα πώς εκτελείται το power(2, 4). Η λογική είναι ακριβώς η ίδια με το παραγοντικό: η αναδρομή κατεβαίνει μέχρι τη βασική περίπτωση power(2, 0) = 1 και στη συνέχεια ανεβαίνει πολλαπλασιάζοντας με 2 κάθε φορά: 1 → 2 → 4 → 8 → 16.

🔍 Εκτέλεση του power(2, 4)
Η στοίβα κλήσεων (call stack) — κάθε πλαίσιο κρατά τις τιμές x και y
Βήμα 0 / 9
Πατήστε ▶ Επόμενο για να ξεκινήσετε, ή ⚡ Αυτόματο για αυτόματη αναπαραγωγή.
● ○ ○ ○ ○ ○ ○ ○ ○

2.6.2 Fibonacci

Η ακολουθία Fibonacci είναι μια από τις πιο γνωστές ακολουθίες στα μαθηματικά και εμφανίζεται παντού στη φύση — από τα πέταλα λουλουδιών μέχρι τα κελύφη σαλιγκαριών. Κάθε αριθμός είναι το άθροισμα των δύο προηγούμενων:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ... F(0) = 0 ← βασική περίπτωση F(1) = 1 ← βασική περίπτωση F(n) = F(n-1) + F(n-2) ← αναδρομική περίπτωση Π.χ.: F(5) = F(4) + F(3) = 3 + 2 = 5
Αναλογίες ανθρώπινου σώματος - Χρυσή Τομή Fibonacci

📐 Οι αναλογίες 38 και 62 στο ανθρώπινο σώμα αντιστοιχούν στους αριθμούς Fibonacci (38/62 ≈ 0.618 — η Χρυσή Τομή!)

Το Fibonacci είναι διαφορετικό από το παραγοντικό σε ένα κρίσιμο σημείο: κάθε κλήση κάνει δύο αναδρομικές κλήσεις, όχι μία. Αυτό σημαίνει ότι το δέντρο εκτέλεσης δεν είναι γραμμικό — διακλαδίζεται. Για το fibonacci(5) γίνονται συνολικά 15 κλήσεις!

⚠️ Σημαντική διαφορά από το παραγοντικό: Εδώ έχουμε δύο βασικές περιπτώσεις — F(0)=0 και F(1)=1. Χρειαζόμαστε και τις δύο γιατί αλλιώς όταν φτάσουμε στο n=1 και καλέσουμε fibonacci(0) και fibonacci(-1), δεν θα υπάρχει τερματισμός για το -1.
#include <stdio.h> int fibonacci(int n) { /* Δύο βασικές περιπτώσεις */ if (n == 0) return 0; if (n == 1) return 1; /* Αναδρομική περίπτωση: ΔΥΟ κλήσεις! */ return fibonacci(n - 1) + fibonacci(n - 2); } int main() { for (int i = 0; i < 10; i++) { printf("F(%d) = %d\n", i, fibonacci(i)); } return 0; }
F(0) = 0 F(1) = 1 F(2) = 1 F(3) = 2 F(4) = 3 F(5) = 5 F(6) = 8 F(7) = 13 F(8) = 21 F(9) = 34

Παρακάτω φαίνεται βήμα-βήμα η εκτέλεση του fibonacci(4). Προσέξτε πώς η στοίβα πρώτα κατεβαίνει στον αριστερό κλάδο (fibonacci(n-1)), τον ολοκληρώνει, και μετά πιάνει τον δεξί κλάδο (fibonacci(n-2)):

🔍 Εκτέλεση του fibonacci(4)
Η C εκτελεί πρώτα τον αριστερό κλάδο fibonacci(n-1), μετά τον δεξί fibonacci(n-2)
Βήμα 0 / 14
Πατήστε ▶ Επόμενο για να ξεκινήσετε, ή ⚡ Αυτόματο για αυτόματη αναπαραγωγή.
● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○

2.6.3 Άθροισμα 1 + 2 + ... + n

Ένα ακόμα απλό παράδειγμα: το άθροισμα των πρώτων n ακεραίων. Η λογική είναι: το άθροισμα μέχρι το n ισούται με n συν το άθροισμα μέχρι το n-1. Βασική περίπτωση: όταν n=1, το άθροισμα είναι 1.

#include <stdio.h> int sum(int n) { if (n == 1) return 1; /* βασική */ return n + sum(n - 1); /* αναδρομή */ } int main() { printf("Άθροισμα 1..5 = %d\n", sum(5)); /* 15 */ printf("Άθροισμα 1..10 = %d\n", sum(10)); /* 55 */ return 0; }
Άθροισμα 1..5 = 15 Άθροισμα 1..10 = 55

2.7 Κίνδυνοι και Λάθη στην Αναδρομή

2.7.1 Stack Overflow — Άπειρη Αναδρομή

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

/* ❌ ΛΑΘΟΣ: Δεν υπάρχει βασική περίπτωση! */ int badFactorial(int n) { return n * badFactorial(n - 1); /* → -∞, CRASH! */ } /* ❌ ΛΑΘΟΣ: Η συνθήκη δεν θα επιτευχθεί ποτέ για αρνητικά! */ int badFactorial2(int n) { if (n == 0) return 1; /* αν n=-1, ποτέ δεν γίνεται 0! */ return n * badFactorial2(n - 1); }
💀 Stack Overflow: Η στοίβα έχει περιορισμένο μέγεθος. Για factorial(10000) θα χρειαστούν 10.000 πλαίσια — η στοίβα θα γεμίσει και το πρόγραμμα θα τερματιστεί με σφάλμα.

2.7.2 Σωστή Αναδρομή — Checklist

✅ Πριν γράψετε αναδρομή, ελέγξτε:
  1. Έχω βασική περίπτωση που τερματίζει;
  2. Κάθε κλήση πλησιάζει τη βασική περίπτωση; (π.χ. n μειώνεται;)
  3. Δεν δέχομαι αρνητικές τιμές χωρίς έλεγχο;
  4. Έχω δοκιμάσει για n=0, n=1, n=2 χειροκίνητα;

2.8 Αναδρομή vs Επανάληψη

Οτιδήποτε γράφεται αναδρομικά μπορεί να γραφεί και επαναληπτικά (με for ή while), και το αντίστροφο. Η επιλογή εξαρτάται από το πρόβλημα και το πόσο ευανάγνωστος θέλουμε να είναι ο κώδικας.

ΧαρακτηριστικόΑναδρομή (Recursion)Επανάληψη (Loop)
ΚώδικαςΠιο σύντομος, κομψόςΠιο λεπτομερής
ΚατανόησηΚοντά στον μαθηματικό ορισμόΠιο «μηχανιστική»
ΜνήμηΧρησιμοποιεί στοίβα (επιπλέον)Σταθερή μνήμη
ΤαχύτηταΣυνήθως πιο αργήΣυνήθως πιο γρήγορη
ΚίνδυνοςStack overflowΑτέρμων βρόχος
Χρησιμοποιείται γιαΔέντρα, γράφους, D&C αλγόριθμοιΑπλές επαναλήψεις

2.8.1 Το ίδιο παράδειγμα με βρόχο

Για σύγκριση, ο ίδιος υπολογισμός χωρίς αναδρομή. Παρατηρήστε ότι δεν χρειάζεται στοίβα — η τιμή συσσωρεύεται στη μεταβλητή result απευθείας:

/* Ίδιο αποτέλεσμα, χωρίς αναδρομή */ int factorialLoop(int n) { int result = 1; for (int i = 1; i <= n; i++) { result = result * i; } return result; }
📌 Πότε επιλέγουμε αναδρομή; Κυρίως όταν το πρόβλημα έχει φυσικά αναδρομική δομή — όπως η διάσχιση δέντρων, ο αλγόριθμος QuickSort, ή γενικά ό,τι "σπάει" εύκολα σε υποπροβλήματα της ίδιας μορφής. Για απλούς βρόχους, η επαναληπτική λύση είναι συνήθως καλύτερη επιλογή.

2.9 Ασκήσεις

📝 Άσκηση 1: Χειρονακτικός Υπολογισμός

Γράψτε χειρονακτικά (χωρίς υπολογιστή) τα βήματα εκτέλεσης του factorial(4). Σχεδιάστε τη στοίβα κλήσεων.

📝 Άσκηση 2: Δύναμη

Γράψτε αναδρομική συνάρτηση int power(int x, int n) που υπολογίζει xn. Βασική περίπτωση: x0=1. Δοκιμάστε: power(2,8)=256, power(3,0)=1.

📝 Άσκηση 3: Ψηφία Αντίστροφα

Γράψτε αναδρομική συνάρτηση void printReverse(int n) που εμφανίζει τα ψηφία ενός ακεραίου αντίστροφα. Π.χ. 1234 → 4 3 2 1. Υπόδειξη: τυπώστε n%10 και μετά καλέστε printReverse(n/10).

📝 Άσκηση 4: Άθροισμα Ψηφίων

Γράψτε αναδρομική συνάρτηση int digitSum(int n) που υπολογίζει το άθροισμα των ψηφίων. Π.χ. digitSum(1234) = 10. Υπόδειξη: n%10 + digitSum(n/10).

📝 Άσκηση 5 (Πρόκληση): Fibonacci με Print

Τροποποιήστε τη συνάρτηση Fibonacci ώστε να εκτυπώνει κάθε βήμα: "Υπολογίζω F(n)..." και "F(n) = αποτέλεσμα". Παρατηρήστε πόσες φορές καλείται για κάθε τιμή!


2.10 Σύνοψη Κεφαλαίου

🎓 Σε αυτό το κεφάλαιο μάθαμε:
  • Τι είναι η αναδρομή — συνάρτηση που καλεί τον εαυτό της
  • Τα δύο απαραίτητα μέρη: βασική περίπτωση (σταματάει) και αναδρομική περίπτωση (προχωρά)
  • Πώς η C χρησιμοποιεί τη στοίβα (stack) για να αποθηκεύει τις ενδιάμεσες τιμές
  • Τις δύο φάσεις: κάθοδο (κλήσεις) και άνοδο (επιστροφές)
  • Τον κίνδυνο stack overflow από ατέρμονη αναδρομή
  • Πότε να επιλέγουμε αναδρομή vs επανάληψη

Επόμενο κεφάλαιο: Τελεστές και Εκφράσεις — πράξεις με μεταβλητές και τυχαίους αριθμούς!

🔢 Κεφάλαιο 3: Τελεστές και Εκφράσεις

Πράξεις με μεταβλητές και τυχαίους αριθμούς — ακέραια διαίρεση, modulo, και σύνθετοι τελεστές

3.1 Από τις Μεταβλητές στις Πράξεις

Στο προηγούμενο εξάμηνο μάθαμε να δηλώνουμε μεταβλητές και να παράγουμε ψευδοτυχαίους αριθμούς με τη rand(). Τώρα έρχεται το επόμενο βήμα: να κάνουμε πράξεις με αυτές τις τιμές.

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

📌 Θυμηθείτε: Ό,τι ξέρετε από τα μαθηματικά ισχύει και εδώ — με μια σημαντική διαφορά που θα δούμε παρακάτω!

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

Η C υποστηρίζει πέντε βασικές αριθμητικές πράξεις:

+
Πρόσθεση
5 + 3 = 8
-
Αφαίρεση
9 - 4 = 5
*
Πολλαπλασιασμός
3 * 4 = 12
/
Διαίρεση
10 / 4 = 2
%
Υπόλοιπο
10 % 3 = 1

3.2.1 Πλήρες Παράδειγμα

#include <stdio.h> int main() { int a = 10; int b = 4; printf("a + b = %d\n", a + b); /* 14 */ printf("a - b = %d\n", a - b); /* 6 */ printf("a * b = %d\n", a * b); /* 40 */ printf("a / b = %d\n", a / b); /* 2 ← ακέραια διαίρεση! */ printf("a %% b = %d\n", a % b); /* 2 (υπόλοιπο) */ return 0; }
a + b = 14 a - b = 6 a * b = 40 a / b = 2 a % b = 2

3.2.2 Η Παγίδα της Ακέραιας Διαίρεσης

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

⚠️ Παγίδα!
10 / 4 = 2    (ΟΧΙ 2.5 — το 0.5 χάνεται!)
7 / 2 = 3     (ΟΧΙ 3.5)
1 / 4 = 0     (ΟΧΙ 0.25)

Αν θέλουμε δεκαδικό αποτέλεσμα, χρησιμοποιούμε float ή double:

float x = 10.0f; float y = 4.0f; printf("%.2f\n", x / y); /* 2.50 ✓ */ /* Ή κάνουμε cast: */ int a = 10, b = 4; printf("%.2f\n", (float)a / b); /* 2.50 ✓ */

3.3 Ο Τελεστής Υπολοίπου — %

Ο τελεστής % (modulo) επιστρέφει το υπόλοιπο της ακέραιας διαίρεσης. Είναι ο πιο χρήσιμος τελεστής για καθημερινά προβλήματα προγραμματισμού:

10 % 3 → 10 = 3×3 + 1 → υπόλοιπο = 1 15 % 4 → 15 = 4×3 + 3 → υπόλοιπο = 3 8 % 2 → 8 = 2×4 + 0 → υπόλοιπο = 0 ← ζυγός! 7 % 2 → 7 = 2×3 + 1 → υπόλοιπο = 1 ← μονός!

3.3.1 Πότε Χρησιμοποιούμε το Modulo;

🔢 Έλεγχος αρτιότητας

Αν n % 2 == 0 → ζυγός
Αν n % 2 != 0 → μονός

🎲 Εύρος τυχαίων αριθμών

rand() % 6 → 0 έως 5
rand() % 6 + 1 → 1 έως 6 (ζάρι!)

#include <stdio.h> int main() { int n = 17; if (n % 2 == 0) printf("%d είναι ΖΥΓΟΣ\n", n); else printf("%d είναι ΜΟΝΟΣ\n", n); /* Εύρος τυχαίου: 1 έως 100 */ int rand_num = rand() % 100 + 1; printf("Τυχαίος (1-100): %d\n", rand_num); return 0; }
17 είναι ΜΟΝΟΣ Τυχαίος (1-100): 42
💡 Γενικός τύπος για εύρος τυχαίων:
rand() % N + MIN → τυχαίος αριθμός από MIN έως MIN + N - 1

Παραδείγματα:
rand() % 6 + 1 → 1 έως 6   (ζάρι)
rand() % 10 + 1 → 1 έως 10
rand() % 100 → 0 έως 99

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

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

int a = 2 + 3 * 4; /* Εκτελείται ως: 2 + (3*4) = 2 + 12 = 14 */ printf("%d\n", a); /* 14 */ int b = (2 + 3) * 4; /* Οι παρενθέσεις αλλάζουν τη σειρά: (2+3)*4 = 5*4 = 20 */ printf("%d\n", b); /* 20 */
14 20
✅ Συμβουλή: Χρησιμοποιείτε παρενθέσεις όταν δεν είστε σίγουροι για τη σειρά εκτέλεσης. Κάνουν τον κώδικα πιο ξεκάθαρο και αποτρέπουν λάθη.

3.5 Πρακτικό Παράδειγμα — Ρίψη Ζαριών

Ας συνδυάσουμε όλα όσα ξέρουμε: μεταβλητές, rand(), και τελεστές. Θα γράψουμε ένα πρόγραμμα που ρίχνει δύο ζάρια και υπολογίζει το άθροισμά τους:

#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand(time(NULL)); /* αρχικοποίηση γεννήτριας τυχαίων */ /* rand()%6 → 0..5 +1 → 1..6 */ int dice1 = rand() % 6 + 1; int dice2 = rand() % 6 + 1; int total = dice1 + dice2; printf("Ζάρι 1: %d\n", dice1); printf("Ζάρι 2: %d\n", dice2); printf("Άθροισμα: %d\n", total); if (total == 12) printf("Μέγιστο! Διπλό 6!\n"); return 0; }
Ζάρι 1: 4 Ζάρι 2: 6 Άθροισμα: 10
⚠️ Γιατί γράφουμε rand() % 6 + 1 και ΟΧΙ rand() % 7;
Το rand() % 7 δίνει αριθμούς 0, 1, 2, 3, 4, 5, 6 — δηλαδή 7 δυνατές τιμές και το 0 δεν ανήκει στο ζάρι!
Το rand() % 6 + 1 δίνει αριθμούς 1, 2, 3, 4, 5, 6 — ακριβώς αυτό που θέλουμε. ✓

3.6 Διαδραστικό Εργαλείο Τελεστών

Δοκιμάστε τις πράξεις με δικές σας τιμές — δείτε άμεσα τα αποτελέσματα:

🧮 Υπολογισμός Τελεστών

Εισάγετε τιμές και πατήστε Υπολόγισε...

3.7 Overflow — Τι Γίνεται όταν Ξεχειλίσει ο int;

Ο τύπος int δεν μπορεί να αποθηκεύσει οποιονδήποτε αριθμό — έχει συγκεκριμένα όρια. Στα περισσότερα συστήματα, ο μέγιστος int είναι 2.147.483.647. Τι γίνεται αν προσπαθήσουμε να πάμε παραπέρα;

📐 Γιατί ακριβώς 2.147.483.647; Ο int καταλαμβάνει 32 bits στη μνήμη — 32 ψηφία του δυαδικού συστήματος (0 ή 1). Το ένα bit χρησιμοποιείται για το πρόσημο (θετικός ή αρνητικός), οπότε μένουν 31 bits για τον αριθμό: 231 − 1 = 2.147.483.647.
💥 Overflow: Ο αριθμός "ξεχειλίζει" και ξεκινά από την αρχή — από την πιο αρνητική τιμή! Είναι σαν ωρολόγιο που μετά τις 23:59 πάει στις 00:00.
#include <stdio.h> int main() { int x = 2147483647; /* μέγιστος int */ printf("Πριν: %d ", x); x++; /* +1 → overflow! */ printf("Μετά: %d ", x); return 0; }
Πριν: 2147483647 Μετά: -2147483648
⚠️ Προσοχή: Ο μεταγλωττιστής δεν προειδοποιεί για overflow — το πρόγραμμα τρέχει κανονικά αλλά δίνει λάθος αποτέλεσμα. Αν χρειάζεστε μεγάλους αριθμούς, χρησιμοποιήστε long ή long long.

3.8 int vs float — Ακρίβεια Αριθμών

Οι αριθμοί float αποθηκεύονται στη μνήμη ως προσεγγίσεις — δεν είναι πάντα ακριβείς. Η κλασική έκπληξη: το 0.1 + 0.2 δεν ισούται με 0.3! Αν το τυπώσουμε με αρκετά δεκαδικά ψηφία, βλέπουμε κάτι τελείως αναπάντεχο:

#include <stdio.h> int main() { /* το 'f' στο τέλος δηλώνει ρητά float (32-bit) αν γράψουμε 0.1 χωρίς 'f', ο compiler το θεωρεί double (64-bit) και κάνει implicit μετατροπή σε float — μερικοί compilers βγάζουν warning γι' αυτό */ float a = 0.1f; float b = 0.2f; float c = a + b; printf("0.1 + 0.2 = %.1f ", c); /* φαίνεται σωστό */ printf("0.1 + 0.2 = %.10f ", c); /* με 10 δεκαδικά... */ return 0; }
0.1 + 0.2 = 0.3 0.1 + 0.2 = 0.3000000119
🔍 Γιατί συμβαίνει αυτό; Ο υπολογιστής αποθηκεύει αριθμούς σε δυαδικό σύστημα (0 και 1). Το 0.1 δεν αναπαρίσταται ακριβώς στο δυαδικό — όπως το 1/3 = 0.333... δεν γράφεται ακριβώς στο δεκαδικό.
✅ Πρακτικός κανόνας: Ποτέ μην συγκρίνετε float με ==. Αντί για if (c == 0.3f) γράφουμε if (c > 0.29f && c < 0.31f) — ελέγχουμε αν είναι "αρκετά κοντά".

3.9 Ασκήσεις με Λύσεις

Δοκιμάστε πρώτα μόνοι σας — και μετά ανοίξτε τη λύση για να ελέγξετε!

📝 Άσκηση 3.1 — Μετατροπή Θερμοκρασίας

Γράψτε πρόγραμμα που έχει αποθηκευμένη μια θερμοκρασία σε Celsius και την εκτυπώνει μετατρεπμένη σε Fahrenheit.

Τύπος: F = C × 9 / 5 + 32

💡 Υπόδειξη: Χρησιμοποιήστε float — τι θα συμβεί αν χρησιμοποιήσετε int;

📝 Άσκηση 3.2 — Χωρισμός Λογαριασμού

Ένας λογαριασμός εστιατορίου είναι 87 ευρώ για 5 άτομα. Χρησιμοποιήστε / και % για να βρείτε:

  • Πόσα ευρώ πληρώνει ο καθένας (χωρίς λεπτά)
  • Πόσα ευρώ περισσεύουν (που θα δοθούν ως φιλοδώρημα)

📝 Άσκηση 3.3 — Μετρητής Βήμα-Βήμα

Ξεκινήστε από counter = 0. Χρησιμοποιώντας ++, += και *=, φτάστε στο 10 με 4 κινήσεις. Εκτυπώστε την τιμή μετά από κάθε κίνηση.

💡 Δεν υπάρχει μία μόνο σωστή λύση!

📝 Άσκηση 3.4 — Τυχαία Βαθμολογία

Παράγετε έναν τυχαίο βαθμό στο εύρος 50 έως 100 και εκτυπώστε τον βαθμό, πόσοι βαθμοί λείπουν για το 100, και αν είναι ζυγός ή μονός.

📝 Άσκηση 3.5 — Δευτερόλεπτα σε Ώρες, Λεπτά, Δευτερόλεπτα

Δοθέντος ενός αριθμού δευτερολέπτων (π.χ. 3725), αναλύστε τον σε ώρες, λεπτά και δευτερόλεπτα.

💡 Υπόδειξη: Χρησιμοποιήστε / και % — ακριβώς όπως στο χωρισμό λογαριασμού!

📝 Άσκηση 3.6 — Τριψήφιος Αριθμός: Ανάλυση Ψηφίων

Δοθέντος ενός τριψήφιου αριθμού (π.χ. 457), εκτυπώστε ξεχωριστά τα ψηφία του: εκατοντάδες, δεκάδες, μονάδες.

💡 Υπόδειξη: Μόνο / και % χρειάζονται!