12: More I/O and Objects

What We Will Cover


Continuations

Midterm Questions?

  • Midterm 2 statistics from Canvas
  • Code that does not compile contains an error
  • When given the tools to compile, your code must compile for a good score
  • How do you make a program with problems compile?
  • Final exam will include all material from the midterms plus the new material
  • Post Midterm 2 Survey

Questions from last class?

Homework Questions?

Last Day to Withdraw Approaching

  • Last day to drop a full-term section with a grade of "W": 4/22/2017
  • See Admissions and Records: Dates and Deadlines
  • Grade calculation "what-if" available in Canvas Grades

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

Name Description
eof Returns true if the stream has reached end-of-file.
fail Returns true if an error has occurred on the stream, including reaching the end of file.
good Returns true if the most recent I/O operation on the stream completed successfully.
bad Returns 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;
}

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.

    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:
    Read: 12.94
    Read: -9.87654
    Read: 2.3131
    Read: -89.506
    Read: 12.9333
    Read: 92.8765
    Read: -123.457
    

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

  9. 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 or click here Click to show answer.

  10. Save your source code file as we will be adding to it in the next Try It.
  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 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;
}

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

  1. Open your readwrite.cpp from the last Try It.
  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.94
    Read: -9.87654
    Read: 2.3131
    Read: -89.506
    Read: 12.9333
    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 or click here Click to show answer.

  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

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() {
    // C++0x vector initializer list
    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. Try It: Read a File with a Loop
    2. Try It: Load Data into a Vector
  2. Add a function with the following prototype to your file.
    void writeData(const vector<double>& data);
    
  3. Call the writeData() function from main() just before return 0.
  4. 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.

  5. 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;
    }
    
  6. Add a statement after the above to close the output stream.
  7. 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.

  8. Save your program source code file to submit to Canvas as part of assignment 12.

Listing of readwrite.cpp

When finished developing your code, click here Click to show answer to compare your solution to mine. After you have completed your own solution, reviewing another is often helpful in learning how to improve your programming skills.

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");
    

    answer

12.2.2: Appending to a File

  • The standard open operation for writing begins with an empty file
  • Even if the file exists we loose all the contents
  • To prevent loosing 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

  • C++ stores its strings in a kind of char array known as a C-string
  • A C-string is a hold over from the C programming language
  • However, many library functions still use C-strings and NOT string variables
  • For example, the argument to open() in C++98 is a C-string:
    fin.open("infile.txt");
    
  • Thus if we want to use a string variable for a file name we need to convert the variable to a C-string
  • To convert to a C-string, we call the c_str() member function
    fin.open(filename.c_str());
    
  • Similarly, we can use a string variable with a stream overloaded constructor
    ifstream fin(filename.c_str());
    
  • Starting in C++11, this problem is fixed
  • By default, g++ (Cygwin) uses C++98
  • If we want to use C++11 then we must add the following extra option when compiling
    g++ -Wall -Wextra -Wpedantic -std=c++11 -o programName sourceFile.cpp
    
  • The other option is to use the c_str() function to convert a string to a C-string
  • Here is an example program using a string with c_str() for file names

Example Using a string for File Names

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.c_str());
    if (fin.fail()) {
        cout << "Input file " << filename << "failed to open.\n";
        exit(-1);
    }

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

    fin.close();

    return 0;
}

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.

    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:
    Read: Milk@3.95
    Read: Bread@2.99
    Read: Cheese@3.95
    Read: @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.

Check Yourself

  1. True or false: the file stream open function requires a C-string parameter and will not work with type string.
  2. To convert a string to a C-string, use the string member function ________.
  3. In the above Try It!, the reason for the last output shown below is ________.
    Read: @3.95
    
    1. there were four products in products.txt
    2. One of the products did not have a name
    3. while reading name, fin reached the end of the file
    4. the computer does strange things at odd times
  4. To fix the problem in the previous question, we can ________.
    1. remove the extra product
    2. add the missing product name
    3. test for stream failure before cout
    4. buy a new computer

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.
  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
    Bread @ 2.99
    Cheese @ 3.95
    

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

  6. Save your program source code file to submit to Canvas as part of assignment 12.
  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

Function Description
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 use two explicit parameters 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 productapp.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 use two explicit parameters:
    if (isHigherPrice(prod1, prod2)) {
        cout << prod1.getName() << " costs more\n";
    } else {
        cout << prod2.getName() << " costs more\n";
    }
    
  • To call the member function we use dot notation and one explicit parameter:
    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 parameter to the function
  • The implicit parameter 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

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 ifstream parameter 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);
        }
        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 ofstream parameter 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 Product class.

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. Exist 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. In the Product class, add a read function with the following prototype:
    void read(ifstream& fin);
    
  5. 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.

  6. Implement the readFile() function using the declared prototype from the starter code and the following psuedocode.
    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.

  7. Call readFile() from main() by uncommenting the menu code like:
    readFile(list, "products2.txt");
    
  8. 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. Exist program.
    1. Load data from file.
    2. Print data in vector.
    3. Write data to a file.
    Choice: 1
    
    Select an option:
    0. Exist program.
    1. Load data from file.
    2. Print data in vector.
    3. Write data to a file.
    Choice: 2
    Milk @ 3.95
    Bread @ 2.99
    Cheese @ 3.95
    
    Select an option:
    0. Exist 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. Exist 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.

  9. After reading from a file works correctly, in the Product class, add a write function with the following prototype:
    void write(ofstream& fout);
    
  10. 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.

  11. Implement the writeFile() function using the declared prototype from the starter code and the following psuedocode.
    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.

  12. Call writeFile() from main() by uncommenting the menu code like:
    writeFile(list, "saved.txt");
    
  13. 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.
  14. Submit your program source code (productfile.cpp) to Canvas as part of assignment 12.
  15. When finished please help those around you.

12.3.7: Converting Between Strings and Numbers (optional)

  • 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;
}

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.
    #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.

Listing of rectanglemain.cpp

Listing of rectanglemain.cpp

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

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
    

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.
    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;
    }
    
  3. Apply the separate compilation process to the MyRectangle class shown above. You should end up with three files:
    • myrectangle.h
    • myrectangle.cpp
    • myrectangleapp.cpp
  4. Make sure you have #include guards in the myrectangle.h file.
    #ifndef MYRECTANGLE_H
    #define MYRECTANGLE_H
    // code goes here
    #endif
    
  5. 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"
    
  6. 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"
    
  7. At the command line, compile the files separately, like:
    g++ -c myrectangle.cpp
    g++ -c myrectangleapp.cpp
    
  8. Link both files together into an application like:
    g++ -o myrectangleapp myrectangleapp.o myrectangle.o
    
  9. Then run the application in the usual way to test the file:
    ./myrectangleapp
    
  10. 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 make program
  • 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 -W -Wall --pedantic

product.o: product.cpp product.h
	g++ -c product.cpp -W -Wall --pedantic

# 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
    
  • A tab character is required prior to defining the commands
  • If using TextPad, 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. We must save the Makefile with tab characters. Thus on Windows, use NotePad or NotePad++ for editing in the classroom rather than TextPad.
  3. Prepare a Makefile that automatically compiles the files when you type make at the command line. Use the Makefile Example as a template and make sure your Makefile does NOT have a .txt extension.
  4. Save the Makefile Example without a .txt extension by saving as type "All Files (*.*)" and then place a dot (.) after the file name.

    save with dot

  5. Once finished, check to see if the student on either side of you needs help. If so, offer to help them.
  6. Zip and submit your three code files and one Makefile as the solution to this exercise.
    • Makefile
    • myrectangle.h
    • myrectangle.cpp
    • myrectangleapp.cpp

    Note: please zip (archive) files to be able to submit the Makefile.

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 -W -Wall --pedantic

product.o: product.cpp product.h
	g++ -c product.cpp -W -Wall --pedantic

# 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?

Wrap Up

Due Next:
A12-Storing Information (4/26/17)
  • When class is over, please shut down your computer
  • You may complete unfinished exercises at the end of the class or at any time before the next class.
Last Updated: May 07 2017 @19:53:52