Μάθημα - Απλά Συνδεδεμένες Λίστες

...ασύγχρονα μαθήματα γλώσσας C

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 02 Ιούλ 2011, 15:46

Μιας κι έχω ακόμα όρεξη, θα συνεχίσω να προσθέτω μέρη στο tutorial, κι ας τα διαβάσει όποιος ενδιαφέρεται με τους δικούς τους ρυθμούς :)

ΑΠΛΑ ΣΥΝΔΕΔΕΜΕΝΕΣ ΛΙΣΤΕΣ (Singly Linked Lists) - ΜΕΡΟΣ 5ο

Αναβαθμισμένη Δομή Λίστας (ενσωμάτωση δεικτών Κεφαλής & Ουράς και ενσωμάτωση Μήκους)
Επίσης: Οι διαχωριστικοί χαρακτήρες της τελείας: . και του βέλους: ->



Μέχρι στιγμής έχουμε καλύψει πολλές από τις βασικές αρχές που διέπουν τις απλά συνδεδεμένες λίστες και μάλιστα είμαστε σε θέση να τις χρησιμοποιήσουμε για την υλοποίηση κι άλλων πολύ συνηθισμένων δομών, όπως είναι η στοίβα (stack) και η ουρά (queue).

Η ανάλυση των στοιβών και των ουρών δεν περιλαμβάνεται στους στόχους αυτής της σειράς των tutorials. Περιληπτικά όμως, οι στοίβες είναι δομές όπου όλοι οι κόμβοι προστίθενται και αφαιρούνται από την αρχή της λίστας (π.χ. φανταστείτε έναν πάκο από τραπουλόχαρτα στα οποία μπορείτε να τραβάτε ή να βάζετε χαρτιά μονάχα στην κορυφή του).

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

Η πιο συνηθισμένη υλοποίηση των στοιβών και ουρών είναι με απλά συνδεδεμένες λίστες (θα βρείτε και με πίνακες) και πάντα συνοδεύονται από μια σειρά συγκεκριμένων πράξεων (operations) που δεν είναι τίποτα άλλο από συναρτήσεις που πρέπει να υλοποιηθούν για τις λειτουργίες των δομών.

Για παράδειγμα, για τις στοίβες πρέπει να γραφτούν κατ' ελάχιστο συναρτήσεις για τις πράξεις: empty(), push(), pop() και peek(). Η empty() εξετάζει αν η στοίβα είναι κενή, η push() προσθέτει ένα νέο στοιχείο στη κορυφή, η pop() εξετάζει κι αφαιρεί όποιο στοιχείο βρίσκεται στην κορυφή και η peek() εξετάζει το στοιχείο της κορυφής χωρίς να το αφαιρεί (μπορεί να την συναντήσετε και ως: top() )
Το πρόγραμμα της Ξερής που έφτιαξα και πόσταρα σε προηγούμενο ποστ χρησιμοποιεί κατά κόρον στοίβες (ο κώδικας των πράξεων είναι στα αρχεία: stack.c και stack.h).

Οι ουρές (queues) μεταξύ άλλων πράξεων περιλαμβάνουν τις enqueue() και dequeue(). Η πρώτη προσθέτει κόμβους στο τέλος της ουράς και η δεύτερη αφαιρεί κόμβους από την αρχή της. Άρα η υλοποίησή της βασίζεται στην ύπαρξη δύο δεικτών, έναν στην αρχή κι έναν στο τέλος της ουράς (που σε όρους απλά συνδεδεμένης λίστας είναι ο head και ο tail που είδαμε στο 4ο μέρος... μόνο που ο head αντιστοιχεί στο πίσω μέρος της ουράς και ο tail στο εμπρός μέρος της ουράς :P)

Η παραπάνω εισαγωγή (πέρα από την ενημερωτική της πλευρά) δείχνει πως υπάρχουν περιπτώσεις όπου η διατήρηση δεικτών στην αρχή και στο τέλος των λιστών παίζουν πολύ συχνά σημαντικό ρόλο, και άρα το να τους έχουμε ως ξεχωριστές μεταβλητές χύμα στην main() δεν είναι από τις καλύτερες επιλογές. Ειδικά σε προγράμματα που χρησιμοποιούν πολλές λίστες, μπορεί να οδηγήσει εύκολα σε... χαώδη κώδικα με ένα κάρο χύμα μεταβλητές.

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

Στο δικό μας παράδειγμα που έχουμε αναπτύξει μέχρι στιγμής στα προηγούμενα μέρη του tutorial, τέτοιες μεταβλητές υψηλότερου επιπέδου είναι οι δείκτες head και tail. Μπορούμε μάλιστα να προσθέσουμε μια ακόμα, την len (ή size) η οποία θα αντιστοιχεί στο συνολικό πλήθος των στοιχείων της λίστας ανά πάσα στιγμή, και θα την ενημερώνουμε κάθε φορά που προσθέτουμε ή αφαιρούμε κάποιον κόμβο. Αυτό θα έχει σαν αποτέλεσμα να μη χρειαζόμαστε πλέον τη συνάρτηση: list_len() η οποία έτσι κι αλλιώς είναι δραματικά αργή!

Η δομή που περιγράφει τον κάθε κόμβο παραμένει ίδια με πριν...

Κώδικας: Επιλογή όλων

typedef struct node {
int value;
struct node *next;
} Node;

Τώρα όμως θα προσθέσουμε μια ακόμα που θα περιέχει τους δείκτες head και tail, καθώς επίσης και έναν int για το συνολικό πλήθος των στοιχείων της λίστας...

