Classes - Part 3

Default Class Behavior

We've been assigning StopWatch objects and initializing them without any regards to how this is done:
  // Construction (conversion constructor)
StopWatch sw1(60);   // 00:01:00
  
  // Initialization
StopWatch sw2 = sw1; // 00:01:00

  // Construction (default constructor)
StopWatch sw3; // 00:00:00

  // Assignment: sw3.operator=(sw1);
  // Where did this operator come from?
sw3 = sw1;  // 00:01:00
The compiler made it for us. For some classes, the default assignment operator is sufficient. (It is fine for the Foo class and the StopWatch class.)

In addition to the default assignment operator, the compiler will also provide a default copy constructor. (Once again, another function will be called to help the compiler perform its job.)

At this point in the discussion there are four methods that the compiler will provide defaults for. (No visible C++ code is actually generated)

Default constructorDefault destructor
Foo::Foo()
{
}
Foo::~Foo()
{
}
Default assignment operatorDefault copy constructor
Foo& Foo::operator=(const Foo& rhs)
{
  a = rhs.a;
  b = rhs.b;
  c = rhs.c;
  return *this;
}
Foo::Foo(const Foo& rhs) : a(rhs.a), b(rhs.b), c(rhs.c)
{
}
For simple classes and structs, these methods are sufficient. Where might it not be sufficient?

Class definitionSome implementations
class Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);
      // Destructor
    ~Student();

  private:
    char *login_; // dynamically allocated
    int age_;
    int year_;
    float GPA_;
};
void Student::set_login(const char* login)
{
    // Delete "old" login
  delete [] login_;

    // Allocate new one
  int len = std::strlen(login);
  login_ = new char[len + 1]; 
  std::strcpy(login_, login);
}

Student::~Student()
{
  std::cout << "Student destructor for " 
            << login_ << std::endl;
  delete [] login_;
}
Given the above "legal" code, this seemingly innocent code below is undefined and may cause a crash:

void f6()
{
  Student john("john", 20, 3, 3.10f);
  Student billy("billy", 21, 2, 3.05f);

  billy = john; // Assignment
}
Output:
Student constructor for john
Student constructor for billy
Student destructor for john
Student destructor for ,oļa,oļa?
  22292 [sig] a 2032 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
  22292 [sig] a 2032 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
 742008 [sig] a 2032 E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** fatal error - 
        E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** called with threadlist_ix -1
The output makes it obvious that there is a problem. On my 64-bit Linux system, I get this message.

Here's a graphic of the problem:

Incorrect assignment behavior (shallow copy)Correct assignment behavior (deep copy)

The diagram shows the painful truth: The default assignment operator won't cut it. Also, the default copy constructor will have the same problem, so this code will also fail:

Student john("john", 20, 3, 3.10f);

  // Copy constructor
Student billy(john);

A Proper Assignment Operator and Copy Constructor

Adding an assignment operator is no different than any other operator:

DeclarationImplementation
class Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);

      // Explicit assignment operator
    Student& operator=(const Student& rhs);

  private:
      // Private data
};
Student& Student::operator=(const Student& rhs)
{
  set_login(rhs.login_); // This is important!
  set_age(rhs.age_);
  set_year(rhs.year_);
  set_GPA(rhs.GPA_);
  return *this;
}
Remember that this is kind of what the default compiler-generated assignment operator looks like:

Default assignment operatorOur correct and safe copy of login_
Student& Student::operator=(const Student& rhs)
{
  login_ = rhs.login_; // This is big trouble!
  age_ = rhs.age_;
  year_ = rhs.year_;
  GPA_ = rhs.GPA_;
  return *this;
}
void Student::set_login(const char* login)
{
    // Delete "old" login
  delete [] login_;

    // Allocate new string
  int len = std::strlen(login);
  login_ = new char[len + 1]; 
  
    // Copy data
  std::strcpy(login_, login);
}

There is more work to be done. Many (if not all) new C++ programmers fall into this trap a lot:

Sample codeOutput
  // Construct a Student object
Student john("jdoe", 20, 3, 3.10f);

  // Self-assignment (legal)
john = john;
Student constructor for jdoe
Error in age range!
Error in year range!
Student constructor for
Student operator= for ,oļa,oļa?
Student destructor for
Student destructor for ,oļa,oļa?

An easy way to prevent this is to simply check first:

