Schedule‎ > ‎

Lesson 12: More I/O and Objects


12.1: Reading Data From Files

Learner Outcomes

At the end of the lesson the student will be able to:

  • Read data of various types from a file
  • Check for end-of-file conditions
  • Read data from a file in a loop


12.1.1: Reviewing Files and IO

  • C++ programs can use streams for input and output

    Stream: an ordered list of data delivered over time

  • A stream connects a program to an I/O object
  • Input stream: an object that provides a sequence of bytes to a program from a source

    input stream

  • Output stream: an object that accepts a sequence of bytes from a program to a source

    output stream

  • We have used streams the entire course:
    • cin: read data from stdin (keyboard)
    • cout: write data to stdout (terminal window)
  • We can read from and write to files using streams as well
  • File I/O streams are of type ifstream and ofstream
  • We set up file streams using the following steps with example file infile.txt
  • If you are using Windows, be sure to save the file in the UNIX file format with TextPad

    TextPad selecting UNIX file format

Procedure For File I/O

  1. Place the following include directives in your program file:
    #include <fstream>   // for file I/O
    #include <iostream>  // for cout
    #include <cstdlib>   // for exit()
    using namespace std;
    
  2. Declare names for input and output streams like:
    ifstream fin;
    ofstream fout;
    
  3. Connect each stream to a file using open() and check for failure:
    fin.open("infile.txt");
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }
    
    fout.open("outfile.txt");
    if (fout.fail()) {
        cout << "Output file failed to open.\n";
        exit(-1);
    }
    
  4. Read or write the data:
    • Read from a file with fin like using cin:
      double first, second, third;
      fin >> first;
      fin >> second;
      fin >> third;
      
    • Write to a file with fout like using cout:
      fout << "first = " << first << endl;
      
  5. Close the streams when finished reading and writing:
    fin.close();
    fout.close();
    

Streams are Objects

  • Notice that streams are objects and have functions associated with them
    • open(): connect a stream to a file
    • close(): disconnect a stream from a file
    • fail(): returns true if the stream has an error
    • good(): returns true if the stream is error free
  • Most of the functions are available using either file streams or cin and cout


12.1.2: Using Loops to Read Files

  • Sometimes we do not know how many data items are in a file
  • To solve this, the typical approach is to use a loop to process the file
  • We read from the file until we reach the end of the file or encounter an error
  • The basic IO stream library has several functions we may call

Commonly Used IO Stream Functions for Error Detection

NameDescription
eofReturns true if the stream has reached end-of-file.
failReturns true if an error has occurred on the stream, including reaching the end of file.
goodReturns true if the most recent I/O operation on the stream completed successfully.
badReturns true if a non-recoverable error has occurred on the stream.

Testing for End of File with Functions

  • We may use the IO stream functions as a loop condition to test for the end of a file like:
    double num;
    while (fin.good()) {
        fin >> num;
        if (fin.good()) {
            cout << num << endl;
        }
    }
    
  • In the above example, we call the good() function to test we have not encountered an error
  • Encountering an error includes reaching the end of a file
  • It is possible to encounter an error during a read operation like fin >> num;
  • The if-statement tests we have not encountered an error in the prior read operation
  • The if-statement prevents the display of an incorrect value

Testing for End of File with the >> Operator

  • We learned in lesson 5.3.7 that cin returns true on success and false on error
  • This property makes it possible to use streams inside test conditions like:
    double input;
    while (cin >> input) {
        // process the input
    }
    
  • If the stream fails or closes the stream returns false and the loop ends
  • We can apply this same technique to file streams like:
    ifstream fin;
    // open stream and test for failure code omitted
    double input;
    while (fin >> input) {
        // process the input
    }
    
  • Since we are testing for failure after reading data, we do not need an extra if-statement as when testing with functions
  • In the following example we read from infile.txt

Example Program Reading a File Using a Loop and the >> Operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <fstream>   // for file I/O
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
    ifstream fin;
    fin.open("infile.txt");
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }

    double nextNum, sum = 0;
    int count = 0;
    while (fin >> nextNum) {
        cout << "Read: " << nextNum << endl;
        sum = sum + nextNum;
        count++;
    }
    cout << "average = " << (sum / count) << endl;
    fin.close();

    return 0;
}

12.1.2a Try It: Read a File with a Loop (5m)

  1. Copy the following program into a text editor, save it as readwrite.cpp, and then compile and run the starter program to make sure you copied it correctly.
    #include <iostream>
    #include <fstream>   // for file I/O
    #include <cstdlib>   // for exit()
    using namespace std;
    
    int main() {
        // Enter code here
    
        return 0;
    }
    
  2. Save the file rawdata.txt to the same directory as your program source code.

    Make sure that there is one blank line at the end of the file to avoid eof complexities.  We will read from this file after writing our program.

  3. Add a function with the following prototype to your program.
    void readData();
    
  4. In main(), add code to call the new function. Compile your code to verify you added the function and function call correctly.
  5. In readData(), add code to declare an input stream named fin and to connect the stream to the input file "rawdata.txt". In addition, make sure you check for failure after calling open().

    For more information see the section Procedure For File I/O.

  6. First we read from the file using the following loop code:
    double nextNum;
    while (fin.good()) {
        fin >> nextNum;
        if (fin.good()) {
            cout << "Read: " << nextNum << endl;
        }
    }
    
  7. Add a statement after the above to close the input stream.
  8. Compile and run your code, then verify you see output like the following:
  9. Read: 12.94
    Read: -9.87654
    Read: 2.3131
    Read: -89.506
    Read: 12.3333
    Read: 92.8765
    Read: -123.457

    If you have problems, ask a classmate or the instructor for help as needed.

  10. Why didn't the final number -123.457 in rawdata.txt get read and printed out?
  11. Now change the loop code to read using the >> operator inside the test condition:
    while (fin >> nextNum) {
        cout << "Read: " << nextNum << endl;
    }
    

    If you have problems, ask a classmate or the instructor for help as needed.

  12. The output should now look like this.  Why does the final number -123.457 print out now?

    Read: 12.94
    Read: -9.87654
    Read: 2.3131
    Read: -89.506
    Read: 12.3333
    Read: 92.8765
    Read: -123.457
  13. Save your source code file as we will be adding to it in the next Try It.
  14. When finished, please help those around you. Then be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. When the number of file data items is unknown you can use a ________ statement to read all the data.
  2. When successfully reading data using fin >> nextValue, the input stream returns a value interpreted as ________.
  3. The problem with the following code is ________.
    double total = 0;
    while (fin >> nextValue) {
        total += nextValue;
    }
    
    1. you cannot read data in a test condition
    2. nextValue should be nextNum
    3. there is no way to exit the loop
    4. nothing


12.1.3: Reading Files using getline()

  • We can read text using a loop and an input stream like fin:
    ifstream fin;
    // open stream and test for failure code omitted
    string word;
    while (fin >> word) {
        cout << word << endl;
    }
    
  • However, just like with cin, there are complications when you want to read text with spaces
  • Operator >> skips whitespace, reads characters and stops when encountering more whitespace
  • Thus, we only get a single word for each input variable
  • If we want to read a complete line of text like "Hello Mom!", we need to use getline()
  • For example:
    ifstream fin;
    // open stream and test for failure code omitted
    string line;
    while(fin.good()) {
        getline(fin, line);
        if (fin.good()) {
            cout << line << endl;
        }
    }
    

Using Loops with getline()

  • Function getline() returns true on success and false on error
  • This property makes it possible to use getline() with streams inside test conditions like:
    ifstream fin;
    // open stream and test for failure code omitted
    string line;
    while (getline(fin, line)) {
        // process input
    }
    
  • Since we are testing for failure after reading data, we do not need an extra if-statement as when testing with functions
  • The following example uses getline() to read lines from a file

Example Program Reading a File Using getline() in a Loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <fstream>   // for file I/O
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
    ifstream fin;
    fin.open("rawdata.txt");
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }

    string line;
    int count = 1;
    while(getline(fin, line)) {
        cout << "Line " << count << ": " << line << endl;
        count++;
    }

    fin.close();

    return 0;
}

