11 File I/O

1. Introduction to File I/O

1.1 What is File I/O?

File Input/Output (I/O) is the process of reading from and writing to files. In programming, especially in C, it's a fundamental concept that allows your program to persist data, or read data from external sources for various applications.

1.2 Importance in Embedded Systems

In embedded systems, File I/O is a bit of a nuanced subject. While some embedded systems have no filesystem and thus no file I/O, others do. Those that do could use file I/O for a variety of reasons, such as:

  • Configuration: Storing system settings that persist between restarts.
  • Data Logging: Recording data for later analysis, which is crucial in many industrial applications.
  • Firmware Updates: Storing new firmware images before a system update.
  • Resource Storage: Holding assets like language files, UI elements, or cryptographic keys.

2. Standard I/O vs. Low-Level I/O

When it comes to file input/output (I/O) in C, you generally have two broad categories of functions that you can use: Standard I/O and low-level I/O. Each has its own set of advantages and disadvantages, particularly in the context of embedded systems. The following sections will discuss each in more detail.

2.1 Standard I/O

Standard I/O is part of the C Standard Library and includes functions like fopen, fread, fwrite, and fclose. This level of I/O is buffered, meaning that it reads or writes data in chunks rather than one byte at a time, which can improve performance.

Example:

FILE *fp = fopen("file.txt", "r");
char buffer[100];
fread(buffer, 1, 100, fp);
fclose(fp);

Advantages

  • Easier to use and more portable.
  • Buffered I/O can offer performance benefits.
  • Rich set of features like formatted I/O (fprintf, fscanf).

Disadvantages

  • Buffering can be a disadvantage in real-time systems where timing is critical.
  • Consumes more memory due to buffering, which might be a concern in embedded systems.

2.2 Low-Level I/O

Low-level I/O involves system calls such as open, read, write, and close. These functions are generally part of the OS API and offer more direct control over the hardware.

Example:

#include <fcntl.h>
int fd = open("file.txt", O_RDONLY);
char buffer[100];
read(fd, buffer, 100);
close(fd);

Advantages

  • Offers more control, which is often necessary in embedded systems.
  • Generally faster for single-byte or unbuffered operations.
  • More suitable for real-time systems where exact timing of data transfer is critical.

Disadvantages

  • Less portable, as the available system calls may vary between operating systems.
  • More complex to use correctly, often requiring more error handling.

3. Standard I/O Operations

3.1 Opening Files

Before any I/O operations, you must open the file. The fopen() function is used for this purpose. It takes the filename and the mode as arguments.

FILE *fp = fopen("example.txt", "r");  // Open for reading

Here, fp is a pointer to a FILE type that will be used for subsequent I/O operations. The mode can be "r" for reading, "w" for writing, "a" for appending, among others.

3.2 Reading from Files

To read from a file, you can use functions like fread() or fgets().

char buffer[100];
fread(buffer, 1, sizeof(buffer), fp);  // syntax: fread(buffer, size_bytes, element_count, file_pointer)

Or, for reading lines:

fgets(buffer, sizeof(buffer), fp);  // syntax: fgets(buffer, buffer_size, file_pointer)

3.3 Writing to Files

Writing is similar to reading and can be done with functions like fwrite() or fprintf().

char data[] = "Hello, World!";
fwrite(data, 1, sizeof(data), fp);  // syntax: fwrite(data, size_bytes, element_count, file_pointer)

Or, for formatted writing:

fprintf(fp, "The answer is %d\n", 42);  // syntax: fprintf(file_pointer, format_string, data)

3.4 Closing Files

After you're done with a file, it's good practice to close it using fclose().

fclose(fp);  // Close the file

Closing files is especially important in embedded systems where resources are limited. Failing to close a file can lead to memory leaks and other unexpected behavior.

3.5 Formatted I/O

Formatted I/O is a powerful feature of Standard I/O that allows you to read and write data in a formatted manner. This is particularly useful for parsing configuration files or writing log files.

Reading Formatted Data

The fscanf function allows you to read formatted data from a file. For example, you can read a string and an integer from a file like this:

FILE *fp = fopen("file.txt", "r");
if (fp == NULL) {
    // Handle error
    return 1;
}

char name[50];
int age;
fscanf(fp, "Hello, %s. You are %d years old.", name, &age);

printf("Name: %s, Age: %d\n", name, age);
fclose(fp);

