40 Intermediate C Tutorials

40 Intermediate C Tutorials

1. Pointer to Pointer

A pointer to a pointer stores the address of another pointer, enabling multi-level indirection.

Example: Using a pointer to a pointer.

#include 
int main() {
int x = 10;
int *ptr = &x;
int **pptr = &ptr;
printf("Value: %d, Pointer: %p, Pointer to Pointer: %p\n", **pptr, (void*)ptr, (void*)pptr);
return 0;
}

Value: 10, Pointer: [address], Pointer to Pointer: [address]

Note: **pptr accesses the value through two levels of indirection. Useful for dynamic 2D arrays or function arguments.

2. Array of Pointers

An array of pointers stores addresses of variables, useful for managing multiple data items.

Example: Storing pointers to integers.

#include 
int main() {
int a = 1, b = 2, c = 3;
int *arr[] = {&a, &b, &c};
for (int i = 0; i < 3; i++) {
printf("Value %d: %d\n", i, *arr[i]);
}
return 0;
}

Value 0: 1
Value 1: 2
Value 2: 3

Note: Each element is a pointer. Useful for arrays of strings or dynamic data structures.

3. Void Pointers

Void pointers are generic pointers that can point to any data type but require casting to access.

Example: Using a void pointer.

#include 
int main() {
int x = 10;
void *vptr = &x;
printf("Value: %d\n", *(int*)vptr);
return 0;
}

Value: 10

Note: Cast void* to the correct type before dereferencing. Useful for generic functions.

4. Function Pointers Advanced

Function pointers can be used in arrays or as parameters for dynamic function calls.

Example: Array of function pointers.

#include 
void add(int a, int b) { printf("Sum: %d\n", a + b); }
void subtract(int a, int b) { printf("Difference: %d\n", a - b); }
int main() {
void (*ops[])(int, int) = {add, subtract};
ops[0](5, 3);
ops[1](5, 3);
return 0;
}

Sum: 8
Difference: 2

Note: Useful for implementing callbacks or operation tables. Ensure correct function signature.

5. Dynamic Array Resizing

Dynamic arrays can be resized using realloc() for flexible memory management.

Example: Resizing an array.

#include 
#include 
int main() {
int *arr = (int*)malloc(2 * sizeof(int));
arr[0] = 1; arr[1] = 2;
arr = (int*)realloc(arr, 4 * sizeof(int));
arr[2] = 3; arr[3] = 4;
for (int i = 0; i < 4; i++) printf("%d ", arr[i]);
free(arr);
return 0;
}

1 2 3 4

Note: realloc() may move the memory block. Always check for NULL and free memory.

6. Linked List Basics

A linked list is a dynamic data structure where nodes contain data and a pointer to the next node.

Example: Creating a simple linked list.

#include 
#include 
struct Node {
int data;
struct Node* next;
};
int main() {
struct Node* head = (struct Node*)malloc(sizeof(struct Node));
head->data = 1;
head->next = NULL;
printf("Node data: %d\n", head->data);
free(head);
return 0;
}

Node data: 1

Note: Each node requires dynamic allocation. Set next to NULL for the last node.

7. Stack Implementation

A stack is a LIFO (Last In, First Out) data structure implemented using arrays or linked lists.

Example: Array-based stack with push.

#include 
#define MAX 5
int stack[MAX], top = -1;
void push(int value) {
if (top < MAX - 1) stack[++top] = value;
}
int main() {
push(1); push(2);
printf("Top element: %d\n", stack[top]);
return 0;
}

Top element: 2

Note: Check for stack overflow. Use top to track the current position.

8. Queue Implementation

A queue is a FIFO (First In, First Out) data structure implemented using arrays or linked lists.

Example: Array-based queue with enqueue.

#include 
#define MAX 5
int queue[MAX], front = -1, rear = -1;
void enqueue(int value) {
if (rear < MAX - 1) queue[++rear] = value;
if (front == -1) front = 0;
}
int main() {
enqueue(1); enqueue(2);
printf("Front element: %d\n", queue[front]);
return 0;
}

Front element: 1

Note: Track front and rear indices. Check for queue full condition.

9. Binary Search

Binary search finds an element in a sorted array by repeatedly dividing the search range.

Example: Searching for a value.

#include 
int binarySearch(int arr[], int size, int target) {
int left = 0, right = size - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
int main() {
int arr[] = {2, 4, 6, 8};
printf("Index of 6: %d\n", binarySearch(arr, 4, 6));
return 0;
}

Index of 6: 2

Note: Array must be sorted. Time complexity is O(log n).

10. Bubble Sort

Bubble sort repeatedly swaps adjacent elements if they are in the wrong order.

Example: Sorting an array.

#include 
void bubbleSort(int arr[], int size) {
for (int i = 0; i < size - 1; i++)
for (int j = 0; j < size - i - 1; j++)
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
int main() {
int arr[] = {5, 2, 8, 1};
bubbleSort(arr, 4);
for (int i = 0; i < 4; i++) printf("%d ", arr[i]);
return 0;
}

1 2 5 8

Note: Simple but inefficient for large datasets. Time complexity is O(n²).

11. Struct Pointers

Pointers to structures allow efficient access and modification of struct members.

Example: Using a struct pointer.

#include 
struct Point {
int x, y;
};
int main() {
struct Point p = {3, 4};
struct Point *ptr = &p;
printf("Point: (%d, %d)\n", ptr->x, ptr->y);
return 0;
}

Point: (3, 4)

Note: Use -> to access members via pointers. More efficient for passing structs to functions.

12. File Binary I/O

Binary file I/O reads/writes data in binary format using fread() and fwrite().

Example: Writing and reading a struct.

#include 
struct Record {
int id;
char name[20];
};
int main() {
struct Record r = {1, "Alice"};
FILE *file = fopen("data.bin", "wb");
fwrite(&r, sizeof(struct Record), 1, file);
fclose(file);
file = fopen("data.bin", "rb");
struct Record r2;
fread(&r2, sizeof(struct Record), 1, file);
printf("ID: %d, Name: %s\n", r2.id, r2.name);
fclose(file);
return 0;
}

ID: 1, Name: Alice

Note: Use "wb" and "rb" for binary mode. Ensure proper struct alignment.

13. Bit Fields

Bit fields in structs allow specifying the exact number of bits for members, saving memory.

Example: Using bit fields.

#include 
struct Flags {
unsigned int flag1 : 1;
unsigned int flag2 : 2;
};
int main() {
struct Flags f = {1, 2};
printf("Flag1: %u, Flag2: %u\n", f.flag1, f.flag2);
return 0;
}

Flag1: 1, Flag2: 2

Note: Bit fields are machine-dependent. Use for compact data storage.

14. Command Line Parsing

Command line parsing processes arguments with flags for flexible program control.

Example: Parsing a flag.

#include 
#include 
int main(int argc, char *argv[]) {
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-v") == 0) {
printf("Verbose mode enabled.\n");
}
}
return 0;
}

Verbose mode enabled.

Note: Use strcmp() for flag comparison. Libraries like getopt() simplify parsing.

15. Variadic Functions

Variadic functions accept a variable number of arguments using stdarg.h.

Example: Summing variable arguments.

#include 
#include 
int sum(int count, ...) {
va_list args;
va_start(args, count);
int total = 0;
for (int i = 0; i < count; i++) total += va_arg(args, int);
va_end(args);
return total;
}
int main() {
printf("Sum: %d\n", sum(3, 1, 2, 3));
return 0;
}

Sum: 6

Note: Use va_start, va_arg, va_end. First argument typically specifies count.

16. Memory Alignment

Memory alignment ensures data is stored at addresses that optimize CPU access.

Example: Checking struct alignment.