Student& Student::operator=(const Student& rhs)
{
    // Check for self-assignment
  if (&rhs != this)
  {
    set_login(rhs.login_);
    set_age(rhs.age_);
    set_year(rhs.year_);
    set_GPA(rhs.GPA_);
  }
  return *this;
}
There are other ways to prevent problems with self-assignment, but at this point, this is easier.

A similar problem exists with the default copy constructor:

Client codeDefault copy constructor
  // Construct a Student object
Student john("jdoe", 20, 3, 3.10f);

  // Copy constructor
Student temp(john);
Student::Student(const Student& student) : login_(student.login_),  // This is bad
                                           age_(student.age_),
                                           year_(student.year_),
                                           GPA_(student.GPA_)
{
}
We need to write our own copy constructor to copy the object's data correctly:

DeclarationImplementation (almost correct)
class Student           
{
  public:
      // Constructor
    Student(const char * login, int age, 
            int year, float GPA);

      // Explicit copy constructor
    Student(const Student& student);

  private:
      // Private data
};
Student::Student(const Student& student)
{
  set_login(student.login_); // This is good
  set_age(student.age_);
  set_year(student.year_);
  set_GPA(student.GPA_);
}

Points:
As a rule, if you use new in your constructor, you will need to create

  1. a destructor to free the memory.
  2. a copy constructor to perform a deep copy.
  3. an assignment operator to perform a deep copy (and also free the original memory).

Creating a String Class

