Module 3: Pointers, strings, arrays, malloc


Pointers

 

What is a pointer?

 
Let's start with an example:
int main ()
{
  int i = 5;
  int *intPtr;

  // Extract the address of variable i into the pointer:
  intPtr = & i;

  // Print this address:
  printf ("Variable i is located at address %p\n", intPtr);

  // Use the address:
  *intPtr = *intPtr + 1;

  // See what happens:
  printf ("i = %d\n", i);          // What is in i now?
}
Note:
 
Exercise 3.1: Type up, compile and execute the above program. Add a second pointer variable that points to i and modifies i using only the second pointer variable. Can two pointers point to the same thing?
 
A more complex example: (source file)
// Declare some global integers:
int i = 5;
int j = 6;
int sum;

int main ()
{
  // Int pointer declarations:
  int *intPtr;
  int *intPtr2;
  int *intPtr3;

  // First, let's print the address of variable i:
  printf ("Variable i is located at address %p\n", &i);

  // Now extract the address of variable i into the pointer:
  intPtr = & i;

  // Print.
  printf ("The int at memory location %p is %d\n", intPtr, *intPtr);

  // Let's do some arithmetic.
  intPtr2 = & j;
  sum = (*intPtr) + (*intPtr2);
  printf ("The sum of %d and %d is %d\n", *intPtr, *intPtr2, sum);

  // Now for something stranger:
  printf ("The integer at location %p is %d\n", (intPtr+1), *(intPtr+1)); 
  printf ("The integer j=%d is at location %p\n", j, &j);

  // Let's see what the default initial value of intPtr3 is:
  if (intPtr3 == NULL) {
    printf ("intPtr3 has been initialized to NULL\n");
  }
  else {
    printf ("intPtr3 has been initialized to %p\n", intPtr3);
  }

}
Note:
 

Consider an example with char pointers: (source file)

int main ()
{
  int i = 5;
  char *charPtr;  // Pointer declaration
  int j;

  // Make the char pointer point to the start of the integer:
  charPtr = (char*) (& i);

  // Extract the byte, store it in the integer j and print j.
  j = (int) *charPtr;
  printf ("First byte: %d\n", j);

  // Get the next byte and print:
  j = (int) *(charPtr+1);
  printf ("Second byte: %d\n", j);

  // Get the third byte and print:
  j = (int) *(charPtr+2);
  printf ("Third byte: %d\n", j);

  // Get the fourth byte and print:
  j = (int) *(charPtr+3);
  printf ("Fourth byte: %d\n", j);
}
 
Exercise 3.2: In addition to the value in j also print the address in charPtr in the first printf statement.
 
Note:
 

Examining pointers in a debugger:

 
Exercise 3.3: Write a program in pointertopointer.c to use a pointer to a pointer to an int. Fill in the needed assignments below to make the program print "5".
int main ()
{
  int i = 0;
  int *p = NULL;
  int **p2 = NULL;

  // Fill in the assignments here to make the program work:


  // Should print "5";
  printf ("i = %d\n", **p2);
}


Strings

 

Strings in C:

Example: (source file)
int main ()
{
  char *hStr = "hello";     // String initialization.
  char *wStr = "world";
  char ch;

  // Note the use of "%s"
  printf ("%s %s\n", hStr, wStr);

  printf ("3rd char in wStr is %c\n", *(wStr+2));     // Prints 'r'

  // Let's get the 6-th char: should be '\0'
  ch = *(wStr+5);
  if (ch == '\0') {
    printf ("char is string terminator\n");
  }
  else {
    printf ("char is not string terminator\n");
  }
}
Note:


Arrays

 

