Fstream and introduction to file handling

Revision Session: From Stream Objects to File Handling in C++

Dear Readers,

After a deep dive into the hardware aspects of file handling which was discussed on July 28, 2023, in their C++ lab session, Sam and Alex had taken a break.  Now, they return to compy to continue their discussion of what took place on July 28, 2023, in their lab session..


Alex: Okay, we're back from the break. Ready to dive back in, Sam?

Sam: Yeah, I am. But I do need a little refresher on those modes for handling files that Compy mentioned: write, read, and append.

Compy: Of course, Sam. There are three basic file operations: read ('r'), write ('w'), and append ('a'). Let's take a look at each of them.

 

1. Write Mode

The 'w' mode creates an empty file for output operations. If a file with the same name already exists, its contents are discarded and the file is treated as a new empty file.

 

Here's an example:

#include <fstream>
int main() {
std::ofstream myfile;
myfile.open ("example.txt");
myfile << "Writing this to a file.\n";
myfile.close();
return 0;
}

This program opens a file called "example.txt" and writes a line of text to it. If "example.txt" already exists, its content will be erased before the new text is written.

 

2. Read Mode

The 'r' mode opens a file for reading. The file must exist.

 

Here's a short example:

#include <iostream>
#include <fstream>
#include <string>

int main() {
std::string line;
std::ifstream myfile ("example.txt");
if (myfile.is_open())
{
while ( getline (myfile,line) )
{
std::cout << line << '\n';
}
myfile.close();
}
else std::cout << "Unable to open file";
return 0;
}

This program reads the file "example.txt" and prints its content to the console.

 

3. Append Mode

The 'a' mode opens a file to append data to it. If the file does not exist, it will be created. The data is added to the end of the file.

 

Here's a small example:

#include <fstream>
int main() {
std::ofstream myfile;
myfile.open ("example.txt", std::ios_base::app);
myfile << "Appending this to the file.\n";
myfile.close();
return 0;
}

This program opens "example.txt" and appends a line of text to it. If "example.txt" does not exist, it will be created.

Sam: Thanks, Compy! Those examples are really helpful.

Alex: I agree, they really illustrate how those modes work.

Compy: I'm glad you find them helpful! Remember, understanding how these file operations work is crucial for handling data in C++. If you have any other questions, don't hesitate to ask.

Alex: Actually, Compy, I do have a question. What would happen if we try to read a file that doesn't exist, or write to a file but run out of storage space?

Compy: Those are great questions, Alex. If you try to open a file for reading (with the 'r' mode) and it does not exist, the open operation will fail. You can check whether it has succeeded by calling the is_open method on the file stream object. Here's a sample code snippet:

#include <fstream>
#include <iostream>

int main() {
std::ifstream file("nonexistent.txt");
if (!file.is_open()) {
std::cerr << "Error: could not open file\n";
return 1;
}
// read from file...
return 0;
}

The program attempts to open a file that does not exist. Since the file does not exist, is_open returns false and an error message is written to the standard error stream.

As for your second question, if you try to write to a file and the device is out of storage space, the output operation will fail. In C++, this causes the stream's badbit to be set, which you can check using the bad method:

#include <fstream>
#include <iostream>

int main() {
std::ofstream file("out.txt");
file << "Hello, world!";
if (file.bad()) {
std::cerr << "Error: write operation failed\n";
return 1;
}
return 0;
}

In this code, we attempt to write a string to a file. If the write operation fails, bad returns true and we print an error message to the standard error stream.

Sam: Thanks, Compy. This makes handling files in C++ much clearer to me.

Compy: I'm glad to hear that, Sam. It's also important to note that whenever you're done with a file, you should close it using the close method. This will free up any system resources that were being used by the file stream. Here's an example:

#include <fstream>

int main() {
std::ofstream file("out.txt");
file << "Hello, world!";
file.close();
return 0;
}

Alex: Got it, always remember to close the file. Thanks for the tips, Compy!

Compy: You're welcome, Alex! It's always important to manage resources properly in a program, including files.

Sam: Speaking of managing resources, I remember our professor mentioning something about file buffers. What's that all about?

Compy: Ah, file buffering. When data is read from or written to a file, it often passes through a buffer first. A buffer is a block of memory that acts as a temporary holding area for data. It can improve efficiency because accessing memory is much faster than accessing a file on disk. In C++, stream objects automatically handle buffering for you.

Alex: So, buffering means storing data temporarily in memory before sending it off to its destination or before it’s processed. Is that why sometimes there’s a delay in the display of the characters I type?

Compy: Exactly, Alex! In fact, the keyboard itself has a hardware buffer where the keystrokes are stored temporarily before they're sent to the operating system. It's a bit like a short-term memory for your keystrokes!

Sam: That's really fascinating. So if I understand correctly, buffering can be used in various scenarios not just in file I/O but also in keyboard inputs, network data transfer and so on?

Compy: Absolutely, Sam! Buffering is a common technique used to manage data efficiently in computer systems. It's all about optimizing the balance between speed and memory usage.