Suppose file.txt contains "Hello, Alice. You are 30 years old." This code would read that data into the name and age variables, and then print "Name: Alice, Age: 30".

Writing Formatted Data

The fprintf function allows you to write formatted data to a file. For example, you can write a string and an integer to a file like this:

FILE *fp = fopen("file.txt", "w");
if (fp == NULL) {
    // Handle error
    return 1;
}

fprintf(fp, "Hello, %s. You are %d years old.\n", "Alice", 30);
fclose(fp);

In this example, the text "Hello, Alice. You are 30 years old." would be written to file.txt.


4. Low-Level I/O Operations

Low-level I/O operations are closer to the system hardware and often bypass standard library buffering. These are especially relevant in embedded systems where direct control over I/O is essential. The system calls for these operations are usually open, read, write, and close.

4.0 Including the Header

To use low-level I/O, you'll need to include the <fcntl.h> header, which stands for "file control". This header contains the necessary definitions for the system calls.

#include <fcntl.h>

4.1 Opening Files

Use the open system call to open a file. This returns a file descriptor, an integer used to identify the opened file.

int fd = open("example.txt", O_RDONLY);  // Open for reading

The flags for opening modes include O_RDONLY for read-only, O_WRONLY for write-only, and O_RDWR for read-write, among others.

4.2 Reading from Files

For low-level reading, use the read system call:

ssize_t bytesRead;
char buffer[100];
bytesRead = read(fd, buffer, sizeof(buffer));

Here, bytesRead will contain the number of bytes actually read, which may be less than sizeof(buffer).

4.3 Writing to Files

Low-level writing can be done using the write system call.

ssize_t bytesWritten;
char data[] = "Hello, World!";
bytesWritten = write(fd, data, sizeof(data));

bytesWritten will contain the number of bytes that were actually written, which could be less than the requested number.

4.4 Closing Files

Closing files in low-level I/O is done using the close system call.

close(fd);  // Close the file

Similar to high-level I/O, it's crucial to close files when they are no longer needed, especially in resource-constrained environments like embedded systems.


5. File Modes

File modes dictate how a file will be accessed, such as read-only, write-only, or both. It's also crucial to distinguish between text and binary modes.

5.1 Text vs. Binary

In text mode, the system performs certain translations (like '\n' to '\r\n' on Windows). In binary mode, no such translations are performed; data is read/written as-is.

FILE *fp1 = fopen("text.txt", "rt");  // Text mode
FILE *fp2 = fopen("data.bin", "rb");  // Binary mode

5.2 fopen Modes

Mode Access Type Precondition File Pointer Position Preserves Content?
r Read-only File must exist Beginning Yes
w Write-only Creates new or truncates existing file Beginning No
a Append Creates file if it doesn't exist End Yes
r+ Read and write File must exist Beginning Yes
w+ Read and write Creates new or truncates existing file Beginning No
a+ Read and write Creates file if it doesn't exist End for writing, anywhere for reading Yes

Note: Suffix a b for binary modes, like rb or wb.


6. Buffering

Buffering techniques can optimize I/O operations but also bring complexity that needs careful management.

6.1 Buffered vs. Unbuffered I/O

  • Buffered I/O: Temporarily stores data in a buffer before reading or writing to the system. This minimizes system calls, which are often expensive, thereby improving performance.

  • Unbuffered I/O: Performs I/O operations directly, without any temporary storage buffer. This gives you greater control but may be slower due to frequent system calls.

6.2 Customizing Buffer Size

In C's Standard Library, you can use setbuf or setvbuf to set the buffer size for a file stream.

  • setbuf(FILE *fp, char *buf): Automatically sets the buffer size to BUFSIZ, which is a system-defined constant.

  • setvbuf(FILE *fp, char *buf, int mode, size_t size): More flexible; allows you to specify the buffer mode (_IOFBF, _IOLBF, _IONBF) and size.

6.3 Setting Buffer to Zero

  • setvbuf(fp, NULL, _IONBF, 0): This disables buffering. All reads and writes will be unbuffered, meaning each read or write operation will directly call the underlying system functions (the low-level I/O functions we discussed earlier). This is useful for real-time systems where timing is critical.

6.4 Trade-offs

  • Speed vs. Control: Buffered I/O is generally faster but offers less control. Unbuffered I/O is slower but allows for greater precision in I/O operations.

  • Memory Usage: Using larger buffers can speed up I/O but consume more memory.

  • Data Integrity: Forgetting to flush the buffer can result in data loss or corruption, especially in cases like system crashes or program termination. Use fflush(fp) to manually flush the buffer.

