C Programming Basics: Part 8 – Pointers and Memory Management

Welcome to the eighth part of our C programming tutorial series! In previous articles, we’ve covered the fundamentals of C, variables, operators, control flow, functions, arrays, strings, structures, and unions. Now, we’re tackling one of the most powerful yet challenging aspects of C programming: pointers and memory management.

Pointers are a fundamental feature of C that gives you direct access to memory. This power allows for efficient memory use, complex data structures, and low-level system programming. However, with this power comes responsibility – improper use of pointers can lead to bugs that are difficult to find and fix.

Understanding Pointers

A pointer is a variable that stores the memory address of another variable. Think of memory as a series of numbered boxes, where each box can hold a value. A pointer stores the “box number” (address) rather than the content of the box.

graph LR
    subgraph Memory ["Memory Space"]
        direction LR
        A[("num<br/>Value: 42<br/>Address: 0x7fff...")];
    end
    B[("ptr<br/>Stores Address: 0x7fff...")];
    B -- "points to" --> A;

Declaring Pointers

The syntax for declaring a pointer is:

C
data_type *pointer_name;

For example:

C
int *p;     // Pointer to an integer
char *str;  // Pointer to a character
float *f;   // Pointer to a float

Address Operator (&) and Dereference Operator (*)

To work with pointers, you need to understand two important operators:

  • The address operator (&) returns the memory address of a variable
  • The dereference operator (*) accesses the value at the address stored in a pointer

Here’s a simple example:

C
#include <stdio.h>

int main() {
    int num = 42;    // Regular integer variable
    int *ptr;        // Pointer to an integer
    
    ptr = #      // ptr now holds the address of num
    
    printf("Value of num: %d\n", num);
    printf("Address of num: %p\n", &num);
    printf("Value of ptr (address it points to): %p\n", ptr);
    printf("Value that ptr points to: %d\n", *ptr);
    
    // Modify the value using the pointer
    *ptr = 100;
    printf("New value of num: %d\n", num);
    
    return 0;
}

Let’s visualize what’s happening:

graph TD
    A[ptr<br>0x7fff5fbff83c] -->|points to| B[num<br>42 → 100]
    C["&num = 0x7fff5fbff83c"] --> B
    D["*ptr = 100"] --> B

Null Pointers

A pointer that doesn’t point anywhere should be initialized to NULL:

C
int *ptr = NULL;  // Safe initialization

Checking for NULL before dereferencing helps prevent errors:

C
if (ptr != NULL) {
    *ptr = 10;  // Safe: only dereference if not NULL
}

Pointer Arithmetic

Pointers can be manipulated using arithmetic operations, allowing you to navigate through memory:

1. Incrementing and Decrementing Pointers

C
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // Points to the first element
    
    printf("*ptr = %d\n", *ptr);     // 10
    ptr++;                           // Move to next element
    printf("*ptr = %d\n", *ptr);     // 20
    ptr += 2;                        // Skip two elements
    printf("*ptr = %d\n", *ptr);     // 40
    ptr--;                           // Move back one element
    printf("*ptr = %d\n", *ptr);     // 30
    
    return 0;
}

When a pointer is incremented, it actually advances by the size of the data type it points to (e.g., 4 bytes for an integer on most systems).

2. Pointer Arithmetic with Arrays

Pointers have a natural relationship with arrays:

C
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // Equivalent to &arr[0]
    
    // Access array elements using pointer notation
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d (using array)\n", i, arr[i]);
        printf("*(ptr+%d) = %d (using pointer)\n", i, *(ptr+i));
    }
    
    // These expressions are equivalent:
    printf("arr[2] = %d\n", arr[2]);
    printf("*(arr+2) = %d\n", *(arr+2));
    printf("*(ptr+2) = %d\n", *(ptr+2));
    printf("ptr[2] = %d\n", ptr[2]);
    
    return 0;
}

