Lesson 03-C Language Functions and Modularization

Functions and Modularization

Function Basics

In C, functions are the fundamental building blocks of a program, used to implement specific functionalities. Functions enhance code reusability, make program structure clearer, and simplify maintenance.

Function Definition

// Function declaration
int add(int x, int y);

// Function definition
int add(int x, int y) {
    return x + y;
}

A function definition consists of a function header and a function body. The header includes the return type, function name, and parameter list.

Function Call

int main() {
    int result = add(5, 3);
    printf("Result: %d\n", result);
    return 0;
}

When a function is called, actual arguments are passed to formal parameters, and the function returns a result after execution.

Modular Programming

Modular programming involves breaking a program into independent, reusable components, each implementing a specific function. In C, this typically means organizing functions and data into separate files.

Header Files

Header files (.h) are used to declare function prototypes and data types for use in other files.

// math.h
#ifndef MATH_H
#define MATH_H

int add(int x, int y);
int subtract(int x, int y);

#endif

Source Files

Source files (.c) contain the implementation of functions.

// math.c
#include "math.h"

int add(int x, int y) {
    return x + y;
}

int subtract(int x, int y) {
    return x - y;
}

Main File

The main file typically contains the main function, which calls functions from other modules.

// main.c
#include <stdio.h>
#include "math.h"

int main() {
    int sum = add(10, 5);
    int diff = subtract(10, 5);
    printf("Sum: %d, Difference: %d\n", sum, diff);
    return 0;
}

Benefits of Modular Programming

  • Code Reusability: Modular functions can be reused across multiple projects.
  • Ease of Maintenance: Each module focuses on a single responsibility, simplifying debugging and updates.
  • Improved Readability: Modular code is structured clearly, making it easier to understand and trace.

Challenges of Modular Programming

  • Dependency Management: Ensure correct header files are included to avoid naming conflicts.
  • Compilation and Linking: Properly link all relevant source files and libraries.

Example Application

Let’s analyze a simple modular C program step-by-step.

Create Header File (math.h)

#ifndef MATH_H
#define MATH_H

int add(int x, int y);
int subtract(int x, int y);

#endif

Create Source File (math.c)

#include "math.h"

int add(int x, int y) {
    return x + y;
}

int subtract(int x, int y) {
    return x - y;
}

Create Main File (main.c)

#include <stdio.h>
#include "math.h"

int main() {
    int sum = add(10, 5);
    int diff = subtract(10, 5);
    printf("Sum: %d, Difference: %d\n", sum, diff);
    return 0;
}

Compile and Run

gcc -o main main.c math.c
./main

Output:

Sum: 15, Difference: 5

Parameter Passing (Value Passing, Pointer Passing)

Value Passing

Value passing is the most common parameter passing method. The actual argument’s value is copied to the formal parameter, so modifications to the parameter inside the function do not affect the actual argument.

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y); // Note: x and y are not swapped
    printf("x: %d, y: %d\n", x, y);
    return 0;
}

Output:

x: 10, y: 20

Despite the swap function attempting to swap a and b, the values of x and y remain unchanged due to value passing.

Pointer Passing

Pointer passing allows a function to directly access and modify actual arguments by receiving their addresses. Changes to the parameter are reflected in the actual argument.

#include <stdio.h>

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(&x, &y); // Note: Address-of operator & is used
    printf("x: %d, y: %d\n", x, y);
    return 0;
}

Output:

x: 20, y: 10

The swap function successfully swaps x and y by operating on their addresses.

Comparison of Value Passing and Pointer Passing

  • Value Passing: Simple and intuitive but may incur significant memory copying overhead, especially for large data structures.
  • Pointer Passing: More efficient, particularly for large data structures, as it passes only addresses, but requires careful handling to avoid null pointers or invalid memory access.

Notes

  • When using pointer passing, ensure pointers are valid to avoid wild or dangling pointers.
  • Use the dereference operator (*) to access the value pointed to by a pointer.
  • For complex structures or arrays, pointer passing is often preferred to avoid copying entire structures.

Function Prototypes and Return Values

Function Prototype

A function prototype is a declaration of a function’s name, return type, and parameter types and counts, informing the compiler before the function is called. Parameter names are optional in prototypes.

int add(int, int);

This declares a function named add that takes two int parameters and returns an int.

