Why Functions Exist
A function is a named, self-contained block of code that performs one specific job. Without functions, every program would be one giant main() — no reuse, no organisation, no testing. Functions solve four fundamental problems:
- Reusability — write once, call from anywhere, any number of times
- Abstraction — caller doesn't need to know HOW the function works, only WHAT it returns
- Modularity — break a 1000-line program into 20 focused functions, each easy to understand
- Testability — each function can be tested independently; bugs are isolated
In C, main() itself is a function — it is called by the operating system when your program starts. Every C program is a collection of functions calling each other.
| Type | Description | Examples |
|---|---|---|
| Library functions | Pre-built — ready to use | printf, scanf, sqrt, strlen |
| User-defined functions | Written by you | add(), isPrime(), factorial() |
| Recursive functions | Call themselves | factorial(n), fibonacci(n) |
| Inline functions | Code expanded at call site (C99) | inline int max(int a, int b) |
Anatomy of a Function — 4 Parts
Every function has exactly four parts — return type, name, parameters, body
void if it gives nothing back.
calculateArea, isPrime, sortArray
#include <stdio.h> /* Return type: int Name: add Parameters: two ints Body: returns their sum */ int add(int a, int b) { return a + b; } /* Return type: void (returns nothing) Name: printLine Parameters: none (empty) Body: prints dashes */ void printLine() { printf("------------------\n"); } /* Return type: float Name: average Parameters: array + its size Body: computes and returns float average */ float average(int arr[], int n) { int sum = 0; for (int i = 0; i < n; i++) sum += arr[i]; return (float)sum / n; } int main() { int nums[] = {10, 20, 30, 40}; printLine(); printf("add(5, 8) = %d\n", add(5, 8)); printf("average(nums,4) = %.1f\n", average(nums, 4)); printLine(); return 0; }
------------------ add(5, 8) = 13 average(nums,4) = 25.0 ------------------
Return Types — Every Possibility
| Return Type | Meaning | return statement | Use case |
|---|---|---|---|
| void | Returns nothing | return; (optional) | print functions, setters |
| int | Returns integer | return n; | calculations, status codes |
| float / double | Returns decimal | return 3.14; | math functions |
| char | Returns character | return 'A'; | char processing |
| int* / char* | Returns pointer | return ptr; | string functions, dynamic memory |
| struct | Returns struct | return s; | complex data objects |
#include <stdio.h> /* void — no return value */ void greet(char *name) { printf("Hello, %s!\n", name); } /* int — returns integer */ int square(int n) { return n * n; } /* char — returns character (grade) */ char getGrade(int marks) { if (marks >= 90) return 'A'; else if (marks >= 75) return 'B'; else if (marks >= 55) return 'C'; else return 'F'; } /* int — returns 1 (true) or 0 (false) — boolean pattern */ int isEven(int n) { return (n % 2 == 0); } int main() { greet("Ananta"); printf("square(7) = %d\n", square(7)); printf("grade(88) = %c\n", getGrade(88)); printf("isEven(4) = %d\n", isEven(4)); /* 1 = true */ printf("isEven(7) = %d\n", isEven(7)); /* 0 = false */ return 0; }
Hello, Ananta! square(7) = 49 grade(88) = B isEven(4) = 1 isEven(7) = 0
Function Prototype — Forward Declaration
C reads files top to bottom. If you call a function before it is defined, the compiler doesn't know its signature — what types it accepts, what type it returns. A prototype (also called a forward declaration) solves this by telling the compiler the function's signature before its full definition.
- Prototype syntax:
return_type function_name(param_types); - Parameter names are optional in prototype — types are enough
- Without prototype: implicit declaration assumed (int) — dangerous in C
- Header files (.h) are essentially collections of prototypes
#include <stdio.h> /* ── PROTOTYPES (forward declarations) ──────────────── */ int factorial(int n); int isPrime(int n); float power(float base, int exp); /* names optional: float power(float, int); also valid */ /* ── main uses the functions ─────────────────────────── */ int main() { printf("5! = %d\n", factorial(5)); printf("isPrime(17) = %d\n", isPrime(17)); printf("2^10 = %.0f\n", power(2, 10)); return 0; } /* ── definitions come AFTER main ────────────────────── */ int factorial(int n) { if (n <= 1) return 1; return n * factorial(n - 1); } int isPrime(int n) { if (n <= 1) return 0; for (int i = 2; i * i <= n; i++) if (n % i == 0) return 0; return 1; } float power(float base, int exp) { float result = 1; for (int i = 0; i < exp; i++) result *= base; return result; }
5! = 120 isPrime(17) = 1 2^10 = 1024
Call Stack and Stack Frames
Every time a function is called, C creates a stack frame in memory — a private block that holds the function's local variables, parameters, and the return address (where to continue after the function returns). These frames are stacked on top of each other. When a function returns, its frame is destroyed and all its local variables disappear.
This is why local variables do not persist between function calls — they live only as long as their stack frame exists.
Call stack during execution of: main → add → square (inner call)
#include <stdio.h> void c_func() { int z = 300; printf(" c_func: z=%d &z=%p\n", z, (void*)&z); } void b_func() { int y = 200; printf(" b_func: y=%d &y=%p\n", y, (void*)&y); c_func(); /* c_func frame pushed ON TOP */ printf(" b_func: back from c_func\n"); } void a_func() { int x = 100; printf("a_func: x=%d &x=%p\n", x, (void*)&x); b_func(); /* b_func frame pushed ON TOP */ printf("a_func: back from b_func\n"); } int main() { printf("=== Call Stack Demo ===\n"); a_func(); printf("main: back from a_func\n"); return 0; }
=== Call Stack Demo === a_func: x=100 &x=0x7fff...c0 b_func: y=200 &y=0x7fff...a0 c_func: z=300 &z=0x7fff...80 b_func: back from c_func a_func: back from b_func main: back from a_func
Scope and Storage Class
Scope defines where a variable is visible (can be accessed). Lifetime defines how long a variable exists in memory. Every variable in C has both a scope and a lifetime determined by where and how it is declared.
Scope levels — inner scopes can access outer, but NOT vice versa
#include <stdio.h> int globalCount = 0; /* global — visible everywhere, persists forever */ void demoStatic() { static int calls = 0; /* static local — persists between calls! */ int normal = 0; /* automatic — re-created each call */ calls++; normal++; globalCount++; printf("calls(static)=%d normal(auto)=%d global=%d\n", calls, normal, globalCount); } void demoRegister() { register int i; /* hint to compiler: store in CPU register (fast) */ for (i = 0; i < 5; i++) printf("%d ", i); printf("\n"); } int main() { demoStatic(); /* calls=1 normal=1 global=1 */ demoStatic(); /* calls=2 normal=1 global=2 ← static persists! */ demoStatic(); /* calls=3 normal=1 global=3 */ demoRegister(); return 0; }
calls(static)=1 normal(auto)=1 global=1 calls(static)=2 normal(auto)=1 global=2 calls(static)=3 normal(auto)=1 global=3 0 1 2 3 4
| Storage Class | Keyword | Scope | Lifetime | Default value |
|---|---|---|---|---|
| Automatic | (default inside fn) | Local — within block | Until block ends | Garbage |
| Static local | static inside fn | Local — within function | Entire program | 0 |
| Global | Declared outside all | Whole file | Entire program | 0 |
| Register | register | Local — within block | Until block ends | Garbage |
| External | extern | Multiple files | Entire program | 0 |
Pass by Value — A Copy is Sent
By default, C passes all arguments by value — the function receives a copy of the argument. Any changes made to the parameter inside the function do not affect the original variable in the caller. The two variables are completely separate.
#include <stdio.h> void tryToChange(int x) { printf(" Inside fn: x = %d (copy)\n", x); x = 999; /* changes LOCAL COPY only — original untouched */ printf(" Inside fn: x = %d (after change)\n", x); } /* Classic swap THAT DOESN'T WORK — pass by value */ void wrongSwap(int a, int b) { int temp = a; a = b; b = temp; /* a and b are copies — originals in main unchanged! */ } int main() { int num = 42; printf("Before: num = %d\n", num); tryToChange(num); printf("After: num = %d (UNCHANGED!)\n", num); int p = 10, q = 20; printf("\nBefore wrongSwap: p=%d q=%d\n", p, q); wrongSwap(p, q); printf("After wrongSwap: p=%d q=%d (STILL SAME!)\n", p, q); return 0; }
Before: num = 42 Inside fn: x = 42 (copy) Inside fn: x = 999 (after change) After: num = 42 (UNCHANGED!) Before wrongSwap: p=10 q=20 After wrongSwap: p=10 q=20 (STILL SAME!)
Pass by Reference — Using Pointers
To actually modify the caller's variable, pass its address using the & operator. The function receives a pointer (the address), uses *ptr to dereference and reach the original value. Any change via *ptr directly modifies the original variable.
#include <stdio.h> /* Correct swap — receives ADDRESSES of a and b */ void swap(int *a, int *b) { int temp = *a; /* read value at address a */ *a = *b; /* write into location a */ *b = temp; /* write into location b */ } /* Get both min and max in one function call */ void minMax(int arr[], int n, int *minOut, int *maxOut) { *minOut = arr[0]; *maxOut = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] < *minOut) *minOut = arr[i]; if (arr[i] > *maxOut) *maxOut = arr[i]; } } int main() { int x = 10, y = 25; printf("Before swap: x=%d y=%d\n", x, y); swap(&x, &y); /* pass ADDRESSES */ printf("After swap: x=%d y=%d\n", x, y); int arr[] = {40, 12, 75, 3, 58}; int mn, mx; minMax(arr, 5, &mn, &mx); printf("Min=%d Max=%d\n", mn, mx); return 0; }
Before swap: x=10 y=25 After swap: x=25 y=10 Min=3 Max=75
Arrays as Function Parameters
Arrays in C are always passed by reference — even without &. When you write void sort(int arr[], int n), C passes the address of the first element. Any changes to arr[i] inside the function modify the original array in the caller. There is no copy of the array made.
#include <stdio.h> /* These three declarations are equivalent: */ /* void sort(int arr[], int n) */ /* void sort(int arr[10], int n) */ /* void sort(int *arr, int n) */ void bubbleSort(int arr[], int n) { int i, j, temp; for (i = 0; i < n-1; i++) for (j = 0; j < n-i-1; j++) if (arr[j] > arr[j+1]) { temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp; } } /* original array IS sorted — not a copy */ void printArr(int arr[], int n) { for (int i = 0; i < n; i++) printf("%d ", arr[i]); printf("\n"); } /* 2D array parameter — MUST specify column count */ void printMatrix(int m[][3], int rows) { for (int i=0;i<rows;i++) { for(int j=0;j<3;j++) printf("%4d",m[i][j]); printf("\n"); } } int main() { int arr[] = {64, 25, 12, 92, 43}; printf("Before: "); printArr(arr, 5); bubbleSort(arr, 5); printf("After: "); printArr(arr, 5); int mat[2][3] = {{1,2,3},{4,5,6}}; printMatrix(mat, 2); return 0; }
Before: 64 25 12 92 43 After: 12 25 43 64 92 1 2 3 4 5 6
Recursion — Deep Understanding
Recursion is when a function calls itself. Every recursive solution must have:
- Base case — the condition that stops the recursion. Without it: infinite recursion → stack overflow → crash
- Recursive case — the function calling itself with a smaller version of the problem
- Progress toward base case — each recursive call must get closer to the base case, never further
#include <stdio.h> /* ── Factorial: n! = n × (n-1)! ─────────────────────── */ int factorial(int n) { if (n <= 1) return 1; /* base case */ return n * factorial(n - 1); /* recursive case */ } /* Trace for factorial(4): factorial(4) = 4 * factorial(3) factorial(3) = 3 * factorial(2) factorial(2) = 2 * factorial(1) factorial(1) = 1 ← BASE CASE unwind: 1 → 2 → 6 → 24 */ /* ── Fibonacci: fib(n) = fib(n-1) + fib(n-2) ──────── */ int fib(int n) { if (n <= 0) return 0; if (n == 1) return 1; /* two base cases */ return fib(n-1) + fib(n-2); /* double recursion */ } /* ── Power: base^exp using recursion ───────────────── */ double power(double base, int exp) { if (exp == 0) return 1; /* anything^0 = 1 */ if (exp < 0) return 1.0 / power(base, -exp); if (exp % 2 == 0) /* FAST power: O(log n) */ return power(base * base, exp / 2); return base * power(base, exp - 1); } /* ── Sum of digits recursively ──────────────────────── */ int digitSum(int n) { if (n == 0) return 0; return (n % 10) + digitSum(n / 10); } int main() { printf("factorial(6) = %d\n", factorial(6)); printf("fib(10) = %d\n", fib(10)); printf("power(2,10) = %.0f\n", power(2,10)); printf("digitSum(9875) = %d\n", digitSum(9875)); printf("\nFibonacci series: "); for (int i = 0; i < 10; i++) printf("%d ", fib(i)); return 0; }
factorial(6) = 720 fib(10) = 55 power(2,10) = 1024 digitSum(9875) = 29 Fibonacci series: 0 1 1 2 3 5 8 13 21 34
Naive recursive fib(n) has O(2ⁿ) time — fib(40) makes ~300 million calls!
Iterative fib(n) has O(n) time — always prefer iterative for Fibonacci.
Fast power using divide-and-conquer has O(log n) — 2^1000 needs only 10 calls.
Function Pointers
Functions in C are stored in memory like variables. A function pointer stores the address of a function, allowing you to call a function through the pointer or pass a function as an argument to another function. This is the foundation of callbacks, jump tables, and plugin architectures.
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } /* A function that TAKES a function pointer as argument */ void apply(int x, int y, int (*op)(int,int), char *name) { printf("%s(%d, %d) = %d\n", name, x, y, op(x, y)); } int main() { /* Declare a function pointer: int (*fp)(int, int) */ int (*fp)(int, int); fp = add; /* point to add function */ printf("Direct call: %d\n", fp(10, 5)); fp = sub; /* now point to sub */ printf("Direct call: %d\n", fp(10, 5)); /* Pass function as argument — callback pattern */ apply(10, 5, add, "add"); apply(10, 5, sub, "sub"); apply(10, 5, mul, "mul"); /* Array of function pointers — dispatch table */ int (*ops[3])(int,int) = {add, sub, mul}; char *opName[] = {"+", "-", "*"}; printf("\nDispatch table:\n"); for (int i = 0; i < 3; i++) printf("10 %s 5 = %d\n", opName[i], ops[i](10, 5)); return 0; }
Direct call: 15 Direct call: 5 add(10, 5) = 15 sub(10, 5) = 5 mul(10, 5) = 50 Dispatch table: 10 + 5 = 15 10 - 5 = 5 10 * 5 = 50
GCD and LCM Using Euclid's Algorithm
#include <stdio.h> /* Euclid's algorithm: gcd(a,b) = gcd(b, a%b) */ int gcd(int a, int b) { if (b == 0) return a; /* base case */ return gcd(b, a % b); /* a%b gets smaller every call */ } int lcm(int a, int b) { return (a / gcd(a, b)) * b; /* avoid overflow: divide first */ } int main() { int pairs[][2] = {{48,18},{100,75},{17,13},{0,5}}; printf("%-8s %-8s %-8s %-8s\n","a","b","GCD","LCM"); printf("--------------------------------\n"); for (int i = 0; i < 4; i++) { int a=pairs[i][0], b=pairs[i][1]; printf("%-8d %-8d %-8d %d\n",a,b,gcd(a,b),lcm(a,b)); } }
a b GCD LCM -------------------------------- 48 18 6 144 100 75 25 300 17 13 1 221 0 5 5 0
Tower of Hanoi — Classic Recursion
Move n disks from peg A to peg C using peg B as auxiliary. Rule: never place a larger disk on a smaller one. The elegant recursive insight: to move n disks, move n-1 to auxiliary, move disk n to target, then move n-1 from auxiliary to target.
#include <stdio.h> int moveCount = 0; void hanoi(int n, char from, char to, char aux) { if (n == 0) return; /* base case — nothing to move */ /* Step 1: move top n-1 disks from → aux */ hanoi(n-1, from, aux, to); /* Step 2: move disk n from → to */ printf("Move disk %d: %c → %c\n", n, from, to); moveCount++; /* Step 3: move n-1 disks from aux → to */ hanoi(n-1, aux, to, from); } int main() { int n = 3; printf("Tower of Hanoi (%d disks):\n", n); printf("------------------------------\n"); hanoi(n, 'A', 'C', 'B'); printf("Total moves: %d (= 2^%d - 1)\n", moveCount, n); }
Tower of Hanoi (3 disks): ------------------------------ Move disk 1: A → C Move disk 2: A → B Move disk 1: C → B Move disk 3: A → C Move disk 1: B → A Move disk 2: B → C Move disk 1: A → C Total moves: 7 (= 2^3 - 1)
Binary Search — Recursive & Iterative
#include <stdio.h> /* Recursive binary search */ int bsearchRec(int arr[], int low, int high, int key) { if (low > high) return -1; /* not found */ int mid = low + (high - low) / 2; /* avoids int overflow */ if (arr[mid] == key) return mid; else if (arr[mid] < key) return bsearchRec(arr, mid+1, high, key); else return bsearchRec(arr, low, mid-1, key); } /* Iterative binary search */ int bsearchIter(int arr[], int n, int key) { int low = 0, high = n - 1; while (low <= high) { int mid = low + (high - low) / 2; if (arr[mid] == key) return mid; else if (arr[mid] < key) low = mid + 1; else high = mid - 1; } return -1; } int main() { int arr[] = {2,5,8,12,16,23,38,56,72,91}; int n = 10; int tests[] = {23, 56, 1, 100}; printf("Array: 2 5 8 12 16 23 38 56 72 91\n\n"); for (int i = 0; i < 4; i++) { int k = tests[i]; int r = bsearchRec(arr, 0, n-1, k); printf("Search %3d: index=%2d (%s)\n", k, r, r==-1?"NOT FOUND":"FOUND"); } }
Array: 2 5 8 12 16 23 38 56 72 91 Search 23: index= 5 (FOUND) Search 56: index= 7 (FOUND) Search 1: index=-1 (NOT FOUND) Search 100: index=-1 (NOT FOUND)
String Utilities — Custom String Library
#include <stdio.h> #include <ctype.h> /* Count length without strlen */ int myLen(char *s){ int i=0; while(s[i]) i++; return i; } /* Reverse string in-place */ void myReverse(char *s) { int l=0, r=myLen(s)-1; while(l<r){ char t=s[l];s[l++]=s[r];s[r--]=t; } } /* Check palindrome */ int isPalin(char *s) { int l=0, r=myLen(s)-1; while(l<r) if(s[l++]!=s[r--]) return 0; return 1; } /* Convert to title case */ void titleCase(char *s) { int newWord = 1; for(int i=0;s[i];i++){ if(s[i]==' ') newWord=1; else if(newWord) { s[i]=toupper(s[i]); newWord=0; } else s[i]=tolower(s[i]); } } /* Count a specific character */ int countChar(char *s, char c) { int cnt=0; for(int i=0;s[i];i++) if(s[i]==c) cnt++; return cnt; } int main() { char a[] = "racecar"; char b[] = "hello world"; char c[] = "hello ananta"; printf("len('%s') = %d\n", a, myLen(a)); printf("isPalin('%s') = %d\n", a, isPalin(a)); printf("isPalin('%s') = %d\n", b, isPalin(b)); myReverse(b); printf("reverse: %s\n", b); titleCase(c); printf("titleCase: %s\n", c); printf("countChar('racecar','a') = %d\n", countChar(a, 'a')); }
len('racecar') = 7
isPalin('racecar') = 1
isPalin('hello world') = 0
reverse: dlrow olleh
titleCase: Hello Ananta
countChar('racecar','a') = 3
IIT-Level Quiz — 6 Questions
What is the output? void f(int x){x=99;} int main(){int a=5; f(a); printf("%d",a);}
A static local variable is initialised to 0 automatically. It retains its value between function calls. A normal local variable does NOT. Why?
How many times is factorial(0) called when computing factorial(4)?
A function needs to return two values — the sum and product of two numbers. What is the correct C approach?
Why is iterative Fibonacci O(n) but recursive Fibonacci O(2ⁿ)?
Arrays are always passed by reference in C — even without &. Why?
Mastery Checklist
- I know the 4 parts of every function: return type, name, parameters, body
- I can write functions with void, int, float, char, and pointer return types
- I understand prototypes and when they are required
- I can explain call stack — each function call creates a stack frame
- I understand scope: local, global, block — and that inner can see outer but not vice versa
- I know the 4 storage classes: auto, static, register, extern
- I know static local variables persist between function calls (data segment, not stack)
- I understand pass by value — function gets a COPY, originals unchanged
- I can implement correct swap using pointers (pass by reference)
- I know arrays are always passed by reference (array name = pointer to first element)
- I can write recursive functions with correct base case and recursive case
- I know recursive Fibonacci is O(2ⁿ) and iterative is O(n)
- I can declare and use function pointers including arrays of function pointers
- I completed all 6 quiz questions