Categories
Computer Science / Information Technology Language: C

Pointers

A pointer is a variable whose value is the address of another variable, i.e., the direct address of the memory location.

Pointer Notation

Consider the declaration,

int i = 3 ;

This declaration tells the C compiler to:

  1. Reserve space in memory to hold the integer value.
  2. Associate the name ‘i’ with this memory location.
  3. Store the value 3 at this location. We may represent i’s location in memory by the following memory map.

We see that the computer has selected memory location 65524 as the place to store the value 3. The location number 65524 is not a number to be relied upon, because some other time the computer may choose a different location for storing the value 3. The important point is, i’s address in memory is a number. We can print this address number through the following program:

int main( )
{
   int i = 3 ;
   printf ( "\nAddress of i = %u", &i ) ;
   printf ( "\nValue of i = %d", i ) ;
}

The output of the above program would be:

Address of i = 65524

Value of i = 3
  • The ‘&’ used in printf( ) statement is C’s ‘address of’ operator. It returns the address of the variable ‘i’, which in this case happens to be 65524.
  • As the address cannot be negative, it is printed out using %u, which is a format specifier for printing an unsigned integer.
  • The other pointer operator available in C is ‘*’, called the ‘value at address’ operator. It gives the value stored at a particular address. The ‘value at address’ operator is also called the ‘indirection’ operator. Observe the output of the following program:
#include <stdio.h>
int main()
{
    int i = 3 ;
    printf ( "\nAddress of i = %u", &i ) ;
    printf ( "\nValue of i = %d", i ) ;
    printf ( "\nValue of i = %d", *( &i ) ) ;
    return 0;
}

The output of the above program would be:

Address of i = 65524
Value of i = 3
Value of i = 3

Note that printing the value of *( &i ) is the same as printing the value of i. The expression &i gives the address of the variable i. This address can be collected in a variable, by saying,

j = &i ;

But remember that j is not an ordinary variable like any other integer variable. It is a variable that contains the address of another variable (i in this case). Since j is a variable the compiler must provide it space in the memory. Once again, the following memory map would illustrate the contents of i and j.

As you can see, i’s value is 3 and j’s value is i’s address. But wait, we can’t use j in a program without declaring it. And since j is a variable that contains the address of ‘i’, it is declared as,

int *j ;

Like any variable or constant, you must declare a pointer before using it to store any variable address. The general form of a pointer variable declaration is −

type *var-name;

Here, type is the pointer’s base type; it must be a valid C data type and
var-name is the name of the pointer variable. The asterisk * used to declare a pointer is the same asterisk used for multiplication. However, in this statement, the asterisk is being used to designate a variable as a pointer. Take a look at some of the valid pointer declarations −

int *ip;     /* pointer to an integer */
double *dp;  /* pointer to a double */
float *fp;   /* pointer to a float */
char *ch     /* pointer to a character */

The actual data type of the value of all pointers, whether integer, float, character or otherwise, is the same, a long hexadecimal number that represents a memory address. The only difference between pointers of different data types is the data type of the variable or constant that the pointer points to.

How to Use Pointers?

There are a few important operations, which we will do with the help of pointers very frequently.

  1. We define a pointer variable.
  2. Assign the address of a variable to the pointer.
  3. Access the value at the address available in the pointer variable, also called dereferencing the pointer.

This is done by using the unary operator * that returns the value of the variable located at the address specified by its operand. The following example makes use of these operations −

#include <stdio.h>
int main ()
{
    int var = 20; /* actual variable declaration */
    int *ip; /* pointer variable declaration */

    ip = &var; /* store address of var in pointer variable*/

    printf("Address of var variable: %x\n", &var );

    /* address stored in pointer variable */
    printf("Address stored in ip variable: %x\n", ip );

    /* access the value using the pointer */
    printf("Value of *ip variable: %d\n", *ip );

    return 0;
}

When the above code is compiled and executed, it produces the following result −

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

The assignment operation (=) between two pointers makes them point to the same pointee. It’s a simple rule for a potentially complex situation, so it is worth repeating: assigning one pointer to another makes them point to the same thing. Consider the following example:

#include <stdio.h>
int main() {
    int num = 42;
    int *numPtr, *second;
    numPtr = &num;
    second = numPtr; // Same as: second = &num;
    printf("Value of num: %d\n", num);
    printf("Address of num: %x\n", &num);
    printf("Value of numPtr: %x\n", numPtr);
    printf("Value numPtr pointing to: %d\n", *numPtr);
    printf("Value of second: %x\n", second);
    printf("Value second pointing to: %d\n", *second);
    return 0;
}

Output

Value of num:             42
Address of num:           6a6e55f4
Value of numPtr:          6a6e55f4
Value numPtr pointing to: 42
Value of second:          6a6e55f4
Value second pointing to: 42