This is our minimal interface. Looking at the interface, what kind of functionality does the String class have? (It's important to be able to look at a public interface, typically in a header file, and determine the functionality of the class.)

#include <iostream> // ostream

class String
{
  public:
    String();                  // default constructor
    String(const char *cstr); // conversion constructor
    ~String();                 // destructor

      // So we can use cout
    friend std::ostream & operator<<(std::ostream & os, const String &str);
    
  private:
    char *string_; // the "real" string (A NUL terminated array of characters)
};
Once these methods are implemented, this trivial program will work:
void f1()
{
  String s("Hello");
  std::cout << s << std::endl;
}
Output:
Conversion constructor: Hello
Hello
Destructor: Hello
Implementations so far:

#include <iostream> // iostream, cout, endl
#include <cstring>  // strcpy, strlen
#include "String.h"

String::String()
{
    // Allocate minimal space
  string_ = new char[1]; 
  string_[0] = 0;        
  
  std::cout << "Default constructor" 
            << std::endl;
}
	
	
	
String::String(const char *cstr)
{
    // Allocate space and copy
  int len = strlen(cstr);
  string_ = new char[len + 1];
  std::strcpy(string_, cstr);      

  std::cout << "Conversion constructor: " 
            << cstr << std::endl;
}
String::~String()
{
  std::cout << "Destructor: "
            << string_ << std::endl;
  delete [] string_; // free memory
}
std::ostream & operator<<(std::ostream & os, 
                          const String &str)
{
  os << str.string_;
  return os;
}
Here's a larger example that demonstrates the construction and destruction of objects:

#include <iostream>
using std::cout;
using std::endl;

String global("Euclid");

void Create1()
{
  cout << "*** Start of Create1..." << endl;

  String local("Plato");
  cout << local << endl;

  cout << "*** End of Create1..." << endl;
}
	
	
	
	
String *Create2()
{
  cout << "*** Start of Create2..." << endl;

  String *dynamic = new String("Pascal");
  cout << *dynamic << endl;

  cout << "*** End of Create2..." << endl;
  return dynamic;
}
Given the functions above, what will be printed by the code below?

int main()
{
  cout << "*** Start of main..." << endl;

    String s("Newton");
    cout << s << endl;

    Create1();
    String *ps = Create2();
    cout << ps << endl;   // what does this display?
    cout << *ps << endl;  // what does this display?
    cout << global << endl;

    delete ps;

  cout << "*** End of main..." << endl;
  return 0;
}


Output:

Conversion constructor: Euclid
*** Start of main...
Conversion constructor: Newton
Newton
*** Start of Create1...
Conversion constructor: Plato
Plato
*** End of Create1...
Destructor: Plato
*** Start of Create2...
Conversion constructor: Pascal
Pascal
*** End of Create2...
0x653290
Pascal
Euclid
Destructor: Pascal
*** End of main...
Destructor: Newton
Destructor: Euclid
Notice the two different uses of new and delete in the program:

At this point, we are missing quite a bit of functionality for a general purpose String class. What else could we add to it?

Fixing the String Class

Here's a program that "appears" to work, but then causes a big problem:
void foo()
{
  String one("Pascal");
  String two(one);

  cout << one << endl;
  cout << two << endl;
}
This is the output:
Pascal
Pascal
Destructor: Pascal
Destructor: ,oļa,oļa?
     69 [sig] a 1864 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
     69 [sig] a 1864 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
Notice that we initialized one String with another. It seemed to work, because we printed it out. Yet the program still crashed. What's the problem?

Problem diagram


Here's another similar use of the class:

void PrintString(String s)
{
  cout << s << endl;
}

void f3()
{
  String str("Pascal");
  PrintString(str);
}
Output:
Conversion constructor: Pascal
Pascal
Destructor: Pascal
Destructor: ,oļa,oļa?
     63 [sig] a 836 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
     63 [sig] a 836 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
1599112 [sig] a 836 E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** fatal error - 
                    E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** called with threadlist_ix -1


What could possibly be causing this?

To help understand the problem, look at the difference between these functions:

void PrintString(String s)
void PrintString(String &s)
void PrintString(const String &s)


Finally, we have this that "appears" to work until it crashes:

void f5()
{
  String one("Pascal");
  String two;

  two = one;

  cout << one << endl;
  cout << two << endl;
}
Output:
Conversion constructor: Pascal
Default constructor
Pascal
Pascal
Destructor: Pascal
Destructor: ,oļa,oļa?
     64 [sig] a 1164 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
     64 [sig] a 1164 open_stackdumpfile: Dumping stack trace to a.exe.stackdump
1508162 [sig] a 1164 E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** fatal error - 
                     E:\Data\Courses\Notes\CS170\Code\Classes3\a.exe: *** called with threadlist_ix -1

Remember that C++ automatically provides certain member functions if you don't: Note that Adding the methods to the class:
class String
{
  public:
    String();                      // default constructor
    String(const String& rhs); // copy constructor
    String(const char *cstr);    // conversion constructor
    ~String();                    // destructor

      // Copy assignment operator
    String& operator=(const String& rhs); 

      // So we can use cout
    friend std::ostream& operator<<(std::ostream& os, const String &str);

  private:
    char *string_; // the "real" string
};
Implementations:
String::String(const String& rhs)
{
  std::cout << "Copy constructor: " 
            << rhs.string_ << std::endl;

  int len = strlen(rhs.string_);
  string_ = new char[len + 1];
  std::strcpy(string_, rhs.string_);
}
String& String::operator=(const String& rhs)
{
  std::cout << "operator=: " 
            << string_ << " = "
            << rhs.string_ << std::endl;

  if (&rhs != this)
  {
    delete [] string_;
    int len = strlen(rhs.string_);
    string_ = new char[len + 1];
    std::strcpy(string_, rhs.string_);
  }
  return *this;
}
Testing:
Copy testOutput
void f4()
{
  String one("Pascal");
  String two(one);

  cout << one << endl;
  cout << two << endl;
}
Conversion constructor: Pascal
Copy constructor: Pascal
Pascal
Pascal
Destructor: Pascal
Destructor: Pascal
Assignment testOutput
void f5()
{
  String one("Pascal");
  String two;

  two = one;

  cout << one << endl;
  cout << two << endl;
}
Conversion constructor: Pascal
Default constructor
operator=:  = Pascal
Pascal
Pascal
Destructor: Pascal
Destructor: Pascal
OK, now things are looking a little better! Let's move on...

Enhancing the String Class

There are many features and functions we could add to the String class to make it more useable. Let's do a real simple one first: The length of the string. (Call the method size)

DeclarationImplementation
class String
{
  public:
      // Other public methods...

      // Number of chars in the string
    int size() const;

  private:
    char *string_; // the "real" string
};
int String::size() const
{
    // Return the length
  return strlen(string_);
}
TestOutput
String s1("Digipen");
std::cout << s1 << std::endl;
std::cout << "Length of string: "
          << s1.size() << std::endl;
Conversion constructor: Digipen
Digipen
Length of string: 7
Destructor: Digipen
A couple more functions:

We need to include this:

#include <cctype>   // islower, isupper
Convert to uppercaseConvert to lowercase
void String::upper()
{
  int len = size();
  for (int i = 0; i < len; i++)
    if (std::islower(string_[i]))
      string_[i] -= 'a' - 'A';
}
void String::lower()
{
  int len = size();
  for (int i = 0; i < len; i++)
    if (std::isupper(string_[i]))
      string_[i] += 'a' - 'A';
}
TestOutput
String s1("Digipen");

std::cout << s1 << std::endl;
s1.lower();
std::cout << s1 << std::endl;
s1.upper();
std::cout << s1 << std::endl;
Conversion constructor: Digipen
Digipen
digipen
DIGIPEN
Destructor: DIGIPEN
How about comparing two Strings?

void f3()
{
  String s1("One");
  String s2("Two");

  if (s1 < s2)
    std::cout << s1 << " is before " << s2 << std::endl;
  else
    std::cout << s1 << " is not before " << s2 << std::endl;

  if (s2 < s1)
    std::cout << s2 << " is before " << s1 << std::endl;
  else
    std::cout << s2 << " is not before " << s1 << std::endl;

  if (s1 < s1)
    std::cout << s1 << " is before " << s1 << std::endl;
  else
    std::cout << s1 << " is not before " << s1 << std::endl;
}
Output:
Conversion constructor: One
Conversion constructor: Two
One is before Two
Two is not before One
One is not before One
Destructor: Two
Destructor: One
Declaration:
bool String::operator<(const String& rhs) const;
Implementation:
bool String::operator<(const String& rhs) const
{
    // if we're 'less' than rhs
  if (std::strcmp(string_, rhs.string_) < 0)
    return true;
  else
    return false;
}
Implementing these operators is also trivial:
bool String::operator>(const String& rhs) const;
bool String::operator==(const String& rhs) const;
bool String::operator<=(const String& rhs) const;
bool String::operator>=(const String& rhs) const;
bool String::operator!=(const String& rhs) const;
bool String::operator==(const String& rhs) const;
What about this?
String s1("Digipen");

if ("Hello" < s1)
  std::cout << "Hello is less" << std::endl;
else
  std::cout << "Hello is NOT less" << std::endl;


This is what we get:
In function ‘void f4()’:
error: no match for ‘operator<’ (operand types are ‘const char [6]’ and ‘String’)
   if ("Hello" < s1)
               ^
We could require the user to do this:
if (String("Hello") < s1)



Or, we could overload a global operator to do the conversion:
bool operator<(const char *lhs, const String& rhs)
{
  return String(lhs) < rhs;
}
Conversion constructor: Digipen
Conversion constructor: Hello
Destructor: Hello
Hello is NOT less
Destructor: Digipen

More Enhancements to the String Class

There is an obvious feature that is missing from the String class: subscripting. We should be able to do this:
String s1("Digipen");

  // c should have the value 'g'
char c = s1[2]; 
Like the other operators, this is also trivial:
DeclarationImplementation
class String
{
  public:
    char operator[](int index) const;
};
char String::operator[](int index) const
{
  return string_[index];
}
Sample usageOutput
void f1()
{
  String s1("Digipen");

  for (int i = 0; i < s1.size(); i++)
    std::cout << s1[i] << std::endl;
}
D
i
g
i
p
e
n
Notes: Now we want to change a character in the string:
Compiler error:
void f4()
{
  String s1("Hello");

    // change first letter
  s1[0] = 'C';

  std::cout << s1 << std::endl;
}
error: non-lvalue in assignment




<----- can't assign to a temporary value
We can't return a temporary value if we want to modify it. We must return a reference:

Return a referenceCompiles and runs
char& String::operator[](int index) const
{
  return string_[index];
}
Output:

Cello
Let's try some tests that most beginners (and students) forget to do (even if told directly to do so!): Read a const object:

Try a const objectCompiles and runs fine
void f5()
{
  const String s1("Digipen");

  for (int i = 0; i < s1.size(); i++)
    std::cout << s1[i] << std::endl;
}
D
i
g
i
p
e
n
Change (write) a const object:

Modify const objectNo problemo
void f6()
{
  const String s1("Hello");

    // Change the const object
  s1[0] = 'C';
}

Output:

Cello
What?!?!?!?!?

Return a const referenceCompiler error as expected
const char& String::operator[](int index) const
{
  return string_[index];
}
error: assignment of read-only 
       location
However, this breaks our previously working and legal code:

void f4()
{
  String s1("Hello");

    // Compiler error: assignment of read-only location
  s1[0] = 'C';

  std::cout << s1 << std::endl;
}


The solution: We need to support both:

void f7()
{
  String s1("Hello");          // non-const object
  const String s2("Goodbye"); // const object

    // non-const: This should be allowed
  s1[0] = 'C';

    // const: This should produce an error
  s2[0] = 'F';
}
We need to overload the subscript operator so that it can handle both return types. Here's is our first failed attempt at the function declarations:
const char& operator[](int index) const; // for r-values
      char& operator[](int index) const; // for l-values
and the implementations:

const char& String::operator[](int index) const
{
  return string_[index];
}
char& String::operator[](int index) const
{
  return string_[index];
}
What is wrong with these? (They won't compile)



They are both const methods, since neither one is modifying the private fields.

One returns a const reference and the other returns a non-const reference.

The proper way:

const char& operator[](int index) const; // for r-values
      char& operator[](int index);       // for l-values

The const at the end is part of the method's signature and the compiler uses it to distinguish between the two methods.

Example code now works as expected:
void f8()
{
  String s1("Hello");          // non-const object
  const String s2("Goodbye"); // const object

    // Calls non-const version, l-value assignment (write) is OK
  s1[0] = 'C';

    // Calls const version, l-value assignment (write) is an error
  //s2[0] = 'F';

    // Calls non-const version, r-value read is OK
  char c1 = s1[0];

    // Calls const version, r-value read is OK
  char c2 = s2[0];
}
Notes:

Class Methods and static Members

Suppose we add a public data member to the String class:

class String
{
  public:
    // Other public members...

    int foo; // public data member

  private:
    char *string_; // the "real" string
};
Test codeOutput
void f5()
{
  String s1("foo");
  String s2("bar");
  String s3("baz");

  s1.foo = 10;
  s2.foo = 20;
  s3.foo = 30;

  std::cout << s1.foo << std::endl;
  std::cout << s2.foo << std::endl;
  std::cout << s3.foo << std::endl;
}
Conversion constructor: foo
Conversion constructor: bar
Conversion constructor: baz
10
20
30
Destructor: baz
Destructor: bar
Destructor: foo
Of course, if we don't initialize the data in the constructor or in this code, we get different output:
void f6()
{
  String s1("foo");
  String s2("bar");
  String s3("baz");

  std::cout << s1.foo << std::endl;
  std::cout << s2.foo << std::endl;
  std::cout << s3.foo << std::endl;
}
Conversion constructor: foo
Conversion constructor: bar
Conversion constructor: baz
1627408208
4268368
4268368
Destructor: baz
Destructor: bar
Destructor: foo

So this code:

String s1("foo");
String s2("bar");
String s3("baz");
s1.foo = 10;
s2.foo = 20;
s3.foo = 30;
would produce something like this in memory:
We must always initialize any non-static data in the constructor. Non-static? As opposed to what? Static?

By default, members of a class are non-static. If you want them to be static, you must indicate it with the static keyword.

Unfortunately, the meaning of static is completely different from the other meanings we've learned.

Adding a static member is trivial:

class String
{
  public:
    // Other public members...

    int foo;         // non-static
    static int bar; // static

  private:
    char *string_; // the "real" string
};
If you fail to define the static member outside of the class, you will get a linker error:
/cygdrive/h/temp/ccZm55jF.o:main.cpp:(.text+0xf8d): undefined reference to 'String::bar'
collect2: ld returned 1 exit status
Each object has a separate storage area for foo, but bar is shared between them:
      

Note: Static data members are NOT counted with the sizeof operator. Only non-static data is included. This is true when using sizeof with either the class itself, or objects of the class.

Methods can be static as well:

DeclarationsDefinitions
class String
{
  public:
    // Other public members...

    static int get_bar(); // static

  private:
    char *string_;  // the "real" string
    static int bar; // static
};
// Initialize outside of class (Definition)
int String::bar = 20;

int String::get_bar()
{
  return bar;
}
Sample usage:

void f8()
{
    // Accessing a static member
  int i = String::get_bar();

    // Error, private now
  i = String::bar;
}
You can access static members through an object as well:
void f9()
{
  String s1("foo");

  int x = s1.get_bar();      // Access through object
  int y = String::get_bar(); // Access through class
}