4

I'm writing a program for a class that gets input from a .txt file and puts it in separate arrays based on variable type.

Here's an example of an input file

6 8
Cody Coder  84 100 100 70 100 80 100 65
Harry Hardware  77 68 65 100 96 100 86 100
Harry Potter  100 100 95 91 100 70 71 72
Mad Mulligun  88 96 100 90 93 100 100 100
George Washington  100 72 100 76 82 71 82 98
Abraham Lincoln  93 88 100 100 99 77 76 93

So, I'm supposed to use the first line to get the amount of names (6 in this case) and the amount of exam scores (8) then use that to initialize two dynamic arrays. I believe I'm initializing the arrays correctly, I'm just having a hard time filling the arrays with the correct data from the input file. I've never read from a file before this semester, so I'm completely unfamiliar.

#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <string>

using namespace std;

#ifdef _MSC_VER
#define _CRTDBG_MAP_ALLOC  
#include <crtdbg.h>
#define VS_MEM_CHECK _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#else
#define VS_MEM_CHECK
#endif

int main(int argc, char* argv[]) {
    VS_MEM_CHECK
    // Variables
    int numNames = 32;
    int numExams = 32;

    // Part 1: Read scores from the input file
    cout << "Input file: " << argv[1] << endl;
    ifstream in(argv[1]);
    if (!in) {                                              // fail case
        cerr << "Unable to open " << argv[1] << " for input";
        return 2;
    }
    cout << "Output file: " << argv[2] << endl;
    ofstream out(argv[2]);
    if (!out) {                                             // fail case
        in.close();
        cerr << "Unable to open " << argv[2] << " for output";
        return 3;
    }

    if (in.is_open()) {
        in >> numNames >> numExams;
        // dynamic string array
        string* fullNames = new string[numNames];                   // new: initialize names array
        // 2-d, dynamic double array
        double** scores = new double* [numNames];                   // new: initialize rows of scores array
        for (int i = 0; i < numNames; ++i) {
            scores[i] = new double[numExams];                       // new: initialize columns of scores array
        }

        string currLine;

        for (int i = 0; i < numNames; ++i) {
            string firstName, lastName;

            in >> firstName >> lastName;
            for (int j = 0; j < numExams; ++j) {
                in >> scores[i][j];
            }
            fullNames[i] = firstName + " " + lastName;
        }

    }
    else {
        return -2;
    }

    if (out.is_open()) {

    }
    else {
        return -3;
    }
    return 0;
}

I'm getting an exception thrown (I think for trying to access something that doesn't exist) and I'm required to use arrays. I wouldn't be having issues if I could use vectors.

Edit: I've been asked to show the code where numNames and numExams is declared and it's near the top of main. Edit 2: Included all the libraries I used below


Thank you.

