Lecture 8: Files and Advanced Topics
Prerequisites: Lecture 5 Functions (defining and calling functions, parameter passing); Lecture 7 Pointers (pointer declaration, dereference,
malloc/free, passing pointers to functions)
1. Overview
- Programmes that have been written thus far lose all data when they terminate; this chapter introduces file I/O, which enables programmes to read and write persistent data stored on disc, and presents the advanced data types (
struct,union,enum) needed to model real-world entities. - After completing this chapter you will be able to define composite data types with
struct, useunionandenumwhere appropriate, open and close files safely, read and write both text and binary data, handle file-related errors robustly, and accept command-line arguments.
2. Learning Outcomes
Upon successful completion of this chapter, students will be able to:
- Declare and initialise
structtypes, access members with the dot (.) and arrow (->) operators, and simplify type names usingtypedef. - Explain the memory-sharing semantics of
unionand identify situations in which aunionis preferable to astruct. - Define and use
enumtypes—including custom-valued enumerators—to improve code readability. - Open, read from, write to, and close files in text mode using
fopen(),fprintf(),fscanf(),fgets(), andfclose(). - Open, read from, write to, and close files in binary mode using
fread()andfwrite(). - Apply defensive error-handling techniques: checking
fopen()forNULL, usingperror(), detecting end-of-file withfeof(), and checking for I/O errors withferror(). - Serialise and deserialise
structdata to and from both text and binary files. - Accept and parse command-line arguments via
argcandargv. - Combine
structdefinitions with file I/O to build small but realistic data-processing programmes.
3. Key Terms
| Term (EN) | 中文 | Notes |
|---|---|---|
struct | 结构体 | A user-defined composite type that groups variables of different types under one name. Members are accessed with . (by value) or -> (by pointer). |
typedef | 类型别名 | Creates an alias for an existing type; commonly used with struct to avoid repeating the struct keyword. |
union | 联合体 | Like a struct, but all members share the same memory; only one member is valid at any time. The size of a union equals the size of its largest member. |
enum | 枚举 | Defines a set of named integer constants; by default values start at 0 and increment by 1, but may be set explicitly. |
| file stream | 文件流 | An abstraction provided by <stdio.h> for sequential reading or writing of data; represented by the FILE * type. |
fopen()/fclose() | 打开/关闭文件 | fopen(path, mode) returns a FILE * (or NULL on failure); fclose(fp) flushes the buffer and releases the resource. |
| text mode | 文本模式 | Human-readable characters; modes "r", "w", "a", etc. Newline translation may occur on some platforms. |
| binary mode | 二进制模式 | Raw bytes preserving the exact memory layout; modes "rb", "wb", "ab", etc. Essential for non-text data such as images, audio, or struct records. |
fprintf()/fscanf() | 格式化文件写/读 | Formatted I/O analogous to printf() / scanf(), but operating on a file stream rather than stdout / stdin. |
fwrite()/fread() | 二进制写/读 | Transfer raw blocks of bytes between memory and a file; parameters are: data pointer, element size, element count, stream. |
perror() | 打印系统错误 | Prints a user-supplied prefix followed by a colon and the system error message corresponding to the current value of errno. |
EOF | 文件结束标志 | A macro (typically −1) returned by I/O functions to signal that the end of the file has been reached or that an error occurred. |
argc / argv | 命令行参数 | argc is the count of command-line tokens (including the programme name); argv is an array of C strings containing those tokens. |
4. Core Concepts
4.1 Structures (struct)
A structure is a user-defined data type that groups together variables—potentially of different types—into a single logical entity. Each variable within the structure is called a member (or field). Structures are invaluable for modelling real-world objects: a student record might contain an integer ID, a character-array name, and a floating-point grade, all bundled into one struct Student.
A structure is declared with the keyword struct followed by an optional tag name and a brace-enclosed list of member declarations. Once declared, variables of that structure type may be created, initialised with brace-enclosed initialiser lists, and their members accessed with the dot operator (.).
Minimal example
File: p08_a.c
Build: gcc p08_a.c -o p08_a
Run: ./p08_a
#include <stdio.h>
struct Student {
int id;
char name[50];
float grade;
};
int main(void) {
struct Student s1 = {1, "Alice", 88.5};
printf("ID : %d\n", s1.id);
printf("Name : %s\n", s1.name);
printf("Grade: %.1f\n", s1.grade);
s1.grade = 90.0; /* modify a member */
printf("Updated grade: %.1f\n", s1.grade);
return 0;
}
What you should see: The student's ID, name, and original grade printed, followed by the updated grade of 90.0.
Takeaway: struct groups heterogeneous data into a single variable; members are accessed with the . operator.
4.2 Using typedef with struct
Writing struct Student every time a variable is declared can become tedious. The typedef keyword creates an alias for a type, enabling shorter, cleaner declarations. A common idiom combines the structure definition and the alias in a single statement:
typedef struct {
int x, y;
} Point;
After this definition, Point alone suffices wherever struct Point would otherwise be required.
Minimal example
File: p08_b.c
Build: gcc p08_b.c -o p08_b
Run: ./p08_b
#include <stdio.h>
typedef struct {
int x, y;
} Point;
int main(void) {
Point p1 = {3, 4};
printf("(%d, %d)\n", p1.x, p1.y);
return 0;
}
What you should see: (3, 4)
Takeaway: typedef eliminates the need to repeat the struct keyword and yields more readable code.
4.3 Structures with Pointers and Functions
A structure may be passed to a function by value (the function receives a copy) or by pointer (the function receives the address of the original). Passing by pointer is generally preferred for two reasons: it avoids the overhead of copying a potentially large structure, and it allows the function to modify the original data.
When accessing a member through a pointer to a structure, the arrow operator (->) is used. The expression ptr->member is syntactic sugar for (*ptr).member.
Minimal example
File: p08_c.c
Build: gcc p08_c.c -o p08_c
Run: ./p08_c
#include <stdio.h>
typedef struct {
int id;
float balance;
} Account;
void deposit(Account *a, float amount) {
a->balance += amount;
}
void display(const Account *a) {
printf("Account %d: balance = %.2f\n", a->id, a->balance);
}
int main(void) {
Account acc = {101, 5000.0};
display(&acc);
deposit(&acc, 1500.0);
display(&acc);
return 0;
}
What you should see: The initial balance of 5000.00, followed by the updated balance of 6500.00.
Takeaway: Use -> to access members through a structure pointer; passing by pointer is both efficient and permits in-place modification.
4.4 Recursively Defined Structures
A structure may contain a pointer to another instance of the same structure type. This pattern —called a self-referential (or recursive) structure—is the foundation of linked data structures such as linked lists and trees.
Because the compiler must know the tag name in order to declare the internal pointer, the full typedef struct Tag { ... } Alias; form is necessary; the anonymous typedef struct { ... } form cannot refer to itself.
Minimal example
File: p08_d.c
Build: gcc p08_d.c -o p08_d
Run: ./p08_d
#include <stdio.h>
typedef struct Node {
int data;
struct Node *next;
} Node;
int main(void) {
Node a = {10, NULL};
Node b = {20, NULL};
Node c = {30, NULL};
a.next = &b;
b.next = &c;
/* traverse the list */
Node *current = &a;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
return 0;
}
What you should see: 10 20 30
Takeaway: A self-referential struct with a pointer to its own type is the building block for linked lists and other dynamic data structures.
4.5 Unions (union)
A union resembles a struct in syntax, but with a critical difference: all members of a union share the same memory location. The size of a union equals the size of its largest member. At any given moment, only one member holds a valid value; writing to one member overwrites the contents of all others.
Unions are useful when a variable must hold values of different types at different times (a "variant" type), or when one needs to inspect the raw byte representation of a value.
Minimal example
File: p08_e.c
Build: gcc p08_e.c -o p08_e
Run: ./p08_e
#include <stdio.h>
union Data {
int i;
float f;
char str[4];
};
int main(void) {
union Data d;
d.i = 10;
printf("d.i = %d\n", d.i);
d.f = 3.14f; /* overwrites d.i */
printf("d.f = %f\n", d.f);
printf("d.i = %d (reinterpreted bytes)\n", d.i);
printf("sizeof(union Data) = %zu\n", sizeof(union Data));
return 0;
}
What you should see: d.i = 10, then d.f = 3.140000, then a seemingly nonsensical integer value for d.i (the float's bit pattern reinterpreted as an int), and a size equal to that of the largest member (4 bytes on most platforms).
Takeaway: All union members occupy the same memory; assigning to one member invalidates the others. The size of the union equals the size of its largest member.
4.6 Combining struct with union — Variant Types
A practical pattern is to embed a union inside a struct alongside a discriminator field (often called a "tag") that records which member of the union is currently active. This produces a lightweight tagged union or variant type.
Minimal example
File: p08_f.c
Build: gcc p08_f.c -o p08_f
Run: ./p08_f
#include <stdio.h>
typedef struct {
int type; /* 1 = int, 2 = float */
union {
int i;
float f;
} value;
} Variant;
void print_variant(Variant v) {
if (v.type == 1)
printf("Int : %d\n", v.value.i);
else if (v.type == 2)
printf("Float: %.2f\n", v.value.f);
else
printf("Unknown type\n");
}
int main(void) {
Variant a = {1, {.i = 42}};
Variant b = {2, {.f = 3.14f}};
print_variant(a);
print_variant(b);
return 0;
}
What you should see: Int : 42 followed by Float: 3.14.
Takeaway: Wrapping a union inside a struct with a type tag yields a safe variant type—check the tag before accessing the corresponding member.
4.7 Enumerated Types (enum)
An enumeration defines a set of named integer constants. By default, the first enumerator is assigned the value 0, the second 1, and so on—but explicit values may be specified. Enumerations improve readability and maintainability by replacing "magic numbers" with meaningful names.
Minimal example
File: p08_g.c
Build: gcc p08_g.c -o p08_g
Run: ./p08_g
#include <stdio.h>
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN };
int main(void) {
enum Day today = WED;
if (today == WED)
printf("Mid-week!\n");
printf("WED has numeric value %d\n", WED);
return 0;
}
What you should see: Mid-week! followed by WED has numeric value 2.
Takeaway: enum replaces magic numbers with descriptive names; values default to 0, 1, 2, … but can be set explicitly.
4.8 Customising enum Values
Explicit integer values may be assigned to any enumerator. Subsequent enumerators without explicit values continue incrementing from the last assigned value. This facility is commonly used for status codes, error codes, and protocol constants.
Minimal example
File: p08_h.c
Build: gcc p08_h.c -o p08_h
Run: ./p08_h
#include <stdio.h>
enum StatusCode {
OK = 200,
CREATED = 201,
BAD_REQUEST = 400,
NOT_FOUND = 404
};
int main(void) {
enum StatusCode code = NOT_FOUND;
printf("HTTP status code = %d\n", code);
return 0;
}
What you should see: HTTP status code = 404
Takeaway: Assign arbitrary integer values to enumerators when the numeric representation carries meaning (e.g., HTTP status codes).
4.9 File I/O Fundamentals
A file is a named region of persistent storage on disc. In C, all file operations are mediated through file streams—an abstraction provided by <stdio.h> and represented by the opaque type FILE *. The lifecycle of a file operation comprises three stages:
- Open the file with
fopen(filename, mode), which returns aFILE *orNULLon failure. - Read from or write to the stream using the appropriate I/O functions.
- Close the file with
fclose(fp), which flushes any buffered data and releases system resources.
The mode string passed to fopen() determines the kind of access:
| Mode | Description |
|---|---|
"r" | Open an existing file for reading. |
"w" | Create a new file for writing; if the file already exists, its contents are discarded. |
"a" | Open for appending; writes are added to the end. The file is created if it does not exist. |
"r+" | Open an existing file for both reading and writing. |
"w+" | Create a new file for both reading and writing; existing contents are discarded. |
"a+" | Open for reading and appending; the file is created if it does not exist. |
Appending "b" to any mode (e.g., "rb", "wb") opens the file in binary mode, which is discussed in §4.12.
Takeaway: Always open before use, check for NULL, and close when finished—this three-step discipline prevents resource leaks and data corruption.
4.10 Writing to a Text File
fprintf() operates identically to printf(), except that its first argument is a FILE * specifying the destination stream. Each call writes formatted text to the file.
Minimal example
File: p08_i.c
Build: gcc p08_i.c -o p08_i
Run: ./p08_i
#include <stdio.h>
int main(void) {
FILE *fp = fopen("numbers.txt", "w");
if (fp == NULL) {
perror("Error opening file");
return 1;
}
for (int i = 1; i <= 5; i++) {
fprintf(fp, "%d\n", i);
}
fclose(fp);
printf("Wrote 5 integers to numbers.txt\n");
return 0;
}
What you should see: A confirmation message on the terminal; the file numbers.txt contains the integers 1 through 5, each on its own line.
Takeaway: fprintf(fp, ...) writes formatted text to the file associated with fp; always check the return value of fopen().
4.11 Reading from a Text File
fscanf() mirrors scanf(), but reads from a file stream. It returns the number of items successfully matched, or EOF when the end of the file is reached (or an error occurs). A common idiom is to loop while fscanf() does not return EOF.
Minimal example
File: p08_j.c
Build: gcc p08_j.c -o p08_j
Run: ./p08_j (after running p08_i to create numbers.txt)
#include <stdio.h>
int main(void) {
FILE *fp = fopen("numbers.txt", "r");
if (fp == NULL) {
perror("Cannot open file");
return 1;
}
int num;
while (fscanf(fp, "%d", &num) != EOF) {
printf("%d\n", num);
}
fclose(fp);
return 0;
}
What you should see: The integers 1 through 5 printed to the terminal, one per line.
Takeaway: Use fscanf() in a loop, comparing its return value against EOF, to read until the file is exhausted.
4.12 Writing and Reading struct Data in Text Mode
Structures can be serialised to a text file field by field using fprintf(), and deserialised in the same order using fscanf(). This approach produces human-readable files, but requires careful format-string design to handle strings that may contain spaces (consider fgets() for such cases).
Minimal example
File: p08_k.c
Build: gcc p08_k.c -o p08_k
Run: ./p08_k
#include <stdio.h>
typedef struct {
int id;
char name[20];
} Student;
void save(Student s) {
FILE *fp = fopen("student.txt", "w");
if (fp == NULL) { perror("save"); return; }
fprintf(fp, "%d %s\n", s.id, s.name);
fclose(fp);
}
void load(void) {
Student s;
FILE *fp = fopen("student.txt", "r");
if (fp == NULL) { perror("load"); return; }
fscanf(fp, "%d %s", &s.id, s.name);
printf("ID : %d\nName: %s\n", s.id, s.name);
fclose(fp);
}
int main(void) {
Student s1 = {42, "Alice"};
save(s1);
load();
return 0;
}
What you should see: ID : 42 and Name: Alice.
Takeaway: Serialise struct members individually with fprintf(); deserialise in the same order with fscanf().
4.13 Binary File I/O
Text-mode I/O converts data to and from human-readable character sequences, which incurs formatting overhead and may alter certain bytes (e.g., newline translation on Windows). Binary mode ("rb", "wb", "ab") transfers raw bytes, preserving the exact in-memory representation. This is essential for non-text data such as images, audio, or structured records.
The key functions are:
| Function | Prototype | Purpose |
|---|---|---|
fwrite() | size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream); | Writes count elements of size bytes each from the buffer ptr to the stream. |
fread() | size_t fread(void *ptr, size_t size, size_t count, FILE *stream); | Reads up to count elements of size bytes each into the buffer ptr. Returns the number of elements actually read. |
Minimal example — write and read a struct in binary
File: p08_l.c
Build: gcc p08_l.c -o p08_l
Run: ./p08_l
#include <stdio.h>
typedef struct {
int id;
float score;
} Record;
int main(void) {
/* --- Write --- */
Record r1 = {1001, 87.5f};
FILE *fp = fopen("data.bin", "wb");
if (fp == NULL) { perror("write"); return 1; }
fwrite(&r1, sizeof(Record), 1, fp);
fclose(fp);
/* --- Read --- */
Record r2;
fp = fopen("data.bin", "rb");
if (fp == NULL) { perror("read"); return 1; }
fread(&r2, sizeof(Record), 1, fp);
fclose(fp);
printf("ID : %d\n", r2.id);
printf("Score: %.2f\n", r2.score);
return 0;
}
What you should see: ID : 1001 and Score: 87.50.
Takeaway: fwrite() and fread() transfer raw bytes; use them with binary mode to persist struct data efficiently and without formatting overhead.
4.14 Error Handling in File I/O
Robust file I/O demands defensive programming. The principal techniques are:
- Check
fopen()forNULL: The file may not exist, the path may be incorrect, or the programme may lack permission. - Use
perror(): Whenfopen()(or another library function) fails,perror("message")prints "message: " followed by the system's description of the error stored inerrno. - Check
feof()andferror(): After a read loop terminates,feof(fp)returns true if the end of the file was reached normally, whilstferror(fp)returns true if a read or write error occurred.
Minimal example
File: p08_m.c
Build: gcc p08_m.c -o p08_m
Run: ./p08_m
#include <stdio.h>
int main(void) {
FILE *fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
perror("fopen"); /* prints: fopen: No such file or directory */
return 1;
}
int num;
while (fscanf(fp, "%d", &num) == 1) {
printf("%d\n", num);
}
if (feof(fp))
printf("Reached end of file.\n");
if (ferror(fp))
printf("A read error occurred.\n");
fclose(fp);
return 0;
}
What you should see: fopen: No such file or directory (since the file does not exist), and the programme exits with code 1.
Takeaway: Always validate file operations; perror() provides informative diagnostics, and feof() / ferror() disambiguate the reason a read loop terminated.
4.15 Command-Line Arguments
The main() function in C may be declared with two parameters that grant access to the tokens supplied on the command line:
int main(int argc, char *argv[])
argc (argument count) is the total number of tokens, including the programme's own name. argv (argument vector) is an array of C strings: argv[0] is the programme name, argv[1] is the first user-supplied argument, and so on. This mechanism allows programmes to be configured at launch without interactive prompts.
Minimal example
File: p08_n.c
Build: gcc p08_n.c -o p08_n
Run: ./p08_n hello 123
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Argument count: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
if (argc < 2) {
printf("Usage: %s <filename> [options...]\n", argv[0]);
}
return 0;
}
What you should see (for ./p08_n hello 123): Three arguments listed—the programme name, hello, and 123.
Takeaway: argc and argv let a programme accept configuration from the command line; always validate argc before accessing argv elements.
5. Worked Examples
Example 1: Student Records — Text File Round-Trip
Problem: Define a Student structure with fields id (int), name (char array), and grade (float). Write an array of three students to a text file, then read them back and print the records.
Steps:
- Define the
Studenttype withtypedef. - Open a file in write mode; iterate over the array and write each student with
fprintf(). - Close and reopen the file in read mode; loop with
fscanf()untilEOF; print each record.
Reference code
File: p08_ex1.c
Build: gcc p08_ex1.c -o p08_ex1
Run: ./p08_ex1
#include <stdio.h>
typedef struct {
int id;
char name[20];
float grade;
} Student;
int main(void) {
Student roster[] = {
{1, "Alice", 92.5f},
{2, "Bob", 85.0f},
{3, "Charlie", 78.3f}
};
int n = sizeof(roster) / sizeof(roster[0]);
/* --- Write --- */
FILE *fp = fopen("roster.txt", "w");
if (fp == NULL) { perror("write"); return 1; }
for (int i = 0; i < n; i++) {
fprintf(fp, "%d %s %.1f\n",
roster[i].id, roster[i].name, roster[i].grade);
}
fclose(fp);
/* --- Read --- */
fp = fopen("roster.txt", "r");
if (fp == NULL) { perror("read"); return 1; }
Student s;
printf("%-4s %-10s %s\n", "ID", "Name", "Grade");
printf("---- ---------- ------\n");
while (fscanf(fp, "%d %s %f", &s.id, s.name, &s.grade) == 3) {
printf("%-4d %-10s %.1f\n", s.id, s.name, s.grade);
}
fclose(fp);
return 0;
}
Output:
ID Name Grade
---- ---------- ------
1 Alice 92.5
2 Bob 85.0
3 Charlie 78.3
Why it works: Each student is written as a single line with space-separated fields. When reading back, fscanf() parses three values per iteration; the loop continues as long as all three conversions succeed (return value == 3), and terminates naturally at end-of-file.
Example 2: Binary Persistence of 3D Points
Problem: Define a Point3D structure with float x, y, z. Create an array of points, write them to a binary file using fwrite(), then read them back with fread() and display the results.
Steps:
- Define
Point3Dwithtypedef. - Populate an array of sample points.
- Open a file in
"wb"mode; write the entire array in a singlefwrite()call. - Reopen in
"rb"mode; read back withfread()and print.
Reference code
File: p08_ex2.c
Build: gcc p08_ex2.c -o p08_ex2
Run: ./p08_ex2
#include <stdio.h>
typedef struct {
float x, y, z;
} Point3D;
int main(void) {
Point3D pts[] = {
{ 1.0f, 2.0f, 3.0f},
{ 4.5f, -1.2f, 0.0f},
{-3.3f, 7.7f, 2.1f}
};
int n = sizeof(pts) / sizeof(pts[0]);
/* --- Write --- */
FILE *fp = fopen("points.bin", "wb");
if (fp == NULL) { perror("write"); return 1; }
fwrite(pts, sizeof(Point3D), n, fp);
fclose(fp);
/* --- Read --- */
Point3D buf[10];
fp = fopen("points.bin", "rb");
if (fp == NULL) { perror("read"); return 1; }
int count = fread(buf, sizeof(Point3D), 10, fp);
fclose(fp);
printf("Read %d points:\n", count);
for (int i = 0; i < count; i++) {
printf(" Point %d: (%7.4f, %7.4f, %7.4f)\n",
i + 1, buf[i].x, buf[i].y, buf[i].z);
}
return 0;
}
Output:
Read 3 points:
Point 1: ( 1.0000, 2.0000, 3.0000)
Point 2: ( 4.5000, -1.2000, 0.0000)
Point 3: (-3.3000, 7.7000, 2.1000)
Why it works: fwrite() copies n * sizeof(Point3D) raw bytes from the array to the file. fread() reverses the process, returning the number of elements actually read. Because binary I/O preserves the exact memory layout, the values are recovered without any formatting overhead or precision loss.
Example 3: Command-Line File Printer
Problem: Write a programme that accepts a filename as a command-line argument, opens the file, and prints its contents line by line. If no argument is provided, print a usage message.
Steps:
- Check
argc; if less than 2, print usage and exit. - Open the file named in
argv[1]for reading. - Read and print each line using
fgets()in a loop.
Reference code
File: p08_ex3.c
Build: gcc p08_ex3.c -o p08_ex3
Run: ./p08_ex3 numbers.txt
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL) {
perror(argv[1]);
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line); /* fgets retains the newline */
}
fclose(fp);
return 0;
}
Output (assuming numbers.txt from earlier):
1
2
3
4
5
Why it works: argv[1] provides the filename at run-time, eliminating the need to hard-code it. fgets() reads one line (up to 255 characters plus the null terminator) at a time. The loop terminates when fgets() returns NULL, indicating end-of-file.
6. Common Mistakes
-
Forgetting to close a file: Failing to call
fclose()may leave buffered data unwritten and leak file handles. In long-running programmes this can exhaust the operating system's limit on open files. Always pair every successfulfopen()with a correspondingfclose(). -
Not checking the return value of
fopen(): Iffopen()returnsNULLand the programme proceeds to callfprintf(),fscanf(),fread(), orfwrite()on the null pointer, undefined behaviour—typically a crash—results. Always check:if (fp == NULL) { perror("..."); return 1; }. -
Using
"w"when"a"is intended: Mode"w"truncates the file to zero length if it already exists. If the goal is to add data to an existing file, use"a"(append). -
Reading a binary file in text mode (or vice versa): On Windows, text-mode I/O translates
\r\nto\non input and vice versa on output, which corrupts binary data. Always use"rb"/"wb"for non-text files. -
Accessing the wrong
unionmember: Writing tod.fand then readingd.ireinterprets the float's bit pattern as an integer—a common source of confusion. Always track which member is active, ideally by using a tagged-union pattern (§4.6). -
Omitting
&for non-arraystructmembers infscanf(): When reading intos.id(anint), write&s.id. Fors.name(achararray), the array name already decays to a pointer, so&is unnecessary—but accidentally including it on the array or omitting it on theintare both common errors. -
Assuming
structlayout is portable across platforms: Binary files written byfwrite()depend on the platform's byte order (endianness) and structure padding. A binary file written on one machine may not be readable on another with different architecture. For cross-platform exchange, prefer text serialisation or standardised formats.
7. Practice
7.1 Basic
-
P1: Define and Display a Structure Define a
struct Bookwith fieldstitle(char array),author(char array), andprice(float). Create a variable, initialise it, and print all fields.
Acceptance: Programme compiles cleanly; output displays all three fields. -
P2: Enum Day of the Week
Define anenum Daywith seven values. Write a function that accepts aDayand prints the corresponding English name (e.g.,MON→"Monday"). Test it frommain().
Acceptance: Correct name is printed for at least three different days. -
P3: Write and Read Integers
Write a programme that creates a filedata.txt, writes the integers 10, 20, 30, 40, 50 (one per line), closes the file, reopens it for reading, and prints the contents.
Acceptance: Output is10 20 30 40 50(each on its own line).
7.2 Required
-
P4: Student Roster File
Define aStudentstructure (id, name, grade). Hard-code an array of 4 students. Write them tostudents.txtusingfprintf(), then read them back withfscanf()and display a formatted table.
Acceptance: The table displays all 4 students with correct values. -
P5: Binary Round-Trip
Define aRecordstructure withint idandfloat score. Create 3 records, write them torecords.binin binary mode, read them back, and print the results.
Acceptance: Output matches the original values exactly (to two decimal places). -
P6: Union Exploration
Declare aunioncontaining anint, afloat, and achar[4]. Assign a value to each member in turn, printing the result after each assignment. After each write, also print theintmember to observe byte reinterpretation. Include aprintfshowingsizeofthe union.
Acceptance: Programme compiles and demonstrates that writing one member overwrites others; the reported size equals the largest member. -
P7: Safe File Copy
Write a programme that takes a source filename and a destination filename as command-line arguments. Open the source for reading, the destination for writing, and copy the source's contents line by line usingfgets()andfprintf(). Handle all error cases (wrong number of arguments, source not found).
Acceptance: The destination file is an exact copy of the source; error cases produce informative messages.
7.3 Optional
-
P8: Tagged Variant Array
Define a tagged-unionVariant(as in §4.6). Create an array of 5Variantvalues mixing integers and floats. Write the array to a binary file, read it back, and dispatch printing through the type tag.
Acceptance: All 5 values are correctly recovered and printed with the appropriate format. -
P9: Mini Address Book
Define aContactstructure (name, phone number, email). Implement a simple menu-driven programme that supports: (a) add a contact, (b) list all contacts, (c) save contacts to a text file, (d) load contacts from a text file, and (e) quit. Limit the address book to a fixed maximum of 50 contacts.
Acceptance: Contacts survive a programme restart (save, quit, relaunch, load, list).
8. Mini Task
Goal: Build a 3D point cloud viewer that reads a set of Point3D records from a binary file, computes basic statistics (minimum, maximum, and mean for each coordinate axis), and prints a summary report. Optionally, accept the filename via a command-line argument.
Deliverables:
README.md: Compilation and usage instructions, including how to generate a sample binary file.src/generate_points.c: A helper programme that creates a binary file of samplePoint3Ddata (e.g., points on a sphere or torus).src/view_points.c: The main programme that reads the binary file, computes statistics, and prints the report.report.md: A short reflection (150–300 words) describing one challenge encountered and how it was resolved.
9. Further Reading
- Kernighan, B. W. & Ritchie, D. M., The C Programming Language, 2nd edn (Prentice Hall, 1988): Chapter 6 — "Structures"; Chapter 7 — "Input and Output".
- King, K. N., C Programming: A Modern Approach, 2nd edn (W. W. Norton, 2008): Chapter 16 — "Structures, Unions, and Enumerations"; Chapter 22 — "Input/Output".
- ISO/IEC 9899:2018, Programming Languages — C (C18 Standard): §6.7.2.1 (Structure and union specifiers), §6.7.2.2 (Enumeration specifiers), §7.21 (Input/output
<stdio.h>).
10. Appendix
A. Quick Reference — File Access Modes
| Mode | Read | Write | Create | Truncate | Append |
|---|---|---|---|---|---|
"r" | Yes | — | — | — | — |
"w" | — | Yes | Yes | Yes | — |
"a" | — | Yes | Yes | — | Yes |
"r+" | Yes | Yes | — | — | — |
"w+" | Yes | Yes | Yes | Yes | — |
"a+" | Yes | Yes | Yes | — | Yes |
Add "b" for binary mode (e.g., "rb", "wb+", "ab").
B. Quick Reference — fread / fwrite Signatures
size_t fread (void *ptr, size_t size, size_t count, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
Both return the number of elements (not bytes) successfully transferred. Always compare the return value against the expected count to detect partial reads/writes.
C. Struct Memory Layout and Padding
Compilers may insert padding bytes between structure members to satisfy alignment requirements. For example:
typedef struct {
char c; /* 1 byte + 3 bytes padding */
int i; /* 4 bytes */
char d; /* 1 byte + 3 bytes padding */
} Padded;
/* sizeof(Padded) may be 12, not 6 */
This affects the size of binary files produced by fwrite(). Use sizeof(StructType) rather than manually summing member sizes, and be aware that binary files are generally not portable across platforms with differing alignment or endianness conventions.