Skip to content

Functions, copied arguments and local state

Lesson 5 of 1313 minPDF notesFree

What changes when a function is called

This program changes an x inside transform. Does the x in main change too? Predict the two printed numbers before reading the trace.

C
#include <stdio.h>

int transform(int x) {
    x = x + 4;
    return 2 * x;
}

int main(void) {
    int x = 3;
    int y = transform(x);
    printf("%d %d\n", x, y);
    return 0;
}

A function packages a calculation under a name. In int transform(int x), the first int says that a call returns an integer. transform is the function’s name, and int x declares its integer parameter. The braces contain its body. The definition appears before main, so its name and parameter type are known when it is called.

In transform(x), the expression between parentheses is an argument. Here it reads the caller’s x. The parameter is the separate variable declared by the function; the argument supplies its starting value. The return value is the result sent back by return. These are three roles: supplied value, local receiving variable, and result of the call.

An ordinary int argument is passed by value: the parameter receives a copy of the argument’s value. Changing the copy does not change the caller’s integer variable. A repeated name does not join the two variables. Even if the parameter had been named p, the call would still supply 3.

Use two separate state records whenever control enters a function: one for the caller and one for the current call. While the helper runs, leave the caller’s record visible but unchanged unless a later caller statement assigns a new value.

Worked trace 1 A copied parameter

  1. Before the call, the caller is main and its state is x = 3. The initialization of y is waiting for a value.
  2. For transform(x), read the caller’s current x: the argument value is 3. Enter transform with a separate parameter also named x, initialized to 3. Record two boxes: caller x = 3; callee x = 3.
  3. In transform, x = x + 4 reads the callee’s 3 and stores 7 in that same callee parameter. The caller’s box still contains 3.
  4. return 2 * x computes 2 * 7 = 14 and ends this call. It returns the number 14; it does not print 14 or assign to the caller’s x.
  5. Back in main, the call expression has value 14, so the waiting initialization completes with y = 14. The caller’s state is now x = 3, y = 14.
  6. printf reads the caller’s two variables in the written output order. The output line is 3 14.

The decisive check is where each assignment happens. x = x + 4 belongs to transform; int y = transform(x) belongs to main. The latter stores the returned number in y. Returning a value and assigning it to a chosen caller variable are separate steps. No “copy back” happens when the helper finishes.

return ends the current function call immediately. If an if branch executes a return, statements after that return are not executed during that call. The integer helpers here provide a return value on every reachable path. printf is the operation that displays output; the return in a helper only supplies the call’s value.

Worked trace 2 A loop inside a helper

The next helper adds the positive odd integers no larger than its argument. It is called twice. Predict whether the second call begins with the first call’s total.

C
#include <stdio.h>

int odd_sum(int n) {
    int total = 0;
    for (int k = 1; k <= n; k += 2) {
        total += k;
    }
    return total;
}

int main(void) {
    int n = 6;
    int first = odd_sum(n);
    int second = odd_sum(3);
    printf("%d %d %d\n", n, first, second);
    return 0;
}
  1. Caller: n = 6. The first call receives parameter n = 6 and initializes its own total = 0.
  2. First call, test k = 1: 1 <= 6 is true. Add 1, so total = 1; then k += 2 makes k = 3.
  3. First call, test k = 3: true. Add 3, so total = 4; update to k = 5.
  4. First call, test k = 5: true. Add 5, so total = 9; update to k = 7.
  5. First call, test k = 7: 7 <= 6 is false. The body has run 3 times and the test 4 times. Return 9; caller stores first = 9.
  6. Second call: the argument is the constant 3. A new parameter has n = 3, and this call’s new local total is initialized to 0. Caller n remains 6.
  7. Second call, test k = 1: true. Add 1, giving total = 1; update to k = 3.
  8. Second call, test k = 3: true. Add 3, giving total = 4; update to k = 5.
  9. Second call, test k = 5: false. The body has run 2 times and the test 3 times. Return 4; caller stores second = 4.
  10. Caller state: n = 6, first = 9, second = 4. The output line is 6 9 4.

