0% found this document useful (0 votes)
3 views27 pages

Structured Programming Lab Lecture 2

This document is a lecture outline for an Advanced Programming and Debugging course in C at the Co-operative University of Kenya. It covers key topics such as error handling, debugging techniques, and file handling in C, emphasizing the importance of using return codes, errno, assertions, and debugging tools like printf() and GDB. The document includes examples and best practices for effectively managing errors and debugging C programs.

Uploaded by

bronevon06
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views27 pages

Structured Programming Lab Lecture 2

This document is a lecture outline for an Advanced Programming and Debugging course in C at the Co-operative University of Kenya. It covers key topics such as error handling, debugging techniques, and file handling in C, emphasizing the importance of using return codes, errno, assertions, and debugging tools like printf() and GDB. The document includes examples and best practices for effectively managing errors and debugging C programs.

Uploaded by

bronevon06
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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

You might also like