Categories
Computer Science / Information Technology Language: C

Arrays

Introduction

  • An array in C is defined as the collection of similar types of data items stored at contiguous memory locations.
  • Arrays are the derived data type in C programming language which can store the primitive type of data such as int, char, double, float, etc. It also can store the collection of derived data types, such as pointers, structure, etc.
  • The array is the simplest data structure where each data element can be randomly accessed by using its index number.
  • Arrays make it a lot easier to deal with a large number of similar types of values such as marks of a student or age of students etc.
  • An array is also known as a subscripted variable.

Basic operations on an Array

Declaration

Syntax:

data_type array_identifier[size];

Where,

  • data_type is any primitive, non-primitive or user-defined data type.
  • array_identifier is any valid C identifier.
  • size is a positive integer value denoting the size of the array. example:
int seats[10]; // Declares an array named seats which can hold 10
integers.
float age[50]; // Declares an array named age which can hold 50
floating-point numbers.
char division[5]; // Declares an array named division which can hold
5 characters.

Initialisation

  • Indexing of an array in C starts from 0. That is, to access or modify the first element of an array, one has to use the following notation:
array_identifier[0] = value;
  • Note that the value should be of the same data type as that of the array.
  • Generalizing the above syntax:
array_identifier[index] = value;
  • Where, the index can attain any value from 0 to size – 1. And this notation of accessing values using an index enclosed in square brackets is called subscript notation.
  • Eg:
#include <stdio.h>
int main()
{
    int integers[5];
    integers[0] = 0;
    integers[1] = 1;
    integers[2] = 2;
    integers[3] = 3;
    integers[4] = 4;
    for(int i = 0 ; i < 5 ; i ++)
    printf("%d ", integers[i]);
    return 0;
}

output:

0 1 2 3 4
  • It can be noted from the example that array elements can be accessed through a loop as well. Similarly, one can initialize the array using a loop. It can be illustrated in the following example:
#include <stdio.h>
int main()
{
    int integers[5];
    for(int i =0 ; i < 5 ; i ++)
    integers[i] = i;
    for(int i =0 ; i < 5 ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • In this particular case, we initialize the array elements to their corresponding indices. That is, integers[0] is 0, integers[1] is 1 and so forth. This produces the same output as the previous example.
  • It is always a best practice to use a preprocessor directive to define the size of the array. This helps a lot when we want to change the size of the array. It is illustrated in the following example:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE];
    for(int i =0 ; i < SIZE ; i ++)
    integers[i] = i;
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This produces the same output as previous examples but is much more
    maintainable. If one has to change the size of the array, a modification to the preprocessor directive takes care of the rest. This reduces the errors and makes debugging easier.
  • C also provides a way to declare and initialize an array in a single statement. The following is the syntax to achieve the same:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE]={0, 1, 2, 3, 4};
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This also produces the same output as the previous examples in the section. An advantage of using this method is that the programmer need not mention SIZE while declaring. The compiler automatically calculates the size, allocates memory and initializes the given values. This can be illustrated using the following
    example:
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[]={0, 1, 2, 3, 4};
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • This produces the same output as previous examples. That is:
0, 1, 2, 3, 4
  • Till the array elements are not given any specific values, they are supposed to contain garbage values as the array is assumed to be an auto variable by default (Storage classes).
#include <stdio.h>
#define SIZE 5
int main()
{
    int integers[SIZE];
    for(int i =0 ; i < SIZE ; i ++)
    printf("%d ", integers[i]);
    return 0;
}
  • The above program produces a random garbage output such as this:
0 0 -498802560 21903 161117280
  • Also, note that trying to access an index other than the range 0 to size – 1 of the array would lead to unpredictable results. There is a good chance that the program will crash with a segmentation fault. The same is illustrated in the following example:
#include <stdio.h>
int main()
{
    int array[] = {1, 2, 3, 4, 5};
    printf("%d", array[6]);
    return 0;
}
  • In the above example, anything other than the values from 0 to 4 would result in an unpredictable output. It is the sole responsibility of the programmer to take care that the written program doesn’t go out of the bounds of the array. C language doesn’t assist in bounds checking.

Properties of an Array

  • Each element of an array is of the same data type and carries the same size, i.e., int = 4 bytes.
  • Elements of the array are stored at contiguous memory locations where the first element is stored at the smallest memory location. Consider the following example:
