C Arrays

Arrays

Arrays are created as follows.

int iNumArray[3];
int iNum[5] = {1,5,2,4,3};
int Numbers[] = {3,6,2,8,7};

You can specify the size of the array explicitly or leave it blank and let the compiler define so it is big enough to store the initial data. The initial declaration must determine the size one way or another though.

int anArray[];

is not valid.

Values in the array can be read or altered using an index value;

int Numbers[] = {3,6,2,8,7};
printf("%d\n", Numbers[2]);
Numbers[3] = 1;

Arrays and Addresses

#include <stdio.h>

int main(void) {

int a[] = {1,1,1,1,1};
int b[] = {2,2,2,2,2};
int c[] = {3,3,3,3,3};

for (int i=0;i<5;i++)
{
  printf("array a[%d] %d at address %p\n",i,a[i], &a[i]);
}
printf("\n");

for (int i=0;i<5;i++)
{
  printf("array b[%d] %d at address %p\n",i,b[i], &b[i]);
}
printf("\n");

for (int i=0;i<5;i++)
{
  printf("array c[%d] %d at address %p\n",i,c[i], &c[i]);
}
printf("\n");

printf("\n");
printf("array a[%d] %d at address %p\n",0,a[0], &a[0]);
printf("array b[%d] %d at address %p\n",0,b[0], &b[0]);
printf("array c[%d] %d at address %p\n",0,c[0], &c[0]);

return 0;
}

Running the code above shows us a few of different things.

Items in an array are stored sequentially. Integers take up four bytes in memory, the addresses of items in an integer array are four bytes apart. The array is stored in increasing memory addresses.

When arrays are created one after another they are spaced so that the beginning is properly aligned. Each subsequent is stored in lower memory addresses.

array a[0] 1 at address 0x7ffcb9621980
array a[1] 1 at address 0x7ffcb9621984
array a[2] 1 at address 0x7ffcb9621988
array a[3] 1 at address 0x7ffcb962198c
array a[4] 1 at address 0x7ffcb9621990

array b[0] 2 at address 0x7ffcb9621960
array b[1] 2 at address 0x7ffcb9621964
array b[2] 2 at address 0x7ffcb9621968
array b[3] 2 at address 0x7ffcb962196c
array b[4] 2 at address 0x7ffcb9621970

array c[0] 3 at address 0x7ffcb9621940
array c[1] 3 at address 0x7ffcb9621944
array c[2] 3 at address 0x7ffcb9621948
array c[3] 3 at address 0x7ffcb962194c
array c[4] 3 at address 0x7ffcb9621950


array c[0] 1 at address 0x7ffcb9621980
array c[0] 2 at address 0x7ffcb9621960
array c[0] 3 at address 0x7ffcb9621940

C Array Bounds

The bounds of an array are the valid indexes that can be used to access the array.

int Numbers[] = {3,6,2,8,7};

Numbers[] above has a length of five, with valid indexes from 0 to 4.

C however, will let you access invalid indexes that are outwith the bounds of the array.

int Numbers[] = {3,6,2,8,7};

Numbers[10] = 1;
Numbers[-2] = 1;