The above program can be summed up with the following block diagram:

Tip: Make drawings. Memory drawings are the key to thinking about pointer code.
When you are looking at code, thinking about how it will use memory at run time, make a quick drawing to work out your ideas. That’s the way to do it.

Sharing

Two pointers referring to the same memory address are said to be “sharing”. That two or more entities can cooperatively share a single memory structure is a key advantage of pointers in all computer languages. Sharing can be used to provide efficient communication between parts of a program.

Pointer Arithmetic

A pointer in c is an address, which is a numeric value. Therefore, you can
perform arithmetic operations on a pointer just as you can on a numeric value. There are four arithmetic operators that can be used on pointers: ++, –, +, and -. Before jumping into the examples, please make sure you have a basic idea of hexadecimal values and arithmetic operations on them as all virtual addresses are hexadecimal values. Consider the following program running on a 32-bit compiler (hence the size of an integer is 4 bytes):

#include <stdio.h>
int main() 
{
    int i = 5;
    int* ptr = &i;
    printf ("%p\n", ptr);
    return 0;
}

The output is:

0x7ffd58311d7c

In the above simple program, the address of an integer value is assigned to a pointer. And the address stored in the pointer is being printed. Now, consider adding 1 to the pointer directly and also by using post increment, which is done in the following program:

#include <stdio.h>
int main()
{
    int i = 5;
    int* ptr = &i;
    printf ("ptr : %p\n", ptr);
    ptr = ptr + 1;
    printf ("ptr + 1 : %p\n", ptr);
    ptr++;
    printf ("ptr++ : %p\n", ptr);
    return 0;
}

The output where the last two characters of the address is highlighted for convenience:

ptr : 0x7ffd58311d7c
ptr + 1 : 0x7ffd58311d80
ptr++ : 0x7ffd58311d84

One may notice that ptr + 1 is 80 and not 7d. Consider another example where we attempt to add 1 to a pointer pointing to a value of data type double:

#include <stdio.h>
int main()
{
    double i = 5;
    double* ptr = &i;
    printf("Size of double: %ld\n", sizeof(double));
    printf ("ptr : %p\n", ptr);
    ptr = ptr + 1;
    printf ("ptr + 1 : %p\n", ptr);
    ptr++;
    printf ("ptr++ : %p\n", ptr);
    return 0;
}

The output where the last two characters of the address are highlighted for convenience. It is important to remember that the addition is happening to hex values and not decimal values.

Size of double: 8
ptr : 0x7ffe92406338

ptr + 1 : 0x7ffe92406340
ptr++ : 0x7ffe92406348

Points to note from the above two examples:

  • One can add integer values to a pointer either by using the ‘+’ operator or by post (and pre) incrementation.
ExpressionData type ptr pointing toSize on 16-bit compilerOperation (hex addition)Result
ptr + 1int47c + 480
ptr++int490 + 484
ptr + 1double838 + 840
ptr++double840 + 848
  • Hence, it can be concluded that the compiler is smart enough to add the integral multiple of the size of the data type it is pointing to with the factor by which it is getting added. That is, if the factor is 1, the addition will be size * 1, if the factor is 2, the addition will be size * 2.

Pointer arithmetic is extensively used in arrays. It is further explored in the ‘Arrays’ section.

Comparison of pointers

Consider the following example:

#include <stdio.h>
int main()
{
    double i = 5;
    double* ptr_1 = &i;
    double* ptr_2 = &i; // Same as double* ptr_2 = ptr_1
    if (ptr_1 == ptr_2)
    printf("ptr_1 and ptr_2 are pointing to same location");
    return 0;
}

Output:

ptr_1 and ptr_2 are pointing to same location

From the above example, it is clear that a comparison operator of equality between two pointers checks if the pointers are pointing to the same memory location. It is often that we use a comparison operator to check if a pointer is a NULL pointer (discussed in the ‘NULL pointers’ subsection).

Caution

It is very important to realize that the pointers are pointing to an unallocated area or possibly an area allocated to some other process or variable local to the program. Any attempt to dereference a pointer in the above examples and similar scenarios will result in segmentation fault errors.
Do not attempt the following operations on pointers… they would never work out.

  1. Addition of two pointers
  2. Multiplication of a pointer with a constant
  3. Division of a pointer with a constant

Exercise: Write a program which decrements ptr values pointing to various other data types, using arithmetic ‘-’ operator and pre and post decrement unary operators. Analyze the behaviour of the pointers in the program.

Shallow and Deep Copying

In particular, sharing can enable communication between two functions. One function passes a pointer to the value of interest to another function. Both functions can access the value of interest, but the value of interest itself is not copied. This communication is called “shallow” since instead of making and sending a (large) copy of the value of interest, a simple pointer is sent.

The alternative where a complete copy is made and sent is known as a “deep” copy. Deep copies are simpler in a way since each function can change its copy without interfering with the other copy, but deep copies run slower because of all the copying. This topic is explained in detail with examples under “Heaps” in the “Advanced topics in Pointers” section.

NULL Pointers

A pointer that is assigned NULL is called a null pointer. It is always a good practice to assign a NULL value to a pointer variable in case you do not have an exact address to be assigned. This is done at the time of variable declaration.

The NULL pointer is a constant with a value of zero defined in several standard libraries. Consider the following program −

#include <stdio.h>
int main ()
{
    int *ptr = NULL;
    printf("The value of ptr is : %x\n", ptr );
    return 0;
}

Output:

The value of ptr is 0

In most operating systems, programs are not permitted to access memory at address 0 because that memory is reserved by the operating system. However, the memory address 0 has special significance; it signals that the pointer is not intended to point to an accessible memory location. But by convention, if a pointer contains the null (zero by address convention) value, it is assumed to point to nothing.

To check for a null pointer, you can use an ‘if’ statement as follows −

if(ptr) /* succeeds if p is not null */
if(!ptr) /* succeeds if p is null */

Void pointers

It is a pointer that has no associated data type with it. A void pointer can hold addresses of any type and can be typecast to any type. It is also called a generic pointer and does not have any standard data type. It is created by using the keyword void.

#include <stdio.h>
int main()
{
    void *p = NULL; //void pointer assigned to NULL
    printf("The size of pointer is:%ld\n", sizeof(p)); //size of p is
    platform dependant
    return 0;
}

Important Points:

  • void pointers cannot be dereferenced directly. It can however be done using type casting the void pointer. The following example illustrates the rule:
#include<stdlib.h>
#include<stdio.h>
int main()
{
    int x = 4;
    void *ptr = &x;
    
    // (int*)ptr - does type casting from void pointer to
    Integer pointer
    // *((int*)ptr) dereferences the typecasted void pointer
    variable.
    
    printf("Integer variable is = %d", *( (int*) ptr) );
    // Similarly, the same void pointer can hold address of a
    float variable
    float y = 3.6;
    ptr = &y;
    printf("\nFloat variable is= %f", *( (float*) ptr) );
    
    return 0;
}
  • In the above example, it is clear that one needs to typecast the pointer to the desired data type and then use it accordingly. The syntax to do this is as follows:
○ Typecast:
     (target_data_type *) pointer_variable
○ Dereference:
    * ((target_data_type *) pointer_variable)
  • Pointer arithmetic is not possible on pointers of void due to lack of concrete value and thus size.

Wild/Bad/Uninitialized pointers

Wild pointers are also called uninitialized pointers or bad pointers. Because they point to some arbitrary memory location and may cause a program to crash or behave badly.

Bad pointers are very common. In fact, every pointer starts out with a bad value. Correct code overwrites the bad value with a correct reference to a pointee, and thereafter the pointer works fine. There is nothing automatic that gives a pointer a valid initialisation.

The following program illustrates an example of wild pointers. This might produce an unpredictable output.

#include <stdio.h>
int main() {
    int *p; //wild pointer
    printf("\n%d",*p);
    return 0;
}

A very common bad pointer example is as follows:

void BadPointer() {
int* p;
// allocate the pointer, but not the pointee
*p = 42;
// this dereference is a serious runtime error
}

As one can see in the above program, a pointer is declared and is uninitialized but an attempt is made to dereference it which will cause the program to crash.

The bad code will compile fine, but at run-time, each dereferences with a bad pointer will corrupt memory in some way. The program will crash sooner or later. It is up to the programmer to ensure that each pointer is assigned a pointee before it is used.

Dangling pointer

To understand what dangling pointers are, first one needs to understand what dynamic memory allocation is along with their corresponding functions such as malloc( ), calloc( ), realloc( ) and most importantly free( ). These functions are discussed in detail in the “Memory allocation functions” of the “Memory allocation” subsection of the “Arrays” section.

When a pointer pointing to a freed memory location is a dangling pointer. Predominantly there are 3 scenarios that can give rise to dangling pointers.

  1. When allocated memory is freed.
#include <stdlib.h>
#include <stdio.h>
int main()
{
      int *ptr = (int *)malloc(sizeof(int));

      // After below free call, ptr becomes a
      // dangling pointer
      free(ptr);

      // No more a dangling pointer
      ptr = NULL; 
}
  1. Returning address of a memory location of a non-static member from function.
#include<stdio.h>
char *foo()
{
    // x is local non-static variable that goes out
    // of scope once foo returns control to caller
    char ch = 'a';
    return &ch;
}
int main()
{
    char *ptr = foo();
    // p points to something which is not
    // valid anymore
    printf("%c", *ptr);
    return 0;
}
  1. This type is an extension of the 2nd type. When a pointer is pointing to the address of a local variable and we are trying to access the pointer values out of the scope of the local variable.
