C Arrays — One <= Corrupted Memory for 6 Weeks
A loop using <= instead of < wrote past int[10], corrupting emergency flags non-deterministically for weeks.
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
- 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)
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.
sizeof() gives the pointer size, not the array length — this is the #1 source of buffer overflows in 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.
int data[5]; data[5] = 42; compiles and runs. It corrupts adjacent memory silently.assert(index >= 0 && index < ARRAY_SIZE) in debug builds, and validate all indexes in production.for (int i = 0; i < array_size; i++). Never <=.int arr[] = {1, 2, 3};. Compiler sets size automatically. Best for lookup tables, configuration data.int arr[10] = {0};. First element explicitly zeroed, rest default-initialized to zero. Guarantees zero-filled array.int arr[10];. Elements contain garbage until assigned. Must fill before reading.static int arr[10]; contains zeros. No need for explicit ={0}.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.
sizeof(arr)/sizeof(arr[0]) only works in the scope where the array was declared, not after it decays to a pointer.int arr[N])sizeof(arr) / sizeof(arr[0]). Computed at compile time, zero runtime cost.void f(int arr[]))void f(int arr[], size_t len). Caller passes len computed via sizeof trick.#define ARR_SIZE 10. Use that constant for both array declaration and loop bounds. No runtime calculation.int arr = malloc(n sizeof(int)); then keep n alongside the pointer.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.
sizeof gotcha, but also the source of pointer arithmetic: arr + 2 points to the third element.memcpy(arr1, arr2, sizeof(arr1)) — no loop needed to copy elements.&arr only when you need pointer to the whole array.int *p = arr;)p now points to &arr[0]. This is automatic, no & needed.sizeof(arr) in same scope as declarationsizeof(arr)/sizeof(arr[0])sizeof(arr) in function parameterint arr[] is treated as int *arr. Cannot recover array size.&arr vs arr&arr returns pointer to entire array (type int ()[5]). arr decays to pointer to first element (type int ). Different types, often same address.arr + 1 vs &arr + 1arr + 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.
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.
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.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.
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.
sizeof(arr)/sizeof(arr[0]) for iteration. For dynamic arrays, drop the C array entirely and use std::array or std::vector.The Firmware Crash That Happened Only on Tuesdays
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.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.- 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.
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.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)int arr[10] = {0}; to zero all elements, or explicitly fill with loop.arr was passed as NULL from caller. Add null check: if (arr == NULL) return -1;gcc -fsanitize=address -g program.c -o program./program 2>&1 | grep -A10 'ERROR: AddressSanitizer'if (index >= 0 && index < ARRAY_SIZE) { ... } else { / error / }Key takeaways
Common mistakes to avoid
5 patternsOff-by-one error — accessing index equal to the array size
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
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.void process(int arr[], size_t len). Call: process(arr, sizeof(arr)/sizeof(arr[0]));Forgetting to initialize array elements
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.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
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.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
arr++ where arr is array name (not pointer). Compiler error: 'lvalue required as increment operand'. Array name cannot be modified.int *p = arr; then p++;. The array name is constant address; pointer variable can be incremented.Interview Questions on This Topic
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?
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 sizeof — sizeof(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.Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.
That's C Basics. Mark it forged?
7 min read · try the examples if you haven't