graph TD
    subgraph Memory ["Array in Memory"]
        direction LR
        arr0[("arr[0]<br/>Value: 10")];
        arr1[("arr[1]<br/>Value: 20")];
        arr2[("arr[2]<br/>Value: 30")];
        arr3[("arr[3]<br/>Value: 40")];
        arr4[("arr[4]<br/>Value: 50")];
        arr0 -- next --> arr1 -- next --> arr2 -- next --> arr3 -- next --> arr4;
    end
    ptr[("ptr")];
    ptr -- "Initially points to" --> arr0;
    ptr -- "After ptr++ points to" --> arr1;
    ptr -- "After ptr+=2 points to" --> arr3;

3. Pointer Subtraction

You can subtract pointers of the same type to find the number of elements between them:

C
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p1 = &arr[1];  // Points to the second element
    int *p2 = &arr[4];  // Points to the fifth element
    
    printf("Elements between p1 and p2: %ld\n", p2 - p1);  // 3
    
    return 0;
}

Pointers and Functions

Pointers are particularly useful when working with functions:

1. Pass by Reference

Recall that C uses pass-by-value by default. With pointers, you can achieve pass-by-reference behavior:

C
#include <stdio.h>

// Swap two integers using pointers
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    
    printf("Before swap: x = %d, y = %d\n", x, y);
    
    swap(&x, &y);
    
    printf("After swap: x = %d, y = %d\n", x, y);
    
    return 0;
}

2. Returning Multiple Values

Pointers allow functions to effectively return multiple values:

C
#include <stdio.h>

// Calculate both the sum and product of two numbers
void calculate(int a, int b, int *sum, int *product) {
    *sum = a + b;
    *product = a * b;
}

int main() {
    int x = 5, y = 7;
    int sum, product;
    
    calculate(x, y, &sum, &product);
    
    printf("%d + %d = %d\n", x, y, sum);
    printf("%d * %d = %d\n", x, y, product);
    
    return 0;
}

3. Pointers to Arrays

When passing arrays to functions, you’re actually passing a pointer to the first element:

C
#include <stdio.h>

// Print array elements
void printArray(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);  // Can use array notation with pointers
    }
    printf("\n");
}

// Sum array elements
int sumArray(int *arr, int size) {
    int total = 0;
    for (int i = 0; i < size; i++) {
        total += *(arr + i);  // Can use pointer arithmetic
    }
    return total;
}

int main() {
    int numbers[5] = {10, 20, 30, 40, 50};
    
    printArray(numbers, 5);
    printf("Sum: %d\n", sumArray(numbers, 5));
    
    return 0;
}

Pointers to Pointers

A pointer can point to another pointer, creating multi-level indirection:

C
#include <stdio.h>

int main() {
    int value = 42;
    int *ptr = &value;     // Pointer to int
    int **pptr = &ptr;     // Pointer to pointer to int
    
    printf("Value: %d\n", value);
    printf("Value via ptr: %d\n", *ptr);
    printf("Value via pptr: %d\n", **pptr);
    
    // Modify value through double pointer
    **pptr = 100;
    printf("New value: %d\n", value);
    
    return 0;
}

Let’s visualize this multi-level indirection:

graph LR
    subgraph Memory ["Memory Space"]
        direction LR
        C[("value<br/>Value: 42<br/>Address: 0xBEF")];
    end
    subgraph Pointers ["Pointer Variables"]
        direction LR
        B[("ptr<br/>Stores Address: 0xBEF<br/>Address: 0xAFC")];
        A[("pptr<br/>Stores Address: 0xAFC")];
    end
    A -- "points to" --> B;
    B -- "points to" --> C;

Multi-level pointers are useful for:

  • Dynamic multi-dimensional arrays
  • Passing pointers by reference
  • Complex data structures like trees

Function Pointers

In C, you can also create pointers to functions. This enables powerful techniques like callbacks and function tables:

C
#include <stdio.h>

// Functions to use
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

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

int divide(int a, int b) {
    if (b != 0) return a / b;
    return 0;
}

