1784916062

The Confusion Every C Programmer Has Had (and No One Explains Right)


There's a question every C programmer has asked at some point, usually around 2 a.m., staring at an unexplained segfault: "isn't an array basically just a pointer?". And the annoying thing is that the answer is sort of yes, sort of no — exactly the kind of answer nobody wants when they just need the code to compile. Let's go straight to the example that causes the most confusion. Take this: ```c #include <stdio.h> void print_size(int arr[]) { printf("Inside the function: %zu\n", sizeof(arr)); } int main() { int numbers[10]; printf("In main: %zu\n", sizeof(numbers)); print_size(numbers); return 0; } ``` Run this and you'll see two completely different numbers. In `main`, `sizeof(numbers)` gives you the actual size of the array — 40 bytes, if `int` is 4 bytes on your machine. Inside `print_size`, though, `sizeof(arr)` gives you the size of a pointer, something like 8 bytes on a 64-bit machine. Same variable name, different result, and this is usually the point where people give up trying to understand and just memorize "okay, never trust sizeof on an array passed as a parameter." The name for this is array-to-pointer decay, and it's worth pausing to actually understand it instead of just memorizing the rule. When you declare `int numbers[10]` inside `main`, the compiler reserves a contiguous block of memory to hold the 10 integers, and the variable `numbers` represents that whole block — that's why `sizeof` behaves correctly there. But the moment you pass that array as an argument to a function, C doesn't copy the whole array (imagine the waste). Instead, it passes just the address of the first element, and inside the function that parameter effectively becomes a regular pointer. `int arr[]` as a function parameter is really just a nicer-looking way of writing `int *arr` — the compiler treats both exactly the same. This explains a bunch of things. For instance, why `arr[i]` and `*(arr + i)` are literally the same instruction to the compiler. Array indexing in C is, quite literally, syntactic sugar for pointer arithmetic — `arr[i]` is defined as `*(arr + i)`, which is also why you can pull off the classic party trick of writing `i[arr]` and having it compile just fine (don't actually do this in production, but it's fun to show someone who's learning). ```c int arr[5] = {10, 20, 30, 40, 50}; printf("%d\n", arr[2]); // 30 printf("%d\n", *(arr + 2)); // 30, exactly the same printf("%d\n", 2[arr]); // also 30, yes, this compiles ``` But decay doesn't happen everywhere, and that's where the second layer of confusion lives. There's a handful of situations where the array is still treated as a real array, not decayed into a pointer. `sizeof` is one, as we just saw. `&arr` is another: the address of the whole array has a different type than the address of the first element, even though the numeric value is the same. ```c int arr[5]; printf("%p\n", (void*)arr); // address of the first element printf("%p\n", (void*)&arr); // address of the whole array // both print the same address, but &arr has type int(*)[5], // while arr alone decays to int* ``` Initializing an array directly with a string literal doesn't count as decay either — `char name[] = "Claude"` creates a real array, with space reserved for each character plus the `\0`, and you're free to modify its contents afterward. Compare that with this: ```c char name[] = "Claude"; // array, mutable char *nickname = "Claude"; // pointer to a literal, modifying is undefined behavior ``` Both look like they're doing the same thing, both will print "Claude" if you run a `printf` on them, but trying `nickname[0] = 'K'` in the second case is dangerous territory — string literals usually live in a read-only section of memory, and the compiler isn't required to warn you at compile time. Meanwhile `name[0] = 'K'` in the first case is perfectly valid, because you have a real array sitting on the stack. One thing that really helps this stick is thinking in terms of who "owns" the memory. An array owns the space — when you declare `int numbers[10]` on the stack, those 40 bytes exist right there, contiguous, and the variable `numbers` is basically an alias for that fixed address. A pointer, on the other hand, is a separate variable, with its own reserved space (typically 8 bytes on a 64-bit system), that stores the address of something else. That's why you can reassign a pointer (`ptr = other_address`), but you can't reassign an entire array once it's declared — the array's name isn't a modifiable lvalue, so trying `numbers = other_array` simply won't compile. ```c int a[5] = {1,2,3,4,5}; int b[5] = {6,7,8,9,10}; // a = b; // compile error, arrays can't be assigned like this int *p = a; p = b; // this works fine, p now points to b ``` It's worth giving multidimensional arrays their own moment, because that's where the confusion gets a notch harder. An `int matrix[3][4]` isn't an array of pointers — it's a single contiguous block of 12 integers, laid out in a way the compiler knows how to calculate the right offset for `matrix[i][j]`. That's quite different from an array of pointers like `int *matrix[3]`, where each `matrix[i]` is a separate pointer that can point to completely different, non-contiguous blocks of memory. Mixing up these two is a classic source of bugs in code that deals with strings (like `char *argv[]` in `main`, which IS an array of pointers, each pointing to a string living somewhere different in memory). And here it's probably worth a small detour for anyone coming from modern C++ who's thinking "but I use `std::vector` and `std::array`, does any of this still matter?". It does, especially if you touch embedded systems, legacy code, or anything that interacts with plain C (drivers, older libraries, OS-level APIs). `std::array<int, 10>` solves a good chunk of the pain because it carries its size along and doesn't suffer from decay the same way — passing a `std::array` by reference keeps `sizeof`/`.size()` working as expected. But underneath it all, understanding this relationship between arrays and pointers is still what separates someone who just uses C++ from someone who actually understands what's going on when things break. A good way to close out this kind of content is to leave the reader with a prediction challenge before they run the code. For example: ```c void my_function(int arr[10]) { printf("%zu\n", sizeof(arr)); } int main() { int numbers[10]; printf("%zu\n", sizeof(numbers)); my_function(numbers); return 0; } ``` Question: does writing `int arr[10]` in the parameter, instead of `int arr[]`, change anything from the first example? (Spoiler for anyone checking after trying it: no, nothing changes — the compiler ignores the declared size in the parameter and treats it as a pointer regardless. The `10` there is purely decorative, at most useful as documentation for whoever reads the code later.) At the end of the day, arrays and pointers aren't the same thing, but they're not enemies either — they're two ways of dealing with contiguous memory that happen to share a lot of syntax, mostly because C was deliberately designed that way, to stay close to the hardware. Once you understand where the equivalence stops and where it starts, the rest of C starts making a lot more sense.

(1) Comments
JavaJuggler
JavaJuggler
1784918276

There! Now I know just how little I know about C++.


Welcome to Chat-to.dev, a space for both novice and experienced programmers to chat about programming and share code in their posts.

About | Privacy | Donate
[2026 © Chat-to.dev]