The compiler will warn you that the index is out of bounds but it will not stop you doing this. Doing this may cause your code to crash or may work. If it works its results may be unpredictable. For example the following code (available at https://replit.com/@andyguest/cArrayBoundaryOverrun)

#include <stdio.h>

int main(void) {

int a[] = {1,1,1,1};
int b[] = {2,2,2,2};

for (int i=0;i<4;i++)
{
  printf("array a[%d] %d at address %p\n",i,a[i], &a[i]);
}
printf("\n");

for (int i=0;i<4;i++)
{
  printf("array b[%d] %d at address %p\n",i,b[i], &b[i]);
}
printf("\n");

for (int i=0; i<8; i++)
{
  b[i]=3;
  printf("setting array b[%d] to %d at address %p\n",i,b[i], &b[i]);
}
printf("\n");

for (int i=0;i<4;i++)
{
  printf("array a[%d] %d at address %p\n",i,a[i], &a[i]);
}
printf("\n");

for (int i=0;i<4;i++)
{
  printf("array b[%d] %d at address %p\n",i,b[i], &b[i]);
}
printf("\n");

return 0;

}

produces the following output

array a[0] 1 at address 0x7fff4d2abdc0
array a[1] 1 at address 0x7fff4d2abdc4
array a[2] 1 at address 0x7fff4d2abdc8
array a[3] 1 at address 0x7fff4d2abdcc

array b[0] 2 at address 0x7fff4d2abdb0
array b[1] 2 at address 0x7fff4d2abdb4
array b[2] 2 at address 0x7fff4d2abdb8
array b[3] 2 at address 0x7fff4d2abdbc

setting array b[0] to 3 at address 0x7fff4d2abdb0
setting array b[1] to 3 at address 0x7fff4d2abdb4
setting array b[2] to 3 at address 0x7fff4d2abdb8
setting array b[3] to 3 at address 0x7fff4d2abdbc
setting array b[4] to 3 at address 0x7fff4d2abdc0
setting array b[5] to 3 at address 0x7fff4d2abdc4
setting array b[6] to 3 at address 0x7fff4d2abdc8
setting array b[7] to 3 at address 0x7fff4d2abdcc

array a[0] 3 at address 0x7fff4d2abdc0
array a[1] 3 at address 0x7fff4d2abdc4
array a[2] 3 at address 0x7fff4d2abdc8
array a[3] 3 at address 0x7fff4d2abdcc

array b[0] 3 at address 0x7fff4d2abdb0
array b[1] 3 at address 0x7fff4d2abdb4
array b[2] 3 at address 0x7fff4d2abdb8
array b[3] 3 at address 0x7fff4d2abdbc

The array b[] is declared immeadiately after the array a[], so b[] is at a lower memory address than a[] and expands up towards a[].

Address Array Value
..b0 b[0] 2
..b4 b[1] 2
..b8 b[2] 2
..bc b[3] 2
..c0 a[0] 1
..c4 a[1] 1
..c8 a[2] 1
..cc a[3] 1
Array Address Value
b[i] &b[i]  
b[0] ..b0 2
b[1] ..b4 2
b[2] ..b8 2
b[3] ..bc 2
b[4] ..c0 1
b[5] ..c4 1
b[6] ..c8 1
b[7] ..cc 1

So we have to be very careful with array bounds. Writing too much data in to an array can overwrite previously declared variables. More than that, remember the way the frame is created on the stack.

C Memory- Frame structure

Before the variables for a function are created in a frame, variables that store the passed in parameters are created and before that the return address is stored. Write enough data in to an array and you can overwrite the return address causing your code to crash.

There are safeguards in place to try to prevent this from causing too much damage, but a buffer overflow attack can deliberately overwrite the return address so the code doesn’t return to the correct place and instead ends up executing malicious code.

We have to be especially careful with strings since strings are actually character arrays terminated by a \0. If we accidentally remove the \0 the code does not know when the string ends. We also have to be careful reading text input and ensure that we do not store a string that is too long in a character array.

Pointer Arithmetic

When we create a pointer we create a pointer to a specific type of variable.

int i_value = 5;
int *i_pointer = &i_value;


i_pointer++;

So what does the line i_pointer++; do? We know i_value++; would increment the number stored in i_value by one. Does i_pointer++; increase the pointer value by one, moving it to the next byte of memory? No it doesn’t, it increases the address by the size of the variable type it is based on. i_pointer is a pointer to an int type, an int has a size of 4 bytes. Therefore i_pointer++ increases the value of ‘i_pointer’ by 4. If we had a pointer to a character char *c_pointer then c_pointer++ would increase the pointer value by 1 (since the size of a char is 1 byte).

Why is this useful? Well one place it is useful is in arrays.

#include <stdio.h> 

int main() 
{ 
  int i; 
  int a[5] = {1, 2, 3, 4, 5}; 
  int *p = &a[0]; 

  for (i = 0; i < 5; i++) 
  { 
    printf("%d", *p); 
    p++; 
  } 
  return 0; 
} 

If we start by creating a pointer to the first element in an array we can then access the array contents directly using the pointer and incrementing it as needed. Note that this also works with p=p+1.

Passing Arrays To Functions

Passing Array Elements

Passing array elements to a function is similar to passing variables to a function.

#include <stdio.h>
void display(int age1, int age2)
{
    printf("%d\n", age1);
    printf("%d\n", age2);
}

int main()
{
    int ageArray[] = {2, 8, 4, 12};

    // Passing second and third elements to display()
    display(ageArray[1], ageArray[2]); 
    return 0;
}

Passing Full Arrays

// Program to calculate the sum of array elements by passing to a function 

#include <stdio.h>
float calculateSum(float age[]);

int main() {
    float result, age[] = {23.4, 55, 22.6, 3, 40.5, 18};

    // age array is passed to calculateSum()
    result = calculateSum(age); 
    printf("Result = %.2f", result);
    return 0;
}

float calculateSum(float age[]) {

  float sum = 0.0;

  for (int i = 0; i < 6; ++i) {
		sum += age[i];
  }

  return sum;
}

To pass an entire array to a function, only the name of the array is passed as an argument.

result = calculateSum(age);

However, notice the use of [] in the function definition.

float calculateSum(float age[]) {
... ..
}

This informs the compiler that you are passing a one-dimensional array to the function.

Note: In C programming, you can pass arrays to functions, however, you cannot return arrays from functions.

Multidimensional Arrays

A multidimensional array is of form, a[i][j].

int a[10][15];

for (int i=0;i<10;i++)
{
  for (int j=0;j<15;j++)
  {
    a[i][j] = (i+15)+j;  /* fill up the array with increasing numbers */
  }
}

We can get a pointer to the address of any element in an array with &a[i][j]. Can we use pointer arithmetic to access it?

Yes we can because of the way arrays are stored in memory. A one dimensional array is stored as consecutive values in memory. A two dimensional array is stored as an array of one dimensional arrays stored as consecutive values in memory. What does this mean? Well starting from the first element in the array, a[0][0] each element in a[0] is stored. a[0][0], a[0][1],a[0][2], .. a[0][14]. Immeadiately after a[0][14] the entry for a[1][0] is stored, followed by the rest of the a[1]’s in order and so one. So far as the memory allocation is concerned a multidimensional array is just a one dimensional array with i*j entries.

This means we can step through the array in a single loop rather than using nested loops. (Nested loops are very useful but they are slower than single loops).

#include <stdio.h>

int main(void) {
  int a[10][15];
  int *ptr = &a[0][0];

  for (int i=0;i<(10*15);i++)
  {
    *(ptr + i)=i;
  }

  for (int i=0;i<10;i++)
  {
    for (int j=0;j<15;j++)
    {
      printf("%d, ",a[i][j]);
    }
    printf("\n");
  }
  return 0;
}

The single loop sets each value in the multi-dimesional array by iterating through rows * cols positions, adding on one more in each loop to a pointer set to the start of the array int *ptr = &a[0][0]. The alternate method, used to print out the results, uses nested loops, looping through each entry in each column for each row. a[i][j] equates to *(ptr + (i * col_count) + j).

Passing Multidimensional Arrays To Functions

To pass multidimensional arrays to a function, only the name of the array is passed to the function(similar to one-dimensional arrays).

 #include <stdio.h>
void displayNumbers(int num[2][2]);
int main()
{
    int num[2][2];
    printf("Enter 4 numbers:\n");
    for (int i = 0; i < 2; ++i)
        for (int j = 0; j < 2; ++j)
            scanf("%d", &num[i][j]);

    // passing multi-dimensional array to a function
    displayNumbers(num);
    return 0;
}

void displayNumbers(int num[2][2])
{
    printf("Displaying:\n");
    for (int i = 0; i < 2; ++i) {
        for (int j = 0; j < 2; ++j) {
           printf("%d\n", num[i][j]);
        }
    }
}

Arrays Of Pointers

We can also make arrays of pointers. There are a number of reasons to do this, here we will consider one useful example, an array of pointers to strings.

Imagine we want an array of strings.

char name[3][20] = {"Adam", "Chris", "Deniel"};

What this would give us in memory, illustrated as a row for each string and a column for each character, is.

  Col                                      
Row 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
0 A d a m                                
1 C h r i s                              
2 D e n i e l                            

For each entry in the array we are storing 20 characters in memory and wasting a lot of space. Instead we could use an array of pointers.

char *name[3] = {"Adam", "Chris", "Deniel"};

Now our array looks like this

Entry Pointer Elsewhere in memory
0 -> “Adam”
1 -> “Chris”
2 -> “Deniel”

We are only using as much memory as required for each name. We are adding an overhead of storing a list of pointers, and in this example we are not making much of a saving since each pointer takes up more space than a single character, but where we need to store longer strings this can be a big saving.

Dynamic Memory Allocation or Resizing Arrays

An array is a collection of a fixed number of values. Once the size of an array is declared, you cannot change it. Sometimes the size of the array you declared may be insufficient. To solve this issue, you can allocate memory manually during run-time. This is known as dynamic memory allocation in C programming.

To allocate memory dynamically, library functions are malloc(), calloc(), realloc() and free() are used. These functions are defined in the <stdlib.h> header file. Note that malloc() and calloc() allocate space on the heap.

malloc()

The name “malloc” stands for memory allocation.

The malloc() function reserves a block of memory of the specified number of bytes. And, it returns a pointer of void which can be casted into pointers of any form.

Syntax of malloc()

ptr = (castType*) malloc(size);

Example

ptr = (float*) malloc(100 * sizeof(float));

The above statement allocates 400 bytes of memory. It’s because the size of float is 4 bytes. And, the pointer ptr holds the address of the first byte in the allocated memory. The expression results in a NULL pointer if the memory cannot be allocated.

calloc()

The name “calloc” stands for contiguous allocation. The malloc() function allocates memory and leaves the memory uninitialized. The calloc() function allocates memory and initializes all bits to zero.

Syntax of calloc()

ptr = (castType*)calloc(n, size);

Example:

ptr = (float*) calloc(25, sizeof(float));

The above statement allocates contiguous space in memory for 25 elements of type float.

free{}

Dynamically allocated memory created with either calloc() or malloc() stays allocated until you release it. You must explicitly use free() to release the space.

Syntax of free()

free(ptr);

This statement frees the space allocated in the memory pointed by ptr.

Example

// Program to calculate the sum of n numbers entered by the user

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

int main()
{
    int n, i, *ptr, sum = 0;

    printf("Enter number of elements: ");
    scanf("%d", &n);

    ptr = (int*) malloc(n * sizeof(int));
 
    // if memory cannot be allocated
    if(ptr == NULL)                     
    {
        printf("Error! memory not allocated.");
        exit(0);
    }

    printf("Enter elements: ");
    for(i = 0; i < n; ++i)
    {
        scanf("%d", ptr + i);
        sum += *(ptr + i);
    }

    printf("Sum = %d", sum);
  
    // deallocating the memory
    free(ptr);

    return 0;
}

realloc()

If the dynamically allocated memory is insufficient or more than required, you can change the size of previously allocated memory using the realloc() function. realloc() can only be used with “arrays” created using malloc() or calloc(), it cannot be used to resize a standard array.

Syntax of realloc()

ptr = realloc(ptr, x);

Here, ptr is reallocated with a new size x.

Example

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

int main()
{
    int *ptr, i , n1, n2;
    printf("Enter size: ");
    scanf("%d", &n1);

    ptr = (int*) malloc(n1 * sizeof(int));

    printf("Addresses of previously allocated memory: ");
    for(i = 0; i < n1; ++i)
         printf("%u\n",ptr + i);

    printf("\nEnter the new size: ");
    scanf("%d", &n2);

    // rellocating the memory
    ptr = realloc(ptr, n2 * sizeof(int));

    printf("Addresses of newly allocated memory: ");
    for(i = 0; i < n2; ++i)
         printf("%u\n", ptr + i);
  
    free(ptr);

    return 0;
}