Looking at code on examples on StackOverflow I've noticed two distinct prototypes for object deallocation:
struct foo *foo_create(int);
void foo_free_v1(struct foo *);
void foo_free_v2(struct foo **);
void bar() {
struct *foo = foo_create(7);
// ...
// Version 1
foo_free_v1(foo);
// Version 2
foo_free_v2(&foo);
}
The standard library function free
uses the first form.
Is the second form idiomatic in C? If yes, for what purpose?
CodePudding user response:
The ones that take a pointer-to-pointer do so because they have the added convenience of automatically nulling out your variable for you.
They might look like:
void foo_free_v1(struct foo *f) {
free(f->a);
free(f->b);
free(f->c);
free(f);
}
void foo_free_v2(struct foo **f) {
if (*f == NULL) return; // This has been freed before, don't do it again!
free((*f)->a);
free((*f)->b);
free((*f)->c);
free(*f);
*f = NULL; // Null out the variable so it can't be freed again.
}
This attempts to protect against double-free bugs. How good of an idea that, is debatable. See the comment thread below.
CodePudding user response:
In the first function
void foo_free_v1(struct foo *);
the original pointer used as an argument expression is passed to the function by value. It means that the function deals with a copy of the value of the pointer passed to the function.
Changing the copy does not influence on the original pointer used as an argument expression.
Consider for example the function definition
void foo_free_v1(struct foo *p)
{
free( p );
p = NULL;
}
This statement within the function
p = NULL;
does not change the original pointer to NULL
. It sets to NULL
the function local variable p
. As a result the pointer used as an argument expression after calling the function will have an invalid value. That is a value that does not point to an existing object.
In the second function declared like that
void foo_free_v2(struct foo **);
the original pointer used as an argument expression is accepted by reference through a pointer to it. So dereferencing the pointer you have a direct access to the original pointer used as an argument expression.
Consider a possible function definition
void foo_free_v2(struct foo **p)
{
free( *p );
*p = NULL;
}
In this case in the statement
*p = NULL;
it is the original pointer that is set to NULL
. As a result the original pointer does not have an invalid value.
Consider for example a sungly-linked list like
struct SinglyLinkedList
{
int data;
SinglyLinkedList *next;
};
In main you can declare the list like
struct SinglyLinkedList *head = NULL;
Then you can add new nodes to the list. As the pointer head
is initialized then the function that adds new nodes will work correctly.
After that you can destroy the list.
If you will call the first function declared like
void foo_free_v1(struct SinglyLinkedList *head );
then after calling the function the pointer head
declared in main will have an invalid value. That is there will be an inconsistency. The list does not have already elements but its pointer to the head ode is not equal to NULL
.
Is you call the function declared lik
void foo_free_v1(struct SinglyLinkedList **head );
then indeed the pointer to the head node in main will be equal to NULL that indeed means that the list is empty. And if you have a function that checks whether a list is empty you can pass the pointer to the function without producing undefined behavior.