At the start of each loop test, total contains the sum of the odd positive integers already processed. Initially none have been processed, so 0 is correct. Each successful body adds the current odd k; the update moves to the next odd integer. This explains the accumulator’s meaning, not just the final number.

For the supplied inputs, k increases by 2 until it exceeds n, so the loop terminates. At the boundary odd_sum(0), the first test is 1 <= 0, which is false: no body executes and the function returns 0. A helper with a loop can still return normally when its loop executes zero times.

The local variable total is initialized by int total = 0 every time this declaration is reached on a new call. “Local” does not mean “automatically zero”: the initializer is what supplies 0. The second call has its own initialized local; it does not continue from 9. Here total can be named from its declaration through the rest of the helper’s body, including the loop body. The k declared in the for initializer can be named in that loop’s test, update and body; it is outside its scope after the loop. main cannot name either helper-local variable. It receives a number through the return value instead.

The caller’s n and the helper’s parameter n are also separate. The second call uses the literal 3, so it does not even read caller n for that argument. The first call’s returned 9 remains in caller first while the second call computes its result.

Combining small functions

One function’s returned value can be another function’s argument. Read outer(inner(value)) by finding the inner call’s result, then using that number as the argument of the outer call. This follows the data needed by the calls; it is not a rule that unrelated C arguments always run left to right.

In the practice below, twice multiplies its integer parameter by 2 and plus3 adds 3. Applying the two calculations in a different order can change the result. Keep each call’s argument and returned value visible instead of mentally merging the two function bodies. These helpers use only their integer parameters and local calculations, so there are no other state changes to track.

A reliable call trace

  1. Read the current value of the argument expression in the caller.
  2. Start a separate callee state and give the parameter that value.
  3. Execute the helper’s statements in order, including tests, loop updates and local initializers.
  4. Stop that call at the executed return and record its value.
  5. Resume the caller’s pending expression and store the value only where the caller’s statement says to store it.

All programs here use C11, initialized ordinary signed int values and small, representable intermediate results. The shown helpers are defined before their calls. Trace only the supplied inputs and the explicitly stated small boundaries; a loop formula is not a promise of safe arithmetic for every possible int.

Practice

For each program, record the caller state, the argument received by each helper, and the returned value. Try all four before opening their solutions.

Practice 1

Give the caller value and returned result.

C
#include <stdio.h>

int decrease(int p){p-=2;return p;}

int main(void) {
    int z=9;
    int ans=decrease(z);
    printf("%d %d\n",z,ans);
    return 0;
}

Practice 2

Compare the two compositions.

C
#include <stdio.h>

int twice(int p){return p*2;}
int plus3(int p){return p+3;}

int main(void) {
    int a=twice(plus3(4));
    int b=plus3(twice(4));
    printf("%d %d\n",a,b);
    return 0;
}

Practice 3

Find results at a negative input, zero, and a positive input.

C
#include <stdio.h>

int nonnegative(int p){if(p>=0){return 1;}return 0;}

int main(void) {
    int a=nonnegative(-2);
    int b=nonnegative(0);
    int c=nonnegative(3);
    printf("%d %d %d\n",a,b,c);
    return 0;
}

Practice 4

Trace two calls and explain why the second total starts at zero.

C
#include <stdio.h>

int sum_to(int n){int total=0;for(int k=1;k<=n;k++){total+=k;}return total;}

int main(void) {
    int a=sum_to(4);
    int b=sum_to(2);
    printf("%d %d\n",a,b);
    return 0;
}

Practice solutions

Practice 1

  1. Caller starts with z = 9. The call decrease(z) supplies argument value 9.
  2. Inside decrease, the separate parameter starts at p = 9. p -= 2 gives p = 7.
  3. return p returns 7. Caller stores ans = 7; caller z is still 9.
  4. The output line is 9 7.

7 7 would incorrectly copy the parameter’s change back to z. 9 9 would ignore the executed subtraction and returned value. 7 9 makes both mistakes. Track the variable that each assignment actually names.