Κώδικας: Επιλογή όλων

typedef struct list {
Node *head, *tail;
int len;
} List;

Οπότε τώρα χρειάζεται να τροποποιήσουμε και τον κώδικα, τόσο στη main() όσο και στις συναρτήσεις.

Σε ότι αφορά τη main, η λίστα μας πλέον δεν θα είναι ένας δείκτης σε Node αλλά θα είναι μια απλή μεταβλητή τύπου struct list (ή λόγω του typedef, τύπου List)...

Κώδικας: Επιλογή όλων

...
int main( void )
{
List list;
...
return EXIT_SUCCESS;
}

η οποία αποτελείται από 3 πεδία: List.head, List.tail, List.len (παρατηρήστε πως εφόσον η μεταβλητή list ΔΕΝ είναι δείκτης, τα πεδία της περιγράφονται με τελείες και όχι με το σύμβολο -> )

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

Κώδικας: Επιλογή όλων

...
int main( void )
{
List list = { .head=NULL, .tail=NULL, .len=0 }; // στάνταρ τρόπος αρχικοποίησης πεδίων στα struct, στην ANSI C
...
return EXIT_SUCCESS;
}

Κατόπιν πρέπει να προσαρμόσουμε αντίστοιχα τον κώδικα των συναρτήσεων, όπου πλέον αρκεί να τους περνάμε ως όρισμα το list (είτε by value, είτε by reference) αντί για τα 2 ξεχωριστά ορίσματα που περνάγαμε μέχρι τώρα, για τους δείκτες head και tail.

Ας δούμε πρώτα τις: list_print() και list_destroy() που είναι συντακτικά πιο βατές και μετά θα δούμε και τον κώδικα των list_prepend() και list_append()...

Κώδικας: Επιλογή όλων

// ------------------------------------------------------------------
void list_print( List list )
{
while ( list.head ) {
printf("%d ", list.head->value);
list.head = list.head->next;
}
putchar('\n'); // αλλαγή γραμμής

return;
}

// ------------------------------------------------------------------
void list_destroy( List *list )
{
if ( list->head == NULL )
return;

Node *dummy = NULL;
while ( list->head ) {
dummy = list->head->next;
free( list->head );
list->head = dummy;
}

return;
}

Η πρώτη παρατήρηση είναι πως στην list_print() την λίστα την περνάμε by value, ενώ στην list_destroy() την περνάμε by reference. Δεν χρειάζεται να περαστεί by reference στην list_destroy() (άλλωστε άμα δείτε στα προηγούμενα μέρη, by value την περνάμε στην list_destroy) απλά το έκανα έτσι τώρα για να γίνει εμφανής η διαφορά μεταξύ του συμβολισμού -> και της απλής τελείας, όταν περιγράφουμε τα πεδία μιας μεταβλητής που είναι τύπου struct.

Στο σώμα της list_print() η list είναι απλή μεταβλητή τύπου struct list (ή ισοδύναμα, τύπου List) με το struct να περιέχει ως ένα από τα πεδία του τον δείκτη head (που δείχνει σε struct node ή, ισοδύναμα, σε σκέτο Node).

Άρα η list ΔΕΝ είναι δείκτης, αλλά ο list.head είναι δείκτης.

Όταν λοιπόν θέλουμε να αναφερθούμε στα πεδία value και next του struct node στο οποίο δείχνει ο δείκτης head, χρησιμοποιούμε τον συμβολισμό ->. Δηλαδή, head->value και head->next. Όταν όμως θέλουμε να αναφερθούμε στον head που είναι πεδίο του struct list της μεταβλητής list η οποία ΔΕΝ είναι δείκτης σε struct list, τότε χρησιμοποιούμε την κλασική τελεία: list.head

Άρα λοιπόν, για να αναφερθούμε π.χ. στο πεδίο value ξεκινώντας από την μεταβλητή list, γράφουμε: list.head->value (και ομοίως, list.head->next).

Εξετάζοντας τώρα τον κώδικα της list_destroy() όπου επίτηδες έχω περάσει την list by reference, ώστε μέσα στο σώμα της συνάρτησης να είναι δείκτης, βλέπουμε πως χρησιμοποιούμε τον συμβολισμό -> ΚΑΙ για να αναφερθούμε στον δείκτη head: list->head και άρα και για να αναφερθούμε στο πεδίο next του struct node στο οποίο δείχνει ο head: list->head->next;

Καταλαβαίνω πως είναι παραπάνω από πιθανό να έχει χαθεί λιγάκι η μπάλα :?
Είναι όμως κρίσιμης σημασίας να τα ξεκαθαρίσετε στο μυαλό σας πριν προχωρήσουμε παρακάτω!

Δεν ξέρω αν θα σας βοηθήσει, αλλά αν προέρχεστε από αντικειμενοστραφείς γλώσσες, φανταστείτε ΣΥΝΤΑKΤΙΚΑ (ΚΑΙ ΜΟΝΟ ΣΥΝΤΑΚΤΙΚΑ) τα struct ως κλάσεις και τα πεδία του struct ως μεθόδους. Οπότε αν η list είναι ένα αντικείμενο της κλάσης List και ο head μια μέθοδος αυτής της κλάσης, για να αναφερθείτε στην μέθοδο head του αντικειμένου list θα γράφατε: list.head
Αν υπήρχε κάποιος τρόπος ώστε το αντικείμενο list να ήταν δείκτης στην κλάση List, τότε αντί για τελεία θα καλούσατε την μέθοδο στο αντικείμενο-δείκτη list με ->... δηλαδή: list->head

