This comprehensive guide offers 25 concept-oriented C Function exercises designed to solidify your understanding. Suitable for both students and experienced developers, the challenges progress from simple definitions to complex areas like function pointers and recursion.
Each exercise includes a Problem Statement, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely understand how and why it works.
What You’ll Practice
- Basic Structure: Declaration, calling, and return types.
- Advanced Topics: Pass by Reference (using pointers), Recursion (including Tail Recursion), and Function Pointers for callbacks.
- Memory & Scope: Local/Global variables, Dynamic Memory (via
malloc), and Storage Classes (static,extern). - Optimization: The
inlinekeyword and the pitfalls of Function-like Macros. - Complex Parameters: Implementing Variadic Functions (using
stdarg.h).
Use Online C Compiler to solve exercises.
+ Table Of Contents (25 Exercises)
Table of contents
- Exercise 1: Simple Function
- Exercise 2: Function with Parameters
- Exercise 3: Function with Return Value
- Exercise 4: Function Prototype/Declaration
- Exercise 5: Pass by Value (Integers)
- Exercise 6: Pass by Reference (Pointers)
- Exercise 7: Array as a Parameter
- Exercise 8: Structure as a Parameter
- Exercise 9: static Local Variable (Lifetime)
- Exercise 10: Global Variable and Scope
- Exercise 11: register Storage Class
- Exercise 12: Function Scope with Helper Function
- Exercise 13: Basic Recursion (Factorial)
- Exercise 14: Simple Recursion (Fibonacci Sequence)
- Exercise 15: Recursion with Base Case
- Exercise 16: Tail Recursion
- Exercise 17: Function Pointers
- Exercise 18: Function as an Argument (Callback)
- Exercise 19: Function Returning a Pointer
- Exercise 20: Function Returning a Structure
- Exercise 21: extern Keyword for Function
- Exercise 22: inline Function (Optimization Hint)
- Exercise 23: Function-like Macro (Simple)
- Exercise 24: Variadic Functions ( … )
- Exercise 25: Exit Function ( exit() and atexit() )
Exercise 1: Simple Function
Problem Statement: Write a function greet() that takes no arguments and returns nothing (void), and simply prints “Hello, C Functions!”.
+ Hint
The function signature should be void greet(void). Since the function returns nothing, you don’t need a return
+ Show Solution
#include <stdio.h>
// Function definition
void greet(void) {
printf("Hello, C Functions!\n");
}
int main() {
// Function call
greet();
return 0;
}Code language: C++ (cpp)
Explanation:
This is the most basic function structure.
- Signature:
void greet(void)specifies that the function takes no arguments (voidinside the parentheses) and returns no value (voidbefore the function name). - Function Call: In
main(),greet();transfers program execution to the function definition. - Execution: The function executes the
printfstatement and control returns to the line immediately following the function call inmain.
Exercise 2: Function with Parameters
Problem Statement: Create a function add_numbers() that takes two integers as parameters, calculates their sum, and prints the result inside the function. Call this function from main with two different numbers.
Sample Output:
The sum of 15 and 7 is: 22
The sum of 100 and 25 is: 125
+ Hint
Use the parameters a and b directly within the function body to perform the addition. Since the printing is done inside the function, the return type remains void.
+ Show Solution
#include <stdio.h>
// Function definition with parameters a and b
void add_numbers(int a, int b) {
int sum = a + b;
printf("The sum of %d and %d is: %d\n", a, b, sum);
}
int main() {
int x = 15;
int y = 7;
// Function call passing values (arguments)
add_numbers(x, y);
// Another call with literal values
add_numbers(100, 25);
return 0;
}Code language: C++ (cpp)
Explanation:
This exercise demonstrates parameter passing.
- Parameters vs. Arguments:
aandbare the parameters (placeholders) defined in the function signature.xandy(or100and25) are the arguments (actual values) passed during the call. - Passing: When
add_numbers(x, y)is called, the value ofx(15) is copied into parametera, and the value ofy(7) is copied into parameterb. The function then works with these copies.
Exercise 3: Function with Return Value
Problem Statement: Implement a function multiply() that takes two integers and returns their product. Call the function from main and store the result in a variable before printing it
Sample Output:
8*6 is 48.
+ Hint
The return type must match the type of the value being returned (which is an int product). The main function must use an assignment operator (=) to capture the value the function returns.
+ Show Solution
#include <stdio.h>
// Function returns an integer (int)
int multiply(int x, int y) {
int product = x * y;
return product; // Return the calculated value
}
int main() {
int num1 = 8;
int num2 = 6;
int result;
// Function call: the returned value is stored in 'result'
result = multiply(num1, num2);
printf("%d multiplied by %d is %d.\n", num1, num2, result);
return 0;
}Code language: C++ (cpp)
Explanation:
This exercise shows how functions can calculate a value and send it back to the caller.
- Return Type: The function signature starts with
intto indicate it returns an integer. returnStatement:return product;immediately stops the function’s execution and sends the value stored inproductback to the point of the call.- Receiving Value: In
main,result = multiply(num1, num2);is essential. The value 48 (the product) is returned by the function, and the assignment operator stores this value in theresultvariable.
Exercise 4: Function Prototype/Declaration
Problem Statement: Write a program where the function definition for is_even(int num) (which returns 1 if even, 0 otherwise) is placed after the main function. This setup requires a function prototype (declaration) before main.
+ Hint
The prototype is just the function signature followed by a semicolon (;). Place it near the top of the file, before the main function.
+ Show Solution
#include <stdio.h>
// 1. Function Prototype (Declaration)
// This tells the compiler the function exists, its signature, and return type.
int is_even(int num);
int main() {
int check_num = 14;
if (is_even(check_num)) { // Function call
printf("%d is even.\n", check_num);
} else {
printf("%d is odd.\n", check_num);
}
return 0;
}
// 2. Function Definition (Must match the prototype exactly)
int is_even(int num) {
if (num % 2 == 0) {
return 1; // True
} else {
return 0; // False
}
}Code language: C++ (cpp)
Explanation:
C compilers read code sequentially from top to bottom.
- The Need for a Prototype: When the compiler encounters the call
is_even(check_num)insidemain, it needs to know whatis_evenis. If the definition is belowmain, the compiler hasn’t seen it yet and will produce an error. - The Prototype:
int is_even(int num);acts as a forward reference. It informs the compiler that a function namedis_evenexists, takes oneintargument, and returns anint. This satisfies the compiler at the time of the call inmain. The actual function body (definition) can then appear anywhere later in the file.
Exercise 5: Pass by Value (Integers)
Problem Statement: Write a function increment(int n) that takes an integer, increments its value by 1 inside the function, and prints the variable’s value before and after the function call in main to demonstrate that the original variable is unchanged by the function
+ Hint
Since C passes arguments by value, the function receives a copy. Changes made to the parameter n inside increment will not affect the original variable in main.
+ Show Solution
#include <stdio.h>
// Function receives a COPY of the argument
void increment(int n) {
printf("Inside function: Value before increment: %d\n", n);
n = n + 1; // Only the copy is modified
printf("Inside function: Value after increment: %d\n", n);
}
int main() {
int number = 10;
printf("In main: Before function call, number = %d\n", number);
increment(number); // The value 10 is copied to the 'n' parameter
printf("In main: After function call, number = %d\n", number);
return 0;
}Code language: C++ (cpp)
Explanation:
This exercises illustrates Pass by Value, the default mechanism for non-array variables in C.
- Copy Created: When
increment(number)is called, a temporary storage location (n) is created, and the value ofnumber(10) is copied into it. - Isolation: The operation
n = n + 1;only changes the local copynto 11. - No Effect: The original variable
numberin themainfunction remains unchanged at 10, proving the function operates on an isolated copy.
Exercise 6: Pass by Reference (Pointers)
Problem Statement: Write a function swap(int *a, int *b) that uses pointers to swap the values of two integer variables passed from main, demonstrating Pass by Reference.
+ Hint
The parameters must be pointers to integers (int *). Inside the function, you must use the dereference operator (*) to access and modify the actual values stored at the memory addresses.
+ Show Solution
#include <stdio.h>
// Function takes pointers (memory addresses) as arguments
void swap(int *a, int *b) {
int temp;
// Dereference: Read the value at address 'a'
temp = *a;
// Dereference: Write the value at address 'b' to address 'a'
*a = *b;
// Dereference: Write the value in 'temp' to address 'b'
*b = temp;
printf("Inside swap: values are now %d and %d\n", *a, *b);
}
int main() {
int x = 10;
int y = 20;
printf("In main: Before swap, x = %d, y = %d\n", x, y);
// Pass the ADDRESSES of x and y using the address-of operator (&)
swap(&x, &y);
printf("In main: After swap, x = %d, y = %d\n", x, y);
return 0;
}Code language: C++ (cpp)
Explanation:
Pass by Reference (simulated in C using pointers) allows a function to modify variables declared in the caller’s scope.
- Passing: The
&operator (address-of) passes the memory addresses ofxandyto the function. - Receiving: The parameters
*aand*bstore these addresses. - Modification: The
*operator (dereference) is used insideswapto follow the address and read/write the value in the calling function’s memory. This directly changesxandy, hence the term “Pass by Reference.”
Exercise 7: Array as a Parameter
Problem Statement: Create a function find_max(int arr[], int size) that takes an integer array and its size as parameters and returns the largest element found in the array.
Given:
int numbers[] = {45, 12, 88, 5, 92, 33};Code language: C++ (cpp)
Expected Output:
The largest element in the array is: 92
+ Hint
Arrays are always passed by reference (using a pointer to the first element) in C. Loop through the array from the second element onward, comparing each element to a running max variable.
+ Show Solution
#include <stdio.h>
// The array parameter decays to a pointer (int *arr)
int find_max(int arr[], int size) {
if (size <= 0) return 0; // Handle empty array case
int max = arr[0]; // Assume the first element is the largest
int i;
for (i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i]; // Update max if a larger element is found
}
}
return max;
}
int main() {
int numbers[] = {45, 12, 88, 5, 92, 33};
int array_size = sizeof(numbers) / sizeof(numbers[0]);
int max_val;
max_val = find_max(numbers, array_size);
printf("The largest element in the array is: %d\n", max_val);
return 0;
}Code language: C++ (cpp)
Explanation:
When an array name is used as a function argument, C passes the address of its first element.
- Array Decay: Even though the parameter is declared as
int arr[], the compiler treats it asint *arr. This is why the size must be passed separately, as the function cannot determine the array size from the pointer alone. - Efficiency: Passing arrays this way is efficient because the function receives only a memory address (a pointer), not a copy of the entire array (unlike standard Pass by Value). The function can directly access and potentially modify the elements of the original array.
Exercise 8: Structure as a Parameter
Problem Statement: Define a structure struct Point {int x; int y;}. Write a function print_point(struct Point p) that takes an instance of this structure by value and prints its coordinates in the format (x, y).
+ Hint
Unlike arrays, structures are passed by value by default. Use the dot operator (.) inside the function to access the structure members of the copy passed to the function.
+ Show Solution
#include <stdio.h>
// Structure definition
struct Point {
int x;
int y;
};
// Function takes the structure BY VALUE (receives a full copy)
void print_point(struct Point p) {
// Access members using the dot operator
printf("Inside function: Point coordinates are: (%d, %d)\n", p.x, p.y);
// If we change the copy, the original is unaffected:
p.x = 999;
}
int main() {
// Initialize a Point structure
struct Point start_point = {10, 20};
printf("In main: Before function call, x=%d\n", start_point.x);
// Pass the structure by value
print_point(start_point);
printf("In main: After function call, x=%d y=%d (original is unchanged)\n", start_point.x, start_point.y);
return 0;
}Code language: C++ (cpp)
Explanation:
This exercise demonstrates the default Pass by Value mechanism for structures.
- Copy Passed: When
print_point(start_point)is called, a complete copy of thestart_pointstructure is created and passed to the parameterp. - Isolation: Any modification made to the members of
p(like settingp.x = 999) only affects the local copy within the function and leaves the originalstart_pointinmainintact. - Alternative (Not Shown): If the intent were to modify the original structure, the function would need to take a pointer to the structure (
struct Point *p) instead, simulating Pass by Reference.
Exercise 9: static Local Variable (Lifetime)
Problem Statement: Write a function test_static_scope() that declares a count a static local variable (static int count = 1;). Increment it by 1 inside a function. Call the function twice to show that the variable’s value is retained across function calls.
Expected Output:
--- First Call ---
Static count retained value: 1
Static count after increment: 2
--- Second Call ---
Static count retained value: 2
Static count after increment: 3
+ Hint
The static storage class changes the lifetime of the local variable from automatic to the entire duration of the program. The initialization (= 1) only happens once.
+ Show Solution
#include <stdio.h>
void test_static_lifetime() {
// 'static' changes the variable's lifetime.
// Initialization to 1 happens ONLY on the first call.
static int count = 1;
printf("Static count retained value: %d\n", count);
count++; // This modification persists
printf("Static count after increment: %d\n", count);
}
int main() {
printf("--- First Call ---\n");
test_static_lifetime();
printf("\n--- Second Call ---\n");
test_static_lifetime(); // 'count' retains the value 2 from the previous call
return 0;
}Code language: C++ (cpp)
Explanation:
The static storage class is used here to alter the variable’s lifetime.
- Lifetime Extended: A
staticlocal variable is created and initialized only once (when the program starts, or the first time control flow passes over its declaration). It maintains its value throughout the execution of the program. - Scope Maintained: Crucially, even though its lifetime is extended, its scope remains local to the function. It cannot be accessed directly from
main. This is often used for counters or flags that need to track function usage internally.
Exercise 10: Global Variable and Scope
Problem Statement: Define a global variable int global_counter = 0; outside of all functions. Write a function update_global_counter() that increments it by 1 and prints its value. Demonstrate how functions can modify global variables and how both main and the function can access it.
Expected Output:
In main: Initial global counter = 0
Inside function: Global counter updated to 1
Inside function: Global counter updated to 2
In main: Final global counter = 2
+ Hint
Global variables have file scope (or external linkage by default) and a program lifetime. They can be accessed by name from any function defined after their declaration without any special declaration inside the function.
+ Show Solution
#include <stdio.h>
// Global variable: accessible by all functions defined below it
int global_counter = 0;
void update_global_counter() {
global_counter++; // Directly access and modify the global variable
printf("Inside function: Global counter updated to %d\n", global_counter);
}
int main() {
printf("In main: Initial global counter = %d\n", global_counter);
update_global_counter();
update_global_counter();
// main can also access the current value directly
printf("In main: Final global counter = %d\n", global_counter);
return 0;
}Code language: C++ (cpp)
Explanation:
Global variables are declared outside all function bodies.
- Scope: They have file scope, meaning they are visible from their point of declaration to the end of the file.
- Lifetime: They have a program lifetime, meaning they exist for the entire duration of the program.
- Access: Any function in the file can directly read and modify a global variable, which is often considered bad practice as it reduces modularity and makes code harder to debug (side effects are difficult to track).
Exercise 11: register Storage Class
Problem Statement: Briefly discuss the intended use of the register storage class for a function parameter and apply it to a simple parameter in a function sum_two(register int a, register int b).
Expected Output:
a:50 b:75
Sum: 125
+ Hint
The register keyword is a suggestion to the compiler to store the variable in a CPU register instead of main memory for faster access. Use it on variables that will be heavily used within a function (like loop counters).
+ Show Solution
#include <stdio.h>
// 'register' is a hint to the compiler for potential optimization
int sum_two(register int a, register int b) {
// These variables are candidates for being stored in CPU registers
printf("a:%d b:%d\n", a, b);
return a + b;
}
int main() {
int result = sum_two(50, 75);
printf("Sum: %d\n", result);
return 0;
}
/*
Explanation (as part of the output):
The 'register' keyword suggests to the compiler that 'a' and 'b' should be
stored in a CPU register for fast access. In modern compilers, this keyword
is often ignored or automatically optimized where appropriate, as compilers
are usually better at register allocation than manual hints.
*/Code language: C++ (cpp)
Explanation:
The register storage class is primarily a performance hint.
- Purpose: It suggests that the variable will be accessed frequently and should ideally be stored in a CPU register. Accessing registers is significantly faster than accessing main memory (RAM).
- Modern C: Modern compilers are highly optimized. They often ignore the
registerkeyword and decide register allocation themselves, making its use mostly historical or academic. You cannot take the address of aregistervariable using&, because a register does not have a conventional memory address.
Exercise 12: Function Scope with Helper Function
Problem Statement: Write a function calculate_net_salary(float gross) which internally calls a private helper function calculate_tax(float income) (declared and defined within the same file) to illustrate modularity. Assume a fixed tax rate of 15%.
Expected Output:
Inside main tax function
Inside Private helper function
Gross Salary: 50000.00
Tax Deducted: 7500.00
Net Salary Received: 42500.00
+ Hint
Define the helper function first, then the main function that calls it. This improves readability by placing sub-tasks into separate, easily understood functions.
+ Show Solution
#include <stdio.h>
// Private helper function definition
float calculate_tax(float income) {
print("Inside Private helper function")
const float TAX_RATE = 0.15;
return income * TAX_RATE;
}
// Main function definition, which uses the helper
float calculate_net_salary(float gross) {
print("Inside main tax function")
float tax_amount = calculate_tax(gross);
float net_salary = gross - tax_amount;
printf("Gross Salary: %.2f\n", gross);
printf("Tax Deducted: %.2f\n", tax_amount);
return net_salary;
}
int main() {
float gross_income = 50000.00;
float net = calculate_net_salary(gross_income);
printf("Net Salary Received: %.2f\n", net);
return 0;
}Code language: C++ (cpp)
Explanation:
This exercise demonstrates functional decomposition and modularity.
- Modularity: By separating the tax calculation logic into a dedicated helper function (
calculate_tax), the main function (calculate_net_salary) becomes cleaner and easier to read, focusing only on the high-level steps (Gross – Tax = Net). - Reusability: The
calculate_taxfunction could be reused by other functions in the program if they also needed to calculate tax on income, improving code reuse. This organization makes the code base more maintainable.
Exercise 13: Basic Recursion (Factorial)
Problem Statement: Write a recursive function factorial(int n) to calculate the factorial of a non-negative integer (n!). The definition is n!=n×(n−1)! and 0!=1.
A factorial, represented by an exclamation point (!), is the product of all positive integers up to a given non-negative integer, written as n!
Formula: For a non-negative integer n, the factorial is defined as:
n! = n × (n - 1) × (n - 2) × ... × 1
Examples:
0! = 1
1! = 1
3! = 3 × 2 × 1 = 6
5! = 5 × 4 × 3 × 2 × 1 = 120
+ Hint
Every recursive function needs two things:
- A Base Case: The condition that stops the recursion (
n == 0). - A Recursive Step: The call to the function itself, usually with a reduced parameter (
n * factorial(n - 1)).
+ Show Solution
#include <stdio.h>
// Recursive function for factorial
long long factorial(int n) {
// Base Case: Stops the recursion
if (n == 0) {
return 1;
}
// Recursive Step: Calls itself with n-1
return n * factorial(n - 1);
}
int main() {
int num = 5;
long long result = factorial(num);
printf("Factorial of %d is: %lld\n", num, result); // 5! = 120
return 0;
}Code language: C++ (cpp)
Explanation:
Recursion is the process where a function calls itself.
- Base Case (n=0): When the function hits the base case, it returns 1, preventing infinite calls.
- Stacking Calls: For
factorial(5), the calls stack up: 5×fact(4)→5×(4×fact(3))→⋯→5×4×3×2×1×fact(0). - Unwinding: Once fact(0) returns 1, the multiplication results are returned back up the stack (unwinding) until the final result is returned to
main. This process uses the program’s call stack to store intermediate results.
Exercise 14: Simple Recursion (Fibonacci Sequence)
Problem Statement: Implement a recursive function fibonacci(int n) to find the 7-th number in the Fibonacci sequence (0,1,1,2,3,5,8,13…).
Fibonacci sequence: It is a series of numbers where each number is the sum of the two preceding ones, typically starting with 0 and 1. The sequence goes 0, 1, 1, 2, 3, 5, 8, 13, 21, and so on, and is represented by the recursive formula F(n) = F(n-1) + F(n-2), with F(0)=0 and F(1)=1.
Expected Output:
The 7-th Fibonacci number is: 13
+ Hint
The recursive definition is: fib(n)=fib(n−1)+fib(n−2). You will need two base cases to stop the recursion: n=0 returns 0, and n=1 returns 1.
+ Show Solution
#include <stdio.h>
// Recursive function for Fibonacci
int fibonacci(int n) {
// Base Cases:
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
// Recursive Step: Sum of the previous two terms
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
int n = 7;
int result = fibonacci(n);
printf("The %d-th Fibonacci number is: %d\n", n, result); // 7th term is 13
return 0;
}Code language: C++ (cpp)
Explanation:
This recursive solution directly implements the mathematical definition of the Fibonacci sequence.
- Dual Base Cases: Since the definition relies on two previous terms, we need base cases for both n=0 and n=1 to ensure the recursion terminates correctly.
- Complexity: While elegant, this recursive implementation is highly inefficient for large n. It recalculates the same Fibonacci numbers repeatedly, leading to exponential time complexity (
O(2n)), which is a common trade-off when using simple recursion.
Exercise 15: Recursion with Base Case
Problem Statement: Write a recursive function sum_digits(int n) that finds the sum of the digits of an integer (e.g., 123→1+2+3=6), clearly showing the base case.
Expected Output:
The sum of digits of 478 is: 19
+ Hint
Use the modulo operator (% 10) to get the last digit and integer division (/ 10) to remove it. The base case is when the number becomes 0.
+ Show Solution
#include <stdio.h>
// Recursive function to sum digits
int sum_digits(int n) {
// Base Case: When the number is reduced to 0, stop and return 0
if (n == 0) {
return 0;
}
// Recursive Step:
// 1. Get the last digit (n % 10)
// 2. Add it to the recursive call result with the number reduced (n / 10)
return (n % 10) + sum_digits(n / 10);
}
int main() {
int num = 478;
int result = sum_digits(num); // 4 + 7 + 8 = 19
printf("The sum of digits of %d is: %d\n", num, result);
return 0;
}Code language: C++ (cpp)
Explanation:
This models the problem as an accumulation across recursive calls.
- Logic: For 478: it calculates
8+sum_digits(47).sum_digits(47)calculates7+sum_digits(4).sum_digits(4)calculates4+sum_digits(0). - Base Case:
sum_digits(0)returns 0. - Unwinding: The results add up as the calls unwind: 8+7+4+0=19. The base case is essential for terminating the chain and providing the final zero value to start the sum.
Exercise 16: Tail Recursion
Problem Statement: Try to implement the factorial function using tail recursion (where the recursive call is the last operation in the function). This typically requires a helper function or an accumulator parameter.
Expected Output:
Factorial of 5 (Tail Recursion) is: 120
+ Hint
Define a helper function, usually tail_factorial(int n, int accumulator). The accumulator holds the running result. The recursive call should return the result of the next recursive call directly, with no operation applied to it afterward.
+ Show Solution
#include <stdio.h>
// Helper function that is tail-recursive
long long tail_factorial(int n, long long accumulator) {
// Base Case: Return the accumulated result
if (n == 0) {
return accumulator;
}
// Tail Recursive Step: The recursive call is the last action
return tail_factorial(n - 1, accumulator * n);
}
// Wrapper function for user convenience
long long factorial_tail(int n) {
return tail_factorial(n, 1); // Start accumulator at 1
}
int main() {
int num = 5;
long long result = factorial_tail(num);
printf("Factorial of %d (Tail Recursion) is: %lld\n", num, result);
return 0;
}Code language: C++ (cpp)
Explanation:
Tail recursion is a special form of recursion that allows modern compilers to perform an optimization called tail-call elimination.
- Mechanism: In the tail-recursive version, the calculation (n×accumulator) is performed before the recursive call, and the result is passed as an argument (
accumulator * n) to the next call. The last operation is simply the recursive call itself. - Optimization: When optimized, the compiler can reuse the existing stack frame for the next call, turning the recursion into an efficient iterative loop, thereby preventing potential stack overflow issues that can occur with deep, non-tail recursion.
Exercise 17: Function Pointers
Problem Statement: Define a function pointer that can point to a function taking two integers and returning an integer. Assign it to the multiply function (from Exercise 3) and call the function using the pointer.
Expected Output:
a:10 b:5
Result of multiplication (via function pointer): 50
+ Hint
The syntax for declaring a function pointer matches the signature of the function it points to: return_type (*pointer_name)(parameter_list). Use the address-of operator (&) to get the function’s address (though it’s often optional for functions). Calling it looks like a normal function call: (*pointer_name)(arg1, arg2).
+ Show Solution
#include <stdio.h>
int multiply(int x, int y) {
return x * y;
}
int main() {
// 1. Declare a function pointer 'ptr_func'
// It points to a function that takes (int, int) and returns int.
int (*ptr_func)(int, int);
// 2. Assign the address of the 'multiply' function to the pointer
ptr_func = multiply; // Or ptr_func = &multiply;
int a = 10, b = 5;
printf("a:%d b:%d\n", a, b);
int result;
// 3. Call the function using the pointer (dereferencing it)
result = (*ptr_func)(a, b);
// Alternative call syntax (often cleaner)
// result = ptr_func(a, b);
printf("Result of multiplication (via function pointer): %d\n", result); // 50
return 0;
}Code language: C++ (cpp)
Explanation:
Function Pointers allow you to treat functions as ordinary data, enabling dynamic function calls.
- Declaration: The parentheses around
*ptr_funcare mandatory to distinguish it from a function that returns a pointer. - Assignment: Since the
multiplyfunction’s signature matches the pointer’s signature, its address can be assigned toptr_func. The compiler handles functions’ names as pointers to their starting address. - Invocation: Calling the function via the pointer,
(*ptr_func)(a, b), dereferences the pointer to execute the code at that memory address, just like a regular function call.
Exercise 18: Function as an Argument (Callback)
Problem Statement: Write a function operate_on_data(int a, int b, int (*op)(int, int)) that takes two integers and a function pointer as arguments. Pass either an add(int, int) or a subtract(int, int) function to it, demonstrating a callback mechanism.
Expected Output:
x:50 y:15
Operation (Add): 65
Operation (Subtract): 35
+ Hint
The third parameter in operate_on_data is the function pointer declaration itself. The function body should call the provided function pointer using the arguments a and b.
+ Show Solution
#include <stdio.h>
// Functions to be used as callbacks
int add(int x, int y) { return x + y; }
int subtract(int x, int y) { return x - y; }
// Function that takes a function pointer as a parameter
int operate_on_data(int a, int b, int (*op)(int, int)) {
// 'op' is the function pointer; call it directly
return op(a, b);
}
int main() {
int x = 50, y = 15;
printf("x:%d y:%d\n", x, y);
int result_add, result_sub;
// Pass 'add' function (address)
result_add = operate_on_data(x, y, add);
printf("Operation (Add): %d\n", result_add); // 65
// Pass 'subtract' function (address)
result_sub = operate_on_data(x, y, subtract);
printf("Operation (Subtract): %d\n", result_sub); // 35
return 0;
}Code language: C++ (cpp)
Explanation:
This is the basis of callback functions and polymorphism in C.
- Mechanism: The
operate_on_datafunction doesn’t care how the operation is performed; it just executes the function provided by theoppointer parameter. - Flexibility: By changing which function address is passed (
addorsubtract), you change the behavior ofoperate_on_datawithout modifying its source code. This is fundamental for implementing things like generic sorting algorithms (where the comparison function is a callback) or event handling.
Exercise 19: Function Returning a Pointer
Problem Statement: Write a function create_int(int value) that dynamically allocates memory for an integer using malloc, initializes it, and returns a pointer to that integer. Remember to free the memory in main.
+ Hint
The return type of the function must be int *. Inside the function, use malloc(sizeof(int)) to allocate memory, and return the pointer returned by malloc.
+ Show Solution
#include <stdio.h>
#include <stdlib.h> // Required for malloc() and free()
// Function returns a pointer to an integer (int *)
int* create_int(int value) {
// Allocate memory for one integer
int *ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
fprintf(stderr, "Memory allocation failed.\n");
return NULL;
}
// Initialize the dynamically allocated memory
*ptr = value;
return ptr; // Return the address of the new integer
}
int main() {
int initial_value = 42;
int *my_dynamic_int;
// Call function, receiving the memory address
my_dynamic_int = create_int(initial_value);
if (my_dynamic_int != NULL) {
printf("Value created dynamically: %d\n", *my_dynamic_int);
// Clean up: Free the dynamically allocated memory
free(my_dynamic_int);
my_dynamic_int = NULL;
}
return 0;
}Code language: C++ (cpp)
Explanation:
Functions returning pointers are essential for dynamic memory management.
- Return Type:
int *indicates the function returns the memory address of an integer. - Heap Memory:
mallocallocates memory from the heap, which means the memory persists even after thecreate_intfunction returns (unlike stack memory for local variables). - Safety: The function returns a valid address that
maincan use. Ifmallocfails, returningNULLis the standard way to handle the error. The caller (main) is then responsible forfreeing this memory to prevent a memory leak.
Exercise 20: Function Returning a Structure
Problem Statement: Define a struct Complex {float real; float imag;}. Write a function add_complex(struct Complex a, struct Complex b) that takes two complex numbers by value and returns the resulting Complex structure (sum of real parts and sum of imaginary parts).
+ Hint
The return type of the function should be struct Complex. Create a local instance of the structure, calculate the result, and return the local structure instance.
+ Show Solution
#include <stdio.h>
struct Complex {
float real;
float imag;
};
// Function returns a structure
struct Complex add_complex(struct Complex a, struct Complex b) {
struct Complex result; // Local structure instance
result.real = a.real + b.real;
result.imag = a.imag + b.imag;
return result; // Returns a COPY of the local structure
}
void print_complex(struct Complex c) {
printf("%.2f + %.2fi\n", c.real, c.imag);
}
int main() {
struct Complex z1 = {2.5, 3.0};
struct Complex z2 = {1.5, 4.5};
struct Complex z_sum;
printf("z1 = "); print_complex(z1);
printf("z2 = "); print_complex(z2);
// Function call: receives the returned structure copy
z_sum = add_complex(z1, z2);
printf("Sum = "); print_complex(z_sum); // 4.00 + 7.50i
return 0;
}Code language: C++ (cpp)
Explanation:
C allows functions to return entire structure instances by value.
- Efficiency Note: For very large structures, passing and returning structures by value can be inefficient due to the large amount of copying involved. In such cases, it is often better to pass and return pointers to structures.
- Mechanism: When
add_complexreturnsresult, the entire structure is copied onto the stack and then copied into the destination variable (z_sum) inmain.
Exercise 21: extern Keyword for Function
Problem Statement: Write a two-file program where the function definition is in one file (helper.c) and it’s called from the main file (main.c) using the extern keyword (typically implied via an included header file) to link the function, demonstrating external linkage.
+ Hint
In C, function declarations are extern by default, so explicitly using the keyword is usually for clarity. You’ll need two separate files and compile them together. The function prototype in the main file (or header) tells the compiler that the function definition exists elsewhere.
+ Show Solution
This solution is conceptual, requires two files.
File 1: helper.c (The definition)
// helper.c
#include <stdio.h>
// Function definition (defaults to external linkage)
int calculate_area_ext(int length, int width) {
return length * width;
}Code language: C++ (cpp)
File 2: main.c (The caller)
// main.c
#include <stdio.h>
// Explicit prototype with extern (optional, but shows intent)
// This tells the linker to look for the definition in another file.
extern int calculate_area_ext(int length, int width);
int main() {
int result = calculate_area_ext(10, 5);
printf("Area calculated externally: %d\n", result);
return 0;
}
/*
To compile this:
gcc main.c helper.c -o program
./program
*/Code language: C++ (cpp)
Explanation:
The extern storage class and prototypes enable separate compilation and linking.
- External Linkage: By default, non-static functions have external linkage, meaning they are visible across multiple object files.
- Compiler vs. Linker: When
main.cis compiled, the prototype tells the compiler the function signature, allowing the call to be validated. The compiler assumes the function is defined elsewhere. The linker then takes the object files formain.candhelper.cand resolves the function call by connecting it to the actual code block inhelper.c. Explicitly usingexternon the prototype emphasizes this external linkage.
Exercise 22: inline Function (Optimization Hint)
Problem Statement: Write a simple, short function like get_min(int a, int b) and declare it as inline to suggest to the compiler that it should replace the function call with the function body at the call site, often called inlining.
+ Hint
The inline keyword is a request, not a command. It is typically used for short functions to eliminate the overhead of the function call (stack setup, parameter passing, return jump).
+ Show Solution
#include <stdio.h>
// 'inline' keyword suggests replacing the call with the body
inline int get_min(int a, int b) {
return (a < b) ? a : b;
}
int main() {
int x = 12;
int y = 8;
// When compiled, the compiler might replace this line with:
// int result = (x < y) ? x : y;
int result = get_min(x, y);
printf("The minimum of %d and %d is: %d\n", x, y, result);
return 0;
}Code language: C++ (cpp)
Explanation:
The inline keyword is an optimization hint that minimizes function call overhead.
- Inlining: Instead of creating a separate block of code for
get_minand jumping to it every time it’s called, the compiler copies the function’s body directly into the calling function’s code at the point of the call. - Trade-offs: This can improve speed by removing call overhead but can increase the overall program size (code bloat) if the function is called many times or if the function body is large. It is typically best reserved for very small, frequently used functions.
Exercise 23: Function-like Macro (Simple)
Problem Statement: Define a simple function-like macro SQUARE(x) that calculates x×x. Demonstrate a simple use case.
+ Hint
Macros are defined using #define. To prevent unexpected behavior due to operator precedence, always enclose the entire macro definition and all its arguments in parentheses.
+ Show Solution
#include <stdio.h>
// Function-like macro definition: everything is wrapped in parentheses for safety
#define SQUARE(x) ((x) * (x))
int main() {
int a = 5;
int b = 3;
int result_a, result_b;
// Preprocessor replaces SQUARE(a) with ((a) * (a))
result_a = SQUARE(a);
printf("SQUARE(%d) = %d\n", a, result_a); // 25
// Preprocessor replaces SQUARE(b + 1) with ((b + 1) * (b + 1))
result_b = SQUARE(b + 1);
printf("SQUARE(%d + 1) = %d\n", b, result_b); // 16
return 0;
}Code language: C++ (cpp)
Explanation:
Macros are processed by the C preprocessor before compilation.
- Text Substitution: A macro is a simple text replacement. When the preprocessor sees
SQUARE(a), it literally replaces it with((a) * (a)). - Safety: The parentheses are vital. If the macro were defined as
#define SQUARE(x) x * x, the callSQUARE(b + 1)would expand tob + 1 * b + 1, which evaluates to 3+3+1=7 (due to operator precedence), which is incorrect. Wrapping ensures the intended arithmetic grouping.
Exercise 24: Variadic Functions (...)
Problem Statement: Write a simple variadic function (using stdarg.h, va_list, etc.) called sum_all(int count, ...) that takes an initial integer indicating the number of subsequent arguments and calculates the sum of that variable number of integers.
+ Hint
You must include <stdarg.h>. Use va_list to declare a list variable, va_start to initialize it using the last fixed argument (count), va_arg to extract each argument, and va_end to clean up.
+ Show Solution
#include <stdio.h>
#include <stdarg.h> // Required for variadic functions
// The '...' indicates a variable number of arguments
int sum_all(int count, ...) {
va_list args; // Declare a list to hold the variable arguments
int sum = 0;
int i, num;
// Initialize args list, starting after the last fixed argument ('count')
va_start(args, count);
for (i = 0; i < count; i++) {
// Extract the next argument, specifying it's an 'int'
num = va_arg(args, int);
sum += num;
}
// Clean up the va_list structure
va_end(args);
return sum;
}
int main() {
// Calling with 3 numbers: 10 + 20 + 30 = 60
int result1 = sum_all(3, 10, 20, 30);
printf("Sum of 3 numbers: %d\n", result1);
// Calling with 5 numbers: 1 + 2 + 3 + 4 + 5 = 15
int result2 = sum_all(5, 1, 2, 3, 4, 5);
printf("Sum of 5 numbers: %d\n", result2);
return 0;
}Code language: C++ (cpp)
Explanation:
Variadic functions (like printf) allow a function to accept an indefinite number of arguments.
va_end: Cleans up the memory associated withargsbefore the function returns.count: A variadic function must have at least one fixed argument (counthere) to tell the function how many arguments follow or what their types are.va_start: Initializes the pointer (args) to the beginning of the variable argument list.va_arg: Retrieves the next argument in the list. The second parameter (intin this case) is crucial because the function must explicitly know the type of the data being extracted.
Exercise 25: Exit Function (exit() and atexit())
Problem Statement: Write a program that registers a cleanup function using atexit() that prints a “Cleanup performed” message, and then calls the exit() function from within another function to demonstrate controlled program termination and cleanup.
+ Hint
Both exit() and atexit() require the <stdlib.h> header. atexit takes a pointer to a function that takes no arguments and returns void. The cleanup function will run automatically when exit() is called.
+ Show Solution
#include <stdio.h>
#include <stdlib.h> // Required for atexit() and exit()
// The cleanup function (must take void and return void)
void cleanup_handler(void) {
printf("[atexit] Cleanup performed: Shutting down resources.\n");
}
// Function that forces program termination
void terminate_program(int status) {
printf("[terminate_program] Calling exit() with status %d...\n", status);
// This terminates the program immediately.
// Registered atexit handlers are called automatically.
exit(status);
}
int main() {
int error_code = 1;
// Register the cleanup function.
// It will be called when exit() is executed.
if (atexit(cleanup_handler) != 0) {
fprintf(stderr, "Could not register atexit handler.\n");
return 1;
}
printf("[main] Program execution starting.\n");
terminate_program(error_code);
// This line will NEVER be reached because exit() terminated the program
printf("[main] This line is after exit() and will not print.\n");
return 0; // The program never gets here
}Code language: C++ (cpp)
Explanation:
These functions provide explicit control over a program’s lifetime.
atexit(): Registers a function (the exit handler or cleanup function) to be called automatically upon normal program termination (whenmainreturns or whenexit()is called). This is essential for closing files, releasing locks, or freeing global resources.exit(): Causes immediate, unconditional termination of the program. Unlike returning frommain, it stops execution wherever it’s called. Crucially, it flushes output buffers and calls all functions registered byatexit()before the process ends. (Note:_exit()or_Exit()terminate immediately without callingatexit()handlers.)

Leave a Reply