1

So there seems to be some problem with the solution to Problem 9-2 in the book "Object-Oriented Programming in C++, 4th edition" by Robert Lafore. So the problem is that if I would like to create a Pstring object with a statement like Pstring = "This is a string", the Pstring constructor will only call the constructor with no arguments in the String class, instead of the second one with uses one char[] argument. Does anyone know what causes this kind of problem, and a fix to this? Thanks!

#include <iostream>
#include <cstring>
using namespace std;
////////////////////////////////////////////////////////////////
class String                      //base class
{
protected:                     //Note: can't be private
    enum {
        SZ = 80
    };           //size of all String objects
    char str[SZ];               //holds a C-string
public:
    String()                    //constructor 0, no args
    {
        str[0] = '\0';
    }
    String(char s[])          //constructor 1, one arg
            {
        strcpy(str, s);
    }      //  convert string to String
    void display() const        //display the String
    {
        cout << str;
    }
    operator char*()            //conversion function
    {
        return str;
    }          //convert String to C-string
};
////////////////////////////////////////////////////////////////
class Pstring: public String     //derived class
{
public:
    Pstring(char s[]);        //constructor
};
//--------------------------------------------------------------
Pstring::Pstring(char s[])      //constructor for Pstring
        {
    if (strlen(s) > SZ - 1)           //if too long,
            {
        for (int j = 0; j < SZ - 1; j++) {  //copy the first SZ-1
            str[j] = s[j];           //characters "by hand"
            str[j] = '\0';
        }           //add the null character
    } else
        //not too long,
        String(s);                  //so construct normally
}
////////////////////////////////////////////////////////////////
int main() {                                        //define String
    String s1 = "This is a string"; // This works great
    s1.display();
    Pstring s2 = "This is a string"; // *** Here, nothing will be assigned to s2****
    s2.display();                    // *** Nothing will be printed here***
    return 0;
}
Grant
  • 13
  • 3
  • 2
    Use `const char *` instead of `char[]`. – 1201ProgramAlarm May 30 '20 at 20:32
  • 1
    `String(s);` in `Pstring` constructor doesn't do what you think it does. It does **not** delegate to a base class constructor. Rather, it creates an immediately discards a temporary unnamed object of type `String`. If this is the actual code from a book, return it to the seller and demand a refund. – Igor Tandetnik May 30 '20 at 20:36
  • Your string class is not designed to serve as a baseclass. Read about the "Law of Five" ("Law of Three" for C++98) and "slicing" or "truncating". – Ulrich Eckhardt May 30 '20 at 20:38

3 Answers3

1

In a function parameter, a T[] (where T is char in your case) is really a T*.

In C++, a string literal is a const char[N] fixed array, which decays into a const char* pointer to the 1st element. But you don't have any constructors that accept either of those types as a parameter. A const char* can't be given to a char*. You need to add const to your constructors:

String(const char s[])

Pstring(const char s[])

Also, calling String(s) in the body of the Pstring constructor does not initialize the Pstring object using the base class String constructor, like you are expecting. It instead constructs a temporary String object that goes out of scope immediately. The Pstring object is not affected by that.

The only place that a base class constructor can be called by a derived constructor is in the member initialization list. In your case, there is no such call, so the compiler implicitly calls the base class default (0-param) constructor before entering the body of the derived constructor. Which doesn't help you, since you want the base class to initialize the str buffer with data.

One way you can do that is add another constructor to String that takes a user-defined length as input, and then call that from the Pstring constructor, eg:

String(const char s[], size_t len)
{
    len = std::min(len, SZ-1);
    memcpy(str, s, len);
    str[len] = '\0';
}

Pstring::Pstring(const char s[])
    : String(s, strlen(s))
{
}

Note that your 1-param String constructor has a buffer overflow waiting to happen, since the user can directly construct a String object with input that is greater than SZ characters in length. The String constructor should use strncpy() instead of strcpy():

String(const char s[])
{
    strncpy(str, s, SZ);
    str[SZ-1] = '\0'; // in case s is >= SZ chars
}

Which then makes the 1-param Pstring constructor redundant - especially since it is not handling the null terminator correctly to begin with, as the assignment of the terminator needs to be outside of the for loop, eg:

Pstring::Pstring(const char s[])
{
    if (strlen(s) >= SZ)
    {
        for (int j = 0; j < SZ - 1; j++) {
            str[j] = s[j];
        }
        // alternatively: memcpy(str, sz, SZ-1);
        str[SZ-1] = '\0'; // <-- moved here
    }
    else
        strcpy(str, s);
}
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 3
    Can someone explain the downvote? This looks correct to me ... – ChrisMM May 30 '20 at 20:45
  • 1
    @ChrisMM - I didn't downvote, but this answer was edited in the grace period. It initially contained only the bit about `const` and string literals. The OP's compiler is offering an extension, obviously, but that was completely beside the point of the question. I would assume the downvote was due to this. – StoryTeller - Unslander Monica May 30 '20 at 20:48
  • How does the missing const lead to "_the Pstring constructor will only call the constructor with no arguments in the String class, instead of the second one with uses one char[] argument_"? – juanchopanza May 30 '20 at 20:53
  • @juanchopanza, that's in the paragraph starting with "Also, calling `String`..." although, indirectly. – ChrisMM May 30 '20 at 20:56
  • @ChrisMM I don't see the role of the missing `const`. – juanchopanza May 30 '20 at 20:59
  • 2
    @juanchopanza a derived constructor *implicitly* calls a base class's default (0-param) constructor if there is no *explicit* call to a base class constructor in the derived constructor's member initialization list – Remy Lebeau May 30 '20 at 21:01
  • @RemyLebeau I know that. But what does the missing const have to do with it? – juanchopanza May 30 '20 at 21:07
  • @juanchopanza nothing at all, because a statement like `Pstring s2 = "This is a string";` will simply fail to compile altogether if a matching constructor can't be found. – Remy Lebeau May 30 '20 at 21:10
  • That's what I thought. – juanchopanza May 30 '20 at 21:11
  • @juanchopanza, but even that's had nothing to do with the failure of matching constructor, cost char* points to immutable string and char* points to mutable string, they behave differently, and using cost char* instead of cost char [] or char* instead of char [] is more efficient for many operations. – 4.Pi.n May 30 '20 at 21:23
  • @RemyLebeau If it simply failed to compile, how would the default constructor end up being called? – juanchopanza May 30 '20 at 21:25
  • @juanchopanza it wouldn't be called at all, since it wouldn't compile. But if it did compile, then the default constructor would be called *implicitly*, like I explained earlier. – Remy Lebeau May 30 '20 at 21:28
  • @RemyLebeau But you're answering a question about observed runtime behaviour. – juanchopanza May 30 '20 at 21:31
  • @juanchopanza re-read my earlier statements again more carefully – Remy Lebeau May 30 '20 at 21:32
  • @RemyLebeau I have. Seriously, they don't make much sense. – juanchopanza May 30 '20 at 21:35
1

In this conversion constructor

Pstring::Pstring(char s[])      //constructor for Pstring
        {
    if (strlen(s) > SZ - 1)           //if too long,
            {
        for (int j = 0; j < SZ - 1; j++) {  //copy the first SZ-1
            str[j] = s[j];           //characters "by hand"
            str[j] = '\0';
        }           //add the null character
    } else
        //not too long,
        String(s);                  //so construct normally
}

at first the default constructor of the class String is called before the control will be passed to the constructor of the class Pstring.

So the data member is set like

String()                    //constructor 0, no args
{
    str[0] = '\0';
}

As the argument that is the string literal "This is a string" that by the way as the argument has the type const char * due to the implicit conversion of arrays to pointers has the length that is less than SZ then within the body of the constructor Pstring nothing is done with the data member str. This statement

String(s);

creates a temporary object of the type String that is at once deleted.

What you need is to write at least

strcpy( str, s );

instead of creating the temporary object.

Pay attention to that the constructors with parameters shall be declared like

String( const char s[] );

and

Pstring( const char s[]);

if you are going to use string literals as arguments of the constructors.

You could move this code snippet

if (strlen(s) > SZ - 1)           //if too long,
        {
    for (int j = 0; j < SZ - 1; j++) {  //copy the first SZ-1
        str[j] = s[j];           //characters "by hand"
        str[j] = '\0';
    }           //add the null character
} else
    //not too long,
    String(s);                  //so construct normally

form the constructor Pstring to the constructor String with parameter and substitute it for one call of strncpy like

strncpy( str, s, SZ - 1 );
str[SZ-1] = '\0';
Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
1

In C++, constructors aren't allowed to be called like this:

else
    //not too long,
    String(s);     

C++ wants you to use its initialization list instead (see the link above for some examples).

If you have a portion of the construction in the parent class you would like to call from inside the child constructor, you can use a protected method instead:

class String                      //base class
{
protected:
    void commonTask(char s[]) {
        // do something...
    }
public:
    String(char s[])
    {
        commonTask(s);
    }
};

class Pstring: public String
{
public:
    Pstring(char s[]) {        //constructor
        if(someCondition) {
            commonTask(s);
        }
    } 
};

I'm using pseudo code here, but hopefully you get the idea.