ΕΠΑΝΑΛΑΜΒΑΝΩ το παραπάνω σκεφτείτε το ΣΥΝΤΑΚΤΙΚΑ ΚΑΙ ΜΟΝΟ ΣΥΝΤΑΚΤΙΚΑ! Μην προσπαθήσετε δηλαδή να αντιστοιχήσετε σε επίπεδο λογικής τα struct με κλάσεις, τα πεδία των structs σε μεθόδους και τις μεταβλητές τύπου struct σε αντικείμενα, γιατί θα μπερδευτείτε χειρότερα! Αν το παραπάνω σας μπερδεύει χειρότερα, ΞΕΧΑΣΤΕ ΤΟ ΤΕΛΕΙΩΣ και μείνετε μονάχα στις έννοιες της C και μόνο, προκειμένου να τα ξεκαθαρίσετε στο μυαλό σας.

Όπως και να έχει πάντως, βεβαιωθείτε πως έχετε κατανοήσει πλήρως τι είναι οι δείκτες, τι είναι τα struct, τι είναι τα πεδία των struct και τις μεταξύ τους σχέσεις και διαφορές, πριν προχωρήσουμε στον κώδικα των συναρτήσεων list_prepend() και list_append()... κι αργότερα στις: list_insert_ascending() και list_insert_descending().

Ο καλύτερος τρόπος είναι να γράφετε δικά σας μικρά παραδείγματα σε κώδικα, για ότι δεν έχετε καταλάβει πλήρως, μέχρι να το κατανοήσετε. Εννοείται πως σε οποιαδήποτε απορία εγώ είμαι πάντα ανοιχτός


Αν από την άλλη μεριά αισθάνεστε πως τα έχετε εμπεδώσει (συμπεριλαμβανομένου και αυτού του 5ου μέρους), τότε είστε έτοιμοι να υλοποιήσετε μόνοι σας και ουρές (queues) και πολύ περισσότερο στοίβες (stacks)... ή τουλάχιστον να παρακολουθήσετε με άνεση σχετικά άρθρα και παραδείγματα όπως στα παραπάνω links.

Εμείς στη συνέχεια των tutorials θα δούμε τον τροποποιημένο κώδικα για τις: list_prepend() και list_append() και κατόπιν θα δούμε και τις συναρτήσεις: list_insert_descend() και list_insert_ascend() που θα εισαγάγουν κόμβους απευθείας ταξινομημένους, σε φθίνουσα και σε αύξουσα σειρά αντίστοιχα.
Τελευταία επεξεργασία από migf1 και 22 Ιούλ 2011, 10:28, έχει επεξεργασθεί 3 φορά/ες συνολικά
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό Star_Light » 02 Ιούλ 2011, 22:18

migf1 έγραψε:Απλά Συνδεδεμένες Λίστες - Μέρος 2ο

Σε συνέχεια του 1ου μέρους και ελπίζοντας πως είναι ξεκάθαρη η διαφορά (αλλά και η σχέση) μεταξύ ενός δείκτη και του κόμβου στον οποίον δείχνει, ας ξεκινήσουμε να φτιάξουμε σιγά-σιγά συνηθισμένες συναρτήσεις για λίστες.

Ας ξεκινήσουμε με 3 βατές:

  1. void list_print( Node *list ) : που τυπώνει τα πεδία value όλων των κόμβων
  2. int list_len( Node *list ) : που μετράει το πλήθος των κόμβων της λίστας
  3. void list_destroy( Node *list ) : που απελευθερώνει όλους τους κόμβους της λίστας.
Θα ξεκινήσω με την list_print() που είναι η πιο βατή από όλες, και που ουσιαστικά απλά μετατρέπει σε συνάρτηση τον κώδικα του while-loop που έχουμε ήδη γράψει στο 1ο μέρος.

Ελπίζω όμως πως θα μας βοηθήσει επίσης να ξεκαθαρίσουμε στο μυαλό μας ένα ακόμα "ευαίσθητο" σημείο, που ισχύει για όλες τις συναρτήσεις στη C (ανεξαιρέτως) και που ονομάζεται πέρασμα ορισμάτων "by value" έναντι του περάσματος ορισμάτων "by reference" (όποιος γνωρίζει τις ακριβείς ορολογίες στα Ελληνικά, παρακαλώ ας τις συμπληρώσει).

Παραθέτω τον κώδικα της: list_print()...

Κώδικας: Επιλογή όλων

/* ------------------------------------------------------------------
* Τύπωμα του πεδίου value όλων των κόμβων της λίστας
* ------------------------------------------------------------------
*/
void list_print( Node *list )
{
while ( list ) {
printf("%d ", list->value);
list = list->next;
}
putchar('\n'); // αλλαγή γραμμής

return;
}

Συγκρίνοντάς τον με τον κώδικα του while-loop στην main() του 1ου μέρους, η 1η χτυπητή διαφορά είναι πως εκεί χρησιμοποιούσαμε έναν βοηθητικό δείκτη dummy προκειμένου να διατρέξουμε τη λίστα από τον πρώτο έως τον τελευταίο της κόμβο, γιατί τον list τον θέλαμε να παραμείνει στην αρχή της λίστας μας και μετά το τέλος του loop.

Αντίθετα τώρα, μέσα στη συνάρτηση print_list() διατρέχουμε τη λίστα απευθείας με τον δείκτη list, άρα φαινομενικά όταν τελειώσει το loop κι επιστρέψει ο έλεγχος στη main() ο δείκτης list δείχνει στο NULL, και άρα χάσαμε την επαφή μας με την αρχή της λίστας.

