Article

EDP

C & DS

Engg.Chemistry

Engg.Physics

Pointers

  • D. Vijay Kumar

    Asst. Prof., Dept IT

    MGIT, Hyd

    Pointers are without a doubt one of the most important mechanisms in C. They are the means by which we implement call by reference function calls, they are closely related to arrays and strings in C, they are fundamental to utilising C's dynamic memory allocation features, and they can lead to faster and more efficient code when used correctly.

    A pointer is a variable that is used to store a memory address. Most commonly the address is the location of another variable in memory.

    If one variable holds the address of another then it is said to point to the second variable

    In the above illustration ivar is a variable of type int with a value 23 and stored at memory location 1012. ivar_ptr is a variable of type pointer to int which has a value of 1012 and is stored at memory location 1004. Thus ivar_ptr is said to point to the variable ivar and allows us to refer indirectly to it in memory.

    NB : It should be remembered that ivar_ptr is a variable itself with a specific piece of memory associated with it, in this 32-bit case four bytes at address 1004 which is used to store an address.

    Pointer Variables

    Pointers like all other variables in C must be declared as such prior to use.

    Syntax : type *ptr ;

    which indicates that ptr is a pointer to a variable of type type. For example

             int *ptr ;

    declares a pointer ptr to variables of type int.

    NB : The type of the pointer variable ptr is int *. The declaration of a pointer variable normally sets aside just two or four bytes of storage for the pointer whatever it is defined to point to.
        In 16-bit systems two byte pointers are termed near pointers and are used in small memory model programs where all addresses are just segment offset addresses and 16 bits in length. In larger memory model programs addresses include segment and offset addresses and are 32 bits long and thus pointers are 4 bytes in size and are termed far pointers.
    In 32-bit systems we have a flat address system where every part of memory is accessible using 32-bit pointers.

    Pointer Operators * and &

    & is a unary operator that returns the address of its operand which must be a variable.

    For Example :-
    int *m ;
    int count=125, i ;/* m is a pointer to int, count, i are integers */
    m = &count ;

    The address of the variable count is placed in the pointer variable m.

    The * operator is the complement of the address operator & and is normally termed the indirection operator. Like the & operator it is a unary operator and it returns the value of the variable located at the address its operand stores.

    For Example :-
    i = *m ;

    assigns the value which is located at the memory location whose address is stored in m, to the integer i. So essentially in this case we have assigned the value of the variable count to the variable i. The final situation is illustrated below.

    One of the most frequent causes of error when dealing with pointers is using an uninitialised pointer. Pointers should be initialised when they are declared or in an assignment statement. Like any variable if you do not specifically assign a value to a pointer variable it may contain any value. This is extremely dangerous when dealing with pointers because the pointer may point to any arbitrary location in memory, possibly to an unused location but also possibly to a memory location that is used by the operating system. If your program tries to change the value at this address it may cause the whole system to crash. Therefore it is important to initialise all pointers before use either explicitly in your program or when defining the pointer.

    A pointer may also be initialised to 0 ( zero ) or NULL which means it is pointing at nothing. This will cause a run-time error if the pointer is inadvertently used in this state. It is useful to be able to test if a pointer has a null value or not as a means of determining if it is pointing at something useful in a program.

    NB : NULL is #defined in <stdio.h>.

    For Example :-
                int var1, var2 ;
                int *ptr1, *ptr2 = &var2 ;
                int *ptr3 = NULL ;
                ...
                ptr1 = &var1 ;

    ptr1 and ptr2 are now pointing to data locations within the program so we are free to manipulate them at will i.e. we are free to manipulate the piece of memory they point to.

    Call by Reference

    Recall when we wanted to swap two values using a function we were unable to actually swap the calling parameters as the call by value standard was employed. The solution to the problem is to use call by reference which is implemented in C by using pointers as is illustrated in the following example.

                #include <stdio.h>

                void swap( int *, int * ) ;

                void main( )
                {
                int a, b ;

                printf( "Enter two numbers" ) ;
                scanf( " %d %d ", &a, &b ) ;
                printf( "a = %d ; b = %d \n", a, b ) ;

                swap( &a, &b ) ;

                printf( "a = %d ; b = %d \n", a, b ) ;
                }
                void swap ( int *ptr1, int *ptr2 )
                {
                int temp ;

                temp = *ptr2 ;
                *ptr2 = *ptr1 ;
                *ptr1 = temp ;
                }

    The swap() function is now written to take integer pointers as parameters and so is called in main() as
                            swap( &a, &b ) ;

    where the addresses of the variables are passed and copied into the pointer variables in the parameter list of swap(). These pointers must be de-referenced to manipulate the values, and it is values in the the same memory locations as in main() we are swapping unlike the previous version of swap where we were only swapping local data values.

    In our earlier call-by-value version of the program we called the function from main() as swap(a,b); and the values of these two calling arguments were copied into the formal arguments of function swap.
    In our call-by-reference version above our formal arguments are pointers to int and it is the addresses contained in these pointers, (i.e. the pointer values), that are copied here into the formal arguments of the function. However when we de-reference these pointers we are accessing the values in the main() function as their addresses do not change.

    Pointers and Arrays

    There is a very close relationship between pointer and array notation in C. As we have seen already the name of an array ( or string ) is actually the address in memory of the array and so it is essentially a constant pointer.

    For Example :-
    char str[80], *ptr ;

    ptr = str ;   /* causes ptr to point to start of string str */
    ptr = &str[0] ;   /* this performs the same as above */

    It is illegal however to do the following

    str = ptr ; /* illegal */

    as str is a constant pointer and so its value i.e. the address it holds cannot be changed.

    Instead of using the normal method of accessing array elements using an index we can use pointers in much the same way to access them as follows.

    char str[80], *ptr , ch;

    ptr = str ; // position the pointer appropriately

    *ptr = 'a' ; // access first element i.e. str[0]
    ch = *( ptr + 1 ) ; // access second element i.e. str[1]

    Thus *( array + index ) is equivalent to array[index].

    Note that the parentheses are necessary above as the precedence of * is higher than that of +. The expression
    ch = *ptr + 1 ;

    for example says to access the character pointed to by ptr ( str[0] in above example with value ‘a’) and to add the value 1 to it. This causes the ASCII value of ‘a’ to be incremented by 1 so that the value assigned to the variable ch is ‘b’.

    In fact so close is the relationship between the two forms that we can do the following

    int x[10], *ptr ;

    ptr = x ;
    ptr[4] = 10 ; /* accesses element 5 of array by indexing a pointer */

    Pointer Arithmetic

    Pointer variables can be manipulated in certain limited ways. Many of the manipulations are most useful when dealing with arrays which are stored in contiguous memory locations. Knowing the layout of memory enables us to traverse it using a pointer and not get completely lost.

    Assignment
    int count, *p1, *p2 ;

    p1 = &count ; // assign the address of a variable directly
    p2 = p1 ; // assign the value of another pointer variable, an address

    Addition / Subtraction

    The value a pointer holds is just the address of a variable in memory, which is normally a four byte entity. It is possible to modify this address by integer addition and subtraction if necessary. Consider the following we assume a 32-bit system and hence 32-bit integers.

    We now have the pointer variable ptr pointing at the start of array which is stored at memory location 2008 in our illustration. Since we know that element array[1] is stored at address 2012 directly after element array[0] we could perform the following to access its value using the pointer.

                            ptr += 1 ;

    This surprisingly will cause ptr to hold the value 1012 which is the address of array[1], so we can access the value of element array[1]. The reason for this is that ptr is defined to be a pointer to type int, which are four bytes in size on a 32-bit system. When we add 1 to ptr what we want to happen is to point to the next integer in memory. Since an integer requires four bytes of storage the compiler increments ptr by 4. Likewise a pointer to type char would be incremented by 1, a pointer to float by 4, etc.

    Similarly we can carry out integer subtraction to move the pointer backwards in memory.

                            ptr = ptr - 1 ;
                            ptr -= 10 ;

    The shorthand operators ++ and -- can also be used with pointers. In our continuing example with integers the statement ptr++ ; will cause the address in ptr to be incremented by 4 and so point to the next integer in memory and similarly ptr-- ; will cause the address in ptr to be decremented by 4 and point to the previous integer in memory.

    NB : Two pointer variables may not be added together ( it does not make any logical sense ).

                char *p1, *p2 ;
                p1 = p1 + p2 ; /* illegal operation */

    Two pointers may however be subtracted as follows.

                int *p1, *p2, array[3], count ;
                p1 = array ;
                p2 = &array[2] ;

                count = p2 - p1 ; /* legal */
    The result of such an operation is not however a pointer, it is the number of elements of the base type of the pointer that lie between the two pointers in memory.

    Comparisons

    We can compare pointers using the relational operators ==, <, and > to establish whether two pointers point to the same location, to a lower location in memory, or to a higher location in memory. These operations are again used in conjunction with arrays when dealing with sorting algorithms etc.

    For Example :- Writing our own version of the puts() standard library function.

    1. Using array notation
    void puts( const char s[ ] ) /* const keyword makes string contents read only */
    {
    int i ;

    for ( i = 0; s[i] ; i++ )
    putchar( s[i] ) ;
    putchar( '\n' ) ;
    }

    2. Using pointer notation

    void puts( const char *s ) // char *const s would make pointer unalterable
    {
    while ( *s )
    putchar( *s++ ) ;
    putchar( '\n' ) ;
    }

    As you can see by comparing the two versions above the second version using pointers is a much simpler version of the function. No extra variables are required and it is more efficient as we will see because of its use of pointer indirection.

    For Example :- Palindrome program using pointers.

    #include <stdio.h>
    int palin( char * ) ; /* Function to determine if array is a palindrome. returns 1 if it is a palindrome, 0 otherwise */
                void main( )
                {
                char str[30], c ;

                puts( "Enter test string" ) ;
                gets( str ) ;

                if ( palin( str ) )
                printf( "%s is a palindrome\n", str ) ;
                else
                printf( "%s is not a palindrome\n") ;
                }

                int palin ( char *str )
                {
                char *ptr ;

                ptr = str ;
                while ( *ptr )
                ptr++ ; /* get length of string i.e. increment ptr while *ptr != '\0' */
                ptr-- ; /* move back one from '\0' */

                while ( str < ptr )
                if ( *str++ != *ptr-- )
                return 0 ; /* return value 0 if not a palindrome */

                return 1 ; /* otherwise it is a palindrome */
                }

    Strings and pointers

    C's standard library string handling functions use pointers to manipulate the strings. For example the prototype for the strcmp() function found in <string.h> is

                int strcmp( const char *string1, const char *string2 ) ;

    where const is a C keyword which locks the variable it is associated with and prevents any inadvertent changes to it within the function.

    Strings can be initialised using pointer or array notation as follows

                char *str = "Hello\n" ;
                char string[] = "Hello\n" ;

    in both cases the compiler allocates just sufficient storage for both strings.

    Arrays of Pointers

    It is possible to declare arrays of pointers in C the same as any other 'type'. For example

                int *x[10] ;

    declares an array of ten integer pointers.

    To make one of the pointers point to a variable one might do the following.

                x[ 2 ] = &var ;

    To access the value pointed to by x[ 2 ] we would do the following

                *x[ 2 ]

    which simply de-references the pointer x[ 2 ] using the * operator.

    Passing this array to a function can be done by treating it the same as a normal array which happens to be an array of elements of type int *.

    For Example : -
                void display( int *q[ ], int size )
                {
                int t ;
                for ( t=0; t < size; t++ )
                printf( "%d ", *q[t] ) ;
                }

    Note that q is actually a pointer to an array of pointers as we will see later on with multiple indirection.

    A common use of pointer arrays is to hold arrays of strings.

    For Example :- A function to print error messages.

                void serror( int num )
                {
                static char *err[] = {
                "Cannot Open File\n",
                "Read Error\n",
                "Write Error\n" } ;

                puts( err[num] );
                }

    Note that using an array of pointers to char initialised as above conserves space as no blank filling characters are required as would be if we used

                char err[3][30] = {
                            ... } ;
    Command Line Arguments

    Command line arguments allow us to pass information into the program as it is run. For example the simple operating system command type uses command line arguments as follows

                c:>type text.dat

    where the name of the file to be printed is taken into the type program and the contents of the file then printed out.

    In C there are two in-built arguments to the main() function commonly called argc and argv which are used to process command line arguments.

                void main( int argc, char *argv[ ] )
                {
                ...
                }

    argc is used to hold the total number of arguments used on the command line which is always at least one because the program name is considered the first command line argument.
    argv is a pointer to an array of pointers to strings where each element in argv points to a command line argument. For example argv[0] points to the first string, the program name.

    For Example :- Program to print a name ( saved in name.c ) using command line arguments.

                #include <stdio.h>
                void main( int argc, char *argv[ ] )
                {
                if ( argc != 2 )
                {
                puts( "Missing parameter. Usage : name yourname" ) ;
                exit( 1 );
                }
                printf( "Hello %s", argv[1] ) ;
                }

    To run the program one might type

                c:\>name tom

    For Example :- Program to count down from a given value, the countdown being displayed if the argument "display" is given.

    #include <stdio.h>
    #include <stdlib.h>
    #include <ctype.h>
    #include <string.h>

    void main( int argc, char *argv[ ] )
    {
    int disp, count ;

    if ( argc < 2 )
                {
                puts("Missing Arguments Usage : progname count [display]" );
                exit(1) ;
                }

    if ( argc > 2 && !strcmp( argv[2], "display" ) )
                disp = 1 ;
    else
                disp = 0 ;

    for ( count = atoi( argv[1] ) ; count ; count-- )
                if ( disp )
                            printf( "%d\n", count ) ;
    printf( “done\n” ) ;
    }

    NB : C has a broad range of functions to convert strings into the standard data types and vice versa. For example atoi() converts a string to an integer above - remember all command line arguments are just character strings.

    Dynamic Memory Allocation

    This is the means by which a program can obtain and release memory at run-time. This is very important in the case of programs which use large data items e.g. databases which may need to allocate variable amounts of memory or which might have finished with a particular data block and want to release the memory used to store it for other uses.

    The functions malloc() and free() form the core of C's dynamic memory allocation and are prototyped in <malloc.h>. malloc() allocates memory from the heap i.e. unused memory while available and free() releases memory back to the heap.

    The following is the prototype for the malloc() function

                void * malloc( size_t num_bytes ) ;

    malloc() allocates num_bytes bytes of storage and returns a pointer to type void to the block of memory if successful, which can be cast to whatever type is required. If malloc() is unable to allocate the requested amount of memory it returns a NULL pointer.

    For example to allocate memory for 100 characters we might do the following

                #include <malloc.h>

                void main()
                {
                char *p ;

                if ( !( p = malloc( sizeof( char ) * 100 ) )
                            {
                            puts( "Out of memory" ) ;
                            exit(1) ;
                            }
                }

    The return type void * is automatically cast to the type of the lvalue type but to make it more explicit we would do the following

                if ( !( (char * )p = malloc( sizeof( char ) * 100 ) )
                            {
                            puts( "Out of memory" ) ;
                            exit(1) ;
                            }

    To free the block of memory allocated we do the following

                free ( p ) ;

    Note :- There are a number of memory allocation functions included in the standard library including calloc( ), _fmalloc( ) etc. Care must be taken to ensure that memory allocated with a particular allocation function is released with its appropriate deallocation function, e.g. memory allocated with malloc() is freed only with free() .

    Multiple Indirection -- Pointers to Pointers

    It is possible in C to have a pointer point to another pointer that points to a target value. This is termed multiple indirection in this case double indirection. Multiple indirection can be carried out to whatever extent is desired but can get convoluted if carried to extremes.

    In the normal situation, single indirection, a pointer variable would hold the address in memory of an appropriate variable, which could then be accessed indirectly by de-referencing the pointer using the * operator.

    In the case of double indirection, we have the situation where a variable may be pointed to by a pointer as with single indirection, but that this pointer may also be pointed to by another pointer. So we have the situation where we must de-reference this latter pointer twice to actually access the variable we are interested in. De-referencing the pointer to a pointer once gives us a normal singly indirected pointer, de-referencing the pointer to a pointer secondly allows us to access the actual data variable. The situation is depicted in the diagram below.

    To declare a pointer to a pointer we include another indirection operator

                float * * ptr ;

    which in this case defines a pointer to a pointer to type float.

    The following illustrates some valid operations using double indirection.

                int x = 10, *p, **q ;

                p = &x ;
                q = &p ;

                **q = 20 ; // de-reference twice to access value
                p = *q ; // de-reference q once to get a pointer to int
                …
                int array1[] = { 1,2,3,4,5,6 ,7 ,8,9,10} ;
                int array2[] = {10,20,30,40,50} ;
                int *pointers[2] ; // an array of pointers to type int
                int **ptr ; // a doubly indirected pointer

                ptr = pointers ; // initialise pointer to array of pointers
                *ptr++ = array1 ; // now we simply de-reference the pointer to a pointer
                *ptr = array2 ; // once and move it on like any pointer

                **ptr = 100 ; // ptr is pointing at pointers[1] which in turn is pointing
                                     // at array2 so array2[0] is assigned 100
    For Example :- Allocation and initialisation of an m x n matrix using double indirection

    What we require here is to allocate an n x n matrix as a collection of discrete rows rather than just as one block of memory. This format has advantages over a single block allocation in certain situations. The structure we end up with is illustrated below.

    #include <stdio.h>
    #include <malloc.h>

    void main( void )
    {
    double **ptr_rows, **user_ptr, *elem_ptr ;
    int m, n, i, j ;

    printf( “\n\nEnter the number of rows and columns required (m, n) : “ ) ;
    scanf( “%d, %d”, &m, &n ) ;
    _flushall() ;

    ptr_rows = ( double **) malloc( m * sizeof ( double * ) ) ; // space for row pointers

    user_ptr = ptr_rows ;
    for ( i = 0; i < m ; i++ ) // and then row elements
                {
                *user_ptr = (double *) malloc( n * sizeof( double ) ) ;

                elem_ptr = *user_ptr ;
                for ( j = 0; j < n ; j++ )
                *elem_ptr++ = 1.0 ;

                user_ptr++ ; // move onto next row pointer
                }

                                  // after use we need to clean up in reverse order
    user_ptr = ptr_rows ;

    for ( i = 0; i < n; i++ )
    free( *user_ptr ++ ) ; // free a row and move onto next

    free( ptr_rows ) ; // free pointers to rows

    }

    Pointers to Functions

    A function even though not a variable still has a physical address in memory and this address may be assigned to a pointer. When a function is called it essentially causes an execution jump in the program to the location in memory where the instructions contained in the function are stored so it is possible to call a function using a pointer to a function.

    The address of a function is obtained by just using the function name without any parentheses, parameters or return type in much the same way as the name of an array is the address of the array.

    A pointer to a function is declared as follows

    Syntax : ret_type ( * fptr ) ( parameter list ) ;

    where fptr is declared to be a pointer to a function which takes parameters of the form indicated in the parameter list and returns a value of type ret_type.

    The parentheses around * fptr are required because without them the declaration

                ret_type * fptr( parameter list ) ;

    just declares a function fptr which returns a pointer to type ret_type !

    To assign a function to a pointer we might simply do the following

                int (*fptr)( ) ;

                fptr = getchar ; /* standard library function */

    To call the function using a pointer we can do either of the following

                ch = (*fptr)( ) ;
                ch = fptr( ) ;

    Example :- Program to compare two strings using a comparison function passed as a parameter.

                #include <stdio.h>
                #include <string.h>
                void check( char *a, char *b, int ( * cmp ) ( ) );

                void main( )
                {
                char s1[80], s2[80] ;
                int (*p)( ) ;

                p = strcmp ;

                gets(s1) ;
                gets( s2 );

                check( s1, s2, p ) ;
                }
                void check ( char *a, char *b, int (* cmp)( ) )
                {
                if ( ! cmp( a, b ) )
                            puts( "equal" ) ;
                else
                            puts( "not equal") ;
                }

    Note that even though we do not specify parameters to the function pointer in the prototype or declarator of the function we must specify them when actually calling the function.

    Note also that instead of using an explicitly declared pointer variable to call the required function in main() we could make the call as follows

                check( s1, s2, strcmp ) ;

    where we essentially pass a constant pointer to strcmp( ).

    For Example : Program that may check for either numeric or alphabetic equality.

                #include <stdio.h>
                #include <ctype.h>
                #include <stdlib.h>
                #include <string.h>

                void check( char *a, char *b, int ( * cmp ) ( ) );
                int numcmp( char *, char * ) ;

                void main( )
                {
                char s1[80], s2[80] ;

                gets(s1) ;
                gets( s2 );

                if ( isalpha( *s1 ) // should have a more rigorous test here
                            check( s1, s2, strcmp ) ;
                else
                            check( s1, s2, numcmp ) ;
                }
                void check ( char *a, char *b, int (* cmp)( ) )
                {
                if ( ! cmp( a, b ) )
                            puts( "equal" ) ;
                else
                            puts( "not equal") ;
                }

                int numcmp( char *a, char *b )
                {
                if ( atoi( a ) == atoi( b ) )
                            return 0 ;
                else
                            return 1 ;
                }

    Efficiency Considerations

    When used correctly pointers can lead to more efficient code in situations where sequential operations on contiguous blocks of memory are required.

    For example when accessing each element of an array sequentially. The inefficient way to do this is

                for ( k = 0; k < 100; k++ )
                            array[ k ] = 0.0 ;

    When done this way the compiler has to index into the array for each iteration of the loop. This involves reading the current value of the index, k, multiplying this by the sizeof( double ) and using this value as an offset from the start of the array.

    The exact same thing occurs if we use a pointer incorrectly as follows

                ptr = array ;
                for ( k = 0; k < 100; k++ )
                            *( ptr + k ) = 0.0 ;

    whereas the most efficient solution is of course to do the following where the pointer itself is moved by the appropriate amount.

                ptr = array ;
                for ( k = 0; k < 100; k++ )
                            *ptr++ = 0.0 ;

    In this case we just incur the addition of sizeof( double ) onto the address contained in the pointer variable for each iteration

     

Industry      Interaction

Higher Education

Job Skills

Soft Skills

Comm. English

Mock Test

E-learning