Check Yourself

  1. True or false: Using the >> operator with string variables only reads one word from a file stream at a time.
  2. To read strings containing multiple words use the ________ function.
  3. True or false: the following code reads input one word at a time from the file input stream named fin.
    string str;
    while (fin >> str) {
       cout << str << endl;
    }
    
  4. True or false: the following code reads input one line at a time from the file input stream named fin.
    string str;
    while (getline(fin, str)) {
       cout << str << endl;
    }
    
  5. True or false: when all data has been read from the file associated with fin, both of the above loops end.


12.1.4: Reading Mixed Data Types

  • Sometimes we need to read a mix of text and numerical data from a file like products2.txt
    Milk
    3.95
    Whole-wheat bread
    2.99
    Cheddar cheese
    3.95
    
  • To read this mix of data types we need to use both getline() and the >> operator
  • Recall that getline() stops reading when it encounters a '\n'
  • By contrast, fin >> variable operates as follows:
    1. Skips whitespace
    2. Reads characters
    3. Stops reading when whitespace is found
  • Thus if we mix fin >> variable followed by getline(fin, line), we get mysterious results
  • Just like with cin, we get around this problem you by using fin >> ws; before using getline()
  • The following example reads mixed data types in a loop from products2.txt

Example Program Reading Mixed Data Types in a Loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <fstream>   // for file I/O
#include <iostream>  // for cout
#include <cstdlib>   // for exit()
using namespace std;

int main() {
    string name;
    double price;
    ifstream fin("products2.txt");
    if (fin.fail())
    {
        cout << "Input file failed to open.\n";
        exit(-1);
    }

    while (fin.good())
    {
        fin >> ws; // clear whitespace including newlines
        getline(fin, name);
        fin >> price;
        if (fin.good()) {
            cout << name << " @ " << price << endl;
        }
    }
    fin.close();

    return 0;
}

Check Yourself

  1. True or false: before you switch from using the >> operator to using getline(), you must clear all newline characters from the input buffer.
  2. To clear all whitespace, including newlines, from the input buffer use: ________.


12.1.5: Reading File Data into a Vector

  • Sometimes you want to process the data in a file several times
  • One way to do this is to load the data into a vector
  • Then you can process the data as a list
  • Processing a list multiple times is faster than reading from a file multiple times
  • The following example shows how to read from a file into a vector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <fstream>
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;

int main() {
    ifstream fin;
    fin.open("infile.txt");
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }

    // Load data into a vector
    vector<int> data;
    int value;
    while(fin >> value) {
        cout << "Read: " << value << endl;
        data.push_back(value);
    }
    fin.close();

    // Process vector data
    double sum = 0;
    int count = data.size();
    for (int i = 0; i < count; i++) {
        sum = sum + data[i];
    }

    cout << "average = " << (sum / count) << endl;

    return 0;
}

12.1.5a Try It: Load Data into a Vector (6m)

  1. Open your readwrite.cpp from the last Try It.  If you don't have it,  use THIS
  2. Include the following library at the top of the program source code file:
    #include <vector>
    
  3. Inside the readData() function parenthesis, add the following parameter:
    void readData(vector<double>& data);
    
  4. In main() before the function call, declare a vector of type double named data. Then add the data variable as an argument to the readData() function like:
    readData(data);
    
  5. Inside the while-loop braces { }, add the following statement to save the values read from the input file into the vector.
    data.push_back(nextNum);
    
  6. At the end of readData(), add a statement to close the input file.

    For more information see the section Procedure For File I/O step 5.

  7. In main() after the function call, add the following cout statement:
    cout << "Vector data:\n";
    
  8. After the above statement, add a for-loop to display all the elements of the vector.
  9. Compile and run your code to verify it works correctly. When run, you should see the data from the file displayed twice like:
    Read: 12.34
    Read: -9.87654
    Read: 2.3131
    Read: -89.506
    Read: 12.3333
    Read: 92.8765
    Read: -123.457
    Vector data:
    12.34
    -9.87654
    2.3131
    -89.506
    12.3333
    92.8765
    -123.457
    

    If you have problems, ask a classmate or the instructor for help as needed.

  10. Save your source code file as we will be adding to it in the next exercise.
  11. When finished, please help those around you. Then be prepared to answer the following Check Yourself questions when called upon.

Check Yourself

  1. When you need to process the data in a file several times, it is faster to load the data into a(n) ________.
  2. To add data to the end of a vector, call the vector function ________.
  3. To discover how many data items were read from a file into a vector, call the vector function ________.


12.1.6: Writing File Data from a Vector

  • When we write from a vector to a file, we know in advance how many items to write
  • We typically write the number of elements that are in the vector
  • This foreknowledge makes a for-loop a better loop choice
  • The following example shows how to write a vector to an output file using a for-loop
  • The following example has a vector initializer list so add the following extra option when compiling
    g++ -Wall -Wextra -Wpedantic -std=c++11 -o programName sourceFile.cpp
    

Example Program Writing a Vector to a File  (vector.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>  // for cout
#include <iomanip>   // for setprecision
#include <fstream>   // for file I/O
#include <cstdlib>   // for exit()
#include <vector>
using namespace std;

void writeData(const vector<double>& data) {
    ofstream fout;
    fout.open("outfile.txt");
    if (fout.fail()) {
        cout << "Output file failed to open.\n";
        exit(-1);
    }
    fout << fixed << setprecision(2);
    for (unsigned i = 0; i < data.size(); i++) {
        fout << setw(10) << right << data[i] << endl;
    }
    fout.close();
}

int main() {
  
    vector<double> data = { 12.94, -9.87654, 2.3131, -89.506, 12.9333,
        92.8765, -123.457, 42 };
    writeData(data);
    cout << "Done writing data to outfile.txt...\n";

    return 0;
}

Check Yourself

  1. Usually the best loop statement for writing the data from a vector to a file is the ________.
    1. for-loop
    2. while-loop
    3. do-while-loop
    4. repeat-loop
  2. When accessing every member of a vector named data, we use a for-loop with the test condition ________ function.
    1. i < data.size()
    2. i > data.size()
    3. i <= data.size()
    4. i >= data.size()
  3. For the following loop accessing a vector, the missing piece is ________.
    for (unsigned i = 0; i < data.size(); i++) {
        cout << setw(10) << right << _____ << endl;
    }
    


Exercise 12.1: Reading Files with Loops (5m)

In this exercise we explore reading all the lines of a file using a loop.

Specifications

  1. If you have not already created the readwrite.cpp file, complete the following exercises:
    1. 12.1.2a Try It: Read a File with a Loop
    2. 12.1.5a Try It: Load Data into a Vector
    3. Alternatively,  start with this file:  HERE
  2. Save your file as readwrite2.cpp
  3. Add an include for the iomanip library:
    #include <iomanip>   // for setprecision
    
  4. Add a function with the following prototype to your file.
    void writeData(const vector<double>& data);
    
  5. Call the writeData() function from main() just before return 0.
  6. In writeData(), add code to declare an output stream named fout and to connect the stream to the output file "neat.txt". In addition, make sure you check for failure after calling open().

    For more information see the section Procedure For File I/O.

  7. After opening the output file, add the following code:
    fout << fixed << setprecision(2);
    for (unsigned i = 0; i < data.size(); i++) {
        fout << setw(10) << right << data[i] << endl;
    }
    
  8. Add a statement after the above to close the output stream.
  9. Compile and run your code to verify it works correctly. When run, you should see the following data in the file neat.txt:
         12.34
         -9.88
          2.31
        -89.51
         12.33
         92.88
       -123.46
    

    If you have problems, ask a classmate or the instructor for help as needed.

  10. Submit your readwrite2.cpp to Canvas Ex 12.1


12.1.7: Summary

  • We can use the extraction operator to read data from a file:
    ifstream fin;
    // ... more code here
    double num;
    fin >> num;
    cout << "Read data: " << num << endl;
    
  • The variable fin is a stream object and works just like cin
  • Like with cin, we use the getline() function to read complete lines of text with spaces between words:
    ifstream fin;
    // ... more code here
    string line;
    getline(fin, line);
    cout << "Read data: " << line << endl;
    
  • Sometimes we do not know how many lines are in a file
  • To solve this, the typical approach is to use a loop to process the file:
    while(fin >> next) {
        // process input
    }
    
  • We can read entire lines in a loop as well:
    while(getline(fin, line)) {
        // process input
    }
    
  • In addition, we can read file data into vectors
    vector<int> data;
    int value;
    while(fin >> value) {
        data.push_back(value);
    }
    

Check Yourself

As time permits, be prepared to answer these questions. You can find more information by following the links after the question.

  1. After opening a stream named fin, what code will read the first item in the file into the variable named first? (12.1.1)
  2. What functions can be used to detect an end-of-file condition? (12.1.2)
  3. What control-flow statement do you use to read every number in a file when you do not know how many numbers are in the file? (12.1.2)
  4. What is a loop condition you can use to read every number in a file? (12.1.2)
  5. How do you code a statement to read from a file a line of text that includes spaces? (12.1.3)
  6. What statement will clear all whitespace, including newlines, from a stream buffer? (12.1.4)
  7. What is the advantage of reading file data into a vector? (12.1.5)
  8. What is a loop condition you can use to read every line in a file? (12.1.5)
  9. What is the code to write to a file from a vector? (12.1.6)


12.2: More I/O Topics

Learner Outcomes

At the end of the lesson the student will be able to:

  • Append data to files
  • Code user input as file names


12.2.1: Alternative Syntax for Open

  • It is possible to construct a stream and open a file in one step
  • Rather than:
    ofstream fout;
    fout.open("outfile.txt");
    if (fout.fail()) {
        cout << "Output file opening failed.\n";
        exit(-1);
    }
    
  • We can use:
    ofstream fout("outfile.txt");
    if (fout.fail()) {
        cout << "Output file opening failed.\n";
        exit(-1);
    }
    
  • We can do the same with input streams:
    ifstream fin("infile.txt");
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }
    

