1
📄 Write Text to a File
fopen "w", fprintf, fputs, fclose — the complete write cycle
Basics
Every file operation in C follows the same three-step cycle: open → use → close.
fopen(name, mode) returns a FILE* pointer — a handle to the file. Mode "w" creates the file if missing, or erases it completely if it already exists. Always check for NULL — fopen returns NULL on failure (wrong path, no permission). fclose is mandatory: it flushes the internal buffer to disk and releases the handle.
| Mode | Meaning | File exists | File missing |
|---|---|---|---|
| "r" | Read only | Opens it | Returns NULL |
| "w" | Write only | Erases it | Creates it |
| "a" | Append only | Adds to end | Creates it |
| "r+" | Read + Write | Opens it | Returns NULL |
| "w+" | Read + Write | Erases it | Creates it |
| "a+" | Read + Append | Adds to end | Creates it |
#include <stdio.h> #include <stdlib.h> int main() { /* Step 1 — open file for writing */ FILE *fp = fopen("notes.txt", "w"); if (fp == NULL) { /* ALWAYS check for NULL */ printf("Error: could not open file.\n"); return 1; } /* Step 2 — write to the file */ fprintf(fp, "Name : Ananta\n"); /* like printf, but to file */ fprintf(fp, "City : Haridwar\n"); fprintf(fp, "Score : %d\n", 95); /* supports format specifiers */ fprintf(fp, "Grade : %c\n", 'A'); fputs("Status: Active\n", fp); /* fputs also works */ /* Step 3 — close (flushes buffer to disk) */ fclose(fp); printf("File written successfully.\n"); return 0; }
File written successfully.
notes.txt — written to disk
Name : Ananta
City : Haridwar
Score : 95
Grade : A
Status: Active
fclose(fp) is not optional. Without it, data sitting in the internal buffer may never reach the disk. Programs that crash before fclose often produce empty or truncated files. Always close every file you open.example 2
2
📖 Read a File — Character by Character
fgetc loop until EOF — the simplest read pattern in C
Read Chars
Open with
"r" and read one character at a time using fgetc. The loop runs until fgetc returns the special constant EOF (End Of File, typically -1). The critical detail: ch must be declared as int, not char — because EOF is -1 and on systems where char is unsigned, the comparison ch != EOF would never be true, creating an infinite loop.
#include <stdio.h> int main() { FILE *fp = fopen("notes.txt", "r"); if (fp == NULL) { printf("Error: file not found.\n"); return 1; } printf("--- Contents of notes.txt ---\n"); int ch; /* int, NOT char — EOF is -1 */ int charCount = 0; while ((ch = fgetc(fp)) != EOF) { putchar(ch); /* print character to stdout */ charCount++; } printf("\n--- Total characters: %d ---\n", charCount); fclose(fp); return 0; }
--- Contents of notes.txt --- Name : Ananta City : Haridwar Score : 95 Grade : A Status: Active --- Total characters: 67 ---
Always declare
ch as int, never char. fgetc returns an int so it can return both any valid byte (0–255) and EOF (−1). If you use char and the platform treats char as unsigned, EOF gets truncated to 255 and the loop never ends.example 3
3
📃 Read a File — Line by Line
fgets with a buffer — safe, line-aware reading with line numbers
Read Lines
fgets(buffer, size, fp) reads one line at a time into a character array. It stops at a newline, at size-1 characters, or at EOF — whichever comes first. It is always safer than fgetc for line-based text because it limits input length and prevents buffer overflow. The newline \n is kept inside the buffer — strip it with strcspn if needed. fscanf is shown as a second approach for reading structured tokens.
#include <stdio.h> #include <string.h> int main() { FILE *fp = fopen("notes.txt", "r"); if (!fp) { printf("Error\n"); return 1; } char line[100]; /* buffer — holds one line at a time */ int lineNo = 0; printf("%-4s %s\n", "Line", "Content"); printf("------------------------------\n"); while (fgets(line, sizeof(line), fp) != NULL) { lineNo++; /* Strip trailing newline for clean display */ line[strcspn(line, "\n")] = '\0'; printf("%4d | %s\n", lineNo, line); } printf("------------------------------\n"); printf("Total lines: %d\n", lineNo); fclose(fp); /* ── fscanf approach: read word by word ── */ fp = fopen("notes.txt", "r"); char word[50]; int words = 0; while (fscanf(fp, "%s", word) == 1) words++; printf("Total words : %d\n", words); fclose(fp); return 0; }
Line Content ------------------------------ 1 | Name : Ananta 2 | City : Haridwar 3 | Score : 95 4 | Grade : A 5 | Status: Active ------------------------------ Total lines: 5 Total words : 10
fgets keeps the \n at the end of the buffer. That's why we use line[strcspn(line,"\n")] = '\0' to strip it. Without stripping, printing with printf("%s\n") would add a blank line after every line of output.example 4
4
➕ Append to a File Without Erasing It
fopen "a" — pointer starts at end, original data untouched
Append Mode
Mode
"a" positions the file pointer at the very end of the file on every write. Original content is never touched. If the file doesn't exist, it is created — same as "w". Append mode is ideal for log files, journals, and any situation where you want to add new entries without risking the loss of existing data. The program then reads the whole file back to confirm the append worked.
#include <stdio.h> #include <string.h> void printFile(const char *filename) { FILE *fp = fopen(filename, "r"); if (!fp) return; char line[100]; while (fgets(line, sizeof(line), fp)) fputs(line, stdout); fclose(fp); } int main() { printf("--- notes.txt BEFORE append ---\n"); printFile("notes.txt"); /* Open in APPEND mode — does NOT erase */ FILE *fp = fopen("notes.txt", "a"); if (!fp) { printf("Error\n"); return 1; } fprintf(fp, "Lang : C\n"); fprintf(fp, "Year : 2025\n"); fprintf(fp, "College: Ananta Institute\n"); fclose(fp); printf("\n3 lines appended successfully.\n\n"); printf("--- notes.txt AFTER append ---\n"); printFile("notes.txt"); return 0; }
--- notes.txt BEFORE append --- Name : Ananta City : Haridwar Score : 95 Grade : A Status: Active 3 lines appended successfully. --- notes.txt AFTER append --- Name : Ananta City : Haridwar Score : 95 Grade : A Status: Active Lang : C Year : 2025 College: Ananta Institute
Mode cheat-sheet in one line:
"w" = overwrite · "r" = read · "a" = append at end · add + to any for both read and write · add b (e.g. "rb", "wb") for binary mode.example 5
5
📋 Copy One File to Another
Open source "r" and destination "w" — pipe bytes with fgetc/fputc
File Copy
File copying is the clearest demonstration of using two
FILE* handles at once. Open the source in "r" and the destination in "w". Read one character from source with fgetc, write it to destination with fputc, repeat until EOF. Count the bytes to confirm. Then verify the copy by reading it back and comparing the line count.
#include <stdio.h> int countLines(const char *name) { FILE *fp = fopen(name, "r"); if (!fp) return -1; int ch, n = 0; while ((ch = fgetc(fp)) != EOF) if (ch == '\n') n++; fclose(fp); return n; } int main() { const char *src = "notes.txt"; const char *dest = "notes_backup.txt"; FILE *in = fopen(src, "r"); FILE *out = fopen(dest, "w"); if (!in || !out) { printf("Error opening file(s).\n"); return 1; } int ch, bytes = 0; while ((ch = fgetc(in)) != EOF) { fputc(ch, out); /* write one char to destination */ bytes++; } fclose(in); fclose(out); printf("Copied %d bytes: %s → %s\n", bytes, src, dest); printf("Source lines : %d\n", countLines(src)); printf("Backup lines : %d\n", countLines(dest)); printf("Copy verified: %s\n", countLines(src) == countLines(dest) ? "OK" : "MISMATCH"); return 0; }
Copied 128 bytes: notes.txt → notes_backup.txt Source lines : 8 Backup lines : 8 Copy verified: OK
For large files, copying byte-by-byte is slow. Use
fread/fwrite with a buffer (e.g. 4096 bytes) instead — one call reads a big chunk, one call writes it. See Example 8 for the binary fread/fwrite approach.example 6
6
📊 Count Lines, Words and Characters
Single-pass scan — how the Unix wc command works internally
Stats
Read the file character by character and track three counters in a single pass. Lines are counted by newline characters. Words are counted using an
inWord flag — it flips from 0 to 1 each time we enter a new non-whitespace run, and that flip is the word count increment. Characters are counted on every iteration. This is exactly how the Unix wc command works at its core.
#include <stdio.h> #include <ctype.h> /* isspace() */ int main() { FILE *fp = fopen("notes.txt", "r"); if (!fp) { printf("Error\n"); return 1; } int ch; long lines = 0, words = 0, chars = 0; int inWord = 0; /* flag: are we currently inside a word? */ while ((ch = fgetc(fp)) != EOF) { chars++; if (ch == '\n') lines++; if (isspace(ch)) { inWord = 0; /* left a word */ } else if (!inWord) { words++; /* just entered a new word */ inWord = 1; } } fclose(fp); printf("File : notes.txt\n"); printf("Lines : %ld\n", lines); printf("Words : %ld\n", words); printf("Chars : %ld\n", chars); printf("Avg word len: %.1f chars\n", words ? (float)(chars - lines) / words : 0); return 0; }
File : notes.txt Lines : 8 Words : 16 Chars : 128 Avg word len: 7.0 chars
The
inWord flag ensures each word is counted exactly once — on the transition from whitespace to non-whitespace. Without it, each character of a word would be counted separately. The same flag logic is inside BSD's wc source code.example 7
7
🔍 Search for a String in a File
fgets + strstr line by line — how grep works at its core
Search
Read the file line by line with
fgets. For each line, call strstr(line, keyword) — it returns a non-NULL pointer if the keyword appears anywhere in the line. Print matching lines with their line numbers. This is the exact algorithm inside the Unix grep command. A second pass demonstrates case-insensitive search using tolower on both strings.
#include <stdio.h> #include <string.h> #include <ctype.h> /* Convert string to lowercase in-place (into dest) */ void toLowerStr(const char *src, char *dest, int max) { int i; for (i = 0; i < max-1 && src[i]; i++) dest[i] = (char)tolower((unsigned char)src[i]); dest[i] = '\0'; } int searchFile(const char *filename, const char *keyword, int ignoreCase) { FILE *fp = fopen(filename, "r"); if (!fp) { printf("Error\n"); return 0; } char line[200], lcLine[200], lcKey[50]; int lineNo = 0, matches = 0; if (ignoreCase) toLowerStr(keyword, lcKey, 50); while (fgets(line, sizeof(line), fp)) { lineNo++; const char *haystack = line; const char *needle = keyword; if (ignoreCase) { toLowerStr(line, lcLine, sizeof(lcLine)); haystack = lcLine; needle = lcKey; } if (strstr(haystack, needle)) { line[strcspn(line, "\n")] = '\0'; printf(" Line %2d: %s\n", lineNo, line); matches++; } } fclose(fp); return matches; } int main() { printf("Search \"Ananta\" (exact):\n"); int m = searchFile("notes.txt", "Ananta", 0); printf(" %d match(es)\n\n", m); printf("Search \"ananta\" (case-insensitive):\n"); m = searchFile("notes.txt", "ananta", 1); printf(" %d match(es)\n\n", m); printf("Search \"2025\" (exact):\n"); m = searchFile("notes.txt", "2025", 0); printf(" %d match(es)\n", m); return 0; }
Search "Ananta" (exact): Line 1: Name : Ananta Line 8: College: Ananta Institute 2 match(es) Search "ananta" (case-insensitive): Line 1: Name : Ananta Line 8: College: Ananta Institute 2 match(es) Search "2025" (exact): Line 7: Year : 2025 1 match(es)
strstr(haystack, needle) returns a pointer to the first occurrence of needle inside haystack, or NULL if not found. The if (strstr(...)) idiom works because a non-NULL pointer is truthy. For case-insensitive matching, lowercase both strings before comparing.example 8
8
⚙️ Binary File I/O — fwrite and fread
Store raw bytes — no text, no commas, no newlines — compact and fast
Binary I/O
Binary mode (
"wb" / "rb") stores data as raw bytes — exactly as they sit in memory. No text conversion, no newlines, no delimiters. fwrite(ptr, size, count, fp) writes count objects of size bytes each. fread mirrors it exactly. Binary files are more compact, faster to read/write, and mandatory for non-text data like images, audio, and number arrays. The file is unreadable in a text editor but perfectly precise.
#include <stdio.h> int main() { /* ── WRITE: store 6 integers as raw bytes ── */ int scores[] = {85, 92, 78, 96, 88, 71}; int n = 6; FILE *fp = fopen("scores.bin", "wb"); /* b = binary mode */ if (!fp) { printf("Error\n"); return 1; } /* fwrite(data, size_per_item, count, file) */ size_t written = fwrite(scores, sizeof(int), n, fp); printf("Written : %zu integers (%zu bytes)\n", written, written * sizeof(int)); fclose(fp); /* ── READ: load them back ── */ int buf[10]; /* buffer large enough */ fp = fopen("scores.bin", "rb"); if (!fp) { printf("Error\n"); return 1; } /* fread(buffer, size_per_item, max_count, file) */ size_t got = fread(buf, sizeof(int), 10, fp); fclose(fp); printf("Read : %zu integers\n\n", got); printf("%-6s %s\n", "Index", "Score"); printf("-----------\n"); int sum = 0, max = buf[0]; for (int i = 0; i < (int)got; i++) { printf(" [%d] %d\n", i, buf[i]); sum += buf[i]; if (buf[i] > max) max = buf[i]; } printf("-----------\n"); printf("Average : %.1f\n", (float)sum / got); printf("Maximum : %d\n", max); return 0; }
Written : 6 integers (24 bytes) Read : 6 integers Index Score ----------- [0] 85 [1] 92 [2] 78 [3] 96 [4] 88 [5] 71 ----------- Average : 85.0 Maximum : 96
Text vs Binary: In text mode, the integer
96 is stored as two characters '9' and '6' (2 bytes). In binary mode it is stored as the 4-byte integer 0x00000060. Six integers = 6 × 4 = 24 bytes in binary. In text mode with commas and newlines it would be around 20+ characters but variable-length and not directly loadable.example 9
9
🗂️ Write and Read Struct Records
fwrite/fread with structs — a simple file database of fixed-size records
Struct Records
Writing an array of structs to a binary file is the foundation of file-based databases in C. One
fwrite call saves all records. One fread call loads them all back. Every record is exactly sizeof(Student) bytes — so you can seek to any record instantly with fseek(fp, n * sizeof(Student), SEEK_SET). This is called a fixed-length record file — the simplest form of a database.
#include <stdio.h> #include <string.h> typedef struct { char name[20]; int roll; float marks; char grade; } Student; void printStudent(const Student *s) { printf(" %-12s Roll:%-4d Marks:%5.1f Grade:%c\n", s->name, s->roll, s->marks, s->grade); } int main() { Student roster[] = { {"Ananta", 101, 88.5, 'B'}, {"Priya", 102, 95.0, 'A'}, {"Rahul", 103, 72.0, 'C'}, {"Sneha", 104, 91.5, 'A'}, {"Vikram", 105, 65.0, 'C'} }; int n = 5; /* ── WRITE all records in one call ── */ FILE *fp = fopen("students.dat", "wb"); fwrite(roster, sizeof(Student), n, fp); fclose(fp); printf("Saved %d records (%zu bytes each, %zu total)\n\n", n, sizeof(Student), n * sizeof(Student)); /* ── READ all records back ── */ Student loaded[10]; fp = fopen("students.dat", "rb"); size_t count = fread(loaded, sizeof(Student), 10, fp); fclose(fp); printf("Loaded %zu records:\n", count); for (int i = 0; i < (int)count; i++) printStudent(&loaded[i]); /* ── SEEK: read only record at index 2 ── */ fp = fopen("students.dat", "rb"); Student one; fseek(fp, 2 * sizeof(Student), SEEK_SET); /* jump to record 2 */ fread(&one, sizeof(Student), 1, fp); fclose(fp); printf("\nDirect access — record[2]:\n"); printStudent(&one); return 0; }
Saved 5 records (28 bytes each, 140 total) Loaded 5 records: Ananta Roll:101 Marks: 88.5 Grade:B Priya Roll:102 Marks: 95.0 Grade:A Rahul Roll:103 Marks: 72.0 Grade:C Sneha Roll:104 Marks: 91.5 Grade:A Vikram Roll:105 Marks: 65.0 Grade:C Direct access — record[2]: Rahul Roll:103 Marks: 72.0 Grade:C
fseek(fp, offset, origin) moves the file position pointer. SEEK_SET = from start, SEEK_CUR = from current, SEEK_END = from end. With fixed-size records, fseek(fp, n * sizeof(Student), SEEK_SET) jumps directly to the nth record — O(1) random access, just like an array.example 10
10
🎓 Student File Manager — Complete Mini App
Add, display, search by roll, update marks, find topper — all persisted to disk
Complete App
Everything combined — a fully functional student record manager that persists data to
students.dat. Five operations: addRecord appends one struct using "ab", displayAll reads and prints all, searchByRoll finds a student with fseek/fread, updateMarks uses "r+b" to overwrite one field in-place, and findTopper scans all records. This is how simple C database applications are structured.
#include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct { char name[20]; int roll; float marks; } Student; const char *DB = "students.dat"; /* 1. Append one record */ void addRecord(Student s) { FILE *fp = fopen(DB, "ab"); /* ab = append binary */ fwrite(&s, sizeof(Student), 1, fp); fclose(fp); } /* 2. Display all records */ void displayAll() { FILE *fp = fopen(DB, "rb"); if (!fp) { printf(" (empty)\n"); return; } Student s; int i = 0; printf(" %-12s %5s %7s\n", "Name", "Roll", "Marks"); printf(" ---------------------------\n"); while (fread(&s, sizeof(Student), 1, fp) == 1) { printf(" %-12s %5d %7.1f\n", s.name, s.roll, s.marks); i++; } printf(" ---------------------------\n"); printf(" Total records: %d\n", i); fclose(fp); } /* 3. Search by roll — returns record index or -1 */ int searchByRoll(int roll, Student *out) { FILE *fp = fopen(DB, "rb"); if (!fp) return -1; Student s; int i = 0; while (fread(&s, sizeof(Student), 1, fp) == 1) { if (s.roll == roll) { *out = s; fclose(fp); return i; } i++; } fclose(fp); return -1; } /* 4. Update marks in-place at record index */ void updateMarks(int roll, float newMarks) { Student s; int idx = searchByRoll(roll, &s); if (idx < 0) { printf(" Roll %d not found.\n", roll); return; } FILE *fp = fopen(DB, "r+b"); /* r+b = read+write binary */ fseek(fp, idx * (long)sizeof(Student), SEEK_SET); fread(&s, sizeof(Student), 1, fp); /* load current record */ s.marks = newMarks; /* modify in memory */ fseek(fp, idx * (long)sizeof(Student), SEEK_SET); fwrite(&s, sizeof(Student), 1, fp); /* write back */ fclose(fp); printf(" Updated %s: marks = %.1f\n", s.name, newMarks); } /* 5. Find topper */ void findTopper() { FILE *fp = fopen(DB, "rb"); if (!fp) return; Student s, best; best.marks = -1; while (fread(&s, sizeof(Student), 1, fp) == 1) if (s.marks > best.marks) best = s; fclose(fp); printf(" Topper: %s (Roll %d) — %.1f marks\n", best.name, best.roll, best.marks); } int main() { /* Start fresh */ remove(DB); printf("=== Adding Records ===\n"); addRecord((Student){"Ananta", 101, 88.5}); addRecord((Student){"Priya", 102, 73.0}); addRecord((Student){"Rahul", 103, 91.5}); addRecord((Student){"Sneha", 104, 58.0}); addRecord((Student){"Vikram", 105, 45.0}); printf(" 5 records saved to %s\n\n", DB); printf("=== All Records ===\n"); displayAll(); printf("\n=== Search Roll 103 ===\n"); Student found; int idx = searchByRoll(103, &found); if (idx >= 0) printf(" Found at index %d: %s — %.1f\n", idx, found.name, found.marks); printf("\n=== Update Priya's marks to 97.0 ===\n"); updateMarks(102, 97.0); printf("\n=== All Records After Update ===\n"); displayAll(); printf("\n=== Topper ===\n"); findTopper(); return 0; }
=== Adding Records === 5 records saved to students.dat === All Records === Name Roll Marks --------------------------- Ananta 101 88.5 Priya 102 73.0 Rahul 103 91.5 Sneha 104 58.0 Vikram 105 45.0 --------------------------- Total records: 5 === Search Roll 103 === Found at index 2: Rahul — 91.5 === Update Priya's marks to 97.0 === Updated Priya: marks = 97.0 === All Records After Update === Name Roll Marks --------------------------- Ananta 101 88.5 Priya 102 97.0 Rahul 103 91.5 Sneha 104 58.0 Vikram 105 45.0 --------------------------- Total records: 5 === Topper === Topper: Priya (Roll 102) — 97.0 marks
All five file I/O patterns in one program:
addRecord appends with "ab" · displayAll reads with a while(fread...==1) loop · searchByRoll does linear scan · updateMarks uses "r+b" + fseek to edit in-place · findTopper scans for the max. Data persists between runs — kill the program, run it again, all five students are still there.checklist
- Ex 1 — Open → Use → Close. Mode
"w"creates or erases. Always checkfp == NULL.fcloseflushes to disk. - Ex 2 —
fgetcreturnsint, notchar. Loop untilEOF. Declaringchascharcan cause an infinite loop on unsigned-char platforms. - Ex 3 —
fgets(buf, size, fp)is safe — limits line length. It keeps\n. Strip withstrcspn.fscanfreads tokens. - Ex 4 — Mode
"a"always writes at end. Original data is untouched. If file missing, it is created. - Ex 5 — Two
FILE*handles at once: one read, one write.fgetc/fputcpipe bytes. For speed, usefread/fwritewith a buffer. - Ex 6 —
inWordflag counts word-entry transitions. Count lines via'\n'. This is exactly howwcworks. - Ex 7 —
strstr(line, keyword)returns non-NULL on match.tolowerboth strings for case-insensitive search. - Ex 8 —
fwrite(ptr, size, count, fp)writes raw bytes.freadmirrors it. Binary = compact, fast, non-human-readable. - Ex 9 — One
fwritesaves all struct records.fseek(fp, n*sizeof(S), SEEK_SET)jumps to record n directly — O(1). - Ex 10 —
"ab"appends structs ·"rb"reads ·"r+b"+fseekedits in-place ·while(fread==1)is the standard read-all loop.