6.5 Flushing the Buffer

Flushing sends the data from the buffer to its intended destination. It's particularly important when:

  • Switching from writing to reading in read/write ("r+") mode. This is because the buffer is still in write mode and needs to be flushed before reading.
  • You need to ensure that written data is physically stored.

You can flush the buffer using fflush(fp);.

In read operations, the system handles buffering more transparently, usually filling the buffer as you read. Flushing is generally not needed unless you're working with a read/write ("r+") stream and you're switching from writing back to reading. In that case, flushing ensures that the data you've written is physically stored before you continue reading from the same file stream.


7. File Positioning

File positioning functions like fseek, ftell, and rewind help you navigate within a file. These are particularly useful when you're working with large files or need random access.

7.1 fseek

The fseek function sets the file position indicator for the stream. It takes three parameters:

  • A file pointer
  • The offset (in bytes)
  • The position from where the offset is added (SEEK_SET, SEEK_CUR, or SEEK_END)
fseek(fp, 10, SEEK_SET);  // Move 10 bytes ahead from the beginning

7.2 ftell

ftell gives you the current position of the file pointer. It's often used to check the size of a file or to save a position for later.

long position = ftell(fp);  // Get current position

7.3 rewind

The rewind function moves the file pointer to the beginning of the file, essentially doing the same as fseek(fp, 0, SEEK_SET) but in a clearer manner.

rewind(fp);  // Go back to the beginning

8. File and Directory Management

Managing files and directories goes beyond just reading and writing. You might need to create, delete, or rename files and directories depending on your application's requirements. Here's how you can do it:

8.1 Creating, Deleting, and Renaming Files

Creating Files

You can create a new file by using fopen with the mode set to w. If the file doesn't exist, it'll be created.

FILE *fp = fopen("newfile.txt", "w");

Deleting Files

You can delete a file using the remove function.

int status = remove("oldfile.txt");

Renaming Files

The rename function allows you to change a file’s name.

int status = rename("oldname.txt", "newname.txt");

8.2 Working with Directories

On systems that support it, you can use functions like mkdir, rmdir, and chdir to manage directories.

Creating Directories

int status = mkdir("newdir");

Removing Directories

int status = rmdir("olddir");

Changing Directories

int status = chdir("newdir");

Note that these functions may not be available on all embedded systems, as they often require a more complete operating system. Always check your platform's documentation to see what's supported.


8. Use Cases in Embedded Systems

File I/O is not just for desktop programs; it has several important roles in embedded systems too. Below are some of the most common use cases.

8.1 Configuration File Parsing

Embedded systems often have configuration files that define system behavior. For example, a sensor's sampling rate might be configurable via a text file on a memory card. Reading this file during system initialization allows for easier updates and customization.

// Sample code to read a config file and set system parameters
FILE *fp = fopen("config.txt", "r");
if (fp) {
    fscanf(fp, "sampling_rate=%d", &sampling_rate);
    fclose(fp);
}

8.2 Logging

Logging is crucial for debugging and monitoring system performance. In embedded systems, logs might be written to a local file system, sent over a network, or stored on an SD card.

// Sample code to log data
FILE *logFile = fopen("log.txt", "a");
if (logFile) {
    fprintf(logFile, "Temperature: %d\n", temp);
    fclose(logFile);
}

8.3 Data Storage

Local data storage is often necessary for embedded systems. For example, a digital camera will store pictures on an SD card, or a data logger may store sensor readings for later analysis.

// Sample code to store sensor data
FILE *dataFile = fopen("data.bin", "wb");
if (dataFile) {
    fwrite(sensor_data, sizeof(sensor_data), 1, dataFile);
    fclose(dataFile);
}

9. Error Handling

9.1 errno

The global variable errno is set by system calls and some library functions in the event of an error to indicate what went wrong.

FILE *fp = fopen("file.txt", "r");
if (fp == NULL) {
    printf("Error: %d\n", errno);
}

9.2 perror and strerror

perror() and strerror() are useful functions for printing the error message corresponding to errno.

if (fp == NULL) {
    perror("Failed to open file");
    // OR
    printf("Failed to open file: %s\n", strerror(errno));
}

10. Optimization Techniques