There are two ways of creating an array in C:

 
Let's look at an example: (source file)
int main ()
{
  int A[10];          // Statically-sized unidimensional array.
  double B[20][20];   // 2D array.

  int *A2 = NULL;     // Declaration of 1D array variable.
  double **B2 = NULL; // Declaration of 2D array variable.

  int i, j;           // For-loop variables.

  int sum;            // For use in examples.
  double dSum;      


  // Static example. The space is already allocated, so the
  // array can be used immediately.
  for (i=0; i < 10; i++) {
    A[i] = i * 100;                // Fill the array with some numbers.
  }

  sum = 0;
  for (i=0; i < 10; i++) {
    sum += A[i];                   // Compute the sum of array's numbers.
  }
  printf ("sum = %d\n", sum);      // Print sum.


  // Dynamic version of example using "malloc" to allocate space.
  // Note the use of the "sizeof" keyword.
  A2 = (int*) malloc (sizeof(int) * 10);
  for (i=0; i < 10; i++) {
    A2[i] = i * 100;               // Fill the array with some numbers.
  }

  sum = 0;
  for (i=0; i < 10; i++) {
    sum += A2[i];                  // Compute the sum of array's numbers.
  }
  printf ("sum = %d\n", sum);      // Print sum.



  // 2D example with static array.
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      B[i][j] = i*j;               // Fill the array with some numbers.
    }
  }

  dSum = 0;
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      dSum += B[i][j];             // Compute the sum of array's numbers.
    }
  }
  printf ("dSum = %lf\n", dSum);   // Print sum.

  
  // Dynamic version of 2D example. Note the two-step allocation.

  // Allocate space for 20 pointers-to-double
  B2 = (double **) malloc (sizeof(double*) * 20);
  for (i=0; i < 20; i++) {
    // For each such pointer, allocate a size-20 double array.
    B2[i] = (double*) malloc (sizeof(double) * 20);
  }

  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      B2[i][j] = i*j;              // Fill the array with some numbers.
    }
  }

  dSum = 0;
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      dSum += B2[i][j];            // Compute the sum of array's numbers.
    }
  }
  printf ("dSum = %lf\n", dSum);   // Print sum.

  // Must free allocated memory when not needed anymore:
  free (A2);
  free (B2);

}
Note:


Memory and malloc

Let's start with a simple example: (source file)

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

int main ()
{
  // Declare a pointer to an int.
  int *p;

  // Get the space from malloc:
  p = (int*) malloc (sizeof(int));

  // Assign a value and print:
  *p = 5;
  printf ("Int value: %d\n", *p);

  // Free the memory when done:
  free (p);
}
Note:
  • The pointer declaration
      // Declare a pointer to an int.
      int *p;
        
    simply declares the pointer variable. The pointer may itself is invalid since it hasn't been assigned a value yet.

  • In C99, pointers are initialized to zero, e.g.,
    int main ()
    {
      // Declare a pointer to an int.
      int *p;
    
      printf ("Initial value of pointer: %p\n", p);    // Prints 0 (C99)
    }
        
    Thus, the "picture" after declaration is:

  • To make the pointer point to something, there are two ways:
    1. Assign it an address of a known variable, e.g.
      int main ()
      {
        int *p;
        int i;
      
        // Extract the address of i and put that in p:
        p = &i;
      }
            
    2. Ask malloc to create some space and return the start address of that space:
      int main ()
      {
        int *p;
      
        // Get the space from malloc:
        p = (int*) malloc (sizeof(int));
      }
            

  • Suppose the space assigned is at location 1023. The picture of memory after the allocation is:

  • After the space is assigned, it can be used:
      *p = 5;
      printf ("Int value: %d\n", *p);
        
    After this assignment, the picture is:

  • The argument to malloc is the number of bytes desired.
    • The program could also have been written as:
      int main ()
      {
        int *p;
      
        p = (int*) malloc (4);
      
        *p = 5;
        printf ("Int value: %d\n", *p);
      }
              
      This works if we happen to know that an int takes up 4 bytes.
    • Notice that a larger number will work:
      int main ()
      {
        int *p;
      
        p = (int*) malloc (16);
      
        *p = 5;
        printf ("Int value: %d\n", *p);
      }
            
      Here, 16 contiguous bytes are allocated, of which only 4 are used.
    • A smaller number will appear to work, but may in fact tamper with existing memory allocations.
    • If you wish to know the number of bytes required for a type, simply print the sizeof value:
        printf ("The number of bytes needed by an integer: %d\n", sizeof(int));
        printf ("The number of bytes needed by a double: %d\n", sizeof(double));
             

  • Notice that the return value is cast into the pointer type:
      p = (int*) malloc (sizeof(int));
        
    An easy way to remember this is: "the cast type is the same as the argument to sizeof but with a * appended".
    • Thus, if the sizeof argument is an int, the return value should be cast into int* (an int pointer).
    • Similarly, if the sizeof argument is an double, the return value should be cast into double*.
    • It looks more complicated with 2D arrays, as we will see, but the essence is the same.

  • After the pointer has a value, we can actually print the address:
    #include <stdio.h>
    #include <stdlib.h>
    
    int main ()
    {
      // Declare a pointer to an int.
      int *p;
    
      // Print the initial value of the pointer:
      printf ("Initial value of pointer: %p\n", p);    // Prints 0 (C99)
    
      // Get the space from malloc:
      p = (int*) malloc (sizeof(int));
    
      // Print the address of the memory block returned by malloc:
      printf ("Current pointer: %p\n", p);
    }
        

  • Whenever we're done with memory allocated by malloc, we need to return it:
      // Free the memory when done:
      free (p);
        