int main() {
    // Declare a function pointer
    int (*operation)(int, int);
    char op;
    int a = 15, b = 5, result;
    
    printf("Enter an operation (+, -, *, /): ");
    scanf(" %c", &op);
    
    // Assign the appropriate function to the pointer
    switch (op) {
        case '+': operation = add; break;
        case '-': operation = subtract; break;
        case '*': operation = multiply; break;
        case '/': operation = divide; break;
        default: 
            printf("Invalid operation\n");
            return 1;
    }
    
    // Call the function through the pointer
    result = operation(a, b);
    printf("%d %c %d = %d\n", a, op, b, result);
    
    return 0;
}

Dynamic Memory Allocation

One of the most powerful applications of pointers is dynamic memory allocation – allocating memory at runtime rather than compile time. C provides several functions for this purpose:

1. malloc() – Memory Allocation

Allocates a specified number of bytes and returns a pointer to the allocated memory:

C
#include <stdio.h>
#include <stdlib.h>  // Required for malloc

int main() {
    int *ptr;
    int n = 5;
    
    // Allocate memory for 5 integers
    ptr = (int*)malloc(n * sizeof(int));
    
    // Check if memory allocation was successful
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    
    // Use the allocated memory
    for (int i = 0; i < n; i++) {
        ptr[i] = i * 10;
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    
    // Free the allocated memory when done
    free(ptr);
    
    return 0;
}

2. calloc() – Contiguous Allocation

Similar to malloc(), but initializes the allocated memory to zero:

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

int main() {
    int *ptr;
    int n = 5;
    
    // Allocate and initialize memory for 5 integers
    ptr = (int*)calloc(n, sizeof(int));
    
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    
    // Memory is already initialized to zero
    for (int i = 0; i < n; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    
    free(ptr);
    
    return 0;
}

3. realloc() – Memory Reallocation

Changes the size of a previously allocated memory block:

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

int main() {
    int *ptr;
    int n = 5;
    
    // Initial allocation
    ptr = (int*)malloc(n * sizeof(int));
    
    if (ptr == NULL) {
        printf("Memory allocation failed\n");
        return 1;
    }
    
    // Initialize the array
    for (int i = 0; i < n; i++) {
        ptr[i] = i * 10;
    }
    
    // Increase the size to 10 integers
    n = 10;
    ptr = (int*)realloc(ptr, n * sizeof(int));
    
    if (ptr == NULL) {
        printf("Memory reallocation failed\n");
        return 1;
    }
    
    // Initialize the new elements
    for (int i = 5; i < n; i++) {
        ptr[i] = i * 10;
    }
    
    // Print all elements
    for (int i = 0; i < n; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }
    
    free(ptr);
    
    return 0;
}

4. free() – Memory Deallocation

Releases memory previously allocated by malloc()calloc(), or realloc():

stateDiagram-v2
    [*] --> HeapAvailable: Program Start
    HeapAvailable --> Allocated: malloc(size)
    Allocated --> HeapAvailable: free(ptr)
    Allocated --> Allocated: Use Memory
    state Allocated {
        Pointer --> Block: Points to allocated block
    }
C
free(ptr);  // Frees the memory pointed to by ptr

Always free dynamically allocated memory when you’re done with it to prevent memory leaks.

Common Memory Management Pitfalls

Working with pointers and dynamic memory can lead to several common errors:

1. Memory Leaks

Forgetting to free allocated memory:

C
void memoryLeak() {
    int *ptr = (int*)malloc(sizeof(int));
    *ptr = 10;
    // Missing free(ptr) - memory leak!
}

The memory remains allocated even after the function returns, and the pointer is lost.

2. Dangling Pointers

Using a pointer after freeing its memory:

C
int *ptr = (int*)malloc(sizeof(int));
*ptr = 10;
free(ptr);
*ptr = 20;  // ERROR: Dangling pointer access

After free(), the memory may be reused for other purposes, making this access unpredictable and potentially harmful.

3. Double Free

Freeing the same memory multiple times:

C
int *ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr);  // ERROR: Double free

This can corrupt the memory allocator’s data structures.

4. Buffer Overflow

Writing beyond the allocated memory:

C
int *arr = (int*)malloc(5 * sizeof(int));
for (int i = 0; i < 10; i++) {  // ERROR: Writing beyond allocated memory
    arr[i] = i;
}

Buffer overflows can corrupt adjacent memory, leading to unpredictable behavior or crashes.

5. Memory Fragmentation

While not a direct error in code, memory fragmentation occurs when repeated allocations and deallocations create small, unusable gaps in memory. Good practice involves allocating memory in larger chunks where possible and using data structures efficiently.

6. Not Checking Allocation Success

Always check if memory allocation was successful:

C
int *ptr = (int*)malloc(1000000 * sizeof(int));
// Bad: No check for failure
*ptr = 10;  // Could crash if allocation failed

// Good practice:
if (ptr == NULL) {
    printf("Memory allocation failed\n");
    return 1;
}

7. Using Uninitialized Pointers

C
int *ptr;        // Uninitialized pointer
*ptr = 10;       // ERROR: Dereferencing uninitialized pointer

Always initialize pointers before using them.

Best Practices for Pointers and Memory Management

To avoid memory-related issues, follow these best practices:

1. Initialize Pointers

Always initialize pointers, either to a valid address or to NULL:

C
int *ptr = NULL;  // Safe initialization

2. Check for NULL After Allocation

C
ptr = (int*)malloc(size);
if (ptr == NULL) {
    // Handle allocation failure
    return 1;
}

3. Free Memory When Done

C
free(ptr);
ptr = NULL;  // Set to NULL after freeing

Setting the pointer to NULL after freeing helps prevent accidental use of freed memory.

4. Use Defensive Programming

Check pointers before dereferencing:

C
if (ptr != NULL) {
    *ptr = value;
}

5. Be Careful with Pointer Arithmetic

Ensure that pointer arithmetic doesn’t exceed array bounds:

C
for (int i = 0; i < array_size; i++) {
    *(ptr + i) = value;  // Make sure i is within bounds
}

6. Avoid Complex Pointer Expressions

Keep pointer expressions simple and clear. Complex expressions with multiple dereferences can be error-prone.

Dynamic Data Structures

Pointers and dynamic memory allocation are essential for creating dynamic data structures, such as linked lists, trees, and graphs:

1. Linked List Example

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

// Define the node structure
struct Node {
    int data;
    struct Node *next;
};

// Function to create a new node
struct Node* createNode(int value) {
    struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = value;
    newNode->next = NULL;
    return newNode;
}

// Function to insert a node at the beginning of the list
struct Node* insertAtBeginning(struct Node *head, int value) {
    struct Node *newNode = createNode(value);
    newNode->next = head;
    return newNode;  // New node becomes the head
}

// Function to display the list
void displayList(struct Node *head) {
    struct Node *current = head;
    while (current != NULL) {
        printf("%d -> ", current->data);
        current = current->next;
    }
    printf("NULL\n");
}

// Function to free the entire list
void freeList(struct Node *head) {
    struct Node *current = head;
    struct Node *next;
    
    while (current != NULL) {
        next = current->next;  // Save the next node
        free(current);         // Free the current node
        current = next;        // Move to the next node
    }
}

int main() {
    struct Node *head = NULL;  // Initialize an empty list
    
    // Insert some nodes
    head = insertAtBeginning(head, 30);
    head = insertAtBeginning(head, 20);
    head = insertAtBeginning(head, 10);
    
    printf("Linked List: ");
    displayList(head);
    
    // Free the list when done
    freeList(head);
    
    return 0;
}

This linked list implementation demonstrates:

graph LR

    subgraph Node1_0x100 ["Node 1 (addr: 0x100)"]
        data1[("data: 10")]
        next1[("next: 0x200")]
    end

    subgraph Node2_0x200 ["Node 2 (addr: 0x200)"]
        data2[("data: 20")]
        next2[("next: NULL")]
    end

    next1 -- "points to" --> data2
  • Dynamic allocation of nodes
  • Pointer manipulation to link nodes
  • Proper memory deallocation

2. Binary Tree Example

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

// Define the tree node structure
struct TreeNode {
    int data;
    struct TreeNode *left;
    struct TreeNode *right;
};

// Function to create a new node
struct TreeNode* createNode(int value) {
    struct TreeNode *newNode = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    if (newNode == NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }
    newNode->data = value;
    newNode->left = NULL;
    newNode->right = NULL;
    return newNode;
}

// Function to insert a node in the binary search tree
struct TreeNode* insert(struct TreeNode *root, int value) {
    // If the tree is empty, create a new node
    if (root == NULL) {
        return createNode(value);
    }
    
    // Otherwise, recur down the tree
    if (value < root->data) {
        root->left = insert(root->left, value);
    } else if (value > root->data) {
        root->right = insert(root->right, value);
    }
    
    // Return the unchanged node pointer
    return root;
}

// Function for inorder traversal
void inorderTraversal(struct TreeNode *root) {
    if (root != NULL) {
        inorderTraversal(root->left);
        printf("%d ", root->data);
        inorderTraversal(root->right);
    }
}

// Function to free the entire tree
void freeTree(struct TreeNode *root) {
    if (root != NULL) {
        freeTree(root->left);
        freeTree(root->right);
        free(root);
    }
}

int main() {
    struct TreeNode *root = NULL;
    
    // Insert nodes
    root = insert(root, 50);
    insert(root, 30);
    insert(root, 20);
    insert(root, 40);
    insert(root, 70);
    insert(root, 60);
    insert(root, 80);
    
    printf("Inorder traversal: ");
    inorderTraversal(root);
    printf("\n");
    
    // Free the tree
    freeTree(root);
    
    return 0;
}

This binary tree example shows:

graph TD

    subgraph Root_Node_50 ["Root Node (50)"]
        R[("data: 50")]
        RL[("left")]
        RR[("right")]
    end

    subgraph Left_Child_30 ["Left Child (30)"]
        L[("data: 30")]
        LL[("left: NULL")]
        LR[("right: NULL")]
    end

    subgraph Right_Child_70 ["Right Child (70)"]
        C[("data: 70")]
        CL[("left: NULL")]
        CR[("right: NULL")]
    end

    RL --> L
    RR --> C
    L --> LL
    L --> LR
    C --> CL
    C --> CR
  • Recursive structures using pointers
  • Tree traversal using pointers
  • Proper memory cleanup

Void Pointers

A void pointer (void*) is a generic pointer that can point to any data type:

C
#include <stdio.h>

int main() {
    int i = 10;
    float f = 3.14;
    char c = 'A';
    
    // Void pointer can point to any type
    void *ptr;
    
    ptr = &i;  // Point to an integer
    printf("Integer value: %d\n", *((int*)ptr));  // Cast before dereferencing
    
    ptr = &f;  // Point to a float
    printf("Float value: %.2f\n", *((float*)ptr));
    
    ptr = &c;  // Point to a char
    printf("Character value: %c\n", *((char*)ptr));
    
    return 0;
}

Key points about void pointers:

  • They cannot be dereferenced directly; they must be cast to the appropriate type first
  • They’re useful for functions that need to handle multiple data types
  • Standard library functions like malloc() return void* so they can work with any data type

Example: Generic Swap Function

C
#include <stdio.h>
#include <string.h>

// Generic swap function using void pointers
void swap(void *a, void *b, size_t size) {
    // Temporary storage
    char temp[size];
    
    // Copy original values
    memcpy(temp, a, size);
    memcpy(a, b, size);
    memcpy(b, temp, size);
}

int main() {
    int a = 5, b = 10;
    printf("Before swap: a = %d, b = %d\n", a, b);
    swap(&a, &b, sizeof(int));
    printf("After swap: a = %d, b = %d\n", a, b);
    
    float x = 3.14, y = 2.71;
    printf("Before swap: x = %.2f, y = %.2f\n", x, y);
    swap(&x, &y, sizeof(float));
    printf("After swap: x = %.2f, y = %.2f\n", x, y);
    
    return 0;
}

Practical Example: Dynamic String Management

Let’s create a simple dynamic string library that uses pointers and dynamic memory allocation:

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

// Define a dynamic string structure
typedef struct {
    char *data;     // Pointer to character array
    size_t length;  // Current string length
    size_t capacity;  // Allocated buffer size
} DynamicString;

// Initialize a dynamic string
DynamicString* initString() {
    DynamicString *str = (DynamicString*)malloc(sizeof(DynamicString));
    if (str == NULL) {
        return NULL;
    }
    
    // Initial capacity is 16 characters
    str->capacity = 16;
    str->data = (char*)malloc(str->capacity * sizeof(char));
    
    if (str->data == NULL) {
        free(str);
        return NULL;
    }
    
    str->data[0] = '\0';  // Empty string
    str->length = 0;
    
    return str;
}

// Ensure the string has enough capacity
void ensureCapacity(DynamicString *str, size_t requiredCapacity) {
    if (requiredCapacity <= str->capacity) {
        return;  // Already enough capacity
    }
    
    // Double the capacity until it's enough
    while (str->capacity < requiredCapacity) {
        str->capacity *= 2;
    }
    
    // Reallocate the buffer
    str->data = (char*)realloc(str->data, str->capacity * sizeof(char));
    
    if (str->data == NULL) {
        fprintf(stderr, "Memory reallocation failed\n");
        exit(1);
    }
}

// Append a string
void appendString(DynamicString *str, const char *text) {
    size_t textLength = strlen(text);
    size_t requiredCapacity = str->length + textLength + 1;  // +1 for null terminator
    
    ensureCapacity(str, requiredCapacity);
    
    // Append the text
    strcpy(str->data + str->length, text);
    str->length += textLength;
}

// Get the string data
const char* getString(DynamicString *str) {
    return str->data;
}

// Free the dynamic string
void freeString(DynamicString *str) {
    if (str != NULL) {
        free(str->data);
        free(str);
    }
}

int main() {
    DynamicString *myString = initString();
    if (myString == NULL) {
        printf("Failed to initialize string\n");
        return 1;
    }
    
    appendString(myString, "Hello");
    appendString(myString, ", ");
    appendString(myString, "World!");
    
    printf("Dynamic string: %s\n", getString(myString));
    printf("Length: %zu\n", myString->length);
    printf("Capacity: %zu\n", myString->capacity);
    
    freeString(myString);
    
    return 0;
}

This example demonstrates:

  • Creating a user-defined type with a pointer to dynamically allocated memory
  • Growing the buffer as needed
  • Proper cleanup to avoid memory leaks

Conclusion

Pointers and dynamic memory management are among the most powerful features of C, but they require careful handling. By understanding how pointers work and following best practices for memory management, you can write efficient, flexible, and robust programs.

In this article, we’ve covered:

  • Basic pointer concepts and operations
  • Pointer arithmetic
  • Passing pointers to functions
  • Multi-level pointers
  • Function pointers
  • Dynamic memory allocation with malloc()calloc(), and realloc()
  • Common memory-related errors and how to avoid them
  • Dynamic data structures
  • Void pointers
  • A practical example of dynamic string management

Mastering pointers opens up a world of possibilities in C programming, from low-level system programming to sophisticated data structures and algorithms.

In the next part of our series, we’ll explore file handling in C, which will allow your programs to interact with the file system for reading and writing data.

Practice Exercises

  1. Write a function that takes an array of integers and reverses it in place using pointer arithmetic.
  2. Implement a dynamic array that grows automatically when elements are added.
  3. Create a linked list implementation with functions to insert, delete, and search for elements.
  4. Write a program that uses malloc() to create a 2D array with dimensions specified by the user.
  5. Implement a simple memory pool allocator that manages a block of memory and allocates smaller chunks from it.
  6. Write a generic function that can find the maximum element in an array of any data type, using void pointers.
  7. Create a binary search tree with functions for insertion, deletion, and traversal.

Happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top