Optimizing File I/O can lead to noticeable performance gains in embedded systems.

10.1 Minimizing Disk Access

Minimizing disk access can speed up I/O operations. For example, reading or writing data in chunks is generally faster than one byte at a time.

char buffer[1024];
fread(buffer, sizeof(char), 1024, fp);

However, this may not be possible in all cases, such as when you're working with a serial port or a network socket. Also, fread is already buffered, so you may not see much of a performance gain here.

10.2 Efficient Buffering

Using buffering effectively can minimize the I/O function call overhead. You can set the buffer size according to your needs using setvbuf().

setvbuf(fp, buffer, _IOFBF, sizeof(buffer));

11. Best Practices

11.1 File Closing and Resource Management

Always close files when you are done with them to free up system resources.

if (fp) {
    fclose(fp);
}

11.2 Error Checks

Always check the return values of functions and take appropriate action when something goes wrong.

if (fwrite(data, sizeof(data), 1, fp) != 1)
{
    perror("Failed to write data");
}

Here, fwrite returns the number of elements successfully written, which should be 1 in this case. If it's not, then something went wrong and you should handle the error.


12. Q&A

1. Question:
What are the primary differences between standard I/O functions (like fopen, fprintf) and low-level I/O functions?

Answer:
Standard I/O functions are part of the C library and provide buffered access to files and devices, making them efficient for multiple small I/O operations. They also handle data conversion (e.g., converting integers to text and vice versa).
Low-level I/O functions, on the other hand, directly interact with the operating system's I/O services without buffering or data conversion. They work with file descriptors instead of file pointers and include functions like open, read, and write.


2. Question:
How can one set a file to be in binary mode using the standard I/O functions in C?

Answer:
When opening a file using fopen, you can append a b to the mode string. For example, to open a file for binary reading, you'd use fopen(filename, "rb").


3. Question:
In the context of file operations, what does "buffering" mean?

Answer:
Buffering refers to the practice of temporarily storing data in memory (the buffer) before writing it to a file or after reading it from a file. This helps improve I/O performance, as accessing memory is generally faster than performing direct I/O operations.


4. Question:
What's the purpose of the fflush function in file operations, and when should you use it?

Answer:
fflush is used to force the buffer to be written to the actual file or device. This is useful when you want to ensure that all buffered data has been committed, for instance, before reading from a file that you've just written to or before closing a file.


5. Question:
What function would you use to determine the current position of the file pointer in a file?

Answer:
You can use the ftell function, which returns the current position of the file pointer relative to the beginning of the file.


6. Question:
How would you open a file in "append" mode using standard I/O functions, and why might this mode be useful?

Answer:
You'd use fopen(filename, "a") to open a file in append mode. This mode is useful because any data written to the file is added to its end, without overwriting any existing data. It's handy for logging and situations where you continuously add data to a file.


7. Question:
Explain the difference between fgetc and fread when reading from a file.

Answer:
fgetc reads a single character from the given file stream, whereas fread can read multiple bytes (or elements) in a single call. While fgetc is generally used for text files, fread is more versatile and can be used for both text and binary files.


8. Question:
If you're working on an embedded system with limited memory, what are some best practices to follow when working with File I/O?

Answer:
- Use buffering judiciously: While buffering can speed up I/O operations, it can also consume precious memory. Adjust buffer sizes as per the system's memory constraints. - Close files promptly after use to free up system resources. - Avoid keeping multiple files open simultaneously if not necessary. - When reading or writing large files, process them in chunks to avoid memory exhaustion. - Monitor file operations for errors, as limited resources can lead to I/O issues.


9. Question:
In the context of embedded systems, why might you prefer low-level I/O functions over standard I/O?

Answer:
Low-level I/O functions offer more direct control over file operations, which can be crucial in resource-constrained environments like embedded systems. They also skip the overhead of buffering and data conversion, which can save memory and processing time.


10. Question:
When might file positioning functions like fseek and ftell be particularly useful in embedded systems?

Answer:
File positioning functions are valuable in scenarios where random file access is needed, like when updating specific records in a database file or accessing configuration settings located at known offsets. This can be especially handy in embedded systems where full-file operations might be resource-intensive.


11. Question:
When might using text mode ("t") during file operations introduce potential issues, especially when transferring files between different systems?