Another example: (source file)

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

int main ()
{
  double *doublePtr;
  char *charPtr;

  printf ("The number of bytes needed by a double: %d\n", sizeof(double));
  printf ("The number of bytes needed by a char: %d\n", sizeof(char));

  // Get the space from malloc:
  doublePtr = (double*) malloc (sizeof(double));
  charPtr = (char*) malloc (sizeof(char));

  // Print the address of the memory block returned by malloc:
  printf ("double pointer is at location: %p\n", doublePtr);
  printf ("char pointer is at location: %p\n", charPtr);

  // Assign values and print:
  *doublePtr = 3.141;
  *charPtr = 'A';

  printf ("double value = %lf   char value = %c\n", *doublePtr, *charPtr);

  // Free the memory when done:
  free (doublePtr);
  free (charPtr);
}
Note:
  • Observe the pattern in calling malloc:
      doublePtr = (double*) malloc (sizeof(double));
      charPtr = (char*) malloc (sizeof(char));
        


Arrays

An example using arrays: (source file)

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

int main ()
{
  int A[10];
  int *B;

  printf ("BEFORE malloc: Address A=%p  Address B=%p\n", A, B);
  // Prints an address for A, but zero for B

  // Assign space to B:
  B = (int*) malloc (sizeof(int) * 10);
  
  printf ("AFTER malloc: Address A=%p  Address B=%p\n", A, B);
  // Prints an address for A and newly allocated address for B.

  // Two ways of assigning values:
  A[3] = 5;
  *(A + 4) = 6;
  printf ("A[3]=%d  A[4]=%d\n", A[3], A[4]);    // Prints 5 and 6.

  // Two ways of assigning values:
  B[3] = 7;
  *(B + 4) = 8;
  printf ("B[3]=%d  B[4]=%d\n", B[3], B[4]);    // Prints 7 and 8.

  // Done.
  free (B);
}
Note:
  • The memory picture after malloc is called is:

  • Array variables are really pointers and can be used as pointers:
      A[3] = 5;
      *(A + 4) = 6;    // Fifth position in array A.
        

  • You should use regular array indexing (A[3]=5) where possible, instead of the pointer manipulation (*(A+4) = 5).

  • The pointer version explains how arrays work:
    • A is the address of the start of the array.
    • A+4 is the address 4 positions into the array, i.e., the address of A[4].
    • The expression *(A+4) follows the address. Thus, the assignment
        *(A + 4) = 6; 
             
      assigns the value 6 into this location.

Next, let's consider 2D arrays: (source file)

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

int main ()
{
  double A[10][10];     // Static allocation of space to array A.
  double **B;           // We'll use malloc to assign space to B.
  int i;

  printf ("BEFORE malloc: Address A=%p  Address B=%p\n", A, B);
  // Prints an address for A, but zero for B

  // Assign space for first dimension of B (an array of pointers-to-double)
  B = (double**) malloc (10 * sizeof(double*));
  for (i=0; i<10; i++) {
    B[i] = (double*) malloc (10 * sizeof(double));
  }
  
  printf ("AFTER malloc: Address A=%p  Address B=%p\n", A, B);
  // Prints an address for A and newly allocated address for B.

  // Two ways of assigning values:
  A[3][4] = 5.55;
  *((double*)A + 5*10 + 6) = 6.66;
  printf ("A[3][4]=%lf  A[5][6]=%lf\n", A[3][4], A[5][6]);    // Prints 5.55 and 6.66.

  // Twy ways of assigning values:
  B[3][4] = 7.77;
  *((*(B + 5)) + 6) = 8.88;
  printf ("B[3][4]=%lf  B[5][6]=%lf\n", B[3][4], B[5][6]);    // Prints 7.77 and 8.88

  // Free the individual small arrays:
  for (i=0; i<10; i++) {
    free (B[i]);
  }
  // Free the array of double-pointers:
  free (B);
}
Note:
  • The array A is given a chunk of space for 100 double's.

  • The picture of memory after the first call to malloc:

  • After the subsequent calls to malloc:

  • Both arrays are accessed using standard array indexing:
      A[3][4] = 5.55;
      B[3][4] = 7.77;
        

  • However, it is also possible (not recommended) to use the actual addresses and perform address arithmetic:
      *((double*)A + 5*10 + 6) = 6.66;
    
      *((*(B + 5)) + 6) = 8.88;
        

  • Let's explain the first one:
      *((double*)A + 5*10 + 6) = 6.66;
        
    • A double occupies 8 bytes.
    • Thus, a 10 x 10 array requires 100*8 = 800 bytes.
    • To get to location A[5][6], we compute the address of the first byte in the 8 bytes allocated to A[5][6].
    • Note that A[5] is the 6-th row and so, we need to skip past five rows: 5*10.
    • Then we skip past 6 positions.
    • The total number of positions to skip past: 5*10 + 6.
    • Adding this to the start address gives the start address of position A[5][6].
    • Finally, because A is declared as a 2D array, we need to cast A into a pointer type to perform arithmetic.

  • The second one is a little more complicated:
      *((*(B + 5)) + 6) = 8.88;
        
    • Recall that B is a pointer that points to a block of double-pointers.
    • Thus, B+5 is the sixth such pointer in the block.
    • The expression *(B+5) follows that address, which is the address of the block of 10 elements assigned to the 6-th row.
    • We now add 6 to this address to get the exact address of element B[5][6].
    • Lastly, we follow the address (pointer) with the (outermost) * operation.

  • Note that the individual array blocks need to be free'd before the double-pointer block pointed to by B is free'd.
 
Exercise 3.4: Draw the "memory picture" for arrays B and B2 in the first arrays example above (arrays.c) just before the free() function calls are executed. That is, draw a picture with sample memory addresses that shows how these arrays are located in memory. Put this in the "answers" PDF for this module.
 
Exercise 3.5: In pascal.c, create a 2D array to store and print Pascal's triangle (Each element is the sum of the two elements immediately above, slightly to the left and to the right). Ensure that only as much space as needed is used for the array, so that the first row is of size 1, the second of size 2, and so on. Sample output:
     1
    1 1 
   1 2 1 
  1 3 3 1 
 1 4 6 4 1 



© 2003, Rahul Simha (revised 2017)