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

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

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

Δημοσίευσηαπό migf1 » 28 Ιουν 2011, 18:32

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

Απλή Δομή Λίστας και Σύγκριση με Πίνακες


έγραψε:ΠΙΝΑΚΑΣ ΠΕΡΙΕΧΟΜΕΝΩΝ:
Μέρος 1ο - Απλή Δομή Λίστας και Σύγκριση με Πίνακες
Μέρος 2ο - Λίστα ως Όρισμα Αναφοράς (by Reference) & ως Όρισμα Τιμής (by Value) - Συναρτήσεις: print, length & destroy
Μέρος 3ο - Προσθήκη Νέων Κόμβων στην Αρχή της Λίστας - Συνάρτηση: prepened
Μέρος 4ο - Προσθήκη Νέων Κόμβων στο Τέλος της Λίστας - Συνάρτηση: append
Μέρος 5ο - Αναβαθμισμένη Δομή Λίστας (ενσωμάτωση δεικτών Κεφαλής & Ουράς και ενσωμάτωση Μήκους)
Μέρος 6ο - Επανακωδικοποίηση των Συναρτήσεων: prepend, append, print και destroy (κατάργηση της: length)

Λοιπόν, αποφάσισα να προσπαθήσω να αποσαφηνίσω όσο μπορώ τα περί singly linked lists (απλά συνδεδεμένες λίστες) ελπίζοντας πως θα φανούν χρήσιμα σε αρκετά παιδιά. Δεν ξέρω πόσα posts θα χρειαστούν. Θα εξαρτηθεί υποθέτω από την όρεξή μου και την ανταπόκριση της 1ης ;)

Οι απλά συνδεδεμένες λίστες είναι άμεσα συγκρίσιμες με τους πίνακες (arrays) ως προς το σκοπό ύπαρξής τους, έχοντας υπέρ και κατά συγκριτικά με τους πίνακες.

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

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

Οι πίνακες είναι λίγο-πολύ γνωστό σε όλους πως αποτελούνται από συνεχόμενα στοιχεία στη μνήμη, που σημαίνει πως είναι πολύ εύκολο & γρήγορο να αναφερθούμε σε οποιοδήποτε στοιχείο του με ένα απλό indexing: array[i].

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

Ας πάρουμε για παράδειγμα έναν πίνακα από int και μια λίστα από int, για να τα συγκρίνουμε.

Για τον πίνακα, ας κάνουμε μια πρόβλεψη για χωρητικότητα 100 στοιχείων, οπότε θα τον ορίζαμε κάπως έτσι:
Κώδικας: Επιλογή όλων

int main( void )
{
int array[100];
...
return 0;
}

Αν αντί για πίνακα επιλέξουμε την υλοποίηση σε λίστα, δεν χρειάζεται να κάνουμε πρόβλεψη για τη μέγιστη χωρητικότητά της, αλλά πρέπει να ορίσουμε μια δομή (struct) που θα αντιστοιχεί στον κάθε της κόμβο (στοιχείο). Κατόπιν πρέπει να ορίσουμε την αρχή της λίστας ως μια μεταβλητή που θα είναι δείκτης στο struct μας (την μεταβλητή συνήθως την ονομάζουμε list, head ή root). Επειδή αρχικά η λίστα μας είναι άδεια, αρχικοποιούμε τη μεταβλητή μας σε NULL.

Για παράδειγμα:
Κώδικας: Επιλογή όλων

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

int main( void )
{
struct node *list;
list = NULL;
...
return 0;
}

Μια παρένθεση εδώ, σχετικά με δυνατότητα της C για ορισμό πρόσθετων τύπων που θα μας εξυπηρετήσει ώστε να μη γράφουμε συνέχεια τη λέξη struct. Στη C λοιπόν με την εντολή typedef μπορούμε να ορίζουμε δικούς μας τύπους δεδομένων ή να δημιουργούμε συνώνυμα για τους στάνταρ τύπους δεδομένων. Το πιο κλασικό παράδειγμα της δεύτερης περίπτωσης, είναι ο ορισμός του πρόσθετου τύπου "bool" για μεταβλητές που χρησιμοποιούνται ως boolean flags, που ως τύπος είναι απλά ένα συνώνυμο του κλασικού τύπου int...

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

typedef int bool;
bool x = 0; // ισοδυναμεί με: int x = 0;

Όταν ο πρόσθετος τύπος θέλουμε να είναι ένα struct (ή ένα enum ή ένα union) τότε μπορούμε να συγχωνεύσουμε σε ένα και μόνο βήμα τον ορισμό του struct και την δήλωσή του ως πρόσθετο τύπο, ως εξής:
Κώδικας: Επιλογή όλων

typedef struct node {
int value;
struct node *next;
}Node;
(μπορούμε και με 2 ξεχωριστά βήματα: 1. struct node { int value; struct node *next; }; 2. typedef struct node Node; αλλά ο συγχωνευμένος τρόπος είναι ο πιο συνηθισμένος).

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

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

int main( void )
{
Node *list;
list = NULL;
...
return 0;
}


Σε αυτό το σημείο, με την 1η υλοποίηση έχουμε ορίσει έναν ΑΔΕΙΟ πίνακα 100 στοιχείων τύπου int, ενώ με τη 2η υλοποίηση έχουμε ορίσει μια ΑΔΕΙΑ λίστα στοιχείων τύπου int (βασικά τύπου δομής struct node ή απλώς τύπου Node, αλλά εφόσον μιλάμε για λίστες είναι αυτονόητο πως το κάθε στοιχείο είναι δομή και μάλιστα με ένα πεδίο *next που δείχνει στο επόμενο στοιχείο, οπότε είναι θεμιτό να πούμε πως η λίστα είναι τύπου int, λόγω του πεδίου value που χαρακτηρίζει τα δεδομένα των κόμβων της).

Για να βάλουμε την τιμή πχ 52 στο 1ο στοιχείο του array, γράφουμε απλώς:
Κώδικας: Επιλογή όλων

int array[100];

array[0] = 52;

Για να βάλουμε όμως την τιμή 52 στο 1ο στοιχείο της λίστας δεν είναι τόσο απλά τα πράματα!