Σωστά;

Λάθος! Η διαφορά είναι πως στην main ο δείκτης list είναι ο αυθεντικός δείκτης που δείχνει στην αρχή της λίστας μας. Όταν όμως τον περάσουμε ως όρισμα σε μια συνάρτηση, τότε μέσα στη συνάρτηση ΔΕΝ περνάει ο αυθεντικός δείκτης, αλλά ένα αντίγραφό του (που το δημιουργεί εσωτερικά η γλώσσα). Αυτός είναι ο στάνταρ τρόπος περάσματος ορισμάτων μέσα σε συναρτήσεις στη C, και ονομάζεται pass by value, διότι περνάει μέσα στις συναρτήσεις τις ΤΙΜΕΣ μεταβλητών που αντιστοιχούν στα ορίσματα, και όχι τις διευθύνσεις των μεταβλητών.

Με απλά λόγια, περνάει μέσα στις συναρτήσεις αντίγραφα και όχι τις αυθεντικές μεταβλητές. Όταν τελειώνει η συνάρτηση τα αντίγραφα των ορισμάτων (καθώς και τυχόν τοπικές μεταβλητές που έχουμε ορίσαμε μέσα στη συνάρτηση) διαγράφονται.

Σημειώστε όμως πως όταν λέμε ότι διαγράφεται το αντίγραφο του δείκτη list ΔΕΝ σημαίνει πως καταστρέφεται ο κόμβος στον οποίον δείχνει (αυτός για να καταστραφεί πρέπει να γίνει free() ). Δηλαδή έχουμε έναν κόμβο χωρίς να δείχνει σε αυτόν κάποιος δείκτης.

Άρα λοιπόν ο list μέσα στην print_list() είναι ένα αντίγραφο του αυθεντικού list που έχουμε ορίσει στην main(). Οπότε όταν τελειώσει η συνάρτηση και η ροή επιστρέψει πίσω στην main() ο αυθεντικός δείκτης list έχει μείνει ανέπαφος, δείχνοντας δηλαδή στην αρχή της λίστας μας.

Απλά μέσα στην συνάρτηση print_list() χρησιμοποιήσαμε το αντίγραφο του list για να διατρέξουμε όλους τους κόμβους της λίστας από την αρχή μέχρι το τέλος της, τυπώνοντας στην πορεία το πεδίο value του κάθε κόμβου και στο τέλος απλά διαγράφηκε ο δείκτης-αντίγραφο.

Έχοντας κατά νου τα παραπάνω, είναι ελπίζω εύκολο τώρα να φτιάξουμε και μια συνάρτηση που θα διατρέχει τη λίστα μας, αλλά αντί να τυπώνει τα πεδία value των κόμβων, απλά θα τα μετράει και θα μας επιστρέφει το σύνολο:

Κώδικας: Επιλογή όλων

/* ------------------------------------------------------------------
* Μέτρημα κι επιστροφή του πλήθους κόμβων της λίστας
* ------------------------------------------------------------------
*/
int list_len( Node *list )
{
register int i = 0;
for (i=0; list; i++) // ισοδυναμεί με: for (i=0; list != NULL; i++)
list = list->next;

/** ΕΝΑΛΛΑΚΤΙΚΟΣ ΚΩΔΙΚΑΣ
while ( list ) { // ισοδυναμεί με: while ( list != ΝULL ) {
list = list->next;
i++;
}
**/
return i;
}
για ποικιλία την υλοποίησα με for-loop αλλά αν σας μπερδεύει, έβαλα σε σχόλια ως εναλλακτικό τρόπο το κλασικό while-loop (προφανώς υπάρχουν κι άλλες υλοποιήσεις).

Ας δούμε τώρα και τον κώδικα της συνάρτησης που θα δέχεται ως όρισμα τη λίστα και θα καταστρέφει όλους τους κόμβους της με free()...

Κώδικας: Επιλογή όλων

/* ------------------------------------------------------------------
* Απελευθέρωση της μνήμης που έχουμε δεσμεύσει για κάθε κόμβο της λίστας
* ------------------------------------------------------------------
*/
void list_destroy( Node *list )
{
if ( !list ) // ισοδυναμεί με: if ( list == NULL )
return;

Node *dummy = NULL;
while ( list ) { // ισοδυναμεί με: while ( list != NULL ) {
dummy = list->next;
free( list );
list = dummy;
}

return;
}

Αυτό που χρειάζεται εξήγηση στον παραπάνω κώδικα είναι η χρήση του βοηθητικού δείκτη dummy, τοπικά μέσα στην συνάρτηση.

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

Η λύση είναι πριν καταστρέψουμε τον κόμβο, να βάλουμε τον βοηθητικό δείκτη dummy να δείχνει εκεί που δείχνει το πεδίο next του υπό καταστροφή κόμβου. Μετά μπορούμε να καταστρέψουμε με free() τον κόμβο. Πριν επιστρέψει η ροή στη συνθήκη του loop (η οποία ελέγχει τον list) λέμε στον δείκτη list να δείχνει εκεί που δείχνει ο δείκτης dummy (δηλαδή στον επόμενο κόμβο από αυτόν που καταστρέψαμε).

Ελπίζω να είναι κατανοητή η εξήγηση που έδωσα. Αν όχι, ρωτήστε, να το ξανά εξηγήσω.

