r/C_Programming 12h ago

Question Working with arrays in functions

Hey everybody. I’m a beginner to C and I was writing some functions today to get used to doing things. I tried to write binary search and bubble sort. I tried to pass in an array as an argument to the functions, but the compiler gave me a bunch of warnings. I looked it up and I saw that passing in an array is the same as passing in its pointer. I haven’t touched pointers yet, but I have two questions:
1. If I dereferenced the pointer to an array, wouldn’t that return the same as indexing the first value?
2. If I wanted to pass in the entire array, could I do that by passing in the pointers of both the first and last elements and using pointer arithmetic to access the other elements? What’s the idiomatic way of doing this?

6 Upvotes

27 comments sorted by

13

u/Total-Box-5169 12h ago

Either learn about pointers because arrays decay into pointers when passed to functions, or use a struct to wrap the array and pass the struct.
Note that you will have to learn about pointers eventually.

1

u/FloridianfromAlabama 11h ago

I’m not avoiding pointers, I just haven’t had to deal with them until now. Most of my programming experience is in Java.

2

u/MFFVD 11h ago

pointers are horrible until you understand them. then they are easy. try writing some stack-based assembly, that helped me to understand it better.

you have ``` push <value> push <label> // pushes address of label load // pops address and fetches that byte from memory store // pops address, pops value and stores val at addr

jmp <label> // unconditional jump jmz <label> // pop and jump if zero <label>:

add // pops 2, adds them and pushes the result sub // pops 2, subtracts and pushes result ```

it basically comes down to

``` ADDR : // array with 5 elements a b c d e push ADDR // pushes address of ADDR, in this example 0. // in c you cant rely on that because // adresses are different // stack is now [0]

load // pops address and pushes the value stored there stack is now [a]

push ADDR push 1 // stack is now [a, 0, 1] add // now [a, 1] load // [a, b] because b is stored at address 1 add // [a+b] push ADDR // [a+b, 0] store // stack empty, ADDR= [a+b, b, c, d, e] ``` ADDR+0 is where a is stored. ADDR+1 is where b is stored, and so forth for the rest of the array

char a[10] => a=0 a[0] => *(a+0) a[1] => *(a+1)

a[] compiles to the same assembly as a*, but the C interface is different.

4

u/SmokeMuch7356 11h ago

An array is just a sequence of objects in memory: declaring

int a[4];

gives you

       +---+
0x8000 |   | a[0]  // addresses are made up and the points don't matter
       +---+
0x8004 |   | a[1]
       +---+
0x8008 |   | a[2]
       +---+
0x800c |   | a[3]
       +---+

That's it.  There's no separate object a storing the starting address anywhere, size is not stored anywhere, etc.

Under most circumstances, an array expression will "decay" to a pointer to the first element. IOW, when the compiler sees the array expression a, it replaces it with something equivalent to &a[0]. The only exceptions to this rule are when the array expression is the operand of the sizeof, typeof or unary & operators, or is a string literal used to initialize the contents of a character array.

When you write something like;

sort( a );

the sort call gets mutated to something equivalent to

sort( &a[0] );

and what sort actually receives is a pointer:

void sort( int *a ) { ... }

The upshot of this is that you can't pass an array "by value";1 you don't get a local copy of the array in the function, you just get a pointer to the array in the caller.

This is how subscripting is defined, btw - a[i] == *(a + i). Given the starting address provided by a, offset i elements and dereference the result. Again, a doesn't store a pointer, it evaluates to a pointer. However, this means you can apply the [] subscript operator to actual pointer objects as well.

Normal practice is to pass the array's size as a separate argument:

void sort( int *a, size_t size )
{
  for ( size_t i = 0; i < size - 1; i++ )  
    for ( size_t j = i+1; j < size; j++ )
      if ( a[j] < a[i] )                 // a is a pointer, not an array, but
        swap( &a[i], &a[j] );            // we can subscript it as though it were
}

In the context of a function parameter declaration, T a[N] and T a[] will be "adjusted" to T *a; all three declare a as a pointer to T. This is not the case for a regular variable declaration.

The expressions a, &a, and &a[0] will all yield the same address value (0x8000 in our example above, modulo any type conversions), but the types will be different (int *, int (*)[N], and int *, respectively).

Some handy rules:

Declaration: T a[N]; // for any type T

Expression        Type         "Decays" to    Equivalent expression
----------        ----         -----------    ---------------------
         a        T [N]        T *            &a[0]
        &a        T (*)[N]     n/a            n/a
        *a        T            n/a            a[0], *(a + 0)
      a[i]        T            n/a            *(a + i)

Declaration: T a[N][M];

