Skip to content
VLSI Mentor

C Part I · Module 7 · Pointers

Pointers as Function Parameters

Chapter 5.3 ran a function called swap that exchanged two values perfectly and left the caller's variables untouched, and was careful to say that the function had not failed — it had succeeded on two objects that were never the caller's. It then stated, deliberately, that the repair was not to make swap cleverer but to give it something that identified the caller's objects, and that C had such a thing waiting four modules away. Chapter 5.4 made a smaller promise, listing the out-parameter among the ways a result can leave a function and marking it unavailable. This chapter settles both. Nothing about how arguments are passed changes here, and that is the part worth understanding: what follows is an application of the copying rule, not an exception to it.

Foundation18 min readCPart IFundamentalsPointers

Part I · Module 7 · Chapter 7.5 · Pointers as Function Parameters

1. The Problem, Restated Precisely

Chapter 5.3's rule:

An argument's value is copied into the parameter, which is a separate object belonging to the call.

Chapter 5.3's consequence: a function given int a receives a copy, so assigning to it changes nothing the caller can see. That is why swap(left, right) swapped a and b and left left and right alone.

Read the rule again and notice what it does not say. It says nothing about which kind of value you choose to copy. If you copy an int, the function has a copy of a number. If you copy an address, the function has a copy of an address — and a copy of an address identifies exactly the same object the original did.

The repair is not to stop copying. It is to copy something more useful.

2. swap, Working

Azvya Education Pvt. Ltd.VLSI Mentor
the function from Chapter 5.3, with one change of type
#include <stdio.h>

void swap(int *a, int *b)
{
    int temporary = *a;
    *a = *b;
    *b = temporary;
}

