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.
- ✓Basic programming fundamentals
- ✓A computer with internet access
- ✓Willingness to follow along with examples
- 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.C23: Array Parameter with static Keyword
The C23 standard introduced a new way to specify array parameters using the static keyword, which enables compilers to perform bounds checking and optimization. When you declare a function parameter as int arr[static 5], you are telling the compiler that the array argument must have at least 5 elements. This is a contract between the caller and the callee: the caller guarantees that the array is at least that size, and the compiler can use this information for optimizations like loop unrolling or vectorization. If the caller passes a smaller array, the behavior is undefined. This feature is particularly useful for functions that expect a minimum array size, as it catches potential buffer overflows at compile time. For example, a function that sums the first 5 elements of an array can declare the parameter with static 5, and the compiler can warn if the argument is too small. Note that this is a C23 feature and may not be supported by all compilers yet; GCC and Clang have experimental support. It does not change the actual type of the parameter (arrays still decay to pointers), but it adds a semantic constraint that the compiler can enforce.
static in array parameters for functions that require a minimum buffer size; it documents the contract and allows compilers to catch violations early.static keyword in array parameters enforces a minimum array size contract, enabling compiler optimizations and safer code.Variable-Length Arrays: C99 Feature and Compiler Support
Variable-length arrays (VLAs) are a feature introduced in C99 that allows arrays whose size is determined at runtime. For example, int n; scanf("%d", &n); int arr[n]; creates an array of size n on the stack. VLAs are useful for avoiding dynamic memory allocation when the size is known only at runtime but is not too large. However, VLAs have several drawbacks: they can cause stack overflow if the size is large, they are not supported in C++ (though some compilers allow them as extensions), and they were made optional in C11. Many compilers support VLAs, but Microsoft's MSVC does not. In embedded systems, VLAs are often discouraged because stack sizes are limited. VLAs can also be used in function parameters: void func(int n, int arr[n]) allows the array size to be passed separately. This can improve readability and enable bounds checking with tools like -fsanitize=bounds. Despite their convenience, VLAs are controversial; the Linux kernel, for instance, bans them. For production code, consider using dynamic allocation or fixed-size arrays unless you are certain the size is small and stack space is ample.
Multidimensional Arrays as Flat Buffers
Multidimensional arrays in C are stored in row-major order, meaning all rows are laid out contiguously in memory. This allows you to treat a 2D array as a flat 1D array, which can be more efficient for certain operations. For example, int matrix[3][4] can be accessed as int flat = (int)matrix; and then indexed as flat[i 4 + j]. This flat buffer approach is useful when you need to pass the array to functions that expect a contiguous block of memory, or when performing operations like memset or memcpy. It also avoids the overhead of pointer-to-pointer structures used in dynamic 2D arrays. However, be careful with the indexing: the formula is row num_cols + col. This technique is common in graphics programming, scientific computing, and embedded systems where memory layout matters. One pitfall is that the compiler may not optimize the flat indexing as well as the native 2D indexing, so profile if performance is critical. Also, when using flat buffers, you lose the type safety of multidimensional array types, so ensure you pass the dimensions correctly.
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 / }| File | Command / Code | Purpose |
|---|---|---|
| io | int main() { | Declaring and Initializing Your First C Array |
| io | int main() { | Looping Through an Array |
| io | int main() { | How Arrays Live in Memory |
| PacketBuffer.cpp | int main() { | Accessing Array Elements |
| SensorData.cpp | int main() { | Updating Array Elements |
| PartialInit.cpp | int main() { | C++ Array With Empty Members |
| ArraySize.cpp | int main() { | C++ Arrays Are Not Complete Types by Default |
| static_array_param.c | int sum_first_five(int arr[static 5]) { | C23 |
| vla_example.c | void print_array(int n, int arr[n]) { | Variable-Length Arrays |
| flat_buffer.c | void print_flat(int* arr, int rows, int cols) { | Multidimensional Arrays as Flat Buffers |
Key takeaways
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?
8 min read · try the examples if you haven't