Expression        Type         "Decays" to    Equivalent expression
----------        ----         -----------    ---------------------
         a        T [N][M]     T (*)[M]       &a[0]
        &a        T (*)[N][M]  n/a            n/a
        *a        T [M]        T *            a[0], *(a + 0)
      a[i]        T [M]        T *            *(a + i)
     *a[i]        T            n/a            a[i][0]
   a[i][j]        T            n/a            *(*(a + i) + j)


Declaration: T a[N][M][L];

Expression        Type           "Decays" to    Equivalent expression
----------        ----           -----------    ---------------------
         a        T [N][M][L]    T (*)[M][L]    &a[0]
        &a        T (*)[N][M][L] n/a            n/a
        *a        T [M][L]       T (*)[L]       a[0], *(a + 0)
      a[i]        T [M][L]       T (*)[L]       *(a + i)
     *a[i]        T [L]          T *            a[i][0]
   a[i][j]        T [L]          T *            *(*(a + i) + j)
  *a[i][j]        T              n/a            a[i][j][0]
a[i][j][k]        T              n/a            *(*(*(a + i) + j) + k)

The pattern for higher-dimensioned arrays should be apparent from here.


  1. At this point someone brings up the "hide it in a struct type" cheat:

    struct foo { int a[N]; } bar;
    ...
    sort( bar );
    

    Yes, you get a local copy of the struct object, which means you get a local copy of the array. I have never seen this used anywhere in production code; it's a cute trick, but nobody uses it.

3

u/No-Experience-3171 12h ago
  1. yes

  2. Typically you'd pass a pointer to the array and the size of the array (i.e. how many elements it has) as a separate argument, so arr[0] is the lower bound and arr[size - 1] is the upper bound.

2

u/MagicWolfEye 12h ago

1 yes 2 you typically pass a length and then the pointer to the array

2

u/duane11583 11h ago

The c language does not have a complex type like a list or array that c++ or other languages have

So to answer your questions:

1) yes the [0]th element is the same as de-referencing  pointer

2) passing an array by value is often a very stack hungry operation that is fraught with stack overflow errors hence it is not done. 

On the other hand if you had a complex type (struct) these tend to be small and are easily passed by value because the compiler will create (allocate) space on the stack and copy the struct

That said if the struct is large you may well blow up your stack if you are not careful

It’s your foot and your foot-gun happy aiming

Edit goofed value/reference fixed

2

u/mc_pm 12h ago

The value of the variable for the array *is* a pointer to it's first memory location. So, yes, derefrencing the pointer will return the first element of the array. In C, accessing an array is actually doing pointer arithmetic.

3

u/flyingron 11h ago

No, array values are definitely NOT pointers. Please read the first comment I wrote in this post. Accessing array values however is doing pointer arithmatic because there is no such thing as applying a [] operator to an array. It only works for pointers. You are just invoking the implicit array-to-pointer conversion when you appear to do that.

From the C standard:

A postfix expression followed by an expression in square brackets is a postfix expression. One of the expressions shall have the type “pointer to T” and the other shall have unscoped enumeration or integral type.

2

u/Zirias_FreeBSD 11h ago edited 11h ago

Sorry, but ... that's simplified, and IMHO, over-simplified. Simple example, look at the following code:

int a[5];
a[2] = 0;

No pointers were used here. a names the array, the compiler emits some calculated absolute address for the correct memory access to write its third element.

The correct understanding is: C simply doesn't allow passing arrays, for historic reasons. Interestingly, it does allow to pass arbitrary structs. The designers of the language still wanted some as if syntax, silently passing some reference (IOW, a pointer) instead.

The most consistent way to achieve this was to define [] in terms of pointer arithmetics: a[b] is always exactly the same as *(a+b). For both that and function arguments to work "as expected", another rule is that in most expression contexts, the identifier of an array evaluates to a pointer to its first element. But there are exceptions, most notably the sizeof operator.

So ... "accessing an array is doing pointer arithmetic" isn't necessarily correct. Address arithmetic, obviously, possibly entirely at compile time, but that's likely unavoidable in any implementation of the concept of an array on some machine with addressable memory cells.

2

u/mc_pm 11h ago

My answer was aimed at where the OP seems to be in his C journey -- if you don't understand something, then basic understanding is more important than a comprehensive answer. I suspect this ^^ is well beyond where OP is now.

There's a difference between "being right" and "being understood".

However, you are right.

1

u/smcameron 10h ago

it does allow to pass arbitrary structs.

