C / POINTERS
Dereferencing safely and the NULL habit
Guard every dereference against NULL, use NULL deliberately to mean 'no object', and see why a dangling pointer can never be tested for validity.
What you will learn
- Write the NULL guard before the first dereference, then trust the pointer in that scope
- Check malloc, fopen and strchr results, since NULL is their documented failure value
- Set a pointer to NULL right after free so its emptiness becomes testable
- Explain why dereferencing NULL is undefined behaviour, not a guaranteed crash
Understanding Dereferencing safely and the NULL habit
The expression *p is not a lookup that can fail. It is an instruction to read or write the object living at the address stored in p, and the language simply assumes an object is there. NULL is the one pointer value guaranteed to compare unequal to the address of every real object, which is what makes it a usable 'nothing here' marker — but that guarantee is about comparison, not about dereferencing. Evaluating *p when p is NULL is undefined behaviour: on a desktop the lowest page is usually unmapped so you get a segmentation fault, on some embedded targets address 0 is a real register you will cheerfully overwrite, and after optimisation the compiler may assume p was non-null and delete a check you wrote afterwards.
So the habit is to test at the boundary rather than at the point of use. Ask where each pointer came from: some sources are contractually allowed to hand back NULL — malloc, calloc, realloc, fopen, strchr, strstr, getenv, fgets — and every pointer parameter is another such source, because a caller can always pass NULL. Put one guard near the top of the function, before the first *p, p[i] or p->field, and treat the pointer as good for the rest of the body. Repeated checks scattered through a loop usually mean nobody decided who owned the check.
The second half of the habit exists because NULL describes only one of the three states a pointer can be in: pointing at a live object, null, or invalid. An invalid pointer — a freed block, the address of a local that has gone out of scope, an uninitialised variable holding stack garbage — is indistinguishable from a good one at runtime, and there is no is_valid(p) in C and cannot be. You therefore engineer the invalid state out of existence: initialise pointer variables to NULL, assign NULL immediately after free, and never hand out the address of something that dies before the pointer does. A NULL check works only because you maintain the invariant that not-null implies usable.
Treat NULL as part of a function's published contract rather than an afterthought. A function that returns a pointer should say in one comment line whether NULL is a possible result and what it means, and a function that takes a pointer should say whether NULL is accepted; once that is written down, the guard you need in each body is obvious and you stop re-checking pointers that were already validated one frame up.
<stdio.h>
/* Returns a pointer to the first digit in s, or NULL when there is none.
A NULL input is not an error here: an absent string has no digits. */
static const char *first_digit(const char *s)
{
if (s == NULL)
return NULL;
while (*s != '\0') {
if (*s >= '0' && *s <= '9')
return s;
s++;
}
return NULL;
}
static void show(const char *name, const char *text)
{
const char *d = first_digit(text);
if (d == NULL) { /* guard sits before the dereference */
printf("%s -> no digit found\n", name);
return;
}
printf("%s -> '%c', tail \"%s\"\n", name, *d, d);
}
int main(void)
{
show("gate 7A", "gate 7A");
show("lobby", "lobby");
show("missing", NULL);
return 0;
}
A pointer is live, null, or invalid, and only null is testable — so safe dereferencing means arranging for every unusable pointer to be null and checking before you read.
Worked examples
Allocation failure and the free-then-NULL pairing
Shows the guard between malloc and first use, and why assigning NULL after free makes a second free harmless.
<stdio.h>
<stdlib.h>
int main(void)
{
int *nums = malloc(3 * sizeof *nums);
if (nums == NULL) { /* NULL is malloc's failure value */
fprintf(stderr, "out of memory\n");
return 1;
}
nums[0] = 10;
nums[1] = 20;
nums[2] = 30;
printf("sum = %d\n", nums[0] + nums[1] + nums[2]);
free(nums);
nums = NULL; /* dangling becomes testable */
free(nums); /* free(NULL) is a defined no-op */
printf("nums == NULL is %d\n", nums == NULL);
return 0;
}
Example explained
Line 1The guard has to sit between the malloc call and nums[0] = 10, because that assignment is already a dereference.
Line 2free(nums) releases the block but leaves the old address in nums, and that stale value passes every test you could write.
Line 3nums = NULL turns an untestable dangling pointer into a testable empty one.
Line 4free(NULL) is required by the standard to do nothing, which is why the second free prints no error instead of corrupting the heap.
Short-circuit && as the guard
Demonstrates that the left operand of && is evaluated first, so a NULL test in the same expression really does protect the dereference.
<stdio.h>
/* s != NULL is evaluated first, and && skips its right operand when the
left one is false, so *s is never reached for a null pointer. */
static int says_yes(const char *s)
{
return s != NULL && (*s == 'y' || *s == 'Y');
}
int main(void)
{
const char *answers[] = { "yes", "no", NULL, "Yep" };
for (int i = 0; i < 4; i++)
printf("%d -> %d\n", i, says_yes(answers[i]));
return 0;
}
Example explained
Line 1&& imposes left-to-right evaluation with a sequence point, so the test and the dereference cannot be reordered by the compiler.
Line 2Writing (*s == 'y' || *s == 'Y') && s != NULL instead would dereference before testing and is undefined behaviour on element 2.
Line 3says_yes(NULL) yields 0, so 'no answer' and 'answer was not yes' collapse into one result; a caller that must distinguish them needs a third return value.
NULL as 'I do not want this output'
Shows a function that guards its out parameter, letting a caller pass NULL to discard a result while a return code reports what happened.
<stdio.h>
/* Writes through out only when the caller supplied a destination.
Returns 1 if a value was written, 0 if there was nowhere to write. */
static int halve(int value, int *out)
{
if (out == NULL)
return 0;
*out = value / 2;
return 1;
}
int main(void)
{
int result = -1; /* defined value even if nothing is written */
if (halve(84, &result))
printf("wrote %d\n", result);
if (!halve(84, NULL))
printf("no destination, result still %d\n", result);
return 0;
}
Example explained
Line 1if (out == NULL) return 0; makes NULL a documented, legal argument meaning 'discard this output', the same convention strtol uses for its endptr.
Line 2The return value is a second channel: without it the caller could not tell a successful write from a skipped one.
Line 3result is initialised at its declaration, so the second printf reads a defined value on the call where nothing was stored.
Important notes
if (p) and if (p != NULL) are the same test, since a null pointer converts to false, and assigning the literal 0 to a pointer also produces a null pointer. Variadic calls are the exception: in something like execl the terminating null must be written (char *)NULL, because an unadorned 0 can be passed as a narrower int.
assert(p != NULL) records a contract but disappears under -DNDEBUG, so use it for situations that would mean a bug in your own code, and a real if for input the function is expected to survive.
Common mistakes
Putting the check after the first use, as in int v = *p; if (p == NULL) return -1;. The read has already happened, and since *p is only defined for a non-null p, an optimiser may remove the test as redundant so the early return never fires.
Assuming free(p) clears the caller's variable and later writing if (p != NULL) free(p);. free gets a copy of the pointer value and cannot modify p, so this is a double free that shows up as heap corruption or an abort far from the real bug.
Relying on printf("%s", p) printing (null) for a null pointer. Glibc happens to do that, but it is undefined behaviour, and strlen(p) or strcpy(dst, p) on the same pointer crashes at once, so the code only looks NULL-tolerant until it is built elsewhere.
Try it yourself
Change, predict, then run
Write const char *find_char(const char *s, char c) that returns NULL when s is NULL or c does not occur in s, and call it three times from main — with (NULL, 'a'), ("hello", 'z') and ("hello", 'l') — printing the matched tail or the word absent for each.
Open the C workspaceCheck your understanding
A function contains int v = *p; if (p == NULL) return -1; return v; and is built with optimisations on. Why is this dangerous even on a machine where reading address 0 always faults?
- The guard must be spelled if (!p); p == NULL is not a reliable null test once the optimiser runs.
- int v = *p; stores the address rather than the pointed-to value, so the later test inspects the wrong thing.
- The dereference runs before the test, and because *p is defined only for a non-null p the optimiser is entitled to delete the test as provably redundant.
- -1 is itself a possible valid result, so the caller cannot distinguish failure from success.
Show answer
*p is evaluated on the first line, so the guard protects nothing; worse, a program that dereferences a null pointer has no defined behaviour, which lets the compiler conclude p was non-null and remove the comparison, so the hoped-for early return may not exist in the binary at all. Option 3 names a genuine API weakness but not this danger, and option 0 is wrong because !p and p == NULL compile to exactly the same test.