int main()
{
   float *ptr;
   .....
   .....
{
    float marks = 90.7;
    ptr = &marks;
}
.....
// Here ptr is dangling pointer
}

Dangling pointers are one of the leading causes of segmentation faults which cause programs to crash erratically. These are one of the most non-descriptive errors and sometimes can get very difficult to trace in a large program without any specialized tools such as GNU GDB (GNU Debugger).

Hence, it is always recommended to make the pointer a NULL pointer soon after the deallocation of the memory to avoid dangling pointers. It is also recommended to resolve all the compiler warnings as compilers are smart enough to detect the possibilities of dangling pointers.

Advanced topics in Pointers

Memory layouts in C

A typical memory representation of a C program consists of the following sections. A pictorial representation:

  1. Text segment (i.e. instructions)
    • A text segment, also known as a code segment or simply as text, is one of the sections of a program in an object file or in memory, which contains executable instructions.
    • This begs a question, what is an object file? An object file is an output of an engine called an assembler which is a part of the compiler. This is an intermediary code that is not directly executable but can be relocated to other machine architectures and executed. To know more about this, please read about the phases of the compilation of a C program.
    • As a memory region, a text segment may be placed below the heap or stack in order to prevent heaps and stack overflows from overwriting it.
    • This segment is write-protected so as to prevent accidental modification of code. It is shared to increase memory utilization efficiency as many applications such as shell or debugger may use the same code. It is also helpful that this segment is shared when a process spawns multiple processes or threads.
  1. Initialized data segment
    • This segment is a portion of the virtual address space of a program, which contains the global, static and extern variables that are initialized by the programmer.
#include <stdio.h>
/* global variables stored in Initialized Data Segment in
read-write area*/
    char c[] = "Quant Masters";
    const char s[] = "C Programming";
int main()
{
    static int i=11; /* static variable stored in Initialized Data
    Segment*/
    return 0;
}
  1. Uninitialized data segment (bss)
    • Data in this segment is initialized to arithmetic 0 before the program starts executing. Uninitialized data starts at the end of the data segment and contains all global variables and static variables that are initialized to 0 or do not have explicit initialization in the source code.
#include <stdio.h>
char c; /* Uninitialized variable stored in bss*/
int main()
{
   static int i; /* Uninitialized static variable stored
   in bss */
   return 0;
}
  1. Heap
    • To understand heap and its uses, one must have the basic knowledge of dynamic memory allocation which is discussed in the Arrays chapter.
    • This is a place in the memory segment where dynamic allocation
      happens. All the memory is given to pointers when malloc, calloc and realloc are from the heap. Heaps are especially efficient when arrays are to be passed among multiple functions as just the pointers are getting copied into the function stack (shallow copy). Consider the following example:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 5
void foo(int *array)
{
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i * 10;
}
int main()
{
    int *array = (int *) malloc (sizeof(int) * SIZE);
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i;
    foo (array);
    for (int i = 0 ; i < SIZE ; i++)
    printf("%d ", array[i]);
    return 0;
}
  • The output of the above function:
0 10 20 30 40
  • In the above example, the array is pointing to a dynamically allocated
    location at heap as malloc is being used. Then the array elements are
    getting initialized to their corresponding index values. Post the
    initialization, the array is sent to foo as an argument. Here it is important to note that calling foo doesn’t create a deep copy array by allocating a new memory set in heap or stack. Instead, a new pointer in the stack frame (go to the advanced section of the functions chapter to learn more about stack frames) is created and is made to point to the same memory location in the heap. Hence the modification made in the function foo( ) is sustained even after the control comes back to the main ( ) function. The following example prints the address of the memory location. This should make the above point clearer.
#include <stdio.h>
#include <stdlib.h>
#define SIZE 5
void foo(int *array)
{
    printf("In foo function:\n");
    for (int i = 0 ; i < SIZE ; i++)
    printf("Address of array[%d]: %p\n", i, &array[i]);
}
int main()
{
    int *array = (int *) malloc (sizeof(int) * SIZE);
    for (int i = 0 ; i < SIZE ; i++)
    array[i] = i;
    foo (array);
    printf("In main function:\n");
    for (int i = 0 ; i < SIZE ; i++)
    printf("Address of array[%d]: %p\n", i, &array[i]);
    return 0;
}
  • The output of the above program:
