By the end of this chapter you'll be able to…

  • 1Predict program output by drawing the memory rather than reading the code
  • 2Read a C declaration from the identifier outward
  • 3Explain why a pointer's type controls its arithmetic
  • 4Compute the result of pointer addition and subtraction
  • 5State when pointer comparison and subtraction are defined
  • 6Distinguish array decay from the exceptions under sizeof and address-of
  • 7Explain why a function cannot recover an array's length from a parameter
  • 8Compute the address of an element in a row-major two-dimensional array
  • 9Distinguish a two-dimensional array from an array of pointers
  • 10Explain why a C string is a character array plus a convention
  • 11Distinguish sizeof from string length on a character array
  • 12Explain why C has no pass-by-reference and how the appearance is achieved
  • 13State when a pointer to pointer is required
  • 14Distinguish call by value, reference, value-result and name
  • 15Compare the four storage classes by lifetime, scope and default value
  • 16Name the four memory regions and what lives in each
  • 17Explain why returning the address of a local is undefined
  • 18State the two required parts of a correct recursive function
  • 19Explain why work before and after the recursive call produce opposite orders
  • 20Identify tail recursion and explain why it can become a loop
  • 21Read a recurrence directly from a recursive function
  • 22Distinguish total call count from maximum stack depth
  • 23Recognise the standard cases of undefined behaviour
  • 24State why signed overflow is undefined while unsigned overflow wraps
💡
Why this chapter matters in GATE
C questions in GATE are not about knowing library functions. They ask you to predict exactly what a short program prints, and that prediction is always made the same way, because C is a thin layer over memory addresses. A variable is a named location, a pointer is an address with a type attached, and an array name is very nearly an address. Almost every surprising output is a consequence of that fact rather than of any special rule, so the method is to draw the memory: a box per variable, an arrow per pointer, a frame per call, then step through updating the drawing. The second organising fact is that the type attached to a pointer controls arithmetic on it, which explains most pointer questions in one line. The third is that recursion is the stack made visible: each call has its own frame with its own copies, so understanding a recursive function means drawing that stack rather than holding the whole unfolding in your head.

Before you start — revise these

🔗
Machine Instructions & Addressing Modes
Activation records, the stack discipline and base-plus-offset addressing of locals are the machine-level view of what a function call does.
🔗
Number Representation & Computer Arithmetic
Signed and unsigned overflow behaviour, and the byte-level layout of multi-byte values, follow directly from the representations there.

Programming in C & Recursion

C questions in GATE are not about knowing library functions. They are about predicting exactly what a short program prints, and that prediction is always made the same way.

C is a thin layer over memory addresses. A variable is a named location, a pointer is an address with a type attached, and an array name is very nearly an address. Almost every surprising output in a GATE question is a consequence of that fact rather than of any special rule.

So the method is: draw the memory. Draw each variable as a box, each pointer as an arrow, each function call as a new frame on the stack. Then step through the program updating the drawing. This is slower to describe than to do, and it is far more reliable than reasoning about the code as text.

The second organising fact is that the type attached to a pointer controls arithmetic on it. Adding 1 to an int * advances by four bytes on a typical machine, not by one, and this single rule explains most pointer-arithmetic questions.

The third is that recursion is the stack made visible. Each call gets its own frame with its own copies of parameters and locals, and understanding a recursive function means drawing that stack rather than trying to hold the whole unfolding in your head.

1. Pointers as Typed Addresses

A pointer holds an address. The type says what is stored at that address, and therefore how many bytes an object there occupies.

int x = 10;
int *p = &x;    /* p holds the address of x */
*p = 20;        /* writes through p; x is now 20 */

The declaration syntax is read from the identifier outward. In int *p, the * binds to p, so p is a pointer to int. This is why int *p, q; declares one pointer and one plain integer, which catches people constantly.

A pointer to a pointer holds the address of another pointer, and dereferencing twice reaches the final value.

A null pointer is a distinct value guaranteed not to point at any object. Dereferencing it is undefined behaviour, and so is dereferencing an uninitialised pointer, which holds whatever bits were in that memory.

The void * type holds an address with no type attached, so it cannot be dereferenced directly and cannot participate in pointer arithmetic. It exists to pass addresses generically, which is why malloc returns one.

2. Pointer Arithmetic

Adding an integer to a pointer of type T * advances the address by bytes.

This scaling is automatic and is the source of most confusion. If p is an int * holding address 1000 and an int is 4 bytes, then p + 3 holds address 1012, not 1003.

Subtracting two pointers of the same type gives the number of elements between them, not the number of bytes, by dividing the byte difference by the element size.

Comparison and subtraction are defined only within a single array, including one position past its end. Comparing pointers into unrelated objects is undefined behaviour even though it usually appears to work.

Incrementing a pointer past one-past-the-end, or dereferencing the one-past-the-end position, are both undefined. The one-past-the-end address may be formed and compared, which is what makes the standard loop idiom legal.