burningyeti
  • 43
  • 1
  • 6
  • I'd gladly help. Just make it a [mcve]. – Ted Lyngmo Jan 25 '20 at 04:08
  • 1
    Pop quiz: which part of your program makes sure that exactly `numNames` lines get read and processed, not more and not less? Bonus question: which part of your program makes sure that exactly `numExams` scores from each line get processed? – Sam Varshavchik Jan 25 '20 at 04:09
  • @TedLyngmo I'm not sure what to add to make sure it's reproducible but I did add the part where numNames and numExams are declared. – burningyeti Jan 25 '20 at 04:26
  • @SamVarshavchik I think it's when I declare/initialize the arrays(?) – burningyeti Jan 25 '20 at 04:28
  • Declaring/initializing arrays only declares/initializes them. Nothing more, nothing less. There's nothing that happens "automatically" in C++. You must write the code to do everything. Again, point your finger to the lines of the shown code that makes sure that exactly `numNames` lines get read, and exactly `numExams` values on each row get processed. Without special trickery, this requires two basic loops. I see only one loop here (and it's not counting anything, either). – Sam Varshavchik Jan 25 '20 at 04:29
  • @SamVarshavchik So, my code just creates two empty arrays? – burningyeti Jan 25 '20 at 04:32
  • Well, you tell me, when you used your debugger to run your program, what did you see? This is what a debugger is for. If you don't know how to use a debugger this is a good opportunity to learn how to use it to run your program one line at a time, monitor all variables and their values as they change, and analyse your program's logical execution flow. Knowing how to use a debugger is a required skill for every C++ developer, no exceptions. With your debugger's help you should be able to quickly find all bugs in this and all future programs you write, without having to ask anyone for help. – Sam Varshavchik Jan 25 '20 at 04:35
  • @burningyeti Pretend you are the one helping out. Could you copy your code and compile it as-is or would you, as someone eager to help, need to add anything? – Ted Lyngmo Jan 25 '20 at 04:35
  • @TedLyngmo I think the only thing I might need to add that's relevant to my issue would be the libraries I've included... Is that right? – burningyeti Jan 25 '20 at 04:43
  • @burningyeti You are the only one who knows. The rest of us lacks context. I have an idea ... but it wouldn't take you long to convert your snippets into a complete program that people who are willing to help could analyze and give feedback on. - and your addition does not connect the dots. Make a small program that can be used by everyone to see the same problem you're having. – Ted Lyngmo Jan 25 '20 at 04:47
  • @TedLyngmo I'm grateful for the help you've given, but direct suggestions would be very welcome. I rewrote the entire block of code. Would it be helpful if I showed the command line? – burningyeti Jan 25 '20 at 04:57
  • @burningyeti Sure, if input needs to be given to the program for it to behave the way you say, share it. - But - I will not try to compile your code as-is. because I see errors even before I let my compiler have a go at it. Suggestion: Copy your block of code _as-is_ and compile it. No edits. Does it work? – Ted Lyngmo Jan 25 '20 at 05:01
  • 2
    @burningyeti [Here is your code as-is](http://coliru.stacked-crooked.com/a/dccc7bc7c7d0ab94). Get rid of those compiler errors, and when you do, update the post with the code. As to the way you're creating 2d arrays, IMO, this method of doing it: `double** scores = new double* [numNames]; for (int i = 0; i < numNames; ++i) {scores[i] = new double[numExams];}` should be outlawed, even if you're not able to use vectors. It is one of, if not the worst way to do this. – PaulMcKenzie Jan 25 '20 at 05:03
  • There's another level of _nooooo - don't_ - but this: "`_string* fullNames = new string[numNames];_`" that you consitantly use is not good for anything. An alternative: `std::vector fullNames(numNames);` will keep your code in better shape. – Ted Lyngmo Jan 25 '20 at 05:16
  • @PaulMcKenzie Thanks for your help, but I'm totally lost. The only way I know I can fix my compiler error(s) is just erasing an entire block of code. I'll update my main.cpp, but I think I'm sunk on this one – burningyeti Jan 25 '20 at 05:42
  • @PaulMcKenzie I made a few changes, and tried to include the entire main function. – burningyeti Jan 25 '20 at 06:09
  • @burningyeti Just wanted to say: Your current question, with the added code, is much more likely to get some attention. .. Btw, is this a question for school? – Ted Lyngmo Jan 25 '20 at 06:20
  • @burningyeti So this code reads the input file correctly from what I can tell. What is the exception you are getting? – super Jan 25 '20 at 08:12

1 Answers1

0

Basically your program is working. I compiled it and fixed some minor stuff.

But it is very important, that you release the memory that you allocate with new.

Please see here the fixed example:

#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <string>


int main(int argc, char* argv[]) {
    // Variables
    int numNames = 32;
    int numExams = 32;

    // Part 1: Read scores from the input file
    std::cout << "Input file: " << argv[1] << std::endl;
    std::ifstream in(argv[1]);
    if (!in) {                                              // fail case
        std::cerr << "Unable to open " << argv[1] << " for input";
        return 2;
    }
    std::cout << "Output file: " << argv[2] << std::endl;
    std::ofstream out(argv[2]);
    if (!out) {                                             // fail case
        in.close();
        std::cerr << "Unable to open " << argv[2] << " for output";
        return 3;
    }

    if (in.is_open()) {
        in >> numNames >> numExams;
        // dynamic string array
        std::string* fullNames = new std::string[numNames];                   // new: initialize names array
        // 2-d, dynamic double array
        double** scores = new double* [numNames];                   // new: initialize rows of scores array
        for (int i = 0; i < numNames; ++i) {
            scores[i] = new double[numExams];                       // new: initialize columns of scores array
        }

        std::string currLine;

        for (int i = 0; i < numNames; ++i) {
            std::string firstName, lastName;

            in >> firstName >> lastName;
            for (int j = 0; j < numExams; ++j) {
                in >> scores[i][j];
            }
            fullNames[i] = firstName + " " + lastName;
        }

        if (out.is_open()) {
            for (int i = 0; i < numNames; ++i) {
                out << fullNames[i] << ":";
                for (int j = 0; j < numExams; ++j) {
                    out << " " << scores[i][j];
                }
                out << "\n";
            }
        }

        // Release memory
        for (int i = 0; i < numNames; ++i) {
            delete[] scores[i];
        }
        delete[] scores;
        delete[] fullNames;

        if (!out.is_open()) {
            return -3;
        }
    }
    else {
        return -2;
    }
    return 0;
}

If we put a little bit more effort in the software, then it could look like that:

#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <string>

// Read file with students and scores and copy write the read data to a new file
int main(int argc, char* argv[]) {

    // Check, if the program has been called with the expected numbers of parameters
    if (3 == argc) {
        // Give feddback to user about filenames:
        std::cout << "\nProgram will work with files: '" << argv[1] << "' and '" << argv[2] << "'\n";

        // Try to open the input file and try to open the output file
        if (std::ifstream in(argv[1]); in) {
            if (std::ofstream out(argv[2]); out) {

                // All files are open. Read number of data. Check, if read worked
                if (size_t numNames{}, numExams{}; (in >> numNames >> numExams) && (numNames > 0U) && (numExams > 0U)) {

                    // We found plausible data Now, allocate memory
                    std::string* fullNames = new std::string[numNames];                   // new: initialize names array
                    // 2-d, dynamic double array
                    double** scores = new double* [numNames];                   // new: initialize rows of scores array
                    for (int i = 0; i < numNames; ++i) {
                        scores[i] = new double[numExams];                       // new: initialize columns of scores array
                    }

                    // Read data
                    for (size_t i = 0U; i < numNames; ++i) {
                        std::string firstName, lastName;
                        in >> firstName >> lastName;
                        for (size_t j = 0U; j < numExams; ++j) {
                            in >> scores[i][j];
                        }
                        fullNames[i] = firstName + " " + lastName;
                    }
                    // Write data
                    for (size_t i = 0U; i < numNames; ++i) {
                        out << fullNames[i] << ":";
                        for (size_t j = 0U; j < numExams; ++j) {
                            out << " " << scores[i][j];
                        }
                        out << "\n";
                    }

                    // Release memory
                    for (int i = 0; i < numNames; ++i) {
                        delete[] scores[i];
                    }
                    delete[] scores;
                    delete[] fullNames;
                }
            }
            else {
                std::cerr << "\nError: Could not open output file '" << argv[2] << "'\n";
            }
        }
        else {
            std::cerr << "\nError: Could not open input file '" << argv[1] << "'\n";
        }
    }
    else {
        std::cerr << "\nError: Please call program with 2 filenames for input data and output data\n\n";
    }
    return 0;
}

And the last step is to go to the more modern C++ approach.

In C++ we do not use raw pointers for owned memory and we should also not use new. If at all we should use std::unique_ptr and std::make_unique. But, not for arrays.

For dynamic arrays, we will use always std::vector or similar. They fit nearly perfect to the requested task. And, it makes life easier.

Then, C++ is an object oriented language. We are using objects, consisting of data and functions that operate on the data.

So, I defined a class (struct) Student, where we will put the names and the scores. And since this is an object, it knows how to read and write its data. Outside the class, nobody cares about that.

We overwrite the inserter and extractor operator for this class and can then use instances of the class with the inserter and extractor operator in the same way as for standard data types. So, it is possible to write std::cout << student.

That makes further operations much more easy. And it does fit better to other algorithms of C++.

Important notice. This approach does not need the first line in the input file. It is fully dynamic! Also the number of scores can be different from line to line.


Let's have a look on the extractor. It first reads a complete line and put this complete line in an istringstream object.

Next we define a std::vector with the name "part" and use its range constructor to fill it. The range constructor gets an iterator to the beginning of some range and the end of some range as parameter and copies the data from there into itself.

The begin iterator is the std:istream_iterator, in this case for a std::string. This ìterator will simply call the extractor operator ( >> ) for the given stream until all data is read. The end is marked with {}. That is the default-constructed std::istream_iterator and known as the end-of-stream iterator. Please see here.

Please note: We can define the std::vector without template argument. The compiler can deduce the argument from the given function parameters. This feature is called CTAD ("class template argument deduction").

So, then, we have all sub strings in the std::vector with the name "part". At the end of this function, we simply copy the relevant parts to our class internal data.

For the scores, we convert the strings to double, and use the std::back_inserter to dynamically add the scores to our scores-vector.


The output, the inserter operator ( << ) is by far more simple. We simply copy the result to the stream. Here we take also advantage of the std::copy function from the standard algorithm library and the std::ostream:iterator. This iterator is also very helpful. It simply calls the inserter operator ( << ) for each given element.

So, after having defined the class, the complete program in main boils down to 2 major statements:

                // Define Roster and read all students
                std::vector roster(std::istream_iterator<Student>(in), {});

                // Write complete roster to out file
                std::copy(roster.begin(), roster.end(), std::ostream_iterator<Student>(out, "\n"));

With 2 lines of code, we can: 1. read the complete file and 2 write it to the desired output file. Please again note that even that could be optimized to one statement:

std::copy(std::istream_iterator<Student>(in), {}, std::ostream_iterator<Student>(out, "\n"));

So, in essence, we are ending up with a one liner . . .

Please see the complete code example:

#include <iostream>
#include <vector>
#include <fstream>
#include <string>
#include <sstream>
#include <algorithm>
#include <iterator>

struct Student {
    // Data for one student
    std::string firstName{};
    std::string lastName{};
    std::vector <double>scores{};

    // Specific extractor operator
    friend std::istream& operator >> (std::istream& is, Student& st) {

        // Read one complete line from the source stream
        if (std::string line{}; std::getline(is, line)) {

            // Put the line in a istringstream
            std::istringstream iss(line);

            // Split the line and get all substrings into a vector
            std::vector part(std::istream_iterator<std::string>(iss), {});

            // Copy the string parts to our local data
            if (part.size() > 1) {
                // Copy name
                st.firstName = part[0]; st.lastName = part[1];

                // Copy scores
                st.scores.clear();
                std::transform(std::next(part.begin(), 2), part.end(), std::back_inserter(st.scores), [](const std::string& s) {return std::stod(s); });
            }
        }
        return is;
    }

    friend std::ostream& operator << (std::ostream& os, const Student& st) {
        os << st.firstName << " " << st.lastName << ": ";
        std::copy(st.scores.begin(), st.scores.end(), std::ostream_iterator<double>(os, " "));
        return os;
    }


};

// Read file with students and scores and copy write the read data to a new file
int main(int argc, char* argv[]) {

    // Check, if the program has been called with the expected numbers of parameters
    if (3 == argc) {
        // Try to open the input file and try to open the output file
        if (std::ifstream in(argv[1]); in) {
            if (std::ofstream out(argv[2]); out) {


                // Define Roster and read all students
                //std::vector roster(std::istream_iterator<Student>(in), {});

                // Write complete roster to out file
                //std::copy(roster.begin(), roster.end(), std::ostream_iterator<Student>(out, "\n"));

                // Copy complete input to output.
                std::copy(std::istream_iterator<Student>(in), {}, std::ostream_iterator<Student>(out, "\n"));


            } else {    std::cerr << "\nError: Could not open output file '" << argv[2] << "'\n";}
        } else {    std::cerr << "\nError: Could not open input file '" << argv[1] << "'\n"; }
    } else {    std::cerr << "\nError: Please call program with 2 filenames for input data and output data\n\n";}

    return 0;
}

What a pity that you will most probably continue with your new solution . . .

A M
  • 14,694
  • 5
  • 19
  • 44
  • As mentioned in the main comments, allocating memory using the `for` loop is not a good approach. If indeed `std::vector` can't be used [here is a much better](https://stackoverflow.com/questions/21943621/how-to-create-a-contiguous-2d-array-in-c/21944048#21944048) way to allocate a 2D array using `new[]`. – PaulMcKenzie Jan 25 '20 at 13:11
  • @Paul: It is not my intention to recommend the usage of new or raw pointers. On the contrary. My statement is: Do **NEVER** use raw pointers for owned memory and do **NEVER** use ````new````. If ````new````, then only in conjunction with ````std::unique_ptr````. In my post, I first fixed the original code, then added some improvements and then I showed the C++ solution that I recommend. Problem is, newbies as the OP will not understand more sophisticated C++, even with good explanation. And please forgive me, I would not use the linked wrapper for the 2d array. But, everybody can do whatever. – A M Jan 25 '20 at 14:29