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 algorithm
s 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 . . .