Skip to content

Arrays and C-Strings — C-Style Arrays, Pointer Decay, and std::array

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Arrays and C. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ supports both C-style arrays inherited from C and the modern std::array container, with C-strings being null-terminated character arrays prone to buffer overflow if mishandled.

What You'll Learn

You will declare and initialize C-style arrays, understand array-to-pointer decay and why it causes bugs, work with C-strings and the cstring library, use std::array as a safer fixed-size container, pass arrays to functions correctly, and avoid common buffer overflow and off-by-one errors.

Why It Matters

Arrays are the most fundamental data structure in computing: contiguous memory of identical elements. C-style arrays are everywhere in legacy code, operating system APIs, and Embedded Systems. Understanding them deeply, and knowing when to prefer std::array or std::vector, separates intermediate C++ programmers from beginners.

Learning Path

graph LR
    A["08: Loops"] --> B["09: Arrays & C-Strings"]
    B --> C["10: Functions"]
    C --> D["11: Classes & Objects"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

C-Style Arrays

#include <iostream>

int main() {
    // Declaration and initialization
    int arr[5];                    // uninitialized: contains garbage
    int arr2[5] = {1, 2, 3, 4, 5};  // fully initialized
    int arr3[5] = {1, 2};           // {1, 2, 0, 0, 0} (rest zero)
    int arr4[]  = {1, 2, 3};        // size deduced: 3 elements
    
    // Access via indexing (zero-based)
    std::cout << arr2[0] << "\n";  // 1
    std::cout << arr2[4] << "\n";  // 5
    
    // Size computation
    int size = sizeof(arr2) / sizeof(arr2[0]);  // 5
    std::cout << size << "\n";
    
    // Iteration
    for (int i = 0; i < 5; ++i) {
        arr2[i] *= 2;
    }
    
    for (int x : arr2) {
        std::cout << x << " ";
    }
    std::cout << "\n";
    // Output: 2 4 6 8 10
}

Arrays have a fixed size determined at compile time. You cannot resize a C-style array.

Array-to-Pointer Decay

#include <iostream>

void printSize(int arr[]) {
    std::cout << sizeof(arr) << "\n";  // prints 8 (pointer size on 64-bit)
}

int main() {
    int arr[10];
    std::cout << sizeof(arr) << "\n";   // prints 40 (10 * 4)
    
    printSize(arr);  // prints 8 (arr decays to pointer)
    
    // The decay means you cannot use range-based for on function arguments
    int* ptr = arr;  // implicit decay
    std::cout << *(ptr + 3) << "\n";  // same as arr[3]
}

When you pass a C-style array to a function, it decays to a pointer to the first element. The function receives no size information. You must pass the size separately.

Multidimensional Arrays

#include <iostream>

int main() {
    // 2D array: 3 rows, 4 columns
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    for (int r = 0; r < 3; ++r) {
        for (int c = 0; c < 4; ++c) {
            std::cout << matrix[r][c] << " ";
        }
        std::cout << "\n";
    }
    
    // Only the first dimension can be omitted (size deduced)
    int mat[][3] = {
        {1, 2, 3},
        {4, 5, 6}
    };
}

Multidimensional arrays are stored in row-major order: all elements of row 0, then all elements of row 1, and so on. This matters for cache performance.

C-Strings

C-strings are arrays of char terminated by a null character ('\0', ASCII 0).

#include <iostream>
#include <cstring>

int main() {
    // String literal (null-terminated automatically)
    const char* greeting = "Hello";
    // Memory layout: {'H', 'e', 'l', 'l', 'o', '\0'}
    
    // Character array with explicit null
    char name[6] = {'A', 'l', 'i', 'c', 'e', '\0'};
    std::cout << name << "\n";
    
    // String literal initializer
    char city[] = "Boston";  // size is 7 (includes null)
    std::cout << city << "\n";
    
    // cstring functions
    char buffer[50];
    std::strcpy(buffer, "Copy ");
    std::strcat(buffer, "this ");
    std::strcat(buffer, "string");
    std::cout << buffer << "\n";
    std::cout << "Length: " << std::strlen(buffer) << "\n";
    
    // Comparison
    if (std::strcmp(greeting, "Hello") == 0) {
        std::cout << "Strings match\n";
    }
}

C-string functions (strcpy, strcat, strcmp) do not check buffer sizes. They are a major source of security vulnerabilities. Prefer std::string in C++.

std::array — Fixed-Size Array (C++11)

std::array wraps C-style arrays in a class that knows its size and provides STL container interface.

#include <iostream>
#include <array>
#include <algorithm>

int main() {
    std::array<int, 5> arr = {1, 2, 3, 4, 5};
    
    // Knows its size
    std::cout << arr.size() << "\n";  // 5
    
    // STL algorithms
    std::reverse(arr.begin(), arr.end());
    for (int x : arr) {
        std::cout << x << " ";
    }
    std::cout << "\n";
    // Output: 5 4 3 2 1
    
    // Bounds-checked access (throws std::out_of_range)
    try {
        std::cout << arr.at(10) << "\n";
    } catch (const std::out_of_range& e) {
        std::cout << "Out of range: " << e.what() << "\n";
    }
    
    // Unchecked access (faster, like C-style)
    std::cout << arr[2] << "\n";  // 3
    
    // Fill with value
    arr.fill(0);
    std::cout << arr[0] << "\n";  // 0
    
    // No decay: passes size information
    auto printArr = [](const auto& a) {
        std::cout << "Size: " << a.size() << "\n";
    };
    printArr(arr);
}

std::array has zero overhead compared to C-style arrays. It stores elements directly (no heap allocation) and the compiler can inline all operations.

Comparison: C-Array vs std::array vs std::vector

Feature C-Array std::array std::vector
Size known at compile time Yes Yes No
Knows its own size No Yes Yes
STL algorithm support No Yes Yes
Dynamic resizing No No Yes
Heap allocation No No Yes
Pass to function safely Pass size too Yes Yes

Common Mistakes

Mistake 1: Off-by-One (Buffer Overflow)

int arr[5];
arr[5] = 42;  // writes beyond array, undefined behavior

C++ does not check array bounds. Accessing beyond the end of an array can corrupt memory or crash your program.

Mistake 2: Array Decay in Function Parameters

void process(int arr[10]) {
    // arr is actually int*, size argument is ignored
    for (int i = 0; i < 10; ++i) { ... }  // time bomb
}

The 10 in the parameter is ignored by the compiler. Pass the size as a separate parameter or use std::array.

Mistake 3: Forgetting Null Terminator

char buf[3] = {'a', 'b', 'c'};  // no null terminator!
std::cout << buf;  // undefined behavior: reads past end

Always leave room for the null terminator.

Mistake 4: Using strcpy Without Size Check

char dest[10];
strcpy(dest, "This is a very long string");  // buffer overflow

Use strncpy or, better yet, std::string.

Mistake 5: Confusing Array of Pointers with Pointer to Array

int* arr1[5];  // array of 5 pointers to int
int (*arr2)[5];  // pointer to array of 5 ints

Mistake 6: Returning Pointer to Local Array

int* getArray() {
    int arr[5] = {1, 2, 3, 4, 5};
    return arr;  // arr is destroyed when function returns
}

Return std::array<int, 5> or std::vector<int> instead.

Practice Questions

  1. What is array-to-pointer decay and why does it happen?
  2. Write code that reverses a C-style array in place without using std::reverse.
  3. What is the difference between char s[] = "hello" and const char* s = "hello"?
  4. Convert a C-style array program to use std::array. Compare the code.
  5. What is strlen guaranteed to return? How does it determine the length?

Challenge

Write a function template that accepts a std::array of any size and any type, finds the maximum element, and returns it. Test with std::array<int, 5> and std::array<double, 3>.

FAQ

Why does C++ still have C-style arrays if we have std::array?

Backward compatibility with C, embedded systems where STL is unavailable, and low-level API interop. However, prefer std::array in normal C++ code.

Can I resize a C-style array?

No. Fixed at compile time. Use std::vector when you need dynamic resizing.

Is std::array slower than a C-style array?

No. std::array has zero runtime overhead. The compiler generates identical code for both.

What is the safe way to copy a C-string?

Use strncpy (which limits bytes copied) or, in C++, just use std::string which handles everything safely.

Can I use range-based for on a pointer?

No. Range-based for requires knowing the size, which a pointer does not provide. That is why array-to-pointer decay breaks range-based for.

What happens if I do not null-terminate a C-string?

Operations like strlen, strcpy, and cout will read past the array until they encounter a null byte, causing undefined behavior or security vulnerabilities.

Mini Project

Write a word reversal program using C-strings:

#include <iostream>
#include <cstring>

void reverse(char str[]) {
    int len = std::strlen(str);
    for (int i = 0; i < len / 2; ++i) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}

int main() {
    char text[100];
    std::cout << "Enter a string: ";
    std::cin.getline(text, 100);
    
    reverse(text);
    std::cout << "Reversed: " << text << "\n";
    
    // Now with std::array and std::reverse
    std::array<char, 100> arr = {};
    std::cout << "Enter another string: ";
    std::cin.getline(arr.data(), arr.size());
    
    size_t len = std::strlen(arr.data());
    std::reverse(arr.begin(), arr.begin() + len);
    std::cout << "Reversed (std::array): " << arr.data() << "\n";
}

What's Next

Arrays store sequences of data. The next lesson covers functions: pass by value, pass by reference, pass by address, function overloading, and default arguments. You will learn how to organize code into reusable blocks.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro