Μετάβαση στο περιεχόμενο
Topos Uranos

Περίληψη

Ένας ακέραιος είναι, για τον επεξεργαστή, μια σειρά από bit, και η C++ προσφέρει τελεστές που δρουν πάνω σε αυτή τη σειρά στήλη προς στήλη: &, |, ^ και ~, καθώς και τις ολισθήσεις << και >>. Αυτό το μάθημα διδάσκει πώς υπολογίζονται με το χέρι, πώς χρησιμοποιούνται για να φυλάσσονται πολλές σημαίες σε έναν μόνο ακέραιο με την τεχνική των μασκών και πώς αναγνωρίζονται οι ολισθήσεις που το πρότυπο δεν ορίζει. Εξηγεί έπειτα γιατί το 2 ^ 3 αξίζει 1 και όχι 8, και γιατί οι τελεστές bit απαιτούν παρενθέσεις. Το δεύτερο μισό παρουσιάζει τη μαθηματική βιβλιοθήκη: τις συναρτήσεις της <cmath>, τις σταθερές της <numbers>, τον μέγιστο κοινό διαιρέτη και το ελάχιστο κοινό πολλαπλάσιο της <numeric> και τις συναρτήσεις bit της <bit>, με μια αρχή που ισχύει σε κάθε γλώσσα: ό,τι η βιβλιοθήκη προσφέρει σωστό και δοκιμασμένο δεν το ξαναγράφουμε.

Μαθησιακοί στόχοι

  1. Να υπολογίζετε με το χέρι το αποτέλεσμα των &, |, ^, ~ και των ολισθήσεων σε μικρούς ακεραίους.
  2. Να διακρίνετε τις ορισμένες ολισθήσεις (και εκείνες των αρνητικών, από τη C++20) από εκείνες που είναι απροσδιόριστη συμπεριφορά, και να προβλέπετε το αποτέλεσμα των πρώτων.
  3. Να χρησιμοποιείτε μάσκες για να ελέγχετε, να ενεργοποιείτε, να απενεργοποιείτε και να εναλλάσσετε ένα bit.
  4. Να εξηγείτε γιατί το 2 ^ 3 αξίζει 1 και όχι 8, και να επιλέγετε ανάμεσα στη std::pow και σε μια ολίσθηση ανάλογα με την περίπτωση.
  5. Να χρησιμοποιείτε συναρτήσεις της <cmath> (std::sqrt, std::pow, std::abs, std::hypot, τις τριγωνομετρικές) και τις σταθερές της <numbers>, όπως το std::numbers::pi, σε τύπους.

Τελεστές bit

Το μάθημα Οι ακέραιοι και η αναπαράστασή τους έδειξε ότι ένας int είναι μια σειρά από bit, 32 στα συστήματα του κύκλου μαθημάτων, και ότι η τιμή του είναι το άθροισμα των βαρών των θέσεων όπου υπάρχει 1. Ως τώρα, ωστόσο, το πρόγραμμα έχει χειριστεί τον ακέραιο μόνο ως αριθμό: τον έχει προσθέσει, πολλαπλασιάσει και διαιρέσει. Όμως η ίδια σειρά δέχεται και άλλη ανάγνωση, ως συλλογή 32 απαντήσεων ναι ή όχι, και για αυτή την ανάγνωση η C++ έχει τέσσερις τελεστές που δουλεύουν πάνω στα bit ένα προς ένα, χωρίς κρατούμενο ανάμεσα στις θέσεις.

Τα bit αριθμούνται από τα δεξιά, αρχίζοντας από το 0: το bit kk είναι εκείνο με βάρος 2k2^k. Έτσι, στο 1212, που στο δυαδικό γράφεται 1100, είναι ενεργά τα bit 2 και 3 και ανενεργά τα bit 0 και 1.

Οι τελεστές είναι οι εξής. Το δυαδικό ΚΑΙ, a & b, έχει 1 σε κάθε θέση όπου τα a και b έχουν και τα δύο 1. Το δυαδικό Ή, a | b, έχει 1 όπου τουλάχιστον ένα από τα δύο το έχει. Το αποκλειστικό Ή, a ^ b, έχει 1 όπου ακριβώς ένα από τα δύο το έχει, δηλαδή όπου τα bit διαφέρουν. Η δυαδική άρνηση, ~a, έχει έναν μόνο τελεσταίο και αντιστρέφει κάθε bit. Οι πίνακες των τριών πρώτων, για ένα ζεύγος bit pp και qq, είναι εκείνοι της σύζευξης, της διάζευξης και της αποκλειστικής διάζευξης:

pqp&qp∣qp^q11110100110101100000\begin{array}{cc|ccc} p & q & p \mathbin{\&} q & p \mid q & p \mathbin{\hat{}} q \\ \hline 1 & 1 & 1 & 1 & 0 \\ 1 & 0 & 0 & 1 & 1 \\ 0 & 1 & 0 & 1 & 1 \\ 0 & 0 & 0 & 0 & 0 \end{array}

Όποιος έχει μελετήσει τον κύκλο μαθημάτων «Μαθηματική λογική I» θα αναγνωρίσει εδώ τους προτασιακούς συνδέσμους, με το 1 για το αληθές και το 0 για το ψευδές: ένας τελεστής bit δεν είναι τίποτε άλλο από έναν σύνδεσμο που εφαρμόζεται σε όλες τις στήλες ταυτόχρονα. Από αυτό έπεται η μέθοδος υπολογισμού με το χέρι: γράφουμε τους δύο αριθμούς στο δυαδικό, τον έναν κάτω από τον άλλον, στοιχισμένους από τα δεξιά, και εφαρμόζουμε τον πίνακα σε κάθε στήλη, χωρίς να μας απασχολούν οι γειτονικές.