Οπότε, ας συνδυάσουμε τώρα τον κώδικα του 1ου μέρους με τις συναρτήσεις αυτού του 2ου μέρους...

Κώδικας: Επιλογή όλων

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

typedef struct node {
int value;
struct node *next;
} Node;

/* ------------------------------------------------------------------
* Μέτρημα κι επιστροφή του πλήθους κόμβων της λίστας
* ------------------------------------------------------------------
*/
int list_len( Node *list )
{
register int i = 0;
for (i=0; list; i++) // ισοδυναμεί με: for (i=0; list != NULL; i++)
list = list->next;

/** ΕΝΑΛΛΑΚΤΙΚΟΣ ΚΩΔΙΚΑΣ
while ( list ) { // ισοδυναμεί με: while ( list != ΝULL ) {
list = list->next;
i++;
}
**/
return i;
}

/* ------------------------------------------------------------------
* Τύπωμα του πεδίου value όλων των κόμβων της λίστας
* ------------------------------------------------------------------
*/
void list_print( Node *list )
{
while ( list ) {
printf("%d ", list->value);
list = list->next;
}
putchar('\n'); // αλλαγή γραμμής

return;
}

/* ------------------------------------------------------------------
* Απελευθέρωση της μνήμης που έχουμε δεσμεύσει για κάθε κόμβο της λίστας
* ------------------------------------------------------------------
*/
void list_destroy( Node *list )
{
if ( !list ) // ισοδυναμεί με: if ( list == NULL )
return;

Node *dummy = NULL;
while ( list ) { // ισοδυναμεί με: while ( list != NULL ) {
dummy = list->next;
free( list );
list = dummy;
}

return;
}

// -------------------------------------------------------------------
int main ( void )
{
Node *list = NULL, *newnode = NULL;

// δημιουργία και εισαγωγή του 1ου στοιχείου

list = calloc(1, sizeof(Node) );
if ( !list ) { // αποτυχία της calloc()
puts("*** out of memory, aborting program");
exit(1);
}
list->value = 52;
list->next = NULL;

// δημιουργία και εισαγωγή του 2ου στοιχείου

newnode = calloc(1, sizeof(Node) );
if ( !newnode ) { // αποτυχία της calloc()
puts("*** out of memory, aborting program");
free( list ); // αποδέσμευση μνήμης 1ου στοιχείου
exit(1);
}
newnode->value = 62;
newnode->next = NULL;
list->next = newnode;

// καταμέτρηση και τύπωμα του πλήθους των κόμβων στη λίστα
printf("The list consists of %d nodes\n", list_len( list) );

// τύπωμα των στοιχείων της λίστας
list_print( list );

// αποδέσμευση όλων των κόμβων από τη μνήμη
list_destroy( list );

fflush(stdin); getchar();
exit(0);
}


Μπορείτε να δείτε τον τελικό κώδικα του 2ου μέρους με syntax-highlighting, καθώς και την έξοδο που παράγει, εδώ: http://codepad.org/Br2OcD8g

Σε επόμενα posts θα προσπαθήσω να αναλύσω 2-3 διαφορετικούς τρόπους για εισαγωγή νέων στοιχείων στη λίστα, μέσω αντίστοιχων συναρτήσεων (με κώδικα προφανώς εκτός από σχόλια).


εΧΩ μια πολυ βασικη απορια... και στο αλλο σαιτ με το τουτοριαλ που ειχες ανεβασει με τους δεικτες.... Μας ειχες πει πως η αλλαγη μεσα σε μια συναρτηση δεν διατηρειται στην κληση με τιμη... pass by value επειδη μεσα στην συναρτηση περάστηκε ενα αντίγραφο...Και οτι αυτο μπορουμε να το αλλαξουμε με την χρηση των δεικτων... αρα πως εδω στην αρχη αρχη του ποστ σου λες ας πουμε οτι περνιεται ενα αντιγραφο του δεικτη στην συναρτηση????? Σαν να μπερδευτηκα....
Γνώσεις ⇛ Linux: Βασικές ┃ Προγραμματισμός: Δέν θέλω μεροκάματο , θέλω C και κακο θάνατο! ┃ Αγγλικά: Lower
Λειτουργικό ⇛ Ubuntu 10.10 σε Dual Boot με Windows 7
Προδιαγραφές ⇛ Επεξεργαστής : Intel(R) Core(TM) i3 CPU 540 @3.07Ghz (64bit)
RAM : Kingston 2GB
HDD : Coreshare 500GB
Κάρτα Γραφικών : Intel Corporation Core Processor Integrated Graphics Controller(rev 18) (prog-if 00 [VGA controller]) [8086:0042]
Star_Light
superbTUX
superbTUX
 
Δημοσιεύσεις: 2787
Εγγραφή: 01 Μάιος 2010, 21:07
Τοποθεσία: Αθήνα
IRC: Star_Light
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 02 Ιούλ 2011, 23:13

Λογική η απορία!

Το θέμα είναι όταν θέλουμε να περάσουμε by reference τον ίδιον τον δείκτη, και όχι αυτό που δείχνει ο δείκτης. Όταν δηλαδή ΔΕΝ θέλουμε να περάσουμε σε μια συνάρτηση απλά τη διεύθυνση μνήμης του struct στο οποίο δείχνει ο δείκτης list (το οποίο struct ισούται με το 1ο στοιχειό της λίστας), αλλά θέλουμε να περάσουμε στη συνάρτηση τη διεύθυνση του δείκτη που δείχνει στο 1ο στοιχείο της λίστας.

