Memory Management in C
There are two ways in which memory can be allocated in C:
• by declaring variables
• by explicitly requesting space from C
We have discussed variable declaration in other lectures, but here we will describe requesting dynamic memory allocation and memory management.
C provides several functions for memory allocation and management:
• malloc and calloc, to reserve space
• realloc, to move a reserved block of memory to another allocation of different dimensions
• free, to release space back to C
These functions can be found in the stdlib library
What happens when a pointer is declared?
Whenever a pointer is declared, all that happens is that C allocates space for the pointer.
For example,
char *p;
allocates 4 consecutive bytes in memory which are associated with the variable p. p’s type is declared to be of pointer to char. However, the memory location occupied by p is not initialised, so it may contain garbage.
It is often a good idea to initialise the pointer at the time it is declared, to reduce the chances of a random value in p to be used as a memory address:
char *p = NULL;
At some stage during your program you may wish p to point to the location of some string
A common error is to simply copy the required string into p:
strcpy(p, “Hello”);
Often, this will result in a “Segmentation Fault”. Worse yet, the copy may actually succeed.
//a.c
#include <stdio.h>
main() {
char *p;
char *q = NULL;
printf("Address of p = %u\n", p);
strcpy(p, "Hello");
printf("%s\n", p);
printf("About to copy \"Goodbye\" to q\n");
strcpy(q, "Goodbye");
printf("String copied\n");
printf("%s\n", q);
}
When p and q are declared, their memory locations contain garbage. However, the garbage value in p happens to correspond to a memory location that is not write protected by another process. So the strcpy is permitted.
By initialising q to NULL, we are ensuring that we cannot use q incorrectly. Trying to copy the string “Goodbye” into location 0 (NULL) results in a run-time Bus Error, and a program crash.
So, how can we use memory properly?
Before we can use a pointer, it must be pointing to a valid area of memory. We can use malloc to request a pointer to a block of memory (calloc to request an array of zero-value initialised blocks).
void *malloc(size_t byteSize)
void *calloc(size_t numElems, size_t byteSize)
//b.c
#include <stdio.h>
#include <stdlib.h>
main() {
char *q = NULL;
printf("Requesting space for \"Goodbye\"\n");
q = (char *)malloc(strlen("Goodbye")+1);
printf("About to copy \"Goodbye\" to q at address %u\n", q);
strcpy(q, "Goodbye");
printf("String copied\n");
printf("%s\n", q);
}
How do we know if the memory allocation has been successful?
Malloc (and calloc) will return a non-NULL value if the request for space has been successful, and NULL if it fails. Using the result of malloc (or calloc) after it has failed to locate memory WILL result in a run-time program crash.
//c.c
#include <stdio.h>
#include <stdlib.h>
main() {
char *q = NULL;
printf("Requesting space for \"Goodbye\"\n");
q = (char *)malloc(strlen("Goodbye")+1);
if (!q) {
perror("Failed to allocate space because");
exit(1);
}
printf("About to copy \"Goodbye\" to q at address %u\n", q);
strcpy(q, "Goodbye");
printf("String copied\n");
printf("%s\n", q);
}
The same applies to reading data from a file. If you use fscanf, etc., you must ensue that there is enough space to store the input, because the functions won’t
Freeing space
When space is allocated using the alloc family of functions, the space is allocated permanently until the program terminates, or it is freed.
Local variables are destroyed when their enclosing function terminates. Although the values are not necessarily overwritten, C may allocate the space to some other requesting process
//d.c
#include <stdio.h>
char *foo(char *);
main() {
char *a = NULL;
char *b = NULL;
a = foo("Hi there, Chris");
b = foo("Goodbye");
printf("From main: %s %s\n", a, b);
}
char *foo(char *p) {
char q[strlen(p)+1];
strcpy(q, p);
printf("From q: the string is %s\n", q);
return q;
}
In this example, q is a variable local to foo. A string created in main is passed to foo and copy to q. The address of q is returned to main, where there is an attempt to preserve and use the strings. The result is disastrous.
//e.c
#include <stdio.h>
#include <stdlib.h>
char *foo(char *);
main() {
char *a = NULL;
char *b = NULL;
a = foo("Hi there, Chris");
b = foo("Goodbye");
printf("From main: %s %s\n", a, b);
}
char *foo(char *p) {
char *q = (char *)malloc(strlen(p)+1);
strcpy(q, p);
printf("From foo: the string is %s\n", q);
return q;
}
In this example, however, the space is requested legitimately, and, although q, a local variable holding an address of the string, is destroyed when foo terminates, the string itself is preserved and can be used safely in the calling function.
The correct way to release the space is to use free().
//f.c
#include <stdio.h>
#include <stdlib.h>
char *foo(char *);
main() {
char *a = NULL;
char *b = NULL;
a = foo("Hi there, Chris");
free(a);
b = foo("Goodbye");
free(b);
printf("From main: %s %s\n", a, b);
}
char *foo(char *p) {
char *q = (char *)malloc(strlen(p)+1);
strcpy(q, p);
printf("From foo: the string is %s\n", q);
return q;
}
If free(b) is omitted, then “Goodbye” can be seen to be written to the location of “Hi there, Chris”.
The free function has the following syntax.
void free(void *ptr)
What happens when the amount of space allocated turns out to be too small?
void *realloc(void *oldptr, size_t newsize)
You’ve been accepting characters from the keyboard into some previously allocated bytes, but the user keeps typing characters and is going to overflow the memory allocation… what do you do?
What you’d want to do, of course, is keep track of the number of characters being written, and when you’re almost out of space, request a larger block from C, copy the old string into the new location, and free the space associated with the old string – which is practically what the realloc function does.
//g.c
#include <stdio.h>
#include <stdlib.h>
char *readline(char *, int *);
char *allocmem(char *, int);
main() {
char *p = NULL;
int max = 10;
p = (char *)malloc(max);
if (!p) {
perror("Memory allocation error 1");
exit(1);
}
*p = ‘\0’;
p = readline(p, &max);
printf("User input\n%s\n", p);
}
char *readline(char *p, int *max) {
char c;
int count = strlen(p);
while ((c = getchar()) != EOF) {
if (count == (*max-1)) {
*(p+(*max-1)) = '\0';
*max += 10;
p = allocmem(p, *max);
if (!p) {
perror("Memory allocation error 2");
exit(1);
}
}
count+=1;
strncat(p, &c, 1);
}
return p;
}
char *allocmem(char *p, int max) {
char *q = NULL;
q = (char *)realloc(p, max);
if (!q) {
perror("hi!");
exit(1);
}
return q;
}
Memory Management and Data Structures
Reading keyboard text and keeping each line input in a linked list…
/*h.c. The program reads lines of input, and stores each line in a linked list. Eventually the list is printed */
#include <stdio.h>
#include <stdlib.h>
struct lineList {
char *line;// a line of input
struct lineList *nextLine; // pointer to the next line
};
// global variable pointing to head of the linked list
struct lineList *theHead = NULL;
char *readline(char *, int *, struct lineList *);
char *allocmem(char *, int);
struct lineList *makeElem(char *, struct lineList *);
void printList(struct lineList *);
main() {
char *p = NULL;
struct lineList *head = NULL;
int max = 10; // initial size of input array
extern struct lineList *theHead;
p = (char *)malloc(max); // request space for the input array
if (!p) {
perror("Memory allocation error 1");
exit(1);
}
*p = '\0'; // we use strlen later, so initialise input array
p = readline(p, &max, head); // read all the input data
printList(theHead); // print all the input data
}
char *readline(char *p, int *max, struct lineList *elem) {
… // some code has been removed
if (c == '\n') { // if a newline is encountered in the input
elem = makeElem(p, elem); // copy the input line (p) to an element
free(p); // we’re going to resize the input array p, to save space
*max = 10; // set max to 10 again (same as in main())
p = (char *)malloc(*max); // request space for the input array
// NB: the lines from free to here could have been replaced by
// p = (char *)realloc(p, 10);
// check that the request was successful (code not shown)
*p = '\0'; // initialise array so string functions will work
count = 0; // reset count
continue;
}
…
struct lineList *makeElem(char *p, struct lineList *elem) {
// add an element to the linked list
//
struct lineList *temp = elem;
struct lineList *head = elem;
extern struct lineList *theHead;
if (!head) { // if the linked list hasn’t been created yet
//request space for it
head = (struct lineList *)malloc(sizeof(struct lineList));
if (!head) {
perror("Couldn't allocate space for head");
exit(3);
}
theHead = head; // set the global variable
//request space to the input line
head->line = (char *)malloc(strlen(p));
if (!head->line) {
printf("Couldn't allocate space for %s because", p);
perror("");
exit(4);
}
// copy the input line to the element
strcpy(head->line, p);
head->nextLine = NULL; // set the pointer to next element to NULL
return head;
}
// otherwise, if the linked list exists already
// look for the last element in the list
while (elem) {
temp = elem;
elem = temp->nextLine;
}
// create a new element, storing its address in the old last element
temp->nextLine = (struct lineList *)malloc(sizeof(struct lineList));
if (!temp->nextLine) {
perror("Failed to allocate list head");
exit(2);
}
// request space to store the input line in the element
temp->nextLine->line = (char *)malloc(strlen(p));
if (!temp->nextLine->line) {
printf("Couldn't allocate space for %s because", p);
perror("");
exit(4);
}
//copy the input line to the new element
strcpy(temp->nextLine->line, p);
temp->nextLine->nextLine = NULL;
return head;
}
// print the lines in the linked list, starting from the first element
void printList(struct lineList *head) {
struct lineList *curr = head;
// loop while the address of the element is not NULL
// NULL indicates the end of the linked list
while (curr) {
printf("%s\n", curr->line);
curr = curr->nextLine;
}
}