#include 
struct Aligned {
char c;
int i;
};
int main() {
struct Aligned a;
printf("Size: %zu, Char offset: %zu, Int offset: %zu\n", 
sizeof(a), (size_t)&a.c - (size_t)&a, (size_t)&a.i - (size_t)&a);
return 0;
}

Size: 8, Char offset: 0, Int offset: 4

Note: Padding ensures alignment. Use sizeof() to check struct size.

17. Linked List Operations

Linked list operations include insertion, deletion, and traversal of nodes.

Example: Inserting a node.

#include 
#include 
struct Node {
int data;
struct Node* next;
};
void insert(struct Node** head, int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = *head;
*head = newNode;
}
int main() {
struct Node* head = NULL;
insert(&head, 1);
printf("Node data: %d\n", head->data);
free(head);
return 0;
}

Node data: 1

Note: Pass pointer to head for modifications. Always free allocated nodes.

18. Circular Linked List

A circular linked list connects the last node to the first, forming a loop.

Example: Creating a circular list.

#include 
#include 
struct Node {
int data;
struct Node* next;
};
int main() {
struct Node* head = (struct Node*)malloc(sizeof(struct Node));
head->data = 1;
head->next = head;
printf("Data: %d\n", head->next->data);
free(head);
return 0;
}

Data: 1

Note: Last node points to head. Use for cyclic data structures.

19. Doubly Linked List

A doubly linked list has nodes with pointers to both next and previous nodes.

Example: Creating a node.

#include 
#include 
struct Node {
int data;
struct Node* prev;
struct Node* next;
};
int main() {
struct Node* node = (struct Node*)malloc(sizeof(struct Node));
node->data = 1;
node->prev = NULL;
node->next = NULL;
printf("Data: %d\n", node->data);
free(node);
return 0;
}

Data: 1

Note: Allows bidirectional traversal. Requires more memory than singly linked lists.

20. Binary Tree Basics

A binary tree is a hierarchical structure with nodes having at most two children.

Example: Creating a binary tree node.

#include 
#include 
struct Node {
int data;
struct Node* left;
struct Node* right;
};
int main() {
struct Node* root = (struct Node*)malloc(sizeof(struct Node));
root->data = 1;
root->left = NULL;
root->right = NULL;
printf("Root data: %d\n", root->data);
free(root);
return 0;
}

Root data: 1

Note: Each node has left and right pointers. Used for hierarchical data.

21. Recursion Optimization

Tail recursion optimizes recursive calls by reusing the stack frame.

Example: Tail-recursive factorial.

#include 
int factorial(int n, int acc) {
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}
int main() {
printf("Factorial of 5: %d\n", factorial(5, 1));
return 0;
}

Factorial of 5: 120

Note: Tail recursion reduces stack usage but requires compiler optimization.

22. String Tokenization

String tokenization splits a string into tokens using delimiters with strtok().

Example: Splitting a string.

#include 
#include 
int main() {
char str[] = "apple,banana,orange";
char *token = strtok(str, ",");
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, ",");
}
return 0;
}

apple
banana
orange

Note: strtok() modifies the original string. Use NULL for subsequent calls.

23. Dynamic 2D Arrays

Dynamic 2D arrays are allocated using pointers for flexible matrix sizes.

Example: Creating a 2x2 matrix.

#include 
#include 
int main() {
int rows = 2, cols = 2;
int **matrix = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) matrix[i] = (int*)malloc(cols * sizeof(int));
matrix[0][0] = 1; matrix[0][1] = 2;
matrix[1][0] = 3; matrix[1][1] = 4;
printf("%d %d\n%d %d\n", matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1]);
for (int i = 0; i < rows; i++) free(matrix[i]);
free(matrix);
return 0;
}

1 2
3 4

Note: Allocate each row separately. Free each row before freeing the main pointer.

24. Function Callbacks

Callbacks are functions passed as arguments to other functions for flexible behavior.

Example: Using a callback.