Answer:
Text mode can introduce issues during file operations when transferring files between systems with different newline conventions, such as Windows (\r\n) and UNIX/Linux (\n). Reading a file in text mode on a system might interpret or convert the newline characters differently than they were written, which could corrupt the file or change its content.


12. Question:
How would you implement a function to check if a file exists in the file system without actually opening it?

Answer:
You could use the stat or access function. While stat provides detailed file information, access simply checks for the file's presence and permissions. Both avoid opening the file.


13. Question:
In the context of embedded systems, why might memory-mapped file I/O be advantageous compared to standard file I/O functions?

Answer:
Memory-mapped file I/O allows a file to be treated as if it were in memory. This facilitates faster data access since it bypasses the standard system calls and buffering. It's particularly beneficial in embedded systems for quick data manipulation and when working with large files.


14. Question:
Describe a scenario where the use of the fsetpos and fgetpos functions might be preferred over fseek and ftell.

Answer:
fsetpos and fgetpos deal with the fpos_t data type, which may store more than just the file position (e.g., state information for multi-byte character parsing). They're preferred in environments where the file position exceeds what a long int (used by fseek and ftell) can represent or when specific state info must be preserved.


15. Question:
How can one detect an end-of-file condition when using the fread function?

Answer:
While fread returns the number of items successfully read, it doesn't directly indicate an end-of-file. To detect EOF, you can check the feof function after calling fread. If feof returns a non-zero value, it means the end of the file has been reached.


16. Question:
What could go wrong if you don't close a file after operations?

Answer:
Not closing a file can lead to: - Memory leaks. - File corruption due to incomplete buffer writes. - Exhaustion of file descriptors, limiting further file operations. - Undefined behavior if the file is accessed by other processes or tasks.


17. Question:
Can the fprintf function be used to write binary data? Why or why not?

Answer:
While fprintf is primarily for formatted text output, it's possible to write binary data using specific format specifiers. However, it's not the best choice due to potential character conversions, especially newline characters. For binary data, functions like fwrite are more appropriate.


18. Question:
Imagine an embedded system with non-volatile storage that undergoes frequent power losses. What file I/O considerations should you have?

Answer:
In such scenarios, considerations might include: - Ensuring atomic writes or using a journaling system to avoid file corruption. - Regularly flushing buffers with fflush. - Implementing wear-leveling techniques if the storage is flash memory to prolong lifespan. - Avoiding frequent write operations, which can wear out the storage.


19. Question:
How can you ensure that two tasks or threads don't simultaneously access a file, leading to data corruption?

Answer:
You can use synchronization mechanisms like mutexes (mutual exclusion) or semaphores to ensure that only one task or thread accesses the file at a time, preventing concurrent writes or reads.


20. Question:
If you need to read a line from a file but don't know its length in advance, how can you handle this situation, especially in a memory-constrained embedded system?

Answer:
You can use functions like fgets that read up to a specified number of characters or until a newline or EOF. In a memory-constrained system, you'd read the line in chunks, process each chunk, and then continue reading, rather than trying to load the entire line into memory.


21. Question:
How would you prevent a file from being accessed simultaneously by multiple functions?

Answer:
Use a lock mechanism, like a mutex, to ensure exclusive access to the file.


22. Question:
Why might you prefer fread and fwrite over fgetc and fputc in an embedded environment?

Answer:
fread and fwrite allow for bulk transfer of data, making them more efficient than byte-wise operations like fgetc and fputc.


23. Question:
In what scenario would you use the fsync function after writing to a file?

Answer:
Use fsync when you need to ensure that written data is flushed to the storage device immediately, providing data durability.


24. Question:
How would you handle errors when a call to fopen fails?

Answer:
Check the global errno variable, and use functions like perror or strerror to obtain a description of the error.


25. Question:
What is the significance of the volatile keyword when dealing with file operations in embedded systems?

Answer:
While volatile is not directly related to file operations, in embedded systems, it ensures that the compiler doesn't optimize away successive reads or writes to a variable. This is crucial when dealing with memory-mapped I/O or hardware registers, but less so with standard file operations.


26. Question:
How does buffering impact file I/O operations, and how can you control it?

Answer:
Buffering can improve I/O performance by reducing the number of system calls. The setbuf and setvbuf functions allow you to control buffering behavior.


27. Question:
Describe a scenario where you would need to use the ftell and fseek functions.

Answer:
When randomly accessing positions within a file, such as updating specific records in a binary file, ftell gives the current position, and fseek moves to a desired position.


