Aussie AI
C++ Pointer and Memory Bugs
-
Bonus Material for "Generative AI in C++"
-
by David Spuler, Ph.D.
Pointer and Memory Bugs
The use of pointers in C++ can be fraught with danger. There are some common pitfalls to watch out for and even experienced programmers have not always managed to avoid them. Some of the insidious errors include:
- Null pointer dereference
- Uninitialized pointers
- Not enough memory allocated
- Null byte forgotten in string allocation
- Dangling pointers
- Double-delete or double-free
- Delete or free of non-allocated address
- Mismatched new/malloc with delete/free
- Returning the address of a local variable
- Memory leaks
- Class member not initialized by constructor
Confusing NULL, nullptr, 0, '0', and '\0'
There are so many versions of zero that it isn't surprising that they are misused. Novice programmers may write '0' (digit) when they mean the null byte that terminates a string. The correct method is to use 0 or preferably '\0' which is equivalent and considered better style by many because it makes it clear that we are using character zero rather than integer zero.
It is a common mistake for programmers to use "NULL" when they really mean the string terminating byte. Some reasons for the confusion are that the ASCII name for character zero is "NUL" (with one L), and many C++ programming texts refer to it as the "null byte." Consider the following example:
char s[10] = ""; if (s[0] == NULL) /* if empty string */
Fortunately, on many compilers the macro NULL expands out to 0, or type cast the pointer type NULL to the zero byte. Hence, the program does not fail. On other platforms, NULL may expand out as (void*)0, but even here there is unlikely to be a run-time failure as all the implicit type conversions preserve the intended meaning of the test.
The proper usage in C++ for pointers is "nullptr", but using this here is also a failure. Depending on the usage and the compiler, this should be at least a warning, and often an error.
No memory allocated for string
A particularly common example of an error involving strings is to use a char* string type without allocating any memory for the string. The underlying problem is usually a misunderstanding of strings. This error is shown in the code:
char *s; strcpy(s, "Hello world\n"); /* WRONG */
Here s is an uninitialized pointer variable and the strcpy function will store the copied string at whatever value this pointer holds, leading to undefined behavior (usually a run-time failure). Fortunately, many compilers will warn that s is used before being initialized in this example. However, the misunderstanding about strings will lead to many more complicated examples of such errors that the compiler will not detect
Dereferencing a null pointer
The modern C++ syntax is "nullptr" but NULL or "0" cause the same problems. Whatever your stylistic preference, the crash is the same.
A common error is to dereference a pointer that is NULL, which causes an immediate crash. It is one of the most common causes of a "segmentation fault" error on Linux machines.
Dereferencing a NULL pointer can occur with either of the two indirection operators (i.e., *ptr and ptr−>field) and also via array indexing of a pointer variable (i.e., ptr[i]), which is equivalent to using the * operator. Dereferencing NULL is most common when using dynamic data structures, but can occur any time you forget to set a pointer. An example is given below:
if (ptr->next != NULL && ptr != NULL) /* WRONG ORDER */
When ptr is NULL, the first condition "ptr−>next" is calculated first, causing a NULL pointer to be dereferenced. Simply reversing the order of the operands solves the problem:
if (ptr != NULL && ptr->next != NULL) /* CORRECT */
If p is NULL, the first condition evaluates as false and in this case we are thankful for the short-circuiting of the && operator, which causes the second condition to be skipped, avoiding the dereference of the NULL pointer.
The new operator does not initialize memory
The new memory allocation operator also usually leaves its memory uninitialized when used to allocate a primitive type. There is no problem when allocating a class type, since the constructor is automatically called (and this should initialize all data members). However, when allocating memory for types without constructors, namely, all primitive types, the memory is not initialized:
Obj *p = new Obj(1); // Correct char *s = new char[100]; // Buggy
Dangling references
Dangling pointer references occur when a pointer is not NULL, but points to the wrong place. More precisely, when it points to memory that is not at present controlled by the program. Some of the ways this can occur include:
- Bogus integer addresses (e.g. "char *ptr = (char*)3;")
- Already-delete'd allocated memory (memory allocation with "new").
- Already-free'd memory (memory allocation with malloc/calloc)
- Pointer to a local variable inside a function that has finished.
This is a common problem in algorithms involving linked data structures, where pointers point to blocks that have already been freed. The most well known example is the method of deallocating all blocks on a linked list. The incorrect code is:
for (ptr = head; ptr != NULL; ptr = ptr->next) // Bug free(ptr);
This code applies the -> operator to a dangling reference, since ptr has already been deallocated by "free" called before the -> operation. In this particular case, many implementations are forgiving since the recently freed block is unlikely to have been changed in a few microseconds. The correct code uses a temporary variable:
for (ptr = head; ptr != NULL; ptr = next_node) { next_node = ptr->next; // compute ptr->next before free() free(ptr); }
Another common occurrence of dangling references is saving structures containing character pointers to a binary file. If strings have been allocated using malloc or the C++ new operator, when saving the structs only the pointers are being saved and not the actual character strings themselves. When the structs are loaded back in, the pointers are dangling references and the strings have been lost.
Garbage and memory leakage
Garbage refers to memory allocated by new or malloc/calloc that no longer has any pointer pointing to it. It is unused by the program, and unusable by the program because neither the new operator nor malloc reuse the memory. This problem in a program is often called a "memory leak."
In a sense, garbage is the opposite problem to dangling pointerreferences — dangling references are pointers without allocated memory, garbage is allocated memory without a pointer to it. The problem is not a major one unless memory is short, because the memory is unused, and just harmlessly sits there. However, if memory is limited, then gradually accumulating garbage can lead to a program eventually running out of memory (and crashing if out-of-memory is not tested for). This is a common problem in server processes that run for a long time handling requests.
Try to be careful to free memory as soon as it is no longer needed. There are various tools and libraries that can help you report on allocated memory usage and track down memory leaks.
Or here's another pro tip for server backends: just restart the program. If your program is a server, service or a daemon, then you can expend a lot of effort tracking down memory leaks. Or you can just configure the launcher to restart the program after 1000 requests, or some other reasonable number, before the program has leaked too much memory.
Pointer and object confused
It is common to confuse a pointer with what it is pointing to. Assigning one pointer to another is not the same as assigning the items they point to. For example, if the statement "p=q" is used instead of "*p=*q", when *q is modified, *p is also modified (i.e. the value p points to is also modified).
String char* confusion: One common example of this occurs when copying "char-pointer" type character strings:
char *s1 = "Hello"; char *s2 = s1; // Incorrect (pointer copy)
If we want to copy a "char*" type string, this is (possibly) the correct way:
strcpy(s2, s1); // Correct? (string copy)
But not if you've done it this way:
char *s2 = ""; strcpy(s2, s1); // Bug
Deallocating a nonallocated memory block
Programmers occasionally make the mistaken assumption that any memory can be deallocated by the free library function or the C++ delete operator. Howev er, it is a bad error to use free or delete on a pointer to memory that has not been allocated by malloc, calloc, or realloc. The effect of this is not defined, but usually causes a program failure. A variable, such as a global array, cannot be freed to the heap.
int arr[10]; ... free(arr); /* ERROR */
This is also incorrect:
delete arr; // ERROR (2 errors actually! See later)
The problem is that the array variable was not dynamically allocated by the corresponding allocation library (i.e., malloc or calloc for free, and new for delete). Therefore, the block has the wrong format and the deallocation can cause a strange run-time error. Memory allocated by malloc, calloc, or new has a special format — in many implementations it has a few information bytes before it.
I recently discovered this form of error in a piece of software written some time ago, namely a tokenizer. The program used structures that contained a token and a string pointer:
struct node { int token; char *text; /* text corresponding to token */ struct node*next; /* linked list pointer */ };
At a number of points in the program it was convenient to change a node from an identifier to whitespace (easier than removing the node from the linked list). The code used to achieve this was:
ptr->token = WHITESPACE; ptr->text = " ";
The problem with this method was that when these nodes were no longer needed, they were deallocated using free. Deallocating the linked list nodes was not a problem in itself, but the text fields were also deallocated (having been allocated much earlier in the tokenizer). However, any text fields that had been assigned a string constant as above were no longer pointing to an allocated block. Once the error was identified (using a memory allocation debugging package), the solution chosen was to modify the characters in the existing allocated string:
ptr->token = WHITESPACE; strcpy(ptr->text, " ");
Mixing malloc/calloc and new for allocations
An important point to remember is that the malloc-based allocation scheme may be different from the C++ new and delete operators. It is an error to use delete to deallocate a block allocated using malloc or calloc. Similarly, applying free to a block allocated using new is also incorrect:
p = malloc(20); delete p; // ERROR
This reverse is also an error:
p = new char[20]; free(p); // ERROR
Double deallocation
Another common memory allocation error is deallocating the same address twice. The various possible errors are similar:
- Double delete
- Double free
- Mixed delete and free
The first deallocation call is correct, but the second call is an error — the memory is no longer allocated. In most environments this will result in a run-time error, possibly immediate abnormal termination.
Applying the C++ delete operator to the same address twice is a critical error. The error of deleting an address twice is often the result of having a missing copy constructor or assignment operator in a class that allocates dynamic memory for one of its data members (e.g., a dynamic string class).
Not allocating enough memory for strings
Problems occur if not enough memory is allocated for an object. Storing an object at this address may overwrite other variables, or cause a crash. The most common example of this is forgetting that a string has an extra zero character at the end. If memory is not allocated for this extra character, there is the potential for a program failure. For example, the use of strlen below is wrong. The strcpy function copies the terminating zero, but strlen does not count it.
new_str = malloc(strlen(str)); strcpy(new_str, str);
The correct call to malloc is:
new_str = malloc(strlen(str) + 1);
It is also possible to have the same problem using C++ allocation. When allocating memory for strings the correct size must be supplied to the new operator. An example of the error in C++ is:
new_str = new char[strlen(str)]; // INCORRECT strcpy(new_str, str);
Again the correct method is to add 1 to the length:
new_str = new char[strlen(str) + 1];
Another common error with this string allocation idiom is to accidentally place the "+1" inside the strlen argument list:
new_str = malloc(strlen(str+1)); /* WRONG */
Or with the new operator:
new_str = new char[strlen(str+1)]; // WRONG
The compiler will not produce an error because it is legal to add 1 to a pointer type. This code will usually allocate 2 bytes fewer than are required, because not only is the "+1" not adding to the count of bytes, but also strlen calculates the length of the string starting at &str[1] rather than character &str[0].
Address arithmetic and incrementing pointers
When accessing bytes of data, it is common to use pointers to step through. It is important to remember that ++ and -- increment a pointer by a number of bytes (one or more), depending on the type of the pointer. Incrementing a char* pointer will move along one char (i.e., byte); incrementing an int* pointer will move along one int. In general, the expression:
p++;
is equivalent to:
p = (char*)p + sizeof(*p);
Similarly, adding an integer to a pointer does not necessarily add that number of bytes — the change is implicitly multiplied by the size of the object the pointer points to (i.e., the type of the pointer).
If access to memory at a byte level is needed, use pointers of type char* because the size of char is one byte. Alternatively, it may be useful to use type casts to char* in expressions that perform fancy address calculations.
Bracket errors with the new operator
There are fewer opportunities for typographical errors involving the new operator than for malloc, because new is type-safe. However, there is one dangerous problem I've been bitten by. Consider the following code:
char* my_strdup(char *s) { int len = ::strlen(s) + 1; char *temp = new char(len); ::strcpy(temp, s); return temp; }
Can you see the error? Perhaps you can, but the C++ compiler can't, and in fact, the program will often run correctly despite a memory allocation problem. The problem is the expression involving the new operator — it should be "new char[size]" with square brackets instead of round brackets. The compiler views "char(size)" as a pointer-to-function type, and will therefore allocate enough bytes to hold a function pointer, typically 4 or 8 bytes. Therefore, for any reasonably long string, the new operator returns too few bytes and the strcpy call is overwriting bad memory
Pointer alignment
Some implementations have restrictions on the addresses that particular data types can be stored in. A typical situation is that an integer must be stored on a word boundary (i.e., an even-valued address); on such a machine the following code would probably cause a run-time error such as a "bus error" or "fixed up unaligned data access":
int x = 0; char *p = (char*) &x; *(int*)(p+1) = 10; /* ERROR */
The problem is that since &x is word-aligned, the expression p+1 will not be word-aligned. The attempt to store an integer through a nonaligned address will cause some form of error (on some machines). Although the above example is obviously contrived, there are situations in practice where alignment must be considered. Fortunately, the most common uses of addresses are safe:
- addresses returned by malloc, calloc, or realloc
- pointers to characters (i.e., strings)
Any address returned by malloc is guaranteed to be compatible with the most restrictive alignment requirement for all data types for the given implementation. Therefore, it is safe to store any data type, whatever its alignment, at an address returned by malloc or calloc. Furthermore, the char* type (and its qualified variants) will have the least restrictive alignment requirements; typically there is no restriction on the addresses that are legal.
Note that there is a major difference here between the malloc function and new operator in that malloc cannot determine what type of object is being allocated, whereas the C++ compiler can determine at compile-time the type of object being allocated by new and perform the allocation accordingly.
malloc must have the most general alignment, but new need not. Some implementations of lint attempt to diagnose instances where alignment error may arise, but are notoriously bad at it. The most common problem is that lint does not know about malloc or calloc, or about the type void*, and will complain about every call to malloc or calloc.
Aliasing and pointer variables
The term aliasing refers to a general idea of two names referring to the same object. If two names x and y are both aliases for the same object, then any modification to the object via x will also affect y. Two names can be aliases in a few different ways — they might be two pointers pointing to the same address, or two C++ reference parameters referring to the same variable or object. Although aliasing is a more common problem for compiler implementors because it prevents many winning code optimizations, there are also some errors that programmers must avoid.
Aliasing problems can involve pointers because any number of pointer variables can point to the same object. This can lead to hidden dangers if code assumes that two pointers are pointing at different objects. For example, consider the situation when one string pointer is copied so that it contains the same characters as in the string pointed to by the other string pointer. The obvious code sequence will be:
free(s1); /* deallocate old string */ s1 = malloc(strlen(s2) + 1); strcpy(s1,s2); /* copy the string */
This will work correctly in all situations but one. Consider what happens when s1 and s2 are aliases for the same string (i.e., s1==s2). The free operation deallocates the memory they point to and then the strlen and strcpy operations read the characters from a location that has just been deallocated (i.e., an illegal address). This may or may not cause some form of program failure. Worse still, after the code fragment, the pointer s2 is pointing at a deallocated location and its string value may change in an undefined way (e.g., if the memory is reallocated by a later call to malloc). Therefore, the problem of the alias of s1 and s2 can cause run-time program failure. The programmer should always be careful to consider whether two pointers could ever be equal, and if necessary, use special code for this case.
Pointer aliases can be a problem in less obvious ways. For example, the strcpy function has undefined behavior if the two strings overlap (i.e., if the two pointers are aliases for parts of the same string). A strcpy call of the following form may cause strange behavior:
strcpy(s + 1, s);
Similarly, incorrect behavior can occur when sprintf has aliases between its destination string and its arguments. For example, the following method of adding a prefix to a string is erroneous:
sprintf(s, "Prefix %s", s);
Interestingly, the main reason for the appearance of the "memmove" standard library function is as a safe replacement for memcpy when both pointers could be aliased to addresses within the same memory block.
Temporary Objects used after Destruction
The classic example of this error is concatenation using an overloaded + operator in a string class with dynamically allocated memory and a char* type conversion member operator. A fully coded example of such a string class is given below:
#include <stream.h> #include <string.h> class String { private: char *str; public: String() { str = new char[1]; str[0] = '\0' } String(char *s); String(char *s, char *s2); ~String(); void operator =(const String &s); String(const String &s); operator char*(); // TYPE CONVERSION OPERATOR friend String operator+(String &s1, String &s2); }; String::String(char*s) // Constructor { str = new char[strlen(s)+1]; strcpy(str,s); } String::String(char*s, char*s2) // Constructor { // concatenates 2 strings str = new char[strlen(s) + strlen(s2) + 1]; strcpy(str,s); strcat(str,s2); } String::~String() // Destructor { cout << "Destructor for " << str << "\n"; delete [] str; } void String::operator = (const String&s) { if (this != &s) { delete [] str; str = new char[strlen(s.str)+1]; strcpy(str, s.str); } } String::String(const String&s) // Copy constructor { str = new char[strlen(s.str)+1]; strcpy(str, s.str); } String::operator char*() // Type conversion operator { return str; // Return pointer to 'str' data member } // DANGER! String operator + (String &s1, String &s2) // Concatenate { return String(s1.str,s2.str); // returns temporary }
This string class looks wholly correct, but there is the hidden danger of using temporary objects after their destruction. The following main function using this class illustrates the problem:
int main() { String s1("abc"), s2("xyz"); cout << s1 + s2 << "\n"; // ERROR }
On some C++ implementations the above code will produce the following output:
Destructor for abcxyz abcxyz Destructor for xyz Destructor for abc
This indicates that the destructor has been called before the concatenated string has been output. Destruction has occurred after computation of s1+s2 but before output using the << operator. The output statement was taking characters from a deallocated address.
The problem is that the + operator returns a class object, which creates a temporary object when the function is called. Therefore, s1+s2 is evaluated into a temporary object. There seems to be no problem since it is immediately used, but this is not so. In fact, the type conversion operator for conversion to char* is called, and this returns a pointer to the str field of the temporary object. Once this conversion has been performed, there is no further need for the temporary object and the compiler can destroy it. This destruction can occur before the char* address is passed to << for output, in which case the address becomes a dangling reference pointing to memory that has been deallocated by the delete operator in the temporary object's destructor.
delete versus delete [ ]
There are two distinct styles for the allocation and deallocation of memory — one for arrays and one for nonarrays. The two styles are distinguished by different syntax, particularly the presence or absence of square brackets. The styles for the new operator are:
Obj *p = new Obj; // one object (nonarray) Obj *p = new Obj[2]; // two objects (array)
The corresponding styles for the delete operator are:
delete p; // delete nonarray delete [] p; // delete array
Mixing the two styles is a common C++ programming error. The programmer is often lazy in the use of the delete operator and forgets the [] specification for the deallocation of an array. In fact, the implementation is often quite forgiving if the array objects are not class objects, or they are class objects for which the destructor does nothing important. Therefore, the programmer is rarely reminded of the necessity of using the correct syntax, and the error is much more difficult to track when it does finally catch the unwary programmer. The problem causes a run-time failure when an array of objects is deallocated without the [] syntax and the destructor for these objects does important work (e.g., deallocating memory allocated to class members). The error is illustrated by the following example program:
#include <iostream.h> class Obj { public: Obj() { cout << "Constructor\n"; } ~Obj() { cout << "Destructor\n"; } }; int main() { Obj *p = new Obj[2]; delete p; /* WRONG */ }
The correct method of allocation using the new operator causes the constructor to be called for both objects. However, the incorrect delete syntax means that only the destructor for the first object in the array is executed. The compiler sees only a pointer to the class object, p, and does not detect that it is actually pointing to an array of objects.
Note that although the wrong delete syntax is rarely fatal for the deallocation of arrays of primitive types, this need not always be true. The current C++ language definition defines the situation when the ordinary delete syntax is used to delete an array as "undefined," and there is no reason to expect that the C++ standard will change this. For example, the following code sequence is harmless on most (all?) current C++ implementations, although the compiler need not always be this forgiving.
char *p = new char[2]; delete p; /* DANGEROUS? */
Freeing a non-allocated memory block
A bad error is to use the "free" function on a pointer to memory that has not been allocated by malloc or calloc. The effect of this is not defined, but usually causes a crash. A variable, such as a global array, cannot be freed to the heap. It was not dynamically allocated by malloc or calloc, and has the wrong format. Memory allocated by malloc or calloc has a special format — it has a few information bytes in a header block before it.
Inconsistent types for malloc
Errors can creep in when programmers cut and paste statements. One dangerous problem that the compiler will not detect is calling malloc with the sizeof operator computing the wrong size. An example of this problem is:
struct A *p; p = malloc(sizeof(struct B)); /* should be struct A */
One way to avoid this problem is to use the following method:
p = malloc(sizeof(*p));
This technique has the danger that the * will be accidentally left out, and too few bytes will be allocated because the pointer size is small:
p = malloc(sizeof(p)); /* ERROR */
malloc does not initialize data
The malloc function does not necessarily initialize any allocated bytes of memory to zero, although this is a common feature of many implementations. However, dependence on this form of initialization will lead to portability problems for implementations without this extension. An example of an error that leaves part of a structure uninitialized is:
typedef struct node { int data; struct node *left, *right; } * Ptr; Ptr new_node(int data) { Ptr temp; temp = malloc(sizeof(*temp)); temp->data = data; return temp; }
The new_node function assumes that malloc initializes its memory to zero, thereby setting the left and right pointers to NULL. Howev er, if malloc does not initialize memory, these pointer fields are dangerous dangling references that could cause any form of program failure when the allocated node is later used.
A common solution is to use calloc instead of malloc, because the calloc function does initialize its allocated memory:
temp = calloc(sizeof(*temp), 1);
However, this is not necessarily a fully portable solution, although few implementations will actually cause a problem.
realloc can move the block
When the realloc function is called to increase the size of a dynamically allocated block, there is a danger that this can create dangling references. If realloc cannot increase the size of the existing block it allocates a new larger block, copies the data from the old block to the new larger block, and then deallocates the old block. The following use of realloc illustrates the erroneous belief that realloc simply resizes the current block:
realloc(ptr, NEW_SIZE); /* WRONG */
If realloc moves the block, then ptr becomes a dangling reference pointing to a block that has already been deallocated. The correct call is simply:
ptr = realloc(ptr, NEW_SIZE); /* CORRECT */
However, even if the correct calling method is used it is important that there be no other pointers to the old block that would become dangling references. For example, this may occur if the block is part of a dynamic linked data structure such as a list or binary tree.
calloc zero-initialization is nonportable
The calloc function initializes all its allocated bytes to zero; all implementations have this feature and it is specified by the C++ standard. However, making use of this initialization feature is nonportable to machines where either floating-point zero (i.e., 0.0) or NULL are not all-bytes-zero. The initialization of calloc is portable only when allocating arrays of characters. The problem is that the floating-point zero, 0.0, is not always represented by a sequence of zero bytes (or equivalently zero bits). Consider the following loop to print out a double variable as a sequence of bytes:
double x = 0.0; /* floating-point zero */ for (i = 0; i < sizeof(double); i++) printf("Byte %d is %d\n", i, ((unsigned char*)&x)[i]);
On most machines this will produce the information that all bytes are 0 bytes. However, this is not guaranteed and there are a few machines in existence where this is not true. On such machines it is incorrect to rely on the initialization feature of calloc when allocating objects containing double or float values, since these will be initialized to some value other than 0.0 (because 0.0 differs from all-bytes-zero).
Analogous portability problems exist for machines where the null pointer is not all-bytes-zero. Although C++ considers integer 0 and NULL to be almost identical within source code, there is no guarantee that the value actually stored to represent the null pointer will be equivalent to integer 0. Try the following code to examine your machine's representation:
int * p; p = NULL; for (i = 0; i < sizeof(int*); i++) printf("Byte %d is %d\n", i, ((unsigned char*)&p)[i]);
On machines where the null pointer is not 0, the compiler is required to add conversion code when 0 (or NULL) is used in a pointer context. This shields the programmer from the actual representation of NULL in most situations. For example, the initialization of global pointer variables to NULL is performed correctly by the compiler, since it recognizes that a pointer type is being initialized. However, the initialization by calloc is one situation where the compiler cannot know that pointer data is being initialized. calloc blindly sets all bytes to zero and sets the pointer data to address 0, which does not correspond to NULL for the implementation.
The same portability problems with floating-point zero and NULL arise in the use of the memset function; in fact, calloc is similar to a malloc followed by a memset call. The only fully portable solution to initializing allocated memory is to explicitly initialize the data in your own program.
It is common style to avoid the use of calloc since its initialization feature is usually nonportable, and therefore the memory must be explicitly initialized in any case. Hence it is more efficient to call malloc, which does not waste time performing the initialization. It is unfortunate that neither calloc nor memset can be used since they are usually much faster than explicit initialization.