Recommended resources
The freely available resources this lesson will direct you to.
Hello! The previous lesson distinguished an object’s storage from the pointers that refer to it. Copying a structure with a pointer member copies the pointer, not the object it points to. That observation becomes critical when storage is allocated and released at runtime: a pointer can still exist after the object it once identified has ceased to be usable.
In this final lesson of the C programming module, you will learn to identify four related faults in code: dangling pointers, memory leaks, invalid accesses, and allocation errors. For GATE tracing, the central question is not merely “What address does this pointer hold?” It is “Does a suitable object still exist there, and is this operation permitted?”
Allocation creates storage, not initialized values
The functions malloc, calloc, realloc, and free are declared in <stdlib.h>. Start with the allocation overview in mycodeschool’s Dynamic memory allocation in C - malloc calloc realloc free. The video provides a useful picture of an allocated block and shows why requests are measured in bytes.
Dynamic memory allocation in C - malloc calloc realloc free
Watch mycodeschool’s Dynamic memory allocation in C - malloc calloc realloc free to connect an allocation request, its returned pointer, and the number of elements that fit in the block.
Watch malloc and sizing, focusing on the use of sizeof and on how a typed pointer accesses successive elements. Then watch calloc for the distinction between uninitialized storage and zeroed bytes. Two corrections for C: a variable-length local array is permitted by some C versions and implementations, and a cast of malloc’s return value is not required when <stdlib.h> is included.
For three integers, a standard allocation pattern is:
#include <stdlib.h>
int *p = malloc(3 * sizeof *p);
if (p == NULL) {
/* Handle allocation failure; do not access p[0]. */
} else {
p[0] = 10;
p[1] = 20;
p[2] = 30;
free(p);
p = NULL;
}
sizeof *p is the size of the type that p points to. The expression requests room for three int objects regardless of the implementation’s int size. Assuming allocation succeeds, valid element indices are 0, 1, and 2. The block remains allocated until it is released; leaving the scope of the pointer variable alone does not release it.
Successful malloc provides storage whose contents are not initialized. Thus, reading p[0] before assigning it a value is not a legitimate way to obtain a predictable integer. By contrast, calloc(3, sizeof *p) allocates storage and sets its bytes to zero; for an array of int, its elements initially have value zero. Neither function guarantees success, so check the returned pointer before using it.
An allocation request can also be wrong even when the function returns a non-null pointer:
int *p = malloc(3 * sizeof p); /* Often the wrong byte count */
Here sizeof p measures the pointer variable, not an int. Depending on the implementation, this may request too few bytes for three integers; p[2] could then exceed the allocated block. Prefer sizeof *p for an element allocation. If the count comes from input, the multiplication itself must also fit in size_t: a wrapped byte count can lead to an undersized allocation. In code that must handle arbitrary counts, check count <= SIZE_MAX / sizeof *p before multiplying (with the appropriate header for SIZE_MAX).
Lifetime: dangling pointers versus leaks
An allocated block and a pointer to it have different lifetimes. free(p) ends the lifetime of the allocated block; it does not automatically reset every pointer variable that referred to it. SEI CERT’s MEM30-C. Do not access freed memory explains why a freed address must not be treated as a valid object merely because its old contents appear to remain there.
MEM30-C. Do not access freed memory | CERT Secure Coding
Read SEI CERT’s explanation of dangling pointers and the consequences of accessing deallocated storage.
In the introductory text, read the explanation, from the opening definition through the paragraph ending “once it is freed.” Focus on the distinction between an apparently unchanged address and a still-valid object. Stop before “Noncompliant Code Example.”
Consider two aliases:
int *p = malloc(sizeof *p);
if (p != NULL) {
*p = 7;
int *q = p;
free(p);
p = NULL;
/* *q is invalid; q has not been made safe by setting p to NULL. */
}
Once free(p) executes, the allocated object is gone. q is dangling too. Setting p to NULL is helpful for that one variable, but it neither changes q nor restores the object. Reading or writing through q, or freeing the same allocation again through an old alias, has undefined behavior. Do not predict “7,” “garbage,” or a segmentation fault as a guaranteed result. Undefined behavior gives no reliable output to trace.
A pointer can also dangle without a call to free. Neso Academy’s Understanding the Dangling Pointers illustrates a function returning the address of a local automatic variable.
Understanding the Dangling Pointers
Watch Neso Academy’s Understanding the Dangling Pointers for a second way an object’s lifetime can end while a pointer to it remains.
Watch the local example. Identify precisely when the local variable ceases to exist. The video describes a segmentation fault as the outcome; treat that as a possibility, not a guarantee. Using the returned pointer to access the expired object has undefined behavior.
For example:
int *bad_address(void)
{
int local = 12;
return &local; /* local's lifetime ends on return */
}
The address does not extend local’s lifetime. Compare that with a memory leak, where an allocated object still exists but the program loses its means of accessing and eventually freeing it:
int *p = malloc(sizeof *p);
if (p != NULL) {
p = NULL; /* The allocated block is now unreachable. */
}
This is not use-after-free: no free occurred. The problem is the opposite—storage was not released, and its only pointer was overwritten. If another usable alias still held the allocation’s starting pointer, the block would not yet be lost.
| State | Does the allocated object still exist? | Is a usable pointer to it available? |
|---|---|---|
| Live, reachable allocation | Yes | Yes |
Dangling pointer after free |
No | An old pointer variable may remain, but it is not usable |
| Unreachable leak | Yes | No |
This distinction is especially useful with structures. Copying a structure containing a pointer creates another pointer to the same allocation; it does not create a second allocation. Freeing through one copy invalidates access through the other. Conversely, freeing neither copy leaks the allocation if both pointers are lost.
Invalid accesses: test the object, then its bounds
A live allocation is necessary for access, but it is not sufficient. The pointer must designate an element that the operation is allowed to access. For an array of three integers, p + 3 is a permitted one-past-the-end pointer. It can be useful in a comparison or as a loop stopping point, but *(p + 3) and p[3] do not access a fourth element: they are invalid. Forming a pointer farther beyond the array is itself a problem; it is not made acceptable by choosing not to dereference it.
SEI CERT’s ARR30-C. Do not form or use out-of-bounds pointers or array subscripts distinguishes these cases and shows why checking only an index’s upper bound is insufficient.
ARR30-C. Do not form or use out-of-bounds pointers or array subscripts
Read SEI CERT’s rules and its negative-index example to sharpen the difference between forming a pointer and accessing an element.
In the summary table near the start of the page, read the entries beginning with out-of-bounds formation and out-of-range subscripts. Then, under “Noncompliant Code Example (Forming Out-of-Bounds Pointer),” read the negative-index explanation, followed by the first “Compliant Solution.” Notice why a check equivalent to index < length needs a lower-bound check as well.
Other invalid accesses arise without stepping past an array:
int *p = NULL;
/* *p = 5; */ /* Invalid: NULL designates no int object. */
int a[3] = {1, 2, 3};
/* free(a); */ /* Invalid: a was not dynamically allocated. */
free requires either NULL—for which it does nothing—or an appropriate pointer returned by an allocation function and not already freed. Passing an address of an automatic array, an interior pointer such as p + 1, or the same allocation a second time is invalid. For a live dynamic array, retain its starting pointer for deallocation even if another pointer is advanced during traversal.
A string operation must satisfy bounds too. The following image illustrates an unchecked copy of a six-character PIN into space intended for four characters. It is a schematic picture of overwriting adjacent storage, not a prediction of a particular C program’s resulting values.

