Dans une récente interview de Slashdot Linus Torvalds a donné un exemple de la façon dont certaines personnes utilisent les pointeurs de manière à indiquer qu’ils ne comprennent pas vraiment comment les utiliser correctement.
Malheureusement, comme je suis l'une des personnes dont il parle, j'ai également échoué à comprendre son exemple:
J'ai vu trop de personnes qui suppriment une entrée de liste à lien unique en gardant une trace de l'entrée "prev", puis de supprimer l'entrée en faisant quelque chose comme
if (prev) prev->next = entry->next; else list_head = entry->next;
et chaque fois que je vois un code comme celui-ci, je me contente de dire "Cette personne ne comprend pas les pointeurs". Et c'est malheureusement assez commun. Les gens qui comprendre les pointeurs simplement utiliser un "pointeur sur le pointeur d’entrée", et initialisez cela avec l'adresse de la list_head. Et puis comme ils parcourir la liste, ils peuvent supprimer l’entrée sans utiliser aucun conditionnels, en faisant juste
*pp = entry->next
Quelqu'un peut-il fournir un peu plus d'explications sur les raisons pour lesquelles cette approche est meilleure et comment elle peut fonctionner sans déclaration conditionnelle?
Au début, vous faites
pp = &list_head;
et, en parcourant la liste, vous avancez ce "curseur" avec
pp = &(*pp)->next;
De cette façon, vous gardez toujours une trace du point d'où vous venez et pouvez modifier le pointeur qui y habite.
Donc, quand vous trouvez que l'entrée est supprimée, vous pouvez simplement faire
*pp = entry->next
De cette façon, vous prenez en charge les 3 cas Afaq mentionne dans une autre réponse, éliminant ainsi le contrôle NULL
sur prev
.
Reconnecter la liste une fois qu'un nœud doit être supprimé est plus intéressant. Considérons au moins 3 cas:
1.Retirer un nœud depuis le début.
2.Retirer un nœud du milieu.
3.Removing un noeud de la fin.
Supprimer depuis le début
Lors de la suppression du nœud au début de la liste, il n'est pas nécessaire de relier les nœuds, car le premier nœud n'a pas de nœud précédent. Par exemple, supprimer un noeud avec un:
link
|
v
--------- --------- ---------
| a | --+---> | b | --+---> | c | 0 |
--------- --------- ---------
Cependant, nous devons fixer le pointeur au début de la liste:
link
|
+-------------+
|
v
--------- --------- ---------
| a | --+---> | b | --+---> | c | 0 |
--------- --------- ---------
Retrait du milieu
La suppression d'un nœud du milieu nécessite que le nœud précédent ignore le nœud en cours de suppression. Par exemple, supprimer le nœud avec b:
link
|
v
--------- --------- ---------
| a | --+--+ | b | --+---> | c | 0 |
--------- | --------- ---------
| ^
+----------------+
Cela signifie que nous avons besoin d'un moyen de faire référence au nœud avant celui que nous voulons supprimer.
Retrait de la fin
Supprimer un nœud de la fin nécessite que le nœud précédent devienne la nouvelle fin de la liste (c’est-à-dire qu’il ne pointe rien après). Par exemple, en supprimant le noeud avec c:
link
|
v
--------- --------- ---------
| a | --+---> | b | 0 | | c | 0 |
--------- --------- ---------
Notez que les deux derniers cas (milieu et fin) peuvent être combinés en disant que "le nœud précédant celui à supprimer doit indiquer l'endroit où se trouve celui à supprimer."
Ce problème a été discuté par Philip Buuck dans cette vidéo sur YouTube . Je vous recommande de regarder cela si vous avez besoin d'explications plus détaillées.
Si vous aimez apprendre des exemples, j'en ai préparé un. Disons que nous avons la liste simple liée suivante:
qui est représenté comme suit (cliquez pour agrandir):
Nous voulons supprimer le noeud avec le value = 8
.
Voici le code simple qui fait ceci:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
struct node_t {
int value;
node_t *next;
};
node_t* create_list() {
int test_values[] = { 28, 1, 8, 70, 56 };
node_t *new_node, *head = NULL;
int i;
for (i = 0; i < 5; i++) {
new_node = (node_t*)malloc(sizeof(struct node_t));
assert(new_node);
new_node->value = test_values[i];
new_node->next = head;
head = new_node;
}
return head;
}
void print_list(const node_t *head) {
for (; head; head = head->next)
printf("%d ", head->value);
printf("\n");
}
void destroy_list(node_t **head) {
node_t *next;
while (*head) {
next = (*head)->next;
free(*head);
*head = next;
}
}
void remove_from_list(int val, node_t **head) {
node_t *del, **p = head;
while (*p && (**p).value != val)
p = &(*p)->next; // alternatively: p = &(**p).next
if (p) { // non-empty list and value was found
del = *p;
*p = del->next;
del->next = NULL; // not necessary in this case
free(del);
}
}
int main(int argc, char **argv) {
node_t *head;
head = create_list();
print_list(head);
remove_from_list(8, &head);
print_list(head);
destroy_list(&head);
assert (head == NULL);
return EXIT_SUCCESS;
}
Si vous compilez et exécutez ce code, vous obtiendrez:
56 70 8 1 28
56 70 1 28
Créons **p
'double' pointeur sur *head
pointeur:
Analysons maintenant le fonctionnement de void remove_from_list(int val, node_t **head)
. Il parcourt la liste indiquée par head
tant que *p && (**p).value != val
.
Dans cet exemple, la liste donnée contient la variable value
que nous souhaitons supprimer (c'est-à-dire 8
). Après la deuxième itération de la boucle while (*p && (**p).value != val)
, (**p).value
devient 8
, nous cessons donc d’itérer.
Notez que *p
pointe vers la variable node_t *next
dans node_t
qui est avant le node_t
que nous voulons supprimer (qui est **p
). Ceci est crucial car cela nous permet de changer le pointeur *next
du node_t
qui se trouve devant le node_t
que nous voulons supprimer, en le supprimant de la liste.
Attribuons maintenant l’adresse de l’élément à supprimer (del->value == 8
) au pointeur *del
.
Nous devons corriger le pointeur *p
de sorte que **p
pointe vers l'élément un élément after*del
que nous allons supprimer:
Dans le code ci-dessus, nous appelons free(del)
; il n'est donc pas nécessaire de définir del->next
sur NULL
, mais si nous souhaitons renvoyer le pointeur sur l'élément 'détaché' de la liste au lieu de le supprimer complètement, nous définirions del->next = NULL
:
Dans la première approche, vous supprimez un nœud en unlink le dans la liste.
Dans la deuxième approche, vous remplacez le nœud à supprimer par le nœud suivant.
Apparemment, la seconde approche simplifie le code de manière élégante. En définitive, la deuxième approche nécessite une meilleure compréhension de la liste chaînée et du modèle de calcul sous-jacent.
Note: Voici une question de codification très pertinente mais légèrement différente. Bon pour tester sa compréhension: https://leetcode.com/problems/delete-node-in-a-linked-list/
Je préfère l'approche du nœud factice, un exemple de mise en page:
|Dummy|->|node1|->|node2|->|node3|->|node4|->|node5|->NULL
^ ^
| |
curr curr->next // << toDel
et ensuite, vous passez au noeud à supprimer (toDel = curr> next)
tmp = curr->next;
curr->next = curr->next-next;
free(tmp);
De cette façon, vous n'avez pas besoin de vérifier si c'est le premier élément, car le premier élément est toujours factice et n'est jamais supprimé.
@glglgl:
J'ai écrit suivant exemple simple. J'espère que vous pourrez voir pourquoi cela fonctionne.
Dans la fonction void delete_node(LinkedList *list, void *data)
, j'utilise *pp = (*pp)->next;
et cela fonctionne. Pour être honnête, je ne comprends pas pourquoi cela fonctionne. Je dessine même le diagramme des pointeurs mais je ne le comprends toujours pas. J'espère que vous pourrez clarifier cela.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _employee {
char name[32];
unsigned char age;
} Employee;
int compare_employee(Employee *e1, Employee *e2)
{
return strcmp(e1->name, e2->name);
}
typedef int (*COMPARE)(void *, void *);
void display_employee(Employee *e)
{
printf("%s\t%d\n", e->name, e->age);
}
typedef void (*DISPLAY)(void *);
typedef struct _node {
void *data;
struct _node *next;
} NODE;
typedef struct _linkedlist {
NODE *head;
NODE *tail;
NODE *current;
} LinkedList;
void init_list(LinkedList *list)
{
list->head = NULL;
list->tail = NULL;
list->current = NULL;
}
void add_head(LinkedList *list, void *data)
{
NODE *node = (NODE *) malloc(sizeof(NODE));
node->data = data;
if (list->head == NULL) {
list->tail = node;
node->next = NULL;
} else {
node->next = list->head;
}
list->head = node;
}
void add_tail(LinkedList *list, void *data)
{
NODE *node = (NODE *) malloc(sizeof(NODE));
node->data = data;
node->next = NULL;
if (list->head == NULL) {
list->head = node;
} else {
list->tail->next = node;
}
list->tail = node;
}
NODE *get_node(LinkedList *list, COMPARE compare, void *data)
{
NODE *n = list->head;
while (n != NULL) {
if (compare(n->data, data) == 0) {
return n;
}
n = n->next;
}
return NULL;
}
void display_list(LinkedList *list, DISPLAY display)
{
printf("Linked List\n");
NODE *current = list->head;
while (current != NULL) {
display(current->data);
current = current->next;
}
}
void delete_node(LinkedList *list, void *data)
{
/* Linus says who use this block of code doesn't understand pointer.
NODE *prev = NULL;
NODE *walk = list->head;
while (((Employee *)walk->data)->age != ((Employee *)data)->age) {
prev = walk;
walk = walk->next;
}
if (!prev)
list->head = walk->next;
else
prev->next = walk->next; */
NODE **pp = &list->head;
while (((Employee *)(*pp)->data)->age != ((Employee *)data)->age) {
pp = &(*pp)->next;
}
*pp = (*pp)->next;
}
int main ()
{
LinkedList list;
init_list(&list);
Employee *samuel = (Employee *) malloc(sizeof(Employee));
strcpy(samuel->name, "Samuel");
samuel->age = 23;
Employee *sally = (Employee *) malloc(sizeof(Employee));
strcpy(sally->name, "Sally");
sally->age = 19;
Employee *susan = (Employee *) malloc(sizeof(Employee));
strcpy(susan->name, "Susan");
susan->age = 14;
Employee *jessie = (Employee *) malloc(sizeof(Employee));
strcpy(jessie->name, "Jessie");
jessie->age = 18;
add_head(&list, samuel);
add_head(&list, sally);
add_head(&list, susan);
add_tail(&list, jessie);
display_list(&list, (DISPLAY) display_employee);
NODE *n = get_node(&list, (COMPARE) compare_employee, sally);
printf("name is %s, age is %d.\n",
((Employee *)n->data)->name, ((Employee *)n->data)->age);
printf("\n");
delete_node(&list, samuel);
display_list(&list, (DISPLAY) display_employee);
return 0;
}
sortie:
Linked List
Susan 14
Sally 19
Samuel 23
Jessie 18
name is Sally, age is 19.
Linked List
Susan 14
Sally 19
Jessie 18
Voici un exemple de code complet, utilisant un appel de fonction pour supprimer les éléments correspondants:
rem()
supprime les éléments correspondants en utilisant prev
rem2()
supprime les éléments correspondants à l'aide d'un pointeur à l'autre
// code.c
#include <stdio.h>
#include <stdlib.h>
typedef struct list_entry {
int val;
struct list_entry *next;
} list_entry;
list_entry *new_node(list_entry *curr, int val)
{
list_entry *new_n = (list_entry *) malloc(sizeof(list_entry));
if (new_n == NULL) {
fputs("Error in malloc\n", stderr);
exit(1);
}
new_n->val = val;
new_n->next = NULL;
if (curr) {
curr->next = new_n;
}
return new_n;
}
#define ARR_LEN(arr) (sizeof(arr)/sizeof((arr)[0]))
#define CREATE_LIST(arr) create_list((arr), ARR_LEN(arr))
list_entry *create_list(const int arr[], size_t len)
{
if (len == 0) {
return NULL;
}
list_entry *node = NULL;
list_entry *head = node = new_node(node, arr[0]);
for (size_t i = 1; i < len; ++i) {
node = new_node(node, arr[i]);
}
return head;
}
void rem(list_entry **head_p, int match_val)
// remove and free all entries with match_val
{
list_entry *prev = NULL;
for (list_entry *entry = *head_p; entry; ) {
if (entry->val == match_val) {
list_entry *del_entry = entry;
entry = entry->next;
if (prev) {
prev->next = entry;
} else {
*head_p = entry;
}
free(del_entry);
} else {
prev = entry;
entry = entry->next;
}
}
}
void rem2(list_entry **pp, int match_val)
// remove and free all entries with match_val
{
list_entry *entry;
while ((entry = *pp)) { // assignment, not equality
if (entry->val == match_val) {
*pp = entry->next;
free(entry);
} else {
pp = &entry->next;
}
}
}
void print_and_free_list(list_entry *entry)
{
list_entry *node;
// iterate through, printing entries, and then freeing them
for (; entry != NULL; node = entry, /* lag behind to free */
entry = entry->next,
free(node)) {
printf("%d ", entry->val);
}
putchar('\n');
}
#define CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val) createList_removeMatchElems_print((arr), ARR_LEN(arr), (match_val))
void createList_removeMatchElems_print(const int arr[], size_t len, int match_val)
{
list_entry *head = create_list(arr, len);
rem2(&head, match_val);
print_and_free_list(head);
}
int main()
{
const int match_val = 2; // the value to remove
{
const int arr[] = {0, 1, 2, 3};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {0, 2, 2, 3};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {2, 7, 8, 2};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {2, 2, 3, 3};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {0, 0, 2, 2};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {2, 2, 2, 2};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
{
const int arr[] = {};
CREATELIST_REMOVEMATCHELEMS_PRINT(arr, match_val);
}
return 0;
}
Voir le code en action ici:
Si vous compilez et utilisez valgrind (un vérificateur de fuite de mémoire), procédez comme suit:gcc -std=c11 -Wall -Wextra -Werror -o go code.c && valgrind ./go
nous voyons que tout va bien.