Senior 7 min · March 06, 2026

C Arrays — One <= Corrupted Memory for 6 Weeks

A loop using <= instead of < wrote past int[10], corrupting emergency flags non-deterministically for weeks.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Arrays in C store elements contiguously in memory — access by index uses base address + (index * element_size) — O(1) access
  • Key components: element type (int, char, float), name, size (fixed at compile time), index (zero-based)
  • Performance: Contiguous memory enables CPU cache prefetching — sequential iteration is ~10x faster than pointer-chasing structures
  • Production trap: No bounds checking — accessing scores[10] in int scores[5] compiles and silently corrupts memory or crashes later
  • Biggest mistake: Using sizeof(arr)/sizeof(arr[0]) inside a function (array decays to pointer, returns pointer size 8, not array size)
✦ Definition~90s read
What is Arrays in C?

C arrays are the rawest form of contiguous memory allocation in any mainstream language — a fixed-size block of memory where elements of the same type sit back-to-back, with no bounds checking, no length prefix, and no safety net. They exist because C was designed for systems programming where you need direct hardware control: embedded firmware, operating system kernels, game engines, and real-time audio processing.

Imagine a row of numbered lockers at a school.

The trade-off is that accessing array[10] on an 10-element array doesn't crash — it silently reads or writes whatever lives at that memory address, corrupting adjacent variables, stack frames, or heap metadata. This is why 70% of all security vulnerabilities in C/C++ codebases (per Microsoft's Security Response Center) trace back to buffer overflows on arrays.

If you need safety, use Rust's slices with bounds checking, Go's slices with runtime guards, or Python's lists — but if you're writing a bootloader, a DMA driver, or a real-time scheduler, C arrays are the only tool that gives you zero-cost, predictable memory layout without a runtime hiding behind you.

Plain-English First

Imagine a row of numbered lockers at a school. Each locker holds one item, every locker has a number on the door, and all the lockers are the same size. An array in C is exactly that — a fixed row of same-type storage slots, each with a number (called an index) you use to get to it. Instead of creating 10 separate variables to store 10 test scores, you get one 'row of lockers' called scores and access each one by its number. The key twist: the numbering starts at 0, not 1 — so the first locker is locker number 0.

Every real program deals with collections of data. A weather app tracks 30 days of temperatures. A game tracks the scores of 8 players. A bank tracks thousands of account balances. If C didn't give us a way to store multiple values under a single name, you'd have to write temperature_day1, temperature_day2, temperature_day3... all the way to temperature_day30. That is not programming — that's madness.

Arrays solve this exact problem. They let you group multiple values of the same type under one variable name and access any individual value instantly using its position number. Instead of 30 separate variables, you get one array with 30 slots. Instead of writing 30 lines to print each temperature, you write one loop. That is the power arrays hand you — and it's the foundation of almost every data structure you'll ever learn.

By the end you'll know how to declare and initialize an array in C, read from it and write to it using indexes, loop through every element with a for loop, understand how arrays sit in memory, and avoid the two bugs that trip up almost every beginner on their first week.

What C Arrays Actually Do — and Why They Corrupt

A C array is a contiguous block of memory holding elements of the same type, accessed via pointer arithmetic. No bounds checking, no length stored at runtime — just a base address and an index offset. This zero-overhead design gives O(1) access but shifts all safety responsibility to the programmer.

In practice, an array decays to a pointer when passed to a function, losing size information. The compiler trusts you to stay within bounds — it will not warn you when you write past the end. A buffer overflow silently corrupts adjacent stack or heap memory, often manifesting as a crash weeks later in unrelated code.

Use C arrays when you need maximum performance and memory control — embedded systems, kernel code, or real-time audio. Avoid them in application-level code where safety matters more than a few CPU cycles. Every production use must be paired with explicit size tracking and static analysis.