3. Arrays and Pointers

Arrays and pointers are related but are not the same thing, and the differences are examined directly.

An array name decays to a pointer to its first element in almost every context, which is why arr[i] and *(arr + i) are equivalent, and indeed why i[arr] compiles and means the same thing.

The exceptions are where it matters. sizeof(arr) gives the size of the whole array, not of a pointer, and &arr has type "pointer to array" rather than "pointer to element", so &arr + 1 skips the entire array.

ExpressionFor int arr[10]
arrPointer to first element
sizeof(arr)40 bytes
&arrPointer to array of 10 ints
&arr + 1Address 40 bytes further
arr + 1Address 4 bytes further

An array cannot be assigned and cannot be passed by value. Declaring a function parameter as int a[] is exactly equivalent to int *a, which is why sizeof inside the function gives the pointer size and why a function can never determine an array's length from the parameter alone.

For a two-dimensional array int a[R][C], the elements are stored in row-major order, so the address of a[i][j] is the base plus .

A two-dimensional array is not an array of pointers. int a[3][4] is 12 contiguous integers; int *b[3] is 3 pointers that may point anywhere. Passing the first to a function expecting the second is a type error, and confusing them is a standard exam trap.

Strings inherit every one of these rules, because a C string is only a character array with a convention attached.

The convention is a terminating null character, and every library function relies on it. A character array without one is not a string, and passing it to a string function reads past the end until a zero byte happens to appear.

The two size measures differ accordingly. sizeof on a character array gives the declared size including the terminator, while the length function counts characters up to but excluding it. For an array declared with the literal "GATE", the first gives 5 and the second gives 4.

A string literal assigned to a pointer rather than an array is stored in read-only memory, so writing through that pointer is undefined even though the code compiles cleanly.

4. Parameter Passing

C passes every argument by value, without exception. A function receives a copy, and modifying the copy cannot affect the caller's variable.

The appearance of pass-by-reference is achieved by passing a copy of an address. The function still cannot change the caller's pointer, but it can change what the pointer points at.

void bad(int x)  { x = 99; }        /* caller unaffected */
void good(int *x){ *x = 99; }       /* caller's variable changed */

A function that must change the caller's pointer needs a pointer to pointer. This is why a function that allocates memory and returns it through a parameter takes a T **.

Passing an array passes a copy of the address of its first element, which is why array modifications inside a function are visible to the caller — not because arrays are special, but because an address was copied.

Other languages offer alternatives worth distinguishing. Call by reference binds the parameter to the caller's variable itself. Call by value-result copies in on entry and copies back on exit, which differs from reference when the same variable is passed twice or is modified concurrently. Call by name substitutes the argument expression textually and re-evaluates it at every use.

5. Storage Classes

Four storage classes control lifetime and visibility.

ClassLifetimeScopeInitial value
auto (default local)BlockBlockIndeterminate
static localWhole programBlockZero
static globalWhole programFileZero
externWhole programGlobalDefined elsewhere
registerBlockBlockIndeterminate

A static local variable keeps its value between calls, because it lives in the data segment rather than on the stack. This is the basis of a standard question shape: a function with a static counter that increments across calls.

Uninitialised locals contain indeterminate values, while uninitialised statics and globals are zero. Relying on the first is undefined behaviour; relying on the second is guaranteed.

Memory divides into four regions with different behaviour: the code segment, the data segment for globals and statics, the heap for malloc, and the stack for locals and call frames.

The heap must be freed explicitly and the stack is freed automatically, which is why forgetting free leaks memory while forgetting to clean up a local does not.

Returning the address of a local variable is a classic error: the frame is destroyed on return, so the pointer refers to memory that no longer belongs to anyone.

6. Recursion and the Stack

Each recursive call creates a new activation record on the stack, containing that call's parameters, locals and return address. This is why each call has its own copies and why recursion terminates the program if the depth exceeds the stack size.

Every correct recursive function has two parts, and questions are usually built by damaging one of them.

A base case that returns without recursing, and a recursive case that moves strictly closer to the base case. Omitting either produces infinite recursion.

The order of operations relative to the recursive call determines the output order, and this is the most frequently examined detail.

void f(int n) {
    if (n == 0) return;
    printf("%d ", n);   /* before the call: descending */
    f(n - 1);
}

void g(int n) {
    if (n == 0) return;
    g(n - 1);
    printf("%d ", n);   /* after the call: ascending */
}

Work done before the call happens on the way down; work done after happens on the way back up. The second version prints in the opposite order despite an almost identical body.

Tail recursion is the case where the recursive call is the last operation, with nothing left to do afterwards. It can be converted mechanically into a loop, and some compilers do so, eliminating the stack growth. The second function above is not tail recursive, because the printf still has to run after the call returns.

