0

I've written a custom string class called DSString, with a private member char* data. I'm trying to parse lines from a csv file, adding each word found in the line of a file to a std::set of DSString objects. I call getline to read in a line from the file into a char* buffer, then call strtok to get a word. However, the while loop below does not work; my debugger tells me that upon reaching that line of code, it calls my constructor in DSString.h with parameter NULL, then the program ends.

int main (int argc, char ** argv) {
    
    ifstream input;
    input.open(argv1);

    int reviewLinesCtr = 0;
    char line[16000];

    set <DSString>positiveWordSet;

    while (input.getline(line,16000)) {
        if (reviewLinesCtr == 10) break;

            //Push review to positive set
            DSString word = strtok(line, " ");
            while (word != nullptr) {         //Program ends abruptly here
                positiveWordSet.insert(word);
                word = strtok(nullptr, " ");
            }
        }
        reviewLinesCtr++;
    }

    return 0;
}

DSString only has private members char *data and int size. Here is the relevant code in DSString:

    DSString::DSString(const char* param) {  //This is the constructor being  passed, param = NULL, which crashes the program.
        data = new char[strlen(param) + 1];
        this->size = strlen(param) + 1;
        strcpy(data, param);
    }

    DSString& DSString::operator= (const char* source)  {
        if (data != nullptr) {
            delete[] data;
        }
        data = new char[strlen(source + 1)];
        size = strlen(source) + 1;
        strcpy(data, source);
        return *this;
    }

    bool DSString::operator!= (const DSString& rhs) const {
        if (strcmp(this->data, rhs.data) != 0) {
            return true;
        }
        else {
            return false;
        }
    } 

This is part of a project for college, so we are required to implement the custom DSString class. Otherwise I would not use it and instead use std::string. We also cannot use a char* anywhere in the program except for a buffer to read from the file. Why is this constructor being called and given NULL as a parameter when I try to start going through this while loop? And how can I fix it?