Καταρχήν δεν υπάρχει καν δημιουργημένο 1ο στοιχείο! Απλά έχουμε έναν δείκτη list τύπου Node που δείχνει στο κενό (NULL). Άρα πρώτα πρέπει να δεσμεύσουμε στη μνήμη τόσο χώρο όσο χρειάζεται ένα στοιχείο της λίστας για τα δυο πεδία του (value και *next). Αυτό το κάνουμε με την στάνταρ συνάρτηση malloc() ή την πιο ασφαλή εκδοχή της, την calloc(). Αφού δημιουργήσουμε στη μνήμη χώρο για το 1ο στοιχείο, θα βάλουμε τιμές στα δυο πεδία του και κατόπιν θα βάλουμε τον δείκτη list να δείχνει σε αυτό το στοιχείο..
Κώδικας: Επιλογή όλων

Node *list, *newnode;

list = NULL;

newnode = calloc(1, sizeof(Node) ); // ισοδυναμεί με: newnode = calloc(1, sizeof(struct node) );
newnode->value = 52;
newnode->next = NULL;

list = newnode;

...
free( list ); // αποδέσμευση της μνήμης του 1ος και μοναδικού στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας

Στην προκειμένη περίπτωση επειδή μιλάμε για ένα και μόνο στοιχείο, και συγκεκριμένα το 1ο στοιχείο, θα μπορούσαμε να κάνουμε απευθείας τη διαδικασία με τον list, και να μη χρησιμοποιούσαμε καθόλου τον *newnode.

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

Node *list = NULL; // ισοδυναμεί με: Node *list; list = NULL;

list = calloc(1, sizeof(Node) ); // ισοδυναμεί με: list = calloc(1, sizeof(struct node) );
list->value = 52;
list->next = NULL;

...
free( list ); // αποδέσμευση της μνήμης του 1ος και μοναδικού στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας


Όταν όμως εισαγάγουμε στοιχεία στη μέση ή στο τέλος μιας λίστας που ΔΕΝ είναι κενή, τότε είναι απαραίτητο να χρησιμοποιήσουμε 2η μεταβλητή (την *newnode στο παραπάνω παράδειγμα) ... θα το δούμε παρακάτω. Προς το παρόν κρατήστε πως ο list πρέπει ΑΠΑΡΑΙΤΗΤΑ να δείχνει στην ΑΡΧΗ της λίστας μας ανά πάσα στιγμή. Αν τον μετακινήσουμε π.χ στο 2ο στοιχείο μετά δεν έχουμε τρόπο να γυρίσουμε πίσω, γιατί στη δομή του κάθε στοιχείου ΔΕΝ υπάρχει δείκτης προς το πίσω, μόνο προς τα εμπρός (ο *next).

Ας υποθέσουμε τώρα πως θέλουμε να προσθέσουμε την τιμή 62 στο 2ο στοιχείο της λίστας μας (στον πίνακα θα γράφαμε απλώς: array[1] = 62;). Η λογική είναι σχεδόν η ίδια με προηγουμένως. Θα δημιουργήσουμε χώρο στη μνήμη για το νέο στοιχείο με τη χρήση ενός δείκτη *newnode, θα του βάλουμε τιμές στα πεδία value και *next και κατόπιν θα βάλουμε το πεδίο *next του 1ου στοιχείου της λίστας να δείχνει στο νέο στοιχείο που δημιουργήσαμε. Η διαφορά με πριν είναι πως αυτή τη φορά θα πρέπει ο list->next να δείχνει στον newnode (και όχι ο list, αυτός δείχνει στο 1ο στοιχείο της λίστας, το 52).

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

Node *list = NULL, *newnode = NULL;

// δημιουργία και εισαγωγή του 1ου στοιχείου
list = calloc(1, sizeof(Node) ); // ισοδυναμεί με: list = calloc(1, sizeof(struct node) );
list->value = 52;
list->next = NULL;

// δημιουργία και εισαγωγή του 2ου στοιχείου
newnode = calloc(1, sizeof(Node) ); // ισοδυναμεί με: newnode = calloc(1, sizeof(struct node) );
newnode->value = 62;
newnode->next = NULL;
list->next = newnode;

...
free( list->next ); // αποδέσμευση της μνήμης του 2ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας
free( list ); // αποδέσμευση της μνήμης του 1ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας


Ο ολοκληρωμένος κώδικας για τη δημιουργία μιας λίστας και εισαγωγή των 2 πρώτων στοιχείων της, καθώς και τύπωμά τους στην οθόνη με ένα while-loop που χρησιμοποιεί έναν βοηθητικό δείκτη (dummy) για να διατρέξει τη λίστα είναι ο εξής:
Κώδικας: Επιλογή όλων

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

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

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

// δημιουργία και εισαγωγή του 1ου στοιχείου
list = calloc(1, sizeof(Node) ); // ισοδυναμεί με: list = calloc(1, sizeof(struct node) );
list->value = 52;
list->next = NULL;

// δημιουργία και εισαγωγή του 2ου στοιχείου
newnode = calloc(1, sizeof(Node) ); // ισοδυναμεί με: newnode = calloc(1, sizeof(struct node) );
newnode->value = 62;
newnode->next = NULL;
list->next = newnode;

// τύπωμα της λίστας με χρήση βοηθητικού δείκτη: dummy
Node *dummy = list;
while ( dummy != NULL ) {
printf("%d ", dummy->value);
dummy = dummy->next;
}

free( list->next ); // αποδέσμευση της μνήμης του 2ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας
free( list ); // αποδέσμευση της μνήμης του 1ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας

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



Δύο σημαντικές επισημάνσεις:

α) Στο while loop που τυπώνει τη λίστα, χρησιμοποιούμε έναν βοηθητικό δείκτη (dummy) που αρχικά τον βάζουμε να δείχνει στην αρχή της λίστας (dummy = list) και κατόπιν μέσα στο loop, αφού πρώτα τυπώσουμε την τιμή value τον μετακινούμε στον επόμενο κόμβο (dummy = dummy->next), μέχρι να βρει NULL. Θα μπορούσα αντί για τον dummy να χρησιμοποιούσα τον newnode για αυτή τη δουλειά, αφού από τη στιγμή που η δομή του newnode εισήχθη στο τέλος της λίστας μας, ο δείκτης αυτός κάθε αυτός δεν έχει πλέον λόγο ύπαρξης (διότι η δομή του 2ου στοιχείου είναι προσβάσιμη από τον δείκτη next του 1ου στοιχείου). Όρισα όμως νέο δείκτη, τον dummy, για να μην μπερδευτείτε... να είναι πιο ξεκάθαρο.

Γιατί δεν χρησιμοποίησα απευθείας τον list για να διατρέξω τη λίστα; Π.χ...
Κώδικας: Επιλογή όλων

while ( list != NULL ) {
printf("%d ", list->value);
list = list->next;
}


Διότι έτσι στο τέλος του loop ο δείκτης list αντί να δείχνει στην αρχή της λίστας θα έδειχνε στο τέλος της (που είναι NULL δλδ). Οπότε μετά πως θα αποδέσμευα παρακάτω με τη συνάρτηση free() τον χώρο του 1ου και του 2ου στοιχείου, πριν τερματίσω το πρόγραμμα; Δεν θα είχα τρόπο να ξαναγυρίσω στην αρχή της λίστας!

β) Για κάθε πετυχημένο malloc() ή calloc() που κάνουμε, πρέπει ΟΠΩΣΔΗΠΟΤΕ να κάνουμε ένα αντίστοιχο free() πριν τερματίσουμε το πρόγραμμά μας, για να αποδεσμεύσουμε το χώρο μνήμης που έχουμε δεσμεύσει πριν επιστρέψουμε τον έλεγχο στο λειτουργικό σύστημα. Και μάλιστα, για να μην καλούμε άδικα τα free() πρέπει σε κάθε μας malloc() ή calloc() να ελέγχουμε αν πέτυχαν.

Οπότε ο σωστός κι ασφαλής κώδικας είναι ο εξής:
Κώδικας: Επιλογή όλων

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

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

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

// δημιουργία και εισαγωγή του 1ου στοιχείου
list = calloc(1, sizeof(Node) ); // ισοδυναμεί με: list = calloc(1, sizeof(struct node) );
if ( !list ) // ισοδυναμεί με: if (list == NULL )
{
puts("*** out of memory, aborting program");
exit(1);
}

list->value = 52;
list->next = NULL;

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

// τύπωμα της λίστας με χρήση βοηθητικού δείκτη: dummy
Node *dummy = list;
while ( dummy ) // ισοδυναμεί με: while (dummy != NULL )
{
printf("%d ", dummy->value);
dummy = dummy->next;
}

free( list->next ); // αποδέσμευση της μνήμης του 2ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας
free( list ); // αποδέσμευση της μνήμης του 1ου στοιχείου της λίστας, πριν τερματίσουμε το πρόγραμμά μας

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


Ο παραπάνω κώδικας είναι ΑΧΡΗΣΤΟΣ στην πράξη, αφού το μόνο που κάνει είναι να δημιουργεί μια λίστα, να τις βάζει 2 στοιχεία, να την τυπώνει και να την καταστρέφει πριν τερματίσει το πρόγραμμα.

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

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

Το αν οι νέοι κόμβοι θα εισαγάγονται στην αρχή ή στο τέλος της λίστας είναι μια απόφαση που θα πρέπει να πάρουμε εμείς, ανάλογα με τις ανάγκες του project μας. Μπορεί μάλιστα να μας εξυπηρετεί να εισαγάγουμε τους νέους κόμβους ΑΠΕΥΘΕΙΑΣ ΤΑΞΙΝΟΜΗΜΕΝΟΥΣ στη σωστή τους θέση, δηλαδή είτε στην αρχή, είτε στη μέση, είτε στο τέλος της λίστας, είτε σε αύξουσα είτε σε φθίνουσα σειρά (αυτό μάλιστα αποτελεί κι ένα από τα σημαντικά πλεονεκτήματα των λιστών έναντι των πινάκων, στην ταξινομημένη δηλαδή εισαγωγή των στοιχείων).

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

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

Απορίες, επισημάνσεις, διορθώσεις, κλπ πάντα ευπρόσδεκτα!

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

ΕΙΚΟΝΑ 1η - Όπου: Data εμείς έχουμε: value, κι όπου: Pointer εμείς έχουμε: next
Εικόνα

ΕΙΚΟΝΑ 2η - Όπου: Head εμείς έχουμε: list
Εικόνα
Τελευταία επεξεργασία από migf1 και 22 Ιούλ 2011, 10:49, έχει επεξεργασθεί 6 φορά/ες συνολικά
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

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

Δημοσίευσηαπό linuxs » 28 Ιουν 2011, 21:31

Δεν μπορώ!!!! Θα το σχολιάσω...ΕΙΣΑΙ ΑΠΙΘΑΝΟΣ!!!!!
Αν το πρόβλημά μας επιλυθεί. Επιλέγουμε το θέμα που βοήθησε στην επίλυση και πατάμε το κουμπάκι Εικόνα.
Γνώσεις ⇛ Linux: Μέτριο┃Προγραμματισμός: C┃Αγγλικά: Καλά
Λειτουργικό ⇛ Linux Ubuntu 10.4 LTS
Προδιαγραφές ⇛ Intel Pentium @T4500 2.3GHz│ 512GB VRAM│ 500 HDD│ ATI RADEON HD545v 512 MB │ Screen: 15.6''
Άβαταρ μέλους
linuxs
daemonTUX
daemonTUX
 
Δημοσιεύσεις: 1060
Εγγραφή: 02 Ιούλ 2010, 13:19
Τοποθεσία: GR
IRC: linuxs
Εκτύπωση

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

Δημοσίευσηαπό Star_Light » 29 Ιουν 2011, 02:10

Μάγος! Ο Μάγος της C!!!!! Eχω αραξει εδω και διαβαζω τον κωδικα... ομολογω πως βρηκα και το λαθος που εκανα... Ουσιαστικα επαιρνα αυτην εδω την περιπτωση

http://students.ceid.upatras.gr/~perisi ... slides.htm

αλλα με βάση τις εξηγησεις του migf1 ειμαι οκ! ΑΝ και εχω διαβασει ακομη το μισο ποστ του..... φευγω να συνεχισω!

Φιλε migf1 ευχαριστουμε για ολα

Π.Σ Θα μου επιτρεψετε να δωσω και εγω το δικο μου τιπ και συμπερασμα.... Τελικα η μεγαλυτερη πηγη λαθων στην C εγκειται οταν δεν γνωριζεις 100% τι κάνεις. Αν δεν το γνωριζεις τοτε αυτο θα ειναι η μεγαλύτερη αιτία εμφάνισης λαθών!
Γνώσεις ⇛ 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 - ΔΕΙΚΤΕΣ

Δημοσίευσηαπό linuxs » 29 Ιουν 2011, 02:52

φυσικα και αυτό ξεκινά απο την στιγμή που δεν κατάλαβες μια μικρή λεπτομέρια στην εκφώνηση. μιλάω γενικά όχι για σένα... :D
Αν το πρόβλημά μας επιλυθεί. Επιλέγουμε το θέμα που βοήθησε στην επίλυση και πατάμε το κουμπάκι Εικόνα.
Γνώσεις ⇛ Linux: Μέτριο┃Προγραμματισμός: C┃Αγγλικά: Καλά
Λειτουργικό ⇛ Linux Ubuntu 10.4 LTS
Προδιαγραφές ⇛ Intel Pentium @T4500 2.3GHz│ 512GB VRAM│ 500 HDD│ ATI RADEON HD545v 512 MB │ Screen: 15.6''
Άβαταρ μέλους
linuxs
daemonTUX
daemonTUX
 
Δημοσιεύσεις: 1060
Εγγραφή: 02 Ιούλ 2010, 13:19
Τοποθεσία: GR
IRC: linuxs
Εκτύπωση

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

Δημοσίευσηαπό Star_Light » 29 Ιουν 2011, 03:36

linuxs έγραψε:φυσικα και αυτό ξεκινά απο την στιγμή που δεν κατάλαβες μια μικρή λεπτομέρια στην εκφώνηση. μιλάω γενικά όχι για σένα... :D


Και για μενα να μιλουσες θα ειχες δικιο φιλε μου. Έχω ακομη πολυ δουλεια μπροστα μου.
Αλλα ευτυχως εχω ορεξη!
Γνώσεις ⇛ 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 » 29 Ιουν 2011, 05:08

Κώδικας: Επιλογή όλων
#include<stdio.h>
#include<stdlib.h>

typedef struct
{
int id;
struct customer *next;
} customer;

int main ()
{
customer *head , *one;
customer *two;

head = NULL;

{
head= calloc(1,sizeof(customer));
head->id=1;
head->next = NULL;
}

one= calloc(1,sizeof(customer));
one->id=2;
one->next=NULL;
head->next=one;

two=calloc(1,sizeof(customer));
two->id=3;
two->next=NULL;
one->next=two;


customer *helper=head;

while(helper!=NULL)
{
printf("%d %d %d",head->id , one->id , two->id);
helper=helper->next;
}

free(head);
free(head->next);
free(one->next);

if(head->id)
printf("H free dn ekane kala tin douleia tis \n");
else
printf("H mnimi eleutherwthike!");


return 0;
}



Να και ο δικος μου! Απλα εμφανιζει λογικο σφαλμα δηλαδη τυπωνει το 1 2 3 (3 φορες συνολικα)

1 2 3 1 2 3 1 2 3
Κατα τα αλλα μου βγαζει μηνυμα οτι η μνημη ελευθερωθηκε επιτυχως! Ελπιζω αυτη τη φορα.... να τα πηγα καλα!
το one->next=two; που εχω βαλει ισως ειναι λαθος... αλλα ετσι το καταλαβαινα τελεια ρε γ*****!
Δοκιμασα να βαλω head->net=two; και οντως μου τα εβγαλε 2 φορες 1 2 3 1 2 3 (οχι 3 οπως πριν)

EDIT: Λύθηκε αλλάζοντας μεσα στην while την printf ετσι

Κώδικας: Επιλογή όλων
printf("%d \n",helper->id);


;)
Γνώσεις ⇛ 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 » 29 Ιουν 2011, 13:20

Καλημέρα παιδιά, 1000 ευχαριστώ για τα καλά σας λόγια. Είναι χαρά μου να βοηθάω όπου μπορώ.

Φίλε Strarlight, ο κώδικάς σου έχει προβληματάκια:

1. στον ορισμό του struct customer πρέπει να βάλεις και τη λέξη customer στην 1η γραμμή, ώστε να ξέρει μέσα στο σώμα του σε τι τύπο αναφέρεται ο δείκτης next. Έτσι όπως το έχεις τώρα:

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

typedef struct { // <--- λάθος, πρέπει: typedef struct customer {
int id;
struct customer *next;
} customer;

ουσιαστικά ο τύπος του *next είναι άγνωστος (άμα δεις ο gcc σου βγάζει warnings όταν πας να εκχωρήσεις στον δείκτη ->next ένα άλλον δείκτη τύπου customer). Επίσης, είναι καλή πρακτική τους πρόσθετους τύπους να τους γράφεις με κεφαλαίο το 1ο τους γράμμα, οπότε το παραπάνω γίνεται:

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

typedef struct customer {
int id;
struct customer *next;
} Customer;


2. Όταν πας να τυπώσεις την λίστα σου, την διατρέχεις σωστά με έναν βοηθητικό δείκτη: helper, αλλά μέσα στο loop αντί να τυπώνεις μόνο το id του τρέχοντος κόμβου (helper->id), εσύ το βάζεις να τυπώσει τα id και των 3 κόμβων της λίστας, σε ένα printf. Και μάλιστα δεν τυπώνεις καν το id του εκάστοτε helper, αλλά κάνεις απευθείας πρόσβαση στους: head, one και two...

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

while(helper!=NULL)
{
printf("%d %d %d",head->id , one->id , two->id); // <-- λάθος, πρέπει: printf("%d , helper->id);
helper=helper->next;
}

