Fixed-size array

We’ve had variables to store data of specific primitive types and to perform powerful tasks like arithmetic operations and several control structures involving conditions and loops. However, this method has its limitations, as we’re only able to store a single data value in each variable. This becomes a problem when we need to maintain data with more than one value. The common solution of using multiple variables is not efficient when we need to search for a specific element. To overcome this issue, we’ll introduce fixed-size arrays.

In an array, contiguous blocks of a fixed length are allocated in the memory.

Press + to interact
An array of size 4
An array of size 4
1 of 2

The above illustration depicts the memory arrangement of an array of four integers generated by the instruction int A[4] = {3, 2, 4, 1};. The result is four adjacent memory blocks, each consisting of four bytes, with a distinct address. This raises a question: how can we effectively access these memory blocks within the array?

Accessing the array

Accessing elements within an array can be achieved through two methods. The first method involves establishing a relationship between arrays and pointers, while the second method utilizes the [] operator. Let’s explore these approaches in detail.

1. Array indexing through the subscript [] operator

When working with arrays, accessing their memory locations can be made easier using the subscript operator ([]). For instance, A[0] allows us to access the data at the first index of the array. Similarly, A[1], A[2], A[3], and so on, access subsequent memory locations. This subscript-based approach simplifies accessing array elements and is particularly useful when dealing with large datasets, as it only requires two variables: the base of the memory, like the name of the array, and the index, which is nothing but an offset you want to move from the base address.

Here are two examples in which we display the content of an array using the subscript operator [], with hardcoded indexing in the ArrayIndexing.cpp file and variable-based indexing in the ArrayIndexingThroughLoop.cpp file.

#include <iostream>
using namespace std;
int main()
{
int A[4]={3, 2, 4, 1};
int size = 4;
cout << "Memory Location \t "<< "Address \t " << "Value \n";
cout << " A[" << 0 <<"] \t" << &A[0] << " " << A[0] <<'\n';
cout << " A[" << 1 <<"] \t" << &A[1] << " " << A[1] <<'\n';
cout << " A[" << 2 <<"] \t" << &A[2] << " " << A[2] <<'\n';
cout << " A[" << 3 <<"] \t" << &A[3] << " " << A[3] <<'\n';
return 0;
}
Accessing the elements of the array using the subscript operator

2. Arrays and pointers

Accessing memory blocks within an array is straightforward. The array name, like A, represents the initial memory location of the array. Essentially, the name A serves as an address for the array’s first memory location. While this appears similar to a pointer, there’s a subtle difference: A has the same address and points to the same memory location. In contrast, a typical pointer usually has a distinct address and points to a different location.

By dereferencing the array’s name as a pointer, we can easily access the data value stored at the first index. We can manipulate the array name by adding an offset to access subsequent memory locations. For example, adding 1 to the base address of array A gives us the address of the next memory location, incrementing by the data type size (such as four bytes for an integer). This approach allows us to access all other memory locations and their data values.

The provided code shows how we can access the data of an array using its name.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int A[6]={3, 2, 4, 1};
cout << "The base address of A: "<<&A << " or "<< A << endl; // both display the same
int* pA = A;
cout << " Address of pA: "<<&pA << " holds "<< pA << endl; // both display the same
// Accessing array through dereferencing the pointer holding addresses
cout << "\n\nMemory Location \t "<< "Address \t " << "Value \n";
cout << " 0 \t" << ( A ) << " " << *( A ) <<'\n'; // Accessing 3
cout << " 1 \t" << (A+1) << " " << *(A+1) <<'\n'; // Accessing 2
cout << " 2 \t" << (A+2) << " " << *(A+2) <<'\n'; // Accessing 4
cout << " 3 \t" << (A+3) << " " << *(A+3) <<'\n'; // Accessing 1
return 0;
}

Visually, the memory of the above program looks as shown in the following illustration. Notice that dotted addresses in memory are not physical labels (they do not exist physically). They are logical representations that the operating system maps and manages automatically during the execution of the program.

Press + to interact
Logical representation of array
Logical representation of array
1 of 3

The sizeof() operator with arrays

The sizeof() operator in C++ determines the size of a data type or object at compile-time. Applying sizeof() to an array name, like sizeof(array_name), provides the total size of the array in bytes. Dividing this by the array’s data type size will give us the array’s capacity. This approach is convenient and simple. Let’s explore an example showing the usage of sizeof() to calculate array size.