In foo function:
Address of array[0]: 0x559b11c572a0
Address of array[1]: 0x559b11c572a4
Address of array[2]: 0x559b11c572a8
Address of array[3]: 0x559b11c572ac
Address of array[4]: 0x559b11c572b0
In main function:
Address of array[0]: 0x559b11c572a0
Address of array[1]: 0x559b11c572a4
Address of array[2]: 0x559b11c572a8
Address of array[3]: 0x559b11c572ac
Address of array[4]: 0x559b11c572b0
  • As the addresses resemble each other in both the functions, it can be
    concluded that the memory location the pointers are pointing to in both foo( ) and main( ) is the same.
  • It is very important to note that the heap grows upwards. The direction of the arrow in the block diagram should make this point obvious.
  1. Stack
    • The stack segment focuses on storing the contents of a function call. It consists of stack frames where each frame corresponds to a function call.
    • When a function is called, a dedicated stack frame is created and pushed on top of the stack segment. The stack frame just below it belongs to the function that called the currently executing function, and so forth. The bottom of the stack essentially should be the stack frame containing the main( ) function call. This frame is popped once the execution of the function is completed.
    • A stack frame stores the following data:
      • Local variables used in the function. Includes the received parameters from the calling function.
      • The return address of the instruction in the caller function that is to be executed after the function call is over.
      • Saved copies of registers modified by subprograms that could need restoration. This is done mainly for optimisation and keeping track of register contents.
    • Consider main( ) function calls a function foo( ) and passes an integer asargument.
1. void foo(int i)
2. {
3.      printf("%d", i);
4. }
5. int main()
6. {
7.      foo(3);
8.      printf("Hello World");
9. }
  • The following is the sequence of execution:
    • The program starts execution from the main function. A stack frame for the main( ) function call is created and pushed onto the stack segment.
    • The first line in the main function calls a function called foo( ). This results in the transfer of the control to foo function at line #1. Along with this, a stack for foo( ) function call is also created. This contains the parameter value ‘i’ and return address where the control has to start execution once foo has completed execution. In this example, it is line #8. This newly created stack frame is pushed onto the stack and the control continues executing the statements in foo( ).
    • Once the value of ‘i’ is printed, the control refers to the stack frame of foo( ), which is at the top of the stack and determines where the control should start executing in the main( ) function. That is from line #8. Hence the control starts executing from line #8 and not from the beginning of the main( ) function.
  • Note that the stack frames are created for each function call. This means that there could be multiple stack frames of the same function in the stack segment in case of recursion. This is the reason why recursion is inefficient when compared to iterative alternatives of the same logic.
  • The stack and heap are traditionally located at opposite ends of the process’s virtual address space.
  • Memory is a finite resource and if too many functions are called before any returns, a program can run out of space on the stack. This usually happens only in the case of recursive functions that are misbehaving but for programs with very tight memory constraints, it may happen in some kinds of programs. This scenario where the process runs out of dedicated memory for the stack is called stack overflow.

Near pointers

A near pointer is a 16-bit pointer to an object contained in the current segment, be it code segment, data segment, stack segment, or extra segment. The compiler can generate code with a near pointer and does not have to concern itself with segment addressing, so using near pointers is the fastest, and generates the smallest code. The limitation is that you can only access 64kb of data at a time because that is the size of a segment – 64kb. A near pointer contains only the 16-bit offset of the object within the currently selected segment.

Far Pointers

A far pointer is a 32-bit pointer to an object anywhere in memory. In order to use it, the compiler must allocate a segment register (segment register is the one that points to the base of the current segment being addressed), load it with the segment portion of the pointer, and then reference memory using the offset portion of the pointer relative to the newly loaded segment register. This takes extra instructions and extra time, so it is the slowest and largest method of accessing memory, but it can access memory that is larger than 64kb, sometimes, such as when dealing with video memory, a needful thing. A far pointer contains a 16-bit segment part and a 16 bit offset part. Still, at any one instant of time, without “touching” segment registers, the program only has access to four 64kb chunks or segments of memory. If there is a 100kb object involved, code will need to be written to consider its segmentation, even with far pointers.

Now, segments overlap. Each segment is 64kb in length, but each one overlaps the next and the prior by 65520 bytes. That means that every address in memory can be addressed by 64kb-1 different combinations of the segment: offset pairs. The result is that the total addressable memory was only 1MB, and the total usable memory address space was 500kb to 600kb. That sounds odd, but Intel built it, Microsoft wrote
it, and DOS/Windows 3.1 grew up around it. I still have that computer, and it still works just fine.

Huge pointers

The far pointer suffers because you can not just add one to it and have it point to the next item in memory – you have to consider segment: offset rules, because of the 16-bit offset issue. The huge pointer is a monolithic pointer to some item with a large chunk of memory, and there is no segment: offset boundaries.

Double Pointers

The concept of pointers can be further extended. Pointer, we know, is a variable that contains the address of another variable. Now this variable itself might be another pointer. Thus, we now have a pointer that contains another pointer’s address. The following example should make this point clear.

