Functions in C: Call by Value vs Simulated Call by Reference and the Swap Question Family

See why a normal C swap changes only local copies, then trace the pointer version and the aliasing, increment, array, struct, and lifetime traps around it.

KnowledgeGate Team

Exam prep & CS education

Updated 20 Jul 20266 min read

"Why does my swap function not work?" is one of the first serious C questions, and the same idea appears repeatedly in output and final-value problems. The cause is simple: C passes every argument by value. A function gets a copy, so reassigning its parameter does not change the caller's variable.

That one rule decides every swap, pointer parameter and array argument you will meet in a paper. The work is always the same: decide which object an expression names before you evaluate anything.

C has one parameter-passing mode: call by value

When a caller passes an int, the called function receives a new int initialised with the same value. The parameter has its own storage in the called function's stack frame.

Consider the familiar broken swap:

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

int x = 3, y = 7;
swap(x, y);

At the call, a receives 3 and b receives 7. Inside swap, the assignments make a = 7 and b = 3. Then the function returns and its frame is discarded. The caller's separate objects were never written, so the final values remain x = 3 and y = 7.

The function did perform a swap, but it swapped two temporary copies.

Simulating call by reference with pointers

To mutate the caller's objects, pass their addresses and dereference the pointer parameters:

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

int x = 3, y = 7;
swap(&x, &y);

Now a contains a copy of &x, and b contains a copy of &y. The pointers themselves are still passed by value. The difference is that *a names the caller's x, while *b names the caller's y.

Trace it step by step. Initially x = 3, y = 7, so t = *a stores 3. The assignment *a = *b writes 7 into x. Then *b = t writes 3 into y. The final caller values are x = 7, y = 3.

Two stack-frame diagrams for a swap of x = 3 and y = 7. On the by-value side the caller frame still holds x = 3 and y = 7 while the swap frame's copies a and b exchange 3 and 7 and are then discarded. On the by-pointer side the swap frame holds a = &x and b = &y, with arrows into the caller frame, where x is now 7 and y is now 3.

This is often described informally as "call by reference in C", but pointer-based mutation is the accurate phrase. C still copied the address into a parameter.

Arrays are the exception that is not an exception

In a function call, an array argument decays to a pointer to its first element. A parameter written as int a[] is adjusted to int *a, so an element write reaches the caller's array:

void setFirst(int a[]) {
    a[0] = 99;
}

int values[] = {1, 2, 3};
setFirst(values);

After the call, values[0] is 99. The array was not copied, but C did not switch parameter-passing modes either. It passed a pointer value that identifies the original elements.

A whole structure behaves differently when passed normally. If struct Point p is a parameter, the function receives a structure copy. Writing p.x = 99 changes only that copy. To mutate the caller's structure, accept struct Point *p and write p->x = 99. The same copy rule drives padding and size questions, worked out in Structures and Unions in C.

The deciding question is always, "Which object does this expression name?" Pointers in C for GATE runs that question through double pointers and pointer arithmetic with box-and-arrow diagrams.

The swap and increment question family

Some questions replace the temporary variable with arithmetic:

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

For distinct objects with values 3 and 7, the writes are *a = 10, then *b = 10 - 7 = 3, then *a = 10 - 3 = 7. The swap works.

It fails when both pointers hold the same address. Suppose x = 3 and the call is swap(&x, &x). Both *a and *b name x. The first line makes x = 3 + 3 = 6. On the second line, both operands read that same current value, so x = 6 - 6 = 0. The third line keeps it at 0 - 0 = 0. Aliasing has zeroed the variable.

Increment questions test a different value rule:

int f(int n) {
    return n++;
}

For f(5), post-increment yields the old value 5 as the return expression, then increments the local n. The caller receives 5. With return ++n;, the increment occurs first and the caller receives 6. In both cases, the caller's original argument remains unchanged because n is a copy.

Returning addresses, stack frames, and dangling pointers

Each call gets its own frame containing its local variables. That frame's automatic objects stop existing when the function returns. Therefore this function is invalid:

int *g(void) {
    int t = 5;
    return &t;
}

The returned address points to an object whose lifetime has ended. Dereferencing it is undefined behaviour. The memory may appear to retain 5 in one run and fail in another, but neither result is promised by C.

A static local has program-long storage duration, so returning its address is valid. Returning the address of a global object is also valid. This lifetime distinction bites hardest in recursion, where every call owns a fresh set of automatic locals and each set stops existing the moment that call returns.

Traps to check before choosing an answer

For a final-value or output question, trace storage locations before arithmetic:

  • A by-value swap(int, int) changes only its parameters.

  • A pointer swap needs addresses at the call site, such as swap(&x, &y).

  • Passing the same address twice can break an arithmetic swap.

  • Post-increment yields the old value; pre-increment yields the new value.

  • Returning &local creates a dangling pointer.

  • A structure passed by value is copied, even though an array argument decays to a pointer.

When asked how many times something prints, also separate function calls from parameter changes. A local increment does not become a caller-side increment unless the function writes through a pointer.

How GATE tests functions in C

GATE commonly turns these rules into program-output, final-value, and "which call mutates the caller" questions. The safest method is to draw a box for every object, put copied parameters in a separate frame, and follow each pointer arrow before evaluating an expression. The active cycle's syllabus and examination information is on the official GATE website.

A typical stem shows both versions of the function and asks which caller values changed. Start with p = 2 and q = 5. The by-value call swap(p, q) leaves them at 2 and 5, because it wrote only its own parameters and then lost them. The pointer call swap(&p, &q) leaves them at 5 and 2, because both writes travelled through addresses that name p and q. Count the writes that reach a caller object, not the writes that appear in the function body.

The short version and next step

C is call by value, always. Passing a pointer copies an address, and dereferencing that copied address lets the function reach the caller's object. Arrays fit the same model through decay. Watch for aliasing, distinguish pre-increment from post-increment, and never return a pointer to an expired automatic local.

KnowledgeGate's practice bank carries over 1,000 C programming questions for drilling these families. Work through C Programming, then use Coding & DSA to connect the same memory model to arrays, structures, and recursion.