#include 
void execute(int x, void (*callback)(int)) {
callback(x);
}
void printSquare(int x) {
printf("Square: %d\n", x * x);
}
int main() {
execute(5, printSquare);
return 0;
}

Square: 25

Note: Callbacks enable modular code. Ensure compatible function signatures.

25. Error Handling with errno

The errno variable in errno.h reports errors for system calls and library functions.

Example: Checking file errors.

#include 
#include 
#include 
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
printf("Error: %s\n", strerror(errno));
}
return 0;
}

Error: No such file or directory

Note: Include errno.h and use strerror() for readable error messages.

26. Signal Handling

Signal handling manages asynchronous events like interrupts using signal().

Example: Handling SIGINT.

#include 
#include 
#include 
void handleSigint(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
signal(SIGINT, handleSigint);
printf("Press Ctrl+C...\n");
sleep(5);
return 0;
}

Press Ctrl+C...
Caught signal 2

Note: Use signal.h for handling signals like SIGINT (Ctrl+C). Avoid complex operations in handlers.

27. Preprocessor Macros

Macros define reusable code snippets or constants processed before compilation.

Example: Defining a macro function.

#include 
#define SQUARE(x) ((x) * (x))
int main() {
printf("Square of 5: %d\n", SQUARE(5));
return 0;
}

Square of 5: 25

Note: Parentheses in macros prevent operator precedence issues. Use for simple operations.

28. Inline Functions

Inline functions suggest to the compiler to insert function code directly, reducing call overhead.

Example: Inline function for max.

#include 
inline int max(int a, int b) {
return a > b ? a : b;
}
int main() {
printf("Max: %d\n", max(5, 3));
return 0;
}

Max: 5

Note: Inline is a suggestion, not guaranteed. Use for small, frequently called functions.

29. Multithreading Basics

Multithreading allows concurrent execution using pthreads (POSIX threads).

Example: Creating a thread.

#include 
#include 
void* threadFunc(void* arg) {
printf("Thread running\n");
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, threadFunc, NULL);
pthread_join(thread, NULL);
return 0;
}

Thread running

Note: Link with -pthread. Use pthread_join() to wait for thread completion.

30. Mutex Usage

Mutexes ensure thread-safe access to shared resources in multithreaded programs.

Example: Using a mutex.

#include 
#include 
pthread_mutex_t lock;
int counter = 0;
void* increment(void* arg) {
pthread_mutex_lock(&lock);
counter++;
printf("Counter: %d\n", counter);
pthread_mutex_unlock(&lock);
return NULL;
}
int main() {
pthread_mutex_init(&lock, NULL);
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
return 0;
}

Counter: 1
Counter: 2

Note: Initialize and destroy mutexes. Lock before accessing shared resources.

31. File Seeking

File seeking with fseek() moves the file pointer to specific positions for reading/writing.

Example: Seeking in a file.

#include 
int main() {
FILE *file = fopen("test.txt", "w+");
fprintf(file, "Hello, World!");
fseek(file, 7, SEEK_SET);
char buffer[20];
fgets(buffer, 20, file);
printf("From position 7: %s\n", buffer);
fclose(file);
return 0;
}

From position 7: World!

Note: SEEK_SET starts from the beginning. Use SEEK_CUR or SEEK_END for relative or end-based seeking.

32. Union Advanced Usage

Unions can share memory for type punning or variant data types.

Example: Type punning with a union.

#include 
union Data {
int i;
float f;
};
int main() {
union Data d;
d.i = 0x41C80000; // Float 25.0 in hex
printf("Float value: %.1f\n", d.f);
return 0;
}

Float value: 25.0

Note: Type punning is platform-dependent. Ensure compatible types and sizes.

33. Const Correctness

Const correctness ensures variables or pointers are not modified unintentionally.

Example: Using const with pointers.

#include 
int main() {
const int x = 10;
int y = 20;
const int *ptr = &y;
printf("Value: %d\n", *ptr);
return 0;
}

Value: 20

