Functions, Arrays
& Strings
Organize code into reusable building blocks, store collections of data efficiently, and work with text — the three pillars that make real programs possible.
Introduction & Why Functions Exist
As programs grow, writing the same logic repeatedly becomes unmanageable. A calculator needs addition, subtraction, multiplication, and division — each potentially called dozens of times. Should we copy and paste the same 10-line block each time? No. We write it once, give it a name, and reuse it. That named, reusable block of code is called a function.
Code Reuse
Write once, call anywhere. The logic lives in one place and is shared across the entire program.
Easier Debugging
A bug in rectArea() is fixed once. Without functions you’d hunt the same bug in 50 places.
Testable Units
Each function can be tested in isolation before being integrated into the larger program.
Modular Design
Large software (an RTOS, a sensor driver library) is built from hundreds of focused functions.
What is a Function?
A function is a named block of code that performs a specific task. You call it by name, it executes its body, and optionally returns a result.
wash(), you get clean clothes. Functions work exactly the same way.Every function has five parts: return type, name, parameter list, body, and a return statement.
Advantages of Functions
| Advantage | What it means | Without functions |
|---|---|---|
| Reusability | Write once, call many times anywhere in the program | Copy-paste the same block everywhere |
| Readability | printMenu() is self-explanatory; 30 raw lines are not | Dense, hard-to-skim code |
| Maintainability | Fix a bug once in the function body | Hunt the same bug in dozens of copies |
| Testability | Test each function with known inputs and expected outputs | Must test the entire program at once |
| Abstraction | Hide complexity behind a clean name like sqrt() | Expose all implementation details everywhere |
| Modularity | Build large programs from small, focused pieces | One giant main() that does everything |
Function Anatomy
Every function has three stages in its lifecycle: declaration (prototype), definition (implementation), and call (invocation). Think of them as DDC — Declare, Define, Call.
#include <stdio.h>
/* 1. DECLARATION (prototype) — tells compiler the signature */
int add(int, int);
/* 3. DEFINITION — actual implementation */
int add(int a, int b)
{
return a + b; /* returns sum to caller */
}
int main()
{
/* 2. CALL — invoke the function */
int sum = add(10, 20);
printf("Sum = %d\n", sum); /* Sum = 30 */
return 0;
}“DDC — Declare, Define, Call” — the three stages of every function
Declaration & Definition
Function Declaration (Prototype)
A declaration tells the compiler everything it needs to know about a function before it sees the definition. It declares the function’s name, return type, and the types of its parameters — but contains no body.
return_type functionName(param_types);
/* Examples */
int add(int, int); /* two int params, returns int */
float average(float, float); /* two float params, returns float */
void greet(void); /* no params, returns nothing */
int square(int n); /* parameter name optional */main() calls add() before its definition appears in the file, the compiler won’t know the return type or parameter types. A prototype placed before main() solves this. Alternatively, define all functions before main().Function Definition
The definition contains the actual code. It specifies the parameter names and the body that runs when the function is called.
/* Returns an integer — caller must store or use the return value */
int square(int n)
{
return n * n;
}
/* Returns a float */
float average(float a, float b)
{
return (a + b) / 2.0f;
}
/* void — performs work but returns nothing */
void printLine()
{
printf("--------------------\n");
}
/* void with parameters */
void displayMarks(char name[], int marks)
{
printf("%s: %d\n", name, marks);
}| Aspect | Declaration | Definition |
|---|---|---|
| Also called | Prototype, forward declaration | Implementation |
| Has a body? | No | Yes |
| Ends with? | Semicolon ; | Closing brace } |
| Required? | Only if call precedes definition | Always required |
| Parameter names? | Optional (types suffice) | Required |
Function Call & Return
A function call transfers execution to the function body. When the function finishes (reaching return or the closing }), control returns to the caller and execution resumes from where the call was made.
Call transfers control to the function; return sends control (and a value) back to the caller.
int square(int x) { return x * x; } /* returns int */
float half(float x) { return x / 2.0f; } /* returns float */
void printBanner() { printf("===\n"); } /* returns nothing */
/* Multiple return points */
int absolute(int x)
{
if(x < 0) return -x; /* early return */
return x; /* normal return */
}Types of Functions
Four Categories by Return Type and Parameters
| Category | Return Type | Parameters | Example |
|---|---|---|---|
| No return, no params | void | None | void printMenu(void) |
| No return, with params | void | Yes | void display(int n) |
| Returns value, no params | non-void | None | int getInput(void) |
| Returns value, with params | non-void | Yes | int add(int a, int b) |
Parameters & Arguments
These two terms are often confused but mean different things. Parameters are the variables listed in the function definition. Arguments are the actual values supplied at the function call.
/* ↓ parameters (variables in definition) */
int multiply(int x, int y)
{
return x * y;
}
int main()
{
int ans = multiply(5, 6);
/* ↑ ↑ arguments (values at call site) */
printf("%d\n", ans); /* 30 */
return 0;
}| Term | Where it appears | What it is | Example |
|---|---|---|---|
| Parameter | Function definition | A named variable (placeholder) | int x, int y |
| Argument | Function call | The actual value passed in | 5, 6 |
add(10) when add expects two integers is a compile error.Call by Value
C passes arguments by value by default. The function receives a copy of each argument — not the original variable. Any changes made inside the function affect only the copy; the caller’s variable is untouched.
#include <stdio.h>
void increment(int x) /* x is a COPY of the argument */
{
x++; /* modifies the copy, not the original */
printf("inside: %d\n", x); /* 11 */
}
int main()
{
int num = 10;
increment(num); /* passes a copy of num */
printf("outside: %d\n", num); /* still 10 — unchanged! */
return 0;
}Different memory addresses — the function’s x is a completely separate copy of num.
Passing Address (Pointer Parameters)
C has no true “call by reference”, but you can achieve the same effect by passing the address of a variable. The function receives a pointer; dereferencing it modifies the original.
#include <stdio.h>
void increment(int *ptr) /* receives address of variable */
{
(*ptr)++; /* dereference to modify original */
}
void swap(int *a, int *b) /* classic pointer swap */
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int num = 10;
increment(&num); /* pass address of num */
printf("num = %d\n", num); /* 11 */
int x = 5, y = 9;
swap(&x, &y);
printf("x=%d y=%d\n", x, y); /* x=9 y=5 */
return 0;
}| Aspect | Call by Value | Passing Address |
|---|---|---|
| What is sent | A copy of the value | The memory address (&var) |
| Original variable | Unchanged | Can be modified via *ptr |
| Syntax at call site | func(x) | func(&x) |
| Syntax in definition | int x | int *ptr |
| Use when | Function should not modify caller’s data | Function needs to modify caller’s data |
Scope of Variables
Scope defines where a variable can be accessed. Variables declared inside a function only exist within that function; variables declared outside all functions exist everywhere in the file.
Local vs Global Comparison
| Property | Local Variable | Global Variable |
|---|---|---|
| Where declared | Inside a function or block | Outside all functions |
| Accessible from | Only that function/block | Any function in the file |
| Lifetime | Created when function starts, destroyed when it returns | Entire program lifetime |
| Default value | Undefined (garbage) | Zero |
| Preferred? | Yes — prefer local | Use sparingly |
Lifetime & Stack Memory
Every function call creates a stack frame — a private region of memory on the call stack that holds the function’s local variables and return address. When the function returns, its frame is popped and that memory is reclaimed.
Each function call pushes a new frame; return pops it. This is why local variables don’t persist between calls.
Variable Lifetime Summary
| Variable | Born | Dies | Where stored |
|---|---|---|---|
| Local (automatic) | When its containing function is called | When the function returns | Stack |
| Global / static | When the program starts | When the program exits | Data segment |
| Heap (dynamic) | When malloc()/calloc() is called | When free() is called | Heap (Ch5) |
Recursion (Introduction)
A function that calls itself is recursive. Each call pushes a new frame on the stack. A base case stops the chain — without one, the stack overflows and the program crashes.
#include <stdio.h>
int factorial(int n)
{
/* Base case: recursion stops here */
if(n == 0 || n == 1)
return 1;
/* Recursive case: call with smaller input */
return n * factorial(n - 1);
}
int main()
{
printf("5! = %d\n", factorial(5)); /* 120 */
return 0;
}What is an Array?
Suppose you need to store the marks of 100 students. You could declare 100 separate variables (marks1, marks2, …). That’s impractical. An array stores multiple values of the same type under a single name, in contiguous memory (side by side in RAM).
marks) but many elements (marks[0], marks[1], …). All units are in the same building; all elements are in contiguous memory.Five contiguous int slots. Index starts at 0; each slot is 4 bytes on a 32-bit system.
arr[0], not arr[1]. For an array of size N, valid indices are 0 through N–1. Accessing arr[N] is out-of-bounds — undefined behaviour that can corrupt memory silently.Declaring & Initializing Arrays
/* 1. Declare only — elements contain garbage */
int marks[5];
/* 2. Declare and initialize all elements */
int marks[5] = {90, 85, 70, 88, 95};
/* 3. Partial initialization — rest become 0 */
int nums[5] = {10, 20}; /* {10, 20, 0, 0, 0} */
/* 4. Size inference — compiler counts elements */
int scores[] = {100, 90, 80}; /* size is 3 automatically */
/* 5. Initialize all to zero (common in embedded) */
int buf[256] = {0}; /* all 256 elements = 0 */
/* 6. Access individual elements */
marks[2] = 100; /* change index 2 */
printf("%d\n", marks[0]); /* print index 0 */Reading & Printing with Loops
#include <stdio.h>
#define SIZE 5
int main()
{
int marks[SIZE];
/* Read */
printf("Enter %d marks:\n", SIZE);
for(int i = 0; i < SIZE; i++)
scanf("%d", &marks[i]);
/* Print */
printf("You entered: ");
for(int i = 0; i < SIZE; i++)
printf("%d ", marks[i]);
printf("\n");
/* Array length via sizeof (works in the same scope) */
int n = sizeof(marks) / sizeof(marks[0]);
printf("Elements: %d\n", n); /* 5 */
return 0;
}sizeof(marks) / sizeof(marks[0]) automatically calculates the number of elements. This is robust: if you change the array size constant, the division still gives the correct answer. Warning: this only works where the variable is declared as an array (not after it decays to a pointer when passed to a function).Array Operations
#include <stdio.h>
int main()
{
int marks[] = {90, 80, 70, 60, 50};
int n = sizeof(marks) / sizeof(marks[0]); /* 5 */
/* Sum */
int sum = 0;
for(int i = 0; i < n; i++)
sum += marks[i];
printf("Sum: %d\n", sum); /* 350 */
/* Average */
float avg = (float)sum / n;
printf("Average: %.1f\n", avg); /* 70.0 */
/* Maximum */
int max = marks[0];
for(int i = 1; i < n; i++)
if(marks[i] > max) max = marks[i];
printf("Max: %d\n", max); /* 90 */
/* Minimum */
int min = marks[0];
for(int i = 1; i < n; i++)
if(marks[i] < min) min = marks[i];
printf("Min: %d\n", min); /* 50 */
return 0;
}Reverse an Array
int arr[] = {1, 2, 3, 4, 5};
int n = 5;
int left = 0, right = n - 1;
while(left < right)
{
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
/* arr is now {5, 4, 3, 2, 1} */2D Arrays (Matrices)
A 2D array stores data in rows and columns — like a spreadsheet. The elements are laid out in memory row by row (row-major order), so they are still contiguous in RAM even though we think of them as a grid.
/* 3 rows, 4 columns */
int marks[3][4] = {
{90, 85, 88, 95}, /* row 0: student 1 */
{80, 75, 82, 91}, /* row 1: student 2 */
{70, 65, 76, 89} /* row 2: student 3 */
};
/* Access element at row 1, col 2 */
printf("%d\n", marks[1][2]); /* 82 */
/* Print entire matrix */
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
printf("%3d ", marks[i][j]);
printf("\n");
}Matrix Addition
int A[2][2] = {{1,2},{3,4}};
int B[2][2] = {{5,6},{7,8}};
int C[2][2];
for(int i = 0; i < 2; i++)
for(int j = 0; j < 2; j++)
C[i][j] = A[i][j] + B[i][j];
/* C = {{6,8},{10,12}} */#define ROWS 3 and #define COLS 4 at the top of your file. This lets you change the matrix size in one place rather than hunting through all the loop bounds.Arrays & Functions
When you pass an array to a function, C passes the address of the first element — not a copy of all the data. This makes array passing efficient (no copying), but means the function can modify the original array.
#include <stdio.h>
/* Size must be passed separately — sizeof doesn't work inside the function */
void printArray(int arr[], int size)
{
for(int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
int findSum(int arr[], int size)
{
int sum = 0;
for(int i = 0; i < size; i++)
sum += arr[i];
return sum;
}
int findMax(int arr[], int size)
{
int max = arr[0];
for(int i = 1; i < size; i++)
if(arr[i] > max) max = arr[i];
return max;
}
int main()
{
int numbers[] = {10, 40, 20, 50, 30};
int n = sizeof(numbers) / sizeof(numbers[0]);
printArray(numbers, n); /* 10 40 20 50 30 */
printf("Sum: %d\n", findSum(numbers, n)); /* Sum: 150 */
printf("Max: %d\n", findMax(numbers, n)); /* Max: 50 */
return 0;
}sizeof(arr) returns the size of the pointer (4 or 8 bytes), not the array size. Always pass the number of elements as a separate int size parameter.Passing 2D Arrays
/* Number of columns must be specified so compiler can calculate addresses */
void printMatrix(int m[][3], int rows)
{
for(int i = 0; i < rows; i++)
{
for(int j = 0; j < 3; j++)
printf("%d ", m[i][j]);
printf("\n");
}
}
int main()
{
int mat[2][3] = {{1,2,3},{4,5,6}};
printMatrix(mat, 2); /* 1 2 3 / 4 5 6 */
return 0;
}What is a String?
C has no built-in string type. A string is simply a char array that ends with the null character '\0' (ASCII 0). The null terminator is what makes functions like printf and strlen know where the string ends.
“Hello” requires 6 bytes, not 5. The '\0' at index 5 tells every string function where the string ends.
/* Method 1: String literal (preferred) — compiler adds \0 */
char name[] = "Hello"; /* 6 bytes: H e l l o \0 */
/* Method 2: Character array with explicit \0 */
char name[6] = {'H','e','l','l','o','\0'};
/* Method 3: Fixed size (must leave room for \0!) */
char city[20] = "Hyderabad"; /* 9 chars + \0, rest are \0 */
/* String pointer (read-only, stored in ROM on embedded) */
const char *msg = "Embedded"; /* pointer to string literal */String Input & Output
#include <stdio.h>
int main()
{
char name[30];
/* %s with scanf stops at whitespace */
printf("Enter single word: ");
scanf("%29s", name); /* no & needed — array decays to pointer */
printf("Hello, %s!\n", name);
/* To read a full line including spaces, use fgets */
char fullname[50];
printf("Enter full name: ");
fgets(fullname, sizeof(fullname), stdin);
printf("Full name: %s", fullname);
return 0;
}| Function | Reads spaces? | Buffer overflow safe? | Notes |
|---|---|---|---|
scanf("%s", s) | No | Only with width: %29s | Fast for single words |
fgets(s, n, stdin) | Yes | Yes — preferred | Includes trailing \n |
gets(s) | Yes | NEVER — removed in C11 | Buffer overflow danger |
gets() was removed from the C standard in C11 because it has no way to prevent buffer overflow. Any user can crash your program by typing more characters than the buffer holds. Always use fgets(name, sizeof(name), stdin) instead.String Library Functions
Include <string.h> to use these. They all rely on the null terminator to know where strings end.
| Function | Purpose | Example | Result |
|---|---|---|---|
strlen(s) | Length (excluding \0) | strlen("Hello") | 5 |
strcpy(dst,src) | Copy src into dst | strcpy(d,"Hi") | d = "Hi" |
strncpy(d,s,n) | Safe copy, max n chars | strncpy(d,s,9) | Up to 9 chars copied |
strcat(dst,src) | Append src to end of dst | strcat("Hi ","Bob") | "Hi Bob" |
strncat(d,s,n) | Safe append, max n chars | Buffer-safe version | |
strcmp(a,b) | Compare: 0 if equal, <0 or >0 otherwise | strcmp("A","A") | 0 |
strchr(s,c) | Pointer to first occurrence of c | strchr("Hello",'l') | Pointer to 3rd char |
strstr(s,t) | Pointer to first occurrence of substring | strstr("Hello","ll") | Pointer to "llo" |
#include <stdio.h>
#include <string.h>
int main()
{
char s1[] = "Hello ";
char s2[] = "World";
char dest[20];
/* strlen */
printf("Length: %zu\n", strlen(s2)); /* 5 */
/* strcpy */
strcpy(dest, s1);
printf("Copied: %s\n", dest); /* Hello */
/* strcat */
strcat(dest, s2);
printf("Joined: %s\n", dest); /* Hello World */
/* strcmp */
if(strcmp(s2, "World") == 0)
printf("Strings are equal\n"); /* prints */
/* Wrong way to compare — never do this */
/* if(s2 == "World") ← compares addresses, not content */
return 0;
}strcmp returns 0 when strings are equal, which is false in C’s boolean system. Always write if(strcmp(a, b) == 0), never if(strcmp(a, b)) when testing equality.Character Array vs String
A character array and a C string are similar but not identical. A C string is a character array that ends with '\0'. Without the null terminator, it is just an array of characters — string functions will not work correctly on it.
| Property | Character Array | C String |
|---|---|---|
Contains '\0'? | Not necessarily | Always |
Printable with %s? | Only if \0 present | Yes |
Works with strlen, strcmp? | Only if \0 present | Yes |
| Memory size | Exactly as declared | Content + 1 for \0 |
Strings & Functions
Strings are passed to functions just like arrays — by passing the address of the first character. Functions don’t need a separate size parameter because they can detect the end using '\0'.
#include <stdio.h>
/* Count characters until \0 */
int myStrlen(char s[])
{
int count = 0;
while(s[count] != '\0')
count++;
return count;
}
/* Print string in reverse */
void reversePrint(char s[])
{
int len = myStrlen(s);
for(int i = len - 1; i >= 0; i--)
printf("%c", s[i]);
printf("\n");
}
int main()
{
char word[] = "Embedded";
printf("Length: %d\n", myStrlen(word)); /* 8 */
printf("Reverse: ");
reversePrint(word); /* deddebmE */
return 0;
}Common Mistakes
Functions
Arrays
Strings
Best Practices
One function, one task
A function named calculateAndPrintAndSave() is doing too much. Split into calculate(), print(), and save().
Use meaningful names
calculateRectArea() is instantly understood. f1() requires reading the implementation.
Always declare prototypes
Prototypes at the top of the file allow you to define functions in any order without forward-declaration errors.
Prefer local variables
Use parameters and return values instead of globals. Functions that rely on globals are harder to test and reuse.
Use strn* functions
Prefer strncpy, strncat, and fgets over their unbounded counterparts to prevent buffer overflows.
Use #define for array sizes
#define MAX 100 lets you change the array size in one place. Never scatter the literal 100 throughout your code.
Base case in recursion
Every recursive function must have a reachable base case that stops the recursion. Missing it causes a stack overflow crash.
Pass array size explicitly
Always pass int size alongside an array parameter. sizeof on an array parameter gives you pointer size, not array size.
Memory Tricks
“DDC — the three stages of every function”
| Concept | Think of it as… |
|---|---|
| Call by value | Fax a document — the recipient gets a copy, your original is unchanged |
| Passing address | Give someone your house key — they can actually change things inside |
| Array index | Elevator floors starting at 0, not 1 |
| String null terminator | Period at the end of a sentence — it signals the sentence is over |
| Stack frame | A whiteboard for a function — erased when the function returns |
| Global variable | A shared whiteboard in the hallway — everyone can read and write it |
Interview Questions
- Declaration (prototype): tells the compiler the function’s name, return type, and parameter types. Has no body; ends with
;. Needed when the call appears before the definition. - Definition: the actual implementation with parameter names and a body in braces. This is where the work happens.
arr[5] is out-of-bounds — it reads or writes memory that belongs to some other variable or to unmapped memory. The result is undefined behaviour: the program may crash, silently corrupt other data, or appear to work. There is no automatic bounds check in C.'\0' (ASCII 0). The null terminator is required because string library functions (printf, strlen, strcpy, etc.) iterate through characters until they encounter '\0' to know where the string ends. Without it, functions would continue reading beyond the intended data.name == "Hello" compares the memory addresses of the two char arrays — which are always different locations even if the contents are identical. Use strcmp(name, "Hello") == 0 to compare the actual characters.Frequently Asked Questions
struct containing multiple fields; (3) use global variables (not recommended). Pointer parameters are the most common approach for returning two values like a quotient and remainder.sizeof or the address-of operator &), it decays to a pointer to its first element. This is why passing an array to a function works, but sizeof inside the function gives you the pointer size, not the array size.Practice Programs & Chapter Summary
- Write a function to add two numbers and return the sum.
- Write a function to find the square of a number.
- Demonstrate call by value: write
increment(n)and shownis unchanged after the call. - Read 5 integers into an array and print them in reverse using a loop.
- Read a string and print its length using
strlen().
- Write a function
maxOfTwo(int a, int b)that returns the larger value. - Write a function to calculate the average of an integer array.
- Write a function to reverse an array in-place.
- Write a program that reads a 3×3 matrix and computes the sum of each row.
- Write a function to count vowels in a string without using any library functions.
- Write a function that swaps two integers using pointers. Verify both variables are swapped in
main(). - Write a recursive function to compute the factorial of N. Handle negative input gracefully.
- Write a function to sort an integer array in ascending order using bubble sort.
- Write a program to check whether a string is a palindrome without using
strlenorstrrev. - Write a function that checks whether two strings are equal without using
strcmp(). Return 1 if equal, 0 otherwise.
- A function is a named block of code; it is declared (prototype), defined (implementation), and called (invoked) — DDC.
- C passes arguments by value (copy). Pass an address (
&var) to allow the function to modify the caller’s variable. - Parameters are variables in the definition; arguments are values at the call site.
- Local variables exist only during their function’s execution; global variables exist for the entire program.
- Every function call pushes a stack frame; when the function returns, the frame (and all its locals) is destroyed.
- Recursive functions must have a base case; without one, the stack overflows.
- An array is a collection of same-type elements in contiguous memory. Index starts at 0; last valid index is size–1.
- Arrays are passed to functions as a pointer to the first element. Always pass the size separately.
- A 2D array is a matrix stored in row-major order. Access:
arr[row][col]. - A string is a
chararray terminated by'\0'. Always allocate one extra byte for the terminator. - Use
fgetsfor safe string input; never usegets(). - Use
strcmp(a,b)==0to compare strings; never use==. - Key string functions:
strlen,strcpy/strncpy,strcat/strncat,strcmp,strchr.