Historical footnote: this is true, and has been true for a very long time, but it wasn't always true. For instance, in the 1st edition of K&R, it wasn't true. In the early days, structs couldn't be passed as parameters, assigned or returned from functions. You'd have to pass/return them as pointers, and memcpy() (or back then, probably bcopy()) them for assignment, or assign individual members. grandpa resumes napping: zzzzzzZZZZZ

1

u/glasket_ 5h ago

The example does technically still include a pointer (until C2y, see N3517) because the subscript operator still decays the array to a pointer. C2y is changing the semantics so now array subscripting won't be using the same *(a+2) logic anymore, and a[2] won't strictly involve a pointer in the abstract machine sense anymore. But currently a[2] is effectively "dereference two locations in front of the pointer that results from a" instead of "access the value at the the address denoted by a[2]," if that makes sense.

1

u/Severe-Reality5546 12h ago

The typical way is to also pass the number elements:

void my_sort(int my_array[], int nelem)

1

u/IronAttom 11h ago

Either have something in the array to know where it's end is or pass the length with the pointer 

1

u/This_Growth2898 11h ago

You'd better ask such questions with your code.

If I dereferenced the pointer to an array, wouldn’t that return the same as indexing the first value?

a[0] is a syntax sugar for *a

a[n] is a syntax sugar for *(a+n)

Is this ok or you need some more details?

If I wanted to pass in the entire array, could I do that by passing in the pointers of both the first and last elements and using pointer arithmetic to access the other elements? What’s the idiomatic way of doing this?

Traditionally, it's array + size; but of course you can also pass array + last pointer, array + end pointer (i.e. last + 1); or set the guardian element at the end, like in strings. Just make sure you're consistent over your code.

1

u/FloridianfromAlabama 10h ago edited 10h ago

The only follow up I need is do I need to change the pointer arithmetic based on the size of the elements in bytes in the array? For example, if I want the next element in an int array, do I add 4 or do I add 1?

2

u/This_Growth2898 10h ago

C does it automatically. If it's an array/pointer of int, index is multiplied by size of(int).

1

u/Ghyrt3 9h ago

Array and pointer are similar but they are not the same thing and it can lead to confusing errors that beginners have trouble correcting, that's why the compiler yells at you to use use the most proper syntax.

  1. When you pass as an argument to a function, the array is always considered a pointer to the first (0-th) element.

  2. You don't need to. To access the array, only the array is needed. But if you don't have proper ways to know where it's finished, the usual way is to pass the size of the array too. (For example, a string is always ended by '\0' so you don't always need to pass the size.)

And I wanted to say more but SmokeMuch (and others) have made compelling explanations so I won't!

But I would stress to you how much you should start by pointer before everything else. They are the cornerstone of all idiomatic C.

1

u/MagicalPizza21 9h ago

The typical way to pass in an array is to pass in the pointer to the first element and the number of elements (length).

1

u/ReallyEvilRob 8h ago

The name of the array always decays to a pointer to the first element. In other words, if arr is defined as follows int arr[5]; then saying arr is the same as &arr[0]. So if you need to pass your array to a function, then the idiomatic way to do that would be to call your function and include the name of the array in your argument list. Your function would then have a pointer to the entire array since arrays are always contiguous. Something to be careful of is you also have to include a size argument so your function knows how big the array is since that can't be inferred from the pointer argument. So your function prototype should look something like this:

void func(int *arr, size_t size);

You would then call the function like this:

int arr[5] = {0,1,2,3,4}; func(arr, sizeof(arr));

1

u/glasket_ 5h ago

If I dereferenced the pointer to an array, wouldn’t that return the same as indexing the first value?

It needs to be clarified that "pointer to array" is different from what happens when an array decays to a pointer. void func(int x[]) is equivalent to void func(int *x), i.e. an int array passed as an argument becomes a pointer to int.

void foo(int x[]);

int main(void) {
  int arr[5] = {0};
  foo(arr);
  // is equivalent to
  foo(&arr[0]);
  // is equivalent to
  int *p = arr;
  foo(p);
  int fst = p[0]; // or *p
}

Pointer to array actually exists as a distinct type, and lets you pass pointers to whole arrays;

void bar(int (*arp)[5]);

int main(void) {
  int arr[5] = { 0 };
  // This isn't how you pass an array pointer:
  // foo(arr);
  // Instead, you use the address-of operator:
  bar(&arr);
  // Similarly, you can store array pointers too:
  int (*arp)[5] = &arr;
  bar(arp);
  int fst = (*arp)[0];
}

Pointers to arrays actually require the size to match or you get a diagnostic from the compiler, whereas the normal pointer form doesn't give any guarantees.

If I wanted to pass in the entire array, could I do that by passing in the pointers of both the first and last elements and using pointer arithmetic to access the other elements? What’s the idiomatic way of doing this?