7. Analysing Recursive Functions

The running time of a recursive function is described by a recurrence, and the recurrence is read directly from the code.

Count the number of recursive calls and the size of each subproblem, then add the work done outside the calls.

A function that makes one call on a problem of size and does constant work gives , which solves to .

A function that makes two calls on size gives , which solves to — the naive Fibonacci case.

A function that makes two calls on size with linear extra work gives , which solves to — the merge sort case.

The space complexity of a recursive function is the maximum stack depth, not the total number of calls. Naive Fibonacci makes exponentially many calls but its stack never exceeds depth , so it uses space.

That distinction between total calls and maximum depth is examined directly and is missed often.

8. Undefined Behaviour

Several constructs have no defined meaning, and a question exploiting one is asking whether you recognise it rather than what the output is.

Modifying a variable more than once between sequence points is undefined, which is why expressions like i = i++ + ++i have no correct answer.

Reading an uninitialised variable, dereferencing a freed or null pointer, and indexing outside an array are all undefined, and none of them reliably crashes — which is precisely what makes them dangerous.

Integer overflow of a signed type is undefined, while unsigned overflow is defined to wrap modulo . The asymmetry surprises people and is worth remembering exactly.

The practical consequence for an exam is that if a question's code contains one of these, the intended answer is usually that the behaviour is undefined or implementation-dependent, and computing a specific value is the trap.

9. Worked Examples

Example 1. What does this print?

int a[5] = {1, 2, 3, 4, 5};
int *p = a;
printf("%d %d %d", *p + 1, *(p + 1), *++p);

Take the three expressions separately, remembering that * and + bind differently in each.

*p + 1 dereferences first, giving a[0] which is 1, then adds 1, giving 2.

*(p + 1) adds first, advancing the pointer by one int, then dereferences, giving a[1] which is 2.

*++p increments p first, so p now points at a[1], then dereferences, giving 2.

All three print 2, but for three different reasons — which is the point of the question.

There is a serious caveat. The order in which the arguments to printf are evaluated is unspecified, and the third expression modifies p while the second reads it. This program therefore has unspecified behaviour, and a well-set question would separate the statements. Recognising that is worth more than the arithmetic.

Example 2. What is the output?

void f() {
    static int count = 0;
    count++;
    printf("%d ", count);
}
int main() { f(); f(); f(); return 0; }

The variable is static, so it lives in the data segment for the whole program rather than on the stack.

The initialisation = 0 happens once, before main runs, not on every call.

Call 1 increments it to 1 and prints 1. Call 2 increments to 2 and prints 2. Call 3 prints 3.

The output is 1 2 3.

Had static been omitted, the variable would be a fresh stack local each time, initialised to 0 on every call, and the output would be 1 1 1.

Example 3. Trace the output of this recursive function for n = 3.

void g(int n) {
    if (n == 0) return;
    g(n - 1);
    printf("%d ", n);
}

Draw the stack. The call g(3) immediately calls g(2) before printing anything. g(2) calls g(1), which calls g(0).

g(0) returns without printing, because the base case fires first.

Now the stack unwinds. g(1) resumes after its call and prints 1. g(2) resumes and prints 2. g(3) resumes and prints 3.

The output is 1 2 3, in ascending order, even though the parameter descended.

Moving the printf above the recursive call would print 3 2 1, because the work would then happen on the way down rather than on the way back up. That single line placement is the whole content of a common question type.

Example 4. What does this print, and why?

void swap(int a, int b) { int t = a; a = b; b = t; }
int main() {
    int x = 3, y = 5;
    swap(x, y);
    printf("%d %d", x, y);
    return 0;
}

C passes by value, so swap receives copies of x and y in its own stack frame.

The function exchanges its local copies perfectly, and then the frame is destroyed on return.

The caller's variables were never touched, so the output is 3 5.

The fix is to pass addresses: void swap(int *a, int *b) with *t = *a; *a = *b; *b = t; and a call of swap(&x, &y). Even then, the addresses themselves are passed by value — the function cannot change which variables the caller's names refer to, only the contents at those addresses.

Example 5. How many times is f called for f(5), and what is the maximum stack depth?

int f(int n) {
    if (n <= 1) return n;
    return f(n - 1) + f(n - 2);
}

This is naive Fibonacci. Let be the number of calls including the initial one.

. For larger , .

. . . .

So f is called 15 times.

The maximum stack depth is different. At any moment the stack holds one chain of calls, and the longest chain follows n-1 repeatedly: f(5), f(4), f(3), f(2), f(1).

The maximum depth is 5, so the space complexity is even though the time complexity is exponential.

The gap between 15 calls and depth 5 is the whole point. Calls that have returned no longer occupy the stack, so total work and peak space are unrelated quantities.

Example 6. What is wrong with this function, and what does the caller see?

int *make(void) {
    int local = 42;
    return &local;
}