Array Decay Is Not a Feature
When you pass an array to a function, sizeof() gives the pointer size, not the array length — this is the #1 source of buffer overflows in C.
Production Insight
A network packet parser wrote 4 bytes past a 64-byte buffer, corrupting a function pointer on the stack.
The crash occurred 6 weeks later in an unrelated logging call, making root cause analysis nearly impossible.
Always pair every stack-allocated array with a #define for its size and use sizeof(array)/sizeof(array[0]) before decay.
Key Takeaway
C arrays are raw memory — no bounds, no length, no safety.
Every index operation is a potential corruption point; treat all array accesses as security-critical.
Use static analysis (e.g., Coverity, clang-tidy) and explicit size tracking in every production codebase.
C Arrays: Memory Layout & Corruption Risks THECODEFORGE.IO C Arrays: Memory Layout & Corruption Risks How arrays work in memory and why they corrupt Array Declaration & Init Fixed-size contiguous memory block Memory Layout Elements stored sequentially in RAM Index Access Pointer arithmetic: base + index * size Out-of-Bounds Write No bounds checking; corrupts adjacent memory Uninitialized Elements Contains garbage values; undefined behavior Array Decay to Pointer Array name becomes pointer; size info lost ⚠ No bounds checking: writing past end corrupts memory silently Always validate indices or use safe wrappers (e.g., std::array) THECODEFORGE.IO
thecodeforge.io
C Arrays: Memory Layout & Corruption Risks
Arrays C

Declaring and Initializing Your First C Array

Declaring an array in C follows a simple pattern: you give the data type, a name, and the number of slots you need in square brackets. That's it.

The syntax looks like this: int scores[5]; — this tells C to reserve 5 consecutive slots in memory, each big enough to hold one int, and label the whole row 'scores'. Nothing is stored in them yet — they hold garbage values until you assign something.

You can also declare and fill the array at the same time using an initializer list — curly braces with values separated by commas. When you do this, C lets you leave out the size entirely and figures it out for you by counting the values you provided.

One thing to burn into memory right now: C arrays are zero-indexed. The first element lives at index 0, the second at index 1, and so on. An array of size 5 has valid indexes 0, 1, 2, 3, and 4. Index 5 does not exist. Accessing it is the single most common bug in C programming, and we'll come back to it in the gotchas section.

Let's declare an array of 5 student test scores, initialize it with real values, and print each one.

io/thecodeforge/c/student_scores.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
    // Declare an integer array that holds 5 test scores.
    // The number in brackets is the SIZE — total number of slots.
    int scores[5] = {88, 92, 75, 100, 63};

    // Access individual elements using their INDEX (position number).
    // Indexes start at 0, so the first score is scores[0], not scores[1].
    printf("First student score:  %d\n", scores[0]);  // prints 88
    printf("Second student score: %d\n", scores[1]);  // prints 92
    printf("Last student score:   %d\n", scores[4]);  // prints 63 — index 4, not 5!

    // You can also change a value after declaration by assigning to a specific index.
    scores[2] = 80;  // Student 3 got a re-grade — update index 2 from 75 to 80
    printf("Updated third score:  %d\n", scores[2]);  // prints 80

    return 0;
}
Watch Out: Zero-Indexing Bites Everyone Once
An array declared as int scores[5] has valid indexes 0 through 4. Writing scores[5] compiles without error but reads memory that doesn't belong to your array — this is undefined behavior and the bug is nearly invisible. Always think: last valid index = size minus one.
Production Insight
C arrays have no bounds checking — not even a warning. int data[5]; data[5] = 42; compiles and runs. It corrupts adjacent memory silently.
The segmentation fault may not happen at the overflow line — it may happen thousands of instructions later when corrupted data is used.
Rule: Assume every array access is a potential buffer overflow. Use assert(index >= 0 && index < ARRAY_SIZE) in debug builds, and validate all indexes in production.
Key Takeaway
C arrays are zero-indexed — the first element is at index 0 and the last valid index is always size minus one. Accessing index equal to the size is undefined behavior and one of C's most common bugs.
There is no bounds checking. The compiler trusts you. If you write past the end, you corrupt memory silently.
Rule: Always use the pattern for (int i = 0; i < array_size; i++). Never <=.
Array Initialization Strategy
IfAll element values known at compile time
UseUse initializer list: int arr[] = {1, 2, 3};. Compiler sets size automatically. Best for lookup tables, configuration data.
IfAll elements must be zero
UseUse int arr[10] = {0};. First element explicitly zeroed, rest default-initialized to zero. Guarantees zero-filled array.
IfValues will be filled at runtime (loop, user input)
UseDeclare without initializer: int arr[10];. Elements contain garbage until assigned. Must fill before reading.
IfArray is static or global
UseZero-initialized automatically by default. static int arr[10]; contains zeros. No need for explicit ={0}.
IfNeed to fill with same non-zero value (e.g., all 1s)
UseUse loop: for (int i = 0; i < N; i++) arr[i] = 1;. C has no built-in way to set all to non-zero without loop or memset (only works for 0).