array[] = {1, 2, 3, 4, 5};
  • The memory representation of the above array would be as follows assuming that the array is starting at 65508 and running on a 16-bit compiler:
  • Elements of the array can be randomly accessed since we can calculate the address of each element of the array with the given base address and the size of the data element.

Advantages of an Array

  1. Code optimisation: Less code to access the data.
  2. Ease of traversing: By using the for loop, we can retrieve the elements of an array easily.
  3. Ease of sorting: To sort the elements of the array, we need a few lines of code only.
  4. Random Access: We can access any element randomly using the array.

Passing Array Elements to a Function

  • An array is a collection of contiguous and independent elements each with a dedicated address, it should be possible to pass elements of an array individually to a function either by value or reference.
  • This is done by passing values or references to functions using subscript notation. The following example illustrates this:
#include <stdio.h>
void print_by_value(int value) 
{
    printf("%d ", value);
}

void print_by_reference(int* value) 
{
    printf("%d ", *value);
}
int main() 
{
    int array[] = {1, 2, 3, 4, 5};

    printf ("Print by value:\t\t");
    for (int i = 0 ; i < 5 ; i++)
    print_by_value (array[i]);
    
    printf ("\nPrint by reference:\t");
    for (int i = 0 ; i < 5 ; i++)
    print_by_reference (&array[i]);
    
    return 0;
}
  • The output of the above example is:
Print by value: 1 2 3 4 5
Print by reference: 1 2 3 4 5
  • Points to note from the above example:
    • The print_by_value( ) accepts an integer value and prints the same value. A local copy of the value being passed to the function is made in the function. Any changes made to the variable inside this function will not result in a change in the array in the main( ) function.
    • The print_by_reference( ) accepts the address of an integer value and stores it in a local pointer. Any changes made to the value being pointed by this pointer will result in a value change in the array of the main( ) function.

Pointers and Arrays

Before jumping into this section, please make sure you understand pointer arithmetic. Pointer arithmetic is discussed in detail in the ‘Pointers’ section. Till now we were accessing array elements using subscript notation. The elements can be accessed using pointers too. Consider the following example:

#include <stdio.h>
int main()
{
    int array[] = {1, 2, 3, 4, 5};
    int* ptr = &array[0];
    if (ptr == array)
    printf("%p\n%p", ptr, array);
    return 0;
}

Output:

0x7ffc0bd9e810
0x7ffc0bd9e810

A very important conclusion can be drawn from the above example. The array variable is also a pointer to the first element of the array. To reinforce this fact, consider the following example:

#include <stdio.h>
int main()
{
     int array[] = {1, 2, 3, 4, 5};
     int* ptr = &array[0];
     printf("%d", ptr[2]);
     return 0;
}

The output of the above program is:

3

So now, it is a fact that any pointer to the first element of the array can be a replacement for the array variable used earlier. With this fact as the premise, consider the following example:

#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    for (int i = 0 ; i < 5 ; i++)
    printf("%d ", *(array+i));

    return 0;
}

The output of the above program is:

1 2 3 4 5

The subscript notation is converted to pointer notation internally. i.e.,
array[i] = *(array + i)
So it is clear from the above example that we can access array elements through pointer notation. And it is also clear that a pointer when incremented always points to an immediately next location of its type.

Another way of traversing all the elements of an array using pointer notation is illustrated in the following example:

#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    const int* traverse_ptr = &array[0];
    for (int i = 0 ; i < 5 ; i++, traverse_ptr++)
    printf("%d ", *traverse_ptr);
    return 0;
}

Discretion between a pointer and an array

After going through the above examples, one may jump to a conclusion that there is no difference between an array and a pointer but there is.

  • An array has memory allocated to it and a pointer may or may not.
  • &array returns the address of the first element and not the address of the array variable which stores the address of the first element. Whereas &ptr gives the address of the ptr variable which stores the address of the first element of the array memory location to which it is pointing. If &array is returning any other address than the first element of the array, we call it “Array decay”
  • Another symptom of array decay is that the sizeof(array) returns a value other than the size of the memory allocated to the array.
  • These differences can be noticed in the following example program:
