Calls to malloc(3) Can Fail
To the annoyance of many students learning C programming, allocating memory by calling malloc(3)
can fail. When it does, malloc(3)
will return NULL
. But, what should our program do when a call to malloc(3)
fails? The question that today’s blog post explores is: when is it okay for our program to exit — that is, simply terminate — because of a memory-allocation failure?
To get started, let’s consider the following code, which might produce a segmentation fault (or “segfault” for short). This code might segfault because, if the call to malloc(3)
fails, the attempt to write to the (non-)allocated memory will deference NULL
. That is, it will attempt to deference the memory address 0x0
:
int *i = malloc(sizeof(int));
*i = 123; // i will be NULL if malloc fails
free(i);
Even if we use calloc(3)
instead of malloc(3)
, as discussed in an earlier blog post, our code still has the same issue:
int *i = calloc(1, sizeof(int));
*i = 123; // i will be NULL if calloc fails
free(i);
In order to deal with the possibility that the memory allocation fails, we need to check if the return value of calloc(3)
is NULL
, then take some appropriate action. For example, a function that uses calloc(3)
could itself return an error value if the memory allocation fails:
#define ERROR_ALLOC_FAIL (-1)
int my_func(void)
{
int *i = calloc(1, sizeof(int));
if (i == NULL) {
return ERROR_ALLOC_FAIL;
}
*i = 123;
free(i);
return 0;
}
In the above example, if the allocation inside my_func
fails, my_func
returns -1
. That’s a perfectly reasonable behaviour for that function. But it doesn’t answer the larger question: what should our program, as a whole, do when an allocation fails? Can we just kill our program dead and call it a day?
A Graceful Approach
There are some applications where, if an attempt to allocate memory fails, we can gracefully try to allocate less memory. One example could be an image-editing application. Our attempt to allocate enough memory to hold a (potentially massive) high-resolution image might fail. In this case, we could warn the user, then attempt to allocate a smaller amount of memory to work with a reduced-resolution image.
There are also times where, if a memory allocation fails, we have no choice but to return an error value to the calling function. The classic example of this restriction is when we’re writing a function in a library that needs to allocate memory. A call to a library function should never cause the calling program to terminate. Instead, the library function has to indicate to the calling function that its memory allocation failed, to provide the calling function the opportunity to take appropriate action. After all, the program calling the library function might be something like an image-editing application, which can deal with an allocation failure gracefully.
Memory Necessary for Basic Functionality
However, there are times when an application we’re writing needs to allocate a specific amount of memory to function at all. Consider a command-line application that processes student records, which needs to allocate a linked list element for each student. Either the allocation succeeds and the program can run, or the allocation fails and the program can’t run. There’s presumably no graceful way to use less memory.
Let’s look at the following function that allocates a struct
to hold a student record, and fills it with the provided data. For now, if the allocation fails, let’s have the function output an error message and return NULL
:
struct student_record
{
const char *name;
int grade;
};
struct student_record *
create_record(const char *name, int grade)
{
struct student_record *r =
calloc(1, sizeof(struct student_record));
if (r == NULL) {
fprintf(stderr, "Allocation failed\n");
return NULL;
}
r->name = name;
r->grade = grade;
return r;
}
But, if our program needs to allocate a student record to work at all, what could a function that calls create_record
possibly do if it gets a NULL
return? It could propagate an error code all the way up through the call stack to main
, but really nothing else. Consider a process_student_records
function that calls create_record
. If process_student_records
gets a NULL
return from create_record
, it could propagate the error up the call stack by returning, for example, -1
:
#define ERROR_ALLOC_FAIL (-1)
int process_student_records(void)
{
struct student_record *r;
const char *name;
int grade;
[...]
r = create_record(name, grade);
if (r == NULL) {
return ERROR_ALLOC_FAIL;
}
[...]
return 0;
}
While this approach does work, let’s talk about the deeper issue here. It’s not only the case that process_student_records
can’t do anything meaningful with the NULL
return from create_record
, other than propagate the failure up the stack. It’s also the case that we need additional code in the process_student_records
function — code that will probably never be tested — just to deal with the possibility that create_record
could return NULL
.
What would be far nicer, from a code-design perspective, is if create_record
were guaranteed never to return NULL
. In that case, process_student_records
wouldn’t have to check if create_record
failed:
// Never returns NULL
struct student_record *
create_record(const char *name, int grade);
int process_student_records(void)
{
struct student_record *r;
const char *name;
int grade;
[...]
r = create_record(name, grade);
// r guaranteed to be non-NULL
[...]
return 0;
}
Taking this idea one step further, process_student_records
might not even have to return an int
value representing success or failure. Provided there isn’t any other way — aside from create_record
failing — that process_student_records
could fail, it might be possible to make process_student_records
a void
function:
// Never returns NULL
struct student_record *
create_record(const char *name, int grade);
void process_student_records(void)
{
struct student_record *r;
const char *name;
int grade;
[...]
r = create_record(name, grade);
// r guaranteed to be non-NULL
[...]
// No return value
}
In this case, any function calling process_student_records
also wouldn’t have to check for a return value indicating an error.
Just exit(3)
Let’s stop and ask: assuming that our application needs the allocation inside create_record
to succeed in order to function at all, what would the purpose of propagating an error from create_record
all the way up to main
even be? Generally speaking, we propagate this sort of error up to main
so that we can close all active resources (for example, free all allocated memory and close any open file descriptors) along the way, then terminate the program with a non-zero exit code.
But, there’s a single function that can do all of that for us: exit(3)
. This function, declared in stdlib.h
, will close all open file descriptors, free all allocated memory, and terminate the program with a an error code of our choice. So, if allocation of memory that’s necessary for a program to function fails, we can just call exit(3)
to instantly stop the program.
Here’s what our create_record
function could look like if it calls exit(3)
instead of returning NULL
when calloc(3)
fails. The argument to exit(3)
, EXIT_FAILURE
, is defined in the C standard to be an exit status indicating unsuccessful program termination:
struct student_record *
create_record(const char *name, int grade)
{
struct student_record *r =
calloc(1, sizeof(struct student_record));
if (r == NULL) {
fprintf(stderr, "Allocation failed\n");
exit(EXIT_FAILURE);
}
r->name = name;
r->grade = grade;
return r;
}
Having removed the NULL
error return from create_record
, opting instead to call exit(3)
, process_student_records
is free to assume the return from create_record
is non-NULL
:
/*
* Returns a newly created student record,
* or exits if memory allocation fails.
*/
struct student_record *
create_record(const char *name, int grade);
void process_student_records(void)
{
struct student_record *r;
const char *name;
int grade;
[...]
r = create_record(name, grade);
// r guaranteed to be non-NULL
[...]
}
With this change to create_record
, our code in process_student_records
is simplified: it’s easier to read, and it’s easier to properly test.
I should emphasize, though, that this approach of just calling exit(3)
when memory allocation fails only makes sense if our program has no meaningful way to recover from an allocation failure. But, given how many programs simply rely on memory allocation to succeed, this situation is quite common. So consider, in your C code, whether just calling exit(3)
when memory allocation fails is the correct approach.
Exercise
As an exercise for students, write a C macro that verifies the return value from a call to malloc(3)
or calloc(3)
is non-NULL
. If the value is NULL
, the macro should output an error message and terminate the program with exit(3)
. The ultimate goal of this exercise is to rewrite the following code:
int *i = calloc(1, sizeof(int));
if (i == NULL) {
fprintf(stderr, "Allocation failed\n");
exit(EXIT_FAILURE);
}
The improved version of the code should define a macro called VERIFY_ALLOC
that will take a pointer argument, performs a NULL
-check on it, then (if necessary) output an error message and exit(3)
the program. That is, the following code should behave identically to the code above:
#define VERIFY_ALLOC [...write your macro here...]
int *i = calloc(1, sizeof(int));
VERIFY_ALLOC(i);
Remember to wrap the body of your VERIFY_ALLOC
macro inside a do/while
, as discussed in an earlier blog post.
For more tips, and to arrange for personalized tutoring for yourself or your study group, check out Vancouver Computer Science Tutoring.