Looping Through an Array — The Real Power Unlocked

Accessing elements one by one with hardcoded indexes only works when your array is tiny and you already know every position you need. The real strength of arrays shows up the moment you combine them with a loop.

A for loop and an array are a natural pair. The loop counter acts as the index — it starts at 0, goes up by 1 each iteration, and stops before it reaches the array's length. This pattern is so standard in C that you'll write it thousands of times in your career.

The critical ingredient here is knowing the array's length. In C, arrays don't carry their own size around — unlike some other languages. The standard trick is to calculate the size at the point where the array is declared using the sizeof trick: sizeof(array) / sizeof(array[0]). This divides the total bytes the array occupies by the bytes one element occupies, giving you the element count. This only works in the same scope where the array was declared — we'll cover why in the gotchas.

Let's build a real example: store seven daily temperatures for a week, print them all, and calculate the average — something a weather app genuinely does.

io/thecodeforge/c/weekly_temperatures.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

int main() {
    // Seven daily temperatures in Celsius for one week.
    // Using an initializer list — C counts the values and sets the size to 7.
    float daily_temps[] = {18.5, 21.0, 19.3, 23.8, 22.1, 17.6, 20.4};

    // sizeof(daily_temps) gives total bytes the whole array uses.
    // sizeof(daily_temps[0]) gives bytes for ONE float element.
    // Dividing them gives us the number of elements — 7 in this case.
    int num_days = sizeof(daily_temps) / sizeof(daily_temps[0]);

    float total = 0.0;  // Running total so we can calculate the average

    printf("Daily temperatures this week:\n");
    printf("-----------------------------\n");

    // The loop counter 'day' doubles as the array index.
    // Condition is day < num_days (NOT <=) to stay within valid indexes.
    for (int day = 0; day < num_days; day++) {
        printf("Day %d: %.1f°C\n", day + 1, daily_temps[day]);
        // day+1 in the print just makes it human-readable (Day 1, not Day 0)

        total += daily_temps[day];  // Add each temperature to the running total
    }

    float average = total / num_days;  // Divide total by number of days
    printf("-----------------------------\n");
    printf("Weekly average: %.2f°C\n", average);

    return 0;
}
Pro Tip: Calculate Size Once, Store It in a Variable
Always store sizeof(arr)/sizeof(arr[0]) in a named int variable like num_days or count before the loop. Writing the sizeof expression directly inside the loop condition works but clutters the code and makes it harder to read during code review.
Production Insight
The sizeof trick sizeof(arr)/sizeof(arr[0]) only works in the scope where the array was declared, not after it decays to a pointer.
A common mistake is to put this in a macro and use it inside a function — the macro will return pointer size (8), not array size.
Rule: Store the array length in a separate variable at the point of declaration, then pass that variable to functions alongside the array pointer.
Key Takeaway
The sizeof trick computes array length at compile time — zero runtime overhead.
But it only works in the same scope where the array was declared. Inside a function that receives an array parameter, it returns pointer size (8), not array size.
Rule: Store the length in a variable at the point of declaration and pass it to functions explicitly.
Array Length Calculation Strategy
IfWithin same scope as array declaration (main, function where array is defined as int arr[N])
UseUse sizeof(arr) / sizeof(arr[0]). Computed at compile time, zero runtime cost.
IfArray passed as function parameter (void f(int arr[]))
UseCannot use sizeof — returns pointer size. Add extra parameter: void f(int arr[], size_t len). Caller passes len computed via sizeof trick.
IfNeed to share array size across multiple functions
UseDefine a constant: #define ARR_SIZE 10. Use that constant for both array declaration and loop bounds. No runtime calculation.
IfDynamic array (malloc)
UseStore size in a separate variable when allocating. int arr = malloc(n sizeof(int)); then keep n alongside the pointer.
IfMulti-dimensional array (int arr[][10])
UseFor inner dimension, sizeof(arr[0]) / sizeof(arr[0][0]) works. For outer, pass rows as separate parameter.

How Arrays Live in Memory — Why This Changes Everything

This section is where things get genuinely interesting — and where C separates itself from beginner-friendly languages. Understanding this will make you a better C programmer immediately.