Function Definition

The function definition provides the actual implementation, including the function header (return type, name, parameter list) and body (executable statements).

int add(int x, int y) {
    return x + y;
}

This defines the add function, performing addition and returning the result.

Return Value

The return value is determined by the return statement, which passes control back to the caller and may carry a value. If a function does not return a value, its return type should be void.

int multiply(int a, int b) {
    return a * b;
}

int main() {
    int result = multiply(10, 5);
    printf("The result is %d\n", result);
    return 0;
}

Here, the multiply function returns the product of two integers, stored in result.

Role of Function Prototypes

  • Type Checking: The compiler uses prototypes to verify that parameter types match during calls.
  • Forward Declaration: Functions can be called before their definition if a prototype is declared earlier.

Notes on Prototypes and Return Values

  • If a function declares a return type but lacks a return statement, it returns an undefined value, leading to unpredictable behavior.
  • Returning complex types like structures involves copying the entire structure.
  • Use void as the return type to indicate no return value.

Recursive Functions

Components of Recursive Functions

Recursive functions have two key parts:

  • Base Case: The condition that stops recursion, returning a result without further calls.
  • Recursive Case: The part where the function calls itself, moving closer to the base case with each call.

Examples of Recursive Functions

Calculating Factorial

The factorial function is a classic recursive example, where n! is the product of all positive integers up to n.

#include <stdio.h>

int factorial(int n) {
    if (n == 0) {
        // Base case: factorial of 0 is 1
        return 1;
    } else {
        // Recursive case: n! = n * (n-1)!
        return n * factorial(n - 1);
    }
}

int main() {
    int num = 5;
    printf("%d! = %d\n", num, factorial(num));
    return 0;
}

Fibonacci Sequence

The Fibonacci sequence is a series where each number is the sum of the two preceding ones.

#include <stdio.h>

int fibonacci(int n) {
    if (n <= 1) {
        // Base case: F(0) = 0, F(1) = 1
        return n;
    } else {
        // Recursive case: F(n) = F(n-1) + F(n-2)
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

int main() {
    int num = 10;
    printf("Fibonacci(%d) = %d\n", num, fibonacci(num));
    return 0;
}

Notes on Recursion

  • Avoid Infinite Recursion: Ensure a clear termination condition to prevent stack overflow.
  • Efficiency Concerns: Recursive functions may be less efficient, especially for deep recursion, due to call overhead. Iterative solutions or other algorithms may be preferable.
  • Tail Recursion Optimization: Some compilers optimize tail recursion into loops, but C standards do not guarantee this.

Recursion and the Stack

Each recursive call creates a new stack frame on the call stack to store local variables and parameters. When the function returns, the frame is popped. Deep recursion can cause stack overflow.

Recursion vs. Iteration

Recursive functions are often concise and intuitive but may be less efficient without tail recursion optimization. Iterative versions (using loops) are typically more efficient but may be less readable.

Use of Standard Library Functions

Input and Output

printf and scanf

  • printf: Formats output to the standard output (usually the screen).
  • scanf: Reads formatted input from the standard input (usually the keyboard).
#include <stdio.h>

int main() {
    int age;
    printf("Enter your age: ");
    scanf("%d", &age);
    printf("Your age is: %d\n", age);
    return 0;
}

String Manipulation

strlen, strcpy, strcat, strcmp

  • strlen: Returns the length of a string (excluding the null terminator \0).
  • strcpy: Copies one string to another.
  • strcat: Concatenates one string to the end of another.
  • strcmp: Compares two strings, returning 0 if equal, non-zero otherwise.
#include <string.h>

int main() {
    char str1[50] = "Hello";
    char str2[] = "World";
    strcat(str1, str2);
    printf("%s\n", str1);
    return 0;
}

Mathematical Functions

sqrt, pow, sin, cos, tan, exp, log These functions, defined in <math.h>, perform various mathematical operations.

#include <math.h>

int main() {
    double x = 4.0;
    printf("Square root of %.2f is %.2f\n", x, sqrt(x));
    return 0;
}

Memory Operations

malloc, calloc, realloc, free

  • malloc: Allocates memory of a specified size.
  • calloc: Allocates memory and initializes it to zero.
  • realloc: Resizes previously allocated memory.
  • free: Releases previously allocated memory.
#include <stdlib.h>

int main() {
    int *arr = malloc(5 * sizeof(int));
    arr[0] = 10;
    free(arr);
    return 0;
}

Date and Time

time, localtime, asctime

  • time: Retrieves the current time.
  • localtime: Converts time to a local time structure.
  • asctime: Converts a time structure to a readable string.
#include <time.h>

int main() {
    time_t rawtime;
    struct tm *timeinfo;
    time(&rawtime);
    timeinfo = localtime(&rawtime);
    printf("Current local time: %s", asctime(timeinfo));
    return 0;
}

Character Classification

isalnum, isalpha, isdigit, toupper, tolower

  • isalnum: Checks if a character is alphanumeric.
  • isalpha: Checks if a character is a letter.
  • isdigit: Checks if a character is a digit.
  • toupper: Converts a lowercase letter to uppercase.
  • tolower: Converts an uppercase letter to lowercase.
#include <ctype.h>

int main() {
    char ch = 'A';
    printf("Is '%c' alphanumeric? %d\n", ch, isalnum(ch));
    return 0;
}

File Operations

fopen, fclose, fread, fwrite, fprintf, fscanf

  • fopen: Opens or creates a file.
  • fclose: Closes an open file.
  • fread and fwrite: Read from or write to a file.
  • fprintf and fscanf: Format file reading and writing.
#include <stdio.h>

int main() {
    FILE *fp = fopen("example.txt", "w");
    fprintf(fp, "Hello, world!");
    fclose(fp);
    return 0;
}

Environment and State

  • getenv: Retrieves the value of an environment variable.
  • setenv: Sets the value of an environment variable.
  • unsetenv: Removes an environment variable.
#include <stdlib.h>
#include <stdio.h>

int main() {
    // Retrieve environment variable
    char *path = getenv("PATH");
    if (path != NULL) {
        printf("PATH: %s\n", path);
    }

    // Set environment variable
    if (setenv("MY_VAR", "Hello World", 1) != 0) {
        perror("setenv");
        return 1;
    }

    // Retrieve set environment variable
    char *myVar = getenv("MY_VAR");
    if (myVar != NULL) {
        printf("MY_VAR: %s\n", myVar);
    }

    // Remove environment variable
    if (unsetenv("MY_VAR") != 0) {
        perror("unsetenv");
        return 1;
    }

    // Attempt to retrieve removed variable
    myVar = getenv("MY_VAR");
    if (myVar == NULL) {
        printf("MY_VAR has been unset.\n");
    }

    return 0;
}

Signal Handling

  • signal: Sets a signal handling function.
  • raise: Sends a signal.
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void signalHandler(int signum) {
    printf("Signal handler called with signal %d\n", signum);
    printf("Interrupt (Ctrl+C) was ignored\n");
}

int main() {
    // Register signal handler
    signal(SIGINT, signalHandler);

    // Loop waiting for signals
    while (1) {
        printf("Waiting for signal...\n");
        sleep(1);
    }

    return 0;
}

Process Control

  • fork: Creates a child process.
  • execv, execvp: Replaces the current process image.
  • wait, waitpid: Waits for a child process to terminate.
  • exit, _exit: Terminates a process.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();

    if (pid < 0) {
        perror("Fork failed");
        return 1;
    } else if (pid > 0) {
        // Parent process
        int status;
        wait(&status); // Wait for child process to terminate
        printf("Child process terminated\n");
    } else {
        // Child process
        execlp("/bin/ls", "ls", "-l", (char *)NULL); // Replace process image
        perror("Exec failed");
        exit(1);
    }

    return 0;
}

Directory and File System Operations

  • opendir, readdir, closedir: Directory operations.
  • mkdir, rmdir: Create and delete directories.
  • rename: Rename files or directories.
  • remove: Delete files.
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

int main() {
    DIR *dir;
    struct dirent *ent;

    if ((dir = opendir(".")) != NULL) {
        while ((ent = readdir(dir)) != NULL) {
            printf("%s\n", ent->d_name);
        }
        closedir(dir);
    } else {
        perror("Could not open directory");
        return EXIT_FAILURE;
    }

    // Create directory
    if (mkdir("new_dir", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == -1) {
        perror("mkdir");
        return EXIT_FAILURE;
    }

    // Delete directory
    if (rmdir("new_dir") == -1) {
        perror("rmdir");
        return EXIT_FAILURE;
    }

    // Rename file
    if (rename("old_file.txt", "new_file.txt") == -1) {
        perror("rename");
        return EXIT_FAILURE;
    }

    // Delete file
    if (remove("new_file.txt") == -1) {
        perror("remove");
        return EXIT_FAILURE;
    }

    return 0;
}

Network Programming

  • socket: Creates a socket.
  • bind, listen: Binds a socket and listens for connections.
  • accept: Accepts a connection.
  • connect: Establishes a connection.
  • send, recv: Sends and receives data.
  • select: Waits for socket readiness.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080

int main(int argc, char const *argv[]) {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    const char *hello = "Hello from server";

    // Create socket
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // Set socket options
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
                   &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    // Bind socket
    if (bind(server_fd, (struct sockaddr *)&address,
             sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // Listen for connections
    if (listen(server_fd, 3) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // Accept connection
    if ((new_socket = accept(server_fd, (struct sockaddr *)&address,
                             (socklen_t*)&addrlen)) < 0) {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    // Send data
    send(new_socket, hello, strlen(hello), 0);
    printf("Hello message sent\n");

    // Receive data
    int valread = read(new_socket, buffer, 1024);
    printf("%s\n", buffer);
    return 0;
}

Multithreading

Although the standard C library does not directly support multithreading, POSIX standards and Windows APIs provide threading support, as shown below:

  • pthread_create: Creates a thread.
  • pthread_join: Waits for a thread to terminate.
  • pthread_mutex_lock, pthread_mutex_unlock: Locks and unlocks a mutex.
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

// Define mutex
pthread_mutex_t lock;

// Thread function
void* thread_function(void *arg) {
    pthread_mutex_lock(&lock); // Lock mutex
    printf("Thread executing\n");
    usleep(1000000); // Simulate work
    pthread_mutex_unlock(&lock); // Unlock mutex
    return NULL;
}

int main() {
    pthread_t thread_id;

    // Initialize mutex
    pthread_mutex_init(&lock, NULL);

    // Create thread
    pthread_create(&thread_id, NULL, thread_function, NULL);

    // Wait for thread to terminate
    pthread_join(thread_id, NULL);

    // Destroy mutex
    pthread_mutex_destroy(&lock);

    return 0;
}

Dynamic Linking

  • dlopen: Opens a shared library.
  • dlsym: Retrieves a symbol from a shared library.
  • dlclose: Closes a shared library.
#include <stdio.h>
#include <dlfcn.h>

int main() {
    void *handle;
    int (*add)(int, int);

    // Open shared library
    handle = dlopen("./libexample.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "%s\n", dlerror());
        return 1;
    }

    // Retrieve symbol
    add = (int (*)(int, int)) dlsym(handle, "add");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        fprintf(stderr, "%s\n", dlsym_error);
        dlclose(handle);
        return 1;
    }

    // Call function
    int result = add(10, 20);
    printf("Result: %d\n", result);

    // Close shared library
    dlclose(handle);

    return 0;
}

Error Handling

  • perror: Prints an error message and system error number.
  • errno: Retrieves the system error number.
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>

int main() {
    int fd = open("nonexistentfile.txt", O_RDONLY);

    if (fd == -1) {
        perror("Error opening file");
        // errno can be used for further error handling
        if (errno == ENOENT) {
            printf("The file does not exist.\n");
        }
    }

    return 0;
}

Miscellaneous

  • rand, srand: Random number generation.
  • abs, labs, llabs: Compute absolute values.
  • qsort: Quick sort function.

Example: Generating Random Numbers with rand and srand

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    int i;
    srand(time(NULL)); // Initialize random number generator
    for (i = 0; i < 10; i++) {
        printf("%d ", rand() % 100); // Generate random numbers between 0 and 99
    }
    printf("\n");
    return 0;
}

Example: Sorting with qsort

#include <stdio.h>
#include <stdlib.h>

int compare(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

int main() {
    int array[] = {5, 1, 9, 3, 7};
    int n = sizeof(array)/sizeof(array[0]);
    
    qsort(array, n, sizeof(int), compare);
    
    for (int i = 0; i < n; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    return 0;
}

Share your love