#include <stdio.h>
int main( )
{
    int i = 3, *j, **k ;
    j = &i ;
    k = &j ;
    printf ( "\nAddress of i = %u", &i ) ;
    printf ( "\nAddress of i = %u", j ) ;
    printf ( "\nAddress of i = %u", *k ) ;
    printf ( "\nAddress of j = %u", &j ) ;
    printf ( "\nAddress of j = %u", k ) ;
    printf ( "\nAddress of k = %u", &k ) ;
    printf ( "\nValue of j = %u", j ) ;
    printf ( "\nValue of k = %u", k ) ;
    printf ( "\nValue of i = %d", i ) ;
    printf ( "\nValue of i = %d", * ( &i ) ) ;
    printf ( "\nValue of i = %d", *j ) ;
    printf ( "\nValue of i = %d", **k ) ;
    return 0;
}

The output of the above program would be:

Address of i = 65524
Address of i = 65524
Address of i = 65524
Address of j = 65522
Address of j = 65522
Address of k = 65520
Value of j = 65524
Value of k = 65522

Remember that when you run this program the addresses that get printed might turn out to be something different than the ones shown in the figure. However, with these addresses the relationship between i, j and k can be easily established.

Observe how the variables j and k have been declared,

int i, *j, **k ;

Here, i is an ordinary int, j is a pointer to an int (often called an integer pointer), whereas k is a pointer to an integer pointer. We can extend the above program still further by creating a pointer to a pointer to an integer pointer. In principle, you would agree that likewise there could exist a pointer to a pointer to a pointer to a pointer to a pointer.
There is no limit on how far we can go in extending this definition.

Function Pointers

A function pointer is a pointer that holds the address of a function. The ability of pointers to point to functions turns out to be an important and useful feature of C. This provides us with another way of executing functions in an order that may not be known at compile time and without using conditional statements.

Branch prediction is a technique whereby the processor will guess which multiple execution sequences will be executed. Pipelining is a hardware technology commonly used to improve processor performance and is achieved by overlapping instruction execution.

One concern regarding the use of function pointers is their inefficiency. The processor may not be able to use branch prediction in conjunction with pipelining.

Declaring Function Pointers

  • Syntax:
return_type (*fptr_id)([dt1, dt2, ...]);
  • Where,
    • return_type is the return data type of the function whose address the pointer is intending to hold.
    • fptr_id is a valid identifier name for the function pointer.
      ○ dt1, dt2, … are the optional. They are the datatypes of the
      parameter list the function has whose address the pointer is
      intending to hold.
  • When function pointers are used, the programmer must be careful to ensure it is used properly because C does not check to see whether the correct parameters are passed.
  • A simple example of a function pointer which can point to a function which has a void as a parameter and returns a void is as follows:
void (*foo) ();
  • Other valid examples of function pointers are:
int (*f1)(double); // Accepts a double value as parameter and
returns an int
void (*f2)(char*); // Accepts a char pointer as parameter and
returns void
double* (*f3)(int, int); // Accepts two integer values as
parameters and returns a pointer to a double
  • One suggested naming convention for function pointers is to always begin their name with the prefix: fptr. This increases the readability and code maintainability.

Using a Function Pointer

  • Consider the following example:
#include <stdio.h>
int (*fptr1)(int);

int square(int num) {
    return num*num;
}
int main() {
    int n = 5;
    fptr1 = square;
    printf("%d squared is %d\n",n, fptr1(n));
    return 0;
}

Output:

5 squared is 25
  • Points to note from the above example:
    • The return type and the data type and the number of elements in the parameter list match between the function square( ) and the pointer which points to square( ): fptr1. That is, the return type is int and it accepts only one parameter which is of type int.
    • The function pointer fptr1 is declared before it is initialized.
    • Once initialized fptr to point square( ), fptr1 behaves as a replacement to the identifier ‘square’. That is, fptr1(n) and square(n) would yield the same results.
  • Another way of fptr1 initialization in the above example is using the address-of operator (&). It can be done as follows:
fptr1 = &square;
  • The use of the address of operator is of no significance and the compiler effectively ignores it.
  • Another way of using a function pointer is to use typedef. It is illustrated in the following example:
#include <stdio.h>
typedef int (*funPtr)(int);
int square(int num) 
{
    return num*num;
}
int main()
{
    int n = 5;
    funPtr fptr1;
    fptr1 = square;
    printf("%d squared is %d\n",n, fptr1(n));
    return 0;
}
  • The above yields the same results as its immediate previous example.
  • It is quite non-intuitive to use typedef for function pointers as typedef behaves differently for function pointers. Usually, typedef’s name is the declaration’s last element.

Passing Function Pointers

  • Passing function pointers to functions is quite intuitive. The following examples illustrate the same:
