Περίληψη
Τα δύο μαθήματα που προηγήθηκαν δίδαξαν τι είναι μια συνάρτηση και πώς μπαίνουν και βγαίνουν τα δεδομένα της· αυτό διδάσκει πώς αποφασίζουμε ποιες συναρτήσεις θα γράψουμε. Η μέθοδος είναι η σταδιακή εκλέπτυνση: γράφεται μια σύντομη main που καλεί συναρτήσεις οι οποίες δεν υπάρχουν ακόμη, η καθεμία με τη σύμβασή της, και κατεβαίνουμε σε καθεμία ώσπου να φτάσουμε σε μέρη που γράφονται σε λίγες γραμμές. Το μάθημα θέτει τα κριτήρια μιας καλής διεπαφής, τον κανόνα «μία συνάρτηση, μία εργασία» με τις ενδείξεις που τον παραβιάζουν, και την υπερφόρτωση, που δίνει το ίδιο όνομα στην ίδια εργασία πάνω σε διαφορετικούς τύπους. Με αυτά τα κριτήρια ξαναγράφει τους αλγορίθμους της ενότητας 4, που ζούσαν μέσα στη main, και τους δοκιμάζει χωριστά. Η έννοια που ενοποιεί τα πάντα είναι η καθοδική αποσύνθεση: ένα καλά σχεδιασμένο πρόγραμμα διαβάζεται σαν η ίδια του η προδιαγραφή.
Μαθησιακοί στόχοι
- Να αποσυνθέτει ένα πρόβλημα σε υποπροβλήματα με σταδιακή εκλέπτυνση και να γράφει το δέντρο της αποσύνθεσης.
- Να σχεδιάζει τη διεπαφή κάθε συνάρτησης (όνομα, παράμετροι, επιστρεφόμενη τιμή και σύμβαση) πριν γράψει το σώμα της.
- Να ξαναγράφει ένα μονολιθικό πρόγραμμα της ενότητας 4 ως σύνολο συνεκτικών συναρτήσεων και να δοκιμάζει καθεμία χωριστά με μια δοκιμαστική
main. - Να κρίνει ένα σχέδιο με βάση τον κανόνα «μία συνάρτηση, μία εργασία» και την απουσία κρυφών επιδράσεων.
- Να υπερφορτώνει μια συνάρτηση, να προβλέπει ποια εκδοχή επιλέγει κάθε κλήση και να εξηγεί γιατί μια κλήση είναι διφορούμενη.
Σταδιακή εκλέπτυνση
Από την εκφώνηση σε μια main λίγων γραμμών
Τα προγράμματα της ενότητας 4 γράφτηκαν ολόκληρα μέσα στη main. Όσο χωρούν σε μία οθόνη, αυτό γίνεται ανεκτό· ωστόσο ένα πρόγραμμα διακοσίων γραμμών γραμμένο έτσι απαιτεί να τα έχει κανείς όλα στο μυαλό του ταυτόχρονα και δέχεται μόνο τη δοκιμή του συνόλου. Η σταδιακή εκλέπτυνση, ή καθοδικός σχεδιασμός, αντιστρέφει τη σειρά της δουλειάς: πρώτα γράφεται τι κάνει το πρόγραμμα, στη γλώσσα του προβλήματος, ως ακολουθία κλήσεων σε συναρτήσεις που δεν υπάρχουν ακόμη· έπειτα κάθε τέτοια συνάρτηση αντιμετωπίζεται ως νέο, μικρότερο πρόβλημα, και η διαδικασία επαναλαμβάνεται ώσπου να φτάσουμε σε συναρτήσεις τόσο απλές ώστε το σώμα τους να γράφεται χωρίς να αποσυνθέσουμε τίποτα.
Ως παράδειγμα χρησιμεύει το ακόλουθο πρόβλημα. Ένας ακέραιος είναι τέλειος αν το άθροισμα των γνήσιων διαιρετών του (των θετικών που είναι διαφορετικοί από τον ) ισούται με , ελλιπής αν είναι μικρότερο και υπερτέλειος αν είναι μεγαλύτερο: είναι τέλειος· ο 8, με , είναι ελλιπής· ο 12, με , υπερτέλειος. Ζητείται πρόγραμμα που διαβάζει τον και γράφει το άθροισμα των γνήσιων διαιρετών του και την κατηγορία του. Το πρώτο επίπεδο του σχεδιασμού είναι η main, και γράφεται πριν από οτιδήποτε άλλο:
#include <cstdint>#include <iostream>#include <print>#include <string>enum class Kind { deficient, perfect, abundant };// Άθροισμα των θετικών διαιρετών του n.// Προσυνθήκη: n >= 1.// Μετασυνθήκη: επιστρέφει το άθροισμα όλων των θετικών διαιρετών του n, μαζί με τον n.std::int64_t divisorSum(int n);// Κατηγορία του n με βάση το άθροισμα των γνήσιων διαιρετών του.// Προσυνθήκη: n >= 1.// Μετασυνθήκη: επιστρέφει deficient, perfect ή abundant αν το άθροισμα είναι μικρότερο, ίσο ή μεγαλύτερο από n.Kind classify(int n);// Όνομα μιας κατηγορίας, για να γραφτεί.// Προσυνθήκη: καμία.// Μετασυνθήκη: επιστρέφει «ελλιπής», «τέλειος» ή «υπερτέλειος».std::string kindName(Kind kind);int main(){ int n{}; std::cin >> n; std::println("Γνήσιοι διαιρέτες του {}: άθροισμα {}", n, divisorSum(n) - n); std::println("Ο {} είναι {}", n, kindName(classify(n)));}/usr/bin/x86_64-linux-gnu-ld.bfd: main.o: in function `main':main.cpp:27:(.text+0x22a): undefined reference to `divisorSum(int)'/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:28:(.text+0x3fd): undefined reference to `classify(int)'/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:28:(.text+0x40d): undefined reference to `kindName[abi:cxx11](Kind)'collect2: error: ld returned 1 exit statusΤο πρόγραμμα μεταγλωττίζεται χωρίς καμία προειδοποίηση, γιατί κάθε κλήση συμφωνεί με τη δήλωσή της, που είναι το μόνο που ελέγχει ο μεταγλωττιστής, όπως δίδαξε το μάθημα Συναρτήσεις. Εκείνο που αποτυγχάνει είναι η σύνδεση: ο συνδέτης αναζητά τους ορισμούς και δεν τους βρίσκει («undefined reference», αναφορά χωρίς ορισμό· η προσθήκη [abi:cxx11] είναι εσωτερική σήμανση του GCC για τις συναρτήσεις που επιστρέφουν std::string). Αυτή η κατάσταση είναι ακριβώς η κατάσταση του σχεδιασμού σε αυτό το σημείο: οι διεπαφές έχουν αποφασιστεί, τα σώματα όχι. Αν αυτό ισχύει, η main διαβάζεται ήδη ως η προδιαγραφή του προγράμματος, και οι τρεις δηλώσεις είναι οι εκκρεμείς εργασίες, η καθεμία μικρότερη από το αρχικό πρόβλημα.
Το δέντρο της αποσύνθεσης
Το δεύτερο επίπεδο κατεβαίνει σε κάθε δήλωση. Η kindName είναι μια απόφαση τριών περιπτώσεων. Η classify στηρίζεται στη divisorSum: ένας αριθμός είναι τέλειος όταν το άθροισμα όλων των διαιρετών του είναι . Και η divisorSum είναι ο βρόχος των διαιρετών σε ζεύγη από το μάθημα Αριθμητικοί αλγόριθμοι, που σταματά στο . Το αποτέλεσμα σχεδιάζεται ως δέντρο: η ρίζα είναι το πλήρες πρόβλημα· τα παιδιά κάθε κόμβου, τα υποπροβλήματα που τον λύνουν· τα φύλλα, πράξεις που η γλώσσα ήδη προσφέρει.
Το σχήμα αναπτύσσει το δέντρο επίπεδο προς επίπεδο, με τη σειρά με την οποία σχεδιάστηκε.
Απόδειξη
Το πρώτο επίπεδο είναι ολόκληρο το πρόβλημα, ευθύνη της
main, που δεν έχει ακόμη αποσυντεθεί.Το δεύτερο επίπεδο είναι η
mainτων πέντε λειτουργιών: διάβασμα, άθροιση, κατάταξη, ονομασία και γραφή. Κάθε κόμβος είναι μια κλήση· οι κλήσεις της βιβλιοθήκης υπάρχουν ήδη, οι υπόλοιπες είναι δηλωμένες και εκκρεμείς.Το τρίτο επίπεδο αποσυνθέτει την
classify, που δεν κάνει τίποτε άλλο από το να καλεί τηdivisorSumκαι να συγκρίνει. Η ίδια συνάρτηση εμφανίζεται δύο φορές στο δέντρο και γράφεται μία μόνο: η επαναχρησιμοποίηση φαίνεται στο δέντρο πριν γραφτεί κώδικας.Το τελευταίο επίπεδο είναι τα φύλλα: ο έλεγχος διαιρετότητας και το όριο των ζευγών, πράξεις της γλώσσας. Όταν φτάσουμε σε αυτά η εκλέπτυνση τελειώνει, γιατί δεν μένει τίποτα να αποσυντεθεί.
Με το δέντρο πλήρες, οι ορισμοί γράφονται από κάτω προς τα πάνω, ώστε κάθε συνάρτηση να στηρίζεται σε άλλες ήδη γραμμένες:
#include <cstdint>#include <iostream>#include <print>#include <string>enum class Kind { deficient, perfect, abundant };// Άθροισμα των θετικών διαιρετών του n.// Προσυνθήκη: n >= 1.// Μετασυνθήκη: επιστρέφει το άθροισμα όλων των θετικών διαιρετών του n, μαζί με τον n.std::int64_t divisorSum(int n){ std::int64_t sum{0}; for (int d{1}; d <= n / d; ++d) { if (n % d == 0) { sum += d; if (d != n / d) { sum += n / d; } } } return sum;}// Κατηγορία του n με βάση το άθροισμα των γνήσιων διαιρετών του.// Προσυνθήκη: n >= 1.// Μετασυνθήκη: επιστρέφει deficient, perfect ή abundant αν το άθροισμα είναι μικρότερο, ίσο ή μεγαλύτερο από n.Kind classify(int n){ const std::int64_t proper{divisorSum(n) - n}; if (proper < n) { return Kind::deficient; } if (proper == n) { return Kind::perfect; } return Kind::abundant;}// Όνομα μιας κατηγορίας, για να γραφτεί.// Προσυνθήκη: καμία.// Μετασυνθήκη: επιστρέφει «ελλιπής», «τέλειος» ή «υπερτέλειος».std::string kindName(Kind kind){ if (kind == Kind::deficient) { return "ελλιπής"; } if (kind == Kind::perfect) { return "τέλειος"; } return "υπερτέλειος";}int main(){ int n{}; std::cin >> n; std::println("Γνήσιοι διαιρέτες του {}: άθροισμα {}", n, divisorSum(n) - n); std::println("Ο {} είναι {}", n, kindName(classify(n)));}28Γνήσιοι διαιρέτες του 28: άθροισμα 28Ο 28 είναι τέλειοςΤρία γνωρίσματα του αποτελέσματος οφείλονται στη μέθοδο. Πρώτον, η main δεν άλλαξε όταν γράφτηκαν όσα βρίσκονται από κάτω. Δεύτερον, το άθροισμα επιστρέφεται ως std::int64_t (από τη <cstdint>, ο ακέραιος των 64 bit που χρησιμοποιεί και το μάθημα Αναδρομή), γιατί για κοντά στο μέγιστο του int το άθροισμα των διαιρετών του μπορεί να το υπερβεί, και η απόφαση μέσα στη διεπαφή προστατεύει όλους όσοι καλούν. Τρίτον, κάθε συνάρτηση ελέγχεται χωριστά: η divisorSum(28) πρέπει να δώσει 56, η divisorSum(1) πρέπει να δώσει 1 και η divisorSum(36) πρέπει να δώσει 91, όπου το ζεύγος μετριέται μία μόνο φορά· η τελευταία παράγραφος του μαθήματος μετατρέπει αυτούς τους ελέγχους σε πρόγραμμα.
Φόρτωση του περιεχομένου…
Ολοκληρώθηκε
Συνδεθείτε για να αποθηκεύσετε την πρόοδό σας