Για αυτό όταν θέλουμε να περάσουμε σε μια συνάρτηση έναν δείκτη by reference, τότε μέσα στη συνάρτηση τον διαχειριζόμαστε ως δείκτη σε δείκτη (Node **list) και όταν καλούμε τη συνάρτηση στη main() της περνάμε σαν όρισμα τη διεύθυνση του δείκτη list.. .δηλαδή: pass_by_ref( &list ).

Αν το καταλαβαίνεις καλύτερα, σκέψου το κάπως έτσι: εφόσον ο list είναι τύπου Node * (δηλαδή δείκτης), η διεύθυνση του list είναι τύπου Node ** (δηλαδή δείκτης σε δείκτη).

Μέσα στην συνάρτηση πειράζουμε τον ίδιο τον δείκτη (τον μετακινούμε, το βάζουμε να δείχνει κάπου αλλού από εκεί που έδειχνε πριν καλέσουμε τη συνάρτηση) οπότε για να διατηρηθεί η αλλαγή που κάνουμε στον δείκτη αυτόν κάθε αυτόν, πρέπει να τον περάσουμε by reference.
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 02 Ιούλ 2011, 23:23

Με άλλη διατύπωση, όταν περνάς στη συνάρτηση σκέτο τον list ουσιαστικά περνάς by reference το 1ο στοιχείο της λίστας (το 1ο struct δηλαδή) και όχι τον δείκτη που δείχνει στο 1ο στοιχείο. Για να περάσεις by reference τον ίδιο τον δείκτη, πρέπει να περάσεις στη συνάρτηση τη διεύθυνση του δείκτη: &list (δηλαδή Node **)
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 03 Ιούλ 2011, 00:19

Έφτιαξα ένα μικρό επεξηγηματικό παράδειγμα, όπου βάζω έναν δείκτη να δείχνει στο 5ο στοιχείο ενός πίνακα και 2 συναρτήσεις που τον μετακινούν 1 θέση, για να δείξει στο 6ο στοιχείο του πίνακα. Στην 1η συνάρτηση τον περνάω by value (οπότε η μετακίνηση δεν διατηρείται) και στη 2η by reference (οπότε η μετακίνηση διατηρείται).

Κώδικας: Επιλογή όλων

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

// ------------------------------------------------------------------------------
void pointer_by_value (int *p )
{
p++;
return;
}

// ------------------------------------------------------------------------------
void pointer_by_ref (int **p )
{
(*p)++;
return;
}

