CS202 Lecture notes -- Constructors, Destructors and the Assignment Overload

  • James S. Plank
  • Directory: ~jplank/cs202/notes/Constructors
  • Lecture notes: http://web.eecs.utk.edu/~jplank/plank/classes/cs202/Notes/Constructors
  • Last modification date: Mon Sep 23 15:14:17 EDT 2019
    We've already gone over basic constructors in the lecture on classes. In this lecture, I'm going to give them a more complete treatment, along with destructors and the assignment overload.

    Constructors, Parameters, Exceptions

    The most basic constructor is one that does not take any parameters. It is called whenever you call a procedure that has the class declared as a local variable, and whenever you create a new instance of a class with new. Alternatively, you can define a constructor that takes parameters. To call this constructor, you need to put the parameters into the declaration of the local variable, or when you make the new call.

    The program src/const1.cpp defines a class called My_Class, which has two constructors -- one with parameters and one without. Both constructors print out the fact that they have been called. Then both the main() and proc() procedures declare two instances of My_Class. This means that the constructors will be called as soon as the procedures are called. They both print some statements, and main() also creates some My_Class instances with new:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    /* I'm declaring this class to have two constructors --
       one without parameters, and one with parameters. */
    
    class My_Class {
      public:
        My_Class();
        My_Class(int a, const string &s);
    };
    
    /* The constructor without parameters prints the string,
       "Constructor called without parameters. */
    
    My_Class::My_Class()
    {
      cout << "Constructor called without parameters" << endl;
    }
    
    /* The second constructor prints out its parameters. */
    
    My_Class::My_Class(int a, const string &s)
    {
      cout << "Constructor called with parameters " 
           << a << " and " << s << endl;
    }
    
    /* The procedure proc() simply declares two
      instances of My_Class, prints a line and exits.  */
    
    void proc()
    {
      My_Class c3;
      My_Class c4(30, "Baby-Daisy");
    
      cout << endl << "proc begins and ends here." << endl;
    }
    
    /* Main has two My_Class's declared as local
      variables, and two pointers, where the My_Class
      instances are created with new. */
    
    int main()
    {
      My_Class c1;
      My_Class c2(5, "Fred");
      My_Class *cp1, *cp2;
    
      cout << endl << "Main is starting" << endl;
      cout << "calling: cp1 = new My_Class;" << endl;
      cp1 = new My_Class;
    
      cout << "cp2 = new My_Class(10, \"Luther\");" << endl;
      cp2 = new My_Class(10, "Luther");
    
      cout << "Calling proc():" << endl << endl;
      proc();
      return 0;
    }
    

    When we run it, it runs to expectation, but you may find it confusing -- I've annotated the output:

    UNIX> bin/const1
    Constructor called without parameters               # These are when main() starts running.
    Constructor called with parameters 5 and Fred
    
    Main is starting                                    # Here's the main body of main().
    calling: cp1 = new My_Class;
    Constructor called without parameters
    cp2 = new My_Class(10, "Luther");
    Constructor called with parameters 10 and Luther
    Calling proc():
    
    Constructor called without parameters               # These are when proc() starts running.
    Constructor called with parameters 30 and Baby-Daisy
    
    proc begins and ends here.                          # And here's the body of proc().
    UNIX> 
    
    To handle errors in a constructor, you need to throw an exception. To demonstrate, take a look at src/const2_exception.cpp:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    /* I've gotten rid of the first constructor, and I'm
       making the second constructor throw an exception
       when its parameter a is zero. */
    
    class My_Class {
      public:
        My_Class(int a);
    };
    
    My_Class::My_Class(int a)
    {
      printf("Constructor.  A = %d\n", a);
      if (a == 0) throw ((string) "Throwing exception");
    }
    /* Proc declares two My_Classes -- one with 
       a value of 1 and a second one with a value 
       of zero.  It does a try/catch, but neither 
       will get called because c2's declaration
       throws an exception.  */
    
    
    void proc()
    {
      My_Class c1(1);
      My_Class c2(0);
    
      try {
        cout << "In proc -- try\n";
      } catch (const string s) {
        cout << "In proc -- catch " << s << endl;
      }
    }
    
    /* Main calls proc(), and catches exceptions. */
    
    int main()
    {
      try {
        proc();
        cout << "This line won't print" << endl;
      } catch (const string s) {
        cout << "Caught " << s << endl;
      }
      return 0;
    }
    

    When we call this, the flow of control works as follows:

    Here it is running:
    UNIX> bin/const2_exception 
    Constructor.  A = 1
    Constructor.  A = 0
    Caught Throwing exception
    UNIX> 
    
    This may seem confusing to you. Make sure you can trace through code like this.

    Destructors

    You can define a destructor for a class. This is defined similarly to the constructor, but you precede the method's name with a tilde "~". Destructors are called primarily in two places: Let's demonstrate with src/const3_destructor.cpp. This is identical to src/const1.cpp, except for the following things: When we run this -- you can see that the destructor is called five times. I'll annotate:
    UNIX> bin/const3_destructor
    Constructor called without parameters
    Constructor called with parameters 5 and Fred
    
    Main is starting
    calling: cp1 = new My_Class;
    Constructor called without parameters
    cp2 = new My_Class(10, "Luther");
    Constructor called with parameters 10 and Luther
    Calling proc():
    
    Constructor called without parameters
    Constructor called with parameters 30 and Baby-Daisy
    
    proc begins and ends here.
    Destructor called.                   # These two are for cp3 and cp4 in proc()
    Destructor called.
    
    Calling delete on cp1
    Destructor called.                   # This is from the delete call
    
    Main is returning
    Destructor called.                   # These two are for cp1 and cp2 in main()
    Destructor called.
    UNIX> 
    
    You'll note that no destructor was called for cp2. That's because we never called delete on it. So we have a few important thing about destructors:

    Destructors and exceptions

    Time to have some fun. Behold src/const4_dest_except.cpp. Read the opening block comment for a description:

    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    /* My_Class has a string, which it sets in the constructor. 
       It also has a destructor that prints the string.  This 
       program will make a few nested procedure calls, each of
       which has four My_Class local variables.  In the deepest
       nesting, I throw an exception, which I catch in main().
       I do this to show how all of the destructors get called
       as the exception travels back to main(). */
    
    class My_Class {
      public:
        My_Class(const string &s);
        ~My_Class();
        string my_string;
    };
    
    /* The constructor sets my_string, 
       and the destructor prints it. */
       
    My_Class::My_Class(const string &s)
    {
      my_string = s;
    }
    
    My_Class::~My_Class()
    {
      cout << "Destructor called. my_string = " 
           << my_string << endl;
    }
    
    /* p1, p2, p3 declare four My_Class's.  
       p3 throws an exception. */
    
    void p3()
    {
      My_Class c1("p3_c1"), c2("p3_c2"); 
      My_Class c3("p3_c3"), c4("p3_c4");
      throw(0);
    }
    void p2()
    {
      My_Class c1("p2_c1"), c2("p2_c2"); 
      My_Class c3("p2_c3"), c4("p2_c4");
      p3();
    }
    
    void p1()
    {
      My_Class c1("p1_c1"), c2("p1_c2"); 
      My_Class c3("p1_c3"), c4("p1_c4");
      p2();
    }
    
    /* Finally, main() allocates one 
       My_Class, then calls p1(), 
       catching the exception. */
    
    int main()
    {
      My_Class c1("main_c1");
    
      try {
        p1();
      } catch (int i) {
        cout << "Caught " << i << endl;
      }
    
      return 0;
    }
    

    When we call this, you'll note:

    I do this to highlight the fact that when the exception is thrown, the following things happen:
    UNIX> bin/const4_dest_except
    Destructor called. my_string = p3_c4
    Destructor called. my_string = p3_c3
    Destructor called. my_string = p3_c2
    Destructor called. my_string = p3_c1
    Destructor called. my_string = p2_c4
    Destructor called. my_string = p2_c3
    Destructor called. my_string = p2_c2
    Destructor called. my_string = p2_c1
    Destructor called. my_string = p1_c4
    Destructor called. my_string = p1_c3
    Destructor called. my_string = p1_c2
    Destructor called. my_string = p1_c1
    Caught 0
    Destructor called. my_string = main_c1
    UNIX> 
    
    Starting in C++ 11, you can't catch an exception thrown in a destructor. It has to do with the program that you just saw -- if you need to call those destructors while catching an exception, then you shouldn't be throwing a new exception. The impact: Just don't throw an exception from a destructor.

    If you want to read typical arrogant and abusive blather on the topic, just Google "why can't i throw an exception in a destructor in C++" and read the prose on Stack Overflow...


    Vectors, Constructors and Destructors

    Suppose I make a vector of My_Class objects through resizing, and I don't specify a default. Then the constructor is called on every element of the vector, and when the procedure exits, the destructor is called on every element as well. I'll illustrate with src/const5_vector1.cpp:

    /* We resize a vector of My_Class elements and 
       exit, just to see what the constructors and
       destructors do */
    
    class My_Class {
      public:
        My_Class();
        ~My_Class();
    };
    
    My_Class::My_Class()
    {
      cout << "Constructor called." << endl;
    }
    
    My_Class::~My_Class()
    {
      cout << "Destructor called." << endl;
    }
    
    int main()
    {
      vector <My_Class> v;
    
      v.resize(5);
      return 0;
    }
    

    Here we go -- five constructor calls, and five destructor calls:

    UNIX> bin/const5_vector1 
    Constructor called.
    Constructor called.
    Constructor called.
    Constructor called.
    Constructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    UNIX> 
    
    Now, suppose we resize by giving a default value. This is done in src/const6_vector2.cpp:

    /* Now we resize with a default value. */
    
    class My_Class {
      public:
        My_Class();
        ~My_Class();
    };
    
    My_Class::My_Class()
    {
      cout << "Constructor called." << endl;
    }
    
    My_Class::~My_Class()
    {
      cout << "Destructor called." << endl;
    }
    
    int main()
    {
      My_Class c;
      vector <My_Class> v;
    
      // We resize with a default
      v.resize(5, c); 
      return 0;
    }
    

    When we run it, we only get one constructor call, and six destructor calls:

    UNIX> bin/const6_vector2
    Constructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    UNIX> 
    
    What gives? Well.....

    The Copy Constructor

    You can define what's called a "copy constructor." It takes a parameter, which is a const reference parameter to an instance of the same type. This is what's called when you resize with a default. The program src/const7_copy.cpp demonstrates:

    /* This program explores the copy constructor. */
    
    class My_Class {
      public:
        My_Class();
        My_Class(const My_Class &mc);
        ~My_Class();
    };
    
    /* This is the copy constructor, which is called
       when a vector is resized with a default, or 
       when you call a procedure with an instance of
       the class as a non-reference parameter. */
    
    My_Class::My_Class(const My_Class &mc)
    {
      (void) mc;
      cout << "Copy Constructor called." << endl;
    }
    
    My_Class::My_Class()
    {
      cout << "Constructor called." << endl;
    }
    
    My_Class::~My_Class()
    {
      cout << "Destructor called." << endl;
    }
    
    void proc1(My_Class c)
    {
      (void) c;
    }
    
    void proc2(My_Class &c)
    {
      (void) c;
    }
    
    void proc3(const My_Class &c)
    {
      (void) c;
    }
    
    int main()
    {
      My_Class c;
      vector <My_Class> v;
    
      v.resize(5, c);
      cout << "vector resized" << endl;
      proc1(c);
      proc2(c);
      proc3(c);
      
      cout << "returning" << endl;
      return 0;
    }
    

    You'll note that in addition to the copy constructor, I have three procedures:

    When we run it, we see that the copy constructor is called five times when we resize the vector, and then once when we call proc1():
    UNIX> bin/const7_copy
    Constructor called.                 # This is for c
    Copy Constructor called.            # These five are when we resize the vector
    Copy Constructor called.
    Copy Constructor called.
    Copy Constructor called.
    Copy Constructor called.
    vector resized
    Copy Constructor called.            # This is when proc1() is called - we copy the argument
    Destructor called.                  # This is when proc1() returns
    returning                           # Main() prints this right before it returns
    Destructor called.                  # There are six of these - 5 for the vector, and one for c
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    Destructor called.
    UNIX> 
    
    If you don't define a copy constructor, a default is defined for you, that simply copies each member variable of the class. Typically, that's what you want, so you don't define a copy constructor. Think about that in our bitmatrix lab -- the only member variable is a vector M. The copy constructor copies that vector, so it does exctly what you want. If your class does some weird stuff, (typically involving pointers or open files), you'll want to rewrite the copy constructor.

    This

    When you are implementing a method in C++, you can access a pointer to the instance of your class with this. Let me give a really simple example, which is not a good example of program design, but at least illustrates this. Here we have a class Person with member variables Name and Age. There is also a Print() method. We have an additional procedure Print_Person() which takes a pointer to a Person, and prints it out with some formatting.

    To implement the Print() method, we simply call Print_Person() on this, which is a pointer to the class. Take a look (in src/this.cpp).

    /* A program to demonstrate "this" */
    
    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    class Person {
      public:
        string Name;
        int Age;
        void Print(); 
    };
    
    /* Print_Person() takes a pointer to a person, and prints out
       the person with some formatting. */
    
    void Print_Person(Person *p)
    {
      printf("Person: %s - Age = %d\n", p->Name.c_str(), p->Age);
    }
    
    /* The Print() method uses Print_Person() -- this is a pointer
       to the instance which is calling Print(). */
    
    void Person::Print()
    {
      Print_Person(this);
    }
    
    /* In main(), we read in pairs of (name,age), and then use the
       Print() method of the Person class to print the person. */
    
    int main()
    {
      Person p;
      
      while (cin >> p.Name >> p.Age) p.Print();
      return 0;
    }
    

    We run it by passing two pairs of names and ages to standard input -- as you can see, the Print() method calls Print_Person(), using this as a pointer to the Person calling Print():

    UNIX> echo Fred 5 Luther 55 | bin/this
    Person: Fred - Age = 5
    Person: Luther - Age = 55
    UNIX> 
    

    Overloading the assigment operator

    C++ allows you to "overload" operators -- this allows you do to things like define what "+" means for a class. Sometimes that's nice (like having "+" be string concatenation in the string class). More often than not, it's confusing, which is why I don't teach much concerning operator overloading.

    Much like the copy constructor, each class has a default assignment operator. This is the equals sign. If you say:

    a = b;
    

    Where a and b are instances of a class, the default action is for a to receive a copy of each of b's member variables. More often than not, this is what you want. However, there are times when it's not, and when that's the case, you can redefine it. Let me show you an example, which is contrived, but it demonstrates how you can redefine the operator. It's in src/overload.cpp:

    /* A program to show an assignment overload */
    
    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    class Crazy_Class {
      public:
        int value;
        Crazy_Class& operator= (const Crazy_Class &cc);
    };
    
    /* The assignment overload copies value+1 from the original class. */
    
    Crazy_Class& Crazy_Class::operator= (const Crazy_Class &cc)
    {
      value = cc.value+1;
      return *this;
    }
    
    int main()
    {
      Crazy_Class c1, c2;
    
      c1.value = 5;
      c2 = c1;                    // This will call the overload, and set c2.value to 6.
      cout << c2.value << endl;   // This prints 6.
    }
    

    The assignment overload copies value+1 to value. So, when you say "c2 = c1", it calls the assignment overload, and sets c2.value to (c1.value+1)=6. Again, this example is contrived, but there are times, again when you're using pointers or open files in a class, that you'll want to redefine the assignment overload.

    UNIX> bin/overload
    6
    UNIX> 
    

    Some Opinions From Dr. Plank

    As you know, I'm pretty opinionated when it comes to programming. I've been doing it forever, and my experience with that, combined with developing large open-source programming projects, combined with doing code review as an expert witness, has given me quite a bit of perspective.

    I'm going to give you some opinions and recommendations now, with respect to C++ and object oriented programming. You don't have to heed these, but you should take serious consideration of them, and think about why I make them. I'll tell you that I am strict with respect to these, and I expect the students who work for me to be strict as well. Here we go:

    1. Variable declarations should come before code, and not be intermixed.

      In both C and C++, you can declare variables pretty much anywhere. I declare variables at the top of my procedures, before any code. That way, you know where to look for them, and you know that they are not undergoing any changes until the first line of real code. It makes the code readable, and easy to reason about. I am sure I'm in the minority here, but again, I have a lot of experience. When you read my code, you know where to find stuff.

      It's much more convenient for the programmer to violate this. Let's take stringstreams for example. When I read a line of text and convert it to a vector of strings, I do the following:

      int main()
      {
        istringstream ss;
        vector <string> sv;
        string line, word;
      
        while (getline(cin, line)) {
          ss.clear();
          sv.clear();
          ss.str(line);
          while (ss >> word) sv.push_back(word());
          ...
      

      It would be more convenient for me to simply declare ss and sv inside the loop:

      int main()
      {
        string line, word;
      
        while (getline(cin, line)) {
          istringstream ss;
          vector <string> sv;
          ss.str(line);
          while (ss >> word) sv.push_back(word);
          ...
      

      Now I don't have to call clear(). I can tell you I intensely dislike code like this -- again, I want declarations before code, and I don't want those declarations to be executing things. Then you know where to find stuff, and you have reasonable assurances what the state of the stuff is when the first line of code is executed.

      Let me give you a really extreme example, in src/code_in_declarations.cpp:

      #include <iostream>
      using namespace std;
      
      /* Here's a program where the constructor has side effects on its parameter.
         By the time you're done declaring parameters, the value of i is 105. */
      
      class My_Class {
        public:
          My_Class(int &i);
      };
      
      My_Class::My_Class(int &i)
      {
        i += 50;
      }
      
      int main()
      {
        int i = 5;
        My_Class c1(i), c2(i);
        
        cout << i << endl;
        return 0;
      }
      

      As mentioned in the comments, by the time you print i, the two constructors have changed its value to 105. You may say that this is an extreme example, but once you start intermixing variable declarations, code, and complex constructor operations, anything goes, and you can get code like this.

    2. Class declarations should have no executable code. That includes constructors that define defaults. This falls in with the above comments -- when class declarations don't have code, then you don't have surprises, and you know where to look for things. You'll note, that includes default-setting constructors, like:

      class My_Class {
        public:
          My_Class() : i(5) {};
        protected:
          int i;
      };
      

    3. You should avoid parameterizing constructors, unless you use a class exclusively with pointers.

      This can be derived from the first recommendation. If you declare an instance of a class as a local variable with parameters, you are executing code. Often, you can't even declare it until you have executed some code that lets you set its parameters. Here's an example -- recall from the Bitmatrix lab, that we defined a hash table of bitmatrices:

      class BM_Hash {
        public:
          BM_Hash(int size);
        ...
      

      Now, suppose you want to create a hash table, and read the table's size from standard input. When you use a pointer, the code is nice and clean:

      void main()
      {
        int table_size;
        BM_Hash *hash_table;
      
        cin << table_size;
        hash_table = new BM_Hash(table_size);
        ....
      

      If you don't want to use a pointer (and I know that y'all students don't want to use pointers), you are forced to declare hash_table in the middle of your code:

      void main()
      {
        int table_size;
      
        cin << table_size;
        BM_Hash hash_table(table_size);
        ....
      

      Convenient? Yes. Easy to read and reason about? No. I don't do it.

    4. If you are going to declare instances of a class as local variables, don't parameterize the constructor. That way, you can declare it wherever you want, and you don't have to worry about errors.

      This may be inconvenient, because then you have to account for an "empty" instance of the class. Let's use the hash table above to exemplify. Its constructor takes a parameter - the table size. For that reason, you never have an empty hash table. On the flip side, if you are going to follow my rules, you'll always use a pointer to it, and you'll always have to call new to create it. Suppose instead that you don't have a parameter to the constructor. Then, you don't even need a constructor -- when you create an instance, the table will be empty. You'll have to add a method, like Set_Size(), to set the hash table's size, and then in your other hash table methods, you'll have to take into account that the hash table may be empty, and return the appropriate errors.

      So it's a trade-off, certainly in code complexity, but you get the ability to declare the class very easily as a local variable. You also get the ability to resize a vector of the class without specifying a default value. There will be additional advantages when we get around to maps as well.

      So consider that, when you are designing your classes.

    5. Remember copy constructors and assignment overloads.

      When someone else uses your class, they will very reasonably expect to be able to make copies of class instances, either through a procedure call, or through the assignment operator. You should do the following:

      • If the default copy constructor and assignment behavior work, that's great!
      • If they don't, but it makes sense to write your own copy constructor and assignment overload, then do it.
      • If it doesn't make sense to write your own, then disable them. You can do that as follows:

        class My_Class {
          public:
            My_Class(const MyClass &c) = delete;
            My_Class& operator= (const My_Class &cc) = delete;
        ....
        }
        

        Now, if a user attempts an operation that would call the copy constructor or assignment overlead, the compiler will emit an error. This is much better than ignoring the copy constructor, and then having it not work when someone calls it.

      Since someone is bound to ask me about this, you can define a "move" constructor as well in the later versions of C++ (11 and beyond). The point of this is to optimize performance when you return a large data structure from a procedure/method. Move constructors are beyond the scope of this class, sorry.

    6. Exceptions should be used to handle errors. I went over that in the introduction to exceptions, so nothing new here.

    7. Be very careful with pointers into the internals of data structures, such as strings and vectors (and later sets and maps). This is because strings and vectors move their data around as they resize, and you'll be left with a stale pointer.

      I'm going to give two example programs here. The first is in src/vector_pointer.cpp

      /* A program to demonstrate why it is dangerous to hold a pointer to
         an element of a vector, because it may get moved. */
      
      #include <iostream>
      #include <cstdio>
      #include <string>
      #include <vector>
      using namespace std;
      
      int main()
      {
        vector <string> v;
        string *sp;
        string line;
      
        while (getline(cin, line)) {
          v.push_back(line); 
          if (v.size() == 1) sp = &(v[0]);          // I set sp to point to the first string in v.
          cout << v.size() << " " << *sp << endl;   // I then print v's size, and *sp.
        }
        return 0;
      }
      

      This program reads lines from standard input, and pushes them onto v. It sets sp to point to the first string in v, right after it is put onto v. At the end of every iteration, it prints v's size and *sp. Let's see it in action:

      UNIX> cat files/input.txt
      Kaylee Hex
      Abigail Piezoelectric MD
      Charlie Waive
      Gabriel Oshkosh
      Lucas Some
      Maya Extravaganza
      Isaiah Stiletto
      Cooper Encumber
      Kayla Littleneck
      Lillian Coroutine
      UNIX> cat files/input.txt | bin/vector_pointer 
      1 Kaylee Hex
      2 Kaylee Hex
      Segmentation fault
      UNIX> 
      
      That's not good. What happened? When the program called v.push_back("Charlie Waive"), the vector needed more memory, so it resized itself to a different memory location, and then copied the strings that were in the old location to the new location. And then they deleted the old location. Frankly, this could have happend after v.push_back("Abigail Piezoelectric MD"), and the memory at sp didn't get reused until v.push_back("Charlie Waive"). Regardless, after v.push_back("Charlie Waive"), the memory pointed to by sp has been changed so that it is no longer a string, and we are alerted to it by the segmentation violation. Keep that in mind.

      Here's a second example, that got me when I was first using exceptions. If you throw a string literal, like:

      throw("Error");
      

      The type of the argument is (const char *), which means that to catch it, you need to do:

      catch (const char *s) {
      

      Suppose that you've already written the code to throw a string literal, and then later in the same constructor, you want to construct a custom error message as a string and throw the string. Unfortunately, you can't catch it with the statement above -- it has to catch a string. So, to avoid writing a second catch() statement and possiblyl duplicate code, what I did was throw the c_str() method of the string. Let me demonstrate with some code, in src/exception_char_star.cpp. I'll start with the class definition, and the constructor, which thows a string literal when a equals zero, and uses a stringstream to construct an error string when a < 0. It then converts the stringstream to a string and uses the c_str() method so that it is throwing a (const char *):

      class My_Class {
        public:
          My_Class(int a);
      };
      
      My_Class::My_Class(int a)
      {
        ostringstream oss;
      
        if (a == 0) throw("Bad constructor for My_Class().  A shouldn't equal zero.");
      
        if (a < 0) {
          oss << "Bad constructor for My_Class().  A shouldn't equal " << a ;
          throw(oss.str().c_str());
        }
      }
      

      In the main() I call new, first when a is zero, and then again when a is negative.

      /* Here, I catch (const char *)'s rather than strings. */
      
      int main()
      {
        My_Class *cp;
      
        try {                        // This one works fine, because the string literal is 
          cp = new My_Class(0);      // really a global variable.
        } catch (const char *s) {
          printf("%s\n", s);
        }
         
        try {                        // This one doesn't work, because the oss's destructor is called, 
          cp = new My_Class(-5);     // and the memory pointed to by s has been freed up (and reused)
        } catch (const char *s) {
          printf("%s\n", s);
        }
        return 0;
      }
      

      The first throw() gets caught fine, because that string literal is a global variable. The second throw gets caught, but because the constructor has exited, the stringstream has been deallocated (actually, technically, the string created by the str() method has been deallocated). That means that the pointer that was returned by c_str() is no longer valid.

      When we run it, the second catch() prints an empty string. To be honest with you, anything could happen there. It just happens to print an empty string on my Macintosh.

      UNIX> bin/exception_char_star
      Bad constructor for My_Class().  A shouldn't equal zero.
      
      UNIX> 
      
      The solution to this is to always throw strings. If you have a string literal, put (string) in front of it, so that the throw() statement throws a string:

      throw((string) "Error");