Οπότε θα πρέπει είτε να διατρέξεις τη λίστα με loop και να τυπώνεις το id του εκάστοτε τρέχοντος κόμβου μέσα στο loop...

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

// τύπωμα της λίστας με loop
Customer *helper = head;
while ( helper != NULL )
{
printf("%d ", helper->id);
helper = helper->next;
}

... είτε να τυπώσεις απευθείας σε ένα printf τα id των head, one και two χωρίς loop (αν και αυτό δεν έχει νόημα, γιατί π.χ. αν έχουμε 100 στοιχεία στη λίστα δεν υπάρχει περίπτωση να τα τυπώσουμε με αυτό τον τρόπο)...
Κώδικας: Επιλογή όλων
printf("\n%d %d %d\n", head->id, one->id , two->id); //<--- μη χρήσιμος τρόπος (απλά για δοκιμή)


3. Όταν απελευθερώνεις τη μνήμη για τους κόμβους...
Κώδικας: Επιλογή όλων

free(head);
free(head->next); // <--- λάθος, το head είναι ήδη κατεστραμμένο
free(one->next);

κάνεις το σφάλμα να απελευθερώνεις πρώτα τον κόμβο στον οποίον δείχνει ο head και κατόπιν να τον χρησιμοποιείς ξανά (ενώ έχει ήδη απελευθερωθεί) για να απελευθερώσεις το head->next. Ο λόγος που ο compiler δεν σου βγάζει error είναι επειδή η free() δεν κάνει έλεγχο εγκυρότητας για το όρισμα που της περνάς (θεωρητικά αν είναι άκυρο ή NULL δεν κάνει τίποτα, αλλά η υλοποίησή της εξαρτάται από τον compiler). Όταν τρέχεις το παραπάνω εκτελέσιμο αρχείο δεν σου βγάζει segmentation-fault επειδή ο gcc έχει έναν ας τον πούμε εσωτερικό μηχανισμό προστασίας για κάποιες περιπτώσεις. Αν αυτό τον κώδικα τον κάνεις compile σε άλλον compiler το εκτελέσιμο αρχείο πιθανότατα θα βαρέσει seg-fault όταν θα πάει να εκτελέσει το: free( head->next ). Όπως και να χει πάντως είναι λανθασμένο!

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

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

free(one->next);
free(head->next);
free(head);

ή ακόμα καλύτερα...

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

free( head->next->next );
free( head->next );
free( head );


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

4. Μετά την απελευθέρωση των κόμβων έχεις μια συνθήκη if-else προκειμένου να ελέγξεις αν πέτυχε η απελευθέρωση μνήμης....

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

if (head->id) // <-- λάθος
printf("H free dn ekane kala tin douleia tis \n");
else
printf("H mnimi eleutherwthike!");

Εδώ υπάρχουν 2 προβλήματα, το σοβαρότερο εκ των οποίων είναι πως και πάλι προσπαθείς να χρησιμοποιήσεις έναν κατεστραμμένο κόμβο (από το προηγούμενο free) και συγκεκριμένα προσπαθείς να χρησιμοποιήσεις το πεδίο: id του κατεστραμμένου κόμβου (head->id). Το επόμενο πρόβλημα είναι πως το πεδίο id είναι τύπου int, οπότε ακόμα και αν ο κόμβος head υπήρχε, η συνθήκη: if ( head->id ) συγκρίνει την int τιμή του id με το 0 για να αποφασίσει αν απελευθερώθηκε ή όχι η μνήμη;

Υποθέτω πως εννοούσες: if (head == NULL). Πάντως δεν είναι καλή ιδέα να συγκρίνεις τον δείκτη ενός κόμβου που έχεις ήδη καταστρέψει με το free(). Προφανώς ούτε να τον χρησιμοποιείς. Για να τον ξαναχρησιμοποιήσει ως κόμβο θα πρέπει να κάνεις εκ νέου malloc()/calloc().

Να πω και κάτι τελευταίο, που έχω την εντύπωση πως δεν έχεις ξεκαθαρίσει στο μυαλό σου. Μπορεί να κάνω και λάθος, αλλά θα το πω καλού-κακού μιας και το φόρουμ το διαβάζει πολύς κόσμος.

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

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

struct coordinates {
int x;
int y;
};

struct coordinates pos; // position

Για να βάλουμε τιμές στα πεδία x και y της μεταβλητής pos, γράφουμε...
Κώδικας: Επιλογή όλων

pos.x = 100;
pos.y = 200;

Αν όμως την μεταβλητή την ορίσουμε να είναι δείκτης, τότε αντί για τελεία χρησιμοποιούμε -> ...
Κώδικας: Επιλογή όλων

struct coordinates {
int x;
int y;
};

struct coordinates *pos; // position

pos = calloc(1, sizeof( struct coordinates) );
// εδώ κανονικά θέλει κι έλεγχο αν πέτυχε το calloc()... if (pos != NULL)

pos->x = 100;
pos->y = 200;

...
free( pos );

Άρα λοιπόν, στις λίστες (και οπουδήποτε) το σύμβολο -> δεν δείχνει στον επόμενο κόμβο, αλλά αναφέρεται σε ένα πεδίο του τρέχοντος κόμβου ;)

Το παραπάνω παράδειγμα είναι ιδανικό για να ξεκαθαρίσει κανείς κι ένα ακόμα σημείο: πως όταν δήλωσα τη μεταβλητή pos ως δείκτη ΔΕΝ επιχείρησα να περάσω απευθείας τιμές στα πεδία της (κάτι τέτοιο θα μου έδινε segmentation fault). Πρώτα έφτιαξα χώρο στη μνήμη, βάζοντας τον δείκτη pos να δείχνει σε εκείνο το χώρο (pos = calloc( ... ) )
και ΜΕΤΑ έβαλα τιμές στα πεδία του, με τον συμβολισμό των δεικτών ( -> ).

Το ίδιο συμβαίνει και με τις λίστες (και παντού). Ο δείκτης head ΔΕΝ είναι κόμβος ο ίδιος, είναι ένας δείκτης που δείχνει σε έναν κόμβο (στον 1ο της λίστας εν προκειμένω) που πρέπει πρώτα να έχει δημιουργηθεί στη μνήμη. Αυτό το κάνει πιο ξεκάθαρο η 2η από τις εικόνες που πόσταρα στο προηγούμενο post.

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

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

