Arrays and Pointers
Learn how to use the fixed-size one-dimensional array in C++.
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.
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;}
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.
#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 sameint* pA = A;cout << " Address of pA: "<<&pA << " holds "<< pA << endl; // both display the same// Accessing array through dereferencing the pointer holding addressescout << "\n\nMemory Location \t "<< "Address \t " << "Value \n";cout << " 0 \t" << ( A ) << " " << *( A ) <<'\n'; // Accessing 3cout << " 1 \t" << (A+1) << " " << *(A+1) <<'\n'; // Accessing 2cout << " 2 \t" << (A+2) << " " << *(A+2) <<'\n'; // Accessing 4cout << " 3 \t" << (A+3) << " " << *(A+3) <<'\n'; // Accessing 1return 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.
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.
#include <iostream>using namespace std;int main(){int A[]={3, 2, 4, 1};int size = sizeof(A)/sizeof(int); // evaluates to 16/4cout << "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* Acout << "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;}
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. Thesize
parameter indicates the amount of memory offset from the base address (held inA
) that needs to be accessed. This flexibility allows us to avoid creating separateprint()
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'sint B[10] = {1,2,3,4,5,6,7,8,9,10}; // This will initialize the array to 1,2,3...10int C[] = {1,2,3}; // This will automatically make an array of size 3 with values 1,2,3int 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;}
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), andD
(initialized with values 1 and 2 followed by eight zeros). Theprint()
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'
)), andC
andD
(both initialized with the string"abc"
followed by a null character ('\0'
)). Theprint()
function is used to display the elements of each character array. Additionally, arraysB
,C
, andD
are directly printed usingcout
, 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 arrayA
directly usingcout
will not work as it lacks a null terminator, leading to unpredictable results. The case for printingE[]
andF[]
is similar.cout<<E
will keep printing letters starting from the base address ofE[]
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.
#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 ofA
are displayed, followed by printing two portions of the array. The first portion consists of the first half ofA
, while the second portion consists of the remaining half. Subsequently, the code reverses the elements in each portion using thereverse()
function. After the reversal, the modified content ofA
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
andend
, are initialized.start
is set to the first index of the array, whileend
is set to the last index. The function utilizes awhile
loop that continues as long asstart
is less thanend
. Inside the loop, theswap()
function is used to exchange the values atA[start]
andA[end]
, effectively reversing the elements. Then,start
is incremented andend
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:
We compare the value at
A[i]
with its immediate neighbor:A[i]>A[i+1]
.If the
i
th number is greater than the (i+1
)th number, we exchange their positions by using theswap()
function.We repeat the above two steps until we reach the last possible pair.
Here’s the code that shows the bubbling step:
for(int i=0;i+1<size;// Because i+1 will reach the end first, we must put the restriction of overflowi++){// Compare two neighbors and shifting the greater to the rightif(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
Here’s the code for sorting the array in ascending order:
#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.
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}
The maximum and the second maximum will reach the array’s last and second to last place.
Only the maximum will be at the last position of the array.
Some random shifting will happen.
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.
#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';}elsecout << "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 arrayA
,size
(which shows the current number of elements in the array),capacity
(which shows the maximum capacity of an array), and theelement
of the integer type that needs to be inserted. First, we check ifsize
is smaller thancapacity
, and then we add the element into the array and incrementsize
.Lines 15–16: We create an array
A
of capacity15
and initialize it with only 10 elements. We then create two variables,size
andcapacity
, and initialize them with the values10
and15
, respectively.Lines 17–22: We call the
addElement()
function to add the elements to the arrayA
.
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.