Check Yourself

  1. True or false: the following two lines of code are equivalent.
    fout.open("messages.txt");
    
    ofstream fout("messages.txt");
    
  2. Enter the code that combines the following two lines of code into one statement.
    ofstream outputStream;
    outputStream.open("myfile.txt");


12.2.2: Appending to a File

  • The standard open operation for writing begins with an empty file
  • Even if the file exists we lose all the contents
  • To prevent losing the information, we must open for appending to a file:
    ofstream fout;
    fout.open("important.txt", ios::app);
    
  • If the file does not exist then ofstream creates it
  • If the file exists then ofstream positions itself to append to the end
  • The second argument is a constant defined in class ios
  • As without appending, the two lines can be combined into one statement like:
    ofstream fout("important.txt", ios::app);
  • The following program will add a message to the file every time we run it

Example Appending to a File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <fstream>
#include <iostream>
#include <cstdlib>
using namespace std;

int main() {
    string message;
    cout << "Enter a message to add to the file: ";
    getline(cin, message);

    ofstream fout("messages.txt", ios::app);
    if (fout.fail()) {
        cout << "Output file opening failed.\n";
        exit(-1);
    }
    fout << message << endl;
    fout.close();

    cout << "See you later!\n";

    return 0;
}

Check Yourself

  1. True or false: opening a file for writing deletes the existing contents by default.
  2. The argument for appending to a file is ________.


12.2.3: File Names as Strings

  • You can use a string as a filename for your input and output streams.

