Η C είναι απλή/Δυναμική Εκχώρηση Μνήμης

ΕισαγωγήΕπεξεργασία

Ας πούμε ότι θέλαμε να λάβουμε έναν ακαθόριστο αριθμό από ακεραίους μέσω της κονσόλας και να τους εκτυπώσουμε όλους σε αύξουσα/φθίνουσα σειρά. Για να το κάνουμε αυτό είναι απαραίτητος ένας πίνακας όπου θα αποθηκεύονταν οι ακέραιοι. Αλλά μέχρι τώρα έχω μιλήσει μόνο για στατικούς πίνακες, των οποίων το μέγεθος είναι σταθερό όσο τρέχει το πρόγραμμα. Κάποιος μπορεί να σκεφτόταν απλά να του δίναμε πάρα πολλά στοιχεία, αλλά αυτή η τακτική έχει πολλά προβλήματα. Απ' τη μία, αν ο χρήστης δώσει μόνο 2-3 στοιχεία, τα υπόλοιπα 500MiB θα πήγαιναν χαμένα( δε ξέρω αν ένας στατικός πίνακας στη C μπορεί να είναι τόσο μεγάλος ). Απ' την άλλη, αν κάποιος δώσει έστω και ένα παραπάνω στοιχείο απ' όσα χωράει ο πίνακας, τη πατήσαμε...

Αυτό που κάνει η δυναμική εκχώρηση μνήμης, είναι να σου δίνει ακριβώς τόση μνήμη RAM όση χρειάζεσαι για να κάνεις κάτι. Στην C συγκεκριμένα, υπάρχουν 4 καθιερωμένες συναρτήσεις που δίνουν στον προγραμματιστή τη δυνατότητα να χρησιμοποιήσει τον "σωρό"/"heap", όπως συνηθίζεται να λέγεται η διαθέσιμη δυναμική μνήμη. Σε αυτές θα αναφερθώ εδώ. Να σημειωθεί ότι πρέπει ήδη να έχετε διαβάσει τα άρθρα για τις συναρτήσεις, τους πίνακες και τους δείκτες αν θέλετε να καταλάβετε τι γίνεται παρακάτω.

Οι συναρτήσειςΕπεξεργασία

Όλες οι παρακάτω συναρτήσεις χρειάζονται το αρχείο κεφαλίδα "stdlib.h"

malloc()Επεξεργασία

Ας αρχίσουμε με τη πιο βασική, τη "malloc()", η οποία επιστρέφει έναν δείκτη στη μνήμη που της ζήτησες να δώσει:

void* malloc(size_t size);
    size: το μέγεθος της μνήμης που θες, σε bytes
    επιστρέφει: δείκτη στο πρώτο στοιχείο της μνήμης, σαν πίνακας. NULL αν δεν τα καταφέρει

Χρησιμοποιώντας την, αν ήθελα έναν πίνακα από 42 double, θα έκανα:

double *array = malloc(sizeof(double)*42);

free()Επεξεργασία

Αμέσως μετά έρχεται η "free()", που ελευθερώνει τη μνήμη που είχαν δεσμεύσει νωρίτερα οι άλλες συναρτήσεις για να χρησιμοποιηθεί αργότερα ή και από άλλα προγράμματα, όταν δε χρειάζεται πλέον:

void free(void* ptr);
    ptr: ο δείκτης που είχε επιστραφεί από τις malloc, realloc ή calloc

Καλό είναι όταν δε χρειαζόμαστε πια τη μνήμη, να την ελευθερώνουμε:

free(array);

calloc()Επεξεργασία

Η βασική διαφορά της "calloc()" από τη "malloc()" είναι ότι αυτή δίνει αρχικές τιμές σε όλα τα στοιχεία, σε αντίθεση με την άλλη που τα στοιχεία έχουν "τυχαίες" τιμές μέσα τους. Συντάσσεται λίγο διαφορετικά, και δίνει τη τιμή μηδέν σε όλα τα bytes του πίνακα:

