Εισαγωγή στη C

Επικοινωνήστε με: Παναγιώτη Παύλου

e-mail: allos@mail.ntua.gr

Μία "αθώα" συνάρτηση!

Οι συναρτήσεις είναι μια έννοια γνωστή μας τόσο από τα μαθηματικά όσο και από τον προγραμματισμό σε άλλες γλώσσες. Για παράδειγμα η παρακάτω συνάρτηση υπολογίζει και επιστρέφει το άθροισμα δύο ακέραιων αριθμών. Παρατηρούμε όμως...

int mySum ( int a, int b ) {
	int s;
    s = a + b;
    return s;
} // Αν την καλέσουμε ως: mySum(3,8) μας δίνει το αποτέλεσμα 11
  • Το όνομα της συνάρτησης
  • Τις παραμέτρους
  • Το σώμα της συνάρτησης
  • Την τιμή που επιστρέφεται
  • Τη μεταβλητή s
  • Τον τύπο δεδομένων int
  • Το ερωτηματικό ;
  • Τους τελεστές = και +
  • Και ένα σχόλιο
Μια καλή εξήγηση

Άγκιστρα, Αγκύλες, Παρενθέσεις! Από τα πιο βασικά να θυμάστε είναι:

{ } Άγκιστρα

[ ] Αγκύλες

( ) Παρενθέσεις

Ονόματα

Τα ονόματα συναρτήσεων και μεταβλητών έχουν τους ίδιους περιορισμούς και ιδιότητες.

Οι χαρακτήρες
που επιτρέπονται σε ένα όνομα είναι μόνο οι: a-z , A-Z , 0-9 και _ που διαβάζεται underscore. Το πλήθος τους μπορεί να είναι "απεριόριστο" στην πράξη λαμβάνονται υπόψη (ανάλογα με τον μεταφραστή compiler) μόνο οι (αρκετοί ~32) πρώτοι χαρακτήρες.
Ο πρώτος χαρακτήρας
δεν επιτρέπεται να είναι αριθμός, με αριθμό ξεκινάνε οι αριθμητικές σταθερές όπως θα δούμε αργότερα
Είναι case sensitive
, δηλαδή: τα mySum και mysum αλλά και MYSum ΔΕΝ είναι η ίδια μεταβλητή (ή συνάρτηση)
Δεν πρέπει να συμπίπτουν με τις δεσμευμένες λέξεις
της γλώσσας C που φαίνονται στην επόμενη διαφάνεια και έχουν ειδική σημασία που θα δούμε στην πορεία.
Λέξεις κλειδιά

Οι παρακάτω είναι οι λέξεις κλειδιά (keywords) που έχουν ειδική σημασία στην C. Ήδη έχουμε γνωρίσει τις int και return.

  • auto
  • register
  • static
  • extern
  • const
  • volatile
  • signed
  • unsigned
  • short
  • long
  • void
  • char
  • int
  • float
  • double
  • while
  • do
  • for
  • continue
  • break
  • if
  • else
  • switch
  • case
  • default
  • goto
  • return
  • struct
  • union
  • enum
  • typedef
  • sizeof
Οι μεταβλητές

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

  • Το όνομα της ✓
  • Τα δεδομένα που βρίσκονται στη μνήμη
  • Τον τύπο δεδομένων της
  • Την εμβέλειά της

Οι μεταβλητές για να χρησιμοποιηθούν μέσα σε ένα πρόγραμμα θα πρέπει να οριστούν. Και για να αποφύγουμε τις "κακοτοπιές" φροντίζουμε πάντα να τις δηλώνουμε στην αρχή του block { } στο οποίο θα "ζήσουν" ( = εμβέλεια ). Μόλις η ροή του προγράμματος βγει εκτός του συγκεκριμένου block, τότε και οι μεταβλητές που έχουν δηλωθεί σε αυτό σταματούν να υπάρχουν, ο υπολογιστής τις "ξεχνάει".

Τα δεδομένα και ο τύπος τους