When you declare int scores[5], C goes to RAM and finds 5 consecutive (side-by-side) memory addresses and reserves them all for you. On most systems, an int is 4 bytes, so your array occupies 20 bytes in a row. Think of it like booking 5 consecutive seats on a train — not 5 seats scattered randomly, but 5 in a row.

This matters because of how fast array access is. When you write scores[3], C doesn't search for element 3. It calculates the exact memory address mathematically: start_address + (3 × size_of_one_element). That's a single calculation — instant access regardless of whether you're accessing element 0 or element 499. This is called O(1) access time and it's why arrays are so fundamental.

The array name itself — scores, without brackets — is actually the memory address of the very first element. This is the bridge between arrays and pointers, which you'll explore later. For now, just know that when you pass an array to a function, you're passing this starting address, not a copy of all the data. That's why sizeof won't give you the element count inside a function that received the array as a parameter.

io/thecodeforge/c/array_memory_layout.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

int main() {
    // Declare a simple integer array with 5 values
    int scores[5] = {88, 92, 75, 100, 63};

    printf("Exploring how scores[] sits in memory:\n");
    printf("---------------------------------------\n");

    // %p is the format specifier for printing a memory address (pointer)
    printf("Address of the entire array (scores):     %p\n", (void*)scores);
    printf("Address of first element (scores[0]):    %p\n", (void*)&scores[0]);
    // These two lines print THE SAME address — the array name IS the first element's address

    printf("\nElement-by-element address breakdown:\n");
    for (int i = 0; i < 5; i++) {
        // &scores[i] gives the memory address of element at index i
        // Notice each address is exactly 4 bytes higher than the previous one
        printf("  scores[%d] = %3d  stored at address: %p\n",
               i, scores[i], (void*)&scores[i]);
    }

    // sizeof the full array vs sizeof one element
    printf("\nTotal bytes used by scores[]: %zu\n", sizeof(scores));
    printf("Bytes per element (one int):  %zu\n", sizeof(scores[0]));
    printf("Number of elements:           %zu\n", sizeof(scores) / sizeof(scores[0]));

    return 0;
}
Interview Gold: Array Name vs. Pointer
In C, the name of an array decays to a pointer to its first element in most expressions. This is why sizeof(scores) inside main gives 20 bytes (the full array), but if you pass scores to a function, sizeof inside that function gives you 8 bytes (just the pointer size on a 64-bit system) — not 20. Always pass the length separately when writing array functions.
Production Insight
The array name decaying to a pointer is the source of the sizeof gotcha, but also the source of pointer arithmetic: arr + 2 points to the third element.
In embedded systems, the address of an array can be critical — e.g., DMA transfer buffers require contiguous memory.
Rule: Knowing that arrays are contiguous is why you can use memcpy(arr1, arr2, sizeof(arr1)) — no loop needed to copy elements.
Key Takeaway
Array names decay to a pointer to the first element when passed to functions — which means sizeof inside a function gives you pointer size (8 bytes), not the array size. Always pass length as a separate parameter.
Arrays guarantee contiguous memory — all elements sit side-by-side in RAM. This makes index access O(1) via simple arithmetic and makes arrays the fastest data structure for positional access.
Rule: The array name in most expressions is the address of the first element. Use &arr only when you need pointer to the whole array.
Array vs Pointer Behavior
IfUsing array name in expression (e.g., int *p = arr;)
UseArray decays to pointer to first element. p now points to &arr[0]. This is automatic, no & needed.
IfUsing sizeof(arr) in same scope as declaration
UseReturns total bytes of array — works correctly. Use to compute element count: sizeof(arr)/sizeof(arr[0])
IfUsing sizeof(arr) in function parameter
UseReturns 8 (pointer size on 64-bit). Array parameter int arr[] is treated as int *arr. Cannot recover array size.
IfUsing &arr vs arr
Use&arr returns pointer to entire array (type int ()[5]). arr decays to pointer to first element (type int ). Different types, often same address.
IfUsing arr + 1 vs &arr + 1
Usearr + 1 advances by 1 element (4 bytes). &arr + 1 advances by whole array (20 bytes). Dangerous if misused.

Accessing Array Elements — The Most Dangerous Thing You'll Do Today

You access an array element with the subscript operator []. packets[0] gets the first element. packets[4] gets the fifth. That's the how. Here's the why it matters: C does zero bounds checking. Ask for packets[100] on a 5-element array, and the compiler happily reads memory that belongs to something else — another variable, a function pointer, another process's data. This isn't an exception. It's not undefined behavior you'll catch during testing. It's a landmine you step on in production at 3 AM.