Press + to interact
#include <iostream>
using namespace std;
int main()
{
int A[]={3, 2, 4, 1};
int size = sizeof(A)/sizeof(int); // evaluates to 16/4
cout << "The size of an array `A` is: " << size;
return 0;
}

Arrays and functions

When passing arrays to functions in C++, we pass an array name as an argument, which acts as a pointer to the first element. Usually, after that array name (the base address), we pass the size on which the function performs manipulation.

In the example of the print() function, the parameter is defined either as int arr[] or int* arr. However, it's important to note that int* may not necessarily represent an array; instead, it can represent the address of a location. On the other hand, logically, int arr[] always suggests the passed base address of an array.

This notation is used to indicate that the function expects an array as the parameter. The array name arr is treated as a pointer (holding an address as the relative base), enabling traversal and access to individual elements of the array, starting from the passed base address.

#include <iostream>
using namespace std;
void print(int A[], int size) // A is a paramater receiver, a pointer pointing to a memory address
{ // Hence int A[] can be written as int* A
cout << "Address of parameter receiver: "<< &A << " pointing at : " << A << endl;
cout<<"{";
for(int i = 0; i < size; i++)
cout << A[i]<< ' ';
cout<<"}\n";
}
int main()
{
int A[10]={2,4,5,6,2,4,5,6,9,5};
int B[5]={5,4,3,2,1};
int sizeA = 10, sizeB=5;
cout << "Address of A: " << A << "\n"
<< "Address of B: " << B << "\n";
cout << "Printing A :\n";
print(A, sizeA);
cout << "Printing B :\n";
print(B, sizeB);
cout << "\nPrinting the half portion of A\n ";
print(A, sizeA/2);
cout << "\nPrinting the half portion of B\n ";
print(B, sizeB/2);
cout << "\nPrinting 3 values starting from the A[2] onwards\n ";
print(A+2, 3);
cout << "\nPrinting 2 values starting from the B[1] onwards\n ";
print(B+1, 2);
return 0;
}
Print function with an array as parameter

Tip: Run the above two codes separately and understand their working.

  • The printFunctionWithMsg.cpp file: On lines 3–10, the powerful concept of only passing the base address as an argument provides great flexibility. On the receiving end, the function parameter is simply a pointer unaware of anything other than the base address. The size parameter indicates the amount of memory offset from the base address (held in A) that needs to be accessed. This flexibility allows us to avoid creating separate print() functions for every array, showcasing the true power of functions—reusability.

This concept also introduces another powerful idea: we can access the entire array and a portion of it by considering any address as the base address. This means we can access a specific range or subset of elements within the array, starting from a desired address. It provides granular control over the data we want to work with, further enhancing the versatility of array usage.

Next, there are some important initialization methods that we should learn.

Revisiting initialization of arrays

Let’s look at the following example:

#include <iostream>
using namespace std;
void print(int base[], int size)
{
cout << "{ ";
for(int i=0; i<size; i++)
cout << base[i] << " ";
cout << "}";
}
int main()
{
int A[10] = {}; // This will initialize all the values to 0's
int B[10] = {1,2,3,4,5,6,7,8,9,10}; // This will initialize the array to 1,2,3...10
int C[] = {1,2,3}; // This will automatically make an array of size 3 with values 1,2,3
int D[10] = {1,2}; // An array of 10 elements {1,2,0,0,...,0}
cout << "\nA[] = "; print(A, 10);
cout << "\nB[] = "; print(B, 10);
cout << "\nC[] = "; print(C, 3);
cout << "\nD[] = "; print(D, 10);
return 0;
}
Initializations of arrays
  • In the intArraysInitializations.cpp file, we define four arrays: A (initialized with all values set to 0), B (initialized with values 1 to 10), C (initialized with values 1, 2, and 3), and D (initialized with values 1 and 2 followed by eight zeros). The print() function is used to display the elements of each array.

  • In the charArraysInitialization.cpp file, we define four character arrays: A (initialized with the characters 'a' to 'j'), B (initialized with 'a', 'b', and eight null characters ('\0')), and C and D (both initialized with the string "abc" followed by a null character ('\0')). The print() function is used to display the elements of each character array. Additionally, arrays B, C, and D are directly printed using cout, which interprets character array names as null-terminated strings and prints the characters of the array until it reaches the null character '\0' and then stops. However, printing the array A directly using cout will not work as it lacks a null terminator, leading to unpredictable results. The case for printing E[] and F[] is similar. cout<<E will keep printing letters starting from the base address of E[] until it reads '\0', and therefore, by combining the values of both arrays, it prints "abab".