Example Using a string for File Names (stringFileName.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <fstream>  // for file I/O
#include <cstdlib>  // for exit()
using namespace std;

int main() {
    string filename, line;

    cout << "Enter a file name: ";
    cin >> filename;

    ifstream fin(filename);
    if (fin.fail()) {
        cout << "Input file " << filename << "failed to open.\n";
        exit(-1);
    }

    while(getline(fin, line)) {
        cout << line << endl;
    }

    fin.close();

    return 0;
}

12.2.3a Try It: Open with a String File Name (7m)

  1. Copy the following program into a text editor, save it as filelist.cpp, and then compile and run the starter program to make sure you copied it correctly.
    #include <iostream>
    #include <fstream>   // for file I/O
    #include <cstdlib>   // for exit()
    using namespace std;
    
    int main() {
        // Enter code here
    
        return 0;
    }
    
  2. Save the file products.txt to the same directory as your program source code.  Add an empty line at the end of your products.txt file.  Set your file for UNIX line endings.

    We will read from this file after writing our program.

  3. Write a function with the following signature:
    void readFile(string filename);
    
  4. Inside the function readFile(), add code to declare an input stream named fin and to connect the stream to the input file using the string fileName parameter. Make sure you connect the stream without calling open() (see lesson 12.2.1).
  5. Add the following code to function readFile() that reads all the values from the input file.
    while (fin.good()) {
        string name;
        double price;
        fin >> ws; // clear whitespace including newlines
        getline(fin, name);
        fin >> price;
        if (fin.good()) { // verify not end-of-file
            cout << name << " @ " << price << endl;
        }
    }
    
  6. Add a statement to close the input stream.
  7. In main(), call the readFile() function with:
    readFile("products.txt");
    
  8. Compile and run your code, then verify you see output like the following:
    Milk @ 3.95
    Whole-wheat bread @ 2.99
    Cheddar Cheese @ 3.95
    

    If you have problems, ask a classmate or the instructor for help as needed.

  9. Save your source code as we will add to it in a future exercise.
  10. When finished, please help those around you.


12.2.4: Stream Parameters

  • Stream types can be formal parameters in functions
  • Here is an example of an ifstream parameter:
    string readLine(ifstream& aStream) {
        string line;
        getline(aStream, line);
        return line;
    }
    
  • Similarly, we can have ofstream parameters:
    void writeLine(ofstream& aStream, string line) {
        aStream << line << endl;
    }
    
  • Notice that both streams are call-by-reference parameters
  • The reason is that the file stream object changes as the file is processed
  • The changes must be recorded in the file stream object to keep the file synchronized properly

Example Using Stream Parameters 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>
#include <fstream>  // for file I/O
#include <cstdlib>  // for exit()
using namespace std;

/**
    Reads a line from the istream.

    @param aStream the output stream.
    @return The line of text read.
*/
string readLine(ifstream& aStream) {
    string line;
    getline(aStream, line);
    return line;
}

/**
    Writes a line to the ostream.

    @param aStream the output stream.
    @param line The string to output.
*/
void writeLine(ofstream& aStream, string line) {
    aStream << line << endl;
}

int main() {
    ifstream fin("infile.txt");
    if (fin.fail()) {
        cout << "Input file opening failed.\n";
        exit(-1);
    }

    ofstream fout("outfile.txt");
    if (fout.fail()) {
        cout << "Output file opening failed.\n";
        exit(-1);
    }

    while (fin.good()) {
        string line = readLine(fin);
        if (fin.good()) {
            writeLine(fout, line);
        }
    }

    fin.close();
    fout.close();
    cout << "Done copying file...\n";

    return 0;
}

Testing the Stream

  • Notice the use of the if-statement inside the loop
    string line = readLine(fin);
    if (fin.good()) {
        writeLine(fout, line);
    }
    
  • If the read operation reaches the end of the file, we do not want to write an extra blank line to the output file
  • We prevent this problem by testing the stream after each read operation
  • Remember that fin.good() returns true if the last I/O operation was successful

Check Yourself

  1. True or false: file streams can be passed to function parameters.
  2. True or false: file stream arguments must always to passed by reference.
  3. If an I/O operation is successful, the stream member function good() returns ________
    1. true
    2. false
    3. indeterminate
    4. nothing, good() is a void function


Exercise 12.2: Functions with Stream Parameters (5m)

In this exercise we explore passing an input stream to a function.

  1. Open filelist.cpp from the last Try It or use this one HERE
  2. Write a function with the following signature:
    void read(ifstream& fin);
    
  3. Move the following code from the inside the while-loop of readFile() into the read()function.
        string name;
        double price;
        fin >> ws; // clear whitespace including newlines
        getline(fin, name);
        fin >> price;
        if (fin.good()) { // verify not end-of-file
            cout << name << " @ " << price << endl;
        }
    
  4. Inside the while-loop of readFile(), call the read() function. When finished the while-loop of readFile() should look like this:
    while (fin.good()) {
        read(fin);
    }
    
  5. Compile and run your code to verify it still works the same. When run, you should see the data from the file displayed twice like:
    Milk @ 3.95
    Whole-wheat bread @ 2.99
    Cheddar Cheese @ 3.95
    

    If you have problems, ask a classmate or the instructor for help as needed.

  6. Submit your filelist.cpp to Canvas Ex 12.2
  7. When finished please help those around you.


12.2.5: Other File Operations

  • Some file operations are not supported by C++ streams
  • In these cases, we use the C-style functions
  • These functions are part of the cstdio library:
    #include <cstdio>
  • Note that these functions take C-string arguments
  • Thus we must use the c_str() function when using string variables

Commonly Used C-Functions for File Manipulation

FunctionDescription
remove(fileName)Deletes the file specified by the C-string fileName.
rename(oldName, newName)Changes the file or directory name specified by the C-string oldName to the newName.
perror(message)Print the error message specified by the C-string along with the system error message.
system(command)Executes a command specified by the C-string like you were using the command line.

Example Code to Remove a File

string fileName;
cout << "File to remove: ";
cin >> fileName;
int result = remove(fileName.c_str());
if (result == 0) {
    cout << "File successfully removed\n";
} else {
    perror("Error removing file\n");
}

Example Code to Rename a File

int result;
string oldName, newName;
cout << "Old file name: ";
cin >> oldName;
cout << "New file name: ";
cin >> newName;
result = rename(oldName.c_str(), newName.c_str());
if (result != 0 ) {
    perror( "Error renaming file" );
}

Example Code Using the system() Function

system("ls");     // list files
system("clear");  // clear the screen
system("cmd.exe /c color 1E"); // change colors

Changing Console Colors

  • Since we are running Cygwin on Windows, we can use Windows to change the console colors
  • The Windows command function is a program named cmd
  • You can set the console colors using the COLOR command using:
    system("cmd.exe /c color attr");
  • Where attr is TWO hex digits
    • The first specifies the background
    • The second specifies the foreground
  • Each digit can be any of the following values:
    0 = Black       8 = Gray
    1 = Blue        9 = Light Blue
    2 = Green       A = Light Green
    3 = Aqua        B = Light Aqua
    4 = Red         C = Light Red
    5 = Purple      D = Light Purple
    6 = Yellow      E = Light Yellow
    7 = White       F = Bright White
    

Check Yourself

  1. True or false: we need to use C functions for some file operations.
  2. The function to delete a file is ________.
  3. The function to rename a file is ________.


12.2.6: Summary

  • We can construct a stream and open it in one step:
    ofstream fout("messages.txt");
  • The standard open operation will create an empty file
  • We can open a file for appending data by using an extra argument:
    ofstream fout("important.txt", ios::app);
    
  • We may use string variables as the names of files
  • However, we must use the c_str() function to convert to a C-string:
    ifstream fin(filename.c_str());
    
  • Streams can be arguments to a function, but we must use call-by-reference
  • Type istream for function parameters works for both cin and ifstream
  • Type ostream for function parameters works for both cout and ofstream

Check Yourself

As time permits, be prepared to answer these questions. You can find more information by following the links after the question.

  1. How can you construct and open a file stream in one statement? (12.2.1)
  2. How do you keep a file opened for writing from destroying existing information? (12.2.2)
  3. What string function do you use to convert a string to a C-string? (12.2.3)
  4. How do you code streams as parameters for a function? (12.2.4)
  5. To delete the file named "error.log", what statement would you write? (12.2.5)
  6. To change the name of the file named "error.log" to "error.bak", what statement would you write? (12.2.5)


12.3: Working with Objects and Files

Learner Outcomes

At the end of the lesson the student will be able to:

  • Code stream parameters in functions
  • Pass objects to functions
  • Return objects from functions


12.3.1: Revisiting Objects and Classes

  • Previously we discussed how to code classes to create objects

Objects

  • Recall that an object in C++ is a location in memory containing a structured collection of variables and functions defined by its class
  • As an example, here is a "product" object in memory:
    Milk
    3.95
    functions
  • The example object has two pieces of data, a name and a price, structured one after the other in memory
  • In addition, the object has access to associated functions
  • To define the data structure of objects, we write a class

Classes

  • A class is a program-code "template" for creating objects
  • Objects are then a particular instance of a class, meaning an object has particular values
  • The particular values are stored in memory as defined by the class template
  • The data values are structured in the order defined by the class:
    class Product {
    private:
        string name;
        double price;
    ...
    }
    

Information Hiding

  • Remember that we always code our class member variables as private
  • The keyword private restricts access to member functions only
  • Keeping member variables private is important so we can make design changes

Object Interface

  • To access private data, we code public member functions like:
    public:
        string getName() const;
        double getPrice() const;
        void setName(string newName);
        void setPrice(double newPrice);
        void print() const;
    
  • These public functions are the interface to our class
  • The interface is how we communicate with and use our objects

Constructing Objects

  • To create objects from the class, we construct an object like:
    Product milk;
  • When the object is created, memory is allocated for the class variables
    Name: ""
    Price: undefined
  • However, the memory is uninitialized
  • We can use the set functions to assign the memory values:
    milk.setName("Low fat milk");
    milk.setPrice(3.95);
    
  • However, this is cumbersome and provides no guarantee that the programmer using our class will completely initialize the object data
  • A better solution is to code constructor functions

Constructor Functions

  • A constructor is a special type of function whose purpose is to initialize member variables
  • Whenever an object is created from a class, a constructor is always called automatically
  • A default constructor must set the member variables to default values:
    Product::Product() {
        name = "none";
        price = 0.0;
    }
    
  • Even though we should always code a default constructor, it is convenient to code other constructors like:
    Product::Product(string newName, double newPrice) {
        setName(newName);
        setPrice(newPrice);
    }
    
  • This lets us construct an object and initialize data members at the same time:
    Product milk("Low fat milk", 3.95);
    
  • When we are done, we have a modular grouping of variables and functions

Example Program with a Class Declaration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
using namespace std;

class Product {
public:
    Product();
    Product(string newName);
    Product(string newName, double newPrice);
    double getPrice() const;
    void setPrice(double newPrice);
    void read();
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "Unknown";
    price = 0.0;
}

Product::Product(string newName) {
    name = newName;
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    name = newName;
    price = newPrice;
}

double Product::getPrice() const {
    return price;
}

void Product::setPrice(double newPrice) {
    price = newPrice;
}

void Product::read() {
    cout << "Enter the name of the product: ";
    cin >> ws;
    getline(cin, name);
    cout << "Enter the price for a " << name << ": ";
    cin >> price;
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

// For testing
int main() {
    Product milk;
    Product bread("Rye Bread");
    bread.setPrice(2.99);
    Product cheese("Cheddar", 6.75);

    cout << "Enter the milk product data\n";
    milk.read();

    milk.print();
    bread.print();
    cheese.print();

    double price = milk.getPrice();
    cout << "The current price of milk is $" << price << endl;
    cout << "Enter the new price: ";
    double newPrice = 0;
    cin >> newPrice;
    milk.setPrice(newPrice);
    milk.print();

    return 0;
}

Check Yourself

  1. True or false: a class contains (encapsulates) both variables and functions.
  2. To allow only member functions and constructors of an object to access a member variable, use the keyword ________.
  3. True or false: good programming practice is to set the accessibility of all member variables to private.
  4. Public functions are the ________ of a class.
  5. True or false: the purpose of a constructor is to initialize all the member variables.


12.3.2: Returning Objects from Functions

  • Objects may be returned from functions
  • When returned, C++ simply makes a copy of the object
  • We make a simple non-member function to demonstrate this technique:
    Product makeProduct() {
        string name;
        cout << "Enter a product name: ";
        cin >> name;
        double price;
        cout << "Enter the price for a "
             << name << ": ";
        cin >> price;
        Product newProd(name, price);
        return newProd;
    }
    
  • We show returning an object in productapp.cpp below

Returning an Object: productapp.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
#include <vector>
using namespace std;

class Product {
public:
    Product();
    Product(string newName, double newPrice);
    string getName() const;
    double getPrice() const;
    void setName(string newName);
    void setPrice(double newPrice);
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "none";
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    setName(newName);
    setPrice(newPrice);
}

string Product::getName() const {
    return name;
}

double Product::getPrice() const {
    return price;
}

void Product::setName(string newName) {
    name = newName;
}

void Product::setPrice(double newPrice) {
    price = newPrice;
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

// Function that returns an object
Product makeProduct();

// For testing class Product
int main() {
    vector<Product> store;
    char repeat = 'Y';
    while ('Y' == repeat || 'y' == repeat) {
        cout << "Enter product data:\n";
        Product temp = makeProduct();
        store.push_back(temp);
        cout << "You entered:"
             << "\n   Name: " << temp.getName()
             << "\n   Price: " << temp.getPrice()
             << endl;

        cout << "Enter another product? (y/n): ";
        cin >> repeat;
    }

    cout << "\nAll your products:\n";
    for (unsigned i = 0; i < store.size(); i++) {
        store[i].print();
    }

    return 0;
}

Product makeProduct() {
    string name;
    cout << "Product name: ";
    cin >> name;
    double price;
    cout << "Price for a " << name << ": ";
    cin >> price;
    Product newProd(name, price);
    return newProd;
}

Check Yourself

  1. True or false: objects can be returned from functions.
  2. True or false: objects are always returned from functions by reference.
  3. True or false: the only way to return an object from a function is by using a return statement.


12.3.3: Passing Objects to Functions

  • Class types can be function parameters and we may pass objects to functions
  • We may pass objects by value or by reference
  • However, usually we pass objects by reference because it requires less work for the computer
  • As an example, let us write a function to compare the price of two products
  • One way we can write the function is as a non-member function
  • For example:
    bool isHigherPrice(Product& prod1, Product& prod2) {
        if (prod1.getPrice() > prod2.getPrice()) {
            return true;
        }
        return false;
    }
    
  • To call the function we need two arguments like:
    if (isHigherPrice(prod1, prod2)) {
        cout << prod1.getName() << " costs more\n";
    } else {
        cout << prod2.getName() << " costs more\n";
    }
    
  • We revise our productapp.cpp file to add this function as shown below

Revised productapp2.cpp with Class Parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
using namespace std;

class Product {
public:
    Product();
    Product(string newName, double newPrice);
    string getName() const;
    double getPrice() const;
    void setName(string newName);
    void setPrice(double newPrice);
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "none";
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    setName(newName);
    setPrice(newPrice);
}

string Product::getName() const {
    return name;
}

double Product::getPrice() const {
    return price;
}

void Product::setName(string newName) {
    name = newName;
}

void Product::setPrice(double newPrice) {
    price = newPrice;
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

// Function with Product parameters
bool isHigherPrice(Product& prod1, Product& prod2);

// Function that returns an object
Product makeProduct();

// For testing class Product
int main() {
    cout << "Enter the first product:\n";
    Product prod1 = makeProduct();
    cout << "Enter the second product:\n";
    Product prod2 = makeProduct();
    if (isHigherPrice(prod1, prod2)) {
        cout << prod1.getName() << " costs more\n";
    } else {
        cout << prod2.getName() << " costs more\n";
    }

    return 0;
}

bool isHigherPrice(Product& prod1, Product& prod2) {
    if (prod1.getPrice() > prod2.getPrice()) {
        return true;
    }
    return false;
}

Product makeProduct() {
    string name;
    cout << "Product name: ";
    cin >> name;
    double price;
    cout << "Price for a "
         << name << ": ";
    cin >> price;
    Product newProd(name, price);
    return newProd;
}

Check Yourself

  1. True or false: objects are usually passed by reference to improve execution speed.
  2. Of the following functions called from main()________ is made to a non-member function.
    1. Product bread;
    2. bread.read();
    3. bread.print();
    4. list(products);
  3. True or false: calling a member function, outside of a class, requires an object. (see lesson 8.2.6)


12.3.4: Comparing Member Functions with Non-member Functions

  • We looked at comparing two Product objects using a non-member function:
    bool isHigherPrice(Product& prod1, Product& prod2) {
        if (prod1.getPrice() > prod2.getPrice()) {
            return true;
        }
        return false;
    }
    
  • Another way is to write the comparison as a member function:
    bool Product::isHigherPrice(Product& prod2) const {
        if (getPrice() > prod2.getPrice()) {
            return true;
        }
        return false;
    }
    
  • Note the difference in the parameter lists
  • To call the non-member function we need two objects:
    if (isHigherPrice(prod1, prod2)) {
        cout << prod1.getName() << " costs more\n";
    } else {
        cout << prod2.getName() << " costs more\n";
    }
    
  • To call the member function we need one object:
    if (prod1.isHigherPrice(prod2)) {
        cout << prod1.getName() << " costs more\n";
    } else {
        cout << prod2.getName() << " costs more\n";
    }
    
  • By using dot notation, the object name supplies an implicit argument to the function
  • The implicit argument identifies which object data to access for comparison against the object specified by the parameter
  • Note that we could have written the member function as:
    bool Product::isHigherPrice(Product& prod2) const {
        if (price > prod2.price) {
            return true;
        }
        return false;
    }
    
  • We do not need to use function calls because member functions can access private member variables directly

When to Write Member and NonMember Functions

  • Which solution is better: member or nonmember functions?
  • It depends on the ownership of the class
  • If you own the class, you should implement useful operations as member functions
  • If you are using a class supplied by someone else, you should write a nonmember function rather than changing the class
  • The author of the class may improve it and give you a new version
  • It would be a nuisance to have to add your modifications every time you received a new version of the class

Check Yourself

  1. True or false: member function tend to have fewer parameters than non-member functions.
  2. True or false: if a member function compares two objects, only one object parameter is needed.
  3. True or false: if you are the owner of a class, it is better to write a member function to implement useful behaviors.


12.3.5: Reading File Data into a Vector of Objects

  • Lets say we have a file with information about products organized like products2.txt:
    Milk
    3.95
    Whole-wheat bread
    2.99
    Cheddar cheese
    3.95
    
  • We want to read data from the file into a vector of Product objects
  • Object-oriented design principles say that an object should know how to read and write it's own data
  • Thus a good way to approach this problem is to add a read() function to the class
  • The read() function reads from an input file stream with code like:
    void Product::read(ifstream& fin) {
        fin >> ws; // clear whitespace including newlines
        getline(fin, name);
        fin >> price;
    }
    
  • We open the input stream in main() and pass the stream in the function call:
    ifstream fin("products.txt");
    ...
    Product temp;
    temp.read(fin);
    
  • To read all the product data into a vector of objects, we use a loop as shown in the following example
  • To run the example, we read from the file: products2.txt

Example Reading a File into a Vector of Objects (filetovector.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <fstream>
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;

class Product {
public:
    Product();
    Product(string newName, double newPrice);
    void read(ifstream& fin);
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "Unknown";
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    name = newName;
    price = newPrice;
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

void Product::read(ifstream& fin) {
    fin >> ws; // clear whitespace including newlines
    getline(fin, name);
    fin >> price;
}

// Read from filename into the vector
void readFile(vector<Product>& list, string filename);

// Display vector data
void listProducts(const vector<Product>& list);

int main() {
    vector<Product> list;
    readFile(list, "products2.txt");

    cout << "\nProducts in my store:\n";
    listProducts(list);

    return 0;
}

void readFile(vector<Product>& list, string filename) {
    ifstream fin(filename.c_str());
    if (fin.fail()) {
        cout << "Input file failed to open.\n";
        exit(-1);
    }

    while(fin.good()) {
        Product temp;
        temp.read(fin);
        if (fin.good()) {
            list.push_back(temp);
        }
    }
    fin.close();
}

void listProducts(const vector<Product>& list) {
    for (unsigned i = 0; i < list.size(); i++) {
        Product temp = list[i];
        temp.print();
    }
}

Check Yourself

  1. True or false: an object should know how to read and write its own data.
  2. True or false: a convenient way to read data from a file into an object is to write a member function to read the data.
  3. True or false: the class member function reading from a file should have an ifstreamparameter rather than a file name (string) parameter.
  4. When a read operation is successful, the istream member function good() returns ________.


12.3.6: Writing File Data from a Vector of Objects

  • After reading data into a vector of objects, we may want to write the data to a file
  • As with reading, an object should know how to write its own data
  • Thus a good way to approach this problem is to add a write() function to the class
  • The write() function writes to an output file stream with code like:
    void Product::write(ofstream& fout) {
        fout << name << endl;
        fout << price << endl;
    }
    
  • We open the input stream in another function and pass the stream in the function call:
    void writeFile(vector<Product>& store, string fileName)
    {
        ofstream fout(fileName);
        if (fout.fail())
        {
            cout << "Output file " << fileName << " failed to open.\n";
            exit(-1);
        }
        fout << fixed << setprecision(2); // two decimal places
        for (unsigned i = 0; i < store.size(); i++)
        {
            store[i].write(fout);
        }
        fout.close();
    }
    
  • To write all the product data into a vector of objects, we use a loop as shown in the above example
  • Notice that writing to a file is like output to a terminal window
  • We know in advance how many sets of data to write
  • Thus the loop for writing is a for-loop

Check Yourself

  1. True or false: an object should know how to write its own data.
  2. True or false: a convenient way to write data from an object into a file is to write a member function to write the data.
  3. True or false: the class member function reading from a file should have an ofstreamparameter rather than a file name (string) parameter.


Exercise 12.3: Read and Writing File Data to and from a Vector of Objects (10m)

In this exercise we explore reading a file into a vector of objects. Start with the following Productclass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <fstream>
#include <iostream>
#include <vector>
#include <cstdlib>
using namespace std;

class Product {
public:
    Product();
    Product(string newName, double newPrice);
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "none";
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    name = newName;
    price = newPrice;
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

// Read product data from a file.
void readFile(vector<Product>& list, string filename);

// List the products in the store.
void listProducts(const vector<Product>& store);

// Write vector of objects to the file.
void writeFile(vector<Product>& store, string fileName);

int main() {
    vector<Product> list;
    int choice;
    do { // simple menu
        cout << "\nSelect an option:\n";
        cout << "0. Exit program.\n";
        cout << "1. Load data from file.\n";
        cout << "2. Print data in vector.\n";
        cout << "3. Write data to a file.\n";
        cout << "Choice: ";
        cin >> choice;
        if (choice == 1) {
            // readFile(list, "products2.txt");
        } else if (choice == 2) {
            listProducts(list);
        } else if (choice == 3) {
            // writeFile(list, "saved.txt");
        } else if (choice != 0) {
            cout << "Please enter a number from 0 - 3.\n";
        }
    } while (choice != 0);
    cout << "Goodbye.\n";

    return 0;
}

void listProducts(const vector<Product>& list) {
    for (unsigned i = 0; i < list.size(); i++) {
        Product temp = list[i];
        temp.print();
    }
}
  1. Start with the starter file listed above and copy it into a text editor and save it as productfile.cpp.
  2. Compile your code to make sure you copied it correctly and after each step.
  3. In the same directory (folder) as your source code, save the data file: products2.txt.
  4. Be sure to put an extra line at the end of products2.txt and save it as with UNIX line endings. 
  5. In the Product class, add a read function with the following prototype:
    void read(ifstream& fin);
    
  6. Outside the Product class, add the implementation of the read function using the prototype just added.
    void Product::read(ifstream& fin) {
        // read the whitespace before getline
        // read the product name
        // read the price
    }
    

    See lesson 12.3.5 for more information.

  7. Implement the readFile() function using the declared prototype from the starter code and the following pseudocode.
    void readFile(vector<Product>& list, string filename) {
        // open an input file stream
        // test if the stream failed to open
        // while the file stream is good
            // construct a temporary object
            // call the read() function on the object
            // if no error during read()
                // then push onto back of vector
        // close the stream after the loop ends
    }
    

    See section 12.3.5 for more information.

  8. Call readFile() from main() by uncommenting the menu code like:
    readFile(list, "products2.txt");
    
  9. Compile and run your code to verify it works. When run, you should see the data from the file displayed like the following. Numbers in red show the input and are NOT part of the code to write.
    Select an option:
    0. Exit program.
    1. Load data from file.
    2. Print data in vector.
    3. Write data to a file.
    Choice: 1
    
    Select an option:
    0. Exit program.
    1. Load data from file.
    2. Print data in vector.
    3. Write data to a file.
    Choice: 2
    Milk @ 3.95
    Whole-wheat bread @ 2.99
    Cheddar cheese @ 3.95
    
    Select an option:
    0. Exit program.
    1. Load data from file.
    2. Print data in vector.
    3. Write data to a file.
    Choice: 4
    Please enter a number from 0 - 3.
    
    Select an option:
    0. Exit program.
    1. Load data from file.
    3. Write data to a file.
    Choice: 0
    Goodbye.
    

    If you have problems, ask a classmate or the instructor for help as needed.

  10. After reading from a file works correctly, in the Product class, add a write function with the following prototype:
    void write(ofstream& fout);
    
  11. Outside the Product class, add the implementation of the write function using the prototype just added.
    void Product::write(ofstream& fout) {
        // write the product name
        // write the price
    }
    

    See lesson 12.3.6 for more information.

  12. Implement the writeFile() function using the declared prototype from the starter code and the following pseudocode.
    void writeFile(vector<Product>& list, string filename) {
        // open an output file stream
        // test if the stream failed to open
        // for each object in the vector
            // call the write function
        // close the stream after the loop ends
    }
    

    Write an endl after each output command. See section 12.3.6 for more information.

  13. Call writeFile() from main() by uncommenting the menu code like:
    writeFile(list, "saved.txt");
    
  14. Compile and run your code to verify it works as before. Select menu 3 to write to a file. Open the "saved.txt" output file and verify it is the same as the original "products2.txt" file.
  15. Submit your program source code (productfile.cpp) to Canvas Ex 12.3
  16. When finished please help those around you.


12.3.7: Converting Between Strings and Numbers

  • We can read from or write to strings, rather than files, using string streams
  • The sstream library contains definitions for both istringstream and ostringstream
    #include <sstream>
  • These string streams make it easy to convert between numbers and strings

Converting Numbers to Strings

  • We can write to an ostringstream like writing to cout
    ostringstream outstr;
    outstr << 12.945;
    
  • Once we have written to an ostringstream, we call its str() member function
    string strVal = outstr.str();
    
  • Member function str() returns a string containing the contents of the ostringstream

Converting Strings to Numbers

  • We can construct an istringstream object using a string parameter
    string str = "123.4567";
    istringstream instr(str);
    
  • Once constructed, we can read data from the istringstream like we do with cin
    double number;
    instr >> number;
    
  • Since string conversion is common, it is useful to have helper functions for these tasks
  • The following program shows such helper functions

Example Using String Streams to Convert Between Strings and Numbers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <sstream>
#include <iostream>
using namespace std;

/**
    Convert a string to a double.
*/
double stringToDouble(string s) {
   istringstream instr(s);
   double number;
   instr >> number;
   return number;
}

/**
    Convert a double to a string.
*/
string doubleToString(double num) {
    ostringstream outstr;
    outstr << num;
    return outstr.str();
}

int main() {
    string str = "123.4567";
    double x = stringToDouble(str);
    cout.setf(ios::fixed);
    cout.setf(ios::showpoint);
    cout.precision(2);
    cout << "Numerical value: " << x * 2 << endl;

    string strVal = doubleToString(x * 2);
    string msg = "String value: " + strVal + "\n";
    cout << msg;

    return 0;
}

Check Yourself

  1. True or false: you can read and write data to strings instead of files.
  2. To read numbers or characters from a string using stream operators >> use the class ________.
  3. To write numbers or characters to a string using stream operators << use the class ________.


12.3.8: Summary

  • You can construct a stream and open it in one step:
    ofstream fout("messages.txt");
  • The standard open operation will create an empty file
  • You can open a file for appending data by using an extra argument:
    ofstream fout("important.txt", ios::app);
    
  • You can use string variables as the names of files
  • However, we must use the c_str() function to convert to a C-string:
    ifstream fin(filename.c_str());
    
  • Streams can be arguments to a function, but you must use call-by-reference
  • Type istream for function parameters works for both cin and ifstream
  • Type ostream for function parameters works for both cout and ofstream
  • You can read or write to strings, rather than files, using string streams
  • This is useful for converting between numbers and strings
  • Using an istringstream, we can read numbers that are stored in a string by using the >>operator
  • Similarly, by writing to an ostringstream, we can convert numbers to strings using the <<operator
  • We developed some helper functions to support these conversions
  • In addition, we discussed file operations for removing and renaming files

Check Yourself

As time permits, be prepared to answer these questions. You can find more information by following the links after the question.

  1. How can you read data from a file into an object before inserting it into a vector? (12.2.5)
  2. Using string streams, you can read from and write to strings rather than files. Why is this useful? (12.3.6)
  3. To delete the file named "error.log", what statement would you write? (12.2.5)
  4. To change the name of the file named "error.log" to "error.bak", what statement would you write? (12.2.5)


12.4: Separate Compilation

Objectives

At the end of the lesson the student will be able to:

  • Separate classes from applications
  • Discuss why programs should be separated into parts
  • Describe how to separate the interface from the implementation
  • Use separate compilation


12.4.1: Separating Classes from the main() Function

  • When we work with classes and objects, we usually specify a class in one file and write code to use the class in another file
  • This creates a more modular set of classes and allows you to reuse classes in other programs without copying and pasting code
  • Recall the #include directive:
    #include <iostream>
  • It turns out we can include our own files into other program files
  • Syntax:
    #include "myfile.cpp"
  • For example:
    #include "product.cpp"

Programs and the main() Function

  • In C++, each program can have only one main() function
  • Usually, the main() function is coded in a file separate from all the other classes and functions
  • Then any functions or classes needed by main are added using the #include preprocessor directive
  • To demonstrate, we remove the main() function from our Product class as shown below
  • Note that without a main() function we can no longer compile the code
  • Trying to compile causes a linker error because every program must have a single main()function:
    ...undefined reference to `_WinMain@16'
    collect2: ld returned 1 exit status
    
  • We will show how to compile and link this code in the next section

Class Product Without a main() Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
using namespace std;

class Product {
public:
    Product();
    Product(string newName, double newPrice);
    string getName() const;
    double getPrice() const;
    void setPrice(double newPrice);
    void print() const;
private:
    string name;
    double price;
};

Product::Product() {
    name = "none";
    price = 0.0;
}

Product::Product(string newName, double newPrice) {
    name = newName;
    setPrice(newPrice);
}

string Product::getName() const {
    return name;
}

double Product::getPrice() const {
    return price;
}

void Product::setPrice(double newPrice) {
    if (newPrice > 0.0) {
        price = newPrice;
    } else {
        cout << "Error: negative price!\n"
             << "Setting price to 0.\n";
        price = 0.0;
    }
}

void Product::print() const {
    cout <<  name << " @ " << price << endl;
}

Check Yourself

  1. To have the preprocessor insert another file into our source code, use the ________ directive.
  2. True or false: if we want to include non-library files, use the include directive with double quote marks rather than angle brackets.
  3. True or false: in C++ you must have exactly one main() function per executable file.


12.4.2: Including a Class in an Application

  • Now let us look at how to create an application by including the separate product.cpp file
  • The following program is called productapp.cpp
  • It consists of a main() function that includes the Product class using the #include directive:
    #include "product.cpp"
  • Notice that standard library includes are placed before our custom includes

Program productapp.cpp Including a File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <vector>
using namespace std;

#include "product.cpp"

// For testing class Product
int main() {
    vector<Product> store;
    char repeat = 'Y';
    while ('Y' == repeat || 'y' == repeat) {
        cout << "Enter product data:\n";
        string name;
        cout << "Product name: ";
        cin >> name;
        double price;
        cout << "Price for a " << name << ": ";
        cin >> price;

        Product temp(name, price);
        store.push_back(temp);
        cout << "You entered:"
             << "\n   Name: " << temp.getName()
             << "\n   Price: " << temp.getPrice()
             << endl;

        cout << "Enter another product? (y/n): ";
        cin >> repeat;
    }

    cout << "\nAll your products:\n";
    for (unsigned i = 0; i < store.size(); i++) {
        store[i].print();
    }

    return 0;
}

12.4.2a Try It: Separating main() (5m)

  1. Copy the following program into a text editor, save it as rectangleclass.cpp, and then compile and run the starter program to make sure you copied it correctly.  You should see the following output:
Printing rec: 0 long x 0 wide
Printing rec3x5: 3 long x 5 wide
    1. #include <iostream>
      using namespace std;
      
      class Rectangle {
      public:
          Rectangle();
          Rectangle(double newLength, double newWidth);
          void print();
      private:
          double length;
          double width;
      };
      
      Rectangle::Rectangle() {
          length = 0;
          width = 0;
      }
      
      Rectangle::Rectangle(double newLength, double newWidth) {
          length = newLength;
          width = newWidth;
      }
      
      void Rectangle::print() {
          cout << length << " long x " << width << " wide\n";
      }
      
      // For testing
      int main() {
          Rectangle rec;
          Rectangle rec3x5(3.0, 5.0);
          cout << "Printing rec: ";
          rec.print();
          cout << "Printing rec3x5: ";
          rec3x5.print();
      
          return 0;
      }
      
    2. Start a new file named rectanglemain.cpp and move the main() function to this file, deleting it from the rectangleclass.cpp file.

      Try compiling rectanglemain.cpp and notice that it will NOT compile at this time. We will make it compile in the next step.

    3. Now we include the Rectangle class in the main application by adding the following code at the top of the file:
      #include "rectangleclass.cpp"
      
    4. Compile rectanglemain.cpp and run the program to verify it works the same.
    5. Be prepared to answer the following Check Yourself questions when called upon.

    Check Yourself

    1. True or false: we can create an application using two separate source code files.
    2. We can include the following number of files in our code: ________.
      1. 1
      2. 2
      3. 4
      4. as many as we need
    3. True or false: standard library includes should be placed before custom includes.
    4. To include a custom library named "mylib.cpp" we add the code: ________.
      1. #include (mylib.cpp)
      2. #include [mylib.cpp]
      3. #include "mylib.cpp"
      4. #include <mylib.cpp>


    12.4.3: Separate Compilation Process

    • We can take the separation of files even further
    • C++ lets us divide a program into separate parts and compile each part separately
    • After all the parts are compiled, the parts are linked together into an executable program
    • We can see the compilation steps in the following diagram

    Compilation Process

    Benefits of Separating the Parts

    • We can place the definition for a class and its functions in files separate from the programs using the class
    • This allows us to build libraries of classes
    • These libraries can be used by many different programs
    • In addition, we can compile a class just one time and use it in many different programs
    • We use a similar process with standard libraries like iostream
    • Moreover, we can define the class itself in two files with the definition of the class in one file and the implementation in another
    • If we only change the implementation of the class, then we only need to recompile the implementation
    • Other files in the program, including files that use the class, need not be changed or recompiled

    Encapsulation Reviewed

    • The principles of encapsulation and data hiding tells us to separate the interface from the implementation
    • The separation should be complete enough that we can change the implementation without changing the program that uses the class
    • We ensure this separation with the following three rules

    Rules of Separation

    1. Make all member variables private
    2. Make each basic operation of the class a public member function. All the public functions become the interface
    3. Make the member function implementations unavailable to the programmer who uses the class

    More Information


    12.4.4: Example of Separate Compilation

    • Separate compilation starts by placing class interfaces and implementations in separate files
    • The interface is placed in a header file with a .h extension
    • The implementation code is placed in an implementation file with a .cpp extension
    • For example, we can save our Product class interface in a header file
    • We name the file: product.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    #ifndef PRODUCT_H
    #define PRODUCT_H
    
    #include <string>
    using namespace std;
    
    class Product {
    public:
        // Constructors
        Product();
        Product(string newName, double newPrice);
        // Member functions
        string getName() const { return name; }
        double getPrice() const { return price; }
        void setName(string newName);
        void setPrice(double newPrice);
    private:
        string name;
        double price;
    };
    
    #endif
    

    #include Guards

    • Notice the use of:
      #ifndef PRODUCT_H
      #define PRODUCT_H
      ... (class declaration)
      #endif
      
    • These are know as #include guards
    • We use these constructs to avoid the problem of double inclusion
    • Once a header files is included, the preprocessor checks if a unique value is defined
    • If the value is not defined, then it defines it and continues with the file
    • If the value is already defined elsewhere, the first ifndef fails and results in a blank file being included
    • We typically use a naming scheme of the class name followed by "_H" to define the header files

    Implementation (Function Definition) Files

    • The remainder of the class definition stays in the .cpp file, such as product.cpp
    • However, the .cpp function definitions still need the declarations to compile
    • Thus, we add them back by including the product.h file in the .cpp
    • The following is an example of product.cpp where the function headers (declarations) have been placed in a separate .h file
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    #include "product.h"
    
    // no-parameter constructor
    Product::Product() {
        name = "Unknown";
        price = 0.0;
    }
    
    Product::Product(string newName, double newPrice) {
        setName(newName);
        setPrice(newPrice);
    }
    
    void Product::setName(string newName) {
        if (newName.length() == 0) {
            name = "Unknown";
        } else {
            name = newName;
        }
    }
    
    void Product::setPrice(double newPrice) {
        if (newPrice > 0.0) {
            price = newPrice;
        } else {
            price = 0.0;
        }
    }
    
    

    Application File

    • The main() function is the starting point of any application
    • We usually place main() in a separate file and include the header files we need for compiling
    • Notice that the includes for our header files are placed after the standard library includes
    • For our example, we call our main file productapp.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    #include <iostream>
    using namespace std;
    
    #include "product.h"
    
    // For testing class Product
    int main() {
        char choice = 'Y';
        string name;
        double price;
        while ('Y' == choice || 'y' == choice) {
            cout << "Enter a product name: ";
            cin >> name;
            cout << "Enter the price for a "
                 << name << ": ";
            cin >> price;
    
            Product prod(name, price);
            cout << "You entered:"
                 << "\n   Name: " << prod.getName()
                 << "\n   Price: " << prod.getPrice()
                 << endl;
    
            cout << "Enter another product? ";
            cin >> choice;
        }
    
        return 0;
    }
    

    Compiling

    • Because we have multiple .cpp files, compiling is now a two-step process:
      1. Compile the all the .cpp files into object files
      2. Link all the objects files together into an executable file
    • We compile the class and the application into object files:
      g++ -c product.cpp
      g++ -c productapp.cpp
      
    • Then we link all object files together into an application
      g++ -o productapp productapp.o product.o
      
    • We then run the application in the usual way:
      ./productapp
      


    12.4.5: Instructions for Separate Compilation

    1. Separate the interface from the implementation
      1. Place the class declaration into a classname.h file
      2. Place #include guards around the declaration in the classname.h file
        #ifndef PRODUCT_H
        #define PRODUCT_H
        // code goes here
        #endif
        
      3. Place the class implementation (function definitions) into a file named classname.cpp
      4. Code a #include "classname.h" directive in the classname.cpp file and place it after the standard library includes
    2. Place the main() function in a separate file
      1. Place the application main function into a file like appname.cpp
      2. Code a #include "classname.h" directive in the appname.cpp file
        #include "product.h"
    3. Compile the class and the driver into object files:
      g++ -c classname.cpp
      g++ -c appname.cpp
      
      for example:
      g++ -c product.cpp
      g++ -c productapp.cpp
      
    4. Link both files together into the application
      g++ -o appname appname.o classname.o
      
      like:
      g++ -o productapp productapp.o product.o
      
    5. Then run the application in the usual way:
      ./appname
      for instance:
      ./productapp
      

    12.4.5a Try It: Separate Compilation (10m)

    1. If you are using a Windows computer, open the Cygwin terminal window.
    2. Copy the following program into a text editor, save it as myrectangle.cpp, and then compile and run the starter program to make sure you copied it correctly.  You should see this output:
    length: 3
    width: 5
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      
      #include <iostream>
      using namespace std;
      
      class MyRectangle {
      public:
          MyRectangle(double length, double width);
          void print() const;
      
      private:
          double length;
          double width;
      
      };
      
      MyRectangle::MyRectangle(double newLength, double newWidth) {
          length = newLength;
          width = newWidth;
      }
      
      void MyRectangle::print() const {
          cout << "length: " << length
               << "\nwidth: " << width
               << endl;
      }
      
      // For testing
      int main() {
          MyRectangle rec(3.0, 5.0);
          rec.print();
      
          return 0;
      }
      
    1. Apply the separate compilation process to the MyRectangle class shown above. You should end up with three files:
      • myrectangle.h
      • myrectangle.cpp
      • myrectangleapp.cpp
    2. Make sure you have #include guards in the myrectangle.h file.
      #ifndef MYRECTANGLE_H
      #define MYRECTANGLE_H
      // code goes here
      #endif
      
    3. Make sure you place the class declaration into the myrectangle.h and then add a #include "myrectangle.h" directive at the top of the myrectangle.cpp file.
      #include "myrectangle.h"
      
    4. Make sure you place the main() function in a separate file and add a #include "myrectangle.h" directive at the top of the file but after using namespace std;
      #include "myrectangle.h"
      
    5. At the command line, compile the files separately, like:
      g++ -c myrectangle.cpp
      g++ -c myrectangleapp.cpp
      
    6. Link both files together into an application like:
      g++ -o myrectangleapp myrectangleapp.o myrectangle.o
      
    7. Then run the application in the usual way to test the file:
      ./myrectangleapp
      
    8. You should see the same output as before:
    length: 3
    width: 5

         11.  Check to see if the student on either side of you needs help. If so, offer to help them.


    12.4.6: Makefiles

    • It quickly becomes tedious to recompile code with multiple source files
    • We can use a program named make to automatically recompile our files
    • However, we must create a file named Makefile that has the instructions for the makeprogram
    • Note that the large blank areas before a command is a tab character
    • To invoke a Makefile, we type make at the command line
    • Most people write a Makefile by modifying an existing one

    Makefile Example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # simple makefile
    
    # define target dependencies and files
    productapp: productapp.o product.o
    	g++ -o productapp productapp.o product.o
    
    # define how each object file is to be built
    productapp.o: productapp.cpp product.h
    	g++ -c productapp.cpp -Wall -Wextra -Wpedantic -std=c++11  
    
    product.o: product.cpp product.h
    	g++ -c product.cpp -Wall -Wextra -Wpedantic -std=c++11 
    
    # clean up
    clean:
    	rm -f productapp.exe *.o
    
    

    Important Information

    • Lines starting with a hash mark (#) are comments and are ignored
    • Rules define which files depend on others and take the form:
      targetfile : sourcefiles
      <tab>commands you normally type
      
    • tab character is required prior to defining the commands
    • If using Notepad++, we need to configure the preferences for the Text document class to NOT substitute spaces for tabs.
    • Save without a .txt extension by saving as type "All Files (*.*)"

    Further Information


    Exercise 12.4: Separate Compilation and Makefiles (5m)

    In this exercise we create a Makefile to make separate compilation easier.

    Specifications

    1. If you have not already applied the separate compilation process to the MyRectangle class, complete the exercise: Try It: Separate Compilation. Make sure you have three separate files:
      • myrectangle.h
      • myrectangle.cpp
      • myrectangleapp.cpp
    2. Copy the Makefile Example below and save it as Makefile without a .txt extension by saving as type "All Types (*.*)" 
    3. We must save the Makefile with tab characters. Thus on Windows Notepad++ go to: Settings->Preferences->
         Languages->uncheck the box on the right side "replace by space"   (You will have to change this back again when you work on other code files).
    4. in a cygwin window:  type  "make"    (without the quotation marks)
    5. next,  type    "./myrectangleapp"        (without the quotation marks)
    6. Your output should be:
    7. length: 3
      width: 5
    8. Zip and submit your three code files and one Makefile as the solution to this exercise.  call your zip file:  myrectangle.zip
      • Makefile
      • myrectangle.h
      • myrectangle.cpp
      • myrectangleapp.cpp

      Note: please zip (archive) files to be able to submit the Makefile.  Do this by selecting the 4 files,  right click and Send To->Compressed(zipped) folder

    Makefile Example

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # simple makefile
    
    # define target dependencies and files
    productapp: productapp.o product.o
    	g++ -o productapp productapp.o product.o
    
    # define how each object file is to be built
    productapp.o: productapp.cpp product.h
    	g++ -c productapp.cpp -Wall -Wextra -Wpedantic -std=c++11
    
    product.o: product.cpp product.h
    	g++ -c product.cpp -Wall -Wextra -Wpedantic -std=c++11
    
    # clean up
    clean:
    	rm -f productapp.exe *.o
    
    


    12.4.7: Summary

    • C++ allows us to place the interface and the implementation in separate files
    • We can write a Makefile for easy recompiling

    Check Yourself

    1. What are the steps for separate compilation?