The fix is discipline. Always validate your index against the array size before access. Use size_t for indices — it's unsigned and matches what sizeof returns. Never trust user input, network data, or computed offsets without an explicit bounds check. If you need safety, wrap the access in a function that aborts on out-of-range. Your future self will thank you when the segfault doesn't happen.

PacketBuffer.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// io.thecodeforge — c-cpp tutorial

#include <iostream>
#include <cstddef>

int main() {
    int packet_buffer[5] = {64, 128, 256, 512, 1024};
    size_t index = 3;

    // Bounds check before access
    if (index < sizeof(packet_buffer) / sizeof(packet_buffer[0])) {
        std::cout << "Packet size at index " << index << ": " << packet_buffer[index] << " bytes" << std::endl;
    } else {
        std::cerr << "ERROR: Index " << index << " out of bounds" << std::endl;
    }

    return 0;
}
Output
Packet size at index 3: 512 bytes
Production Trap:
The compiler will not warn you about out-of-bounds access on stack arrays. Static analysis tools (like Valgrind, AddressSanitizer) are your only safety net. Run them before every release.
Key Takeaway
Always bounds-check array indices. C trusts you. Don't make it regret that.

Updating Array Elements — It's Just a Memory Write

Updating an array element is a single assignment: sensor_readings[2] = 42;. That's it. No function call. No copy. Just a direct write to the memory address base_address + index * element_size. That speed is why C arrays are everywhere in embedded systems, game engines, and real-time audio — they're the rawest, fastest way to mutate a sequence of data.

But that speed has a sting. Because you're writing directly to memory, there's no protection against data races in multithreaded code. Two threads updating adjacent elements can cause cache line thrashing, killing performance. And nothing stops you from overwriting the wrong index, corrupting adjacent data. The rule: keep array updates in a single thread or use atomic operations. If you need thread-safe mutations, wrap the array in a mutex or switch to std::vector with proper synchronization. But for single-threaded hot paths, raw assignment is unbeatable.

SensorData.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — c-cpp tutorial

#include <iostream>

int main() {
    int sensor_readings[3] = {10, 20, 30};

    // Update the middle sensor value
    sensor_readings[1] = 25;

    // Print all readings
    for (size_t i = 0; i < sizeof(sensor_readings) / sizeof(sensor_readings[0]); ++i) {
        std::cout << "Sensor " << i << ": " << sensor_readings[i] << std::endl;
    }

    return 0;
}
Output
Sensor 0: 10
Sensor 1: 25
Sensor 2: 30
Senior Shortcut:
Use memcpy or std::copy for bulk updates instead of a loop — the compiler can vectorize them. Always profile before optimizing, though. Sometimes the loop is faster for small sizes.
Key Takeaway
Array updates are raw memory writes — fast, dangerous, and perfect for single-threaded hot paths.

C++ Array With Empty Members — The Uninitialized Landmine

You will see code where someone declares an array, slaps initializers on the first few elements, and walks away. They assume the rest are zero. They are correct — in C++. Partial initialization zero-fills the remaining members. That is a feature, not a bug. But it's also a trap.

The moment you rely on that default zero, you tie your logic to a specific initialization pattern. Change the initializer list and your zeros vanish. Worse: if you skip initialization entirely, those array slots hold whatever garbage was on the stack. You get undefined behavior. No warning. Just corruption three calls later.

Production takeaway: never assume array members are initialized unless you explicitly zero them. Use = {0} or std::fill to make intent obvious. Empty members are only safe when they are intentionally empty. Otherwise, you are debugging a ghost.

PartialInit.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — c-cpp tutorial

#include <iostream>

int main() {
    int partial[5] = {1, 2};         // members 2,3,4 are zero-initialized
    int garbage[5];                   // stack garbage, do not touch

    std::cout << "partial[2]: " << partial[2] << "\n";
    std::cout << "garbage[2]: " << garbage[2] << "\n"; // undefined behavior

    // Production: always zero init
    int safe[5] = {};                 // all zeros, guaranteed
    std::cout << "safe[0]: " << safe[0] << "\n";
    return 0;
}
Output
partial[2]: 0
garbage[2]: 12453456
safe[0]: 0
Production Trap:
Partial initialization zeros the tail, but only at declaration. Reassigning later leaves untouched members unchanged. Always initialize the full array to zero before use.
Key Takeaway
A partially initialized C++ array zeros the remaining members — but only if you initialize at declaration. After that, you own the memory.