For a C string, capacity must include its terminating '\0'. A four-byte char array can hold at most three ordinary characters as a null-terminated string. If a question shows an unchecked copy beyond its destination, do not continue tracing the neighboring variables as though the diagram prescribed a guaranteed outcome: the program has an invalid write.
realloc: treat every result as a new ownership decision
realloc changes the size of an existing dynamic allocation. It may use the same region or move the data to a new one. The mycodeschool video shows these physical possibilities; the important tracing rule is that a successful realloc invalidates the old pointer value for subsequent use, even if the returned address happens to look unchanged.
Dynamic memory allocation in C - malloc calloc realloc free
Return to mycodeschool’s allocation video for its visualization of a resize that may extend a block or move it.
Watch resize possibilities. Focus on why code must use the pointer returned on success. Stop before the later demonstrations: this lesson avoids relying on the video’s claim that requesting size zero is simply equivalent to free, which is not a portable rule to use here.
Assume p owns a valid allocation and the requested new size is nonzero. A common error is:
p = realloc(p, 6 * sizeof *p);
If the request fails, realloc returns NULL and leaves the original allocation intact. Assigning that result directly to p loses the only pointer to the original block, causing a leak if there is no other usable alias. Instead:
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp != NULL) {
p = tmp; /* Use p for the resized block. */
} else {
/* p still identifies the original live block. */
}
After success, use tmp or the newly assigned p; do not use an alias retained from before the call. The elements within the portion common to the old and new sizes retain their values. If the block grows, do not assume the additional elements have been initialized.
For GATE questions, keep the two outcomes separate rather than guessing whether a move happens:
- Failure: the returned pointer is
NULL; the original allocation remains available through its valid old pointer. - Success: the returned pointer designates the resized allocation; old pointer values must not be used, whether or not the numerical address appears to have changed.
A reliable fault-tracing method
When reading a short C program, maintain a small ledger for each object: where it came from, whether its lifetime is active, its size, and which pointers currently refer to it. Then inspect each operation in program order.
First, check the allocation request: is the byte count for the intended type and number of elements, and is failure handled? Next, check every read or write against initialization, lifetime, and bounds. At free, mark the allocation dead and all its aliases unusable. At a pointer overwrite, ask whether it discarded the last way to release a live allocation. At realloc, branch on success versus failure before tracing further.
This method prevents four tempting but incorrect shortcuts: treating an address as proof of a live object, treating a copied pointer as a copied allocation, treating one-past-the-end as a valid element, and treating a failed resize as though it had freed the original block.
Key takeaways
Dangling pointers refer to objects whose lifetimes have ended; leaks leave allocated storage live but unreachable. Invalid accesses include use-after-free, null dereferences, and out-of-bounds reads or writes. Allocation errors include requesting the wrong number of bytes, ignoring failure, misusing free, and losing the original pointer on a failed realloc.
This completes the C tracing module. The next module applies the same pointer, lifetime, and bounds reasoning to linked lists, stacks, and queues—where each operation will change not just values, but the connections between objects.