#include <stdio.h>
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    int* ptr = &array[0];
    printf("Address of ptr: %p\n", &ptr);
    printf("Address of array: %p\n", &array);
    printf("sizeof of ptr: %u\n", sizeof(ptr));
    printf("sizeof of ptr: %u\n", sizeof(array));
    return 0;
}

The output of the above program is as follows:

Address of ptr: 0061FF08
Address of array: 0061FF0C
sizeof of ptr: 4
sizeof of ptr: 20

Best practices

  1. Making array elements write protected while assigning a pointer for traversing using ‘const int*’ is essential to prevent accidental data modification while traversing.
  2. Accessing array elements by pointers is always faster than accessing them by subscripts.
  3. Array elements should be accessed using pointers if the elements are to be accessed in a fixed order, say from beginning to end, or from end to beginning, or every alternate element or any such definite logic.
  4. Instead, it would be easier to access the elements using a subscript if there is no fixed logic in accessing the elements.

Passing an Entire Array to a Function

At times there arise situations such as array sorting, where we need to pass entire arrays to functions instead of individual elements.
The following table contains the statements showing 2 ways to pass an array as arguments and 2 ways to receive an array as a parameter in the called function.

Consider that the function being called is named ‘foo’. Note that the C language does not have explicit library functions to determine the size of the array being passed. So one needs to make size available to all functions by declaring it at global space or pass the size as another parameter as shown in the table:

Calling statementCalled function prototype
foo(array, size);
or
foo(&array[0], size);
foo(int *array, int size)
or
foo(int array[], int size)

Notice that an array variable is nothing but a pointer to the first element of the array. In both of the calling ways described in the above table, implicitly, just the address of the first element of the array is being passed. Once an array is passed to the function, the array can be traversed in both subscript notation and pointer notation.

One can choose pairs of any combination from the above table to pass the array. The following example illustrates all 4 possibilities from the above table.

#include <stdio.h>
void print_array_1 (int array[], int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
void print_array_2 (int* array, int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", *(array+i));
}
void print_array_3 (int array[], int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
void print_array_4 (int* array, int size) 
{
    for (int i = 0 ; i < size ; i++)
    printf("%d ", array[i]);
}
int main() 
{
    int array[] = {1, 2, 3, 4, 5};
    print_array_1 (array, 5);
    printf("\n");
    print_array_2 (array, 5);
    printf("\n");
    print_array_3 (&array[0], 5);
    printf("\n");
    print_array_4 (&array[0], 5);
    return 0;
}

Output:

1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5

Notice that in the print_array_2( ) function, the array is being traversed using pointer notation and there is no change in the output.

It is very important to make a note that any changes we make to the array in the called functions will result in changes in the caller function too as we are sending the memory location to the function and not making a copy of it. The arrays we saw in this section are technically called ‘static arrays’ as we would know the size of the array at the compile time. These arrays are stored in the program stack.

Declaration

The syntax to declare a 2 dimensional array is:
data_type array_id[row_size][column_size];
Where,

  1. data_type is any valid primary or secondary data type supported by C.
  2. array_id is any valid identifier for the array.
  3. row_size is the length of the row needed.
  4. col_size is the length of the column
    Example: int array[2][3];
    The above array can be visualized as follows:

Points to note:

  • Indexing of row and column starts from 0 and ends at length-1. In the above example, the row index is in the range [0, 1] and the column index is in the range [0, 2].
  • Indexing of each cell in the two-dimensional array starts with the row index followed by the column index. So to address the 0th row 1st column, the index is [0][1].

Initialization

In order to initialize a value to a memory location in the array, the following is the
Syntax:

array_id [i][j] = value;

Where

  1. array_id is array declared.
  2. i is the row index which is in the range between 0 and row_size – 1
    inclusive.
  3. j is the column index which is in the range between 0 and column_size- 1 inclusive.
  4. value is the value to be initialized at the ith row and jth column of the
    array and should strictly belong to the same data type under which the
    array is declared.

Here is a sample program that stores roll numbers and marks obtained by a student side by side in a matrix.

#include <stdio.h>
int main( ) 
{
    int students[2][2] ;
    students[0][0] = 1;
    students[0][1] = 96;
    students[1][0] = 2;
    students[1][1] = 93;

    printf("Roll number: %d\tMarks: %d\n", students[0][0],
    students[0][1]);
    printf("Roll number: %d\tMarks: %d\n", students[1][0],
    students[1][1]);
    return 0;
}

The output of the above program is:

Roll number: 1 Marks: 96
Roll number: 2 Marks: 93

To understand the memory locations of the array in the above program, let us print the memory locations of the array:

#include <stdio.h>
int main( )
{
     int students[2][2] ;
     students[0][0] = 1;
     students[0][1] = 96;
     students[1][0] = 2;
     students[1][1] = 93;

     printf("%p\t%p\n", &students[0][0], &students[0][1]);
     printf("%p\t%p\n", &students[1][0], &students[1][1]);
     return 0;
}

The output (highlighted last 2 characters for readability):

0x7ffc2873dac0 0x7ffc2873dac4
0x7ffc2873dac8 0x7ffc2873dacc

Note that the size of int is 4 bytes as the program is executed on a 32-bit compiler. By analyzing the above output, it can be concluded that two-dimensional arrays are nothing but single dimensional arrays of length = row size * column size but indexed as rows and columns.

In the previous example, we saw that the arrays are initialized individually which can be a very tedious job and we might need to read the user input and store it in an array. This can be achieved by coupling scanf and loops. The following example illustrates the same:

#include <stdio.h>
int main( )
{
    int students[2][2] ;
    for (int i = 0 ; i < 2 ; i++)
    {
        printf("Enter the roll number: ");
        scanf("%d", &students[i][0]);
        printf("Enter the marks: ");
        scanf("%d", &students[i][1]);
    }
    for (int i = 0 ; i < 2 ; i++)
        printf("Roll number: %d\tMarks: %d\n", students[i][0], students[i][1]);
    return 0;
}

The output of the above program is:

Enter the roll number: 1
Enter the marks: 93
Enter the roll number: 2
Enter the marks: 96
Roll number: 1 Marks: 93
Roll number: 2 Marks: 96

The above example employs a for loop to iterate over rows as there are only 2 columns. When there are numerous columns, one can use nested loops as done in the following example:

#include <stdio.h>
#define ROWS 4
#define COLS 6
int main( )
{
        int array[ROWS][COLS] ;
        printf("Enter %d values: ", ROWS * COLS);
        for (int i = 0 ; i < ROWS ; i++)
        {
            for (int j = 0 ; j < COLS ; j++)
            {
            scanf("%d", &array[i][j]);
            }
            
        }
        printf("\nEntered values:\n");
        for (int i = 0 ; i < ROWS ; i++) {
        for (int j = 0 ; j < COLS ; j++) {
        printf("%d ", array[i][j]);
        }
        printf("\n");
        }
        return 0;
}

The output of the above program is:

Enter 24 values: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

Entered values:
1 2 3 4 5 6
7 8 9 10 11 12
13 14 15 16 17 18
19 20 21 22 23 24

Declaration and initialisation

Declaration and initialization of an array can be done in a single statement. The syntax is as follows:

data_type array_id[][] = {
{value_00, value_01, ..., value_0n},
{value_10, value_11, ..., value_1n},
18
…
{value_m0, value_m1, ..., value_mn},
};

Where,

  1. data_type is the data type of the value that has to be stored in the array.
  2. array_id is the identifier of the array being declared and initialized.
  3. value_ij are values of the array, where 0 <= i < desired row size of the array and 0 <= j < desired column size of the array

Another way to declare and initialize is as follows:

data_type array_id[row_size][col_size] = {
    value_00, value_01, ..., value_0n,
    value_10, value_11, ..., value_1n,
    …
    value_m0, value_m1, ..., value_mn,
};

One may notice that just the nested brackets are removed. This method mandates the programmer to mention at least column size if not both row size and column size. This method often leads the compiler to give a warning saying that the brackets are missing. Hence the first method is always prefered.

The following example illustrates declaration and initialisation:

#include <stdio.h>
#define ROWS 3
#define COLS 2
int main( ) {
    int array[ROWS][COLS] = {{1, 2}, {3, 4}, {5, 6}};
    for (int i = 0 ; i < ROWS ; i++) {
        for (int j = 0 ; j < COLS ; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
    return 0;
}

Output:

1 2
3 4
5 6

Please note that the rows and column size mentioned in the declaration are not mandatory as the compiler will determine the size of the rows and columns automatically by the initialisation part.

Jagged arrays

Jagged arrays are two-dimensional arrays where the column length is not constant throughout the rows. The following is a pictorial representation of a jagged array:

         0       1         2        3
    +--------+--------+
0   | [0][0] | [0][1] |
    +--------+--------+--------+
1   | [1][0] | [1][1] | [1][2] |
    +--------+--------+--------+
2   | [2][0] |
    +--------+--------+--------+--------+
3   | [3][0] | [3][1] | [3][2] | [3][3] |
    +--------+--------+--------+--------+

One might intuitively try to create a jagged array of the above sort by the method just discussed in the following way:

int array[][] = {{1, 2}, {3}, {5, 6}};

Please note that this will produce unpredictable results as this is NOT the proper way of creating jagged arrays in C. Jagged arrays can be created using dynamic arrays which we shall see in the latter part of this section.

Pointers and two-dimensional arrays

As mentioned earlier, two-dimensional arrays are nothing but a contiguous collection of single-dimensional arrays. The C language embodies this unusual but powerful capability that it can treat parts of arrays like arrays. More specifically, each row of a two-dimensional array can be thought of as a one-dimensional array. This is a very important fact if we wish to access array elements of a two-dimensional array using pointers.
Thus, the declaration,

int array_id[5][2] ;

can be thought of as setting up an array of 5 elements, each of which is a 1D (one-dimensional) array containing 2 integers. We refer to an element of a 1D array using a single subscript.

Similarly, if we can imagine array_id to be a one-dimensional array then we can refer to its zeroth element as array_id[0], the next element as array_id[1] and so on. More specifically, array_id[0] gives the address of the zeroth one-dimensional array, array_id[1] gives the address of the first one-dimensional array and so on. This fact can be demonstrated by the following program.

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
    int array_id[ROWS][COLS] = {
                                { 1234, 56 },
                                { 5678, 33 },
                                { 9012, 80 },
                                { 3456, 78 }
                            } ;
    for (int i = 0 ; i < ROWS ; i++ )
        printf ( "\nAddress of %d th 1-D array = %p", i, array_id[i]) ;

    return 0;
}

The output of the above program when run on a 32-bit compiler:

Address of 0 th 1-D array = 0x7fffe85bda40
Address of 1 th 1-D array = 0x7fffe85bda48
Address of 2 th 1-D array = 0x7fffe85bda50
Address of 3 th 1-D array = 0x7fffe85bda58

Points to note from the above example:

  • Compiler figures out the number of rows (4) and columns (2) from the declaration and initialisation line of code.
  • The compiler allocates the necessary amount of memory (8 bytes for 2 integers in the first row) linearly for the first row (from 40 to 44 for the first element, 44 to 48 for the second element in the above example).
  • Once allocated memory for the first row, the compiler allocates memory for the second row contiguously next to the first row’s memory location (from 48 to 4c for the first element, 4c to 50 for the second element in the above example)
  • The above pattern is followed for the 3rd and 4th row as well.

Pointer access to elements in a 2D array

Similar to the one-dimensional array, there are 2 ways to access the elements of a 2D array: subscript notation and pointer notation. We have been using the subscript access method so far and in this section, we shall peek into the pointer access method.
Syntax:

*(*(array_id + row_index) + column_index)

The evolution of the above syntax from subscript notation is as follows:

array_id[row_index][column_index]
* ( array_id[row_index] + column_index )
* ( * ( array_id + row_index ) + column_index)

Notice that the pointer access method is just a multi-level dereferencing of array element addresses.

Consider the following example illustrating array element access using the pointer method:

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
int array[ROWS][COLS] = {
                            { 1234, 56 },
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
} ;

for (int i = 0 ; i < ROWS ; i++ )
{
    for (int j = 0 ; j < COLS ; j++)
    printf ("\nAddress: %p Value: %d\t", (*(array + i) + j),
    *(*(array + i) + j) );
    printf("\n");
}
return 0;
}

Output when run on 32-bit machine:

Address: 0x7ffc2a646d30 Value: 1234
Address: 0x7ffc2a646d34 Value: 56

Address: 0x7ffc2a646d38 Value: 1212
Address: 0x7ffc2a646d3c Value: 33

Address: 0x7ffc2a646d40 Value: 1434
Address: 0x7ffc2a646d44 Value: 80

Address: 0x7ffc2a646d48 Value: 1312
Address: 0x7ffc2a646d4c Value: 78

Pointer to a 2D array

In this section, we shall explore how we can have a pointer to a 2-dimensional array. Consider the following example:

#include <stdio.h>
#define ROWS 4
#define COLS 2
int main( )
{
int array[ROWS][COLS] = {
                            { 1234, 56 },
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
                      } ;

int ( *p )[2] ;
int i, j, *pint ;
for ( i = 0 ; i <= 3 ; i++ )
{
    p = &array[i] ;
    pint = p ;
    for ( j = 0 ; j <= 1 ; j++ )
    printf ( "%d ", *( pint + j ) ) ;
    printf ( "\n" ) ;
}
return 0;
}

The output:

1234 56
1212 33
1434 80
1312 78

Here p is a pointer to a single-dimensional array of two integers. Note that the parentheses in the declaration of ‘p’ are necessary. The absence of them would make ‘p’ an array of 2 integer pointers. In the outer for loop each time we store the address of a new one-dimensional array. Thus the first time through this loop p would contain the address of the zeroth 1-D array. This address is then assigned to an integer pointer ‘pint’. Lastly, in the inner ‘for’ loop using the pointer ‘pint’, we have printed the individual elements of the 1-D array to which p is pointing.

The entity pointer to an array is immensely useful when we need to pass a 2-D array to a function. This is discussed in the next section.

Passing 2-D Array to a Function

There are three ways in which we can pass a 2-D array to a function. These are illustrated in the following program.

#include <stdio.h>
#define ROWS 4
#define COLS 2
void display ( int *array)
{
    int i, j ;
    for ( i = 0 ; i < ROWS ; i++ )
    {
        for ( j = 0 ; j < COLS ; j++ )
            printf ( "%d ", * ( array + i * COLS + j ) ) ;
        printf ( "\n" ) ;
    }
        printf ( "\n" ) ;
}
void show ( int ( *array )[COLS] )
{
    int i, j ;
    int *p ;
    for ( i = 0 ; i < ROWS ; i++ )
        {
        p = array + i ;
            for ( j = 0 ; j < COLS ; j++ )
            printf ( "%d ", * ( p + j ) ) ;
            printf ( "\n" ) ;
        }
        printf ( "\n" ) ;
}
void print ( int array[ ][COLS] ) 
{
    for ( int i = 0 ; i < ROWS ; i++ )
    {
        for ( int j = 0 ; j < COLS ; j++ )
        printf ( "%d ", array[i][j] ) ;
        printf ( "\n" ) ;
    }
        printf ( "\n" ) ;
}
int main( )
{
    int array[ROWS][COLS] = {
                            { 1234, 56 }
                            { 1212, 33 },
                            { 1434, 80 },
                            { 1312, 78 }
    } ;
    display(array);
    show(array);
    print(array);
    return 0;
}

The output of the above program is as follows:

1234 56
1212 33
1434 80
1312 78

1234 56
1212 33
1434 80
1312 78

1234 56
1212 33
1434 80
1312 78

display( )

In the display( ) function we have collected the base address of the 2-D array being passed to it in an ordinary int pointer. Then through the two for loops using the expression

* ( array + i * col + j )

we have reached the appropriate element in the array. Suppose i is equal to 2 and j is equal to 3, then we wish to reach the element array[2][3]. A more general formula for accessing each array element would be:

* ( base address + row no. * no. of columns + column no. )

show( )

In the show( ) function we have defined an array to be a pointer to an array of 4 integers through the declaration:

int ( *array )[COLS] ; // Where COLS is 4

To begin with, the array holds the base address of the zeroth 1-D array. This address is then assigned to ‘p’ which is an int pointer, and then using this pointer all elements of the zeroth 1-D array are accessed. Next time through the loop when ‘i’ takes a value of 1, the expression array + i fetches the address of the first 1-D array. This is because the array is a pointer to the zeroth 1-D array and adding 1 to it would give us
the address of the next 1-D array. This address is once again assigned to ‘p’ and using it all elements of the next 1-D array are accessed.

print( )

In the third function print( ), the declaration of the array looks like this:

int array[ ][COLS] ; // Where COLS is 4

This is the same as

int ( *array )[COLS],

where the array is a pointer to an array of 4 integers. The only advantage is that we can now use the more familiar expression array[i][j] to access array elements. We could have used the same expression in the show( ) function as well.

Consider the following example:

#include <stdio.h>
int main( ) 
{
        int *array[4] ; // array of integer pointers
        int p = 101, q = 102, r = 103, s = 104;
        array[0] = &p ;
        array[1] = &q ;
        array[2] = &r ;
        array[3] = &s ;
        
        printf ( "Address of p : %p\n", &p ) ;
        printf ( "Address of q : %p\n", &q ) ;
        printf ( "Address of r : %p\n", &r ) ;
        printf ( "Address of s : %p\n\n", &s ) ;

    for ( int i = 0 ; i <= 3 ; i++ ) 
    {
        printf ( "array[%d] : %p\n", i, array[i] ) ;
        printf ( "Value : %d\n\n", *array[i] );
    }
return 0;
}

Output:

Address of p : 0x7fff0c6dd45c
Address of q : 0x7fff0c6dd460
Address of r : 0x7fff0c6dd464
Address of s : 0x7fff0c6dd468

array[0] : 0x7fff0c6dd45c
Value : 101

array[1] : 0x7fff0c6dd460
Value : 102

array[2] : 0x7fff0c6dd464
Value : 103

array[3] : 0x7fff0c6dd468
Value : 104

As one can observe, arr contains addresses of isolated int variables i, j, k and l. The for loop in the program picks up the addresses present in the array and prints the values present at these addresses.

An array of pointers can even contain the addresses of other arrays. Consider the following example:

#include <stdio.h>
int main( )
{
    int a[ ] = { 0, 1, 2, 3, 4 } ;
    int *p[ ] = { a, a + 1, a + 2, a + 3, a + 4 } ;
    printf ( "%p %p %d", p, *p, * ( *p ) ) ;
    return 0;
}

Output:

0x7ffc5cf1dd10 0x7ffc5cf1dcf0 0

Remember that a subarray of another array is also an array.

Memory allocation

C gives two choices when a programmer wants to reserve memory for arrays. But before diving in, it is advised to go through “Memory layouts in C” under the “Advanced topics in Pointers” of the “Pointers” section.

  1. Static memory allocation
    1. Static memory allocation requires that the declaration and definition of memory be fully specified in the source program. The number of bytes reserved cannot be changed during runtime. This is the technique we have been using to allocate memory for variables, arrays, and pointers.
  2. Dynamic Memory allocation
    1. Dynamic memory allocation uses predefined functions to allocate and release memory for data while the program is running. It effectively postpones the data definition, but not the data declaration, to run time. It is important to note that we can refer to memory allocated in the heap only through a pointer.
      To use dynamic memory allocation, we use either standard data types or derived types that we have previously declared. Unlike static memory allocation, dynamic memory allocation has no identifier associated with it; it has only an address that must be used to access it.

Memory allocation functions.

There are 4 memory management functions:

  1. malloc ( )
  2. calloc ( )
  3. realloc ( )
  4. free ( )

The first three are used for memory allocation and the fourth is used to return memory when it is no longer needed. All 4 functions are found in the standard library file (stdlib.h).

Block memory allocation (malloc)

The malloc function allocates a block of memory that contains the number of bytes specified in its parameter. It returns a void pointer to the first byte of the allocated memory. The allocated memory is not initialized. The malloc ( ) function declaration is
shown below:

void* malloc (size_t size);

The type size_t is defined in several header files including stdio.h. This type is usually an unsigned integer, and by the standard, it is guaranteed to be large enough to hold the maximum address of the computer.
To provide portability, the size specification in malloc’s actual parameter is generally computed using sizeof operator. For example, if we want to allocate an integer in the heap we code the call as shown below:

pInt = malloc (sizeof(int));

If the memory allocation fails, instead of returning a pointer to the first byte of allocated memory, malloc ( ) returns a NULL pointer. An attempt to allocate memory from the heap when memory is insufficient is known as overflow. It is always the best practice for the program to check for memory overflow.

There is one subtlety to note here. As mentioned earlier, the malloc returns a void pointer but often we need a pointer of a specific type. C99 and above versions handle this situation with an implicit type casting but it is always recommended to explicitly typecast the pointer. It is to be done as follows:

pointer = (type *) malloc (size);

For example:

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

The malloc ( ) function has one more potential error. If we call malloc ( ) with a zero size, the results are unpredictable. It may return a NULL pointer, or it may return some other implementation-dependent value. Never call malloc with a zero size.

The following example shows the best way to use malloc ( ) to allocate a block of memory for an integer:

if (!(pInt = (int *) malloc (sizeof(int)))) {
    // No memory is available.
    exit(100);
}
// Memory available

Contiguous memory allocation (calloc)

The second memory allocation function, calloc, is primarily used to allocate memory for arrays. It differs from malloc only in that it sets memory to NULL characters. The calloc function declaration is shown below:

void *calloc (size_t element-count, size_t element_size);

The result is the same for both malloc and calloc when overflow occurs and when a zero size is given. A sample calloc call is illustrated in the following example. Here we attempt to allocate memory for an array of 200 integers:

if (!(pInt = (int *) calloc (200, sizeof(int)))) {
   // No memory is available.
   exit(100);
}
// Memory available

Reallocation of memory (realloc)

The realloc ( ) function can be highly inefficient and therefore should be used advisedly. When given a pointer to a previously allocated block of memory, realloc ( ) changes the size of the block by deleting or extending the memory at the end of the block. If the memory cannot be extended because of other allocations, realloc allocates a completely new block, copies the existing memory allocation to the new allocation, and deletes the old allocation.
The programmer must ensure that any other pointers to the data are correctly changed.

The realloc function declaration is shown below:

void *realloc (void* ptr, size_t newSize);

Releasing memory (free)

Garbage collection, that is, releasing the memory allocated by the above 3 functions once the need is satisfied should be done explicitly by the programmer. This can be done using the free( ) function.
It is an error to free memory:

  1. With a null pointer.
  2. A pointer to a location other than the first element of an allocated block of memory.
  3. A pointer that is different from the pointer that allocated the memory.
  4. Free the same memory location more than once.
    It is also a potential error to refer to memory after it is freed (dangling pointer). The function declaration statement for free is shown below:
void free(void* ptr);

Exercises

  1. Eleven integer values are entered from the keyboard. The first 10 are to be stored in an array. Search for the 11th integer in the array. Write a program to display the number of times it appears in the array.
  2. Read the size of the array and allocate an array dynamically using calloc. Read the array elements and also read a key value. Display the number of times the key has appeared in the array. Do not forget to free the allocated memory before exiting the program.
  3. Implement the Selection Sort, Bubble Sort and Insertion sort algorithms on a set of 10 numbers.
  4. Read the size of the array and allocate an array dynamically using calloc. Read the array elements and sort the array using selection sort, bubble sort and insertion sort. Do not forget to free the allocated memory before exiting the program.
  5. Implement the Sieve of Eratosthenes and print the first 100 prime numbers. The algorithm is as follows:
    1. Fill array values[100] with numbers from 1 to 100.
    2. Starting with the second entry in the array, set all its multiples to zero.
    3. Proceed to the next non-zero element and set all its multiples to zero.
    4. Repeat step 3 till you have set up the multiples of all the non-zero elements to zero.
    5. After step 4, all the non-zero entries left in the array would be prime numbers, so print out these numbers.F. Write a program to copy the contents of one array into another in reverse order.
  6. Write a program to copy the contents of one array into another in reverse order.
  7. Write a program to check if an array is symmetric. That is, in an array array_id check if array_id[0] = array_id[n-1], array_id[1] = array_id[n-2] and so on, where n is the size of the array. Statically initialize the array or take user input.
  8. Find the smallest number, the largest number, and an average of values in a floating numbers array using pointers.
  9. Write a program that performs the following tasks:
    1. initialize an integer array of 10 elements in main( )
    2. pass the entire array to a function modify( )
    3. in modify( ) multiply each element of the array by 4
    4. return the control to main( ) and print the new array of elements in main( )
  10. Write a function to find the norm of a matrix. The norm is defined as the square root of the sum of squares of all elements in the matrix.
  11. Write a program to read 10 coordinates. Each coordinate contains 2
  12. floating-point values X and Y. Print the two farthest points and 2 nearest points in the array.

Leave a Reply

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

You cannot copy content of this page