C Language

Amnesia

In large or long-running programs that explicitly allocate memory by calls to C's malloc memory allocation function, you may try to allocate more memory than is available. If malloc can't allocate the amount of memory you request, it returns NULL. Always check for a NULL return value after calling malloc. By doing this, you can avoid the problems that occur when you use a NULL pointer. Most C programmers follow this rule - most of the time. But that's not good enough. Even if you allocate only one byte as the very first statement in a trivial main block (an operation you think can "never" fail), check what malloc returns. The test takes 30 seconds to code and practically no time to be executed, and you'll never be unpleasantly surprised when the "it couldn't happen" does.

If a program does much explicit memory allocation, you also need to guard against C's form of amnesia - unexpected memory loss or memory "leakage." This quaint term refers to the situation where memory you've allocated isn't available for reuse when you're done with it.

Figure 6.8 shows how leakage can occur. The first malloc operation allocates a memory block and stores a pointer to it in ptr. After using this memory to hold a character string, the code reuses ptr to point to memory containing a different string. This code will be executed fine, but the memory originally allocated to hold the first string will remain marked "in use," even though it can't be referenced or deallocated after the second malloc operation (assuming the pointer value isn't copied to another pointer variable).

Figure 6.8 How Memory Leakage Occurs

 string_t val1 = "abc";
 string_t val2 = "xyz";
 string_t ptr;
 ptr = (string_t) malloc( strlen( val1 ) + 1 );
 strcpy( ptr, val1 );
           .
           .
           .
 ptr = (string_t) malloc( strlen( val2 ) + 1 );
 strcpy( ptr, val2 );
           .
           .
           .

A simple solution is the allocate macro in Figure 6.9, which uses malloc when the pointer is NULL and realloc when it's not. (In a subsequent chapter, I'll explain why allocate uses so many parentheses. Simply put, the parentheses prevent unintended changes in the generated code's evaluation order.) Figure 6.10 shows how to reuse allocated memory with the allocate macro. Note that allocate requires the pointer to be NULL or a value returned by one of the memory allocation functions. Another rule, always initialize pointers in their definitions, partially satisfies this requirement. Although C initializes static pointer variables to NULL, it doesn't initialize automatic pointer variables. Coding an explicit NULL initializer covers all cases and emphasizes pointer declarations.

Figure 6.9 allocate Macro

 /*
 | ptr MUST be NULL or address of block allocated
 | by calloc, malloc, or realloc functions
 |
 | The value of allocate is either a valid pointer
 | of the specified ptr_type, or NULL if no memory
 | can be allocated.
 */
 #define allocate( ptr, ptr_type, alloc_size )    \
                                 \
 ( ( ptr ) = ( ptr_type ) ( ( ( ptr ) == NULL ) ? \
           malloc(           ( alloc_size ) ) : \
           realloc( ( ptr ), ( alloc_size ) ) ) )

Figure 6.10 Avoiding Memory Leakage

 string_t val1 = "abc";
 string_t val2 = "xyz";
 string_t ptr  = NULL;
 allocate( ptr, string_t, ( strlen( val1 ) + 1 ) );
 strcpy( ptr, val1 );
          .
          .
          .
 allocate( ptr, string_t, ( strlen( val2 ) + 1 ) );
 strcpy( ptr, val2 );
          .
          .
          .

By using typedef, local variables instead of dereferenced parameters, array notation, and various macros and functions, you can reduce the number of places where you must code *, whether in declarations or as the dereferencing operator. Because * isn't a very intuitive symbol for "contents of" (in most 3X/400 programmers' experience, * stands for multiplication, a multicharacter wildcard, or the start of a S/38 or AS/400 special value), you can improve your C programs' readability by minimizing its use. To further improve readability, you may even want to define the three macros in Figure 6.11 - PTR, contents_of, and address_of.

Figure 6.11 Pointer-Related Macros

 #define PTR *
 #define contents_of( x ) ( * ( x ) )
 #define address_of( x ) ( & ( x ) )

Figure 6.12 shows a revised version of the fees function, using PTR and contents_of. (Instead of using PTR, you could define an int_ptr_t typedef for integer pointers.) Figure 6.13 shows how you can use the address_of macro to emphasize what are being passed as arguments to fees.

Figure 6.12 Improved fees Function Using PTR and contents_of Macros

 void function fees(  int PTR rfee,
                int PTR afee,
             const int  age,
             const int  income ) {
   int reg_fee = contents_of( rfee );
   int act_fee = contents_of( afee );
   reg_fee += age >= 60 ? ( income < 50000 ?   0 :  50 ) :
                ( income < 50000 ? 100 : 200 );
   act_fee += age >= 60 ? ( income < 50000 ?  10 :  20 ) :
                ( income < 50000 ?  30 :  40 );
   /*
   | Return values
   */
   contents_of( rfee ) = reg_fee;
   contents_of( afee ) = act_fee;

Figure 6.13 Calling fees Function Using address_of Macro

 int reg_fee;
 int act_fee;
 int age;
 int income;
       .
       .
       .
 reg_fee = 100;
 act_fee = 50;
       .
       .
       .
 fees( address_of( reg_fee ), address_of( act_fee ), age, income );

These macros don't provide a complete safety net for your C programs, but they can make your programs clearer and easier to check for pointer-related mistakes. The examples in this C programmer's magazine will give you a wide sampling of how impenetrable and varied typical C code is. After reading some of the published code, you'll appreciate both the necessity and the benefits of efforts to improve C readability.

by BrainBellupdated
Advertisement: