Up to this point, each variable we have created stores a single piece of data: one integer, one floating-point number, or one boolean. But real-world applications frequently deal with groups of related items: 100 student exam scores, thousands of coordinates in a 3D mesh, or daily temperature logs across a year.
Declaring individual variables like score1, score2, …, score100 is impossible to maintain.
In C++, an array solves this by providing a fixed-size, sequential collection of elements of the exact same data type stored together in computer memory.
What You’ll Learn
- What a C-style array is and why it exists
- Array declaration, initialization styles, and zero-initialization
- Zero-based indexing and the underlying memory offset mathematics
- How arrays are laid out contiguously in memory
- Iterating through arrays using standard index loops and range-based
for - Calculating array capacity safely
- Multi-dimensional arrays (2D matrices and row-major layout)
- Passing arrays to functions and understanding pointer decay
- The hazards of out-of-bounds memory access and undefined behavior
- Why modern C++ recommends
std::arrayandstd::vector
What Is an Array?
An array is a fixed-length container that holds multiple values of the same type.
Key Characteristics of Raw C-style Arrays
- Homogeneous: Every element in the array must be of the identical data type (e.g., all
int, alldouble, or allchar). - Fixed Size: The size is determined at compile time and cannot grow or shrink during program execution.
- Contiguous Memory: Elements are placed back-to-back in memory with zero gaps between them.
Declaring and Initializing Arrays
1. Declaration Without Initialization
int scores[5]; // Allocates space for 5 integers containing random garbage values!
[!WARNING] An uninitialized local array contains garbage memory. Reading elements before assigning values leads to undefined behavior.
2. Initialization List
int primes[5] = {2, 3, 5, 7, 11}; // Explicit initialization
3. Inferred Array Size
If you provide an initializer list, you can omit the size inside []; the compiler counts the elements automatically:
int measurements[] = {10, 20, 30, 40}; // Inferred size: 4
4. Zero-Initialization
If you initialize fewer elements than the declared size, the remaining elements are automatically zero-initialized:
int data[5] = {1, 2}; // Elements are: 1, 2, 0, 0, 0
int allZeros[100] = {}; // All 100 elements are initialized to 0
Zero-Based Indexing and Memory Layout
In C++, array indices start at 0, not 1. An array of size has valid indices ranging from up to .
int temperatures[4] = {72, 75, 68, 80};
// Accessing elements:
int morningTemp = temperatures[0]; // 72 (First element)
int eveningTemp = temperatures[3]; // 80 (Last element)
// Modifying elements:
temperatures[2] = 70; // Replaces 68 with 70
Why Zero-Based? The Memory Address Formula
The index is not merely an arbitrary number—it is a memory offset. Because array elements are laid out contiguously in memory, the memory address of any element can be calculated directly using arithmetic:
Consider an array int arr[4] where each int occupies 4 bytes:
Index: [0] [1] [2] [3]
Value: 72 75 70 80
Memory Address: 0x1000 0x1004 0x1008 0x100C
|<--4 bytes->|<--4 bytes->|<--4 bytes->|<--4 bytes->|
- For
arr[0]: bytes from base. - For
arr[1]: bytes from base. - For
arr[3]: bytes from base.
Because the CPU computes this address in a single multiplication and addition operation ( constant time), array indexing is instantaneous regardless of whether the array contains 5 elements or 5 million elements!
Traversing Arrays
1. Traditional Index Loop
Use a standard for loop when you need to know the index number during iteration:
#include <iostream>
using namespace std;
int main() {
int grades[] = {88, 92, 79, 95, 84};
const int count = sizeof(grades) / sizeof(grades[0]);
for (int i = 0; i < count; ++i) {
cout << "Student #" << i + 1 << " Grade: " << grades[i] << '\n';
}
return 0;
}
2. Modern Range-Based for Loop
When you simply need to read or update every element, the range-based loop is cleaner and eliminates off-by-one errors:
// Read-only traversal
for (int grade : grades) {
cout << grade << " ";
}
// In-place modification using a reference (&):
for (int& grade : grades) {
grade += 5; // Curve every grade up by 5 points!
}
Calculating Array Size
In the scope where an array is declared, you can compute its number of elements in two ways:
int items[8];
// Method 1: Traditional sizeof division
size_t length1 = sizeof(items) / sizeof(items[0]); // 32 bytes / 4 bytes = 8
// Method 2: Modern C++17 size() (from <iterator>)
#include <iterator>
size_t length2 = size(items); // 8
Multidimensional Arrays
A multidimensional array is an array of arrays. The most common form is a two-dimensional (2D) array, representing a grid, matrix, or spreadsheet table:
// A 2D array with 3 rows and 4 columns
int matrix[3][4] = {
{1, 2, 3, 4}, // Row 0
{5, 6, 7, 8}, // Row 1
{9, 10, 11, 12} // Row 2
};
Row-Major Storage in Memory
Although we visualize a 2D array as a rectangular grid, computer RAM is strictly linear. C++ stores 2D arrays in row-major order: Row 0 is stored first, followed immediately by Row 1, followed by Row 2:
Linear Memory: [ 1 ][ 2 ][ 3 ][ 4 ] [ 5 ][ 6 ][ 7 ][ 8 ] [ 9 ][ 10 ][ 11 ][ 12 ]
|------ Row 0 -----| |------ Row 1 -----| |------- Row 2 ------|
Nested Traversal Example
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
const int ROWS = 3;
const int COLS = 4;
int grid[ROWS][COLS] = {
{10, 20, 30, 40},
{50, 60, 70, 80},
{90, 95, 99, 100}
};
for (int r = 0; r < ROWS; ++r) {
for (int c = 0; c < COLS; ++c) {
cout << setw(5) << grid[r][c];
}
cout << '\n';
}
return 0;
}
Passing Arrays to Functions: The “Array Decay” Phenomenon
In C++, you cannot pass a raw array by value to a function.
When an array is passed as an argument to a function, it automatically decays into a pointer to its first element (int[] becomes int*):
#include <iostream>
using namespace std;
// These two parameter declarations are completely identical to the compiler:
// void printArray(int arr[], int size)
// void printArray(int* arr, int size)
void printArray(const int arr[], int size) {
// WARNING: Inside this function, sizeof(arr) returns the size of a POINTER (8 bytes),
// NOT the size of the original array!
for (int i = 0; i < size; ++i) {
cout << arr[i] << " ";
}
cout << '\n';
}
int main() {
int data[] = {5, 10, 15, 20, 25};
int count = sizeof(data) / sizeof(data[0]);
// We MUST pass the size explicitly because the function cannot know it!
printArray(data, count);
return 0;
}
[!IMPORTANT] Because raw arrays decay into pointers when passed to functions, a function has no way of knowing how many elements the array contains. You must always pass the array length as a separate parameter.
The Danger of Out-of-Bounds Access
C++ does not perform automatic bounds checking on raw array indexing for performance reasons:
int numbers[3] = {10, 20, 30};
// Valid indices are 0, 1, 2.
// Reading or writing numbers[5] is an Out-Of-Bounds error!
cout << numbers[5]; // Reading unowned memory!
numbers[5] = 999; // Memory corruption!
Accessing indices outside triggers undefined behavior. Your program might crash immediately with a segmentation fault, or worse, silently overwrite adjacent variables, leading to security vulnerabilities or corrupted data.
Modern C++ Alternatives: std::array and std::vector
Because raw C-style arrays decay to pointers, lack bounds checking, and cannot be easily copied, modern C++ introduces standard container alternatives:
1. std::array (#include <array>)
A modern, type-safe wrapper over fixed-size stack arrays:
#include <array>
#include <iostream>
using namespace std;
int main() {
array<int, 4> nums = {10, 20, 30, 40};
// Knows its own size without decaying:
cout << "Size: " << nums.size() << '\n';
// Optional bounds checking using .at() (throws out_of_range on invalid index):
cout << nums.at(2) << '\n';
return 0;
}
2. std::vector (#include <vector>)
When you need an array that can grow or shrink dynamically at runtime, use std::vector (covered in our STL deep dives).
Common Beginner Mistakes
- Off-by-One Indexing: Forgetting that an array with 10 elements ends at index 9 (
arr[10]is out of bounds). - Calling
sizeofon an Array Parameter Inside a Function: Yields pointer size (typically 8 bytes), not the array length. - Attempting to Reassign an Entire Array:
int a[3] = {1, 2, 3}; int b[3]; b = a; // COMPILE ERROR: Array type 'int[3]' is not assignable! - Using Non-Constant Variables for Raw Array Sizes: In standard C++, raw array dimensions must be compile-time constants:
int n; cin >> n; int arr[n]; // Non-standard Variable Length Array (VLA)! Use vector instead.
Best Practices
- Use Range-Based Loops: They prevent out-of-bounds index errors completely.
- Prefer
std::arrayover raw C-arrays: For fixed-size collections,std::arrayprovides size safety and copy semantics with zero performance cost. - Always validate user indices: Check
if (index >= 0 && index < size)before indexing raw arrays with dynamic input. - Pass arrays as
constpointers when read-only: Protect caller data by declaringvoid process(const int arr[], int size).
Summary
- An array stores a fixed-size sequence of elements of the same type in contiguous memory.
- Indexing starts at 0; accessing an element computes an address offset in constant time.
- Passing an array to a function causes array decay into a pointer to its first element.
- C++ does not perform runtime bounds checking on raw arrays; out-of-bounds access causes undefined behavior.
- Modern C++ recommends
std::arrayfor fixed collections andstd::vectorfor dynamic lists.
Continue Learning C++
- Previous Article: Functions in C++: Declaration, Scope, Overloading, and Parameters
- Next Article: Strings in C++: std::string vs C-Style Strings and Practical Operations
- Explore Topic Hub: C++ Programming & Systems Engineering
- Related Advanced Container: Mastering std::list in Modern C++