// ------------------------------------------------------------------------------
int main( void )
{
int array[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int *p;

p = array + 4;
printf("\nstart:\t\t*p=%d\n", *p);

pointer_by_value( p );
printf("\nafter by_value:\t*p=%d\n", *p);

pointer_by_ref( &p );
printf("\nafter by_ref:\t*p=%d\n", *p);

fflush(stdin); getchar();
exit(EXIT_SUCCESS);
}
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό Star_Light » 03 Ιούλ 2011, 03:17

Α οκ καταλαβα τον διπλο δεικτη... αλλα ηθελα να πω πως

στο αλλο τουτοριαλ ειχαμε αυτο εδω το παραδειγμα

Κώδικας: Επιλογή όλων


1.#include<stdio.h>
2.void foo(int n)
3.{
4.n=1000;
5.printf("μεσα στην foo():%d\n",n);
6.return;
7.}
8.int main(void)
9.{
10.int n;
11.n=30;
12.printf("πριν την foo(n):%d",n);
13.foo(n);
14.printf("μετα την foo(n):%d\n",n);
15.getchar();
16.return 0:
17.}


και αυτο εδινε εξοδο 30 1000 30

αν αλλαξουμε πχ και βαλουμε

Κώδικας: Επιλογή όλων


2.void foo (int *n)
4.*n=1000;

και μετα *n μεσα στις printf



θα δωσει 30 1000 1000



και ειχαμε πει πως η αλλαγη δεν διατηρηθηκε οταν τερματισε η συναρτηση επειδη μεσα στην συναρτηση περαστηκε ενα αντιγραφο. Και αυτο μπορεις να το αλλαξεις με την χρηση των δεικτων δηλαδη ας πουμε τωρα σαν να το αναιρεις.... εκει μπερδευομαι...

"Κάνοντας ξανά compile το πρόγραμμα και τρέχοντάς το θα δείτε πως πλέον η αλλαγή της μεταβλητής n μέσα στη συνάρτηση foo() διατηρείται και μετά τον τερματισμό της συνάρτησης. Αυτό ονομάζεται "pass by reference". Το πρόγραμμα θα τυπώσει τις παρακάτω γραμμές στην οθόνη σας:"

το λες ξεκαθαρα δηλαδη..... και εχω σκαλωσει :S
Γνώσεις ⇛ Linux: Βασικές ┃ Προγραμματισμός: Δέν θέλω μεροκάματο , θέλω C και κακο θάνατο! ┃ Αγγλικά: Lower
Λειτουργικό ⇛ Ubuntu 10.10 σε Dual Boot με Windows 7
Προδιαγραφές ⇛ Επεξεργαστής : Intel(R) Core(TM) i3 CPU 540 @3.07Ghz (64bit)
RAM : Kingston 2GB
HDD : Coreshare 500GB
Κάρτα Γραφικών : Intel Corporation Core Processor Integrated Graphics Controller(rev 18) (prog-if 00 [VGA controller]) [8086:0042]
Star_Light
superbTUX
superbTUX
 
Δημοσιεύσεις: 2787
Εγγραφή: 01 Μάιος 2010, 21:07
Τοποθεσία: Αθήνα
IRC: Star_Light
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό Star_Light » 03 Ιούλ 2011, 03:32

ΑΚΟΥΝΑ ΜΑΤΑΤΑ! Δεν τον καλεις.... με την διευθυνση του.... επομενως δεν ειναι call by reference σωστα ????

Ουσιαστικα εφοσον δεν τον καλεις μεσα στην συναρτηση με ορισμα την διευθυνση του... οπως τον καλουσες και στο αλλο τουτοριαλ δηλαδη foo(&n) τοτε ΔΕΝ ειναι by reference ε??? αλλα by value και το κανεις αυτο για να μην "χαλασεις" τον δεικτη που ουσιαστικα βρισκεται εξω απο την main και θελεις να δειχνει στην αρχη της λιστας σου ? !

Eιναι ισως και αυτο που ειχες πει οτι ακομη και αν βαλω αστερισκο μεσα στην συναρτηση στο ορισμα δεν σημαινει πως περναω με αναφορα.....
αν δεν καλεσω την συναρτηση με την διευθυνση της μεταβλητης ειτε ειναι δεικτης ειτε απλη μεταβλητη??? γιατι και ο δεικτης επι της ουσιας μεταβλητη ειναι τι ειναι..... δεν κανουν τα ρασα τον παππα που λεμε! Αλλα ο παππας τα ρασα...!!!
Γνώσεις ⇛ Linux: Βασικές ┃ Προγραμματισμός: Δέν θέλω μεροκάματο , θέλω C και κακο θάνατο! ┃ Αγγλικά: Lower
Λειτουργικό ⇛ Ubuntu 10.10 σε Dual Boot με Windows 7
Προδιαγραφές ⇛ Επεξεργαστής : Intel(R) Core(TM) i3 CPU 540 @3.07Ghz (64bit)
RAM : Kingston 2GB
HDD : Coreshare 500GB
Κάρτα Γραφικών : Intel Corporation Core Processor Integrated Graphics Controller(rev 18) (prog-if 00 [VGA controller]) [8086:0042]
Star_Light
superbTUX
superbTUX
 
Δημοσιεύσεις: 2787
Εγγραφή: 01 Μάιος 2010, 21:07
Τοποθεσία: Αθήνα
IRC: Star_Light
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό Star_Light » 03 Ιούλ 2011, 04:56

Λοιπον ενταξει ολοκληρωσα και το διαβασμα του 3ου μερους των λιστων.... καπου ο δασκαλος migf1 (ας μου επιτρεψει αυτο τον χαρακτηρισμο μιας και ειναι καλλιτεχνης) ειπε για το αν θα πρεπει να συνεχισει η οχι..... βασικα η δικια μου αποψη ειναι λιγο πιο σιγα ειναι πολυ ενδιαφεροντα ολα αυτα και τα διαβαζω σιγα σιγα και με προσοχη οπως και αλλα μελη ... φυσικα δεν ξερω και τον χρονο του σιγουρα θα εχει και αλλες δουλειες οποτε ειναι καθαρα και στην δικη του κριση... Παντως εγω C αν και διαβαζα και στο παρελθον και ειχα βγαλει 2 βιβλια του γκιουρδα.... δεν θα ντραπω να πω πως τωρα αρχισα να μαθαινω καλα. Δεν βαριεσαι ποτε δεν ειναι αργα ο θεος να εχει καλα τον migf1!!!!!!

Εχω μερικες αποριες καταρχην μ αρεσε περισσοτερο η υλοποιηση στο 3ο που λες οτι επιστρεφει την ανανεωμενη εκδοση της λιστας... βεβαια και η κληση με αναφορα μου αρεσε αρκετα... κοιταξε να δεις... εκει που λες πχ για διπλους δεικτες... ωραια... εγω ξερω σε εναν μονο δεικτη πως αν πχ δηλωσω

Κώδικας: Επιλογή όλων
int *x;


τοτε το x ειναι μια μεταβλητη δεικτη... δηλαδη δειχνει σε μια αλλη μεταβλητη.

επομενως αν εγω ορισω

Κώδικας: Επιλογή όλων
int **x;


τοτε το *x ειναι αντιστοιχως η μεταβλητη δεικτη μου??? δηλαδη δειχνει σε μια αλλη μεταβλητη που ειναι αυτη τη φορα δεικτης???

ευχαριστω!

Π.Σ Απο assembly ξερεις???? (ασχετο)
Γνώσεις ⇛ Linux: Βασικές ┃ Προγραμματισμός: Δέν θέλω μεροκάματο , θέλω C και κακο θάνατο! ┃ Αγγλικά: Lower
Λειτουργικό ⇛ Ubuntu 10.10 σε Dual Boot με Windows 7
Προδιαγραφές ⇛ Επεξεργαστής : Intel(R) Core(TM) i3 CPU 540 @3.07Ghz (64bit)
RAM : Kingston 2GB
HDD : Coreshare 500GB
Κάρτα Γραφικών : Intel Corporation Core Processor Integrated Graphics Controller(rev 18) (prog-if 00 [VGA controller]) [8086:0042]
Star_Light
superbTUX
superbTUX
 
Δημοσιεύσεις: 2787
Εγγραφή: 01 Μάιος 2010, 21:07
Τοποθεσία: Αθήνα
IRC: Star_Light
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 03 Ιούλ 2011, 10:36

Star_Light έγραψε:ΑΚΟΥΝΑ ΜΑΤΑΤΑ! Δεν τον καλεις.... με την διευθυνση του.... επομενως δεν ειναι call by reference σωστα ????

Ουσιαστικα εφοσον δεν τον καλεις μεσα στην συναρτηση με ορισμα την διευθυνση του... οπως τον καλουσες και στο αλλο τουτοριαλ δηλαδη foo(&n) τοτε ΔΕΝ ειναι by reference ε??? αλλα by value και το κανεις αυτο για να μην "χαλασεις" τον δεικτη που ουσιαστικα βρισκεται εξω απο την main και θελεις να δειχνει στην αρχη της λιστας σου ? !

Eιναι ισως και αυτο που ειχες πει οτι ακομη και αν βαλω αστερισκο μεσα στην συναρτηση στο ορισμα δεν σημαινει πως περναω με αναφορα.....
αν δεν καλεσω την συναρτηση με την διευθυνση της μεταβλητης ειτε ειναι δεικτης ειτε απλη μεταβλητη??? γιατι και ο δεικτης επι της ουσιας μεταβλητη ειναι τι ειναι..... δεν κανουν τα ρασα τον παππα που λεμε! Αλλα ο παππας τα ρασα...!!!


Αυτό ακριβώς :)