28. Question:
What are the differences between fopen modes "a+" and "r+"?

Answer:
"a+" opens the file for reading and appending; it creates the file if it doesn't exist. "r+" opens the file for reading and writing, but it doesn't create a new file if one doesn't exist.


29. Question:
Why is direct memory access (DMA) sometimes used in conjunction with file I/O in embedded systems?

Answer:
DMA allows data transfer between memory and peripherals without CPU intervention, which can greatly speed up file I/O operations in embedded systems.


30. Question:
How would you differentiate between end-of-file and an error using the fgetc function?

Answer:
Both situations return EOF. To differentiate, after receiving EOF, check feof (end-of-file condition) and ferror (error condition) functions.


41. Question:
What's the mistake in the following code?

FILE *fp = fopen("file.txt", "r");
char content[50];
if (fp != NULL) {
    fread(content, sizeof(char), 50, fp);
}
fclose(fp);

Answer:
The fclose(fp) should be inside the if statement, after the fread. If fopen fails, attempting to close an invalid file pointer can lead to undefined behavior.


42. Question:
Identify the error in this code snippet:

FILE *fp = fopen("data.txt", "w");
if (fp) {
    fputs("Sample data", fp);
}
fclose(fp);
fp = NULL;
fputs("More data", fp);

Answer:
There's an attempt to write to a NULL file pointer using fputs at the end. This will result in undefined behavior.


43. Question:
Spot the flaw here:

FILE *fp = fopen("notes.txt", "r");
if (fp) {
    char c = fgetc(fp);
    while (c != EOF) {
        // process c
        c = fgetc(fp);
    }
}
fclose(fp);

Answer:
The variable c should be of type int to correctly handle the EOF condition. Using char might result in an infinite loop if EOF corresponds to a valid char value.


44. Question:
What's wrong with the following code?

FILE *fp = fopen("output.txt", "w");
for (int i = 0; i < 100; i++) {
    fprintf(fp, "%d\n", i);
    fclose(fp);
    fp = fopen("output.txt", "w");
}

Answer:
The file is being opened and closed inside the loop in write mode. This will overwrite the file on each iteration, resulting in only the last number (99) being written.


45. Question:
Identify the issue in this code:

char *filename = "data.txt";
FILE *fp = fopen(filename, "w");
filename[0] = 'b';
fputs("Sample content", fp);
fclose(fp);

Answer:
The filename string is modified after opening the file. While it doesn't cause an error with the current operations, it's a risky practice that can lead to confusion and potential bugs in more complex scenarios.


46. Question:
Spot the problem in this snippet:

FILE *fp;
fp = fopen("log.txt", "a");
fprintf(fp, "Log entry");

Answer:
The file is never closed using fclose(fp). This can lead to resource leaks and potential data loss if the buffer isn't flushed.


47. Question:
What's the flaw in the following?

FILE *fp = fopen("records.txt", "w");
char data[100];
fgets(data, 100, stdin);
fwrite(data, sizeof(char), strlen(data), fp);
fclose(fp);

Answer:
There's no check to see if fopen was successful. Before writing to fp, there should be a check like if(fp != NULL) to ensure it's valid.


48. Question:
Given this code, identify the mistake:

FILE *fp = fopen("sample.txt", "r");
char buffer[10];
fread(buffer, sizeof(char), 20, fp);
fclose(fp);

Answer:
The fread function tries to read 20 characters into a buffer that can only hold 10. This results in a buffer overflow.


49. Question:
Examine the following:

FILE *fp1 = fopen("A.txt", "r");
FILE *fp2 = fopen("A.txt", "w");
char c;
while((c = fgetc(fp1)) != EOF) {
    fputc(c, fp2);
}
fclose(fp1);
fclose(fp2);

Answer:
The file "A.txt" is being opened simultaneously for reading and writing. This can cause undefined behavior, especially since reading and writing operations are interleaved.


50. Question:
What's the problem with the following code?

FILE *fp = fopen("file.txt", "r+");
if (fp) {
    fputs("Hello, World!", fp);
    rewind(fp);
    char data[50];
    fgets(data, 50, fp);
}
fclose(fp);

Answer:
The code writes "Hello, World!" and then immediately tries to read it back. However, the internal buffer might not have been flushed before the read operation, so fgets might not retrieve the recently written data. Using fflush(fp) after writing would ensure the data is written to the file before reading it back.