#include <stdio.h>
int multiply(int num1, int num2) 
{
   return num1 * num2;
}
int divide(int num1, int num2)
{
    return num1 / num2;
}
typedef int (*fptrOperate)(int, int);
int calculate(fptrOperate operation, int num1, int num2)
{
    return operation(num1, num2);
}
int main()
{
    int op1 = 10, op2 = 5;
    fptrOperate fptr1;
    fptr1 = multiply;
printf("%d * %d = %d\n", op1, op2, calculate(fptr1, op1,
op2));
    fptr1 = divide;
    printf("%d / %d = %d\n", op1, op2, calculate(fptr1, op1,
op2));
    return 0;
}
  • The output of the above program is:
10 * 5 = 50
10 / 5 = 2
  • Points to note from the above example:
    • A function pointer can simply be declared and passed to functions just like any other variable.
    • A function pointer can point to different functions at different points of execution times. In this example, initially fptr1 points to multiple( ) functions and later it points to divide( ).
    • A function pointer type definition should be declared before the function prototype or definition which accepts the function type definition. In this example, the type definition fptrOperate is declared before calculate( ) which uses fptrOperate in its parameter list.

Returning Function Pointers

  • Returning a function pointer requires declaring the function’s return type as a function pointer. Consider the following example:
#include <stdio.h>
int multiply(int num1, int num2)
{
    return num1 * num2;
}

int divide(int num1, int num2) 
{
    return num1 / num2;
}
typedef int (*fptrOperate)(int, int);
fptrOperate select(char opcode) 
{
    switch(opcode) {
        case '*': return multiply;
        case '/': return divide;
    }
}
int evaluate(char opcode, int num1, int num2)
{
    fptrOperate operation = select(opcode);
    return operation(num1, num2);
}
int main()
{
    int op1 = 10, op2 = 5;
    printf("%d * %d = %d\n", op1, op2, evaluate('*', op1, op2));
    printf("%d / %d = %d\n", op1, op2, evaluate('/', op1, op2));
    return 0;
}

Output:

10 * 5 = 50
10 / 5 = 2
  • Structure of the above program:
  1. Type definition of function which returns an integer value and accepts 2 integer values are made to fptrOperate.
  2. A function select( ) reads the operator and returns the function pointer of the corresponding function.
  3. Function evaluate( ) which:
    • Reads operator and operands and sends the operator to select( ).
    • Stores the function pointer returned by select( ) in ‘operation’.
    • Calls function pointed by ‘operation’ and passes the operands which it received as parameters (num1, num2).
    • Returns the value which is returned by the call from ‘operation’.
  4. Functions multiply( ) and divide( ) accept two integers and return an
    integer after performing multiplication and division operations on the
    parameter list. Hence, the function pointer return operation is similar to any other value return from a function.

Using an Array of Function Pointers

Arrays of function pointers can be used to select the function to evaluate on the basis of some criteria. Declaring such an array is straightforward.

typedef int (*funPointer)(int, int);
funPointer fptr_array[64] = {NULL};

Alternatively, one can use the syntax used in the following snippet:

int (*fptr_array[64])(int, int) = {NULL};

Both of the above code snippets declare a function pointer array named fptr_array with the capacity to hold 64 function pointers, each one of which returns an integer and accepts 2 parameters, both of which are of type integer. The entire array is initialized to NULL.

The example given in ‘Returning Function Pointers’ can be rewritten using an array of function pointers as follows: (Note that this example includes addition, subtraction and modulus operation as well)

#include <stdio.h>
int add (int num1, int num2) {
    return num1 + num2;
}

int subtract (int num1, int num2) {
    return num1 - num2;
}

int multiply (int num1, int num2) {
    return num1 * num2;
}

int divide (int num1, int num2) {
    return num1 / num2;
}

int mod (int num1, int num2) {
    return num1 % num2;
}

typedef int (*fptrOperate) (int , int);

fptrOperate fptr_array[5] = {NULL};

void initFptrArray () {
    fptr_array[0] = add;
    fptr_array[1] = subtract;
    fptr_array[2] = multiply;
    fptr_array[3] = divide;
    fptr_array[4] = mod;
}

fptrOperate select (char opcode) {
    switch (opcode) {            
        case '+': return fptr_array[0];
        case '-': return fptr_array[1];
        case '*': return fptr_array[2];
        case '/': return fptr_array[3];
        case '%': return fptr_array[4];
        default : return NULL;
    }
}

int evaluate (char opcode, int num1, int num2) {
    fptrOperate operation = select (opcode);  
    return operation (num1, num2);
}

int main () {
    initFptrArray ();
    int op1 = 10, op2 = 5;
    printf ("%d + %d = %d\n", op1, op2, evaluate ('+', op1, op2));
    printf ("%d - %d = %d\n", op1, op2, evaluate ('-', op1, op2));
    printf ("%d * %d = %d\n", op1, op2, evaluate ('*', op1, op2));
    printf ("%d / %d = %d\n", op1, op2, evaluate ('/', op1, op2));
    printf ("%d %% %d = %d\n", op1, op2, evaluate ('%', op1, op2));
    return 0;
}
  • The above program produces the following output:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