Δημοσίευσηαπό migf1 » 29 Ιουν 2011, 18:57

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

Λίστα ως Όρισμα Αναφοράς (by Reference) & ως Όρισμα Τιμής (by Value)
ΣΥΝΑΡΤΗΣΕΙΣ: list_print, list_length, list_destroy



Σε συνέχεια του 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 διαφορετικούς τρόπους για εισαγωγή νέων στοιχείων στη λίστα, μέσω αντίστοιχων συναρτήσεων (με κώδικα προφανώς εκτός από σχόλια).
Τελευταία επεξεργασία από migf1 και 22 Ιούλ 2011, 10:20, έχει επεξεργασθεί 4 φορά/ες συνολικά
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

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

Δημοσίευσηαπό migf1 » 30 Ιουν 2011, 17:09

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

Προσθήκη Νέων Κόμβων στην Αρχή της Λίστας
ΣΥΝΑΡΤΗΣΗ: list_prepend



Στο 3ο και 4ο μέρος θα προσπαθήσω να εξηγήσω μερικές υλοποιήσεις συναρτήσεων για εισαγωγή νέων κόμβων στη λίστα μας. Υπάρχουν διάφοροι τρόποι και μάλιστα διάφορες υλοποιήσεις για κάθε τρόπο. Τα πράγματα μπορούν να γίνουν εξαιρετικά σύνθετα, ανάλογα την υλοποίηση και ανάλογα τις ανάγκες του εκάστοτε project.

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

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

Ας ονομάσουμε αυτή τη συνάρτηση: list_prepend( Node *list, int value ) και ας δούμε καταρχήν κώδικα που ΔΕΝ δουλεύει, λόγω του περάσματος της λίστας by value μέσα στη συνάρτηση. Αλγοριθμικά όμως θα είναι σωστή, οπότε αμέσως μετά απλά θα αλλάξουμε το πέρασμα του ορίσματος από by value σε by reference και θα δουλεύει. Θα δούμε κατόπιν και ένα συνηθισμένο "κολπάκι" με το οποίο μπορούμε να περνάμε by value τη λίστα και να δουλεύει η εισαγωγή.

Ξεκινάμε με τον κώδικα που ΔΕΝ δουλεύει...

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

/*
* Εισαγωγή νέου κόμβου στην αρχή της λίστας BY VALUE
* !!! ΔΕΝ ΔΟΥΛΕΥΕΙ !!!
*/

// -------------------------------------------------------
void list_prepend( Node *list, int value )
{
Node *new = calloc(1, sizeof(Node) );
if ( !new )
return;

new->value = value;
new->next = list;
list = new;

return;
}

// -------------------------------------------------------
int main( void )
{
Node *list = NULL;
...
list_prepend( list, 52 );
...
return 0;
}

Καταρχήν η αλγοριθμική σκέψη είναι η σωστή:
  1. δημιουργούμε πρώτα χώρο στη μνήμη για ένα νέο κόμβο, στον οποίον βάζουμε να δείχνει ο τοπικός δείκτης new: new = calloc(1, sizeof(Node) );
  2. σε περίπτωση που η δέσμευση μνήμης αποτύχει, τερματίζουμε την συνάρτηση χωρίς να κάνουμε τίποτα
  3. εκχωρούμε την τιμή του ορίσματος value στο αντίστοιχο πεδίο του νέου κόμβου
  4. για να κάνουμε τον νέο κόμβο αρχή της λίστας, βάζουμε το πεδίο next του νέου κόμβου να δείχνει όπου δείχνει ο δείκτης list (που είναι βασικά η προηγούμενη αρχή της λίστας)
  5. και τέλος ενημερώνουμε τον δείκτη list να δείχνει στον νέο κόμβο, και τερματίζουμε την συνάρτηση
Το πρόβλημα είναι πως όταν τερματίσει η συνάρτηση κι επιστρέψει η ροή στην main() ο αυθεντικός δείκτης list δεν δείχνει στον νέο κόμβο, αλλά δείχνει εκεί που έδειχνε και πριν καλέσουμε τη συνάρτηση. Ο λόγος είναι πως στη συνάρτηση δεν περάστηκε ως όρισμα ο αυθεντικός δείκτης list, αλλά ένα αντίγραφό του το οποίο (όπως εξηγήσαμε στο 2ο μέρος) διαγράφεται έτσι κι αλλιώς μόλις τερματίσει η συνάρτηση.

Όπως εξηγήσαμε στο 2ο μέρος, στη C όλα τα ορίσματα συναρτήσεων περνιούνται στις συναρτήσεις by value, δηλαδή ως προσωρινά αντίγραφα των αυθεντικών μεταβλητών. Για να μπορέσει ένα όρισμα να διατηρήσει τυχόν αλλαγές που του κάνει η συνάρτηση, θα πρέπει να περαστεί by reference. Αυτό γίνεται περνώντας στη συνάρτηση τη διεύθυνση μνήμης της αυθεντικής μεταβλητής και κατόπιν μέσα στον κώδικα της συνάρτησης δηλώνοντας και χρησιμοποιώντας τον εν λόγω όρισμα ως δείκτη.

Για να το καταλάβουμε καλύτερα, ας δούμε ένα απλό παράδειγμα με μια αυθεντική μεταβλητή τύπου int...

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

/*
* ΠΑΡΑΔΕΙΓΜΑ PASS BY VALUE
*/

void test( int x )
{
x = 10;
return;
}

int main( void )
{
int x = 0;
test( x );
printf("%d", x);

return 0;
}

Στον παραπάνω κώδικα το printf θα τυπώσει 0 (και όχι 10) διότι η αλλαγή του x από 0 σε 10 μέσα στη συνάρτηση έγινε σε ένα προσωρινό αντίγραφο του x, και όχι στο αυθεντικό x της main()... pass by value.