ΠαράδειγμαΔώδεκα και δέκα, στήλη προς στήλη

Οι αριθμοί 12=12 = 1100 και 10=10 = 1010 έχουν την ιδιότητα ότι οι τέσσερις στήλες τους διατρέχουν τους τέσσερις συνδυασμούς δύο bit· γι’ αυτό χρησιμεύουν για να δούμε τους τρεις πίνακες ταυτόχρονα.

Απόδειξη

  1. Τα δεδομένα: το 1100 είναι 12 και το 1010 είναι 10. Οι στήλες, από αριστερά προς τα δεξιά, έχουν τα ζεύγη (1,1)(1, 1), (1,0)(1, 0), (0,1)(0, 1) και (0,0)(0, 0).

  2. Πρώτη στήλη του 12 & 10: και τα δύο bit είναι 1, και το ΚΑΙ δίνει 1.

  3. Δεύτερη στήλη: 1 και 0. Αρκεί ένα 0 για να δώσει το ΚΑΙ 0.

  4. Τρίτη στήλη: 0 και 1· το ΚΑΙ δίνει πάλι 0.

  5. Τέταρτη στήλη: 0 και 0 δίνουν 0. Η γραμμή του αποτελέσματος, 1000, είναι 8: το 12 & 10 αξίζει 8.

  6. Το Ή ενεργοποιεί κάθε στήλη που έχει τουλάχιστον ένα 1: 1110, δηλαδή το 12 | 10 αξίζει 14.

  7. Το αποκλειστικό Ή ενεργοποιεί μόνο τις στήλες των οποίων τα bit διαφέρουν: 0110, και το 12 ^ 10 αξίζει 6.

Το πρόγραμμα που ακολουθεί επαληθεύει το σχήμα. Το πεδίο μορφοποίησης {:04b}, που παρουσιάστηκε στο μάθημα για τους ακεραίους, γράφει μια τιμή στο δυαδικό με τέσσερα ψηφία, συμπληρωμένα με μηδενικά.

Πρόγραμμα σε C++
#include <print>​int main(){    const int a{0b1100};    const int b{0b1010};    std::println("a     = {:04b} = {}", a, a);    std::println("b     = {:04b} = {}", b, b);    std::println("a & b = {:04b} = {}", a & b, a & b);    std::println("a | b = {:04b} = {}", a | b, a | b);    std::println("a ^ b = {:04b} = {}", a ^ b, a ^ b);    std::println("~a    = {}", ~a);}
Έξοδος
a     = 1100 = 12b     = 1010 = 10a & b = 1000 = 8a | b = 1110 = 14a ^ b = 0110 = 6~a    = -13

Η τελευταία γραμμή θέλει μια εξήγηση. Η άρνηση αντιστρέφει τα 32 bit του a, όχι μόνο τα τέσσερα που γράφονται: τα 28 μηδενικά στα αριστερά γίνονται άσσοι, και ένας int που αρχίζει με 1 είναι αρνητικός σε συμπλήρωμα ως προς δύο. Όμως, σε συμπλήρωμα ως προς δύο, ο αντίθετος ενός αριθμού προκύπτει αν αντιστρέψουμε τα bit του και προσθέσουμε 1, δηλαδή −x=∼x+1-x = {\sim}x + 1· επομένως ∼x=−x−1{\sim}x = -x - 1, και το ~12 αξίζει −13-13. Η δυαδική άρνηση σε έναν ακέραιο με πρόσημο δίνει λοιπόν έναν αριθμό που σπάνια ενδιαφέρει ως αριθμός: ενδιαφέρει ως συνδυασμός bit, και γι’ αυτό τα bit τα χειριζόμαστε κατά προτίμηση σε τύπους χωρίς πρόσημο, όπως θα φανεί στην παράγραφο για τις μάσκες.

Μια προειδοποίηση για τους μικρούς τύπους. Πριν από την πράξη, η C++ μετατρέπει σε int κάθε τελεσταίο ενός στενότερου ακέραιου τύπου, όπως ο std::uint8_t· είναι η προαγωγή που το μάθημα Αριθμητικές πράξεις περιέγραψε για τις αριθμητικές πράξεις, και ισχύει και εδώ. Από αυτό έπεται ότι το ~flags, με το flags τύπου std::uint8_t και τιμή 5, δεν είναι το byte 11111010, αλλά ο int −6-6, του οποίου τα 32 bit περιλαμβάνουν 24 άσσους προστιθέμενους στα αριστερά· μόνο όταν αποθηκευτεί ξανά σε έναν std::uint8_t περικόπτεται σε οκτώ bit και ανακτάται το αναμενόμενο byte, 250.

Δεν πρέπει να συγχέονται οι τελεστές bit με τους λογικούς τελεστές του μαθήματος Σχέσεις και λογική. Το 6 && 3 είναι true: και οι δύο τελεσταίοι είναι διάφοροι του μηδενός, και το αποτέλεσμα είναι bool. Το 6 & 3, αντίθετα, είναι ακέραιος, εκείνος που σχηματίζουν οι στήλες όπου και οι δύο έχουν 1. Επιπλέον, το && αποτιμάται με βραχυκύκλωση, ενώ το & αποτιμά πάντα και τους δύο τελεσταίους.