The variable local lives in make's stack frame. When make returns, that frame is released and the memory becomes available for the next call.

The returned pointer therefore refers to memory that no longer belongs to anyone. Dereferencing it is undefined behaviour.

What makes this dangerous rather than merely wrong is that it often appears to work. Immediately after the return, the bytes may still contain 42 because nothing has overwritten them yet. The first unrelated function call will reuse that stack space, and the value will change at an unpredictable moment.

Three correct alternatives exist. Return the value rather than a pointer, which is simplest. Allocate on the heap with malloc, making the caller responsible for free. Or take a pointer to caller-provided storage as a parameter, which avoids allocation entirely.

Note that declaring local as static would also make the pointer valid, since the variable would then live in the data segment — but every call would return the same address, which is a different bug.

Summary

C is a thin layer over memory addresses, so draw the memory: boxes for variables, arrows for pointers, frames for calls.

A pointer's type controls its arithmetic. Adding advances by times the element size, and subtracting two pointers gives an element count, not a byte count.

An array name decays to a pointer except under sizeof and &. sizeof(arr) is the whole array; &arr + 1 skips it entirely.

An array parameter is a pointer, so a function cannot recover an array's length from the parameter. A two-dimensional array is contiguous and is not an array of pointers.

Everything is passed by value. Passing an address gives the appearance of reference; changing the caller's pointer requires a pointer to pointer.

Static locals live in the data segment, keep their value between calls, and are initialised once and to zero by default. Uninitialised locals are indeterminate.

Each recursive call gets its own frame. Work before the call happens on the way down and work after it on the way back up, which is what flips the output order.

A recurrence is read straight from the code: number of calls, subproblem size, and work outside the calls. Space is the maximum stack depth, not the total number of calls.

Undefined behaviour includes modifying a variable twice between sequence points, reading uninitialised memory, dereferencing freed or null pointers, indexing out of bounds, and signed overflow — while unsigned overflow is defined to wrap.

Key formulas & results

Everything to memorise for the exam hall, in one card. Screenshot this for revision.