Για να διατηρηθεί στο x η αλλαγή που του κάνει η συνάρτηση test θα πρέπει να της περάσουμε το χ by reference, δηλαδή ως δείκτη στη διεύθυνση μνήμης του αυθεντικού x. Δείτε τον τροποποιημένο κώδικα...

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

/*
* ΠΑΡΑΔΕΙΓΜΑ PASS BY REFERENCE
*/

void test( int *x )
{
*x = 10;
return;
}

int main( void )
{
int x = 0;
test( &x );
printf("%d", x);

return 0;
}

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

Άρα λοιπόν, για να επιστρέψουμε στον προβληματικό κώδικα της συνάρτησης: list_prepend( list, 52) το μόνο που χρειάζεται να κάνουμε για να δουλεύει σωστά, είναι το όρισμα list να το περάσουμε by reference (αντί για by value που είναι τώρα).

Επειδή είναι όμως ήδη δείκτης σε τύπο Node ( Node *list ), πρέπει να το περάσουμε ως δείκτη σε δείκτη τύπου Node... δηλαδή: Node **list. Κατόπιν, μέσα στον κώδικα της συνάρτησης θα πρέπει να τον διαχειριστούμε ως τέτοιον, άρα όπου τον γράφουμε τώρα ως list θα τον γράφουμε ως: *list ή ως (*list) για πιο σίγουρα. Και τέλος, όταν καλούμε στην main() τη συνάρτηση list_prepend() θα της περνάμε ως 1ο όρισμα τη διεύθυνση μνήμης του list, δηλαδή: list_prepend( &list, 52 ).

Ορίστε ο τροποποιημένος κώδικας που δουλεύει...

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

/*
* Εισαγωγή νέου κόμβου στην αρχή της λίστας BY REFERENCE
* !!! ΔΟΥΛΕΥΕΙ ΑΛΛΑ ΔΕΝ ΕΧΟΥΜΕ ΕΝΔΕΙΞΗ ΕΠΙΤΥΧΙΑΣ ή ΑΠΟΤΥΧΙΑΣ !!!
*/

// -------------------------------------------------------
void list_prepend( Node **list, int value )
{
Node *new = calloc(1, sizeof(Node) );
if ( !new )
return;

new->value = value;
new->next = (*list);
(*list) = new;

return;
}

// -------------------------------------------------------
int main( void )
{
Node *list = NULL;
...
list_prepend( &list, 52 );
...
return 0;
}

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

Αντί λοιπόν να την έχουμε void, που δεν επιστρέφει τίποτα, θα την κάνουμε να επιστρέφει έναν int που θα είναι 0 σε περίπτωση σφάλματος και 1 αν όλα εξελίχθηκαν ωραία και καλά. Και για να είναι ξεκάθαρο πως αυτό που επιστρέφει δεν είναι απλά ένας τυχαίος int αλλά μια boolean τιμή FALSE ή TRUE, θα ορίσουμε με typedef έναν πρόσθετο τύπο, που θα τον ονομάσουμε Bool και θα ορίσουμε ως πιθανές τιμές του πρόσθετου τύπου Bool, τις τιμές FALSE και TRUE, που θα αντιστοιχούν στις τιμές 0 και 1, αντίστοιχα.

Ένας λίγο ασυνήθιστος αλλά ενδεδειγμένος τρόπος να κάνουμε κάτι τέτοιο στη C είναι συνδυάζοντας τις εντολές typedef και enum (enumerated types) ως εξής:

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

typedef enum { FALSE=0, TRUE } Bool; // το TRUE ισούται με 1 επειδή έπεται αμέσως μετά το FALSE που το ορίσαμε να ισούται με 0

Οπότε, ο τροποποιημένος κώδικας του προηγούμενου παραδείγματος είναι ο εξής...

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

typedef enum { FALSE=0, TRUE } Bool;

/*
* Εισαγωγή νέου κόμβου στην αρχή της λίστας BY REFERENCE
* !!! ΔΟΥΛΕΥΕΙ ΚΑΝΟΝΙΚΑ ΚΑΙ ΕΠΙΣΤΡΕΦΕΙ TRUE ή FALSE !!!
*/

// -------------------------------------------------------
Bool list_prepend( Node **list, int value )
{
Node *new = calloc(1, sizeof(Node) );
if ( !new )
return FALSE;

new->value = value;
new->next = (*list);
(*list) = new;

return TRUE;
}

// -------------------------------------------------------
int main( void )
{
Node *list = NULL;
...
if ( list_prepend( &list, 52 ) == FALSE ) {
// εδώ γράφουμε κώδικα που διαχειρίζεται την αποτυχία εισαγωγής
// συνήθως τυπώνουμε μήνυμα σφάλματος, αποδεσμεύουμε τυχόν προηγούμενα
// δεσμευμένους κόμβους της λίστας (π.χ. με: list_destroy( list ) και
// τερματίζουμε βίαια το πρόγραμμα (π.χ. με: exit(EXIT_FAILURE); ή return EXIT_FAILURE; )
}
...
return EXIT_SUCCESS;
}

Ρισκάροντας να σας μπερδέψω λιγάκι τώρα που τα ξεκαθαρίσαμε, αλλά επειδή σας το "υποσχέθηκα", θα σας δείξω μια δεύτερη υλοποίηση της συνάρτησης: list_prepend() κατά την οποία περνάμε την λίστα by value και κατόπιν την αναγκάζουμε να μας επιστρέφει την ανανεωμένη λίστα πριν τερματίσει.

Ο κώδικας είναι σχεδόν ίδιος με την πρώτη-πρώτη υλοποίηση αυτού του post (αυτή που έχω χαρακτηρίσει πως δεν δουλεύει) με τη διαφορά πως αυτή τη φορά θα βάλουμε τη συνάρτηση να επιστρέφει την ανανεωμένη λίστα, άρα ο τύπος επιστροφής της θα αλλάξει από void σε Node *. Για να πάρουμε την ανανεωμένη λίστα στην main() θα πρέπει να εκχωρήσουμε την τιμή επιστροφής της list_prepend() στον αυθεντικό δείκτη list.

Ο κώδικας πιστεύω αποσαφηνίζει καλύτερα την παραπάνω περιγραφή:

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

/*
* Εισαγωγή νέου κόμβου στην αρχή της λίστας BY VALUE
* !!! ΔΟΥΛΕΥΕΙ ΑΝ ΕΚΜΕΤΑΛΛΕΥΤΟΥΜΕ ΤΗΝ ΤΙΜΗ ΕΠΙΣΤΡΟΦΗΣ ΤΗΣ ΣΥΝΑΡΤΗΣΗΣ!!!
* !!! ΕΠΙΣΤΡΕΦΕΙ NULL ΣΕ ΠΕΡΙΠΤΩΣΗ ΑΠΟΤΥΧΙΑΣ
*/

// -------------------------------------------------------
Node *list_prepend( Node *list, int value )
{
Node *new = calloc(1, sizeof(Node) );
if ( !new )
return NULL;

new->value = value;
new->next = list;
list = new;

return list; // μπορούμε κι απευθείας: return new; ενοποιώντας τις 2 τελευταίες γραμμές σε 1
}

// -------------------------------------------------------
int main( void )
{
Node *list = NULL;
...
list = list_prepend( list, 52 );
if ( !list ) { // ισοδυναμεί με: if ( list == NULL ) {
// εδώ γράφουμε κώδικα που διαχειρίζεται την αποτυχία εισαγωγής
// συνήθως τυπώνουμε μήνυμα σφάλματος, αποδεσμεύουμε τυχόν προηγούμενα
// δεσμευμένους κόμβους της λίστας (π.χ. με: list_destroy( list ) και
// τερματίζουμε βίαια το πρόγραμμα (π.χ. με: exit(EXIT_FAILURE); ή return EXIT_FAILURE; )
}
...
return EXIT_SUCCESS;
}

Το "κλειδί" στον παραπάνω κώδικα είναι στην main() στη γραμμή:
Κώδικας: Επιλογή όλων
list = list_prepend( list, 52 );
που επειδή περνάει by value τον αρχικό δείκτη list στην συνάρτηση, εκχωρεί το αλλαγμένο αντίγραφο (αλλαγμένο από τη συνάρτηση) απευθείας στον αυθεντικό δείκτη.

Και φυσικά η παραπάνω εκχώρηση έχει νόημα επειδή αλλάξαμε ελαφρώς τον κώδικα της list_prepend() ώστε αντί για void να επιστρέφει το αλλαγμένο αντίγραφο του list ;)

Προσωπικά προτιμώ την BY REFERENCE εκδοχή της list_prepend() επειδή την θεωρώ πιο "straight forward" και πιο συνεπή ως προς τη στάνταρ χρήση όλων των συναρτήσεων που χρησιμοποιούν ορίσματα by reference. Συνδυασμένη μάλιστα με τιμή επιστροφής FALSE ή TRUE νομίζω είναι και πιο κατανοητή σε όσους προέρχονται από άλλες γλώσσες (με κόστος όμως τον ελαφρώς δυσκολότερο κώδικα λόγω του διπλού δείκτη).

Όμως και η BY VALUE εκδοχή της με επιστροφή του ανανεωμένου αντίγραφου της λίστας είναι πολύ συνηθισμένη υλοποίηση και χρησιμοποιείται κατά κόρον. Εγώ θα συνεχίσω να χρησιμοποιώ τις BY REFERENCE εκδοχές των συναρτήσεων στη συνέχεια αυτού του tutorial, αλλά εννοείται πως κανείς δεν σας εμποδίζει να χρησιμοποιήσετε την BY VALUE αν την καταλαβαίνετε πιο καλά.

Οπότε ο ολοκληρωμένος κώδικας στο τέλος του 2ου μέρους, μπορεί πλέον να γραφτεί ως εξής (με την BY REFERENCE εκδοχή της list_prepend() )...

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

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

typedef enum { FALSE=0, TRUE } Bool; // ο δικός μας πρόσθετος τύπος Boolean

typedef struct node { // δομή του κάθε κόμβου της λίστας
int value;
struct node *next;
} Node;

/* ------------------------------------------------------------------
* Δημιουργία και εισαγωγή νέου κόμβου με τιμή value, στην αρχή της λίστας.
* Επιστρέφει FALSE σε περίπτωση αποτυχίας της calloc(), αλλιώς TRUE
* ------------------------------------------------------------------
*/
Bool list_prepend( Node **list, int value )
{
Node *new = calloc(1, sizeof(Node) );
if ( !new )
return FALSE;

new->value = value;
new->next = (*list);
(*list) = new;

return TRUE;
}

/* ------------------------------------------------------------------
* Μέτρημα κι επιστροφή του πλήθους κόμβων της λίστας
* ------------------------------------------------------------------
*/
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;

// δημιουργία και εισαγωγή του 1ου στοιχείου
if ( list_prepend( &list, 52) == FALSE ) { // αποτυχία
puts("*** out of memory, aborting program");
exit( EXIT_FAILURE);
}

// δημιουργία και εισαγωγή του 2ου στοιχείου
if ( list_prepend( &list, 62) == FALSE) { // αποτυχία
puts("*** out of memory, aborting program");
list_destroy( list) ; // καταστροφή προηγούμενων κόμβων
exit(1);
}

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

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

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

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

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

Προς το παρόν μπορείτε να δείτε τον τελικό κώδικα αυτού του ποστ με syntax-highlighting, δείγμα εξόδου και σύνδεσμο κατεβάσματος εδώ: http://codepad.org/cpRAjH5F
Τελευταία επεξεργασία από migf1 και 22 Ιούλ 2011, 10:22, έχει επεξεργασθεί 4 φορά/ες συνολικά
Go under the hood with C: Pointers, Strings, Linked Lists
Άβαταρ μέλους
migf1
powerTUX
powerTUX
 
Δημοσιεύσεις: 2082
Εγγραφή: 03 Ιουν 2011, 16:32
Εκτύπωση

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

Δημοσίευσηαπό Star_Light » 30 Ιουν 2011, 18:25

Μigf1 μας εστρωσες στο διαβασμα!!! Αποψε θα διαβασω και τα καινουργια σου!
Γνώσεις ⇛ 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
Εκτύπωση

Επόμενο

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