0

I have set up a dynamic 2d array, the user would input a number to set n-1 amount of rows, then input a string, in which an example string input would be:


"shipment1,20180208,4" and "shipment2,20180319,5" and so on. (format will always be this way)


There are no white space separators after commas so I was wondering, if I were to add the 4 and 5, would strtok or something token related work? First split them into 3 tokens (after comma) and perhaps use atoi to concatenate?
I'm just starting out and also haven't found much about said topic specifically, would deeply appreciate if someone had a general idea, thanks!

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

int main()
{
 int n = 0;
 cin >> n; //int to set size

//allocate 2d array with varying lengths
char** cppStrings = new char*[n];

for(int i = 0; i < n; i++)
{
    cppStrings[i] = new char[100];
}

//input all the strings into the array
for(int i = 0; i < n; i++)
{
    cin.getline(cppStrings[i], 100);
}

//outputs the strings just to see
for(int i = 0; i < n; i++)
{
    cout << cppStrings[i] << endl;
}


//deallocates the array
for(int i = 0; i < n; i++)
{
    delete [] cppStrings[i];
}
delete [] cppStrings;
}
Jason
  • 1
  • 1
  • 1
  • use stringstream. – perreal Nov 18 '18 at 04:23
  • `atoi` is a C function that provides absolutely 'zero' error checking on the conversion and should not be used. If you are simply looking to handle the data, then there is no need for conversion from a string to a numeric value, unless you need the value for computation purposes. – David C. Rankin Nov 18 '18 at 06:46

2 Answers2

0

Based on your comment I am updating my answer. Instead of using char*, array of char* better would be to use std::strings, std::vector to simplify the code. However the main logic is to split the string using comma (as you mentioned above) and parse the data properly. Here is the code :

#include<iostream>
#include<string>
#include <vector>
#include <map>

using namespace std;
std::vector<std::string> split(const std::string& s, char seperator)
{
    std::vector<std::string> output;

    std::string::size_type prev_pos = 0, pos = 0;

    while ((pos = s.find(seperator, pos)) != std::string::npos)
    {
        std::string substring(s.substr(prev_pos, pos - prev_pos));

        output.push_back(substring);

        prev_pos = ++pos;
    }

    output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word

    return output;
}



int main()
{
    map<int, string> monthMap{ pair<int, string>(1, "January"),
        pair<int, string>(2, "February"),
        pair<int, string>(3, "March"),
        pair<int, string>(4, "April"),
        pair<int, string>(5, "May"),
        pair<int, string>(6, "June"),
        pair<int, string>(7, "July"),
        pair<int, string>(8, "August"),
        pair<int, string>(9, "September"),
        pair<int, string>(10, "October"),
        pair<int, string>(11, "November"),
        pair<int, string>(12, "December"),
    };

    map<int, int> countOfShipmentsPerMonth = { pair<int, int>(1, 0),
        pair<int, int>(2, 0),
        pair<int, int>(3, 0),
        pair<int, int>(4, 0),
        pair<int, int>(5, 0),
        pair<int, int>(6, 0),
        pair<int, int>(7, 0),
        pair<int, int>(8, 0),
        pair<int, int>(9, 0),
        pair<int, int>(10, 0),
        pair<int, int>(11, 0),
        pair<int, int>(12, 0) };

    int n = 0;
    cin >> n; //int to set size
    std::cin.ignore(1000, '\n');

    vector<string> cppStrings;
    cppStrings.resize(n);

    //input all the strings into the array
    for (int i = 0; i < n; i++)
    {
        getline(cin, cppStrings[i]);
        cin.clear();
    }

    for each (string var in cppStrings)
    {
        //split string
        vector<string> v = split(var, ',');

        //v[0] = shipment1
        //v[1] = date of shipment
        //v[2] = number of shipments

        //if date is always of format yyyymmdd then we can simply get the month for getting 5th and 6th char of date string
        string month = v[1].substr(4, 2);
        int nMonth = stoi(month);       

        //Now find the already stored value of shipment in that month and add new value to it
        std::map<int, int>::iterator it = countOfShipmentsPerMonth.find(nMonth);
        if (it != countOfShipmentsPerMonth.end())
        {
            it->second = it->second + stoi(v[2]);
        }
    }


    //print results
    for each (pair<int, int> var in countOfShipmentsPerMonth)
    {
        if (var.second != 0)
        {
            cout << monthMap.at(var.first) << " : " << var.second << endl;
        }
    }

    return 0;
}

