Lecture 16:  Dynamic Memory Allocation - stack

 

Objectives of this lecture

q       Learn why we need a better ADT implementation that that of array

q       Learn how memory is allocated dynamically using malloc

q       Learn how to link different storage together using pointer variables

q       Implement stack using dynamic memory allocation

 

Why do we need another implementation method?

q       So far we have used array to construct ADTs (Stack & Queue)

q       This was done in a relatively simple manner since array is supported by the compiler, and it has a simple method of accessing the elements.

q       However, there is a problem with the array implementation – the fact that array must be declared with a fixed size at compilation.  This leads to two possible problems:

Ø      Either the size is too much, in which case the memory is being wasted or

Ø      The size is too small, in which case stack overflow may be encountered

q       We shall see that it is possible to delay specifying the size of the array until run-time, but even then, the size must be fixed at allocation time.

q       In this section we shall develop a better way of constructing ADTs that does not have the drawback encountered with array.

 

Using malloc to obtain memory at run-time.

q       Memory can be allocated dynamically (at run-time) using the function malloc() – accessible through <stdlib.h>

q       The allocation is made from a special memory area called the heap

q       The function, malloc() returns a pointer (address) to the allocated storage.

q       However, malloc() does not associate any type to the pointer it returns – it is said to be void.

 

q       For the pointer to be useful, it must be associated with a type using casting.

e.g.  int  *int_ptr;

        int_ptr=(int *) malloc(2);

       *int_ptr =17; 

q       The above statements reserve two bytes and returns the address of first byte, cast it to int and assigns it to integer pointer int_ptr.

q       Since the bytes allocated to int is system-dependent, it is safer to use the function sizeof () to get the actual number of bytes associated with the particular type being considered.

q       sizeof() is system-independent and can be used even with user-defined types.

q       Thus, the above statements are better represented as follows:

    int *int_ptr;

    int_ptr=(int *) malloc(sizeof(int));

    *int_ptr =17; 

q       Note that there is no name associated with the memory obtained by malloc.  It can only be accessed as *int_ptr.  It is sometimes called anonymous variable.

q       Thus, should int_ptr be given another address, the location (returned by malloc) will be lost .  It can neither be accessed by the program nor by the system.  It is said to be a lost object.

q       When we no longer need a dynamic variable, we can return the storage it occupies using the free() function.

 E.g.   free(int_ptr);

 

Stack Implementation using dynamic memory allocation

q       In this implementation, the stack is implemented as a sequence of memory cells, called nodes.

q       Each node consists of two fields; the item being stored and a pointer (address) to the next node.

q       Unlike the array implementation in which the stack is a structure, here stack is just a pointer to the first node (assumed to be the top element).

q       The last element in the sequence points to NULL.  The following figure illustrates this idea.

 

 

 

q     We now look at how the various operations can be implemented using the approach described above.

q       Notice that our aim is not just to implement the operations, but to do so in such a way that all programs that use the array implementation can work without any change with this implementation.

 

User Interface:  This can include the following:

 

typedef char ITEM_TYPE;

typedef struct node_type { ITEM_TYPE item;

                                  struct node_type *next;

                                    } NODE_TYPE;

 

typedef NODE_TYPE *NODE_PTR;

typedef NODE_TYPE *STACK_TYPE;

 

typedef enum{FALSE,TRUE} BOOLEAN;

 

void create_stack(STACK_TYPE *stack);

void destroy_stack(STACK_TYPE *stack);

BOOLEAN empty_stack(STACK_TYPE *stack);

BOOLEAN full_stack(STACK_TYPE *stack);

void push(STACK_TYPE *stack, ITEM_TYPE newitem);

void pop(STACK_TYPE *stack, ITEM_TYPE *old_item);

 

Notice that:

1.               The user can change the type of ITEM_TYPE

2.               The pointer field next in the declaration of struct node_type is declared to be of type struct node_type.  i.e. struct node_type is used before its declaration is completed. This is called self-referencing definition and is allowed in C in this case.

3.               NODE_PTR and STACK_TYPE are pointer types and they are actually the same.  The two are being used to distinguish between the special pointer (stack) that points to the top node and other pointers that points to other nodes.

4.               The function prototypes are the same as those in array implementation.  Thus, even though stack is a pointer, we are still passing its address to ensure compatibility.  This (passing address of a pointer variable) is called  double indirection.

Implementation details: 

#include <stdlib.h>  /*needed for malloc */

#include "stack.h"

 

/* initializes stack to NULL */

void create_stack(STACK_TYPE *stack)

{ *stack=NULL;

}

 

/* return the memory cells occupied by elements of stack to the system */

void destroy_stack(STACK_TYPE *stack)

{  NODE_PTR temp_ptr;

 

   while (*stack != NULL)

   { temp_ptr = *stack;

     *stack=temp_ptr->next;

     free(temp_ptr);

   }

}

 

/* returns true if stack is NULL) */

BOOLEAN empty_stack(STACK_TYPE *stack)

{ if (*stack== NULL)

     return TRUE;

  else

     return FALSE;

}

        

/*always return false since in this case, stack is never full */

BOOLEAN full_stack(STACK_TYPE *stack)

{  return FALSE;

}

 

/*create a node, add newitem to it and link it to top of stack */

void push(STACK_TYPE *stack, ITEM_TYPE newitem)

{  NODE_PTR temp_ptr;

 

   temp_ptr=(NODE_PTR) malloc(sizeof(NODE_TYPE));

   if (temp_ptr != NULL)

   {   temp_ptr->item=newitem;

       temp_ptr->next=*stack;

       *stack=temp_ptr;

   }

}

  

/* returns the item at top of stack and returns its storage to the system */

void pop(STACK_TYPE *stack, ITEM_TYPE *old_item)

{   NODE_PTR temp_ptr;

 

    temp_ptr=*stack;

    *old_item=temp_ptr->item;

    *stack=temp_ptr->next;

    free(temp_ptr);

}

 

Notice that:

1.               The destroy_stack is different from that of array implementation.  The storage occupied my elements of the stack is physically returned to the system.

2.               To push an element, memory has to be physically created for it and when an element is popped, its memory is returned to the system.  Thus, no memory is wasted and the stack is never full –really?

3.               Memory on the heap is not infinite.  If malloc cannot create a storage, it returns null. Thus, the push function has a potential bug.

4.               The full_stack is not necessary in this implementation.  It is only being used for compatibility purpose.  The compiler may warn that the pointer variable stack in this function is never used.  This should be ignored.