Practice 2

  1. For a, the inner call is plus3(4). Its parameter is 4 and it returns 4 + 3 = 7.
  2. The outer call is therefore twice(7). It returns 7 * 2 = 14, stored in a.
  3. For b, the inner call is twice(4), which returns 8.
  4. The outer call is therefore plus3(8), which returns 11, stored in b.
  5. The output line is 14 11. Each helper uses a new parameter for its own call.

11 14 reverses which composition belongs to a and b. 14 14 assumes adding 3 and multiplying by 2 can be interchanged; they cannot, because in the first composition the added 3 is also doubled. 7 8 stops after the inner calls and omits the outer calls.

Practice 3

  1. With argument -2, parameter p = -2. The test p >= 0 is false, so execution reaches return 0. Thus a = 0.
  2. With argument 0, the test 0 >= 0 is true. return 1 ends this call immediately, so the later return 0 is not executed. Thus b = 1.
  3. With argument 3, the test is true and the call returns 1. Thus c = 1.
  4. The output line is 0 1 1. Both routes return an integer.

0 0 1 treats >= as > and wrongly excludes zero. 0 0 0 assumes the final return always overwrites an earlier return, but a returned call has already ended. -2 0 3 repeats the arguments even though the function returns a classification value.

Practice 4

  1. First call: parameter n = 4, local total = 0.
  2. For k = 1, 2, 3, 4, the successive totals are 1, 3, 6, 10. After each body, k increases by 1. At k = 5, the test fails. There are 4 body executions and 5 tests.
  3. Return 10 and store a = 10 in the caller.
  4. Second call: parameter n = 2, a fresh local total = 0. For k = 1, 2, totals are 1, 3; at k = 3, the test fails. There are 2 body executions and 3 tests.
  5. Return 3 and store b = 3. The output line is 10 3.

10 13 incorrectly starts the second accumulator at 10. 6 1 omits the endpoint despite k <= n. 15 6 wrongly executes the body for the first failed-test values 5 and 3. The update prepares the next test; it does not guarantee another body execution.

Check your explanation

A complete explanation distinguishes the caller’s variable, the current call’s parameter, and the returned number. If a helper has a loop, include its first failed test. Use these same records in the separate C foundation learning review.

Source note

C11 semantics are referenced to the WG14 N1570 committee draft. For copied arguments, local initialization, loop scope and return values, see §§6.5.2.2, 6.2.1–6.2.4, 6.8.5.3 and 6.8.6.4.

Analogy

Think of a helper as a calculation desk. You write the number 3 on a request slip while keeping your own record of 3. The desk writes 3 in its own working box, changes that box to 7, and sends back a result slip showing 14. Your record still says 3. If you file the result under y, then y becomes 14.

On the next visit, the desk begins a new worksheet. In odd_sum, the written instruction int total = 0 starts that worksheet’s total at zero. An old worksheet’s total is not reused. The separate boxes model distinct integer variables; the paper story is only a tracing aid, not a claim about how a computer physically stores them.

Quick reference

  • Function definition: return type, function name, typed parameters, then a braced body; place these definitions before calls.
  • Argument: the value supplied by the caller. Parameter: a separate receiving variable. Return value: the result of the call expression.
  • An ordinary int parameter receives a copy. Assigning to it does not assign to the caller’s variable.
  • return expression; evaluates the expression, ends this call and supplies its result. It does not print.
  • A caller stores the returned number only when its own statement does so.
  • When a local declaration is reached in a new call, its initializer runs for that call. int total = 0 supplies zero; locality alone does not.
  • A variable declared in a block is usable only within its scope. The int k in a for initializer belongs to that loop, not the statements after it.
  • For a nested single-argument call, compute the inner return value to obtain the outer argument. Do not infer a general left-to-right argument-order rule.
  • Trace loops inside helpers exactly as before: test, body, update, next test, including the final false test.
  • Worked trace 1 prints 3 14; Worked trace 2 prints 6 9 4. Restrict arithmetic claims to the stated small inputs.

Notes for this lesson

Sign in to keep your progress. Sign in