Τελικά τα περισσότερα που ήθελα να γράψω σε αυτό το νήμα για τους έξτρα ελέγχους που πρέπει να κάνουμε όταν δουλεύουμε με δείκτες, τα έγραψα στο νήμα του
buffer-overflow (στο τέλος εκείνης της δημοσίευσης δίνω και παραδείγματα κώδικα ειδικά για δείκτες).
Πέρα από τον έλεγχο για το αν είναι ή όχι NULL ένας δείκτης πριν τον χρησιμοποιήσουμε (που ισχύει για δείκτες οποιουδήποτε τύπου), για τα strings συχνά μας εξυπηρετεί να ελέγχουμε και για το αν ο πρώτος χαρακτήρας τους είναι ή όχι ο μηδενικός ('\0')... αν είναι δηλαδή κενά ή όχι τα strings. Αν είναι κενά ίσως δεν χρειάζεται να συνεχίσουμε τη ροή της συνάρτησης, οπότε επιστρέφουμε απευθείας την τιμή που θέλουμε, χωρίς να πάει η ροή στον βασικό αλγόριθμο της συνάρτησης.
Για παράδειγμα, ας υποθέσουμε πως θέλουμε να φτιάξουμε μια συνάρτηση:
- Κώδικας: Επιλογή όλων
char s_append( char *dst, const char *src );
η οποία θα "κολλάει" στο τέλος του string dst τα περιεχόμενα του string src. Αν το src όμως είναι ανύπαρκτο ( src == NULL ) ή υπαρκτό αλλά κενό ( *src == '\0' ) τότε δεν χρειάζεται να μπούμε σε καμία διαδικασία, απλά επιστρέφουμε το dst ίδιο όπως μας περάστηκε στην συνάρτηση.
Αυτοί οι έλεγχοι ονομάζονται έλεγχοι πρόωρου τερματισμού (early exit conditions) και προφανώς γίνονται στην αρχή των συναρτήσεων. Πολύ συχνά μάλιστα μας βοηθάνε να απλοποιήσουμε τον βασικό αλγόριθμο της συνάρτησης, αφού δεν χρειάζεται πλέον να τον βάλουμε να ελέγχει ξανά για early exits (αν και μερικές φορές δεν μπορούμε να το αποφύγουμε τελείως)
Οπότε για το παραπάνω παράδειγμα, μπορούμε να γράψουμε κάτι σαν το παρακάτω:
- Κώδικας: Επιλογή όλων
char s_append( char *dst, const char *src )
{
if ( !src || !*src ) // ισοδυναμεί με: if ( src == NULL || *src == '\0' )
return dst;
...
return dst;
}
Ένα ΒΑΣΙΚΟ σημείο που χρειάζεται ΜΕΓΑΛΗ ΠΡΟΣΟΧΗ εδώ είναι ότι ΠΡΩΤΑ ελέγχουμε για το αν ο δείκτης src είναι NULL και ΜΟΝΟ ΑΝ ΔΕΝ ΕΙΝΑΙ (και μόνο τότε) ελέγχουμε το περιεχόμενο του στοιχείου στο οποίο δείχνει!Αν ελέγξουμε πρώτα το περιεχόμενο του δείκτη ( *src == '\0 ') τότε αν ο src είχε για παράδειγμα περαστεί με τιμή NULL στην συνάρτηση, έστω και κατά λάθος, τότε προκαλούμε segmentation-fault (και άρα κενό ασφαλείας) διότι NULL σημαίνει πως δείχνει στο ΠΟΥΘΕΝΑ (που στην πράξη σημαίνει πως δείχνει σε κάποιο τυχαίο σημείο της μνήμης ... undefined ). Οπότε, γράφοντας εμείς *src == '\0' ρισκάρουμε να μετατραπεί κάποια στιγμή στο run-time του προγράμματος σε *NULL == '\0'... κι εκεί αρχίζουν τα όργανα!