Τα δεδομένα βρίσκονται στη μνήμη σε δυαδική μορφή (bit με τιμή 0 ή 1) οργανωμένα σε οκτάδες ( bytes που το καθένα αντιστοιχεί σε μία θέση μνήμης - αυτό ισχύει για τους microcontrollers τουλάχιστον ) και σε αυτή τη μορφή αξιοποιούνται από τον επεξεργαστή. Η ακολουθία αυτή των 0 και 1 δεν έχει νόημα απο μόνη της. Π.χ. 1000 0001 Ο τύπος των δεδομένων είναι αυτός που σε συνδυασμό με το περιεχόμενο καθορίζει την τιμή τους. Στη δήλωση των μεταβλητών ο τύπος αναγράφεται ακριβώς στα αριστερά τους char x;

Εδώ ο τύπος δεδομένων είναι ο char που υποδηλώνει έναν ακέραιο αριθμό που στη μνήμη καταλαμβάνει 8bits. Οι τιμές είναι εξορισμού προσημασμένες στη C, έτσι μπορούμε να παραστήσουμε τιμές από: -128 έως και +127 (= εύρος τιμών 256 = 28). Βάσει του συμπληρώματος ως προς δύο (Σ2) η τιμή του παραπάνω δυαδικού είναι -127 = -128+1. Εάν μπροστά από τον τύπο προσθέταμε και τη λέξη unsigned θα ήταν unsigned char x; και η μεταβλητή δεν θα μπορούσε να πάρει αρνητικές τιμές (μόνο από 0 έως 255 = 28-1) δηλαδή για το ίδιο περιεχόμενο μνήμης η τιμή θα ήταν 129 = 128+1.

Bασικοί τύποι δεδομένων

Οι βασικοί αριθμητικοί τύποι δεδομένων στη C φαίνονται στον ακόλουθο πίνακα.

* Ο τύπος αυτός αφορά μόνο συναρτήσεις
** Το πραγματικό μήκος εξαρτάται από τον κάθε επεξεργαστή και την αρχιτεκτονική του συστήματος στο οποίο εκτελείται ο κώδικας.
Τύπος Bits
void 0*
char 8
int 16**
float 32
double 64

Ο τύπος void περιγράφει την έλλειψη τύπου δεδομένων ή και την ίδια την έλλειψη δεδομένων. Το χρησιμοποιούμε για να δηλώσουμε μια συνάρτηση που δεν έχει αριθμητικό αποτέλεσμα. Spoiler Alert : Θα τον ξαναδούμε αργότερα στους δείκτες (pointers) με την έννοια της έλλειψης τύπου δεδομένων.

Οι λογικές μεταβλητές (τύπου true / false) στην πραγματικότητα αντιμετωπίζονται ως ακέραιες τιμές. Το 0 σημαίνει false και ό,τι άλλο ισοδυναμεί με true. Όμως όταν το true προκύπτει ως αποτέλεσμα τότε ισούται με το 1. Δηλαδή το NOT του 8 !8 είναι το 0 και το ΝΟΤ του 0 !0 είναι το 1 (ενδιαφέρον!)

Παραλλαγές τύπων δεδομένων

Οι παραπάνω τύποι δεδομένων μπορούν να μεταβληθούν με τις λέξεις κλειδιά που παρουσιάζονται σε αυτή τη διαφάνεια. Ο κάθε τύπος δεδομένων μπορεί να έχει keywords διαμόρφωσης διαφόρων ιδιοτήτων. Η μία ιδιότητα είναι το μέγεθος σε bits του τύπου δεδομένων. Το οποίο μπορεί να αυξάνουν (διπλασιάζουν) ή να μειώνουν εφόσον αυτό υποστηρίζεται από τον συγκεκριμένο κάθε φορά επεξεργαστή. Οι λέξεις κλειδιά αυτές είναι short και long.
Η άλλη κατηγορία αφορά την υποστήριξη προσήμου οπότε έχουμε τις λέξεις κλειδιά signed και unsigned.
Τέλος μπορεί να ορίζεται η δυνατότητα μεταβολής της τιμής με τις λέξεις κλειδιά const και volatile που η πρώτη δεν επιτρέπει την αλλαγή της τιμής της μεταβλητής μετά την αρχική ανάθεση, ενώ η δεύτερη προειδοποιεί τον μεταφραστή για την πιθανότητα να αλλάξει η τιμή της μεταβλητής από εξωτερικούς ως προς το πρόγραμμα παράγοντες.
Περισσότερες πληροφορίες μπορείτε να βρείτε και online π.χ. https://en.wikipedia.org/wiki/C_data_types

