Table of Contents
- Introduction to Functions
- Function Declaration and Definition
- Function Prototypes
- The main() Function
- Call by Value
- Call by Reference
- Nested Functions
- Variadic Functions
- User-Defined Functions
- Callback Functions
- Return Statements
- Recursion
- Predefined Identifier func
Introduction to Functions
What is a Function?
A function in C is a self-contained block of code designed to perform a specific, well-defined task. Think of functions like specialized workers in a factory: each worker has a specific job, and when you need that job done, you call upon that particular worker.
Why Functions Are Needed
Functions serve several critical purposes in programming:
Code Reusability: Like having a template for a common document, you write the code once and use it multiple times throughout your program.
Modular Programming: Complex problems become manageable when broken into smaller, independent pieces. Consider a banking system where separate functions handle deposits, withdrawals, balance checks, and interest calculations.
Easier Maintenance: When code is organized into functions, fixing bugs or adding features becomes simpler. You only need to modify the specific function rather than searching through thousands of lines.
Better Readability: Well-named functions make code self-documenting. Reading calculateInterest() is clearer than reading 20 lines of mathematical operations.
Real-World Analogy
Imagine a restaurant kitchen. Instead of one chef doing everything, you have:
- A prep chef (input processing)
- A line cook (main processing)
- A pastry chef (specialized operations)
- A plating specialist (output formatting)
Each performs their specific task efficiently, and the head chef (main function) coordinates them all.
Function Declaration and Definition
Structure of a Function
Every function in C consists of several components:
return_type function_name(parameter_list) {
// Function body
// Statements to execute
return value;
}
Components Explained:
- Return Type: Specifies what type of data the function sends back (int, float, void, etc.)
- Function Name: An identifier following C naming rules
- Parameter List: Input values the function needs (optional)
- Function Body: The actual code that executes
- Return Statement: Sends data back to the caller (required for non-void functions)
Example: Temperature Converter
#include <stdio.h>
// Function to convert Celsius to Fahrenheit
float celsiusToFahrenheit(float celsius) {
float fahrenheit = (celsius * 9.0 / 5.0) + 32.0;
return fahrenheit;
}
int main() {
float temp = 25.0;
float converted = celsiusToFahrenheit(temp);
printf("%.2f°C = %.2f°F\n", temp, converted);
return 0;
}
When to Use Functions
Use functions when:
- A task will be performed multiple times
- Code complexity needs reduction
- Testing specific functionality in isolation
- Creating reusable libraries
Avoid excessive functions when:
- The task is performed only once and is very simple
- The function would be just 2-3 lines with no reusability
- Function call overhead outweighs benefits (rare in modern systems)
Function Prototypes
What is a Function Prototype?
A function prototype is a forward declaration that tells the compiler about a function's existence before its actual definition appears. Think of it as a table of contents in a book—it tells you what's coming without providing the full details yet.
Syntax
return_type function_name(parameter_types);
Example: Multiple Prototypes
#include <stdio.h>
// Function prototypes
int add(int, int);
int subtract(int, int);
int multiply(int, int);
float divide(int, int);
int main() {
int a = 20, b = 10;
printf("Addition: %d\n", add(a, b));
printf("Subtraction: %d\n", subtract(a, b));
printf("Multiplication: %d\n", multiply(a, b));
printf("Division: %.2f\n", divide(a, b));
return 0;
}
int add(int x, int y) {
return x + y;
}
int subtract(int x, int y) {
return x - y;
}
int multiply(int x, int y) {
return x * y;
}
float divide(int x, int y) {
return (float)x / y;
}
Why Function Prototypes Are Important
Type Checking: The compiler verifies that function calls match the declared signature, catching errors at compile-time rather than runtime.
Code Organization: Prototypes allow functions to be defined in any order or even in separate files, improving code structure.
Documentation: Prototypes serve as a quick reference for how to use a function.
When Prototypes Are Required
- When a function is called before its definition
- When functions are defined in separate source files
- When creating header files for libraries
When Prototypes Can Be Omitted
- When the function definition appears before any calls to it
- Not recommended even then, as it's good practice to use them
The main() Function
Role of main()
The main() function is the entry point of every C program. When you run a program, the operating system calls main() to begin execution. Think of it as the conductor of an orchestra—it starts the performance and coordinates all other functions.
Valid Signatures
// Standard form
int main() {
// Code here
return 0;
}
// Explicit void parameters
int main(void) {
// Code here
return 0;
}
// With command-line arguments
int main(int argc, char *argv[]) {
// Code here
return 0;
}
Return Values from main()
- 0: Indicates successful execution
- Non-zero: Indicates an error occurred
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
printf("Error: Could not open file\n");
return 1; // Error code
}
// Process file...
fclose(file);
return 0; // Success
}
Command-Line Arguments
Command-line arguments allow programs to receive input when launched from a terminal or command prompt.
argc: Argument count—number of arguments passed
argv: Argument vector—array of string pointers
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Program name: %s\n", argv[0]);
printf("Number of arguments: %d\n", argc);
for (int i = 1; i < argc; i++) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}
Important Constraints on main()
- Cannot be declared as
inline - Cannot be declared as
static - Cannot have its address taken
- Cannot be called recursively from within the program
- Should return
int(notvoid)
Call by Value
What is Call by Value?
Call by Value is the default parameter passing mechanism in C. When you pass a variable to a function, the function receives a copy of the value, not the original variable itself. Imagine photocopying a document—you can write on the copy without affecting the original.
How It Works
#include <stdio.h>
void modifyValue(int x) {
x = 100; // This only changes the local copy
printf("Inside function: x = %d\n", x);
}
int main() {
int num = 50;
printf("Before function call: num = %d\n", num);
modifyValue(num);
printf("After function call: num = %d\n", num);
return 0;
}
Output:
Before function call: num = 50
Inside function: x = 100
After function call: num = 50
Memory Visualization
When modifyValue(num) is called:
Main Function Memory:
num (address: 1000) = 50
Function Memory:
x (address: 2000) = 50 (copy of num)
The parameter x is a separate variable in a different memory location. Changes to x don't affect num.
When to Use Call by Value
Use when:
- The function needs to perform calculations without affecting original data
- You want to prevent accidental modifications
- Working with simple data types (int, float, char)
- The data size is small
Example: Interest Calculator
#include <stdio.h>
float calculateInterest(float principal, float rate, int time) {
// These calculations don't affect the original values
float interest = (principal * rate * time) / 100;
return interest;
}
int main() {
float amount = 10000.0;
float interestRate = 5.5;
int years = 3;
float interest = calculateInterest(amount, interestRate, years);
printf("Principal: %.2f\n", amount); // Unchanged
printf("Interest: %.2f\n", interest);
printf("Total: %.2f\n", amount + interest);
return 0;
}
Limitations of Call by Value
- Cannot modify the original variable
- Inefficient for large data structures (arrays, structs)
- Cannot return multiple values through parameters
Call by Reference
What is Call by Reference?
Call by Reference passes the address of a variable rather than its value. The function can then access and modify the original variable directly. Think of it like giving someone the key to your house rather than a photograph of it.
The Address Operator (&) and Pointers
Address Operator (&): Retrieves the memory address of a variable
Pointer: A variable that stores a memory address
Dereference Operator (*): Accesses the value at a memory address
#include <stdio.h>
int main() {
int value = 42;
int *ptr = &value; // ptr stores the address of value
printf("Value: %d\n", value);
printf("Address of value: %p\n", (void*)&value);
printf("Pointer contains: %p\n", (void*)ptr);
printf("Value at pointer: %d\n", *ptr);
return 0;
}
Call by Reference Example
#include <stdio.h>
void swap(int *x, int *y) {
int temp = *x; // Store value at address x
*x = *y; // Put value at y into location x
*y = temp; // Put temp into location y
}
int main() {
int a = 10, b = 20;
printf("Before swap: a = %d, b = %d\n", a, b);
swap(&a, &b); // Pass addresses
printf("After swap: a = %d, b = %d\n", a, b);
return 0;
}
Output:
Before swap: a = 10, b = 20
After swap: a = 20, b = 10
Returning Multiple Values
#include <stdio.h>
void getRectangleProperties(float length, float width,
float *area, float *perimeter) {
*area = length * width;
*perimeter = 2 * (length + width);
}
int main() {
float len = 5.0, wid = 3.0;
float area, perimeter;
getRectangleProperties(len, wid, &area, &perimeter);
printf("Rectangle: %.1f x %.1f\n", len, wid);
printf("Area: %.2f\n", area);
printf("Perimeter: %.2f\n", perimeter);
return 0;
}
When to Use Call by Reference
Use when:
- You need to modify the original variable
- Returning multiple values from a function
- Working with large data structures (to avoid copying)
- Implementing operations like swapping or updating
Avoid when:
- The function should not modify parameters
- Working with small, simple data types where copying is cheap
- You want to prevent accidental modifications
Mixed Calling Mechanism
You can combine both approaches in a single function:
#include <stdio.h>
#include <math.h>
// Takes value by value, returns results by reference
void analyzeNumber(int num, int *square, int *cube) {
*square = num * num;
*cube = num * num * num;
}
int main() {
int number = 5;
int sq, cu;
analyzeNumber(number, &sq, &cu);
printf("Number: %d\n", number);
printf("Square: %d\n", sq);
printf("Cube: %d\n", cu);
return 0;
}
Nested Functions
What Are Nested Functions?
Nested functions are functions defined within other functions. While this concept exists in some languages, standard C does not support nested functions. However, some compilers (like GCC with extensions) provide limited support.
Standard C Limitation
// This is NOT valid in standard C
void outerFunction() {
void innerFunction() { // ERROR: Not allowed
printf("Inside inner function\n");
}
innerFunction();
}
Why Nested Functions Are Not Standard
- Scope Complexity: Managing variable scope and lifetime becomes complicated
- Stack Management: Creates challenges for memory management
- Portability: Code becomes non-portable across compilers
- Standardization: The C standard committee has not included this feature
GCC Extension (Non-Standard)
GCC allows nested functions as an extension, but this code won't compile with other compilers:
#include <stdio.h>
void processData() {
int multiplier = 5;
// Nested function (GCC extension only)
int multiply(int x) {
return x * multiplier; // Can access outer function's variables
}
printf("Result: %d\n", multiply(10));
}
int main() {
processData();
return 0;
}
Alternative Approaches in Standard C
1. Using Separate Functions
#include <stdio.h>
// Helper function at file scope
int helperFunction(int value) {
return value * 2;
}
void mainFunction() {
int result = helperFunction(10);
printf("Result: %d\n", result);
}
int main() {
mainFunction();
return 0;
}
2. Using Static Functions for Encapsulation
#include <stdio.h>
// Static function - only visible within this file
static int privateHelper(int x) {
return x * x;
}
void publicFunction() {
int result = privateHelper(5);
printf("Square: %d\n", result);
}
int main() {
publicFunction();
// privateHelper(5); // Error: Not accessible here
return 0;
}
When You Might Want Nested Functions (And Alternatives)
Scenario: Local helper that uses outer function's variables
Standard C Solution: Pass parameters explicitly
#include <stdio.h>
void processArray(int arr[], int size) {
int threshold = 50;
// Instead of nested function, use a separate function
// and pass necessary parameters
for (int i = 0; i < size; i++) {
if (arr[i] > threshold) {
printf("%d exceeds threshold\n", arr[i]);
}
}
}
Recommendation
Avoid nested functions in C programming because:
- Not portable across compilers
- Not part of the C standard
- Modern code organization techniques (modular programming, static functions) provide better alternatives
- Can make code harder to understand and maintain
Variadic Functions
What Are Variadic Functions?
Variadic functions can accept a variable number of arguments. The most familiar example is printf(), which can take anywhere from one to many arguments. Think of it like a restaurant buffet where you can take as many items as you want.
The stdarg.h Library
C provides special macros in <stdarg.h> to handle variadic functions:
- va_list: Type for holding argument information
- va_start: Initializes the argument list
- va_arg: Retrieves the next argument
- va_end: Cleanup after processing arguments
Basic Syntax
return_type function_name(fixed_parameters, ...) {
va_list args;
va_start(args, last_fixed_parameter);
// Process arguments using va_arg
va_end(args);
}
Example: Sum of Variable Numbers
#include <stdio.h>
#include <stdarg.h>
// Calculate sum of variable number of integers
int sum(int count, ...) {
va_list args;
va_start(args, count); // Initialize, count is the last fixed parameter
int total = 0;
for (int i = 0; i < count; i++) {
int value = va_arg(args, int); // Get next int argument
total += value;
}
va_end(args); // Cleanup
return total;
}
int main() {
printf("Sum of 3 numbers: %d\n", sum(3, 10, 20, 30));
printf("Sum of 5 numbers: %d\n", sum(5, 1, 2, 3, 4, 5));
printf("Sum of 2 numbers: %d\n", sum(2, 100, 200));
return 0;
}
Example: Finding Maximum Value
#include <stdio.h>
#include <stdarg.h>
double findMax(int count, ...) {
va_list args;
va_start(args, count);
double max = va_arg(args, double); // First value as initial max
for (int i = 1; i < count; i++) {
double value = va_arg(args, double);
if (value > max) {
max = value;
}
}
va_end(args);
return max;
}
int main() {
printf("Maximum: %.2f\n", findMax(5, 3.5, 7.2, 2.1, 9.8, 4.6));
printf("Maximum: %.2f\n", findMax(3, 15.0, 12.5, 18.3));
return 0;
}
Custom Printf-like Function
#include <stdio.h>
#include <stdarg.h>
void printValues(const char *format, ...) {
va_list args;
va_start(args, format);
while (*format != '\0') {
if (*format == 'd') {
int value = va_arg(args, int);
printf("%d ", value);
} else if (*format == 'f') {
double value = va_arg(args, double);
printf("%.2f ", value);
} else if (*format == 's') {
char *value = va_arg(args, char*);
printf("%s ", value);
}
format++;
}
printf("\n");
va_end(args);
}
int main() {
printValues("dfs", 42, 3.14, "Hello");
printValues("ddd", 10, 20, 30);
return 0;
}
When to Use Variadic Functions
Use when:
- Number of arguments is truly variable and unpredictable
- Creating logging or formatting functions
- Building wrapper functions around existing variadic functions
Avoid when:
- A fixed number of parameters would suffice
- Type safety is critical (variadic functions bypass type checking)
- Better alternatives exist (arrays, structs)
Limitations and Cautions
- No Type Safety: The compiler cannot verify argument types
- Must Know Count: You need a way to know how many arguments were passed
- Same Type Promotion: All arguments undergo default promotion
- Potential Errors: Easy to access wrong types or too many arguments
Best Practices
#include <stdio.h>
#include <stdarg.h>
// Use sentinel value (-1) to mark end of arguments
int calculateSum(int first, ...) {
va_list args;
va_start(args, first);
int sum = first;
int value;
while ((value = va_arg(args, int)) != -1) {
sum += value;
}
va_end(args);
return sum;
}
int main() {
printf("Sum: %d\n", calculateSum(10, 20, 30, -1)); // -1 marks the end
printf("Sum: %d\n", calculateSum(5, 15, 25, 35, -1));
return 0;
}
User-Defined Functions
What Are User-Defined Functions?
User-defined functions are custom functions created by programmers to solve specific problems. Unlike library functions (printf, scanf, etc.) that come with C, these are tailored to your program's needs.
Categories of Functions
1. No Arguments, No Return Value
#include <stdio.h>
void printWelcome(void) {
printf("================================\n");
printf(" Welcome to the Banking System\n");
printf("================================\n");
}
int main() {
printWelcome();
// Rest of program
return 0;
}
2. With Arguments, No Return Value
#include <stdio.h>
void displayRectangle(int width, int height) {
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
printf("* ");
}
printf("\n");
}
}
int main() {
displayRectangle(5, 3);
return 0;
}
3. No Arguments, With Return Value
#include <stdio.h>
int getUserChoice(void) {
int choice;
printf("Enter your choice (1-3): ");
scanf("%d", &choice);
return choice;
}
int main() {
int option = getUserChoice();
printf("You selected: %d\n", option);
return 0;
}
4. With Arguments, With Return Value
#include <stdio.h>
float calculateBMI(float weight, float height) {
return weight / (height * height);
}
int main() {
float w = 70.0; // kg
float h = 1.75; // meters
float bmi = calculateBMI(w, h);
printf("BMI: %.2f\n", bmi);
if (bmi < 18.5) {
printf("Underweight\n");
} else if (bmi < 25.0) {
printf("Normal weight\n");
} else {
printf("Overweight\n");
}
return 0;
}
Real-World Example: Student Grade System
#include <stdio.h>
// Function to calculate grade
char calculateGrade(int marks) {
if (marks >= 90) return 'A';
else if (marks >= 80) return 'B';
else if (marks >= 70) return 'C';
else if (marks >= 60) return 'D';
else return 'F';
}
// Function to check pass/fail status
int isPassed(int marks) {
return marks >= 60; // Returns 1 (true) if passed, 0 (false) otherwise
}
// Function to display report
void displayReport(int rollNo, int marks) {
printf("\n--- Student Report ---\n");
printf("Roll Number: %d\n", rollNo);
printf("Marks: %d\n", marks);
printf("Grade: %c\n", calculateGrade(marks));
printf("Status: %s\n", isPassed(marks) ? "PASSED" : "FAILED");
printf("---------------------\n");
}
int main() {
int students[] = {85, 72, 55, 93, 68};
int numStudents = 5;
for (int i = 0; i < numStudents; i++) {
displayReport(i + 1, students[i]);
}
return 0;
}
Utility Functions Example
#include <stdio.h>
// Check if a number is even
int isEven(int num) {
return num % 2 == 0;
}
// Check if a number is prime
int isPrime(int num) {
if (num <= 1) return 0;
if (num == 2) return 1;
if (num % 2 == 0) return 0;
for (int i = 3; i * i <= num; i += 2) {
if (num % i == 0) return 0;
}
return 1;
}
// Get factorial
long factorial(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int main() {
int num = 17;
printf("%d is %s\n", num, isEven(num) ? "even" : "odd");
printf("%d is %s\n", num, isPrime(num) ? "prime" : "not prime");
printf("Factorial of 5: %ld\n", factorial(5));
return 0;
}
Design Principles for User-Defined Functions
- Single Responsibility: Each function should do one thing well
- Meaningful Names: Function names should describe what they do
- Appropriate Size: Keep functions concise (ideally under 50 lines)
- Minimal Parameters: Fewer parameters make functions easier to use
- Clear Return Values: Make it obvious what the function returns
Callback Functions
What Are Callback Functions?
A callback function is a function passed as an argument to another function. The receiving function can then call (invoke) this function at an appropriate time. Think of it like hiring a cleaning service—you provide them with instructions (the callback), and they execute those instructions when they clean your house.
Function Pointers
To pass a function as an argument, we use function pointers:
return_type (*pointer_name)(parameter_types);
Basic Example
#include <stdio.h>
// Callback function that adds two numbers
int add(int a, int b) {
return a + b;
}
// Callback function that multiplies two numbers
int multiply(int a, int b) {
return a * b;
}
// Function that takes a callback
int compute(int x, int y, int (*operation)(int, int)) {
return operation(x, y); // Call the callback function
}
int main() {
int a = 10, b = 5;
printf("Addition: %d\n", compute(a, b, add));
printf("Multiplication: %d\n", compute(a, b, multiply));
return 0;
}
Real-World Example: Array Processing
#include <stdio.h>
// Callback to check if number is positive
int isPositive(int num) {
return num > 0;
}
// Callback to check if number is even
int isEven(int num) {
return num % 2 == 0;
}
// Function that filters array based on condition
void filterArray(int arr[], int size, int (*condition)(int)) {
printf("Filtered elements: ");
for (int i = 0; i < size; i++) {
if (condition(arr[i])) {
printf("%d ", arr[i]);
}
}
printf("\n");
}
int main() {
int numbers[] = {-5, 10, -3, 8, 15, -2, 7};
int size = 7;
filterArray(numbers, size, isPositive);
filterArray(numbers, size, isEven);
return 0;
}
Sorting with Custom Comparisons
#include <stdio.h>
// Comparison callback for ascending order
int ascending(int a, int b) {
return a > b;
}
// Comparison callback for descending order
int descending(int a, int b) {
return a < b;
}
// Bubble sort using callback for comparison
void bubbleSort(int arr[], int n, int (*compare)(int, int)) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (compare(arr[j], arr[j + 1])) {
// Swap
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int numbers[] = {64, 34, 25, 12, 22, 11, 90};
int size = 7;
printf("Original: ");
printArray(numbers, size);
bubbleSort(numbers, size, ascending);
printf("Ascending: ");
printArray(numbers, size);
bubbleSort(numbers, size, descending);
printf("Descending: ");
printArray(numbers, size);
return 0;
}
Menu-Driven System Example
#include <stdio.h>
void addRecord() {
printf("Adding new record...\n");
}
void deleteRecord() {
printf("Deleting record...\n");
}
void viewRecords() {
printf("Viewing all records...\n");
}
void executeMenuOption(int choice, void (*callbacks[])(void)) {
if (choice >= 1 && choice <= 3) {
callbacks[choice - 1]();
} else {
printf("Invalid choice!\n");
}
}
int main() {
void (*menuCallbacks[])(void) = {addRecord, deleteRecord, viewRecords};
int choice;
printf("Menu:\n");
printf("1. Add Record\n");
printf("2. Delete Record\n");
printf("3. View Records\n");
printf("Enter choice: ");
scanf("%d", &choice);
executeMenuOption(choice, menuCallbacks);
return 0;
}
When to Use Callback Functions
Use callbacks when:
- Implementing generic algorithms that need custom behavior
- Creating event-driven or plugin-based systems
- Allowing users to customize function behavior
- Implementing sorting, filtering, or mapping operations
Avoid when:
- Simple direct function calls suffice
- Performance is critical (callbacks add slight overhead)
- Code clarity suffers from excessive indirection
Return Statements
What is a Return Statement?
The return statement terminates function execution and sends a value back to the caller. Think of it like a delivery service completing a job and handing you the package.
Syntax
return expression;
Return with Values
#include <stdio.h>
int square(int x) {
return x * x; // Returns the square of x
}
float average(int a, int b) {
return (a + b) / 2.0; // Returns average as float
}
int main() {
int num = 5;
printf("Square of %d: %d\n", num, square(num));
printf("Average of 10 and 15: %.1f\n", average(10, 15));
return 0;
}
Void Functions (No Return Value)
#include <stdio.h>
void printLine(int length) {
for (int i = 0; i < length; i++) {
printf("-");
}
printf("\n");
return; // Optional in void functions
}
int main() {
printLine(30);
return 0;
}
Multiple Return Statements
#include <stdio.h>
int getAbsolute(int num) {
if (num < 0) {
return -num; // Early return for negative numbers
}
return num; // Return for positive numbers
}
char getLetterGrade(int marks) {
if (marks >= 90) return 'A';
if (marks >= 80) return 'B';
if (marks >= 70) return 'C';
if (marks >= 60) return 'D';
return 'F';
}
int main() {
printf("Absolute value of -15: %d\n", getAbsolute(-15));
printf("Grade for 85 marks: %c\n", getLetterGrade(85));
return 0;
}
Returning Pointers
#include <stdio.h>
#include <string.h>
char* getDayName(int day) {
static char* days[] = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"};
if (day >= 0 && day <= 6) {
return days[day];
}
return "Invalid";
}
int main() {
printf("Day 0: %s\n", getDayName(0));
printf("Day 5: %s\n", getDayName(5));
return 0;
}
Early Return for Error Handling
#include <stdio.h>
float divide(int numerator, int denominator) {
if (denominator == 0) {
printf("Error: Division by zero!\n");
return 0.0; // Early return on error
}
return (float)numerator / denominator;
}
int main() {
printf("10 / 2 = %.2f\n", divide(10, 2));
printf("10 / 0 = %.2f\n", divide(10, 0));
return 0;
}
Best Practices
- Single Exit Point (Debatable): Some prefer one return at the end, others use multiple returns for clarity
- Type Consistency: Ensure returned value matches declared return type
- Meaningful Returns: Return values that make sense in context
- Document Special Values: Explain what special return values mean (e.g., -1 for error)
Recursion
What is Recursion?
Recursion occurs when a function calls itself to solve a problem by breaking it into smaller, similar subproblems. Imagine standing between two mirrors—you see infinite reflections of yourself, each one smaller than the last.
Basic Structure
Every recursive function needs:
- Base Case: The stopping condition that prevents infinite recursion
- Recursive Case: The function calling itself with a modified argument
return_type function(parameters) {
if (base_case_condition) {
return base_case_value; // Stop recursion
}
return function(modified_parameters); // Recursive call
}
Example: Factorial
Mathematical definition: n! = n × (n-1)!
Base case: 0! = 1 and 1! = 1
#include <stdio.h>
long factorial(int n) {
// Base case
if (n <= 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}
int main() {
int num = 5;
printf("Factorial of %d = %ld\n", num, factorial(num));
// Trace: factorial(5)
// = 5 * factorial(4)
// = 5 * 4 * factorial(3)
// = 5 * 4 * 3 * factorial(2)
// = 5 * 4 * 3 * 2 * factorial(1)
// = 5 * 4 * 3 * 2 * 1
// = 120
return 0;
}
Example: Fibonacci Sequence
Each number is the sum of the previous two: 0, 1, 1, 2, 3, 5, 8, 13...
#include <stdio.h>
int fibonacci(int n) {
// Base cases
if (n == 0) return 0;
if (n == 1) return 1;
// Recursive case
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
printf("First 10 Fibonacci numbers:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\n");
return 0;
}
Example: Sum of Digits
#include <stdio.h>
int sumDigits(int num) {
// Base case
if (num == 0) {
return 0;
}
// Recursive case: last digit + sum of remaining digits
return (num % 10) + sumDigits(num / 10);
}
int main() {
int number = 1234;
printf("Sum of digits of %d = %d\n", number, sumDigits(number));
// 4 + sumDigits(123)
// 4 + 3 + sumDigits(12)
// 4 + 3 + 2 + sumDigits(1)
// 4 + 3 + 2 + 1 + sumDigits(0)
// 4 + 3 + 2 + 1 + 0 = 10
return 0;
}
Example: Power Calculation
#include <stdio.h>
int power(int base, int exp) {
// Base case
if (exp == 0) {
return 1;
}
// Recursive case
return base * power(base, exp - 1);
}
int main() {
printf("2^5 = %d\n", power(2, 5));
printf("3^4 = %d\n", power(3, 4));
return 0;
}
Example: Binary Search (Recursive)
#include <stdio.h>
int binarySearch(int arr[], int left, int right, int target) {
// Base case: element not found
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
// Base case: element found
if (arr[mid] == target) {
return mid;
}
// Recursive cases
if (arr[mid] > target) {
return binarySearch(arr, left, mid - 1, target); // Search left half
} else {
return binarySearch(arr, mid + 1, right, target); // Search right half
}
}
int main() {
int numbers[] = {2, 5, 8, 12, 16, 23, 38, 45, 56, 67, 78};
int size = 11;
int target = 23;
int result = binarySearch(numbers, 0, size - 1, target);
if (result != -1) {
printf("Element %d found at index %d\n", target, result);
} else {
printf("Element %d not found\n", target);
}
return 0;
}
Tower of Hanoi
Classic recursive problem: Move disks from source to destination using auxiliary peg.
#include <stdio.h>
void towerOfHanoi(int n, char source, char destination, char auxiliary) {
if (n == 1) {
printf("Move disk 1 from %c to %c\n", source, destination);
return;
}
// Move n-1 disks from source to auxiliary
towerOfHanoi(n - 1, source, auxiliary, destination);
// Move the nth disk from source to destination
printf("Move disk %d from %c to %c\n", n, source, destination);
// Move n-1 disks from auxiliary to destination
towerOfHanoi(n - 1, auxiliary, destination, source);
}
int main() {
int disks = 3;
printf("Tower of Hanoi solution for %d disks:\n", disks);
towerOfHanoi(disks, 'A', 'C', 'B');
return 0;
}
When to Use Recursion
Use recursion when:
- Problem naturally divides into similar subproblems
- Working with tree or graph structures
- Implementing divide-and-conquer algorithms
- The recursive solution is significantly clearer than iterative
Avoid when:
- Simple iterations work better
- Stack overflow is a concern (deep recursion)
- Performance is critical (recursion has overhead)
- The problem doesn't divide naturally
Recursion vs Iteration
Recursion Advantages:
- Cleaner, more elegant code for certain problems
- Natural fit for hierarchical structures
- Easier to understand for divide-and-conquer problems
Recursion Disadvantages:
- Higher memory usage (function call stack)
- Slower execution (function call overhead)
- Risk of stack overflow
- Can be harder to debug
Optimization: Tail Recursion
#include <stdio.h>
// Regular recursion
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Operation after recursive call
}
// Tail recursion (can be optimized by compiler)
int factorialTail(int n, int accumulator) {
if (n <= 1) return accumulator;
return factorialTail(n - 1, n * accumulator); // Recursive call is last operation
}
int main() {
printf("Factorial (regular): %d\n", factorial(5));
printf("Factorial (tail): %d\n", factorialTail(5, 1));
return 0;
}
Predefined Identifier func
What is func?
__func__ is a predefined identifier in C99 and later standards that holds the name of the current function as a string. Think of it as an automatic nameplate that each function carries with itself.
Basic Usage
#include <stdio.h>
void demonstrateFunc(void) {
printf("Current function: %s\n", __func__);
}
int main(void) {
printf("Current function: %s\n", __func__);
demonstrateFunc();
return 0;
}
Output:
Current function: main
Current function: demonstrateFunc
Debugging and Logging
#include <stdio.h>
#define LOG(msg) printf("[%s] %s\n", __func__, msg)
void processData(void) {
LOG("Starting data processing");
// Processing logic here
LOG("Data processing complete");
}
void validateInput(void) {
LOG("Validating user input");
// Validation logic
LOG("Input validation successful");
}
int main(void) {
LOG("Program started");
validateInput();
processData();
LOG("Program finished");
return 0;
}
Output:
[main] Program started
[validateInput] Validating user input
[validateInput] Input validation successful
[processData] Starting data processing
[processData] Data processing complete
[main] Program finished
Error Reporting
#include <stdio.h>
void reportError(const char *message) {
fprintf(stderr, "ERROR in %s: %s\n", __func__, message);
}
int divide(int a, int b) {
if (b == 0) {
reportError("Division by zero attempted");
return 0;
}
return a / b;
}
int openFile(const char *filename) {
// Simulating file open failure
reportError("Could not open file");
return -1;
}
int main(void) {
divide(10, 0);
openFile("data.txt");
return 0;
}
Function Entry/Exit Tracing
#include <stdio.h>
void functionEntry(void) {
printf(">>> Entering %s\n", __func__);
}
void functionExit(void) {
printf("<<< Exiting %s\n", __func__);
}
void calculateSum(int a, int b) {
functionEntry();
int sum = a + b;
printf("Sum = %d\n", sum);
functionExit();
}
void processOrder(void) {
functionEntry();
calculateSum(10, 20);
functionExit();
}
int main(void) {
functionEntry();
processOrder();
functionExit();
return 0;
}
Advanced: Custom Assert Macro
#include <stdio.h>
#include <stdlib.h>
#define ASSERT(condition) \
if (!(condition)) { \
fprintf(stderr, "Assertion failed in %s at line %d: %s\n", \
__func__, __LINE__, #condition); \
exit(1); \
}
void processArray(int *arr, int size) {
ASSERT(arr != NULL);
ASSERT(size > 0);
printf("Processing array in %s\n", __func__);
// Processing logic
}
int main(void) {
int numbers[] = {1, 2, 3, 4, 5};
processArray(numbers, 5);
// This will trigger assertion
// processArray(NULL, 5);
return 0;
}
When to Use func
Use when:
- Implementing logging or debugging systems
- Creating error reporting mechanisms
- Building testing frameworks
- Tracing program execution flow
Characteristics:
- Automatically available in every function
- Standard since C99
- Always contains the current function name
- Cannot be modified (it's not a variable)
Comparison with FUNCTION
Some compilers provide __FUNCTION__ as an extension, which works similarly to __func__. However, __func__ is standard and should be preferred for portability.
Summary
Key Takeaways
Functions are fundamental building blocks that enable:
- Code reusability and modularity
- Better organization and maintenance
- Abstraction of complex operations
- Testing individual components
Important Concepts:
- Prototypes: Declare functions before use for type safety and organization
- Parameters: Choose call by value for data protection, call by reference for modification
- Return Values: Communicate results back to callers effectively
- Recursion: Elegant solutions for naturally recursive problems
- Callbacks: Enable flexible, reusable code patterns
Best Practices:
- Write small, focused functions (single responsibility)
- Use descriptive names that explain purpose
- Validate input parameters
- Handle error conditions gracefully
- Document complex functions
- Prefer standard C features for portability
- Consider performance implications of design choices
Common Pitfalls to Avoid
- Missing function prototypes leading to compilation errors
- Forgetting base case in recursive functions (infinite recursion)
- Not validating pointer parameters before dereferencing
- Using call by value for large structures (inefficient)
- Assuming variadic functions are type-safe
- Relying on compiler extensions (nested functions in GCC)
Further Learning
To master functions in C:
- Practice writing functions for common algorithms
- Study standard library function implementations
- Experiment with different parameter passing methods
- Learn about function pointers and their applications
- Understand stack frames and calling conventions
- Explore how compilers optimize function calls
Functions transform programming from writing scripts to building systems. Master them, and you master structured programming in C.
Top comments (0)