The Co-operative University of Kenya
Department of Computer Science and
Information Technology
Lecture 2: Advanced Programming &
Debugging in C
BCSE 1203: Structured Programming Lab
Dr. Shem Mbandu Angolo, PhD.
February - May Semester
1 Introduction to Advanced Programming &
Debugging in C
C is a powerful, low-level programming language that allows developers fine
control over system resources. However, with great power comes complex-
ity—errors, undefined behavior, and debugging challenges are common.
Key Topics Covered:
• Error handling and strategies for dealing with runtime errors.
• Debugging techniques and tools to analyze and fix bugs.
• File handling in C to read, write, and manage files efficiently.
2 Error Handling in C
C does not provide built-in error handling mechanisms like exceptions in Java
or Python. Instead, error handling relies on:
• Return Codes
• Global errno variable
1
• Assertions
• Signal Handling
2.1 Using Return Codes
Why Use Return Codes for Error Handling?
• Provides a simple and efficient way to detect and handle errors.
• Enables debugging by analyzing function return values.
• Allows the program to recover from errors gracefully.
A common convention in C is:
• Return 0 for success.
• Return a nonzero value (1, -1, or custom error codes) for failure.
Example: Handling File Opening Errors
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " data . txt " , " r " ) ;
5 if ( file == NULL ) {
6 printf ( " Error : Unable to open file \ n " ) ;
7 return 1; // Exit with an error code
8 }
9 printf ( " File opened successfully \ n " ) ;
10 fclose ( file ) ;
11 return 0; // Indicate success
12 }
Listing 1: Handling File Open Errors
Use Case: Checking if a file exists before attempting to read from it.
2.1.1 Using Custom Return Codes
For better clarity, use custom return codes instead of generic numbers.
2
Example: Defining Custom Error Codes
1 # include < stdio .h >
2
3 # define SUCCESS 0
4 # define FILE_NOT_FOUND 1
5
6 int main () {
7 FILE * file = fopen ( " non_existing . txt " , " r " ) ;
8 if ( file == NULL ) {
9 printf ( " Error : File not found \ n " ) ;
10 return FILE_NOT_FOUND ; // Use custom return code
11 }
12 fclose ( file ) ;
13 return SUCCESS ; // Indicate successful execution
14 }
Listing 2: Using Custom Return Codes
Use Case: Improving code readability by replacing raw numbers with
meaningful names.
Example: Safe Division Function
1 # include < stdio .h >
2
3 int safe_divide ( int a , int b , int * result ) {
4 if ( b == 0) {
5 return 1; // Error : Division by zero
6 }
7 * result = a / b ;
8 return 0; // Success
9 }
10
11 int main () {
12 int result ;
13 if ( safe_divide (10 , 0 , & result ) != 0) {
14 printf ( " Error : Cannot divide by zero \ n " ) ;
15 return 1;
16 }
17 printf ( " Result : % d \ n " , result ) ;
18 return 0;
19 }
Listing 3: Checking for Division by Zero
Use Case: Ensuring that division operations do not cause runtime errors.
3
Example: Checking for Memory Allocation Failures
1 # include < stdio .h >
2 # include < stdlib .h >
3
4 int main () {
5 int * arr = ( int *) malloc (1000 * sizeof ( int ) ) ;
6 if ( arr == NULL ) {
7 printf ( " Error : Memory allocation failed \ n " ) ;
8 return 1; // Exit program if allocation fails
9 }
10 printf ( " Memory allocated successfully \ n " ) ;
11 free ( arr ) ; // Free allocated memory
12 return 0;
13 }
Listing 4: Handling Memory Allocation Errors
Use Case: Preventing crashes due to insufficient memory.
Example: Checking for File Read Errors
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " data . txt " , " r " ) ;
5 char buffer [100];
6
7 if ( file == NULL ) {
8 printf ( " Error : Unable to open file \ n " ) ;
9 return 1;
10 }
11
12 if ( fgets ( buffer , sizeof ( buffer ) , file ) == NULL ) {
13 printf ( " Error : Failed to read from file \ n " ) ;
14 fclose ( file ) ;
15 return 1;
16 }
17
18 printf ( " Read from file : % s \ n " , buffer ) ;
19 fclose ( file ) ;
20 return 0;
21 }
Listing 5: Detecting File Read Errors
Use Case: Handling unexpected file read failures.
4
2.2 Using errno for System Errors
Why Use errno for Error Handling?
• Provides system-generated error codes for debugging.
• Helps detect issues in file handling, memory allocation, and system
calls.
• Standardized across different platforms.
Understanding errno
errno is a global integer variable declared in <errno.h>. It stores the last
error code set by a system call or standard library function.
2.2.1 Using errno and perror() for Error Handling
The perror() function prints a human-readable error message for the last
system error.
Example: Handling File Opening Errors with errno
1 # include < stdio .h >
2 # include < errno .h >
3 # include < string .h >
4
5 int main () {
6 FILE * file = fopen ( " non_existing . txt " , " r " ) ;
7 if ( file == NULL ) {
8 perror ( " Error opening file " ) ; // Print system -
generated error message
9 return errno ; // Return the system error code
10 }
11 fclose ( file ) ;
12 return 0;
13 }
Listing 6: Using errno for File Open Errors
Use Case: Detecting system-level errors like file permission issues or
missing files.
2.2.2 Using strerror() for Custom Error Messages
strerror(errno) converts an error code into a readable message.
5
Example: Displaying Custom Error Messages
1 # include < stdio .h >
2 # include < errno .h >
3 # include < string .h >
4
5 int main () {
6 FILE * file = fopen ( " non_existing . txt " , " r " ) ;
7 if ( file == NULL ) {
8 printf ( " Error : % s \ n " , strerror ( errno ) ) ; // Print
detailed error
9 return errno ; // Return the system error code
10 }
11 fclose ( file ) ;
12 return 0;
13 }
Listing 7: Using strerror() for Error Messages
Use Case: Providing detailed error messages for logging and debugging.
2.2.3 Handling Memory Allocation Errors with errno
malloc() does not set errno when it fails, but brk() and sbrk() do.
Example: Checking for Memory Allocation Failures
1 # include < stdio .h >
2 # include < stdlib .h >
3 # include < errno .h >
4
5 int main () {
6 int * arr = ( int *) malloc (1000000000 * sizeof ( int ) ) ; //
Large allocation
7 if ( arr == NULL ) {
8 perror ( " Memory allocation failed " ) ;
9 return errno ; // Return system error code
10 }
11 printf ( " Memory allocated successfully \ n " ) ;
12 free ( arr ) ; // Free allocated memory
13 return 0;
14 }
Listing 8: Detecting Memory Allocation Errors
Use Case: Preventing crashes due to insufficient memory.
6
2.2.4 Common errno Error Codes and Their Meanings
The following are common error codes defined in <errno.h>:
• EACCES (13) – Permission denied.
• ENOENT (2) – No such file or directory.
• ENOMEM (12) – Out of memory.
• EIO (5) – Input/output error.
2.2.5 Best Practices for Using errno
• Always check system call return values before using errno.
• Use perror() for standard error messages.
• Use strerror(errno) for custom messages.
• Do not assume errno is always set—some functions do not modify it
on failure.
• Reset errno before calling system functions to ensure correct error
detection.
2.3 Using Assertions for Debugging
An assertion is a programming construct used to verify that certain condi-
tions hold true during program execution. In C, assertions are implemented
using the assert() macro from the <assert.h> library.
Why Use Assertions?
• Helps catch programming errors early.
• Ensures program correctness by validating assumptions.
• Facilitates debugging by halting execution when an assumption fails.
• Reduces reliance on manual debugging techniques.
7
3 Debugging Techniques in C
Debugging is the process of identifying, analyzing, and fixing bugs in a pro-
gram. In C, debugging is crucial since the language does not provide built-in
safeguards like exception handling in higher-level languages.
Why Debugging is Important?
• Helps detect logical errors that cause incorrect program behavior.
• Prevents undefined behavior and segmentation faults.
• Ensures memory safety by identifying memory leaks and invalid ac-
cesses.
• Improves code maintainability and efficiency.
3.1 Using printf() for Debugging
The simplest debugging technique is inserting printf() statements to trace
program execution.
Why Use printf() for Debugging?
• Helps track variable values at different points in a program.
• Allows tracing program flow to identify logic errors.
• Works on any system without requiring specialized debugging tools.
• Useful for detecting segmentation faults and runtime errors.
3.1.1 Setting Up printf() Debugging in C
Basic Syntax of printf()
printf() is a standard function in stdio.h used to print formatted output.
1 # include < stdio .h >
2
3 int main () {
4 int x = 10;
5 printf ( " The value of x is : % d \ n " , x ) ;
6 return 0;
7 }
Listing 9: Basic printf() Example
8
3.1.2 Using printf() for Debugging Variables
You can print variables at different points to check their values.
1 # include < stdio .h >
2
3 int main () {
4 int x = 5 , y = 10;
5 printf ( " Before modification : x = %d , y = % d \ n " , x , y ) ;
6
7 x += 10;
8 y *= 2;
9
10 printf ( " After modification : x = %d , y = % d \ n " , x , y ) ;
11 return 0;
12 }
Listing 10: Tracking Variables with printf()
Use Case: Checking if variables hold expected values at runtime.
3.1.3 Debugging Control Flow
printf() statements can help verify if conditions and loops execute as ex-
pected.
Example: Checking Loop Execution
1 # include < stdio .h >
2
3 int main () {
4 for ( int i = 0; i < 5; i ++) {
5 printf ( " Loop iteration : % d \ n " , i ) ;
6 }
7 return 0;
8 }
Listing 11: Debugging a Loop with printf()
Use Case: Ensuring loops execute the correct number of times.
Example: Debugging Conditional Statements
1 # include < stdio .h >
2
3 int main () {
4 int num = 10;
5
6 if ( num > 0) {
9
7 printf ( " The number is positive .\ n " ) ;
8 } else if ( num < 0) {
9 printf ( " The number is negative .\ n " ) ;
10 } else {
11 printf ( " The number is zero .\ n " ) ;
12 }
13
14 return 0;
15 }
Listing 12: Checking If-Else Conditions
Use Case: Ensuring correct condition evaluation.
3.1.4 Debugging Segmentation Faults with printf()
A segmentation fault occurs when a program accesses invalid memory. printf()
can help locate such errors.
Example: Debugging a Segmentation Fault
1 # include < stdio .h >
2
3 int main () {
4 int * ptr = NULL ; // Uninitialized pointer
5 printf ( " Pointer address : % p \ n " , ptr ) ;
6 * ptr = 10; // Causes segmentation fault
7
8 return 0;
9 }
Listing 13: Using printf() to Detect Invalid Pointer Access
Use Case: Checking if pointers are properly initialized.
3.1.5 Debugging Functions with printf()
You can use printf() to track function calls and return values.
Example: Debugging Function Execution
1 # include < stdio .h >
2
3 int add ( int a , int b ) {
4 printf ( " Entering add () function with a = %d , b = % d \ n " , a
, b);
5 int result = a + b ;
10
6 printf ( " Exiting add () function with result = % d \ n " ,
result ) ;
7 return result ;
8 }
9
10 int main () {
11 int sum = add (5 , 10) ;
12 printf ( " Sum = % d \ n " , sum ) ;
13 return 0;
14 }
Listing 14: Tracking Function Calls with printf()
Use Case: Ensuring function calls work correctly.
3.1.6 Debugging Arrays and Memory Issues
Example: Debugging Array Out-of-Bounds Errors
1 # include < stdio .h >
2
3 int main () {
4 int arr [3] = {1 , 2 , 3};
5
6 for ( int i = 0; i < 5; i ++) { // Intentional error
7 printf ( " Index % d : % d \ n " , i , arr [ i ]) ;
8 }
9
10 return 0;
11 }
Listing 15: Checking Array Access with printf()
Use Case: Detecting access beyond valid array indices.
3.1.7 Best Practices for Using printf() Debugging
• Place printf() statements strategically to minimize clutter.
• Use descriptive messages to differentiate debug outputs.
• Print memory addresses when debugging pointer-related errors.
• Remove debugging statements once the issue is resolved.
• If debugging output is excessive, consider logging output to a file.
11
3.2 Using GDB (GNU Debugger)
GDB (GNU Debugger) is a powerful debugging tool that allows develop-
ers to analyze and troubleshoot C programs by inspecting variables, setting
breakpoints, and stepping through code execution.
Why Use GDB?
• Identifies segmentation faults and runtime errors.
• Allows stepping through code execution line by line.
• Enables real-time variable inspection.
• Provides backtraces for analyzing function calls.
3.2.1 Setting Up GDB for C Programming
1. Installing GDB
• Linux (Ubuntu/Debian): Install with:
sudo apt i n s t a l l gdb
• MacOS (via Homebrew): Install with:
brew i n s t a l l gdb
• Windows (via MinGW-w64): Install MinGW-w64 and ensure [Link]
is added to the system PATH.
2. Compiling C Programs with Debugging Information
GDB requires debug symbols to analyze a program correctly. Compile using
the -g flag:
g c c −g program . c −o program
3.2.2 Basic Commands in GDB
1. Starting a Debugging Session
To start GDB, run:
gdb . / program
12
2. Running the Program in GDB
To execute the program inside GDB, use:
run
3. Setting Breakpoints
Breakpoints allow you to pause execution at specific lines:
break main // Se t a b r e a k p o i n t a t th e s t a r t o f main ( )
break 10 // S et a b r e a k p o i n t a t l i n e 10
break myfunc // S et a b r e a k p o i n t a t t h e f u n c t i o n myfunc ( )
4. Stepping Through Code
next // Execute th e next l i n e without s t e p p i n g i n t o f u n c t i o n s
step // Step i n t o f u n c t i o n c a l l s
c o n t i n u e // Continue e x e c u t i o n u n t i l t he next b r e a k p o i n t
5. Inspecting Variables
p r i n t var name // P r i n t t h e v a l u e o f a v a r i a b l e
print i // P r i n t t h e v a l u e o f i
watch i // Monitor changes t o v a r i a b l e i
6. Displaying a Backtrace
To see the function call stack:
backtrace
7. Quitting GDB
Exit the debugger with:
quit
13
3.2.3 Debugging Examples
Example 1. Debugging a Segmentation Fault
1 # include < stdio .h >
2
3 int main () {
4 int * ptr = NULL ; // Uninitialized pointer
5 * ptr = 10; // Causes segmentation fault
6 return 0;
7 }
Listing 16: Example: Detecting Segmentation Faults
Steps to Debug:
1. Compile the program with debugging symbols:
g cc −g s e g f a u l t . c −o s e g f a u l t
2. Run GDB:
gdb . / s e g f a u l t
3. Start execution:
run
4. When the program crashes, check the backtrace:
backtrace
Example 2. Debugging an Infinite Loop
1 # include < stdio .h >
2
3 int main () {
4 int i = 0;
5 while (1) { // Infinite loop
6 printf ( " Iteration % d \ n " , i ) ;
7 i ++;
8 }
9 return 0;
10 }
Listing 17: Example: Debugging an Infinite Loop
Steps to Debug:
14
1. Compile the program:
g cc −g l o o p . c −o l o o p
2. Run in GDB:
gdb . / l o o p
3. Pause execution manually:
Ctrl + C
4. Find the issue using:
backtrace
Example 4. Debugging Function Calls
1 # include < stdio .h >
2
3 void add ( int a , int b ) {
4 printf ( " Sum : % d \ n " , a + b ) ;
5 }
6
7 int main () {
8 add (5 , 10) ;
9 add (3 , -2) ;
10 return 0;
11 }
Listing 18: Example: Debugging Function Calls
Steps to Debug:
1. Set a breakpoint in the function:
break add
2. Run the program:
run
3. Step through function execution:
step
next
15
3.2.4 Best Practices for Debugging with GDB
• Always compile with -g to include debugging symbols.
• Use breakpoints to stop execution at specific points.
• Use print and watch to inspect variables in real-time.
• Check the backtrace when debugging segmentation faults.
• Quit GDB properly to avoid memory leaks in debug sessions.
3.3 Using Valgrind for Memory Debugging
Valgrind is a powerful tool for detecting memory-related errors in C pro-
grams. It helps find memory leaks, uninitialized memory reads, and invalid
memory accesses.
Why Use Valgrind?
• Detects memory leaks caused by missing free() calls.
• Identifies uninitialized memory usage.
• Catches invalid memory accesses (e.g., accessing freed memory).
• Helps optimize memory usage and improve program reliability.
3.3.1 Setting Up Valgrind
1. Installing Valgrind
• Linux (Ubuntu/Debian): Install using:
sudo apt i n s t a l l v a l g r i n d
• MacOS: Install via Homebrew:
brew i n s t a l l v a l g r i n d
• Windows: Use WSL (Windows Subsystem for Linux) to run Valgrind.
2. Compiling C Programs for Valgrind
To enable debugging symbols, compile the program with:
g c c −g program . c −o program
16
3.3.2 Using Valgrind for Memory Debugging
1. Running a Program with Valgrind
To analyze memory usage, run:
v a l g r i n d −−l e a k −check= f u l l . / program
2. Understanding Valgrind Output
A typical Valgrind output looks like:
==12345== ERROR SUMMARY: 1 e r r o r s from 1 c o n t e x t s
==12345== LEAK SUMMARY:
==12345== d e f i n i t e l y l o s t : 4 bytes in 1 blocks
Key Information:
• definitely lost: Memory that was allocated but never freed.
• indirectly lost: Memory that was referenced by a leaked pointer.
• possibly lost: Memory that may be unreachable.
• still reachable: Memory that remains allocated but accessible.
3.3.3 Debugging Memory Leaks with Valgrind
Example 1. Detecting Memory Leaks
1 # include < stdlib .h >
2
3 int main () {
4 int * ptr = ( int *) malloc ( sizeof ( int ) ) ; // Allocated but
not freed
5 * ptr = 10;
6 return 0;
7 }
Listing 19: Example: Memory Leak Detection
Steps to Debug:
1. Compile the program:
g cc −g memory leak . c −o memory leak
2. Run Valgrind:
v a l g r i n d −−l e a k −check= f u l l . / memory leak
17
Fixing Memory Leaks
1 # include < stdlib .h >
2
3 int main () {
4 int * ptr = ( int *) malloc ( sizeof ( int ) ) ;
5 * ptr = 10;
6 free ( ptr ) ; // Memory is freed properly
7 return 0;
8 }
Listing 20: Fixing Memory Leaks with Free()
Example 2. Using Uninitialized Memory (Detecting Uninitialized
Memory Access)
1 # include < stdio .h >
2 # include < stdlib .h >
3
4 int main () {
5 int * ptr = ( int *) malloc ( sizeof ( int ) ) ; // Uninitialized
memory
6 printf ( " Value : % d \ n " , * ptr ) ; // Reading uninitialized
memory
7 free ( ptr ) ;
8 return 0;
9 }
Listing 21: Uninitialized Memory Access
Steps to Debug:
1. Compile the program:
g cc −g u n i n i t . c −o u n i n i t
2. Run Valgrind:
v a l g r i n d −−t r a c k −o r i g i n s=y e s . / u n i n i t
Example 3. Accessing Freed Memory (Detecting Invalid Memory
Access)
1 # include < stdlib .h >
2
3 int main () {
4 int * ptr = ( int *) malloc ( sizeof ( int ) ) ;
18
5 free ( ptr ) ;
6 * ptr = 20; // Accessing freed memory ( use - after - free
error )
7 return 0;
8 }
Listing 22: Accessing Freed Memory
Example 4: Array Out-of-Bounds Access
1 # include < stdio .h >
2
3 int main () {
4 int arr [5] = {1 , 2 , 3 , 4 , 5};
5 printf ( " Invalid access : % d \ n " , arr [10]) ; // Accessing out
of bounds
6 return 0;
7 }
Listing 23: Array Out-of-Bounds Access
Steps to Debug:
1. Compile the program:
g cc −g i n v a l i d a c c e s s . c −o i n v a l i d a c c e s s
2. Run Valgrind:
v a l g r i n d −−t r a c k −o r i g i n s=y e s . / i n v a l i d a c c e s s
3.3.4 Best Practices for Memory Debugging
• Always compile with -g to enable debugging information.
• Use valgrind --leak-check=full to detect memory leaks.
• Free allocated memory using free() before exiting a program.
• Initialize pointers to NULL before use.
• Avoid accessing freed or out-of-bounds memory.
19
3.4 Using Static Code Analyzers
Static code analysis is the process of analyzing C source code without ex-
ecuting it. This method helps detect errors, security vulnerabilities, and
non-compliance with coding standards before runtime.
Why Use Static Code Analyzers?
• Identifies bugs and potential errors before execution.
• Detects memory leaks, undefined behavior, and code smells.
• Improves code readability and maintainability.
• Ensures compliance with coding standards (e.g., MISRA C, CERT C).
3.4.1 Common Static Code Analysis Tools for C
GCC Compiler Warnings (-Wall, -Wextra)
GCC provides built-in static analysis through warning flags.
Example: Enabling Compiler Warnings
g c c −Wall −Wextra −o program program . c
Example: Detecting Unused Variables
1 # include < stdio .h >
2
3 int main () {
4 int x = 10; // Compiler warns : variable x is set but not
used
5 return 0;
6 }
Listing 24: Unused Variable Warning
3.5 Cppcheck
Cppcheck is a static code analysis tool that detects memory leaks and unde-
fined behavior.
20
Installing and Running Cppcheck
• Install Cppcheck:
sudo apt i n s t a l l cppcheck
• Run analysis:
cppcheck −−e n a b l e=a l l program . c
Example: Detecting Memory Leaks
1 # include < stdlib .h >
2
3 int main () {
4 int * ptr = malloc ( sizeof ( int ) ) ;
5 return 0; // Memory not freed
6 }
Listing 25: Detecting Memory Leaks with Cppcheck
Cppcheck output:
Memory l e a k : p t r
3.6 Clang Static Analyzer
Clang provides advanced static analysis with precise error reports.
Installing and Running Clang Static Analyzer
scan−b u i l d gc c program . c −o program
Example: Detecting Unused Memory
1 # include < stdlib .h >
2
3 int main () {
4 int * ptr = malloc (10 * sizeof ( int ) ) ;
5 free ( ptr ) ;
6 free ( ptr ) ; // Double free error
7 return 0;
8 }
Listing 26: Using Clang Static Analyzer
21
Clang output:
warning : Attempt t o f r e e memory t w i c e
3.7 Splint (Secure Programming Lint)
Splint is a tool for detecting security vulnerabilities in C.
Installing and Running Splint
sudo apt i n s t a l l s p l i n t
s p l i n t program . c
Example: Detecting Uninitialized Variables
1 # include < stdio .h >
2
3 int main () {
4 int x ;
5 printf ( " % d \ n " , x ) ; // Uninitialized variable
6 return 0;
7 }
Listing 27: Detecting Uninitialized Variables with Splint
Splint output:
V a r i a b l e x used b e f o r e d e f i n i t i o n
3.8 Best Practices for Static Code Analysis
• Enable compiler warnings using -Wall -Wextra.
• Use multiple static analyzers (Cppcheck, Clang, Splint) for better cov-
erage.
• Regularly check code for memory leaks and undefined behavior.
• Follow secure coding standards (e.g., MISRA C, CERT C).
• Automate static analysis in CI/CD pipelines.
22
4 File Handling in C
File handling in C enables programs to read from and write to external files,
allowing persistent data storage.
Why Use File Handling?
• Stores data permanently beyond program execution.
• Allows structured data management (logs, configurations, records).
• Enables file-based inter-process communication.
4.1 Types of File Operations in C
• Opening a File: Using fopen().
• Reading from a File: Using fgetc(), fgets(), fread().
• Writing to a File: Using fputc(), fputs(), fprintf(), fwrite().
• Closing a File: Using fclose().
4.2 Opening and Closing Files
4.2.1 Opening a File
The fopen() function opens a file and returns a pointer to the file.
4.2.2 Syntax
1 FILE * fopen ( const char * filename , const char * mode ) ;
4.2.3 File Opening Modes
• "r" – Read mode.
• "w" – Write mode (overwrites existing content).
• "a" – Append mode.
• "r+" – Read and write.
• "w+" – Write and read (clears existing content).
• "a+" – Read and append.
23
4.2.4 Example: Opening and Closing a File
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " example . txt " , " r " ) ;
5 if ( file == NULL ) {
6 printf ( " Error : Unable to open file \ n " ) ;
7 return 1;
8 }
9 printf ( " File opened successfully \ n " ) ;
10 fclose ( file ) ;
11 return 0;
12 }
Listing 28: Opening and Closing a File
Use Case: Ensuring a file exists before reading from it.
4.3 Reading from a File
4.3.1 Using fgetc() to Read a Single Character
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " example . txt " , " r " ) ;
5 char ch ;
6 if ( file == NULL ) {
7 printf ( " Error : File not found \ n " ) ;
8 return 1;
9 }
10 while (( ch = fgetc ( file ) ) != EOF ) {
11 putchar ( ch ) ;
12 }
13 fclose ( file ) ;
14 return 0;
15 }
Listing 29: Reading a Character from a File
Use Case: Reading text files character by character.
4.3.2 Using fgets() to Read a Line
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " example . txt " , " r " ) ;
24
5 char buffer [100];
6 if ( file == NULL ) {
7 printf ( " Error : File not found \ n " ) ;
8 return 1;
9 }
10 while ( fgets ( buffer , sizeof ( buffer ) , file ) ) {
11 printf ( " % s " , buffer ) ;
12 }
13 fclose ( file ) ;
14 return 0;
15 }
Listing 30: Reading a Line from a File
Use Case: Reading configuration or log files line by line.
4.4 Writing to a File
4.4.1 Using fprintf() to Write Formatted Data
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " output . txt " , " w " ) ;
5 if ( file == NULL ) {
6 printf ( " Error : Unable to open file \ n " ) ;
7 return 1;
8 }
9 fprintf ( file , " Name : %s , Age : % d \ n " , " Alice " , 25) ;
10 fclose ( file ) ;
11 return 0;
12 }
Listing 31: Writing Formatted Data to a File
Use Case: Storing structured data in text format.
4.4.2 Using fputc() and fputs() for Writing
1 # include < stdio .h >
2
3 int main () {
4 FILE * file = fopen ( " output . txt " , " w " ) ;
5 if ( file == NULL ) {
6 printf ( " Error : Unable to open file \ n " ) ;
7 return 1;
8 }
9 fputc ( ’A ’ , file ) ;
10 fputs ( " \ nThis is a string .\ n " , file ) ;
25
11 fclose ( file ) ;
12 return 0;
13 }
Listing 32: Writing Characters and Strings to a File
Use Case: Writing log messages and string data.
4.5 Using Binary Files in C
Binary files store data in raw format, making them efficient for numerical or
structured data.
Example: Writing and Reading a Structure to/from a Binary File
1 # include < stdio .h >
2
3 struct Student {
4 int id ;
5 char name [50];
6 };
7
8 int main () {
9 struct Student s1 = {1 , " Alice " };
10 FILE * file = fopen ( " student . dat " , " wb " ) ;
11
12 fwrite (& s1 , sizeof ( struct Student ) , 1 , file ) ;
13 fclose ( file ) ;
14
15 struct Student s2 ;
16 file = fopen ( " student . dat " , " rb " ) ;
17 fread (& s2 , sizeof ( struct Student ) , 1 , file ) ;
18
19 printf ( " ID : %d , Name : % s \ n " , s2 . id , s2 . name ) ;
20 fclose ( file ) ;
21 return 0;
22 }
Listing 33: Writing and Reading Binary Data
Use Case: Storing structured data like records, images, and sensor data.
4.5.1 Best Practices for File Handling in C
• Always check if a file opened successfully before reading/writing.
• Use binary files when working with structured or numerical data.
26
• Close files using fclose() to prevent memory leaks.
• Use appropriate file modes ("r", "w", "a", etc.) based on requirements.
27