First I commend you for asking the question and trying to affirm your understanding of the language and what you are learning. It is admirable and requires confidence and a degree of bravery. And it shows to me you are not just trying to solve the questions but truly learn the material and understanding of the course. This makes me confident that you will, and that you are on your way to success in this field. This is far and away one of the better questions on this forum I have seen in weeks, and I wish I could give you more than one upvote for asking it.
TLDR short version
Your first point is 100% right: the variable name of an array in C decays to being a pointer to the first element the array. But this is completely unrelated from the rest of what you talk about.
You are kinda right in regard to argument passing but also kinda not right. Remember it extends beyond just arrays, but to structures
as well. In fact they needed a system to pass EVERY type of variable. If you pass an integer
to a function by value for example, none of the changes are made to the original variable, the changes made in the function are function local and are made on a copy of the variable. If you want the changes to be visible outside the function and avoid the copy, you pass by address/pointer. The copy is avoided and the actual variable is modified. If you can only pass by a pointer, then you can use the const keyword in one of two places to denote that either the pointer cannot be changed or what it points to can not be changed. But what can and should you pass for structs and arrays? Well the only thing you have is generally the address of the struct or array generally speaking.
Full Answer
Nothing in C is 'passed by reference', that is a C++ concept. In C you can pass by value or 'pass by address'. This is subtly different from 'pass by reference' and some folks in C who have never gone into C++ do use this terminology but you should be aware that there is a difference. But for the sake of your question we can say that you have the gist of that correct.
In C the variable of an array decays into a pointer to the array because that is effectively what it is to start with; it's just that you don't have to malloc the memory, and you can't change it later (you can only change the elements of the array). What else could the variable name itself "do"? It has to point to something does it not? This just is. It's a mildly confusing thing which is why CS50x avoids the subject with the string
class early on instead of exposing you to the char *
array that it really is. This property has nothing to do with the second area you discuss. Effectively creating what is often called a c-style array
you are really creating a static array
and what's happening under the hood for <type> foo[4]
is creation of const <type> *foo = malloc_from_stack(sizeof(<type>) * 4);
Thus foo
and &foo[0]
have the same address, and with the caveat that this is a stack variable and when out of scope will be automatically free'd just like a non-pointer. Notice the leading const
which means that foo
can't be reassigned to a point to a different address, but foo's contents can be changed. foo
is permanently typed to by pointer to array of 4
. It should be somewhat self evident the reason for this is because if you reassigned foo
to point to something else it would create leakage problems for its memory. (point to something else as in changing it later to be foo = int[5]
or foo = malloc(sizeof(<type> * <any number including 4>)
becuase then what should happen to foo's original stack memory? They didn't want to open up this rabbit hole. If you want to be able to treat foo dynamically it has to be just a pointer from the beginning.
Your understanding regarding passing arguments is only mildly incorrect. While the need for efficiency is one piece, it also has to do with ability to be precise and limitations of the language combined with a need to be able to pass all kinds of different variables: built-in (struck atomic) types such as int
, c-style arrays like int[]
, pointers to types like char *
, and finally and perhaps most importantly passing 'structures' (I don't want to call them objects and conflate the term here). The need for efficiency is just one of the many reasons for this. Ultimately they are mimicking the way the assembly instructions would work. This is also handy because what if you don't want to pass the whole array, what if I just want to pass from the 5th element on? Now I don't need a copy, I just pass function(&foo[4], length)
where length is often how many more elements are present (often used for non-null terminated cases).
There are times where you want to make a copy and times where you don't, there are times where you want to build or populate the variable/structure or pointer and times where you don't. There are times where you want to modify what the pointer points to and times where you do not. C is not C++, it is a low level language and does not support overriding and defining many of the operations that would be needed in order to make custom structures properly assignable/copyable/etc which would be needed for being passed by value to other functions. For example if you defined a struct foo
which had both pointer members and non-pointer members, which ones should be copied over (deep copy) and which ones should not (and how do we define this) if you passed it by value (or even assignment)? C deliberately doesn't give you a good way to define this. C kept things simpler, more predictable, straight forward and user definable. They left the copying to be done externally by the user, because it's a low level language, and the concept was the user would know whether they wanted to do a copy or not, and also because it is more akin to the way the assembly language would work itself.
By passing a pointer to the variable, we mimic what the assembly would do, and we sidestep having to have the language define any of these behaviors, which makes the language simpler and more deterministic, which in turn makes the compiler simpler and the optimization simpler.
In C++ this is an area of many pieces of change through the years in terms of context, optimization and evolving practices. We had the rule of 3, and now the rule of 5/0, we now have r-values, move-assignment, emplace-back and we also have copy-ellision and return-value-optimization, and all sorts of other quirks. If your head is spinning you are not to blame. Its complicated and it has vastly complicated optimization where taking certain actions can prevent other optimizations from occurring. Since C++11 a once mildly complicated area became drastically more complicated. Practices of 'always pass by reference or const-reference' have quite often been shifted to pass by value or by r-value or by reference or pointer... and I'll spare you the headache of going deeper here. It's a real rabbit hole
This is one of the many reasons for the saying "C makes it easy to shoot yourself in the foot, C++ makes it a harder but when you do you blow your leg off".
I hope that helps and makes sense. Best of luck to you on your journey