Το να περαστεί με τιμή NULL ο δείκτης src στην s_append() μπορεί να συμβεί μόνο όταν στην main() (ή σε όποια άλλη συνάρτηση καλεί την s_append() ) έχουμε επιλέξει να ορίσουμε τον src ως δείκτη και όχι ως πίνακα. Όταν ορίζουμε πίνακες, τότε όχι μόνο αποκλείεται να είναι NULL ο δείκτης που χρησιμοποιείται για όνομα του πίνακα, αλλά η γλώσσα ΔΕΝ μας αφήνει κιόλας να του αλλάξουμε απευθείας την τιμή του (μέσω άλλου δείκτη που τον βάζουμε να δείχνει στον πίνακα μας αφήνει όμως, αλλά είναι άλλη ιστορία αυτό). Επίσης, δεσμεύει αυτόματα & εγγυημένα μνήμη για τον src, την οποία μάλιστα την αποδεσμεύει κιόλας αυτόματα στο τέλος.
Οπότε αν δοκιμάσετε κάτι σαν το παρακάτω, ο compiler θα σας βγάλει σφάλμα...
- Κώδικας: Επιλογή όλων
int main( void )
{
char src[10]; // ορισμός πίνακα
src = NULL; // ΔΕΝ ΜΑΣ ΑΦΗΝΕΙ !!!
...
Άρα δεν μας αφήνει να κάνουμε έστω και κατά λάθος NULL τον src απευθείας! Και όχι μόνο αυτό, έχει δεσμεύσει κιόλας αυτόματα μνήμη για 10 στοιχεία. Με λίγα λόγια, όταν χρησιμοποιούμε σύνταξη πινάκων στους ορισμούς, μειώνουμε σημαντικά τις πιθανότητες για σφάλματα απροσεξίας.
Αντίθετα, αν επιλέξουμε να ορίσουμε τον src με σύνταξη δεικτών ώστε να διαχειριστούμε μόνοι μας τη μνήμη που θα χρησιμοποιήσει, χρειάζεται μεγαλύτερη προσοχή από μεριά μας. Π.χ. μπορεί να ξεχαστούμε και να τον περάσουμε σαν όρισμα της s_append() ή όποιας άλλης συνάρτησης (ακόμα και της στάνταρ puts() ) χωρίς να έχουμε πρώτα δεσμεύσει μνήμη.
Παράδειγμα 1ο...
- Κώδικας: Επιλογή όλων
int main( void )
{
char *src = NULL;
...
s_append( ..., src );
...
Παράδειγμα 2ο...
- Κώδικας: Επιλογή όλων
int main( void )
{
char *src;
...
s_append( ..., src );
...
Στο 1ο παράδειγμα περνάμε τον src στην s_append() με τιμή NULL, ενώ στο δεύτερο παράδειγμα τον περνάμε με περιεχόμενο... ΑΠΡΟΣΔΙΟΡΙΣΤΟ (που είναι ακόμα χειρότερα, διότι δείχνει σε ένα τυχαίο σημείο της μνήμης !!! ).
Επειδή μέσα στην s_append() έχουμε βάλει ειδικό έλεγχο για το αν είναι NULL ή όχι ο src πριν καν επιχειρήσουμε να εξετάσουμε το περιεχόμενό του, κι αν είναι επιστρέφουμε τη ροή πίσω στη main(), δεν έχουμε πρόβλημα

Σε αυτή την περίπτωση λοιπόν η συνάρτησή μας λειτουργεί ΜΕ ΑΣΦΑΛΕΙΑ (κάτι που δεν ισχύει για την στανταρ συνάρτηση: strcat(), που επίσης κολλάει το string src στο string dst).
Στη δεύτερη περίπτωση όμως που περνάμε με απροσδιόριστη τιμή τον src στην s_append() έχουμε τεράστιο πρόβλημα, και μάλιστα άλυτο στο επίπεδο της συνάρτησης. Διότι θα περάσει τον έλεγχο για NULL, αλλά ο έλεγχος *src == '\0' θωρεί δεδομένο πως αφού ο src δεν είναι NULL τότε δείχνει σε έγκυρη μνήμη, δηλαδή σε μνήμη που έχει δεσμευτεί ειδικά για τον src πριν περαστεί στη συνάρτηση.... μόνο που εμείς έχουμε ΞΕΧΑΣΕΙ να κάνουμε αυτή τη δέσμευση μνήμης για τον src στην main() !!!! Οπότε, πάρε ένα ωραίο seg-fault να το ευχαριστηθεί η καρδία σου!

Αν ο src είχε οριστεί ως πίνακας στην main(), τότε θα είχε δεσμευτεί αυτόματα & εγγυημένα μνήμη για αυτόν

Επίσης δεν χρειάζεται να κάνουμε κάτι εμείς για την απελευθέρωση της μνήμης που έχει δεσμευτεί για τον πίνακα (γίνεται κι αυτή αυτόματα).
Επειδή όμως εμείς γουστάρουμε τα χειροκίνητα και μη-εγγυημένα

ο σωστός τρόπος να ορίσουμε ένα string με σύνταξη δεικτών και να το διαχειριστούμε, είναι ο παρακάτω..
- Κώδικας: Επιλογή όλων
int main( void )
{
char *src = calloc( 10, sizeof( char ) ); // ορισμός του src και δέσμευση μνήμης στην οποία δείχνει
...
s_append( ..., src );
...
if ( src ) {
free( src );
src = NULL;
}
Ελπίζω τώρα να είναι πιο σαφές γιατί στο προηγούμενο ποστ σας είπα να προτιμάτε να ορίζετε τα strings με σύνταξη πινάκων
Σημειώστε επίσης πως ότι έγραψα εδώ περί NULL, απροσδιόριστων τιμών, δυναμική κι αυτόματη διαχείριση μνήμης, κλπ ισχύει ΑΤΟΦΙΟ για οποιοδήποτε data-type της γλώσσας και όχι αποκλειστικά για strings!Αυτό που χρειάζεται ειδική μέριμνα για τα strings είναι πως για να μπορούν να διαχειριστούν με ασφάλεια με τις στάνταρ συναρτήσεις της string.h πρέπει να περιέχουν τουλάχιστον έναν μηδενικό χαρακτήρα, με τον 1ο από αυτούς να σηματοδοτεί το τέλος του τρέχοντος μήκους το. Αυτό είναι ΑΝΕΞΑΡΤΗΤΟ από ότι είπαμε σήμερα περί NULL, κλπ !!!! Είναι άλλο πράγμα δηλαδή, και προϋποθέτει πως έχει ήδη δεσμευτεί μνήμη για το string (είτε αυτόματα, είτε δυναμικά).
Εδώ πλέον αρχίζει να παίζει ρόλο η αρχικοποίηση που κάνουμε όταν πρωτο-δεσμεύουμε μνήμη για το string (το τι βάζουμε δηλαδή ως αρχικές τιμές μέσα σε αυτή τη μνήμη), καθώς και το αν φροντίζουμε να διατηρούμε πάντα τουλάχιστον έναν μηδενικό χαρακτήρα μέσα στο string, στο τέλος του τρέχοντος μήκους του, όταν "παίζουμε" με τα περιεχόμενά του.
Τα έχουμε ήδη αναλύσει σε προηγούμενα ποστς

Απορίες;
ΥΓ. @Star_Light: Ευχαριστώ για τα καλά λόγια!