void* calloc(size_t num, size_t size);
    num: το πλήθος των στοιχείων του πίνακα
    size: το μέγεθος σε bytes του κάθε στοιχείου του πίνακα
    επιστρέφει: ότι και η malloc

Για να δείτε τις διαφορές, ορίστε ένα απλό πρόγραμμα:

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

int main(void)
{
    int i;
    int *array;

    array = malloc(sizeof(int)*10);
    for (i = 0; i < 10; i++){
        printf("%d ", array[i]);
    }
    putchar('\n');

    free(array);
    array = calloc(10, sizeof(int));
    for (i = 0; i < 10; i++){
        printf("%d ", array[i]);
    }
    putchar('\n');

    free(array);
    return 0;
}

realloc()Επεξεργασία

Τέλος, υπάρχει η realloc(), ίσως η πιο χρήσιμη συνάρτηση απ'όλες. Αυτό επειδή μπορεί να αλλάξει το μέγεθος ενός δυναμικού πίνακα! Αν δε μπορεί να το κάνει χρησιμοποιώντας την ίδια βάση, κάνει μόνη της τη μεταφορά των δεδομένων στη καινούργια θέση μνήμης και επιστρέφει τον καινούργιο δείκτη! Αυτές οι ιδιότητες την κάνουν πολύ χρήσιμη και ευέλικτη:

void* realloc (void* ptr, size_t size);
    ptr: ο δείκτης που είχε επιστραφεί από τις malloc, realloc ή calloc. Αν είναι NULL, δουλεύει σαν malloc
    size: το καινούργιο μέγεθος που θέλουμε, μπορεί να είναι και μικρότερο απ' το αρχικό και μεγαλύτερο
          Αν είναι μηδέν ίσως να δουλέψει σαν free, αλλά ίσως και όχι, οπότε μην βασίζεστε σ' αυτή τη συμπεριφορά.
    επιστρέφει: ptr αν έμεινε στην ίδια βάση ή καινούργιο δείκτη αν πήγε αλλού. Αν αποτύχει, επιστρέφει NULL, και
                ο παλιός ptr μπορεί ακόμα να χρησιμοποιηθεί. Γι' αυτό, καλό είναι να μη διαγράφετε τον παλιό δείκτη
                με τον καινούργιο, σε περίπτωση που επιστρέψει NULL.

Τελικό παράδειγμαΕπεξεργασία

Οκ λοιπόν, ας υλοποιήσουμε τώρα σαν τελικό παράδειγμα αυτό που ανέφερα στην αρχή:

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

/* Χρειάζεται για την qsort() */
int compareQsort(const void* a, const void* b)
{
  if (*(int*)a < *(int*)b)
    return -1;

  if (*(int*)a > *(int*)b)
    return 1;

  return 0;
}

int main(void)
{
    /* Αρχικοποιώ τις τιμές, για σιγουριά */
    int arraySize = 0;
    int *array = NULL;

    int i;

    /* Εδώ γίνεται η είσοδος των ακεραίων απ'την κονσόλα */
    printf("Give as many integers as you like, separated by blanks:\n");
    printf("( Anything other than a number quits the program )\n");
    for (;;){/* Ατέρμονας βρόχος, έτσι συνηθίζω να το συντάσσω */
        int input;

        if (scanf("%d", &input) != 1) /* Ελέγχω αν η scanf κατάφερε να διαβάσει αυτό που ήθελα */
            break; /* Αν όχι, απλά βγαίνω έξω απ'τη for */

        array = realloc(array, (++arraySize)*sizeof(int)); /* Μεγαλώνω τον πίνακα, δεν ελέγχω αν δε τα κατάφερε */
        array[arraySize - 1] = input;
    }

    /* Ταξινομεί τα στοιχεία του πίνακα */
    qsort(array, arraySize, sizeof(int), compareQsort);

    printf("These are the integers you gave, sorted:\n");
    for (i = 0; i < arraySize; i++){
        printf("%d ", array[i]);
    }
    putchar('\n');

    free(array);
    return 0;
}

Και αυτό ήταν. Όπως πάντα, happy coding! :-)