Pete tells us why we should be meticulous about freeing allocated storage, and why everybody needs a friend sometimes.
To ask Pete a question about C or C++, send e-mail to pbecker@oec.com, use subject line: Questions and Answers, or write to Pete Becker, C/C++ Users Journal, 1601 W. 23rd St., Ste. 200, Lawrence, KS 66046.
Q
I read with interest your discussion of realloc in the August CUJ Q&A column. The discussion brought up an issue about memory leaks which I have been concerned about for some time, but for which I haven't seen a definitive statement in the literature I have read.Let's say I have a program that dynamically allocates a large array using malloc (C) or new (C++). The program does its thing and exits normally. My question is: is that dynamically allocated memory automatically returned to the system by the program finishing, or does it need to be explicitly returned by a call to free (C) or delete (C++) before the program finishes? Also, does it matter whether the program finishes with an early call to exit or simply gets to the end of the main function?
I've written a few programs that I use at work that assume the automatic deallocation of that array memory, and I don't want to be an unintentional memory hog. Thank you.
Chuck Isaacs
A
There are really three questions here, and since I'm writing this just before Christmas, I'm in an expansive mood and I'll answer all three. The questions are:
1) What does the C language definition say about memory that has not been freed;
2) What do most compilers and operating systems do with memory that has not been freed; and
3) How should we write code to be reasonably sure that our programs will be good citizens.
Before we take up in earnest what the C language definition says about memory management, I want to mention the distinction between hosted and freestanding implementations of the C language, because the rules are significantly different depending on which one your implementation supports. According to the language definition, in a freestanding environment (in which C program execution may take place without any benefit of an operating system), the name and type of the function called at program startup are implementation-defined. There are no reserved external identifiers. Any library facilities available to a freestanding program are implementation-defined. The effect of program termination in a freestanding environment is implementation-defined. [ANSI C Standard, Paragraph 2.1.2.1]
So if you're coding in a freestanding environment you're on your own. You cannot rely on portable startup behavior or shutdown behavior. You can't even rely on having a function named free. It's entirely up to your compiler vendor to decide what happens on program startup and shutdown, and what library support to make available. Now, that may seem to contradict the idea of portability, which is supposed to be one of C's greatest strengths, but remember that when you're talking about a freestanding environment you're talking about embedded systems. Code written for embedded systems tends to be fairly specialized, and portability is not a primary concern.
Most people learn C in the context of a hosted environment. That means that the program's entry point is named main, and main takes no parameters or two parameters (the usual argc and argv we all know and love). Having a hosted environment also means that the library functions defined in the language specification must be supported by the compiler, and that startup and shutdown of a program must be in accordance with the Standard. This gives us a much more solid base to work from if we understand what the Standard requires we know what behavior we can rely on from conforming compilers.
The natural place to begin is with the definition of free. Here's what the C Standard says:
The free function causes the space pointed to by [its argument] to be deallocated, that is, made available for further allocation. If [the argument] is a null pointer, no action occurs. Otherwise, if the argument does not match a pointer earlier returned by the calloc, malloc, or realloc fucntion, or if the space has been deallocated by a call to free or realloc, the behavior is undefined. [Paragraph 4.10.3.2]
Not much help, is it? The Standard doesn't tell us what the phrase "made available for further allocation" means. It may seem intuitive, but if you try to write a portable program to determine whether the space has in fact been "made available" you'll find that you can't, in general, decide this. I don't have space to go into the details here, so you'll have to take my word for it.
The definition of malloc doesn't help, either:
The malloc function allocates space for an object whose size is specified by [its argument] and whose value is indeterminate.
That is, malloc magically gets memory. No promises beyond that.
What about program termination? Here's what the C Standard says:
A return from the initial call to the main function is equivalent to calling the exit function with the value returned by the main function as its argument. If the main function executes a return that specifies no value, the termination status returned to the host environment is undefined.
And here's what the C standard says about the exit function:
The exit function causes normal program termination to occur. If more than one call to the exit function is executed by a program, the behavior is undefined. First, all functions registered by the atexit function are called, in the reverse order of their registration. Next, all open streams with unwritten buffered data are flushed, all open streams are closed, and all files created by the tmpfile function are removed. Finally, control is returned to the host environment. If the value of status is zero or EXIT_SUCCESS, an implementation-defined form of the status successful termination is returned. If the value of status is EXIT_FAILURE, an implementation-defined form of the status unsuccessful termination is returned. Otherwise the status returned is implementation-defined.
Hmm, nothing there about releasing memory. In fact you can read the entire C Standard, and you won't find anything that tells you whether a conforming implementation must return all its memory to the operating system.
The C Standard describes what a conforming implementation must do. Since the Standard does not require any specific action in regard to freeing memory when the program terminates, an implementation that does not release memory back to the operating system is a conforming implementation.
However, in practice you can usually rely on the operating system to recover memory used by a program when that program terminates. After all, the operating system's primary job is to manage system resources to keep the computer running as efficiently as possible. Recovering memory when a program terminates is critical to doing this effectively. I don't know of any operating system that does not recover the memory used by a program.
Still, I recommend developing the coding habits and the discipline to properly manage all resources within your program. With respect to memory management, this means always freeing memory that you have allocated. That can take more work, but the work pays off by producing more robust and more maintainable code.
Two primary reasons explain why this practice produces better code. First, keeping track of resources is part of writing good code. You cannot claim to understand what your program is doing if you do not know whether a pointer points to a valid memory block. If you've designed your program properly, however, you can always tell either by reading the code or by checking flags at run time which pointers point to actual memory. You can also tell when that memory is no longer needed. Given that knowledge, it's usually trivial to free the memory when it's no longer needed. If you insist that your code free all the memory it allocates you force yourself to carefully analyze your program's memory usage. This analysis will often turn up serious problems that would be hard to find with a debugger. For example:
#include <stdio.h> void show() { char *cptr; int i; for( i = 0; i < 10; i++ ) { cptr = malloc(20); /* check for NULL return omitted */ strcpy( cptr, "Hello, world\n" ); printf( cptr ); } }There's a memory leak here, and it's not harmless. Every time through the loop a block of memory is abandoned. You might not notice the problem in the course of testing, but you can bet that your customers will find it when your program mysteriously fails on their systems. By requiring that all memory be released before program termination, and enforcing this with a memory tracking tool, you'd catch this problem before you put your program into production use.
The second reason this approach produces more robust code is that you never know what might have to be done to the program during future maintenance. Suppose the function show is called only once in the current version of your program. That single call probably won't cause any problems. It only drops a few hundred bytes of memory. But in the course of enhancing the program, someone might rewrite the code to call this function several thousand times. Now the problem becomes much more significant, and some unfortunate maintenance programmer will have to track down the cause of this memory leak. Of course, he or she won't know the code as well as you did when you first wrote it, so it will take a lot longer to find the problem than it would have taken you to do it right in the first place.
Don't be lazy. Even if that maintenance programmer is you, you won't understand the code as well as when you first wrote it. Save yourself the time and aggravation of having to debug mysterious memory leaks by not tolerating them in the first place. It's a discipline that will pay off in the long run.
Q
In an earlier column you wrote: "However, neither strcpy nor memcpy can handle strings of wchar_t, ..."I realize that strcpy can't be used on wchar_t strings. But I believe that memcpy can! Isn't the following code portable?
void CopyString(wchar_t*dest, wchar_t*source, int length) { memcpy(dest, source, length * sizeof(wchar_t)); }I know that copying an object of a class with non-trivial constructors or destructors (or which contains or is derived from such an object) can introduce problems. However, I'm under the impression that almost anything else can be copied with memcpy, provided that the user satisfies any needed alignment criteria, which I'm guessing would always be less than or equal to sizeof(type).
Is any part of this wrong?
A
Nope, none of it's wrong. The statement I made was too broad: memcpy can be used to copy wide strings if you know the length of the string. However, as you mention, you cannot use memcpy to copy objects with non-trivial copy semantics. That's really the problem that I was talking about, and it's the problem that char_traits solves: it allows the creation of types with arbitrary complexity that can still be used with the basic_string template, and it does this without forcing pollution of the global namespace.Q
I'm trying to think up a situation where there is no solution other than to use friend. What about operator<<? Does it require friendship? The answer seems to be No:class foo { public: void print(ostream & os) const; }; ostream & operator<<(ostream & os, const foo & f) { f.print(os); return os; }What about operator==? No:
class foo { public: int equal(const foo & f) const; }; int operator==(const foo & f1, const foo & f2) { return f1.equal(f2); }Does this mean that friend is never needed? Can anyone come up with an example that requires friendship?
Robert SchwartzA
You've demonstrated that it is possible to implement these two operators without having to use any friend declarations. But just because you can do something doesn't mean you should do it. One of the most important considerations in writing classes is designing them to be easily understood and used. From a design perspective, it's hard to defend implementing either of these operators in this manner. This implementation compromises the principle of encapsulation without good reason.Let's look at the design of this class a bit more carefully. Of course, without having a specification of what it is supposed to do, I can only guess at what its design really is. I'd rather not do that, but I'll try to identify all of the assumptions I'm making as I go along.
We'll begin with high-level design by creating a description of this class's purpose. This description will give us a framework for discussing the design of the public interface to the class and how to implement that interface. Then we'll move to low-level design, figuring out what we must add to the class to implement it. Along the way we'll look at the actual implementation.
Let's create a counted class: instances of this class are numbered, and each new class gets a number that's one higher than the previously created class. Copies of a class have the same number as the one that they are copied from. The first such class has the number 1.
Here's the initial class definition to implement the requirements we've listed so for:
class Counted { public: Counted(); // create new instance of class Counted( const Counted& ); // copy existing instance Counted& operator = ( const Counted& ); // copy existing instance };As an additional requirement, we should be able to get the number of a given instance by calling a function. Obviously this should be done through a member function. Or should it? We ought to at least consider using a global rather than automatically going with a member function. Here there's no benefit from providing a global function, so we'll follow our first instinct, and make it a member function. Let's call that member function number:
class Counted { public: Counted(); // create new instance of class Counted( const Counted& ); // copy existing instance Counted& operator = ( const Counted& ); // copy existing instance int number() const; // get number of instance };Most classes should provide a way of writing their contents out to a standard stream. Even if this capability is not needed as part of an application's design, a programmer should probably include it for most classes because it is very useful during debugging. The usual idiom for doing this is through operator<<, which takes a reference to an ostream as its left operand and a reference to one of our objects as its right operand. That lets users of our class write code like this:
int main() { Counted c; cout << c << endl; return 0; }Since the first argument to our operator<< is an ostream and not an object of our class, we can't make << a member. So it must be global:
ostream& operator << ( ostream&, const Counted& );Similarly, it is often useful to compare two objects for equality. The standard idiom for doing this is to overload operator==, allowing users to write code that looks like this:
int main() { foo f, g; if( f == g ) cout << "Objects compare" << equal." << endl; return 0; }Here we have a choice: we can make operator== a member taking one argument (with the implicit this pointer supplying the other argument) or we can make it a global function taking two parameters. Comparisons such as this should usually be done through global operators. Making the operator global simplifies coding of classes that provide conversions from other types, because the compiler won't use such a conversion on the left side of an operator if that operator is a member function. For example, consider a complex number class that provides a constructor taking an integer as a parameter. If the comparison operator is a member the expression 1 == c is not valid. If the comparison is a global function this expression is valid, since the compiler will convert the value 1 to an object of type complex. For this reason, most class designs make operator== a global function rather than a member. We'll do the same here:
bool operator == ( const Counted&, const Counted& );The documentation for this class should describe the intended uses of Counted, and should describe what the two global operators do. The descriptions of the two global operators can be fairly short, since they support standard programming idioms that are well understood. Users of our class will know how to use them without needing detailed explanations.
When we implement this class we'll need to add some details to make it work. We'll need some sort of counter to keep track of the number to be assigned to the next instance, and each instance will need a member to hold its number. There's no reason to make the counter available outside the class, so we'll make it a static member. Now our class definition looks like this:
class Counted { public: Counted(); // create new instance of class Counted( const Counted& ); // copy existing instance Counted& operator = ( const Counted& ); // copy existing instance int number() const; // get number of instance private: int num; static int counter; };Counter's implementation looks like this:
Counted::Counted() : num( ++counter ) {} Counted::Counted( const Counted& c ) : num(c.num) {} Counted& operator = ( const Counted& c ) { num = c.num; return *this; } int Counted::number() const { return num; } int Counted::counter = 0; ostream& operator << ( ostream& out, const Counted& c ) { out << c.number(); return out; } bool operator == ( const Counted& c1, const Counted& c2 ) { return c1.number() == c2.number(); }Note that both operator<< and operator== use the public member function number to get the information they need.
Now that we've done all this we must be sure that our documentation of this class is up to date. All the public member functions and both of the global operators should be described in the documentation. The private members are implementation details and should not be described in end-user documentation. Doing so is pointless, since users of our class cannot get at its private members. Of course, our internal documentation should describe them, so that maintainers of our code will have a better shot at figuring out what we've done.
Now suppose we change the specification a bit, and prohibit users from getting at the number of an instance. The obvious way to prevent access is to remove the member function number from the public interface: we've decided that this value is of no interest to users, so we shouldn't make it available to them. We change the class definition to look like this:
class Counted { public: // create new instance of class Counted(); // copy existing instance Counted( const Counted& ); // copy existing instance Counted& operator = ( const Counted& ); private: int num; static int counter; friend ostream& operator << ( ostream&, const Counted& ); friend bool operator == ( const Counted&, const Counted& ); };I've declared both operators to be friends of Counted, and their implementation will have to be changed a bit to reflect the removal of the member function number. Once this change is made, we also must remove the description of number from our documentation.
Here is where your suggested implementation technique comes in: instead of making the operators friends, you suggest adding two public member functions to handle printing a Counted object to a stream and comparing two Counted objects. Notice that we add these functions strictly as an aid to implementation: nothing in our discussion of what Counted should do suggests that users need an alternative syntax for printing and comparing Counted objects. In fact, having two different ways to do the same thing would be confusing, which is another reason why implementation details like this are usually private. If we add these as public members we probably shouldn't document them. If we do document them, I'd certainly be embarrassed to describe each of them as "for implementation use only," but I don't see any other way to avoid confusing users of our class.
The problem with adding these member functions is that they are not driven by users' needs, but by our own implementation needs. The public interface to a class is for users. There is no user requirement for these functions, so they do not belong in the public interface. That's why we need friends. o
Pete Becker is director of development at Borland International's Open Environment Division in Boston, MA. He has been actively involved in C++ development work at Borland for seven years, both as a developer and a manager. He is Borland's representative to the ANSI/ISO C++ standardization committee. He can be reached by e-mail at pbecker@oec.com.