10 % 5 = 0
  • One may notice that the indexing scheme for the fptr_array is not so obvious and we need a select( ) function to identify and map the right index for the right operator. This can be eliminated using the operator characters as indices. For example, fptr_array[‘*’] should hold the function pointer to multiply( ).
  • This can be achieved in the following example which produces the same output as that of the previous example
#include <stdio.h>
int add(int num1, int num2) {
    return num1 + num2;
}
int subtract(int num1, int num2) {
    return num1 - num2;
}
int multiply(int num1, int num2) {
    return num1 * num2;
}
int divide(int num1, int num2) {
    return num1 / num2;
}
int mod(int num1, int num2) {
    return num1 % num2;
}
typedef int (*fptrOperate)(int, int);
fptrOperate fptr_array[47] = {NULL};
void initFptrArray() {
    fptr_array['+'] = add;
    fptr_array['-'] = subtract;
    fptr_array['*'] = multiply;
    fptr_array['/'] = divide;
    fptr_array['%'] = mod;
}
int evaluate(char opcode, int num1, int num2) {
    fptrOperate operation = fptr_array[(int)opcode];
    return operation(num1, num2);
}

int main() {
    initFptrArray();
    int op1 = 10, op2 = 5;
    printf("%d + %d = %d\n", op1, op2, evaluate('+', op1, op2));
    printf("%d - %d = %d\n", op1, op2, evaluate('-', op1, op2));
    printf("%d * %d = %d\n", op1, op2, evaluate('*', op1, op2));
    printf("%d / %d = %d\n", op1, op2, evaluate('/', op1, op2));
    printf("%d %% %d = %d\n", op1, op2, evaluate('%', op1, op2));
    return 0;
}
  • This begs the question, why is the size of fptr_array 47! As we are trying to subscript fptr_array using characters: ‘+’, ‘-’, ‘*’, ‘/’ and ‘%’, the highest ASCII value attained by this set of characters is 47 which is of ‘/’. Hence the size of the array is 47.
  • This begs one more question, is it not inefficient to declare an array of size 47 and just use 5 locations out of them? The C compiler doesn’t allocate memory right away just because it is declared. It allocates once the process attempts to use it. Hence the memory efficiency isn’t affected.

Comparing Function Pointers

Function pointers can be compared to one another using the equality and inequality operators. The following example illustrates the same:

#include <stdio.h>
int multiply(int num1, int num2) {
     return num1 * num2;
}
int divide(int num1, int num2) {
     return num1 / num2;
}
typedef int (*fptrOperate)(int, int);

int main() {
    fptrOperate fptr1 = multiply;
    if (fptr1 == multiply)
        printf("Pointing to multiply( )");
    if (fptr1 != multiply)
        printf("Not pointing to multiply( )");
    return 0;
}

The output of the above program is as follows:

Pointing to multiply( )

Casting Function Pointers

A pointer to one function can be cast to another type. This should be done with care since the runtime system does not verify that the parameters used by a function pointer are correct. It is also possible to cast a function pointer to a different function pointer and then back. The resulting pointer will be equal to the original pointer. The size of function pointers used is not necessarily the same. The following sequence illustrates this operation:

#include <stdio.h>
int multiply(int num1, int num2) {
        return num1 * num2;
}
int main() {
        typedef int (*fptr_1)(int);
        typedef int (*fptr_2)(int,int);
        fptr_2 fptrFirst = multiply;
        fptr_1 fptrSecond = (fptr_1)fptrFirst;
        fptrFirst = (fptr_2)fptrSecond;
        printf("%d", fptrFirst(5,6));
}

This sequence, when executed, will display 30 as its output.

The use of void* is not guaranteed to work with function pointers. That is, we should not assign a function pointer to void* as shown below:

void* pv = add;

However, when interchanging function pointers, it is common to see a “base” function pointer type as declared below. This declares fptrBase as a function pointer to a function, which is passed void and returns void:

typedef void (*fptrBase)();

The following sequence demonstrates the use of this base pointer, which duplicates the previous example:

fptrBase basePointer;
fptrFirst = multiply;
basePointer = (fptr_1)fptrFirst;
fptrFirst = (fptr_2)basePointer;
printf("%d",fptrFirst(5,6));

A base pointer is used as a placeholder to exchange function pointer values.

A word of warning: Function pointer casting is one of the most error-prone and vulnerable areas. It is always advised to use function pointers whose declaration matches the function signature it is pointing to

Leave a Reply

Your email address will not be published. Required fields are marked *

You cannot copy content of this page