C++ Arrays Are Not Complete Types by Default — The Compiler Knows

When you write int nums[] = {10, 20, 30};, the compiler counts the elements for you. That is convenient. But it also means the array size is deduced from the initializer — and you cannot change it later. The type of nums becomes int[3], not "some array of unknown length". This matters at compile time.

If you declare int nums[]; without an initializer, the compiler rejects it. Incomplete arrays have no size and no storage. You cannot pass them to functions without a size parameter. The moment you let the compiler deduce the size, you are locked into that exact length. There is no resizing, no dynamic growth.

Production advice: for fixed-size data, let the compiler count. For arrays that grow, use std::vector. Don't fight the type system. An array with empty members — size unknown — is a declaration with no storage. The compiler will laugh at you.

ArraySize.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — c-cpp tutorial

#include <iostream>

int main() {
    int known[] = {10, 20, 30};          // size = 3, deduced
    // int unknown[];                     // ERROR: incomplete type

    std::cout << "size: " << sizeof(known)/sizeof(known[0]) << "\n";

    // Array decays to pointer when passed
    auto ptr = known;                     // ptr is int*, size info lost
    std::cout << "ptr[1]: " << ptr[1] << "\n";
    return 0;
}
Output
size: 3
ptr[1]: 20
Senior Shortcut:
Let the compiler count elements. Use sizeof(arr)/sizeof(arr[0]) for iteration. For dynamic arrays, drop the C array entirely and use std::array or std::vector.
Key Takeaway
An array with no explicit size is an incomplete type — it has no storage and cannot be used until the compiler deduces its size from an initializer.
● Production incidentPOST-MORTEMseverity: high

The Firmware Crash That Happened Only on Tuesdays

Symptom
Robot arm worked fine Monday, Wednesday, Thursday, Friday. On Tuesdays, it would crash anywhere between 10 AM and 2 PM, never at the same time. The crash corrupted unrelated memory: the emergency stop flag would randomly trigger, or joint angles would report impossible values. No memory leak in the logs, no heap corruption. The only pattern was 'Tuesday'.
Assumption
The team assumed a hardware fault on the Tuesday operator's station. They swapped controllers, cables, even the robot arm. The problem followed the software, not the hardware. They spent 6 weeks RMA-ing components before looking at the code's array bounds.
Root cause
A sensor reading array was declared as int readings[10]. The Tuesday shift operator's sensor had slightly higher output range, occasionally returning an 11th value. The code wrote readings[10] = value because the loop condition was for (int i = 0; i <= 10; i++) (<= instead of <). This wrote one int (4 bytes) past the end of the array, corrupting the adjacent variable in memory — which happened to be the emergency stop flag on some days, joint angle parameters on others. The crash was non-deterministic: the location of the overflow depended on stack layout, compiler optimizations, and even the phase of the moon metaphorically. The Tuesday operator's jacket caused static discharge that changed the starting address of the stack frame by a few bytes, shifting which variable was corrupted. The team spent 6 weeks chasing a ghost.
Fix
1. Fixed the loop condition: for (int i = 0; i < 10; i++) (strictly less than, never <=). 2. Added bounds assertion: assert(index >= 0 && index < ARRAY_SIZE); in debug builds. 3. Used static analysis tool: cppcheck --enable=all caught the buffer overflow at compile time. 4. Switched to using ARRAY_SIZE macro: #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) and loop with for (int i = 0; i < ARRAY_SIZE(readings); i++). 5. Added a canary value (sentinel) at the end of the array and checked it before accessing.
Key lesson
  • C arrays have no bounds checking. int arr[5] has valid indexes 0..4. Index 5 does NOT exist and compiles anyway — with silent memory corruption.
  • Off-by-one errors (using <= instead of <) are not style issues; they are security vulnerabilities that can corrupt memory silently for months before crashing.
  • Use static analysis tools (cppcheck, clang-static-analyzer, Coverity) on every build. They catch buffer overflows that human review misses.
  • In embedded systems, memory corruption can be non-deterministic — the same bug may crash at wildly different times depending on stack layout, optimizations, and even environmental factors like temperature.