Let’s practice a few nontrivial examples for traversing and manipulating arrays.

Example 1: Reversing an array

Problem statement: Implement a function named reverse() that will take two arguments as input: the starting point of an array A and the total number of elements that need to be reversed. The function should reverse the specified elements of the array in place.

Press + to interact
#include <iostream>
using namespace std;
void print(const char Msg[], int A[], int size)
{
cout<< Msg << ": {";
for(int i = 0; i < size; i++)
{
cout << A[i]<< ' ';
}
cout<<"}\n";
}
void reverse(int A[], int size)
{
int start = 0;
int end = size - 1;
while(start < end)
{
swap(A[start], A[end]);
start++;
end--;
}
}
int main()
{
int A[10]={2,4,5,6,2,4,5,6,9,5};
int size = 10;
print("Before Reversing\n A[0...9] ",A, size);
print("The portion A[0...4]",A, size/2);
print("The portion A[5...9]",A+size/2, size-size/2);
reverse(A , size/2);
reverse(A + size/2 , size - size/2);
print("\n\nAfter Reversing\n A[0...9]",A, size);
print("After A[0...4]",A, size/2);
print("After A[5...9]",A+size/2, size-size/2);
return 0;
}

Note: We use the swap() function that is already implemented in the language.

  • Lines 25–39: The given code demonstrates the manipulation of an integer array A by reversing its elements in different portions. Initially, the contents of A are displayed, followed by printing two portions of the array. The first portion consists of the first half of A, while the second portion consists of the remaining half. Subsequently, the code reverses the elements in each portion using the reverse() function. After the reversal, the modified content of A is displayed, and the two portions are printed again to showcase the changes. Overall, the code shows how to reverse elements within specific portions of an array, offering insights into array manipulation techniques.

  • Lines 12–22: Within the reverse() function, two variables, start and end, are initialized. start is set to the first index of the array, while end is set to the last index. The function utilizes a while loop that continues as long as start is less than end. Inside the loop, the swap() function is used to exchange the values at A[start] and A[end], effectively reversing the elements. Then, start is incremented and end is decremented to move toward the middle of the array.

Example 2: Bubble sort

So far, we’ve learned how to manipulate the data within the array. In some cases, we need sorted data to solve our problem effectively. Various sorting algorithms have been proposed, and one of them is named bubble sort. In this example, we’ll learn the concept of the bubble sort algorithm and how we can implement it.

We know that when we boil a liquid, the molecules with higher kinetic energy form bubbles. These bubbles rise and reach the top. This is the inspiration for bubble sorting, according to which the elements that meet a certain condition after comparison are swapped (they rise).

The nested loop implementation

Sample program:

A = {6 7 8 4 3 2 1}
Sorted A = {1 2 3 4 5 6 7}

Before sorting in ascending order, let’s ensure we understand the central strategy of bubbling.

The bubbling strategy

First, we take the starting position i = 0. Then:

  1. We compare the value at A[i] with its immediate neighbor: A[i]>A[i+1].

  2. If the ith number is greater than the (i+1)th number, we exchange their positions by using the swap() function.

  3. We repeat the above two steps until we reach the last possible pair.

Here’s the code that shows the bubbling step:

Press + to interact
for(int i=0;
i+1<size;// Because i+1 will reach the end first, we must put the restriction of overflow
i++)
{
// Compare two neighbors and shifting the greater to the right
if(A[i]>A[i+1])
swap(A[i],A[i+1]);
}

Let’s look at the animation that shows the bubbling process.

The above animation clearly shows that the maximum value will reach the end after the first bubbling ends. Similarly, the second iteration of bubbling will shift the second maximum value at the second to last position of the array.

But how many times do we need to repeat this bubbling step to complete sorting? To sort the array of nn elements, we must repeat the bubbling step n1n-1 times on the given array.

Here’s the code for sorting the array in ascending order:

Press + to interact
#include <iostream>
using namespace std;
void print(int A[], int size)
{
for(int i = 0; i<size; i++)
cout<< A[i] <<' ';
cout<<'\n';
}
void ascSort(int A[], int size)
{
for(int t = 1; t<=size-1; t++)
{
for(int i = 0; i+1 < size ; i++)
if(A[i]>A[i+1])
swap(A[i], A[i+1]);
}
}
int main()
{
int A[8] = {5,6,3,5,1,4,7,2};
int size = 8;
cout << "Before sorting: ";
print(A, size);
ascSort(A, size);
cout << "\nAfter sorting: ";
print(A, size);
return 0;
}

Practice problem

To enhance the efficiency of bubble sort, we can minimize the area of the array subject to the bubbling operation after every step. For the first time, the inner loop should run size-1 times; for the second time, the inner loop should run size-2 times, and so on. What changes should we make in the above code to make it more efficient?


Quiz

Let’s test your understanding with a short quiz.

1

What would be the outcome if we performed two iterations of the bubbling step on the following array?

A[5]={5, 6, 2, 4,1}

A)

The maximum and the second maximum will reach the array’s last and second to last place.

B)

Only the maximum will be at the last position of the array.

C)

Some random shifting will happen.

Question 1 of 20 attempted

Managing array size and capacity

An array is a powerful tool, but it has some limitations. For example, look at the array below. It has a specific size and elements.

int A[4]={2,3,4,5};

The compiler allocates 16 consecutive bytes of memory and initializes them with four elements (each having four bytes of memory).

However, what if we need to add more elements to the array?

One approach is to create an array of a larger size (than required), like int A[1000]={2,3,4,5}. In this case, the compiler allocates 4,000 consecutive bytes of memory (1,000 blocks where each block is four bytes). It initializes the first four positions with the given elements, and the remaining part of the array is initialized to 0. This strategy enables us to add up to 994 more elements to the array, expanding its functionality.

So, generally, to address these limitations, it becomes crucial to manage both the size and capacity of the array, where the size of an array represents the number of elements currently present. In contrast, the capacity refers to the maximum number of elements the array can hold. By maintaining these two variables separately, we can optimize the efficiency of functions that operate on arrays.

In the following example code, the addElement() function is introduced. This function accepts an array A[] (the base address of the allocated memory), its size, capacity, and an element to be added. The function determines whether there is space to add the element by checking if the size is less than the capacity. The element is inserted at the next available index if space is available, and the size is incremented accordingly. This approach ensures efficient array capacity usage, preventing unnecessary iterations over unused elements.

Press + to interact
#include <iostream>
using namespace std;
void addElement(int A[], int &size, int capacity, int element)
{
if(size < capacity)
{
A[size++] = element;
cout << "Successfully added an element: "<< element <<'\n';
}
else
cout << "The capacity of array A is full \n";
}
int main()
{
const int capacity = 15;
int A[capacity]={2,4,5,6,2,4,5,6,9,5};
int size = 10;
addElement(A, size, capacity, 10);
addElement(A, size, capacity, 14);
addElement(A, size, capacity, 21);
addElement(A, size, capacity, 12);
addElement(A, size, capacity, 15);
addElement(A, size, capacity, 18);
return 0;
}
  • Lines 3–12: We implement the function addElement(), which takes four arguments: an array A, size (which shows the current number of elements in the array), capacity (which shows the maximum capacity of an array), and the element of the integer type that needs to be inserted. First, we check if size is smaller than capacity, and then we add the element into the array and increment size.

  • Lines 15–16: We create an array A of capacity 15 and initialize it with only 10 elements. We then create two variables, size and capacity, and initialize them with the values 10 and 15, respectively.

  • Lines 17–22: We call the addElement() function to add the elements to the array A.

By separating the size and capacity parameters, functions can operate on arrays without considering unnecessary elements, improving their performance. This approach also provides flexibility for arrays to grow dynamically without needing to redefine a larger-sized array from scratch.

Is the array with the size and capacity variables the ultimate solution? Or are there more advanced techniques available to enhance the flexibility and efficiency of array structures?

 Limitations of the array

As we encounter scenarios where the array reaches its maximum capacity, we need to devise effective solutions to manage this situation. Simply increasing the capacity to an excessively large value may seem like a quick fix, but it leads to unnecessary memory waste when multiple arrays are involved.

If each array is not fully occupied, a significant amount of memory remains unused. To optimize memory usage, we require a more efficient approach that can dynamically allocate memory based on the actual needs of each array. We’ll explore alternative techniques that overcome the limitations of the array, ensuring efficient memory utilization.