Skip to content
VLSI Mentor

C Part I · Module 7 · Pointers

Arrays as Function Parameters

Three earlier chapters wrote promissory notes against this one. Chapter 5.3 warned that an array argument would appear to break the rule that arguments are copied, and insisted it does not. Chapter 6.1 refused to pass a single array to a function for an entire module, saying the reason deserved an explanation rather than an assertion. And Chapter 6.3's count idiom was introduced with an explicit caveat — that it works because the compiler can see the declaration, and that there is a situation where it cannot. All three are the same situation, and none of them needs a new rule: everything here follows from the conversion established in the previous chapter plus the fact that a function call is an expression. The result is the single consequence that catches every C programmer exactly once.

Intermediate17 min readCPart IFundamentalsPointersArrays

Part I · Module 7 · Chapter 7.7 · Arrays as Function Parameters

1. The Derivation

Two facts, both already established:

Chapter 7.6: in almost every expression, an array is converted to a pointer to its first element — and the exceptions are sizeof, &, and a string-literal initialiser.

Chapter 5.2: the arguments of a call are expressions, evaluated before the function runs.

A call is not one of the exceptions. So:

When you pass an array to a function, the function receives a pointer to its first element. Nothing else travels.

And since Chapter 6.1 proved that an array stores no length anywhere, there is nothing attached to that pointer to say how many elements follow it.

The length does not travel, because there was never anywhere for it to live.

That is the whole chapter. Everything below is consequences and the fix.

2. What the Parameter Actually Is

Three spellings, which look like they should mean different things:

Azvya Education Pvt. Ltd.VLSI Mentor
three parameters that are the same parameter
#include <stdio.h>

void takes_brackets(int a[])        { printf("  int a[]   : sizeof a = %zu\n", sizeof a); }
void takes_sized(int a[10])         { printf("  int a[10] : sizeof a = %zu\n", sizeof a); }
void takes_pointer(int *a)          { printf("  int *a    : sizeof a = %zu\n", sizeof a); }