The organising tool
C IS A THIN LAYER OVER MEMORY ADDRESSES. DRAW THE MEMORY: A BOX PER VARIABLE, AN ARROW PER POINTER, A FRAME PER CALL, THEN STEP THROUGH UPDATING IT.
ALMOST EVERY SURPRISING OUTPUT IS A CONSEQUENCE OF THE MEMORY LAYOUT RATHER THAN OF ANY SPECIAL LANGUAGE RULE.
Reading declarations
READ A DECLARATION FROM THE IDENTIFIER OUTWARD. IN int *p, THE STAR BINDS TO p, SO p IS A POINTER TO int.
THIS IS WHY int *p, q DECLARES ONE POINTER AND ONE PLAIN INTEGER, WHICH CATCHES PEOPLE CONSTANTLY.
Pointer arithmetic
ADDING n TO A POINTER OF TYPE T ADVANCES THE ADDRESS BY n TIMES sizeof(T) BYTES.
IF p IS AN int POINTER AT ADDRESS 1000 AND int IS 4 BYTES, THEN p PLUS 3 IS ADDRESS 1012, NOT 1003. THE SCALING IS AUTOMATIC AND CAUSES MOST CONFUSION.
Pointer subtraction
SUBTRACTING TWO POINTERS OF THE SAME TYPE GIVES THE NUMBER OF ELEMENTS BETWEEN THEM, NOT THE NUMBER OF BYTES.
COMPARISON AND SUBTRACTION ARE DEFINED ONLY WITHIN A SINGLE ARRAY, INCLUDING ONE POSITION PAST ITS END, WHICH IS WHAT MAKES THE STANDARD LOOP IDIOM LEGAL.
Array decay
AN ARRAY NAME DECAYS TO A POINTER TO ITS FIRST ELEMENT IN ALMOST EVERY CONTEXT, WHICH IS WHY arr[i] AND *(arr + i) ARE EQUIVALENT.
THE EXCEPTIONS ARE sizeof, WHICH GIVES THE WHOLE ARRAY'S SIZE, AND ADDRESS-OF, WHICH GIVES A POINTER TO ARRAY SO THAT ADDING 1 SKIPS THE ENTIRE ARRAY.
Array parameters
A PARAMETER DECLARED int a[] IS EXACTLY EQUIVALENT TO int *a.
SO sizeof INSIDE THE FUNCTION GIVES THE POINTER SIZE, AND A FUNCTION CAN NEVER DETERMINE AN ARRAY'S LENGTH FROM THE PARAMETER ALONE. AN ARRAY CANNOT BE ASSIGNED OR PASSED BY VALUE.
Two-dimensional arrays
FOR int a[R][C] IN ROW-MAJOR ORDER, THE ADDRESS OF a[i][j] IS THE BASE PLUS (i TIMES C PLUS j) TIMES sizeof(int).
A TWO-DIMENSIONAL ARRAY IS CONTIGUOUS AND IS NOT AN ARRAY OF POINTERS. int a[3][4] IS 12 CONTIGUOUS INTEGERS; int *b[3] IS 3 POINTERS THAT MAY POINT ANYWHERE.
Strings
A C STRING IS A CHARACTER ARRAY WITH A TERMINATING NULL CHARACTER, AND EVERY LIBRARY FUNCTION RELIES ON THAT TERMINATOR.
sizeof GIVES THE DECLARED SIZE INCLUDING THE TERMINATOR WHILE THE LENGTH FUNCTION EXCLUDES IT: FOR THE LITERAL GATE, 5 AND 4 RESPECTIVELY.
Parameter passing
C PASSES EVERY ARGUMENT BY VALUE WITHOUT EXCEPTION. THE APPEARANCE OF REFERENCE IS ACHIEVED BY PASSING A COPY OF AN ADDRESS.
THE FUNCTION CANNOT CHANGE THE CALLER'S POINTER, ONLY WHAT IT POINTS AT. CHANGING THE CALLER'S POINTER REQUIRES A POINTER TO POINTER.
Other passing mechanisms
CALL BY REFERENCE BINDS THE PARAMETER TO THE CALLER'S VARIABLE. CALL BY VALUE-RESULT COPIES IN ON ENTRY AND BACK ON EXIT. CALL BY NAME SUBSTITUTES THE ARGUMENT EXPRESSION AND RE-EVALUATES IT AT EVERY USE.
VALUE-RESULT DIFFERS FROM REFERENCE WHEN THE SAME VARIABLE IS PASSED TWICE OR MODIFIED CONCURRENTLY, WHICH IS EXACTLY WHAT SUCH QUESTIONS EXPLOIT.
Storage classes
AUTO LOCALS LIVE IN A BLOCK AND ARE INDETERMINATE. STATIC LOCALS LIVE FOR THE PROGRAM, ARE SCOPED TO THE BLOCK, AND DEFAULT TO ZERO. STATIC GLOBALS ARE FILE-SCOPED. EXTERN REFERS TO A DEFINITION ELSEWHERE.
A STATIC LOCAL KEEPS ITS VALUE BETWEEN CALLS BECAUSE IT LIVES IN THE DATA SEGMENT RATHER THAN ON THE STACK, AND ITS INITIALISER RUNS ONCE BEFORE MAIN.
The four memory regions
CODE SEGMENT FOR INSTRUCTIONS, DATA SEGMENT FOR GLOBALS AND STATICS, HEAP FOR ALLOCATED MEMORY, STACK FOR LOCALS AND CALL FRAMES.
THE HEAP MUST BE FREED EXPLICITLY AND THE STACK IS FREED AUTOMATICALLY, WHICH IS WHY FORGETTING TO FREE LEAKS MEMORY WHILE FORGETTING A LOCAL DOES NOT.
Returning a local's address
THE FRAME IS DESTROYED ON RETURN, SO THE POINTER REFERS TO MEMORY THAT NO LONGER BELONGS TO ANYONE.
IT OFTEN APPEARS TO WORK BECAUSE NOTHING HAS OVERWRITTEN THE BYTES YET, WHICH IS EXACTLY WHAT MAKES IT DANGEROUS.
The two parts of a recursion
A BASE CASE THAT RETURNS WITHOUT RECURSING, AND A RECURSIVE CASE THAT MOVES STRICTLY CLOSER TO THE BASE CASE.
OMITTING EITHER PRODUCES INFINITE RECURSION, AND QUESTIONS ARE USUALLY BUILT BY DAMAGING ONE OF THEM.
Order of work in recursion
WORK DONE BEFORE THE RECURSIVE CALL HAPPENS ON THE WAY DOWN; WORK DONE AFTER IT HAPPENS ON THE WAY BACK UP.
MOVING A SINGLE PRINT STATEMENT ACROSS THE RECURSIVE CALL REVERSES THE OUTPUT ORDER, WHICH IS THE WHOLE CONTENT OF A COMMON QUESTION TYPE.
Tail recursion
THE RECURSIVE CALL IS THE LAST OPERATION, WITH NOTHING LEFT TO DO AFTERWARDS.
IT CONVERTS MECHANICALLY INTO A LOOP AND SOME COMPILERS DO SO, ELIMINATING STACK GROWTH. A PRINT AFTER THE CALL MAKES A FUNCTION NOT TAIL RECURSIVE.
Reading a recurrence from code
COUNT THE NUMBER OF RECURSIVE CALLS AND THE SIZE OF EACH SUBPROBLEM, THEN ADD THE WORK DONE OUTSIDE THE CALLS.
ONE CALL ON n-1 WITH CONSTANT WORK GIVES LINEAR TIME. TWO CALLS ON n-1 GIVE EXPONENTIAL. TWO CALLS ON n/2 WITH LINEAR WORK GIVE n log n.
Space versus calls
THE SPACE COMPLEXITY OF A RECURSIVE FUNCTION IS THE MAXIMUM STACK DEPTH, NOT THE TOTAL NUMBER OF CALLS.
NAIVE FIBONACCI MAKES EXPONENTIALLY MANY CALLS BUT ITS STACK NEVER EXCEEDS DEPTH n, SO IT USES ONLY LINEAR SPACE. THIS DISTINCTION IS MISSED OFTEN.
Undefined behaviour
MODIFYING A VARIABLE MORE THAN ONCE BETWEEN SEQUENCE POINTS, READING UNINITIALISED MEMORY, DEREFERENCING A FREED OR NULL POINTER, AND INDEXING OUT OF BOUNDS.
NONE OF THESE RELIABLY CRASHES, WHICH IS WHAT MAKES THEM DANGEROUS. IF A QUESTION'S CODE CONTAINS ONE, THE INTENDED ANSWER IS USUALLY THAT THE BEHAVIOUR IS UNDEFINED.
Overflow asymmetry
SIGNED INTEGER OVERFLOW IS UNDEFINED. UNSIGNED OVERFLOW IS DEFINED TO WRAP MODULO 2 TO THE n.
THE ASYMMETRY SURPRISES PEOPLE AND IS WORTH REMEMBERING EXACTLY, SINCE IT DECIDES WHETHER A GIVEN EXPRESSION HAS A PREDICTABLE ANSWER.
⚠️

