Books / C Language Primer for Java Developers / Chapter 7
Memory Management - Differences in C vs Java
In this chapter
Java does a lot of memory management for you, behind the scenes. C’s use of memory will seem clunky and weird by contrast. But there are advantages to being able to do your own memory management. For one thing, not having to rely on Java’s automated “garbage collection” can noticeably speed up your program. For another, there is less need to take the time to allocate throwaway data structures. We will discuss C pointers first, and then work into how memory management works, and its ramifications.
Pointers
In Java, a complex-type variable is really holding an address—a location in memory. When you pass an object to a method as an argument, you’re really just passing the address. That’s why changes to the object inside the method persist when it is done: the object inside the method is an alias of the one outside. For example, in Java you can do the following:
Arrays.sort(myArray); // Java's built-in sort function
When this method is done, the array will have been changed—even though it has no return value. It used myArray’s address to effect all its changes.
In C, any variable can be handled this way, including basic
variables like ints and doubles. The trick is to pass the variable’s
address in memory, rather than the variable itself. That’s what a
pointer is: it’s a variable that holds the address of another
variable. (It “points to” the other variable.) To declare a pointer
variable, we add a *
before its name. (In this context, *
has
nothing to do with multiplication.) So to declare a pointer to an int,
we use a line like the following:
int *myIntPointer; // can point to an int
Two new operators become useful when we’re working with pointers: &
and *
. The &
operator can be read as “address of”, and it returns
the address of its variable. We can use it to set a pointer, as we do
here:
myIntPointer = &value; // set pointer to be int's address
The *
operator can be read as “contents of”, and it returns the
variable to which the pointer points. We can use it to print out the
int that myIntPointer points to like this:
printf("%d\n", *myIntPointer); // prints pointed-to int
If you actually wanted to know the value held in myIntPointer
(not the
value held by the int it points to), use the %p
format specifier:
printf("%p\n", myIntPointer); // prints address (not int)
We don’t use the *
operator here, since we’re interested in the value
of the pointer itself.
For every variable type, there’s a pointer type that can point to it.
So there are int pointers (int*)
, char pointers (char*
), etc. There
are even pointers to pointers, such as the int**
type that points to
an int*. If you want to make a function that takes a pointer as an
argument, use the *
in its definition. This function takes a double
pointer, and returns an int
pointer:
int *doPointerThing(double *doublePointer) {
That’s how functions like scanf()
appear to modify their arguments.
They aren’t really doing so—the actual arguments are addresses, and
those can’t be changed by the function. But the values they point to
are easily altered. This means that in C, some arguments are really
functioning as extra return values in disguise.
Consider this function to swap the values of two int
variables:
// exchange 2 ints\
void swap(int *value1, int *value2) {
int temp = *value1; // temp <- contents of 1st pointer *value1
= *value2; // contents of 1st <- contents of 2nd *value2 =
temp; // contents of 2nd <- temp
}
This void function technically doesn’t return anything. Instead, it
just takes two int
pointers, and exchanges their contents. To call the
function, just do this:
swap(&x, &y); // x and y are just ints!
After it’s done, x
and y
will have switched values.
This style of coding is extremely common in C, but it can’t be done in Java. It lets a programmer return multiple values, or deal with primitive variables like they’re complex types. It’s efficient, because there’s no need to mess with arrays or complicated temporary data structures. By taking in the addresses of the ints, everything becomes streamlined. However it does require care and understanding that are not needed in Java.
Finally, the void
pointer is a generic pointer, that can point at
anything:
void *anythingPointer; // not limited to one type
This kind of pointer can be especially useful as a return value, when a function needs to effectively return any kind of pointer.
You might be surprised that when we declare a pointer, we attach the
*
to the name of the variable, and not the type. The following code
might look more intuitive to a Java programmer:
int* myIntPointer; // not standard C
The reason we don’t do this is because every single pointer variable
must have its own *
. If you were to do this:
int* pointer1, pointer2; // bug: pointer2 is an int
The compiler would associate the *
with pointer1
, while pointer2
would mistakenly be just an int
. The proper way to code this is as
follows:
int *pointer1, *pointer2; // both are int pointers
Figure 1: The stack and the heap. The stack is used to store
separate data for each function. Here, main()
has called func1()
,
which has called func2()
, and each needs its own space for local
variables and the like. Meanwhile the heap is a general-use space for
allocation. Here, seven different blocks are currently checked out from
the operating system.
The Heap & the Stack
Before we continue, we need to explain the heap and the stack. These are the areas of memory on a computer that we allocate to store variables and data structures.
First of all, in both C and Java we need memory every time we call a function. We need this memory for:
-
all the local variables and arguments
-
the address of the previous function, to return to upon completion
-
other miscellaneous overhead
A function stores all this data in a structure called a frame, and each currently running function has its own frame. Each time a function is called, a new frame is created for it. When a function ends, its frame isn’t needed anymore and it can be deleted. If a function is called recursively, the new incarnation of the function needs its own local variables and return address that are distinct from those of the old version. Therefore each call gets its own frame.
All of the frames are stored on a stack data structure, as shown in
Figure 1. Properly, this stack is called the execution stack, but it
is so important to a program that it is usually just called the
stack. The top frame will always be the frame of the currently
running function; it is called the active frame. When a program
first starts up, the only frame is that of the main()
function. But
when a new function is called, its frame gets created and pushed onto
the top of the stack, becoming the new active frame. When a function ends, its frame is removed and the one beneath it is once
more the active frame. Eventually the main()
function’s frame will be
the only one left, and when it terminates the stack is empty, and the
program is done.
By contrast, a program’s heap is organized more as a “lending library”of memory, as shown in Figure 1. Regions of contiguous memory can be”checked out” for the program’s use. It is managed by the computer’s operating system (OS), which has ultimate control over how much memory each active program gets. When a program needs memory from the heap, it uses a system call to flag the OS and request how much memory it needs. Since the OS is probably busy with other programs, this program must pause while it waits for an answer. Eventually the OS will be able to check if it has enough available memory. If so, it will likely approve the request, returning the address where the newly checked-out memory is located. But it may also deny the request and return a null pointer instead. If this happens, it will probably result in the program not being able to do its task, causing it to shut down. But assuming the request was approved, the program may use the memory for any needed data structure for as long as it wishes.
Note: The heap we’re talking about here is not at all related to the heap data structure that is used by heapsort and priority queues. Don’t confuse them.
When a program is done with memory that was checked out, it is expected to free the memory—returning it to the heap. In C this must be done explicitly, or else it will never be returned to the heap for future use. This condition is called a memory leak, and it is a serious bug. If one program continues to request memory without giving any back, it will slowly eat up more and more of the computer’s resources, until the computer slows to a crawl (because it is using virtual memory), and eventually other programs begin to fail due their memory requests being denied.
In general, data stored on the heap is more permanent. However, it takes more management, and allocating it can be slower because it requires the attention of the OS. By contrast, allocating data from the stack is more temporary. But it can be very fast because no system calls need to be made. Keeping this in mind, there are two major differences between memory management in Java and C:
-
In Java, local variables are always allocated from the stack, while arrays and objects are always allocated from the heap. But in C, you can often choose whether to allocate from the stack or the heap.
-
In Java, returning memory to the heap is done automatically by the Java garbage collector. This finds objects that aren’t being used anymore, and automatically frees them. But in C, you must explicitly free memory that you previously allocated (or else have a memory leak).
Allocating Arrays
The array declaration techniques you already know allocate memory from the stack:
int myArray[20]; // array of 20 ints on the stack
This has the problem mentioned above: the array is inherently linked to the function in which it is declared. When the function ends, so does the array. Soon, a new frame will be created, which overwrites the array.
There’s another problem here that is less obvious: an array stored on the stack is stored very close to the function’s return address. A malicious hacker might be able to trick your program into overwriting the return address, and executing hostile code!∗Thus, you should be very careful when indexing arrays with you allocated from the stack.
To make a permanent array from the heap, you must use the malloc()
function, and store the result into a pointer variable. The name
“malloc”means “memory allocation”, and it is roughly equivalent to
Java’s new keyword. It takes one argument: the size of the array in
bytes, and if successful it returns a pointer to the allocated memory.
Here is an example:
int *myArray = (int*)malloc(20 * sizeof(int)); // from heap
This requests space for 20 ints: on most computers sizeof(int) will be
4, and so this requests 80 bytes. Since malloc()
returns a generic
void, you must cast this into an int. You can permanently allocate
a string in a similar way:
// allocate space for a length-30 string (plus 0 at end) char
*someString = (char*)malloc(31 * sizeof(char));
In C, arrays and pointers are almost interchangeable. After all, what
is an array, but the address of a series of contiguous elements?
Because the elements are contiguous, the address of the array is also
the address of the first element, element 0. But that’s what a pointer
is: the address of an element. So when we cast the results of a
malloc()
as, for example, an int pointer, we can treat it just like an
int array. Look at this code:
∗This is the big problem with the gets()
function, which you should
never, ever use. It takes an input string from the user but cannot
control how big the input is, making it very easy for an intelligent
adversary to overwrite data and hijack your program.
int *myArray = (int*)malloc(20 * sizeof(int)); // allocate
*(myArray + 9) = 13; // set element 9 to 13
myArray[9] = 13; // set element 9 to 13 (same thing)
The second and third lines do the exact same thing: they take
myArray[9]
(which is 36 bytes away from myArray[0]
on a system
with 4-byte ints), and set it to 13. It is very common practice to use
malloc()
to allocate a pointer, but from that point on use it as an
array.
This last detail illustrates an important detail about pointer
arithmetic. You may add or subtract an int from a pointer. But when
you do so, the int is first multiplied by the size of the type that the
pointer points to. That is why in the above case adding 9 to an
address resulted in an address 36 bytes away. This helps you to treat
pointers like arrays. Further, if you subtract one pointer from
another, the result is divided by the type size. So myArray[9] - myArray[0]
is in fact 9, even though the two addresses are 36 bytes
apart.
Remember, when your program calls malloc()
, it only requests memory
from the heap. This request is never guaranteed: the operating system
can always decide to deny it (usually when it cannot find enough
space). If the request gets denied, malloc()
will return a NULL
pointer, which is address 0. Thus, it is usually prudent to test that
the program gets a valid address back. That way if there’s a problem,
the program can terminate in a controlled manner:
if (!myArray) {
// allocation was denied; time to shut down gracefully
}
(Remember that ! works on ints in C, since there are no booleans. So
the code if (!myArray)
is equivalent to the code if (myArray ==
0)
.) In Java, the garbage collector takes over your program
periodically. It scans your objects and arrays, finding ones that are
no longer being used and returning their memory to the heap,
automatically. This makes the program less efficient, but it does make
development much easier. However, C does not do this for you. Rather,
you are expected to know when memory from the heap is no longer
needed, and to return it explicitly. This is done via the free()
function:
free(myArray); // give myArray back to the heap
If you do not free memory properly, you will have a memory leak
bug—with all the bad effects detailed above. Every call to malloc()
must have a corresponding call to free().
To allocate a two-dimensional array, you need a pointer to a pointer. For example, the following code will allocate a 4 x 5 int array from the heap:
// start by mallocing the array of arrays
int **array = (int**)malloc(4 * sizeof(int*));\
for (int i=0; i<4; i++) { // now for each subarray...
array[i] = (int*)malloc(5 * sizeof(int)); // ...malloc
}
A three-dimensional pointer would require a ***int
type, and so on.
Also remember that a string is already a char array. So if you want a
string array, it must really be a 2D char array. This is why the
main()
argument argv
, which holds an array of the command-line
arguments, needs to be of type char**
.
Segmentation Faults
One of the most common errors you will get in a C program is a segmentation fault. A segment is a region of memory on your computer, that your operating system allocates out to different programs. Both the heap and the stack are made of segments. However, the OS will not permit programs to access the wrong segment. This is for both protection and security: one rogue program shouldn’t be able to interfere with the others or to bring the whole system down. Thus, whenever you try to access memory outside of your allotted region, the OS will forcibly halt your program. This is what a segmentation fault is.
These “seg faults” can be particularly frustrating, because your program often won’t give you any clues as to what caused it. (Unlike Java, that will usually give you a helpful error message when it crashes.) Here are some of the most common causes behind seg faults:
-
You tried to use a null pointer, or a pointer that hasn’t been properly set yet. (This would usually be a
NullPointerException
in Java.) -
You tried to access an array beyond its bounds, reaching into a forbidden area of memory. (This would be an
ArrayIndexOutOfBoundsException
in Java.) -
You requested too much memory in a
malloc()
, and it returned anull
pointer that you followed. (This would be anOutOfMemoryError
in Java. -
Your recursive function could not stop and kept recursing, running out of stack space. (This would be a
StackOverflowError
in Java.) -
You accidentally overwrote the return address in a frame, and when the function ended you tried to follow it back to a forbidden address. (This cannot happen in Java.)
-
You tried to use a pointer that has already been freed. (This cannot happen in Java.)
-
You accidentally overwrote a pointer variable, and then tried to follow it to a forbidden address. (This cannot happen in Java.)
-
You tried to use an array stored in the stack, after its function finished and its frame was overwritten. (This cannot happen in Java.)
Remember that your bug might not cause a crash immediately. It could corrupt your data in some way that only causes a segmentation fault later on.