Note: const int* means the pointed data is constant. Use to prevent accidental modifications.

34. Modular Programming

Modular programming splits code into separate files for better organization.

Example: Separate header and source (simulated in one file).

#include 
// Header-like function declaration
int add(int, int);
// Source-like function definition
int add(int a, int b) { return a + b; }
int main() {
printf("Sum: %d\n", add(2, 3));
return 0;
}

Sum: 5

Note: Use .h files for declarations and .c for definitions. Include guards prevent multiple inclusions.

35. Stack vs Heap

Stack stores local variables; heap stores dynamically allocated memory.

Example: Stack and heap allocation.

#include 
#include 
int main() {
int stackVar = 10; // Stack
int *heapVar = (int*)malloc(sizeof(int)); // Heap
*heapVar = 20;
printf("Stack: %d, Heap: %d\n", stackVar, *heapVar);
free(heapVar);
return 0;
}

Stack: 10, Heap: 20

Note: Stack is automatic, heap requires manual management. Free heap memory to avoid leaks.

36. Memory Leaks Detection

Memory leaks occur when allocated memory is not freed, detectable with tools or checks.

Example: Avoiding a memory leak.

#include 
#include 
int main() {
int *ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
printf("Allocation failed\n");
return 1;
}
*ptr = 10;
printf("Value: %d\n", *ptr);
free(ptr);
return 0;
}

Value: 10

Note: Always free allocated memory. Use tools like Valgrind for leak detection.

37. Hash Table Basics

A hash table maps keys to values using a hash function for fast lookup.

Example: Simple hash table with array.

#include 
#define SIZE 10
int hashTable[SIZE] = {0};
int hash(int key) { return key % SIZE; }
int main() {
int key = 15, value = 100;
hashTable[hash(key)] = value;
printf("Value at key %d: %d\n", key, hashTable[hash(key)]);
return 0;
}

Value at key 15: 100

Note: Handle collisions in real implementations. Simple hash function used here.

38. String Formatting

Advanced string formatting with sprintf() or snprintf() builds custom strings.

Example: Using snprintf().

#include 
int main() {
char buffer[50];
int age = 25;
snprintf(buffer, sizeof(buffer), "Age: %d years", age);
printf("%s\n", buffer);
return 0;
}

Age: 25 years

Note: snprintf() is safer than sprintf() as it limits buffer size.

39. Command Line Menu

A command line menu provides an interactive interface using loops and input.

Example: Simple menu system.

#include 
int main() {
int choice;
do {
printf("1. Option 1\n2. Option 2\n3. Exit\nChoice: ");
scanf("%d", &choice);
switch (choice) {
case 1: printf("Selected Option 1\n"); break;
case 2: printf("Selected Option 2\n"); break;
case 3: printf("Exiting...\n"); break;
default: printf("Invalid choice\n");
}
} while (choice != 3);
return 0;
}

1. Option 1
2. Option 2
3. Exit
Choice: 1
Selected Option 1
Choice: 3
Exiting...

Note: Use loops for menu persistence. Validate user input for robustness.

40. Student Database Project

A student database combines structs, file I/O, and dynamic memory for data management.

Example: Adding and reading student records.

#include 
#include 
#include 
struct Student {
int id;
char name[50];
};
void addStudent(const char* filename, struct Student* s) {
FILE *file = fopen(filename, "ab");
fwrite(s, sizeof(struct Student), 1, file);
fclose(file);
}
void readStudents(const char* filename) {
FILE *file = fopen(filename, "rb");
struct Student s;
while (fread(&s, sizeof(struct Student), 1, file)) {
printf("ID: %d, Name: %s\n", s.id, s.name);
}
fclose(file);
}
int main() {
struct Student s1 = {1, "Alice"};
addStudent("students.bin", &s1);
readStudents("students.bin");
return 0;
}

ID: 1, Name: Alice

Note: Uses binary I/O for persistence. Append mode ("ab") adds records without overwriting.

Macro Nepal Helper