Start and end pointers are one way, while the other usual way is passing the size of the array. When they're turned into data structures the former is sometimes called a slice or a span, while the latter is usually called a "fat pointer" or vector.

When working with static sizes and matrices the pointer to array approach becomes useful, but the vast majority of the time either of the other options will work and are more idiomatic.

1

u/flyingron 12h ago

Welcome to the brain-damaged array types in C. Arrays can not be assigned, nor can they be passed to or returned from functions. Whenever an array appears as a function parameter or return type, that type is replaced with a pointer to the first element of the array. If you then attempt to pass an array name as a parameter, the implicit array to pointer-to-first-element conversions occurs.

Note that arrays, pointer to arrays, and pointers to the first element are all distinct types:

int a[10];   // a is an ten-element array of type int.
int (*ap)[10];  // ap is a poitner to a ten-element array of int.
int *ip;   // ip is a pointer to an int.

Now to answer your questions. First, no it is not the same.

*ap yields an array, not an element.

*ap = 5 is invalid. You can't assign an integer to an array.

Second, you can't pass an "entire array." As I said, they can't be passed or assigned. You can pass a pointer (or using the treat it as a pointer behind the scenes method)

void func_a(int p) {
   p = 5;
}

void func_b(int param[10])  { // param is really treated as an int*
    param[0] = 5;
}

int main() {
    int i = 0;
    func_a(i);  // pass p by value.
    printf("%d\n", i);  // prints zero, the p in func_a is only a copy of i.

    int a[10];
    func_b(a);  // really passes &(a[0])
    printf("%d\n", a[0]);  // prints 5, as main's a was changed in the function.
}

Hope this helps. We really should have fixed this back in 1977 when we fixed structs. But we didn't bite the bullet then, and now we're screwed.

Note, that you can pass and return structs (in all the modern compilers). If you really want to pass an array to a function by value, you can wrap it in a struct:

struct wrapper {
    int a[10];
} w;

void func_c(struct wrapper wparam) {
    wparam.a[0] = 5 ;
}

int main() {
    w.a[0] = 0;
    func_c(w);
    printf("%d\n", w.a[0]);  // prints 0.   wparam in func_c is just a copy
}

1

u/stianhoiland 11h ago edited 11h ago

> Welcome to the brain-damaged array types in C.
> We really should have fixed this back in 1977 when we fixed structs.

This is a misconception and a pet peeve of mine.

You can't pass arrays because you can't pass arrays. It's that simple. Well, you can pass arrays: Arrays of 8, 16, 32, or 64 bits, depending on how far back you go/what machine you're programming.

You can't just magically pass an N-element array of ints or whatever. And you shouldn't be able to. Because you can't. CPUs have registers and registers have a size—fixed sizes. Function-calling conventions orchestrate the usage of registers. It's dumb to want to fit, say, a 24kb payload in a 64-bit register. You can't.

You can't. You can't. You can't. Anymore than you can fit a bridge in a cigarette pack.

C isn't dumb or broken because of it. But you're dumb for thinking it should—not as an insult but as an actual state of understanding.

C is wonderful for exactly this reason. It hasn't introduced a whole meta-level of abstraction above the hardware for its own constructs. Every language that semantically allows you to pass arrays to functions have to contend with exactly the same status quo as C—CPUs have registers and registers have a fixed size—and invent its own abstractions and conventions to make it look like you can do that. But you can't. And C just shows you that you can't and it's up to you to come up with a way to pretend to do it. Like say, pass a pointer and size—which you CAN do.

1

u/flyingron 11h ago

Your argument is specious. Nothing in C has ever limited you to what you can fit in registers. In fact the original C implementation didn't pass parameters in registers at all. It pushed them on the stack. Returns on the other hand were done in registers.

And you have been able (since the phototypesetter / V7 compilers, around 1977) to pass large objects by value. See my example of wrapping the array with a struct.

It's not a misconception at all. Your understanding of how the language works now and worked historically is deficient. I have been programming in C since those early days.

And I am able to express facts and opinions without resorting to ad hominem attacks.

1

u/stianhoiland 11h ago

Imagine complaining about being limited by a mechanism with limitations bound to register size and then saying that nothing about that was ever limited to what could fit in a register. I can explain it to you but I can't understand it for you.

1

u/flyingron 10h ago

Huh? There is no such limitation. There really never was. By the time we had machines with a sufficient number of registers to pass things in registers, the language already had parameters that wouldn't fit in a register. C could pass large structs by value back circa 1979 (I was off by a couple of years in my first post. Struct assignment/passing came in the V7 compiler, which came out in 1979, rather than the typesetter C, which came out a couple of years earlier).