Traps GATE sets — and how to dodge them

These are the exact option-traps and misreads that cost marks under negative marking.

WATCH OUT
Reading int *p, q as declaring two pointers
The star binds to the identifier, not to the type. Only p is a pointer; q is a plain integer. Writing the star next to the identifier rather than the type makes the binding visible.
WATCH OUT
Treating pointer addition as byte addition
Adding n advances by n times the size of the pointed-to type. For an int pointer at address 1000, p plus 3 is 1012 on a machine with 4-byte integers, not 1003.
WATCH OUT
Expecting sizeof on an array parameter to give the array size
A parameter declared as an array is a pointer, so sizeof gives the pointer size. A function cannot determine an array's length from the parameter, which is why the length must be passed separately.
WATCH OUT
Confusing arr + 1 with &arr + 1
The first advances by one element because the array name decays to a pointer to element. The second advances by the whole array, because the address-of operator yields a pointer to array.
WATCH OUT
Passing a two-dimensional array where an array of pointers is expected
They have different layouts and different types. A two-dimensional array is one contiguous block, while an array of pointers is a block of addresses that may point anywhere, and neither converts to the other.
WATCH OUT
Expecting a swap function taking values to affect the caller
C passes by value, so the function exchanges its own copies and the caller is untouched. Addresses must be passed, and even then the addresses themselves are copies.
WATCH OUT
Passing a pointer where a pointer to pointer is needed
A function that must change which object the caller's pointer refers to needs the address of that pointer. Passing the pointer by value lets the function change the pointed-to data, not the pointer itself.
WATCH OUT
Expecting a static local to be re-initialised on each call
The initialiser runs once before main, not per call. The variable lives in the data segment for the whole program, which is exactly why it retains its value between calls.
WATCH OUT
Relying on the value of an uninitialised local
Locals are indeterminate while statics and globals are zero-initialised. Reading an uninitialised local is undefined behaviour, and it frequently produces plausible-looking values that change between runs.
WATCH OUT
Returning the address of a local variable
The frame is released on return, so the pointer refers to memory that belongs to no one. Return the value, allocate on the heap, or write into caller-provided storage instead.
WATCH OUT
Putting the print statement on the wrong side of a recursive call
Before the call means the output appears on the way down, in descending order for a decreasing parameter. After the call means it appears on the way back up, in ascending order. The bodies look almost identical.
WATCH OUT
Confusing total recursive calls with stack depth
Calls that have returned no longer occupy the stack. Naive Fibonacci makes exponentially many calls with a stack depth of only n, so its time is exponential while its space is linear.
WATCH OUT
Calling a function tail recursive when work follows the call
Tail recursion requires the recursive call to be the very last operation. A print statement or an addition after the call means the frame must be retained, and the recursion cannot become a loop.
WATCH OUT
Computing a specific answer for an expression with undefined behaviour
Expressions modifying a variable twice between sequence points have no correct answer. If a question's code contains one, the intended response is that the behaviour is undefined, and computing a value is the trap.
WATCH OUT
Treating a character array as a string without a terminator
Library functions read until a null character appears, so an array without one is read past its end. The declared size and the string length also differ by exactly the terminator.
WATCH OUT
Writing through a pointer to a string literal
A literal assigned to a pointer lives in read-only memory, so modification is undefined even though it compiles. Declaring a character array instead copies the literal into writable storage.