Note : Split function is taken from this answer from stackoverflow.

  • e.g. shipment1,20180214,4 and shipment2,20180220,3 would both have dates in February. So output: February: 7 – Jason Nov 18 '18 at 04:45
0

I have set up a dynamic 2d array, ... if I were to add the 4 and 5, would strtok or something token related work?

Short answer "Yes". Before looking at an answer, let's look at what you are doing.

While there is nothing wrong with using basic types and dynamic allocation with new, you are missing all of the benefit of automatic memory management provided by C++ container types such as vector and string (as well as missing out on all the nice member functions they include) It would be far easier to declare your storage for your strings as std::vector<std::string> which would give you the ability to fill each sting and simply .push_back(string) to add it to your vector of string.

Presuming that this is an educational task and that you really do want to use char** to allow allocating a given number of pointers (determined by the number input by the user) and you then want to allocate storage to hold each string entered, you can use an approach similar to what you have tried. However, if you are dynamically allocating storage for each string, then it makes little sense to allocate a fixed number of characters (e.g. cppStrings[i] = new char[100];) If that were the case, you may as well just declare a 2D array of char.

Instead, you want to read each string (c_string) entered by the user and then determine the length of the string entered. (e.g. using strlen()) and then allocate storage for length + 1 characters (the +1 to provide storage for the nul-terminating character that terminates each c_string). Then it is just a matter of copying the string read to the new block of memory allocated and assigning the starting address for that block of memory to you next available pointer in turn.

Understand, in approaching your allocation and storage in this way, you are effectively writing your code in C, with the exception of using iostream for input/output and new/delete instead of malloc/free. Basically how C++ was used 30 years ago. As mentioned before, it's not wrong, it just loses the benefit of the past 30 years of progress in C++. Ironically, unless you create a stringstream from your buffer and tokenize using getline with a delimiter, then you will be using the C function strtok to split your string on commas. Since you have included <cstring> you may as well make use of strtok.

The approach is not far from what you have attempted. If you have determined that an array of 100 characters is sufficient for your anticipated input, then declare a fixed buffer of 100-chars to serve as your temporary read buffer to take input from the user. This allows you to take input, determine the length of the input in your buffer before allocating and copying the input to your dynamically allocated storage. With this approach you are able to size your allocated memory to exactly fit the user input.

std::strtok modifies the original string during tokenization, so make a copy of the string you will tokenize if you need to preserve the original. Also, since you have a fixed buffer available from the read of input, you can simply use that buffer to tokenize from.

Note, that there is actually no need to storage the original strings read from the user in dynamically allocated storage, you can simply read the input into the fixed buffer and tokenize from there. However, since you have started with the dynamic storage of the input, we will use that approach in an example and simply copy back to our fixed buffer to tokenize.

What you do with the tokens is up to you. You mention concatenation. If that is your goal, you can simply use a second fixed buffer of your maximum length to concatenate all c_strings with strcat (not you must either strcpy the first string or make your second buffer the empty-string before calling strcat as it requires a nul-terminated buffer to concatenate to).

Putting all the pieces together you could do something like the following:

#include <iostream>
#include <iomanip>
#include <cstring>

#define MAXS 100        /* if you need constants, #define one (or more) */
#define DELIM ",\n"

using namespace std;

