Εισαγωγή στη C (#2)

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

e-mail: allos@mail.ntua.gr

Εκτέλεση εντολών υπό συνθήκη (if)

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

#include <stdio.h>
double myAbs(double x) {
    if (x < 0) {
        return -x;
    } else {
        return x;
    }
}

int main()
{
    double x = -123.45;

    printf("|%lf| = %lf!\n", x, myAbs(x));

    return 0;
}
Run online
Η εντολη if

H if (μία ακόμα λέξη κλειδί της C), στην πιο απλή μορφή της, ελέγχει μία παράσταση (ή συνθήκη) για το κατά πόσο είναι αληθής (= μη μηδενική) ή ψευδής (μηδενική). Εφόσον η συνθήκη είναι αληθής, τότε η εντολή που την ακολουθεί εκτελείται, αλλιώς όχι. Προκειμένου να λειτουργεί για περισσότερες από μία εντολές αυτό το σχήμα πρέπει αυτές να ομαδοποιηθούν με τη βοήθεια των αγκίστρων.

Εάν θέλουμε μία ( ή περισσότερες με τη βοήθεια των άγκιστρων { } ) εντολές να εκτελεστών εναλλακτικά της πρώτης ομάδας, δηλαδή όταν η παράσταση της if είναι ψευδής, τότε μετά την εντολή της απόδοσης της if τοποθετούμε την εντολή else και μετά την εναλλακτική εντολή που θέλουμε.

Παράδειγμα με χαρακτήρες

Για να παραστήσουμε στη C (και γενικότερα στους υπολογιστές) χαρακτήρες χρησιμοποιούμε κάποια κωδικοποίηση. Δηλαδή αντιστοιχούμε σε κάθε γράμμα έναν αριθμό. Η πιο τυπική κωδικοποίηση που αφορά τους λατινικούς χαρακτήρες είναι η ASCII που έχει τυποποίηση για 7bit. Επειδή δεν υπάρχει λόγος να θυμάται ο προγραμματιστής τους κωδικούς αυτούς, η C δίνει έτοιμη την αντιστοίχηση ενός χαρακτήρα με την αριθμητική τιμή του γράφοντας τον χαρακτήρα αυτό μέσα σε μονά εισαγωγικά '. Γενικά για όλες τις κωδικοποιήσεις γραμμάτων στον Η/Υ ισχύει ότι η διάταξη των γραμμάτων και των αριθμητικών χαρακτήρων (0-9) διατηρείται με τους κωδικούς που τους αντιστοιχούν. Με βάση αυτά μπορούμε να γράψουμε:

#include <stdio.h>

char myCapitalize(char x) {
    if ('a' <= x && x <= 'z') {
        return x - 'a' + 'A';
    } else {
        return x;
    }
}

int main()
{
    char x = 'q';

    printf("%c -> %c\n", x, myCapitalize(x));

    return 0;
}
Run online
"Αλυσίδα" else if

Παρατηρούμε ότι δεν έχουμε θέσει κάποιο περιορισμό ως προς την εντολή που αντιστοιχεί να εκτελεστεί όταν η if είναι αληθής ή ψευδής (= μετά το else). Έτσι έαν σαν εναλλακτική εντολή χρησιμοποιήσουμε ένα άλλο if, τότε δημιουργείται ένα σχήμα σαν αυτό του ακόλουθου κώδικα, όπου το 2o if που συναντάμε ελέγχεται μόνο εφόσον είναι ψευδές το 1o, το 3o if μόνο όταν είναι ψευδή τα δύο πρώτα, κ.ο.κ., ενώ ο κώδικας που αντιστοιχεί στο τελικό else (εφόσον αυτό υπάρχει) εκτελείται μόνο όταν δεν υπάρχει καμία συνθήκη από αυτή την αλυσίδα που να αληθεύει. Εδώ απαιτείται προσοχή από τον προγραμματιστή ώστε όταν εξετάζονται συνθήκες που αλληλοεπικαλύπτονται ή η μία εμπεριέχει την άλλη, να τοποθετούνται με τη σωστή σειρά, δηλαδή από την ειδική προς τη γενική!

#include <stdio.h>

#define FALSE 0
#define TRUE (!0)

int isLeapYear(int y) {
    if (y%400 == 0) {   // Η συνθήκη συναληθεύει με στις δύο επόμενες !!
        return TRUE;
    } else if (y%100 == 0) { // Η συνθήκη συναληθεύει με την επόμενη !!
        return FALSE;
    } else if (y%4 == 0) {
        return TRUE;
    } else {
        return FALSE;
    }
}

int main()
{
    int year = 2000;

    if (isLeapYear( year )) {
        printf("%d has 366 days!\n", year);
    } else {
        printf("%d has 365 days.\n", year);
    }

    return 0;
}
Run online
Ένα απλό "κομπιουτεράκι"

Σε αυτό το παράδειγμα βλέπουμε ένα παράδειγμα εμφωλευμένου if, δηλαδή ενός if που βρίσκεται μέσα σε ένα άλλο if. Όσο υπάρχουν άγκιστρα, ο κώδικας είναι ξεκάθαρος για το πως "ζευγαρώνουν" τα if/else μεταξύ τους. Όμως χωρίς αυτά πάντα και απλούστατα, το κάθε else αντιστοιχεί στο αμέσως προηγούμενο if ανεξάρτητα από την οπτική στοίχιση. Σε αυτό το παράδειγμα επίσης βλέπουμε τη σταθερά DBL_MAX που αποτελεί τη μέγιστη τιμή ενός double για αυτό τον υπολογιστή. Εδώ (κακώς) τη χρησιμοποιούμε ως ένδειξη σφάλματος προς χάρην της απλότητας του κώδικα.

#include <stdio.h>
#include <float.h>

double calc(char op, double a, double b) {
    if (op == '+') {
        return a+b;
    } else if (op == '-') {
        return a-b;
    } else if (op == '*') {
        return a*b;
    } else if (op == '/') {
        if (b == 0) {
            return DBL_MAX;
        } else {
            return a/b;
        }
    }
    return DBL_MAX;
}

int main()
{
    char theOperator = '+';
    double operandA = 123.45,
           operandB = -23.45;

    printf("The result is : %lf\n\n", calc( theOperator, operandA, operandB ));

    return 0;
}
Run online
switch / case

Όταν οι συνθήκες αφορούν απλή σύγκριση μιας παράστασης με διάφορες σταθερές τιμές μπορούμε να χρησιμοποιήσουμε αντί για τα if / else την εντολή switch / case όπως φαίνεται παρακάτω. Το πλεονέκτημα της είναι ότι διαβάζοντας τον κώδικα γίνεται αμέσως ξεκάθαρο ότι η σύγκριση αφορά μία παράσταση με διάφορες ποσότητες χωρίς να χρειάζεται να εξετάσουμε τις συνθήκες μία προς μία όπως θα κάναμε σε ένα if. Το μειονέκτημά του είναι ότι εξετάζει μόνο ισότητες με σταθερές ποσότητες. Επίσης σε περίπτωση που θελήσουμε να κάνουμε κάτι διαφορετικό (πχ να εξετάσουμε μια ανισότητα ή να κάνουμε μια σύγκριση με άλλη μεταβλητή) η μετατροπή του κώδικα θα απαιτήσει λίγο χρόνο και αρκετή προσοχή.

#include <stdio.h>
#include <float.h>

double calc(char op, double a, double b) {
    switch(op) {
        case '+':
            return a+b;
        case '-':
            return a-b;
        case '*':
            return a*b;
        case '/':
            if (b == 0) {
                return DBL_MAX;
            } else {
                return a/b;
            }
        default:
            return DBL_MAX;
    }
}

int main()
{
    char theOperator = '/';
    double operandA = 123.45,
           operandB = .0;

    printf("The result is : %lf\n\n", calc( theOperator, operandA, operandB ));

    return 0;
}
Run online
Αυτό που δεν φαίνεται ...

στο προηγούμενο παράδειγμα είναι ότι οι περιπτώσεις του switch/case δεν τερματίζονται από μόνες τους, έτσι όταν ολοκληρωθεί η επιλεγμένη περίπτωση, η εκτέλεση του κώδικα συνεχίζει με την αμέσως επόμενη εντολή που ανήκει σε άλλη περίπτωση και όχι μετά το τέλος του άγκιστρου που αντιστοιχεί στο switch. Έτσι το παρακάτω παράδειγμα είναι ΛΑΘΟΣ και υπολογίζει πάντα τη διαίρεση μεταξύ των a και b!!

// THIS CODE IS BUGGY
#include <stdio.h>
#include <float.h>

double calc(char op, double a, double b) {
    double result;

    switch(op) {
        case '+':
            result = a+b;
        case '-':
            result = a-b;
        case '*':
            result = a*b;
        case '/':
            if (b == 0) {
                result = DBL_MAX;
            } else {
                result = a/b;
            }
        default:
            result = DBL_MAX;
    }

    return result;
}

int main()
{
    char theOperator = '+';
    double operandA = 123.45,
           operandB = 11.0;

    printf("The result is : %lf\n\n", calc( theOperator, operandA, operandB ));

    return 0;
}
Run online
Η εντολή break

Η ορθή έκδοση του προηγούμενου προγράμματος είναι η ακόλουθη που χρησιμοποιεί την εντολή break η οποία τερματίζει την εκτέλεση του κώδικα μέσα στα άγκιστρα (block του switch) και συνεχίζει την εκτέλεση με τις επόμενες από αυτό το μπλοκ εντολές.
Επίσης σε αυτό το παράδειγμα φαίνεται ότι η σειρά των case δεν παίζει ρόλο και το ίδιο ισχύει και για το default το οποίο ασχέτως της σειράς με την οποία είναι γραμμένο στον κώδικα, επιλέγεται για εκτέλεση μόνο εφόσον αποτύχουν όλοι οι έλεγχοι των case.

#include <stdio.h>
#include <float.h>

double calc(char op, double a, double b) {
    double result;

    switch(op) {
        default:
            result = DBL_MAX;
            break;
        case '+':
            result = a+b;
            break;
        case '*':
            result = a*b;
            break;
        case '-':
            result = a-b;
            break;
        case '/':
            if (b == 0) {
                result = DBL_MAX;
            } else {
                result = a/b;
            }
            break;
    }

    return result;
}

int main()
{
    char theOperator = '+';
    double operandA = 123.45,
           operandB = 11.0;

    printf("The result is : %lf\n\n", calc( theOperator, operandA, operandB ));

    return 0;
}
Run online
"Πρελούδιο" πινάκων

Για να μπορέσουμε να έχουμε μεγαλύτερη άνεση στην επίδειξη των πινάκων και του τρόπου λειτουργίας τους στη C, θα δούμε την βασικότερη και απλούστερη εντολή for που εκτελεί κατ' επανάληψη μια ομάδα εντολών (πάντα μέσα σε άγκιστρα), μία φορά για κάθε ακέραια τιμή μιας μεταβλητής, μετρώντας από μια αρχική, έως μία τελική ανά ένα.

#include <stdio.h>

int main()
{
    int i;

    for (i=0; i<10; i++) {
        printf("%d ", i);
    }
    printf("\n");

    return 0;
}
Run online
Οι πίνακες στη C

Οι πίνακες στη C δηλώνονται με τη βοήθεια των αγκυλών [ ] που ακολουθούν το όνομα του πίνακα. Όλα τα στοιχεία του πίνακα έχουν κοινό τύπο δεδομένων και αυτό αναγράφεται μπροστά από το όνομα του πίνακα κατά τη δήλωσή του. Επίσης κάθε στοιχείο του πίνακα συμπεριφέρεται(=είναι) ακριβώς όπως μία απλή μεταβλητή αυτού του τύπου.
Τα στοιχεία του πίνακα είναι αριθμημένα με έναν (ακέραιο) δείκτη που παίρνει τιμές από το μηδέν μέχρι το μέγεθος του πίνακα μείον ένα. Στη δήλωση του πίνακα μέσα στις αγκύλες αναφέρεται το μέγεθός του, ενώ κατά τη χρήση (αναφορά) σε ένα στοιχείο του, μέσα στις αγκύλες αναγράφεται η τιμή του δείκτη.
Για να δώσουμε αρχικές τιμές με μαζικό τρόπο κατά τη δήλωση ενός πίνακα μπορούμε να χρησιμοποιήσουμε τα άγκιστρα και να γράψουμε μέσα τις τιμές χωρισμένες με κόμμα. Εάν δεν ορίσουμε όλες τις αναμενόμενες τιμές τότε οι υπόλοιπες συμπληρώνονται με μηδενικά (ή επαναλαμβάνοντας το τελευταίο δηλωμένο στοιχείο - αυτό εξαρτάται από τον compiler). Όμως εάν δεν δοθούν καθόλου αρχικές τιμές, τότε οι τιμές των στοιχείων είναι ακαθόριστες. Δοκιμάστε στο παρακάτω παράδειγμα να μην δώσετε αρχικές τιμές σε κανένα στοιχείο του πίνακα.
Αντίθετα όταν δώσετε αρχικές τιμές για όλα τα στοιχεία του πίνακα, τότε δεν χρειάζεται μέσα στις αγκύλες να ορίσετε το μέγεθός του. Κάνε και με αυτό μία δοκιμή!

#include <stdio.h>

#define ARR_SZ 10

int main()
{
    int x[ARR_SZ] = { 0, 10, 11, 32, 21, 888, 9, -12 };
    int i;

    for (i=0; i<ARR_SZ; i++) {
        printf("%d ", x[i]);
    }
    printf("\n");

    return 0;
}
Run online
Αρχικοποίηση σε πραγματικές συνθήκες

Συχνά οι χρησιμοποιούμενοι πίνακες είναι μεγάλοι σε μέγεθος, οπότε η αρχικοποίηση γίνεται με έναν κώδικα όπως ο ακόλουθος.
Με την ευκαιρία εδώ να σημειώσουμε της χρήση της σταθεράς ARR_SZ (ή με όποιο άλλο όνομα) που ορίζει το προκαθορισμένο μέγεθος του πίνακα. Η χρήση της αποτελεί σημαντική πρακτική στον προγραμματισμό γιατί μας βοηθάει να αποφύγουμε σφάλματα όπως είναι να ξεχάσουμε κάπου να αντικαταστήσουμε το μέγεθος του πίνακα (σε περίπτωση που αυτό αλλάξει). Σημαντικό είναι επίσης να αποφεύγουμε τη χρήση "μαγικών σταθερών" μέσα στον κώδικα όπως δηλαδή να εμφανίζεται ξαφνικά ένας αριθμός (εδώ 100) από το πουθενά, κρύβοντας έτσι την "προέλευση" αλλά και τον ρόλο του!

#include <stdio.h>

#define ARR_SZ 100

int main()
{
    int x[ARR_SZ];
    int i;

    for (i=0; i<ARR_SZ; i++) {
        x[i] = 100;
    }
    printf("Ok\n");

    return 0;
}
Run online
Επαναληπτικές διαδικασίες

Η εντολή for που είδαμε νωρίτερα αποτελεί την πιο συνηθισμένη μορφή επαναληπτικές διαδικασίας στη C. Συντάσσεται με παρενθέσεις μετά το όνομα της. Το περιεχόμενο των παρενθέσεων χωρίζεται στα τρία με τη χρήση του Ελληνικού ερωτηματικού (= Αγγλική άνω τελεία). Τις παρενθέσεις όπως και στο if τις ακολουθεί μία εντολή (ή περισσότερες μέσα σε άγκιστρα). Ας αριθμήσουμε τις περιοχές αυτές όπως φαίνεται στο παρακάτω σχήμα.

for (  1   ;   2   ;   3   )
    4;
  • Η 1η περιοχή αντιστοιχεί στην αρχικοποίηση τιμών που εμπλέκονται στην επαναληπτική διαδικασία και εκτελείται πάντα και μόνο μία φορά στην αρχή.
  • Η 2η περιοχή αντιστοιχεί σε μία παράσταση ακριβώς όπως της if. Εφόσον αυτή επαληθεύεται τότε η διαδικασία συνεχίζεται αλλιώς διακόπτεται και η εκτέλεση του προγράμματος συνεχίζει από την αμέσως επόμενη μετά το for εντολή.
  • Η 3η περιοχή αντιστοιχεί στη μεταβολή των μεταβλητών που εμπλέκονται στην επαναληπτική διαδικασία και εκτελείται αμέσως μετά από την 4η περιοχή.
  • Η 4η περιοχή αποτελεί το σώμα του βρόχου και εκτελείται πάντα μετά από τη συνθήκη και εφόσον αυτή αληθεύει. Συνήθως αποτελείται από περισσότερες από μία εντολές, μπορεί όμως να μην αποτελείται και από καμία (αυτό δεν είναι φαινομενικά εφικτό ή χρήσιμο) όμως στη C όταν βάλουμε ένα ; μόνο του, αυτό αποτελεί μία "άδεια" εντολή.

Η τυπική σειρά εκτέλεσης των εντολών λοιπόν είναι:
1, 2, 4, 3, 2, 4, 3, 2, 4, 3, 2, ... 4, 3, 2
δηλαδή η σειρά πάντα τερματίζει στη συνθήκη. Εάν η συνθήκη δεν αληθεύει εξαρχής, τότε οι περιοχές 3 και 4 δεν εκτελούνται ούτε μία φορά.

Ώπα!! Παράδειγμα

Στο παρακάτω παράδειγμα εμφανίζονται δύο πίνακες, ένας με αρχικές τιμές και ένας χωρίς, και με την κλήση της μίας συνάρτησης με ορίσματα τους δύο πίνακες και το κοινό τους μέγεθος, αντιγράφεται ο ένας πίνακας στον άλλο. Σε αυτή τη συνάρτηση βλέπουμε ότι η for δεν έχει συμπληρωμένες τις περιοχές 1 & 3 καθώς δεν χρειάζονται. Αυτή αποτελεί ένα κομμάτι "κρυπτικού" κώδικα ο οποίος παράγει αρκετά ταχύτερο εκτελέσιμο αρχείο αλλά είναι δυσνόητος δεν είναι και αυτοεξηγούμενος, χαρακτηριστικό ιδιαίτερα σημαντικό όταν επιστρέφουμε σε έναν κώδικα που έχουμε γράψει ακόμα και εμείς οι ίδιοι αρκετό καιρό πριν.

ΩΠΑ!! καλώ μια συνάρτηση που αντιγράφει τον ένα πίνακα στον άλλο ενώ δίνονται ως ορίσματα; Μα αφού δεν μπορώ να αλλάξω τις τιμές των ορισμάτων!

Αυτό φαίνεται παράδοξο αλλά όπως θα δούμε σε παρακάτω μαθήματα δεν είναι. Όντως το ποιοι είναι οι δύο πίνακες ΔΕΝ μεταβάλλεται κατά την κλήση της συνάρτησης. Αυτό όμως δεν σημαίνει ότι δεν γίνεται να μεταβληθούν οι τιμές των στοιχείων τους! (Περισσότερα προσεχώς! Μέχρι τότε... πίστη!)

#include <stdio.h>

#define ARR_SZ 5

void printIntArray(int a[], int size) {
    int i;
    for (i=0; i<size-1; i++)
        printf("%d , ", a[i]);
    printf("%d", a[size-1]);
    printf("\n");
}

void copyIntArray(int a[], int b[], int size) {
    for ( ; size > 0; )
        a[size] = b[--size];
}

int main()
{
    int x[ARR_SZ] = { 8, 88, 123, 5, 1 };
    int y[ARR_SZ];

    printf("x[]\n");
    printIntArray(x, ARR_SZ);
    printf("y[]\n");
    printIntArray(y, ARR_SZ);

    copyIntArray(y,x, ARR_SZ);

    printf("y[]\n");
    printIntArray(y, ARR_SZ);

    printf("Ok\n");

    return 0;
}
Run online
Πολυδιάστατοι πίνακες

Όπως ξέρουμε και από την if οι εντολές μπορούν να είναι εμφωλευμένες η μία μέσα στην άλλη και φυσικά το ίδιο ισχύει και για την περίπτωση της for. Στο παράδειγμα μας αυτό βλέπουμε πως μπορούμε να αρχικοποιήσουμε έναν πίνακα δύο διαστάσεων (εδώ τετράγωνος). Ουσιαστικά βλέπουμε πως η δήλωση ενός πίνακα δύο (ή και περισσοτέρων) διαστάσεων γίνεται (κατ'επέκταση της μίας διάστασης) προσθέτοντας ζεύγη αγκυλών μετά από τις αγκύλες της προηγούμενης διάστασης. Δηλαδή ουσιαστικά αυτό μας λέει ότι η C του πολυδιάστατους πίνακες τους συμπεριφέρεται ως πίνακες πινάκων!

#include <stdio.h>

#define ARR_SZ 5

int main()
{
    int x[ARR_SZ][ARR_SZ];
    int i;
    int j;

    for (i=0; i<ARR_SZ; i++) {
        for (j=0; j<ARR_SZ; j++) {
            x[i][j] = i*1000 + j;
        }
    }

    for (i=0; i<ARR_SZ; i++) {
        for (j=0; j<ARR_SZ; j++) {
            printf("%5d ", x[i][j]);
        }
        printf("\n");
    }

    printf("Ok\n");

    return 0;
}
Run online
Αποθήκευση στοιχείων πινάκων στη μνήμη

Τα στοιχεία ενός μονοδιάστατου πίνακα στη C είναι διαδοχικά στη μνήμη του υπολογιστή. Δηλαδή δεν υπάρχουν "άσχετα" bytes ανάμεσα στα bytes που περιέχουν τα δεδομένα δύο διαφορετικών στοιχείων. Σχεδόν το ίδιο ισχύει και για τους πολυδιάστατους πίνακες. Όταν οι πίνακες ορίζονται εξαρχής και με συγκεκριμένες διαστάσεις τα στοιχεία είναι συνεχόμενα στη μνήμη, και μάλιστα τα στοιχεία της διάστασης που αντιστοιχεί στον δεξιότερο δείκτη αντιστοιχούν σε γειτονικά στοιχεία στη μνήμη. Έτσι π.χ. το Q[i][j][3] και το Q[i][j][4] είναι τα συνεχόμενα. Στο παρακάτω παράδειγμα επιδεικνύεται ακριβώς αυτό το γεγονός, "αναγκάζοντας" το πρόγραμμά μας να συμπεριφερθεί στον πίνακα x ως μονοδιάστατο με ARR_SZ*ARR_SZ στοιχεία.

#include <stdio.h>

#define ARR_SZ 5

int main()
{
    int x[ARR_SZ][ARR_SZ];
    int i;
    int j;

    for (i=0; i<ARR_SZ; i++) {
        for (j=0; j<ARR_SZ; j++) {
            x[i][j] = i*1000 + j;
        }
    }

    for (i=0; i<ARR_SZ*ARR_SZ; i++) {
        printf("%d\n", x[0][i]);
    }
    printf("Ok\n");

    return 0;
}
Run online
break και continue

Επιστρέφοντας στις επαναληπτικές διαδικασίες πρέπει να αναφέρουμε τις δύο πιο χρήσιμες εντολές break και continue οι οποίες είναι από αυτές που μας επιτρέπουν να κάνουμε ελάχιστη έως και μηδενική χρήση της goto (για την οποία θα αποφύγουμε συστηματικά να μιλήσουμε ακριβώς όπως πρέπει να την αποφεύγουμε και στον κώδικα που γράφουμε).
Η break όταν εμφανίζεται μέσα σε ένα βρόχο, τερματίζει άμεσα την εκτέλεσή του και οδηγεί την εκτέλεση του προγράμματος στην εντολή που ακολουθεί το βρόχο.
Αντίθετα η continue τερματίζει μόνο την τρέχουσα επανάληψη και οδηγεί στην εκτέλεση της περιοχής 3, δηλαδή της προετοιμασίας για την επόμενη επανάληψη.
Το επόμενο παράδειγμα είναι διαφωτιστικό για τη χρήση και τις διαφορές τους.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define ARR_SZ 10

double randAB(double a, double b) {
    return a + rand()*(b-a)/RAND_MAX;
}

void randArray(double a[], int size, double m, double M) {
   for (; size > 0; ) {
       a[--size] = randAB(m,M);
   }
}

void printArray(double a[], int size) {
   int i;
   for (i=0; i<size; i++) printf("%2.3lf ",a[i]);
   printf("\n");
}

double sumAllPositives(double a[], int size) {
    double s = 0.0;
    int i;

    for (i=0; i<size; i++) {
        if (a[i] < 0)
            continue;
        s += a[i];
    }
    return s;
}

double sumFirstPositives(double a[], int size) {
    double s = 0.0;
    int i;

    for (i=0; i<size; i++) {
        if (a[i] < 0)
            break;
        s += a[i];
    }
    return s;
}

int main() {

    double a[ARR_SZ];

    srand((unsigned) time(0));  // Trick to setup the random seed by the computer time

    randArray(a,ARR_SZ,-5,15);
    printArray(a,ARR_SZ);

    printf("\n\nSum All + : %2.3lf", sumAllPositives(a,ARR_SZ));
    printf("\n\nSum First + : %2.3lf", sumFirstPositives(a,ARR_SZ));

    return 0;
}
Run online
Τυχαίοι αριθμοί

Ένας τυπικός υπολογιστής δεν μπορεί να παράγει πραγματικά τυχαίους αριθμούς και αντ'αυτών παράγει ψευδοτυχαίους, δηλαδή αριθμούς που υπολογίζονται αλγοριθμικά αλλά είναι δύσκολο να προβλεφθούν και έχουν και όλα τα στατιστικά χαρακτηριστικά των τυχαίων αριθμών. Ένα βασικό μέγεθος που καθοδηγεί την παραγωγή αυτών των ψευδοτυχαίων αριθμών είναι ο σπόρος (seed) αλλάζοντας τον οποίο, ξεκινά διαφορετική σειρά παραγωγής τυχαίων αριθμών και αντίστροφα, θέτοντας το σε συγκεκριμένη τιμή παράγεται πάντα ακριβώς η ίδια ακολουθία αριθμών. Γι'αυτό όπως είδαμε και στο προηγούμενο παράδειγμα για καθορισμό του seed συχνά χρησιμοποιείται η ώρα του υπολογιστή ώστε καθώς κάθε φορά το πρόγραμμα εκτελείται σε τυχαίες χρονικές στιγμές, το seed είναι "απρόβλεπτο" και το ίδιο και η ακολουθία των ψευδοτυχαίων αριθμών που παράγεται.

Βρόχος while

Η απλούστερη μορφή ενός βρόχου for είναι να υπάρχει μόνο η συνθήκη του, όπως το συναντήσαμε και σε ένα προηγούμενο παράδειγμα. Σε αυτή τη μορφή και με τη συντατκτική απλότητα της if, χρησιμοποιούμε το βρόχο while ο οποίος συντάσσεται ακριβώς όπως η if και εφόσον ισχύει η συνθήκη του εκτελούνται οι εντολές του σώματος του βρόχου. Κατόπιν επανυπολογίζεται η συνθήκη και για όσο παραμένει αληθής συνεχίζεται αυτή η κυκλική εκτέλεση των εντολών. Η χρήση του είναι πολύ συχνή στα προγράμματα της C. Πάρτε για παράδειγμα το ακόλουθο πρόγραμμα που υπολογίζει τους πρώτους όρους μιας φθίνουσας ακολουθίας που είναι μεγαλύτεροι από μία οριακή τιμή.

#include <stdio.h>

int main()
{
    double a = 1.0;

    printf("%2.5lf\n", a);

    while ( (a = a/3.0) > 1/730.0 )
        printf("%2.5lf\n", a);

    return 0;
}
Run online
Βρόχος do while

Κάποιες φορές, όπως στο προηγούμενο παράδειγμα, πριν την έναρξη ενός βρόχου πρέπει να εκτελεστεί μια διαδικασία (εδώ απλά το printf) η οποία κατόπιν επαναλαμβάνεται και ως σώμα του βρόχου. Σε αυτές τις περιπτώσεις - που θα θέλαμε πρώτα να εκτελείται η διαδικασία - και στο τέλος να γίνεται ο έλεγχος της αλήθειας της έκφρασης, μπορούμε να χρησιμοποιήσουμε το σχήμα do / while όπου μετά από την αρχική λέξη κλειδί do ακολουθεί η εντολή (ή εντολές μέσα σε άγκιστρα) και στο τέλος γράφεται το while, το οποίο όμως υποχρεωτικά θα πρέπει να ακολουθείται από μία κενή εντολή ( ; ).

#include <stdio.h>

int main()
{
    double a = 1.0;

    do
        printf("%2.5lf\n", a);

    while ( (a = a/3.0) > 1/730.0 ) ;

    return 0;
}
Run online
Αναδρομή

Ένας έμμεσος τρόπος επαναληπτικών διαδικασιών είναι η αναδρομική κλήση μιας συνάρτησης, δηλαδή η κλήση μιας συνάρτησης μέσα από την ίδια τη συνάρτηση. Για παράδειγμα στον ακόλουθο κώδικα έχει δημιουργηθεί μια συνάρτηση που υπολογίζει τον n-οστό όρο της ακολουθίας Fibonacci. Κάθε όρος της ακολουθίας Fibonacci ισούται με το άθροισμα των δύο προηγούμενων, ενώ οι δύο πρώτοι ισούνται με 1. Στην επόμενη διαφάνεια βλέπουμε τον ίδιο κώδικα με τη χρήση της while.

#include <stdio.h>

int fibonacci(int n) {
    if (n < 3)
        return 1;
    return fibonacci(n-1) + fibonacci(n-2);
}

int main()
{
    printf("%d\n", fibonacci(10));

    printf("Done!\n");

    return 0;
}
Run online
Αναδρομή
#include <stdio.h>

int fibonacci(int n) {
    int f1 = 1;
    int f2 = 1;
    int f3;

    if (n < 3) {
        return 1;
    }

    n -= 2;

    while (n > 0) {
        f3 = f1 + f2;
        f1 = f2;
        f2 = f3;
        n--;
    }

    return f3;
}

int main()
{
    int i1 = 1;
    int i2 = 1;

    printf("%d\n", fibonacci(10));

    printf("Done!\n");

    return 0;
}
Run online
Ατέρμονες Βρόχοι

Πολλές φορές η μια διαδικασία πρέπει να επαναλαμβάνεται για ακαθόριστο αριθμό επαναλήψεων, ενώ τα κριτήρια τερματισμού μπορεί να είναι σύνθετα ή γνωστά σε σημείο του κώδικα στο μέσο του σώματος της συνάρτησης. Χαρακτηριστικό παράδειγμα είναι όταν υπάρχει αλληλεπίδραση με τον χρήστη, όπου μέχρι να ορίσει ο χρήστης ότι πρέπει να τερματιστεί το πρόγραμμα, αυτό πρέπει να εκτελείται συνεχώς.
Επειδή ακόμα δεν έχουμε δει πως γίνεται η είσοδος από το πληκτρολόγιο, θα χρησιμοποιήσουμε μία δική μας συνάρτηση που επιστρέφει έναν (κάθε φορά) από μια προκαθορισμένη ακολουθία χαρακτήρων, που υποτίθεται ότι εισάγει ένας χρήστης. Για να το πετύχουμε αυτό χρειαζόμαστε μία μεταβλητή που να "θυμάται" ποιόν χαρακτήρα είχε επιστρέψει μέχρι στιγμής.
Αυτό το πετυχαίνουμε με τη χρήση μιας static μεταβλητής η οποία διατηρεί την τιμή της ανάμεσα στις κλήσεις της συνάρτησης.

#include <stdio.h>

int myGetChar() {
    static int k = 0;
    char keys[] = { 'm', '1', 2, 'q', '1', '1', '2', '3' };
    return keys[k++];
}

int main () {
    int c;
    int n;
    int m = 0;

    while (1) {

        printf("Please select command letter (press 'm' for help): \n");
        c = myGetChar();
        printf("Executing %c\n", c);

        if (c == 'm') {
            printf("Menu\n\n");
            printf("1. Show random number from 0 to 9\n");
            printf("2. Show largest number so far\n");
            printf("3. Exit\n");
        } else if (c == '1') {
            n = rand() % 10;
            printf("%d\n", n);
            if (m < n) {
                m = n;
            }
        } else if (c == '2') {
            printf("So far max value is : %d\n", m);
        } else if (c == '3') {
            printf("Exiting!\n");
            break;
        } else {
            printf("Please press 'm' for available options!\n\n");
        }

    }

    printf("Program complete!\n");

    return 0;
}
Run online
Συμβολοσειρές/Strings

Η παράσταση κειμένων στη C γίνεται με τις συμβολοσειρές ή strings στα Αγγλικά. Ουσιαστικά πρόκειται για μονοδιάστατους πίνακες χαρακτήρων. Οι πίνακες αυτοί έχουν ένα χαρακτήρα σε κάθε θέση ενώ μετά το τέλος της συμβολοσειράς υπάρχει ένα ακόμα στοιχείο με αριθμητική τιμή (κωδικό) 0 που ορίζει το τέλος του κειμένου.
Προκειμένου να αποφεύγονται τα άγκιστρα, όπως στο προηγούμενο παράδειγμα, για την δημιουργία ενός κειμένου, αλλά και να είναι ευανάγνωστο το ίδιο το κείμενο μέσα στον κώδικα, μπορούμε να ορίσουμε αυτούς τους πίνακες ως κείμενο μέσα σε διπλά εισαγωγικά ". Με βάσει αυτά μπορούμε να γράψουμε τον ακόλουθο κώδικα που χρησιμοποιεί μια δική μας συνάρτηση για να μετρήσει το μήκος ενός κειμένου σε χαρακτήρες.

#include <stdio.h>

int lengthOfString (char s[]) {
    int L = 0;
    while (s[L++])
        ;
    return L-1;
}

int main() {

    char text1[] = "This is a sample text!";
    char text2[] = "";
    char text3[] = "123456789\n123456789!";

    printf( "%d\n\n", lengthOfString(text1) );
    printf( "%d\n\n", lengthOfString(text2) );
    printf( "%d\n\n", lengthOfString(text3) );

    return 0;

}
Run online
Ασκήσεις

Με βάση όσα παρουσιάζονται σε αυτές τις διαφάνειες μπορείτε να γράψετε τους παρακάτω κώδικες:

  • Μία συνάρτηση που να μετράει τις λέξεις σε ένα κείμενο, όπου ως λέξη ορίζουμε μια οποιαδήποτε ακολουθία χαρακτήρων εκτός από το κενό ' ', την αλλαγή γραμμής '\n' και το tab '\t'.
  • Μία συνάρτηση που να δέχεται δύο συμβολοσειρές και να επιστρέφει τη θέση που βρέθηκε η 2η μέσα στην 1η ή -1 αν δεν υπάρχει.
  • Μία συνάρτηση που να δέχεται μία συμβολοσειρά και να μετατρέπει τους πεζούς της χαρακτήρες σε κεφαλαία.
  • Ένα πρόγραμμα (με τις κατάλληλες συναρτήσεις) που να υπολογίζει τις τιμές μιας συνάρτησης σε κάποια σημεία, τις τιμές της παραγώγου στα ίδια σημεία (με αναλυτικό τρόπο) και τις τιμές της παραγώγου με αριθμητικό τρόπο στα ίδια σημεία. Στο τέλος να εκτυπώνει τις τριάδες αυτές και την απόκλιση των δύο διαφορετικών υπολογισμών των παραγώγων.