Exam-pattern practice

PYQ-style questions with full solutions. Work through them as a readiness check — mark yourself honestly and get your gap report at the end.

Readiness check

Are you exam-ready for Programming in C & Recursion?

9 problems from this chapter. Try each one, reveal the worked solution, mark yourself honestly — get your gap report at the end.

9 questions~6 min

5-minute revision

The whole chapter, distilled. Read this the night before the exam.

  • Draw the memory; do not reason about the text.
  • Read declarations from the identifier outward.
  • The star binds to the identifier, not the type.
  • Pointer addition scales by the pointed-to size.
  • Pointer subtraction gives an element count.
  • Comparison is defined only within one array.
  • An array name decays to a pointer to its first element.
  • sizeof and address-of are the two decay exceptions.
  • sizeof(arr) is the whole array.
  • &arr + 1 skips the entire array.
  • An array parameter is a pointer.
  • A function cannot recover an array's length.
  • Two-dimensional arrays are row-major and contiguous.
  • A 2D array is not an array of pointers.
  • A string is a character array plus a null terminator.
  • sizeof includes the terminator; length excludes it.
  • String literals through a pointer are read-only.
  • C passes everything by value.
  • Passing an address gives the appearance of reference.
  • Changing a caller's pointer needs a pointer to pointer.
  • Value-result differs from reference under aliasing.
  • Static locals live in the data segment.
  • Static initialisers run once, before main.
  • Uninitialised locals are indeterminate; statics are zero.
  • The heap is freed explicitly; the stack automatically.
  • Returning a local's address is undefined.
  • Every recursion needs a base case and progress.
  • Work before the call happens on the way down.
  • Work after the call happens on the way back up.
  • Tail recursion has nothing after the call.
  • Read the recurrence from calls, sizes and outside work.
  • Space is maximum stack depth, not total calls.
  • Modifying a variable twice between sequence points is undefined.
  • Signed overflow is undefined; unsigned wraps.

GATE question blueprint

How this topic is asked, tier by tier — so you can prep to the pattern.

Typical weightage: Programming & Data Structures contributes roughly 8-10 of the 72 core-CS marks; C programming and recursion supply 2-3 of those across 2-3 questions

Question styleMarks eachTypical countWhat it tests
Pointers1~1Declaration reading, pointer arithmetic scaling and dereference precedence
Arrays and pointers2~1Array decay, its two exceptions and the difference between arr+1 and &arr+1
Two-dimensional arrays2~1Row-major address computation and the contrast with an array of pointers
Parameter passing1~1Pass by value and when a pointer to pointer is required
Parameter passing mechanisms2~1Distinguishing value, reference, value-result and name under aliasing
Storage classes1~1Static locals, default initialisation and lifetime
Storage and lifetime2~1Frame destruction, returning local addresses and the correct alternatives
Recursion2~1Output order relative to the call and tail recursion
Recursion analysis2~1Counting calls, reading a recurrence and distinguishing depth from call count

Exam-hall strategy

Battle-tested tips from mentors and toppers for this topic under the sectional clock.

  1. Draw boxes, arrows and frames before computing any output.
  2. Check the pointed-to type before doing any pointer arithmetic.
  3. For sizeof questions, decide first whether array decay applies.
  4. For a swap or modify function, check whether an address was passed.
  5. For recursion output, check which side of the call the work sits on.
  6. Scan for undefined behaviour before computing a specific answer.
  7. C output questions are often MSQs or NATs, both of which carry no negative marking, so never leave one blank.
  8. For 1-mark and 2-mark MCQs, negative marking is -1/3 and -2/3, so guess only after eliminating an option.
  9. GATE gives a single freely-navigable 180-minute window, so flag a long trace and return to it.

Beyond the exam

Where this skill shows up in the job you're competing for — and in life.

Debugging a pointer bug

Drawing the frames and arrows is exactly what a debugger's stack and memory views show, and the reasoning is identical.

Reading a function signature

Recognising that a double pointer parameter means the function will change the caller's pointer is how allocation APIs are understood at a glance.

Diagnosing a use-after-return crash

The intermittent bug where a returned local's address works in testing and fails in production is the direct consequence of frame lifetime.

Choosing iteration over recursion

Knowing that stack depth rather than call count sets the space cost is what decides whether a recursive traversal is safe on deep inputs.

Where else this topic is tested

Prepare once, score in every exam that asks it.

GATE DAModerate overlap — programming questions appear in Python rather than C, but the recursion and complexity reasoning transfers directly
UGC NET Computer ScienceHigh overlap — pointer arithmetic, storage classes and parameter passing mechanisms are examined as direct recall
ISRO / BARC / DRDO computer science papersVery high overlap — C output prediction, recursion tracing and pointer questions are recurring MCQ topics