int main (void) {

    int n = 0,
        nstr = 0;               /* counter for c_strings read/allocated */
    char **strings = nullptr,   /* pointer to pointer to char */
        buf[MAXS];              /* temporary buffer to hold c_string */

    cout << "enter max number of strings: ";
    if (!(cin >> n)) {          /* validate every user input */
        cerr << "error: invalid entry\n";
        return 1;
    }
    cin.getline (buf, MAXS);    /* read/discard trailing '\n' */

    strings = new char*[n];     /* allocate n pointers to char */

    /* protect pointers limit / validate read of line */
    while (nstr < n && cin.getline (buf, MAXS)) {
        size_t len = strlen (buf);          /* get length of string */
        strings[nstr] = new char[len+1];    /* allocate +1 for '\0' */
        strcpy (strings[nstr++], buf);      /* copy buf, increment nstr */
    }

    for (int i = 0; i < nstr; i++) {        /* ouput strings / tokenize */
        char *p = buf;                      /* ptr to buf, tokenize copy
                                            * (strtok modifies string) */
        strcpy (buf, strings[i]);           /* copy string[i] to buf p */
        cout << "\nstring[" << setw(2) << i << "]: " << p << '\n';
        /* example tokenizing string in p with strtok */
        for (p = strtok (p, DELIM); p; p = strtok (NULL, DELIM))
            cout << "  " << p << '\n';
    }

    for (int i = 0; i < nstr; i++)  /* free all allocated memory */
        delete[] strings[i];        /* free allocated c_strings */
    delete[] strings;               /* free pointers */
}

(note: don't forget to free the memory you allocate with new by calling delete when you are done with it)

Example Use/Output

$ ./bin/newptr2array2
enter max number of strings: 2
shipment1,20180208,4
shipment2,20180319,5

string[ 0]: shipment1,20180208,4
  shipment1
  20180208
  4

string[ 1]: shipment2,20180319,5
  shipment2
  20180319
  5

Memory Use/Error Check

In any code you write that dynamically allocates memory, you have 2 responsibilities regarding any block of memory allocated: (1) always preserve a pointer to the starting address for the block of memory so, (2) it can be freed when it is no longer needed.

It is imperative that you use a memory error checking program to insure you do not attempt to access memory or write beyond/outside the bounds of your allocated block, attempt to read or base a conditional jump on an uninitialized value, and finally, to confirm that you free all the memory you have allocated.

For Linux valgrind is the normal choice. There are similar memory checkers for every platform. They are all simple to use, just run your program through it.

$ valgrind ./bin/newptr2array2
==4344== Memcheck, a memory error detector
==4344== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==4344== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==4344== Command: ./bin/newptr2array2
==4344==
enter max number of strings: 2
shipment1,20180208,4
shipment2,20180319,5

string[ 0]: shipment1,20180208,4
  shipment1
  20180208
  4

string[ 1]: shipment2,20180319,5
  shipment2
  20180319
  5
==4344==
==4344== HEAP SUMMARY:
==4344==     in use at exit: 0 bytes in 0 blocks
==4344==   total heap usage: 4 allocs, 4 frees, 72,762 bytes allocated
==4344==
==4344== All heap blocks were freed -- no leaks are possible
==4344==
==4344== For counts of detected and suppressed errors, rerun with: -v
==4344== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Always confirm that you have freed all memory you have allocated and that there are no memory errors.

(This is where the automatic memory management of vector and the other containers make like a lot easier. They handle freeing the memory when the final reference to it goes out of scope freeing you from that responsibility.)

Look things over, there is good educational value in understanding how to properly use new/delete as there is still a substantial codebase from the past few decades that makes use of that approach. Going forward, you will want to use the container classes that C++ provides. If you have any further questions, just drop a comment and I'm happy to help further.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • thank you for the really detailed information @David, I ended up trying strtok. – Jason Nov 19 '18 at 04:00
  • Sure, it is been the backbone for splitting strings in C and C++ for decades. Nothing wrong with it, just always remember, it modifies the string (by replacing the tokens with `'\0'`) so it you need to original string after tokenizing it, make a copy first. – David C. Rankin Nov 19 '18 at 05:29