I apologize if this is a repeat question, I have been searching the internet for some time and have not found anything that helps explain what's going on here. Any help would be appreciated.

  • `DSString word = strtok(line, " ");` will invoke [Copy Initialization](https://en.cppreference.com/w/cpp/language/copy_initialization) – user4581301 Sep 13 '20 at 03:30
  • What is your proof that in the following line: `word = strtok(nullptr, " ");`, that this call to `strtok` will never return `NULL`, thus passing a null pointer to your constructor or `operator=`, resulting in the observed segfault? (Hint: you have no proof of that). P.S. Your `operator=` has a nasty memory leak. – Sam Varshavchik Sep 13 '20 at 03:32
  • `strtok` is _rarely_ used in C++, so, just try to live without it. – Ted Lyngmo Sep 13 '20 at 03:32
  • Your assignment operator leaks memory. The problem is that you're trying to use a broken class within an application that requires `DSString` has correct copy semantics, and `DSString` does not have correct copy semantics. – PaulMcKenzie Sep 13 '20 at 03:57
  • `set` -- You really need to post your entire `DSString` class. Assuming this is `std::set`, as it stands now, this will fail miserably since the `DSString` class does not have correct copy semantics, thus your runtime errors could stem from multiple errors in the code. – PaulMcKenzie Sep 13 '20 at 04:32

2 Answers2

2

The cause of the crash is most likely outlined in eeorika's answer. No point repeating it here.

Why the constructor is called:

DSString word = strtok(line, " ");

Is Copy Initialization, so the constructor, not the assignment operator, is called.

Side notes:

There is another fatal error. Since strtok returns nullptr when there are no more tokens, DSString::DSString(const char* param) will be called if there are no tokens and

data = new char[strlen(param) + 1];

will invoke strlen on a null pointer. Kabooom.

Solution: Capture strtok's return value in a const char * variable and test it for nullptr. Only proceed with construction if there is a token.

Since the assignment requirements are that you cannot use a char * variable, confirm whether or not you can use a const char *. And if not, well that's an interesting (and really stupid) pickle.

The assignment operator leaks memory.

DSString& DSString::operator= (const char* source)  {
    delete[] data; // must release the object's current string
                   // before allocating a new one
    data = new char[strlen(source + 1)]; 
    size = strlen(source) + 1;
    strcpy(data, source);
    return *this;
}
user4581301
  • 33,082
  • 7
  • 33
  • 54
  • ... and test for "_nullptr_" would be a better advice. The rest of the sliding scale isn't hard. Just `static_assert`. – Ted Lyngmo Sep 13 '20 at 03:47
  • @TedLyngmo The only useful thing this answer really does is explain why the constructor is called rather than the assignment operator. I think I'll delete the sucker and add a comment under eerorika's answer. I totally missed the problem in the while loop. And the leak in `DSString::operator=` – user4581301 Sep 13 '20 at 03:51
  • Don't delete it! I personallty think it's usefull - I just don't know if it fits OP's question yet. – Ted Lyngmo Sep 13 '20 at 03:53
  • Thanks for your answer and for pointing out the memory leak. I replied to eeororika saying this as well, but I don't think i can use const char * either. Which is dumb, but it's the requirement I've been given. Otherwise this would be much easier to solve – Landon Wood Sep 13 '20 at 04:16
  • @LandonWood consult your instructor to be absolutely certain before that is a hard requirement before you spend time working around it. Any solution that doesn't capture and test will be teaching you to be a worse programmer. Perhaps the expectation is throw an exception, but you'll have a null pointer every time at the last token and always happens is about as far from exceptional as it gets. You can have "empty" `DSString`s, but then you have to test every `DSString` every time you use it to make sure it's not empty, defeating the point of a constructor. – user4581301 Sep 13 '20 at 04:29
2
DSString word = ....
while (word != nullptr) {         //Program ends abruptly here

Here, you compare a DSString object to nullptr. Let's look at the operator overaload:

bool DSString::operator!= (const DSString& rhs) const {

It accepts a DSString reference, not a pointer. However, your class is implicitly convertible from a pointer and thus indirectly from astd::nullptr_t because it has a converting constructor:

SString::DSString(const char* param) {  //This is the constructor being  passed, param = NULL, which crashes the program.

Since the constructor calls strlen(param) and strcpy(data, param);, passing null to that constructor has undefined behaviour.

To avoid accidentally calling this constrcutor with an implicit conversion, you should declare it explicit.

Passing null to the constructor is also a problem here: DSString word = strtok(line, " ");.

To fix the logic, you should use a pointer with strtok, and only create your string after you've checked that it's non-null:

        char* word = strtok(line, " ");

We also cannot use a char* anywhere in the program except for a buffer to read from the file.

Frankly, using strtok but not using char* is just silly requirement.

Seems like you already violate this requirement:

DSString only has private members char *data and ...

Regardless, if you need to follow such restriction, you need to change the constructor to behave correctly when null is passed into it. Specifically, it may not pass that pointer into functions which have undefined behaviour in such case.

And, if you set the member null, then you must also check whether the member is null before passing it to such functions mentioned above.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • strtok is not a requirement, it's just the only way I could think of to accomplish this task. I tried using a stringstream but could not get that to work either. The DSString member data is allowed to be a char* - sorry for the misleading wording. That and the file buffer are the only two things allowed to be char*. So I can't implement your ```char* word = strtok(line, " ");``` . I understand now why the constructor is behaving this way, thank you. But if I change the constructor to be explicit, then I have to change the != operator as well, right? Agreed, requirements r silly :) – Landon Wood Sep 13 '20 at 04:13
  • @LandonWood Until you fix the copy semantics for your `DSString` class, it really is a moot point about `strtok` and what the other members are. – PaulMcKenzie Sep 13 '20 at 04:16
  • @PaulMcKenzie I edited the OP with what I perceived to be the right fix to the copy assignment operator, based on user4581301 's answer. Does that follow correct copy semantics? Sorry, I'm not familiar with the term and what exactly it implies... – Landon Wood Sep 13 '20 at 04:22
  • No it doesn't. Your class fails to implement the [rule of 3](https://stackoverflow.com/questions/4172722/what-is-the-rule-of-three). Go to the **Managing resources** section of that link and read it carefully. – PaulMcKenzie Sep 13 '20 at 04:27