int main(void)
{
    int arr[4] = {1, 2, 3, 4};

    printf("in main   : sizeof arr = %zu\n", sizeof arr);
    takes_brackets(arr);
    takes_sized(arr);
    takes_pointer(arr);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
in main   : sizeof arr = 16
  int a[]   : sizeof a = 8
  int a[10] : sizeof a = 8
  int *a    : sizeof a = 8

Sixteen in main. Eight in all three functions.

A parameter declared as an array is adjusted to a pointer. int a[], int a[10] and int *a are three spellings of the same thing, and the compiler treats them identically.

The middle one deserves particular suspicion. int a[10] looks like it constrains the caller to pass ten elements. It does not. The 10 is accepted, recorded nowhere, and discarded — a caller passing a four-element array gets no complaint, as the output above shows.

The compiler is explicit about it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: sizeof on array function parameter will return size of 'int *'
    instead of 'int[10]' [-Wsizeof-array-argument]

It even names int[10] while telling you it is giving you the size of int * — a precise description of a size that was written down and then thrown away.

3. The Number That Makes It Stick

Chapter 6.1's count idiom is where this bites, because it does not fail loudly.

Azvya Education Pvt. Ltd.VLSI Mentor
the same expression, before and after conversion
#include <stdio.h>
#include <stddef.h>

int main(void)
{
    int values[6] = {10, 20, 30, 40, 50, 60};
    int *as_pointer = values;

    printf("sizeof values / sizeof values[0]          = %zu\n",
           sizeof values / sizeof values[0]);
    printf("sizeof as_pointer / sizeof as_pointer[0]  = %zu\n",
           sizeof as_pointer / sizeof as_pointer[0]);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sizeof values / sizeof values[0]          = 6
sizeof as_pointer / sizeof as_pointer[0]  = 2

Six, and two. Same data, same expression, written out in the same function.

The second is 8 / 4 — the size of a pointer divided by the size of an int. It is not an error, not a crash, and not an obviously wrong number. It is a small plausible integer that a loop will happily use, processing the first two elements of a six-element array and reporting a confident, wrong answer.

4. The Fix

There is only one, and it follows directly from §1: if the length cannot travel with the array, pass it as a separate argument.

Azvya Education Pvt. Ltd.VLSI Mentor
the shape to write
#include <stdio.h>
#include <stddef.h>

int sum(const int *values, size_t count)
{
    int total = 0;

    for (size_t i = 0; i < count; i++) {
        total = total + values[i];
    }

    return total;
}

int main(void)
{
    int values[6] = {10, 20, 30, 40, 50, 60};
    size_t count = sizeof values / sizeof values[0];

    printf("count in main   %zu\n", count);
    printf("sum             %d\n", sum(values, count));

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
count in main   6
sum             210

Four details of that signature are deliberate.

The count is computed in main, where values is still an array and the idiom is valid. That is the only place it can be computed correctly, and it is the reason this pattern works.

The parameter type is size_t, matching what sizeof produces — Chapter 6.3's three-facts-must-agree argument, now spanning a function boundary.

The parameter is written const int * rather than int *. Chapter 7.5 §3 noted that a plain pointer parameter tells every caller I may change this; sum does not, and says so. Chapter 7.11 covers what const means in a pointer declaration; here it is doing one job — making an honest signature — and you can read it as this function will not write through this pointer.

The loop is Chapter 6.3's, unchanged. Indexing works on a pointer exactly as it works on an array name, for the reason Chapter 7.6 §5 previewed and Chapter 7.8 completes.

The length and the pointer are two halves of one argument. Keep them adjacent in the parameter list, name them so the relationship is obvious, and never let one be passed without the other.

5. Why This Is Not an Exception to Pass-by-Value

Chapter 5.3 §8 promised this would look like an exception and is not, so it is worth being explicit.

A function given an array can modify the caller's elements:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void zero_all(int *values, size_t count)
{
    for (size_t i = 0; i < count; i++) {
        values[i] = 0;
    }
}

and that looks like the caller's array was passed by reference. It was not. What was copied was a pointer — one address, copied by value exactly as Chapter 5.3 describes, exactly as in Chapter 7.5's swap. The function then reaches the caller's elements through that copy.

The difference from Chapter 7.5 is only that you did not have to write &. The conversion supplied the address for you, which is why it looks different and why Chapter 5.3 warned about it in advance.

The rule is intact, and the test is the same one Chapter 7.5 §4 used: assigning to the parameter changes nothing in the caller.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void pointless(int *values, size_t count)
{
    int other[3] = {0};
    values = other;          /* changes this call's pointer object only */
    (void)count;
}

6. What a Caller Can and Cannot Promise

Once the length is a separate argument, nothing connects the two except your discipline. The compiler checks that the count is a size_t and that the pointer is an int *; it cannot check that the count is that array's count.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    sum(values, 6);        /* correct */
    sum(values, 60);       /* compiles; reads 54 elements that do not exist */

The second line is Chapter 6.4's undefined behaviour, reached through a function boundary. Nothing in C prevents it.

Three habits reduce the exposure, and they are the same ones Chapter 6.4 recommended with one addition:

Compute the count from the array, never by hand. sizeof values / sizeof values[0] at the call site, not a literal.

Keep the two parameters adjacent and named for each other — values and count, not values and n thirty characters apart with three unrelated parameters between them.

Build your tests with -fsanitize=address. It catches the over-long count the moment the loop steps past the end, exactly as it did in Chapter 6.4.

There is a niche C99 notation, void f(int a[static 10]), which declares that a caller must supply at least ten elements. It is rarely used, support for diagnosing violations is uneven, and it does not change what the parameter is — it is still a pointer. Mentioned so you recognise it; not something to reach for.

7. The Ledger, Closed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
5.3 §8  "arrays will look like an exception to pass-by-value"
        -> they do, and they are not: a pointer is copied, by value.

6.1 §7  "array work stays inside main; 7.7 explains why"
        -> because a function receives a pointer, and the length is gone.

6.1 §3  "the sizeof idiom works here; it will stop working"
        -> inside a function it reports on a pointer: 2 instead of 6.

Module 6 can now be reread with its restriction lifted. Every traversal it taught works unchanged inside a function, provided the count arrives as its own argument.

8. What You Should Be Able to Say Now

  1. A call is an expression, so an array argument is converted to a pointer to its first element.
  2. The length does not travel — Chapter 6.1 proved there was nowhere for it to live.
  3. int a[], int a[10] and int *a are the same parameter; the size in the brackets is discarded.
  4. sizeof a inside such a function is the size of a pointer — 8, not 16.
  5. The count idiom gives 6 in main and 2 inside the function — a plausible, silent, wrong number.
  6. -Wsizeof-pointer-div and -Wsizeof-array-argument catch the common shapes, and are in -Wall.
  7. The fix is to pass the count as a separate argument, computed where the array is still an array.
  8. Use size_t for the count, and const int * when the function only reads.
  9. This is not an exception to pass-by-value — a pointer was copied; assigning to the parameter still changes nothing.
  10. Nothing connects the pointer and the count but your discipline; a wrong count is Chapter 6.4's undefined behaviour.

9. Exercises

Exercise 1 — predict the sizes. Give sizeof x in each place, assuming int is 4 and pointers are 8.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void f(int x[])    { /* here */ }
void g(int x[20])  { /* here */ }
void h(int *x)     { /* here */ }
int main(void) { int x[7]; /* here */ f(x); g(x); h(x); }

Exercise 2 — compute the lie. For each element type, give what sizeof a / sizeof a[0] evaluates to inside a function taking a as a parameter, on an implementation with 8-byte pointers.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
char a[100]      int a[100]      double a[100]

Say which of those three answers would be most likely to go unnoticed, and why.

Exercise 3 — fix the signature. Rewrite each so it works, and say what the caller must now supply.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int largest(int a[]) { int m = a[0]; for (size_t i = 1; i < sizeof a / sizeof a[0]; i++) ... }
void clear(int a[50]) { for (int i = 0; i < 50; i++) { a[i] = 0; } }

Exercise 4 — explain the non-exception. zero_all in §5 sets the caller's elements to zero. Explain in three sentences why Chapter 5.3's rule is intact, and say what was copied.

Exercise 5 — find the undefined call. Given int readings[8]; and int sum(const int *a, size_t n);, say which calls are correct and which are Chapter 6.4's undefined behaviour.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sum(readings, 8);
sum(readings, sizeof readings / sizeof readings[0]);
sum(readings, 10);
sum(readings, 0);
sum(&readings[2], 6);

The last one is legal — say why, and what it computes.

Exercise 6 — place the count. A colleague writes void process(int *data, int mode, int retries, size_t count);. Give two reasons to move count next to data, and rewrite the signature.

Exercise 7 — choose the qualifier. For each function, say whether the array parameter should be int * or const int *, and what the choice tells a caller.

  • compute the average of the elements
  • set every element to zero
  • find the index of the largest element
  • sort the elements into ascending order

Exercise 8 — reread Module 6. Take Chapter 6.3's "find the extreme" traversal and turn it into a function with an honest signature. State what it must do when count is 0, and why that case cannot be handled inside the loop.

10. Summary

  • A call is an expression, so an array argument becomes a pointer to its first element.
  • The length does not travel, because Chapter 6.1 proved there is nowhere for it to be stored.
  • int a[], int a[10], int *a are one parameter; the bracketed size is discarded.
  • sizeof inside the function reports on a pointer — the count idiom gives 2 instead of 6, silently.
  • -Wsizeof-pointer-div catches the common shape and is in -Wall.
  • Pass the count separately, computed where the array is still an array, as a size_t.
  • Use const int * when the function only reads — Chapter 7.11 explains the notation.
  • Not an exception to pass-by-value: a pointer was copied.
  • Nothing ties the pointer to the count but discipline; a wrong count is undefined behaviour.

The loop in §4 indexed a pointer — values[i] where values is an int * — and it worked without comment, as though indexing were something arrays do and pointers borrow. It is the other way round. Chapter 7.6 said that indexing is defined in terms of pointers and deferred the second half of the definition, which is what it means to move a pointer along by i elements. That arithmetic is the last piece of the relationship between pointers and arrays, it explains why values[i] works at all, and it is the subject of the next chapter.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.