Skip over navigation

Why Use Pointers?

Dynamic Memory Allocation

Problems

Problems

Your computer science professor has just asked you to write a program for him (he would do it himself but he's too busy grading your assignments). The program is supposed to read in all of his students' grades and then print them back out in sorted order. Simple, right? You grab your trusty bubble sort algorithm, write up a function to sort an array of data, then write a simple little program to read in all of the numbers, sort them, and print them back out, maybe something like:


int main()
{
	int grades[100], i=0;
	do {
		printf("Enter grade #%d:\n", i+1);
		scanf("%d\n", &grades[i]);
		i++;
	} while(&grade[i] != -1); /* the last grade is a -1 */
}
Easy, right? You're proud of your program and you head to the professor, code in hand and smile on face. The only problem is, when you get to the professor, he looks at your code and he doesn't have a smile on his face. Why?

There could be many reasons why your professor isn't happy with the code above. For example, there's not much in the way of error checking. More importantly though, he's probably a little wary of that 100 you have there in the code. You realize that, of course, he's got over 100 students in his class, so we'll just change this number to 500, allow him to have up to 500 students. You go home that night, again feeling very proud of yourself. The next year, though, you get a call from that professor again, and he's upset. Seems this year he had an influx of students and your program wasn't robust enough to handle all of them; you hadn't set aside enough memory and as such your program was of no more use to him. You think to yourself, "Back to the drawing board; there must be an easier way so that I don't have to keep rewriting this program every time the professor's class size changes." You're in luck, there is an easier way. Or at least a better one.

Static Memory

Up to this point, the memory we've been using has been static memory. What does this mean? Static memory is memory that is set aside automatically by the compiler for your program. When you declare a variable, such as the int arr[100] array we declared in the above program, you're telling the computer to set aside space for 100 integers. The computer of course obliges. The problem with this is that the computer needs to know how much memory to set aside before your program starts running. When you run your program, the computer gives it the memory it needs to hold all of the variables you've declared; in other words, you've statically allocated memory.

But this method fails in the above case with the professor. What we'd like to be able to do is to create an array whose size is specified at run time. This time, the computer doesn't oblige; it fact, neither does the compiler. If you try to write code that looks like:


int steve;
scanf("%d\n", &steve);
int arr[steve];
the compiler will refuse to build and executable. The reason is that at compile time the compiler has absolutely no idea how big an array arr will need to be. The user could enter any value he wanted for steve, which means the arr could be any size at all. Since the compiler needs to know how much space to tell the computer to set aside, this code won't work.

So, how do we get around this? The answer is dynamic memory allocation, and for that, we need pointers.

Dynamic Memory Allocation

Dynamic memory allocation is a process that allows us to do exactly what we're looking to do above, to allocate memory while our program is running, as opposed to telling the computer exactly how much we'll need (and for what) ahead of time.

With dynamic memory allocation, while the program is running, the program requests more memory from the computer. If there's enough memory available, the computer will grant the program the right to use the amount it requests.

Dynamic Memory and Pointers

When we ask the computer for memory dynamically, what do you think it gives us? That's right, an address. When we ask the computer for memory, it goes and sees what memory it has available. Assuming it has enough to give us, the operating system will set aside the amount of memory we requested and give us that memory's address so that we can then use it. How do we store an address? In a pointer.

The functions we'll use to grab dynamic memory return a pointer to that memory (or if for some reason we couldn't get the memory we requested, they'll return the NULL value). We can then use that memory through the pointer just like it was ours to begin with, setting values in the memory, getting values from the memory, etc.

As this tutorial is primarily on pointers, we won't delve too much into memory allocation here as the point of this section is simply to point out that you can do dynamic allocation. However, we will still show you a few basics.

Allocating memory

Allocating memory in C primarily revolves around two functions: malloc() and free(). malloc() is used to allocate memory (to request it) and free() is used to give it back. In C++, the operators new and delete are used to accomplish similar tasks, however due to the additional complexity surrounding these operators, we will not discuss them here (refer to the C++ SparkNote for more information).

malloc()

So how exactly do we ask the system for memory? With the function malloc(). There are other functions as well, all part of the malloc() family, but we'll only be discussing malloc() here as it is the most common. malloc() takes a single argument, the number of bytes to allocate, and returns a pointer to the allocated memory if the allocation was successful, or NULL otherwise. For example, to allocate a chunk of memory 1024 bytes in length (1KB), we would use the instruction malloc(1024).

Figure %: ptr = malloc(1024);

Normally, however, we wouldn't allocate some random number of bytes; we would want to allocate enough space to hold some specific data, some number of variables. As such, a commonly used operator is the sizeof() operator. sizeof() takes a type as an argument and gives back the size, in bytes, of that type. It is often used in conjunction with malloc to allocate enough space to hold a single variable or array of variables. The sizeof() operator is particularly useful for programs that must run under more than one operating system as the length in bytes of a data type may not be the same on different systems.

Some examples:

To allocate: We use:
An integer malloc(sizeof(int));
An unsigned character malloc(sizeof(unsigned char));
An array of 21 longs malloc(21 * sizeof(long));
An array of 10 pointers to integers malloc(10 * sizeof(int *));

So how can we use this in real code? Here's an example. Remember our unhappy professor? We can easily change our array grades program such that the size of the array can be set at run-time (meaning while actually running the program as opposed to at compile time).


int main()
{
	int i=0;
	int *grades;
	int size;
	
	printf("Enter the number of students:\n");
	scanf("%d\n", &size);
	grades = malloc(size * sizeof(int));
	
	do {
		printf("Enter grade #%d:\n", i+1);
		scanf("%d\n", &grades[i]);
		i++;
	} while(i<size);
}
So what does this do? It reads in the size of the array to create into the variable size. It then uses malloc to allocate enough memory to hold that many integers. As the memory it allocates will be continuous, we can use this memory just like an array. We store the address of that memory into grades and the rest program is basically as it was above.

There are still a few key elements missing here. The first, a very important part of programming, is error detection. Remember that if we try to dereference a NULL pointer, it will very often cause something bad to happen, like making our program crash. If for some reason malloc() cannot allocate memory, it will return NULL. So there exists the possibility that if malloc() cannot allocate the requested memory, the value of NULL will be stored in grades, and then when we try to access the ith element of grades, we will have a problem. To prevent problems like these, we need to check to see if the result of the call to malloc() returns NULL. If it does, there was an error, and we need to handle it. How you handle it depends on how you're using the memory, but in this case we'll just display an error and exit the program.


int main()
{
	int i=0;
	int *grades;
	int size;
	
	printf("Enter the number of students:\n");
	scanf("%d\n", &size);
	if ((grades = malloc(size * sizeof(int)) == NULL) {
		printf("Error: Unable to allocate memory for array\n");
		exit(1);
	}
	
	do {
		printf("Enter grade #%d:\n", i+1);
		scanf("%d\n", &grade[i]);
		i++
	} while(i<size);
}

The second key element missing here deals with giving back this memory we've allocated when we're done using it.

free()

Thus far we've only discussed allocating memory. When your program requests memory and the operating system gives it, the operating system marks that memory as "in use" and will not allow any other application to use it (in fact, if another application does attempt to use it, the operating system will most likely try to kill that program; remember what happens when we try to dereference a pointer that doesn't point to memory we own). If your program never frees up the memory it requested once it is done using it, no one else will be able to use it. So, when we're done using memory we've requested, we need to give it back so other programs can use it. It's that easy.

To free memory in C we use the function free(). The free() function takes one argument, a pointer to the memory we want to free. This memory must have been previously allocated with free()'s counterpart, malloc(). For example, if we have an integer pointer int *steve and if steve points to some memory your program previously requested, to free it, all we have to do is make the call free(steve). Easy enough. There are, however, a few things to be careful of when using free():

  • Don't free() memory twice. When you free memory, you relinquish your rights to it. After you've free'd memory once, it is no longer yours. If you try to free it again, what you're really trying to do is free memory that you don't own; it doesn't matter that you once owned it, you don't anymore. So, free()ing memory twice is like coding your program with the explicit instruction to crash.
  • Don't free() static memory, as in:
    
    int arr[100];
    free(arr);	/* bad! */
    
  • Don't free anywhere but the beginning of a chunk of memory you've allocated. For example, if you allocate a block of memory and store the address into a variable int *steve, don't do something like free(steve + 5). This will result in the computer attempting to free up the memory at steve + 5 which is not the exact address previously returned by the operating system. This most likely won't crash your program, but it might result in strange behavior.

Follow Us