Για να περάσεις μια μεταβλητή σε μια συνάρτηση by reference πρέπει ΚΑΙ στην main() να περάσεις ως όρισμα τη συνάρτησης τη διεύθυνση μνήμης της μεταβλητής (δηλαδή με & μπροστά από το όνομά της) ΚΑΙ μέσα στη συνάρτηση να την διαχειρίζεσαι ως δείκτη ;)

Η λογική παραμένει η ίδια, είτε η αυθεντική μεταβλητή σου είναι μια απλή μεταβλητή, είτε είναι δείκτης, είτε είναι δείκτης σε δείκτη!

Δηλαδή, αν η αρχική σου μεταβλητή είναι δηλωμένη ως μια απλή μεταβλητή n στην main, τότε την περνάς στην συνάρτηση ως &n και μέσα στη συνάρτηση την διαχειρίζεσαι ως *n.

Αν η αρχική σου μεταβλητή είναι δηλωμένη ως δείκτης *n στην main, τότε την περνάς στην συνάρτηση πάλι ως &n και μέσα στη συνάρτηση την διαχειρίζεσαι ως **n (διπλός δείκτης).

Αν η αρχική σου μεταβλητή είναι δηλωμένη ως δείκτης σε δείκτη **n στην main, τότε την περνάς στην συνάρτηση πάλι ως &n και μέσα στη συνάρτηση την διαχειρίζεσαι ως ***n (τριπλός δείκτης).

Και πάει λέγοντας...

Ορίστε κι άλλο ένα μικρό παράδειγμα (δες πως συμπεριφέρεται ο px)...
Κώδικας: Επιλογή όλων

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

// ------------------------------------------------------------------------------
void px_by_value (int *px, int *py )
{
px = py;
return;
}

// ------------------------------------------------------------------------------
void px_by_ref (int **px, int *py )
{
*px = py;
return;
}

// ------------------------------------------------------------------------------
int main( void )
{
int x = 10, y = 20;
int *px = &x, *py = &y;

printf("\nstart:\t\t*px=%d, *py=%d\n", *px, *py);

px_by_value( px, py );
printf("\nafter by_value:\t*px=%d, *py=%d\n", *px, *py);

px_by_ref( &px, py );
printf("\nafter by_ref:\t*px=%d, *py=%d\n", *px, *py);

fflush(stdin); getchar();
exit(EXIT_SUCCESS);
}
Τελευταία επεξεργασία από migf1 και 03 Ιούλ 2011, 10:48, έχει επεξεργασθεί 3 φορά/ες συνολικά
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

Re: ΚΕΦΑΛΑΙΟ 6 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό migf1 » 03 Ιούλ 2011, 10:41

Star_Light έγραψε:
Εχω μερικες αποριες καταρχην μ αρεσε περισσοτερο η υλοποιηση στο 3ο που λες οτι επιστρεφει την ανανεωμενη εκδοση της λιστας... βεβαια και η κληση με αναφορα μου αρεσε αρκετα... κοιταξε να δεις... εκει που λες πχ για διπλους δεικτες... ωραια... εγω ξερω σε εναν μονο δεικτη πως αν πχ δηλωσω

Κώδικας: Επιλογή όλων
int *x;


τοτε το x ειναι μια μεταβλητη δεικτη... δηλαδη δειχνει σε μια αλλη μεταβλητη.

επομενως αν εγω ορισω

Κώδικας: Επιλογή όλων
int **x;


τοτε το *x ειναι αντιστοιχως η μεταβλητη δεικτη μου??? δηλαδη δειχνει σε μια αλλη μεταβλητη που ειναι αυτη τη φορα δεικτης???

ευχαριστω!


Έτσι ακριβώς! :)

έγραψε:Π.Σ Απο assembly ξερεις???? (ασχετο)

Ήξερα, ήταν η 2η γλώσσα που διδάχτηκα μετά την Pascal (είχαμε φτιάξει κιόλας σε Assembly έναν editor, έναν compiler, έναν linker και έναν loader μιας μίνι γλώσσας που ήταν πολύ μικρό υποσύνολο της Pascal). Όλα αυτά όμως μπήκαν στο χρονο-ντούλαπο από τη στιγμή που έμαθα C. Δεν ξανασχολήθηκα με Assembly (ούτε με Pascal).
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

ΠροηγούμενηΕπόμενο

Επιστροφή στο Μαθήματα C