Alex: I think I'm getting a good grip on this. So, we use 'fstream' for file handling, it has functions for reading, writing and appending data to files. We have buffers to make this process more efficient. But, what about reading or writing data at a specific position in the file?

Compy: That's a great question, Alex! For that, we use file pointers. File pointers allow us to read or write data at any location in the file, not just at the beginning or the end. Would you like to see an example of how it works?

Sam: Yes, please! An example would be really helpful.

Compy: Alright, here's a simple example. Let's say we have a file and we want to add some text right in the middle. Here's how we could do it:


#include <iostream>
#include <fstream>
using namespace std;

int main() {
fstream file;
file.open("example.txt", ios::out | ios::in);

// Check if file opened successfully
if(!file) {
    cout << "Error in opening the file...\n";
    return 0;
}

// Position the put (write) pointer to the 5th byte in file
file.seekp(5);

file << "New Text";

// Close the file
file.close();
return 0;
}

Alex: That's interesting. So, using 'seekp' we can move the write pointer to any location in the file. What about the read pointer?

Compy: That's correct, Alex! And yes, we can also move the read pointer using the 'seekg' function. This allows us to read from any specific location in the file.

Sam: This opens up a lot of possibilities. Like, we can create a file that works like a database, with records stored at specific positions. Right?

Compy: Yes, Sam! That's an excellent point. File handling in C++ is really powerful and can be used to build simple databases, log files, configuration files, and much more.

Compy: Absolutely, Sam. Now, let's move on to the 'getline()' function. This function is used to read a string or a line from an input stream. It is often used with 'cin' to read user input, but it can also be used with file streams to read data from a file. Here's an example:


#include <iostream>
#include <fstream>
#include <string>
using namespace std;

int main() {
fstream file;
file.open("example.txt", ios::in);

// Check if file opened successfully
if(!file) {
    cout << "Error in opening the file...\n";
    return 0;
}

string line;
while(getline(file, line)) {
    cout << line << '\n';
}

// Close the file
file.close();
return 0;
}

Alex: Oh, I see! So, the 'getline()' function keeps reading from the file until it encounters a newline character or end of file, right?

Compy: That's correct, Alex! And as it reads, it also moves the "get" pointer forward. That's why the 'while' loop in the example doesn't go into an infinite loop. The 'getline()' function keeps reading new lines until there are no more lines to read.

Sam: Got it. This is really helpful, Compy. I think we have a much better understanding of file handling in C++ now. Thanks!

Compy: You're welcome, Sam! I'm glad I could help. And remember, practice is key when it comes to programming. So, make sure to write lots of code!

Sam: Compy, we discussed about the file handling and get pointer, but I've been wondering. You mentioned earlier that cin, cout, and cerr are objects of the stream. Can you expand on that? What exactly are these objects and what do they do?

Compy: Great question, Sam! In C++, cin, cout, cerr, and clog are predefined objects of classes istream and ostream, which are part of the iostream library. They provide the functionality to perform input and output operations. Here's a brief on what each object does:

  1. cin: It's an object of class istream and is used for taking input. We usually use the extraction operator (>>) with cin to read input.
  2. cout: It's an object of class ostream and is used for output. The insertion operator (<<) is generally used with cout to display output.
  3. cerr: It's an object of class ostream and is used for outputting error messages. It's tied to the standard error device, which is usually the display screen, but it can be redirected to point to other devices as needed.
  4. clog: It's also an object of class ostream and is used for logging errors. It's similar to cerr, but the output is buffered. This means that it only displays the output after a buffer is filled.

Alex: So, cin and cout are for general input and output, while cerr and clog are specifically for handling errors. That makes sense!

Compy: Exactly, Alex! Understanding these objects is crucial for handling I/O operations in C++.

Sam:What about the cerr and clog objects, Compy?

Compy: Absolutely! To demonstrate cerr and clog, let's write a simple C++ program. We will intentionally cause an error to see how these objects work.

#include <iostream>
using namespace std;

int main() {
int number;
cout << "Please enter an integer: ";
cin >> number;

if (cin.fail()) {
    cerr << "That was not an integer!" << endl;
    return 1;
}

cout << "You entered: " << number << endl;
return 0;
}

Compy: In this example, the program will ask for an integer input. If we enter a non-integer, cin will go into a fail state, and cin.fail() will return true. At that point, we use cerr to output an error message.

Sam: Oh, I see. So the .fail() is a method that's used to check if the previous input operation failed, right?

Compy: That's correct, Sam! Now, let's look at a simple example of clog.

#include <iostream>
using namespace std;

int main() {
int number;
cout << "Please enter an integer: ";
cin >> number;

if (cin.fail()) {
    cerr << "That was not an integer!" << endl;
    clog << "User entered a non-integer." << endl;
    return 1;
}

cout << "You entered: " << number << endl;
return 0;
}

Compy: This program is very similar to the previous one. However, this time, we're also using clog to log the error. Usually, you'd redirect the clog stream to an error log file, so you could keep track of any errors that occurred while your program was running.

Alex: Oh, I get it now. So, cerr and clog are pretty handy for debugging and logging errors!

Compy: Exactly, Alex! They're very useful tools for any C++ programmer.

Related Projects