Production debug guideSymptom → Action mapping for common array failures in production C code.5 entries
Symptom · 01
Segmentation fault or HardFault (ARM) at runtime — crashes randomly
Fix
Likely buffer overflow: writing past array bounds corrupts adjacent memory. Use valgrind: valgrind --tool=memcheck ./program. Look for 'Invalid write of size X'. Add bounds assertions in debug mode. Enable stack canaries: -fstack-protector-all in GCC.
Symptom · 02
sizeof(arr) inside function returns 8 (pointer size) instead of array size
Fix
Array decayed to pointer. The function received int arr[] which is equivalent to int *arr. Use sizeof only in the same scope where array was declared. Pass length as separate parameter: void process(int arr[], int length)
Symptom · 03
Array values are garbage — large unexpected numbers
Fix
Array was declared but not initialized. Local arrays are not zero-initialized automatically. Use int arr[10] = {0}; to zero all elements, or explicitly fill with loop.
Symptom · 04
Segfault when accessing arr[0] — first element crashes
Fix
Array pointer is NULL. Check if arr was passed as NULL from caller. Add null check: if (arr == NULL) return -1;
Symptom · 05
Array elements appear shifted — arr[0] has value that should be in arr[1]
Fix
Off-by-one in insertion or deletion: loop started at 1 instead of 0, or ended at length instead of length-1. Trace array state before and after the operation.
★ C Array Debug Cheat SheetFast diagnostics for array issues in production C/C++ code. Run these commands before blaming hardware.
Segmentation fault — suspected buffer overflow
Immediate action
Run with AddressSanitizer to catch out-of-bounds access
Commands
gcc -fsanitize=address -g program.c -o program
./program 2>&1 | grep -A10 'ERROR: AddressSanitizer'
Fix now
Add bounds checks: if (index >= 0 && index < ARRAY_SIZE) { ... } else { / error / }
sizeof returning 8 instead of expected array size+
Immediate action
Check where sizeof is called — if inside function with array parameter, it's a pointer
Commands
echo 'void f(int arr[]) { printf("%zu\n", sizeof(arr)); }'
grep -n 'sizeof(' src/*.c | grep -v 'sizeof.*\[.*\]'
Fix now
Pass length as separate parameter: void process(int *arr, size_t len). Use macro ARRAY_SIZE in same scope: #define ARRAY_SIZE(x) (sizeof(x)/sizeof((x)[0])).
Array contains garbage values — not zeros+
Immediate action
Check if array was initialized
Commands
grep -n 'int\s\+\w+\[' src/*.c | grep -v '='
valgrind --track-origins=yes ./program | grep 'Uninitialised value'
Fix now
Initialize: int arr[10] = {0}; (zeros all elements). For static/global, they are zero-initialized automatically.
Cannot calculate array length in function — always 2 or 1+
Immediate action
Array decayed to pointer; sizeof returns 8 (pointer) / 4 (int) = 2
Commands
gcc -Wall -Wextra program.c 2>&1 | grep -i 'sizeof.*array.*function'
echo 'Fix: pass length as separate argument'
Fix now
In the caller: process(arr, sizeof(arr)/sizeof(arr[0]));. In the callee: void process(int arr[], size_t len)
Array index out of bounds — no error, just wrong data+
Immediate action
Check loop boundaries — likely using <= instead of <
Commands
grep -n 'for.*<=\|for.*>=' src/*.c
cppcheck --enable=all src/*.c | grep -i 'array.*bound'
Fix now
Change for (int i = 0; i <= size; i++) to for (int i = 0; i < size; i++). Always use < for upper bound.
C Arrays vs Other Data Structures
Feature / AspectC Array (int[])Pointer + mallocLinked List (struct node*)
Size flexibilityFixed at compile timeDynamic (realloc)Dynamic (add/remove nodes)
Memory allocationStack (auto) or globalHeap (manual malloc/free)Heap (per node)
Access by indexO(1) — address calculationO(1)O(n) — traverse list
Insert at beginningO(n) — shift all elementsO(n) — shift or reallocO(1) — just rewire head
Insert at endOnly if preallocated spaceO(1) amortised (realloc)O(1) with tail pointer
Memory overhead per element0 bytes (values inline)0 bytes (values inline)~16 bytes (two pointers)
Cache localityExcellent — contiguousExcellent — contiguousPoor — scattered nodes
Requires length tracking?Yes — no built-in lengthYes — caller tracks sizeYes — traverse to count
Best forFixed-size, high-performance, embeddedVariable size, large datasetsFrequent insert/delete at arbitrary positions

Key takeaways

1
C arrays are zero-indexed
the first element is at index 0 and the last valid index is always size minus one. Accessing index equal to the size is undefined behavior and one of C's most common bugs.
2
There is no bounds checking
int arr[5]; arr[5] = 42; compiles and runs, corrupting adjacent memory silently. Use static analysis and runtime assertions.
3
Array names decay to a pointer to the first element when passed to functions
which means sizeof inside a function gives you pointer size (8 bytes), not the array size. Always pass length as a separate parameter.
4
sizeof(array) / sizeof(array[0]) is the correct way to calculate element count
but only in the same scope where the array was declared. Store the result in a named variable before your loop.
5
Arrays guarantee contiguous memory
all elements sit side-by-side in RAM. This makes index access O(1) via simple arithmetic and makes arrays the fastest data structure for positional access in the entire C language.

Common mistakes to avoid

5 patterns
×

Off-by-one error — accessing index equal to the array size

Symptom
For an array int data[5], writing data[5] goes one past the end. C won't stop you and won't throw an error — it'll read whatever bytes happen to be in memory after the array. Your program might crash, produce wrong output, or appear to work fine until it doesn't.
Fix
Always use index < length (strictly less than) as your loop condition, never index <= length. For loops: for (int i = 0; i < size; i++). For direct access, assert: assert(index >= 0 && index < size);
×

Using sizeof inside a function to get array length

Symptom
Writing int count = sizeof(numbers) / sizeof(numbers[0]) inside a function that received the array as a parameter gives you the pointer size (8 bytes on 64-bit systems), not the array size. The result will be 2 or 1 instead of the correct count.
Fix
Always pass the array length as a separate int parameter from the caller, where sizeof does give you the full array size. Declaration: void process(int arr[], size_t len). Call: process(arr, sizeof(arr)/sizeof(arr[0]));
×

Forgetting to initialize array elements

Symptom
Declaring int scores[5]; without an initializer list leaves all 5 slots holding garbage values (whatever bits happened to be in that memory). If you then loop through and print or use them, you'll get random large numbers.
Fix
Either use an initializer list int scores[5] = {0}; (which zeroes all elements), or explicitly assign every element before you read from it. For large arrays, use a loop: for (int i = 0; i < N; i++) arr[i] = 0;
×

Assuming array is copyable by assignment

Symptom
Writing int b[5] = a; where a is another array of the same size. C does not copy arrays by assignment — this is a syntax error or decays to pointer assignment.
Fix
Use memcpy(b, a, sizeof(a)); (include <string.h>). Or loop copying each element: for (int i = 0; i < 5; i++) b[i] = a[i];
×

Using `arr++` on array name

Symptom
Writing arr++ where arr is array name (not pointer). Compiler error: 'lvalue required as increment operand'. Array name cannot be modified.
Fix
Create a pointer: int *p = arr; then p++;. The array name is constant address; pointer variable can be incremented.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between an array's name and a pointer in C, and i...
Q02SENIOR
If I pass an array to a function and use sizeof inside that function to ...
Q03SENIOR
An array int buffer[10] is declared but not initialized. A colleague say...
Q04SENIOR
How would you design a bounds-checked array in C for a safety-critical e...
Q01 of 04SENIOR

What is the difference between an array's name and a pointer in C, and in what situations does the array name not decay to a pointer?

ANSWER
An array's name is the address of the first element and is constant — cannot be changed (you cannot do arr++). A pointer is a variable that holds an address and can be incremented. In most contexts, the array name decays to a pointer (e.g., when passed to a function). The three situations where it does NOT decay are: (1) as an operand of sizeofsizeof(arr) gives total array bytes, not pointer size; (2) as an operand of &&arr gives pointer to entire array (type int (*)[5]); (3) as a string literal initializer for a character array — char str[] = "hello"; copies the string, doesn't decay to pointer. Understanding decay is critical for correct sizeof usage and pointer arithmetic.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the size limit of an array in C?
02
Can you change the size of an array in C after declaring it?
03
Why does C array indexing start at 0 instead of 1?
04
How do I copy one array to another in C?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

7 min read · try the examples if you haven't

Previous
Functions in C
6 / 17 · C Basics
Next
Strings in C