Ανάθεση και (σταθερές) τιμές

Οι μεταβλητές παίρνουν τιμή με τη χρήση του τελεστή εκχώρησης = και καλή πρακτική είναι να γίνεται αυτό ταυτόχρονα με τη δήλωσή τους, ώστε να είμαστε πάντα βέβαιοι για την αρχική τους τιμή. Π.χ. int x = 0;.
Αν και οι τιμές που αναθέτουμε είναι συνήθως σταθερές αυτό δεν είναι απαραίτητο. Παρόλα αυτά τόσο στην εκχώρηση τιμής όσο και σε κάθε άλλο σημείο που χρησιμοποιούνται σταθερές τιμές στη C υπάρχουν οι εξής περιπτώσεις:

Όταν γράφουμε Τύπος/Σύστημα Τιμή στο δεκαδικό
10 Ακέραιος Δεκαδικός 10
010 Ακέραιος Οκταδικός 8
0x10 Ακέραιος Δεκαεξαδικός 16
12.34 Κινητής υποδιαστολής 12.34
25.88e-6 Επιστημονική γραφή 25.88 * 10-6

Μπροστά από αυτές τις τιμές αυτές μπορεί να υπάρχει πρόσημο + (που αγνοείται) ή - για την αναπαράσταση των αρνητικών αριθμών.

Αριθμητικές Παραστάσεις

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

float A, B, C;
int y = 1;
float x = 10.22 + mySum(y, 11);

A = ( x + y ) / 2;
B = ( A - 2 ) * 4.11;
C = ( mySum(A,B) % 7 ) + 1;
  • Μεταβλητές A,B,C,x,y
  • Σταθερές 10.22 , 2 , 4.11 , 7
  • Τελεστές = + - * / % ( )
Οι Τελεστές

Υπάρχουν τρεις τύποι τελεστών, ενώ τους διακρίνουμε και ανά λειτουργία:

  • Μοναδιαίοι ή Unary ~ ! + - * που δέχονται ένα όρισμα στα δεξιά τους. Τα + - εδώ αναφέρονται ως πρόσημα.
  • Δυαδικοί ή Binary + - * / % & && | || = == != < <= > >= κλπ που εμπλέκουν δύο ποσότητες
  • Τριαδικός ή Ternary που είναι μόνο ένας και είναι ο ?: που θα γνωρίσουμε αργότερα

  • Αριθμητικούς που αφορούν πράξεις και εκχώρηση τιμής + - * / % =
  • Αριθμητικούς μαζί με εκχώρηση τιμής += -= *= κοκ και ++ --
  • Σύγκρισης == != < > <= >= δίνουν "λογικό" αποτέλεσμα (αληθές/ψευδές)
  • Λογικούς && || ! που συνδυάζουν μεταξύ τους λογικά αποτελέσματα και προκύπτει νέο λογικό αποτέλεσμα
  • Δυαδικού συστήματος ~ ! & | ^ << >> αφορούν πράξεις ακεραίων bit προς bit
  • Και άλλους όπως είναι οι ( ) , οι [ ] και τα { }
Η προτεραιότητα των τελεστών

Όπως ξέρουμε και από τα μαθηματικά υπάρχει συγκεκριμένη σειρά με την οποία εκτελούνται οι πράξεις. Έτσι και στη C ορίζεται πολύ συγκεκριμένα η προτεραιότητα των τελεστών. Μπορείτε να δείτε εδώ online.

Operator Description Associativity
( )
[ ]
.
->
++ --
Parentheses (function call)
Brackets (array subscript)
Member selection via object name
Member selection via pointer
Postfix increment/decrement
left-to-right
++ --
+ -
! ~
(type)
*
&
sizeof
Prefix increment/decrement
Unary plus/minus
Logical negation/bitwise complement
Cast (convert value to temporary value of type)
Dereference
Address (of operand)
Determine size in bytes on this implementation
right-to-left
*  /  % Multiplication/division/modulus left-to-right
+  - Addition/subtraction left-to-right
<<  >> Bitwise shift left, Bitwise shift right left-to-right
<  <=
>  >=
Relational less than/less than or equal to
Relational greater than/greater than or equal to
left-to-right
==  != Relational is equal to/is not equal to left-to-right
& Bitwise AND left-to-right
^ Bitwise exclusive OR left-to-right
| Bitwise inclusive OR left-to-right
&& Logical AND left-to-right
| | Logical OR left-to-right
? : Ternary conditional right-to-left
=
+=  -=
*=  /=
%=  &=
^=  |=
<<=  >>=
Assignment
Addition/subtraction assignment
Multiplication/division assignment
Modulus/bitwise AND assignment
Bitwise exclusive/inclusive OR assignment
Bitwise shift left/right assignment
right-to-left
, Comma (separate expressions) left-to-right
Μετατροπές τύπων δεδομένων

Όταν στην ίδια παράσταση εμπλέκονται διαφορετικοί τύποι δεδομένων τότε αυτόματα οι "μικρότεροι" τύποι μετατρέπονται σε "μεγαλύτερους". Αυτό συνήθως σημαίνει σε αυτόν με τον\ μεγαλύτερο αριθμό bits. Αυτά αφορούν τόσο σταθερές όσο και μεταβλητές.

Για παράδειγμα στη σχέση 10 + 11.34 που εμπλέκεται ένας ακέραιος με έναν float θα μετατραπεί πρώτα ο ακέραιος σε float και μετά θα γίνει η πράξη. Αντίθετα στην σχέση 9/12 το αποτέλεσμα είναι 0 καθώς και οι δύο τελεστές είναι ακέραιοι οπότε προκύπτει ακέραιο αποτέλεσμα. Δηλαδή η ίδια η πράξη δέν καθορίζει το αποτέλεσμα, αλλά μόνο οι εμπλεκόμενοι τύποι!

Εάν εμείς θέλουμε να αναγκάσουμε να γίνει κάποια συγκεκριμένη μετατροπή τότε κάνουμε casting δηλαδή γράφουμε μπροστά από το μέγεθος που πρέπει να μετατραπεί το όνομα του νέου τύπου μέσα σε παρενθέσεις. Έτσι το προηγούμενο παράδειγμα γίνεται: 9/(float)12 οπότε και το αποτέλεσμα που προκύπτει είναι 0.75

Το 1ο μου πρόγραμμα! : H main, μια ξεχωριστή συνάρτηση

Για να γράψουμε λοιπόν ένα πρόγραμμα σε C μπορούμε να έχουμε κώδικα μόνο μέσα σε συναρτήσεις. Από που ξεκινάει η εκτέλεση του προγράμματος τότε;! Η απάντηση ποικίλει ανάλογα με το σύστημα για το οποίο γράφετε. Συνήθως όταν γράφουμε πρόγραμμα για γραμμή εντολών, ορίζεται η συνάρτηση main όπως φαίνεται παρακάτω.

void main() {
    // Εδώ μπαίνει ο κώδικας...
}

ή όχι τόσο απλά αλλά με περισσότερες δυνατότητες

int main(int argc, char *argv[]) {
    // Εδώ μπαίνει ο κώδικας...
}
Μεταγλώττιση : Compile και Link