int main(void)
{
    int left = 1;
    int right = 2;

    printf("before: left %d, right %d\n", left, right);
    swap(&left, &right);
    printf("after:  left %d, right %d\n", left, right);

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before: left 1, right 2
after:  left 2, right 1

Compare it with Chapter 5.3's version line by line. The algorithm is identical — a temporary, two assignments, done. Three things changed:

The parameters are int * instead of int. The function now asks for addresses.

The body dereferences. *a is Chapter 7.2's operator, and it designates the caller's object rather than a local copy.

The call site writes &left, &right. The caller supplies addresses, which is the only change a caller has to make.

Two names refer to two separate objects in two different functions. In the caller, the name left refers to an int object at address 1000 holding the value 1. Inside swap, the parameter name a refers to a different object, of type pointer to int, at address 3000, and that object holds a copy of the address 1000. An arrow runs from the pointer object to the int object, labelled: dereferencing a reaches it.left — in maina name in the callerint object at 1000holds 1a — swap's parametera name inside the calleeint * object at 3000holds a copy of 1000refers torefers to*a reaches it12
Figure 1 — deliberately the same shape as Chapter 5.3's figure, because nothing about argument passing changed. The caller's object left holds 1. The parameter a is still a separate object belonging to the call, and it still starts out holding a copy of the argument. What differs is that the argument was an address, so the copy identifies the caller's object — and dereferencing it reaches that object rather than a local duplicate. The addresses shown are arbitrary labels; real ones differ on every run.

3. Reading the Signature

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void swap(int *a, int *b);

Read int *a as a is a pointer to int — Chapter 7.1 §3's rule, in a parameter list. What that tells a caller is: this function wants the address of an int, and intends to reach the object there.

A signature taking a pointer is making a claim about the relationship between caller and callee, and it is worth being able to state which claim:

SignatureWhat the function can do
void f(int n)read a copy; the caller's object is untouchable
void f(int *p)read and modify the caller's object
void f(const int *p)read the caller's object; promise not to modify it

The third row is the one that makes a pointer parameter honest rather than merely capable, and Chapter 7.11 is about writing it. Until then, be aware that a plain int * parameter is telling every caller I may change this, whether or not you do.

4. Pass-by-Value Is Still in Force

It is tempting to conclude that pointer parameters are an exception to Chapter 5.3's rule. They are not, and the experiment that proves it is the same one Chapter 5.3 ran.

Chapter 5.3 showed that assigning to an int parameter changes nothing in the caller. The same is true of a pointer parameter — assigning to p itself, rather than through it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void attempt_retarget(int *p)
{
    int local = 99;
    p = &local;                /* changes this call's own pointer object */
}

A caller invoking attempt_retarget(&value) finds value unchanged, and would find its own pointer unchanged too. The parameter p is an object belonging to the call, initialised with a copy of the address, and assigning to it replaces that copy and nothing else — precisely Chapter 5.3, applied to a pointer.

This is Chapter 7.2 §4's distinction arriving where it matters most:

*p = x reaches the caller's object. p = &something does not.

Everything a function can change about its caller goes through a dereference. If you find yourself assigning to a pointer parameter and expecting the caller to notice, you want a pointer to a pointer — which exists, and is Chapter 7.10's.

5. The Out-Parameter

Chapter 5.4 established that a function returns exactly one value, listed four ways a result can leave, and marked the third unavailable:

ApproachAvailable then
Return the valueyes
Return a status codeyes
Out-parameterChapter 7.5
Return a composite valueChapter 10.4

Here it is. The pattern is: the return value carries the status, and a pointer parameter carries the result.

Azvya Education Pvt. Ltd.VLSI Mentor
a conversion that can fail
#include <stdio.h>

const int parse_ok = 0;
const int parse_out_of_range = 1;

int code_to_millivolts(unsigned int code, unsigned int *millivolts_out)
{
    if (millivolts_out == NULL) {
        return parse_out_of_range;
    }

    if (code > 4095u) {
        return parse_out_of_range;
    }

    *millivolts_out = (code * 3300u) / 4095u;
    return parse_ok;
}

int main(void)
{
    unsigned int mv = 0u;

    if (code_to_millivolts(2048u, &mv) == parse_ok) {
        printf("2048 -> %u mV\n", mv);
    } else {
        printf("2048 rejected\n");
    }

    if (code_to_millivolts(5000u, &mv) == parse_ok) {
        printf("5000 -> %u mV\n", mv);
    } else {
        printf("5000 rejected, mv untouched: %u\n", mv);
    }

    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2048 -> 1650 mV
5000 rejected, mv untouched: 1650

Four decisions in that function are worth naming, and the last one is a bug waiting to happen in somebody else's code.

The null check is first. Chapter 7.4 §5's table said a pointer arriving as a parameter is the case that most needs checking, and this is why: code_to_millivolts cannot know what its caller passed. One guard at the boundary, and the rest of the function dereferences freely.

The out-parameter is named _out. A reader scanning the signature can tell which parameter is an input and which is a result without reading the body.

Nothing is written on the failure paths. The function returns before touching *millivolts_out.

And that is exactly why the second output line is a warning. mv reads 1650 after the failed call — the value the previous call left there. A caller that ignores the status and uses mv anyway gets a stale number that looks entirely plausible.

6. When to Use Which

A pointer parameter is not automatically the right answer, and reaching for one by default produces functions that are harder to use than they need to be.

SituationPrefer
One result, cannot failreturn it — unsigned int to_millivolts(unsigned int code)
One result, can failstatus + out-parameter
Several resultsseveral out-parameters, or Chapter 10.4's composite
A large object you only need to reada pointer, marked const — Chapter 7.11
The function must modify the caller's objecta pointer, necessarily

Chapter 5.9's criterion still applies: prefer the form that makes the function easiest to check on its own. A function that returns its result can be exercised by comparing the returned value; one that writes through a pointer needs a caller to supply an object first. That is a small cost, and it is a real one — so pay it when the problem requires it, not out of habit.

7. Two Debts, Settled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
5.3  "a function cannot change a caller's variable"
     -> true, and unchanged. A function cannot change a caller's variable.
        It can change an object whose address it was given.

5.4  "out-parameter — Chapter 7.5"
     -> status by return, result by pointer, caller checks the status first.

Both repairs needed one idea — a value that identifies an object — and neither required any rule from Module 5 to be withdrawn.

One debt is still open. Chapter 5.3 §8 warned that arrays would appear to be an exception to pass-by-value and promised they are not. Everything in this chapter passed the address of a single object, explicitly, with &. What happens when the argument is an array is different in a way that looks identical at the call site, and it is the subject of the next two chapters.

8. What You Should Be Able to Say Now

  1. Pass-by-value is unchanged: the argument's value is copied into the parameter.
  2. What changed is what you copy — an address identifies the same object the original did.
  3. void swap(int *a, int *b) plus swap(&left, &right) is Chapter 5.3's function, working.
  4. Reading int *p in a parameter list: this function wants an address and intends to reach the object there.
  5. Assigning to the pointer parameter changes nothing in the caller; only a dereference reaches out.
  6. The out-parameter pattern: status in the return value, result through a pointer.
  7. Guard the pointer against NULL at the top — Chapter 7.4's rule, applied where it was predicted.
  8. A failed call leaves the out-parameter untouched, which can mean a plausible, stale value.
  9. Check the status before reading the result — structure the call so there is no other path.
  10. Prefer returning a value when it cannot fail; a pointer parameter is a real cost, worth paying when needed.

9. Exercises

Exercise 1 — predict the output. Do not compile yet.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdio.h>

void bump(int *p) { *p = *p + 1; }
void retarget(int *p) { int local = 100; p = &local; }

int main(void)
{
    int n = 5;
    bump(&n);
    printf("%d\n", n);
    retarget(&n);
    printf("%d\n", n);
    return 0;
}

Explain the second number using §4.

Exercise 2 — fix the signature. Each of these is meant to modify the caller's object and does not. Give the corrected signature, the corrected body, and the corrected call.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void reset(int n)          { n = 0; }           /* called as reset(count); */
void double_it(int v)      { v = v * 2; }       /* called as double_it(x);  */
void clear_both(int a, int b) { a = 0; b = 0; } /* called as clear_both(p, q); */

Exercise 3 — trace the swap. For §2's program, give the values of left, right, *a, *b and temporary after each of the three statements in swap.

Exercise 4 — write an out-parameter function. Write int divide(int numerator, int denominator, int *quotient_out) that returns a status and writes the quotient only on success. Say what it must do when denominator is 0, and what it must do when quotient_out is NULL.

Exercise 5 — find the stale read. Say what this prints and why it is dangerous, then restructure main so the bug cannot occur.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unsigned int mv = 0u;
code_to_millivolts(2048u, &mv);
code_to_millivolts(9999u, &mv);
printf("%u\n", mv);

Exercise 6 — classify three signatures. For each, say what the function is permitted to do to the caller's object, and what a caller should expect.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
void a(int n);
void b(int *n);
void c(const int *n);

Exercise 7 — choose the shape. For each, say whether you would return the value, return a status with an out-parameter, or something else, and why.

  • convert a raw code to millivolts, where any code is acceptable
  • look up a calibration value by channel, where the channel may be out of range
  • report both the minimum and the maximum of a set of readings
  • tell the caller whether a status word has any fault bits set

Exercise 8 — explain the non-exception. A colleague says pointer parameters "break pass-by-value". Using §4's attempt_retarget, explain in three sentences why the rule is intact, and what was actually copied.

10. Summary

  • Pass-by-value is unchanged. The address is copied like any other argument.
  • Copying an address gives the function something that identifies the caller's object.
  • swap(int *a, int *b) is Chapter 5.3's function with two types and a call site changed.
  • Assigning to the parameter changes nothing; only *p = x reaches out.
  • The out-parameter pattern is status returned, result written through a pointer.
  • Null-guard a pointer parameter at the top, once.
  • A failed call leaves the out-parameter untouched — which can look like a plausible stale value.
  • Check the status before reading the result, structurally rather than by memory.
  • Prefer returning a value where you can; a pointer parameter is a cost worth paying deliberately.

Every call in this chapter wrote & at the call site, which made it obvious that an address was being passed and left no doubt about what the function received. Arrays do not work that way. An array argument is written with no & at all, the function appears to receive the array itself, and the parameter can be declared in a notation that looks exactly like an array declaration — and yet Chapter 6.1 proved that an array carries no length, so something must be getting lost. The next chapter states the conversion rule that explains all of it, and the one after that applies it.

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.