C Part I · Module 5 · Functions and Program Decomposition
Parameters, Arguments and Pass-by-Value
The previous chapter described a call carefully and then moved on: the parameter starts out holding the argument's value. That sentence was written the way it was because the obvious alternative reading — that the function is given your variable — is wrong, and believing it produces a specific and memorable kind of bug in which a function appears to do nothing at all. This chapter is about one rule. It is short to state, it has no exceptions, and it is the rule every pointer chapter in Module 7 is built on top of. Getting it properly established now is the difference between pointers being a natural next step and pointers being the point at which C stops making sense.
Foundation18 min readCPart IFundamentalsFunctions
Part I · Module 5 · Chapter 5.3 · Parameters, Arguments and Pass-by-Value
1. A Function That Appears to Do Nothing
#include <stdio.h>
void increment(int value)
{
value = value + 1;
printf(" inside increment, value is now %d\n", value);
}
int main(void)
{
int count = 5;
printf("before the call, count is %d\n", count);
increment(count);
printf("after the call, count is %d\n", count);
return 0;
}Before reading on, predict all three lines.
Output:
before the call, count is 5
inside increment, value is now 6
after the call, count is 5The middle line says 6. The last line says 5.
Both are true at the same time, and that is only possible if there are two objects. value holds 6. count holds 5. The assignment inside increment worked perfectly — it just did not work on count.
2. The Rule
An argument is an expression. Its value is used to initialise the parameter, which is a separate object belonging to the call. Assigning to a parameter changes that object and nothing else.
This is called pass-by-value, and the two words are worth separating.
Pass — something crosses the boundary into the function.
By value — what crosses is the value, not the object. The function receives a number; it does not receive your variable.
Chapter 2.1 made exactly this distinction for a different reason: a name refers to an object, and an object holds a value. A call transfers the value. The object stays where it was, with the name that referred to it.
Read the arrow between the two objects carefully. It points one way, and it happens once, at the moment of the call. After that the two objects have nothing to do with each other. There is no link back, and no continuing relationship — which is why a later assignment to value cannot travel anywhere.
3. The Swap That Swaps the Wrong Objects
The classic demonstration is a function that exchanges two values:
#include <stdio.h>
void swap(int a, int b)
{
int temporary = a;
a = b;
b = temporary;
printf(" inside swap, a is %d and b is %d\n", a, b);
}
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:
before: left 1, right 2
inside swap, a is 2 and b is 1
after: left 1, right 24. Why the Parameter Is an Object, Not a Name for Yours
Chapter 5.2's step 2 said the parameter starts out holding the argument's value. Three consequences follow, and all three are visible in §1 and §3.
A parameter is a declaration. void increment(int value) declares an int object called value. It is as real an object as anything declared inside the body, and it exists for the duration of the call.
The argument does not have to be an object at all. increment(5), increment(count + 1) and increment(2 * limit) are all valid. There is nothing for the function to modify in any of those cases — which tells you the mechanism cannot be receive the caller's object, because most calls do not pass one.
Assigning to a parameter is legal and local. This is not a mistake to avoid; it is a perfectly normal technique, and you have already used it.
5. You Have Already Relied on This
Chapter 5.1's bit counter modifies its parameter on every pass of the loop:
int count_set_bits(unsigned int word)
{
int set = 0;
while (word != 0u) {
set = set + (int)(word & 1u);
word = word >> 1;
}
return set;
}word = word >> 1 destroys the value being examined, repeatedly, until nothing is left. If a parameter were the caller's object, that function would be a disaster — it would reduce the caller's status word to zero as a side effect of counting its bits.
#include <stdio.h>
int count_set_bits(unsigned int word)
{
int set = 0;
while (word != 0u) {
set = set + (int)(word & 1u);
word = word >> 1;
}
return set;
}
int main(void)
{
unsigned int status = 0x0Cu;
printf("before: status is 0x%02X\n", status);
printf("faults: %d\n", count_set_bits(status));
printf("after: status is 0x%02X\n", status);
return 0;
}Output:
before: status is 0x0C
faults: 2
after: status is 0x0Cstatus is untouched. The function consumed its own copy, which is exactly why it was safe to write it that way.
A parameter doubles as a free local object, already initialised with what you were given. Using it as working space is idiomatic C, not a hack.
Chapter 5.1 made a point of copying pattern into remaining before its loop, because the loop was in main and there was no call to make the copy for you. Inside a function the copy has already happened.
6. What To Do Instead
If a function cannot change the caller's object, how does a result get back? Return it, and let the caller assign.
#include <stdio.h>
int incremented(int value)
{
return value + 1;
}
int main(void)
{
int count = 5;
printf("before: count is %d\n", count);
count = incremented(count);
printf("after: count is %d\n", count);
return 0;
}Output:
before: count is 5
after: count is 6The assignment is in main, which is where count lives and where the change is visible in the source. A reader scanning main can see that count changes on that line; in §1's version, nothing at the call site suggested a change at all — which is the deeper reason the first program was confusing.
Notice the name changed too, from increment to incremented. A function that returns a new value should be named for the value it produces, not for an action it cannot perform. Chapter 5.9 returns to naming as a design tool.
A realistic use
#include <stdio.h>
unsigned int next_backoff(unsigned int current_ms, unsigned int ceiling_ms)
{
unsigned int doubled = current_ms * 2u;
if (doubled > ceiling_ms) {
return ceiling_ms;
}
return doubled;
}
int main(void)
{
const unsigned int ceiling_ms = 800u;
unsigned int delay_ms = 100u;
for (int attempt = 1; attempt <= 5; attempt++) {
printf("attempt %d: waiting %u ms\n", attempt, delay_ms);
delay_ms = next_backoff(delay_ms, ceiling_ms);
}
printf("final delay %u ms\n", delay_ms);
return 0;
}Output:
attempt 1: waiting 100 ms
attempt 2: waiting 200 ms
attempt 3: waiting 400 ms
attempt 4: waiting 800 ms
attempt 5: waiting 800 ms
final delay 800 msnext_backoff owns one rule — double it, but never past the ceiling — and can be checked on its own at 100, at 400, and at the ceiling. The state lives in main, where the loop that changes it also lives. That division is the normal shape of a C program: functions compute, the caller keeps the state.
7. Three Beliefs to Correct
The second belief: assigning to a parameter is a mistake. It is not. §5 is an ordinary, correct function that does it deliberately. The only thing to be careful about is the reader's expectation: if a parameter is used as scratch space, it no longer holds what the caller passed, so do not use it later in the function as though it did.
The third belief: the function will modify my variable if I give it the same name. Names do not cross the boundary; Chapter 5.2 §8 established that. A parameter called count in a function called from a main that also has a count is still a different object. Chapter 5.6 covers which name refers to what.
8. What Module 7 Will Add, and What It Will Not
It is worth being precise now about what is coming, so that nothing in this chapter has to be withdrawn later.
Module 7 will let a function modify a caller's object. It does that by introducing a value that identifies an object — an address — which can be passed like any other value. The function then has something it can use to reach the original. The rule in §2 is untouched: a value was copied into a parameter, as always.
Module 6 will produce something that looks like an exception. When arrays arrive, passing one to a function will appear to let the function modify the caller's array, which seems to contradict everything here. It does not, and Chapters 7.6 and 7.7 explain exactly what is actually being passed. Flagging it now matters — a reader who meets that behaviour without warning will reasonably conclude this chapter was simplified. It was not.
9. What You Should Be Able to Say Now
- An argument's value initialises the parameter; the parameter is a separate object belonging to the call.
- That is pass-by-value, and it is why
incrementprinted 6 inside and 5 outside. - The copy happens once, at the call, and there is no continuing link back.
swapdoes not fail — it swaps the two objects it has, which were never the caller's.- An argument need not be an object at all:
f(5)andf(a + b)are ordinary calls. - Assigning to a parameter is legal and local — and useful, as
count_set_bitsshows. - A parameter is a free local already holding what you were given.
- To change a caller's object, return the value and let the caller assign.
- C has no pass-by-reference, for any type or size. Structures are copied too.
- Module 7 passes the value of an address — the same rule, a different value, not an exception.
10. Exercises
Exercise 1 — predict all the lines. Do not compile yet.
#include <stdio.h>
void modify(int n)
{
n = n * 10;
printf("inside: %d\n", n);
}
int main(void)
{
int value = 3;
modify(value);
printf("outside: %d\n", value);
modify(value + 1);
printf("outside: %d\n", value);
return 0;
}The third call passes value + 1. Say what object the function could possibly have modified, and what that tells you about the mechanism.
Exercise 2 — explain the two truths. In §1, the middle line prints 6 and the last line prints 5. Explain in three sentences how both can be correct, using the words object and value.
Exercise 3 — reframe the failure. A colleague says "C's swap is broken." Correct them in two sentences without using the word pointer, and say what swap would need in order to do what they want.
Exercise 4 — trace the backoff. For §6's program with delay_ms starting at 100 and a ceiling of 500, give the five printed delays and the final value. Say which attempt first hits the ceiling.
Exercise 5 — find the bug. This is meant to halve the reading until it is below the limit. Say what it prints, name the wrong mental model behind it, and repair it without changing main's structure.
#include <stdio.h>
void reduce(int reading, int limit)
{
while (reading > limit) {
reading = reading / 2;
}
}
int main(void)
{
int reading = 400;
reduce(reading, 50);
printf("%d\n", reading);
return 0;
}Exercise 6 — repair it the other way. Rewrite Exercise 5 so the function returns the reduced value, give the new function an honest name, and show the corrected call.
Exercise 7 — judge the scratch use. This function uses its parameter as working space and then uses it again. Say what goes wrong, and give the smallest repair.
int report_and_count(unsigned int word)
{
int set = 0;
while (word != 0u) {
set = set + (int)(word & 1u);
word = word >> 1;
}
printf("word 0x%02X has %d bits set\n", word, set);
return set;
}Exercise 8 — predict across the boundary. main has an int count = 1; and calls a function whose parameter is also called count and which assigns 99 to it. State what main's count holds afterwards and why, then say which chapter owns the question of which count a given line refers to.
11. Summary
- An argument's value is copied into the parameter, which is a separate object belonging to the call.
- The copy happens once and there is no link back.
incrementprinting 6 inside and 5 outside is not a contradiction — it is two objects.swapsucceeds on the objects it was given. It was never given the caller's.- Arguments need not be objects:
f(5)is an ordinary call, and there is nothing in it to modify. - Assigning to a parameter is normal, and a parameter is a convenient pre-initialised local.
- The way to change a caller's object is to return the value and let the caller assign.
- C is pass-by-value everywhere. No size exception, no type exception, no second mechanism.
- Module 7 passes the value of an address, which extends this rule rather than replacing it.
One half of the boundary is now precise: what goes in, and what it does not do. The other half has been used in every example without being examined. return has appeared as the last line of a function and in the middle of one, in functions that produce a value and in one that produces none, and in main where the value goes somewhere that has not been explained since Chapter 1.7. The next chapter takes it apart — including what happens when a function that promises a value reaches the end of its body without ever executing one.
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.