Πως εκτελείται τώρα ο κώδικας μας; Η απάντηση δεν είναι τόσο απλή, πολύ γενικά όμως ισχύουν τα εξής:

  • Γράφουμε τον κώδικά μας σε ένα αρχείο κειμένου
  • Καλούμε τον compiler που προεπεξεργάζεται τον κώδικα και μετά τον μετατρέπει από κείμενο σχεδόν σε γλώσσα μηχανής (object code)
  • Καλούμε τον linker (συνήθως το κάνει αυτόματα ο compiler) για να συνδέσει το πρόγραμμά μας με τον loader και τις βιβλιοθήκες.

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

Ο προεπεξεργαστής

Είναι ένα τμήμα του compiler που μεταφράζει ψευδοεντολές (όλες όσες αρχίζουν με #). Οι δύο πιο ευρέως χρησιμοποιούμενες είναι:

#include <filename> ή #include "filename"
Αντικαθιστά την εντολή αυτή με ολόκληρο το περιεχόμενο του αρχείο με όνομα filename. Οι δύο παραλλαγές με < > και με τα εισαγωγικά διαφέρουν στον φάκελο στον οποίο αναζητά ο προεπεξεργαστής (preprocessor) το εν λόγω αρχείο.
#define NAME VALUE ή #define NAME(x) EXPRESSION_with_X
Αντικαθιστά το NAME σε όλη την έκταση του προγράμματος με το VALUE ακριβώς όπως το find/replace στους επεξεργαστές κειμένου. Γι'αυτό συχνά χρειάζεται προσοχή ώστε να βάζουμε σε παρένθεση την τιμή (VALUE) και να μην δημιουργούνται στο σημείο αντικατάστασης προβλήματα προτεραιότητας. Πχ #define AB_SUM a+b και μετά ποιό κάτω int x = k*AB_SUM; που παράγει τον κώδικα int x = k*a+b; και αντί να πολλαπλασιαστεί το k με το άθροισμα πολλαπλασιάζεται μόνο με το a.
Η παραλλαγή με την παράμετρο χρησιμοποιείται για inline κώδικα π.χ. για να δηλώσουμε μια απλή έκδοση της MAX(x,y) ως εξής: #define MAX(x,y) ((x>y)?x:y)

ΠΡΟΣΟΧΗ! Οι παρενθέσεις δεν στοιχίζουν τίποτα σε υπολογιστικό χρόνο. Βάλτε όσες θέλετε. Είναι "τσάμπα"!

Εκτέλεση στον υπολογιστή μας

Για την ανάπτυξη και για να υπάρχει ενιαίο περιβάλλον εργασίας, προτείνουμε να χρησιμοποιήσετε το CLion της JetBrains, το οποίο διατίθεται δωρεάν στους φοιτητές. Πάνω σε αυτό θα γίνονται και οι παρουσιάσεις του μαθήματος.
Εναλλακτικά μπορείτε να δείτε τα Code::Blocks, DevCpp, VSCode.

Οδηγίες για την εγκατάσταση και ρύθμιση του CLion μπορείτε να βρείτε στο link στο site του μαθήματος.
Περισσότερες συναρτήσεις

Αντίστοιχα με την mySum μπορούμε να γράψουμε για το γινόμενο την myProd ως εξής

int myProd(int a, int b) {
    return a * b;   // Συμπτυγμένη μορφή
}

Εφόσον έχουμε ήδη ορίσει κάποιες συναρτήσεις μπορούμε να τις καλέσουμε μέσα από άλλες συναρτήσεις π.χ.

int mySumProd(int a1, int b1, int a2, int b2) {
    return mySum( myProd(a1,b1) ,
                  myProd(a2,b2)
                );
}

Παρατηρήστε πόσο πιο ευανάγνωστο και ξεκάθαρο κάνει τον κώδικα η ελέυθερη σύνταξη της C!

Κλήση με τιμή ( by value )

Δείτε την παρακάτω συνάρτηση:

int myIncrease(int x) {
    return ++x;
}

Η συνάρτηση αυτή εκτελείται όποτε την καλέσουμε αλλά στην πραγματικότητα δεν επηρεάζει την τιμή της μεταβλητής που της δόθηκε. Αυτό γίνεται επειδή στην C η κλήση των συναρτήσεων γίνεται πάντα by value, δηλαδή δημιουργώντας ένα αντίγραφο της τιμής που δίνεται ως παράμετρος. Έτσι οι παράμετροί μπορούν να χρησιμοποιηθούν ως τοπικές "πρόχειρες" μεταβλητές.

Η C δεν έχει καμία έτοιμη συνάρτηση

Παράδειγμα για να εκτυπώσουμε στη γραμμή εντολών υπάρχει η printf που ανήκει σε εξωτερική βιβλιοθήκη. Για τη σύνταξή της δείτε: http://www.tutorialspoint.com/c_standard_library/c_function_printf.htm

Για να τη χρησιμοποιήσετε θα πρέπει να γράψετε πριν την 1η αναφορά σε αυτή
#include <stdio.h>
Ουσιαστικά η printf δέχεται τουλάχιστον ένα όρισμα. Αυτό είναι ένα κείμενο που θέλουμε να εμφανιστεί στην οθόνη. Το κείμενο μπαίνει αυτούσιο μέσα σε διπλά εισαγωγικά π.χ. "Hello there!". Μέσα σε αυτό το κείμενο μπορούμε να έχουμε κάποιους ειδικούς χαρακτήρες. Η μία τυπική περίπτωση είναι το \n που συμβολίζει την αλλαγή γραμμής και η άλλη περίπτωση είναι το % που μαζί με τον χαρακτήρα που ακολουθεί παριστάνει το σημείο στο οποίο πρέπει να εισαχθεί μια τιμή. Π.χ. "My name is %s\n". Στο παράδειγμα αυτό στην printf θα πρέπει να δοθεί και ένα 2ο όρισμα που θα είναι μορφής κειμένου (αφού γράψαμε %s = κείμενο). Εναλλακτικά στο %s αναφέρουμε μερικά από όσα περιλαμβάνει το παραπάνω link.

Σύμβολο Δίνεται Χρήση
%d Ακέραιος Στο δεκαδικό σύστημα
%x Ακέραιος Στο δεκαεξαδικό
%o Ακέραιος Στο οκταδικό
%c Ένας χαρακτήρας Ένας χαρακτήρας
%f float Αριθμό κινητής υποδιαστολής
%lf double Αριθμό κινητής υποδιαστολής
%g float Αριθμό σε επιστημονική γραφή
Η C δεν έχει καμία έτοιμη συνάρτηση (#2)

Οι μαθηματικές συναρτήσεις (πχ τριγωνομετρικές) είναι όλες εξωτερικές. Σχετικά με το ποιές υπάρχουν και πως συντάσσονται δείτε εδώ: http://www.tutorialspoint.com/c_standard_library/math_h.htm
και για να τις χρησιμοποιήσετε θα πρέπει να γράψετε πριν την 1η αναφορά σε αυτές την εντολή
#include <math.h>

Ομάδα Συναρτήσεις
Τριγωνομετρικές sin(x) , cos(x), tan(x)
Αντίστροφες τριγωνομετρικές asin(x) , acos(x) , atan(x), atan2(y,x)
Δυνάμεις pow(x,y) , sqrt(x)
Στρογγυλεύσεις round(x) , floor(x), ceil(x)
Άσκηση για το... σπίτι

Τι κάνουν οι ακόλουθες συναρτήσεις;

double whatAmIDoing_1 ( double a, double b ) {
	double m;
    m = sqrt( a*a + b*b );
    return m;
}
double whatAmIDoing_2 ( double a, double b, double c ) {
    double disc = b * b - 4.0 * a * c;
    return ( disc >= 0 ) ? ( (-b + sqrt(disc)) / (2.0 * a) ) :
}
double whatAmIDoing_3 ( double a, double b, double c ) {
    double M = ( a>b ) ? a : b;
    double m = ( a<b ) ? a : b;

    M = ( M>c ) ? M : c;
    m = ( m<c ) ? m : c;

    return a + b + c - M - m;
}