Questions aspirants ask

Pulled from the Q&A community and mentor sessions.

Because C's surprises come almost entirely from where values live and what an expression actually names, and both are visible in a drawing and invisible in the text. Consider a swap function taking two integers by value. Read as text, the body clearly exchanges its two parameters, and the code looks correct. Drawn as memory, the function's frame contains two boxes distinct from the caller's, the exchange happens in those boxes, and the frame vanishes on return — at which point the answer is obvious. The same applies to pointer arithmetic, where the scaling by element size is nowhere in the text but is unmissable if the boxes are drawn to scale, and to recursion, where the ordering of output depends on whether a statement sits above or below a call. The discipline is short. Draw a box for every variable, labelled with its name and current value. Draw an arrow from each pointer to the box it addresses. Draw a new group of boxes for each function call, stacked below the previous one, and erase a group when its function returns. Then execute the program one statement at a time, updating the drawing. It takes perhaps ninety seconds for a typical GATE program and it converts a question about language rules into a question about bookkeeping, which is far harder to get wrong under time pressure.

Because what was passed was a copy of an address, and the function modified the memory at that address rather than the copy itself. Nothing about arrays is special here; the same thing happens with any pointer. When an array name appears as an argument, it decays to a pointer to its first element, and that pointer value is copied into the parameter. The parameter is a genuine local variable holding an address, and assigning to it inside the function changes nothing outside. But dereferencing it reaches the caller's array, so writing through it changes the caller's data. Two consequences follow that questions exploit. First, a function cannot make the caller's array name refer to a different array, because it received only a copy of the address. Second, a function that must change the caller's pointer — for example, one that allocates memory and hands it back through a parameter — needs the address of that pointer, which is a pointer to pointer. Passing the pointer by value lets the function modify the pointed-to data and nothing more. The clean way to state the whole rule is that C copies the argument, always; what varies is what the copied value happens to be, and when that value is an address the function gains access to whatever lives there.

Because the recursive call divides the function body into a part that runs while descending and a part that runs while returning, and the two parts see the parameter values in opposite orders. Take a function called with n that prints and then calls itself with n minus 1. The print happens before the call, so the outermost frame prints its value first, then the next frame prints a smaller value, and so on: the output descends. Now move the print below the call. The outermost frame reaches the call immediately and does nothing; so does every frame below it, all the way to the base case. Only when the base case returns does anything print, and it prints from the innermost frame outward, so the output ascends. The parameter still descended on the way down; the printing simply happened during the unwinding. This is worth internalising because it generalises. Anything computed before the call — validation, accumulation into a parameter, printing — happens in the order of descent. Anything computed after the call — combining a returned value, printing, freeing — happens in the reverse order. A tree traversal is exactly this observation applied twice: preorder places the visit before both recursive calls, inorder between them, and postorder after both, and those three placements are the entire difference between the traversals.

Because a frame occupies the stack only while its call is active, and a call that has returned releases its frame immediately for reuse. Naive Fibonacci is the clean illustration. Computing the sixth value makes 25 calls in total, but those calls do not coexist. The evaluation is depth-first: the function calls itself on n minus 1, that call recurses down to the base case, unwinds completely, and only then does the original frame make its second call on n minus 2. At any instant, the stack contains one path from the root of the call tree to the currently executing call, and the longest such path follows the n minus 1 branch repeatedly, giving depth n. So the time is exponential in n while the space is linear. The distinction is examined directly and is easy to get wrong, because the call tree is the natural object to draw and its size is exponential. The correct question is not how many nodes the tree has but how deep it is. Two refinements are worth carrying. If a recursive function allocates a local array of size n at each level, the space becomes the depth times that array size, so a linear-depth recursion with linear locals uses quadratic space. And tail recursion, where nothing remains to be done after the call, allows a compiler to reuse the current frame instead of pushing a new one, reducing the space to constant.

By checking the code against a short list before attempting to compute anything, because if one of these appears, the intended answer is usually that no answer exists. The most common is modifying a variable more than once between sequence points, or modifying it and reading it for a purpose other than computing its new value. Expressions such as one that increments a variable twice within a single statement fall here, and different compilers genuinely produce different results. A related case is relying on the order in which function arguments are evaluated, which is unspecified: if one argument modifies a variable that another reads, the output depends on the compiler. Next come the memory errors: reading an uninitialised local, dereferencing a null or freed pointer, using a pointer to a variable whose frame has been destroyed, and indexing outside an array. None of these reliably crashes, which is why they survive testing and fail in production. Finally there is signed integer overflow, which is undefined, in contrast to unsigned overflow which is defined to wrap modulo two to the width. The practical exam consequence is that when a question presents code containing one of these, the options usually include an undefined or implementation-dependent choice, and that is the intended answer. Computing a specific value by assuming one particular evaluation order is precisely the trap the question was built around.
Header Logo