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 copy assignment operator is sufficient. (It is fine for the Foo class and the StopWatch class.)

In addition to the default copy 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 constructor
(does nothing)
Default destructor
(does nothing)
Foo::Foo()
{
}
Foo::~Foo()
{
}

Default copy assignment operator
(memberwise assignment)
Default copy constructor
(memberwise initialization)
Foo& Foo::operator=(const Foo& rhs)
{
  a = rhs.a; // assignment
  b = rhs.b; // assignment
  c = rhs.c; // assignment

  return *this;
}
Foo::Foo(const Foo& rhs) : 
         a(rhs.a), // initialization
         b(rhs.b), // initialization 
         c(rhs.c)  // initialization
{
}
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)
(Default behavior)
Correct assignment behavior (deep copy)
(This is what we want)

The diagram shows the painful truth: The default copy 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 a copy 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 copy 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 copy assignment operator looks like:

Default copy 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;
}
Refer to this diagram.
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?

For example, this is perfectly legal (if not strange):

int i = 5;
i = i; // Should this invalidate your program?

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 in your C++ career, this is safe and easy.

A similar problem exists with the default copy constructor:

Client codeDefault copy constructor
(not good)
  // 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:
    char *login_; // dynamically allocated
    int age_;
    int year_;
    float GPA_;
};
Student::Student(const Student& student)
{
  set_login(student.login_); // This is better
  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. a copy assignment operator to perform a deep copy (and also free the original memory).
This is because the default versions provided by the compiler will usually be inadequate.

This is Item #11 from Scott Meyers' book. The option for GNU (-Weffc++) will warn you about that.

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 (empty string)
    String(const char *cstr); // conversion constructor (C-style to String)
    ~String();                // destructor (clean up)

      // 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)
};
What is sizeof(String)? (Remember this)

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 = std::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:
 1.  Conversion constructor: Euclid
 2.  *** Start of main...
 3.  Conversion constructor: Newton
 4.  Newton
 5.  *** Start of Create1...
 6.  Conversion constructor: Plato
 7.  Plato
 8.  *** End of Create1...
 9.  Destructor: Plato
10.  *** Start of Create2...
11.  Conversion constructor: Pascal
12.  Pascal
13.  *** End of Create2...
14.  0x653290
15.  Pascal
16.  Euclid
17.  Destructor: Pascal
18.  *** End of main...
19.  Destructor: Newton
20.  Destructor: Euclid

Output: (The line numbers are not part of the output.)

 1.  Conversion constructor: Euclid
 2.  *** Start of main...
 3.  Conversion constructor: Newton
 4.  Newton
 5.  *** Start of Create1...
 6.  Conversion constructor: Plato
 7.  Plato
 8.  *** End of Create1...
 9.  Destructor: Plato
10.  *** Start of Create2...
11.  Conversion constructor: Pascal
12.  Pascal
13.  *** End of Create2...
14.  0x653290
15.  Pascal
16.  Euclid
17.  Destructor: Pascal
18.  *** End of main...
19.  Destructor: Newton
20.  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. But first, let's fix the problems. There are problems?

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? Remember this rule?

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

void PrintString(String s); // How expensive is this? What is sizeof(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:

Copy constructorCopy assignment operator
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;
}

Aside:
1. What if new fails? (It can and does happen.)
2. Why aren't we doing any data validation when copying or assigning the private data?

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. (We'll 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 (not optimal, but works for now)
  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(); // size() is linear
  for (int i = 0; i < len; i++)
    if (std::islower(string_[i]))
      string_[i] -= 'a' - 'A';
}
void String::lower()
{
  int len = size(); // size() is linear
  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

Policy: Converting a String to uppercase is simple and straight-forward and everyone that has ever programmed in their lives is familiar with a function like this. The resulting String will be exactly what everyone expected, without exception. However, there is this thing called Policy which determines how the String is manipulated and there are generally two schools of thought, neither or which is right/wrong, better/worse. For example, some String libraries will convert the original string to uppercase in-place, overwriting the original lowercase characters. Other libraries will make a copy of the orginal string and convert that copy to uppercase and return it.

Policy examples:
String& upper(String &str);       // Modifies String in-place, returns a reference.
   void upper(String &str);       // Modifies the String in-place, returns nothing.
 String upper(const String &str); // Makes a copy of the String and returns the copy in uppercase.
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 in the String class:
bool 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 will also be trivial. (What's the difference between the two const keywords in each prototype?)
bool operator>(const String& rhs) const;
bool operator==(const String& rhs) const;
bool operator<=(const String& rhs) const;
bool operator>=(const String& rhs) const;
bool operator!=(const String& rhs) const;
bool operator==(const String& rhs) const;
What about this?
String s1("Digipen");

if (s1 < "Hello")
  std::cout << "Hello is less" << std::endl;
else
  std::cout << "Hello is NOT less" << std::endl;
Output:
Conversion constructor: Digipen
Conversion constructor: Hello
Destructor: Hello
Destructor: Digipen


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)
as the compiler will not automatically convert the left-hand side because it's looking for a global overloaded operator.


But this may be cumbersome. 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
Now the user can compare Strings with NUL-terminated strings as easily as comparing two Strings. This is the whole point of overloading operators: Give the users the ability to work with user-defined types as naturally as they work with built-in types (e.g. integers).

Going one step further

It would be nice if we could directly compare a String with a NUL-terminated string without having to construct a temporary String first. We can do that if we can access the underlying NUL-terminated string. (Remember, the String class is just a wrapper around a C-style NUL-terminated string.)
// declare in public section of .h file 
const char *c_str() const;
// implement in .cpp file
const char *String::c_str() const
{
  return string_;
}
Now our global less-than operator looks like this:

bool operator<(const char *lhs, const String& rhs)
{
  return std::strcmp(lhs, rhs.c_str()) < 0;
}
Now this code is more efficient:
String s1("Digipen");
  
if ("Hello" < s1)
  std::cout << "Hello is less" << std::endl;
else
  std::cout << "Hello is NOT less" << std::endl;

New outputOld output
Conversion constructor: Digipen
Hello is NOT less
Destructor: Digipen
Conversion constructor: Digipen
Conversion constructor: Hello
Destructor: Hello
Hello is NOT less
Destructor: Digipen
We can also "optimize" the member function by overloading it:
// optimization
bool String::operator<(const char *rhs) const
{
    // if we're 'less' than rhs
  if (std::strcmp(string_, rhs) < 0)
    return true;
  else
    return false;
}
This function does not need to make a temporary copy before comparing.
bool operator<(const char *lhs, const String& rhs) // global, e.g. "Hello" < s1
bool String::operator<(const String& rhs) const    // member, e.g. s1 < s2
bool String::operator<(const char *rhs) const      // member, e.g. s1 < "Hello"

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/modify 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';

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

Output:

Cello
What?!?!?!?!? Look at this to see why.


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 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 class, otherwise it's undefined. 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
Note that you must do it this way:
int String::bar = 0; // Need the leading int keyword (It's a definition)
Just doing this:
String::bar = 0; // Assignment
is simply assigning a new value to String::bar.

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
}

Example

Suppose we want to keep track of how many String objects were created, how many total bytes were allocated, and how many Strings are currently active? This is a good candidate for static members.

This is what the updated String class looks like:

In String.h:

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); 

    static int created_;    // Total number of Strings constructed
    static int alive_;      // Total number of Strings still around
    static int bytes_used_; // Total bytes allocated for all Strings

    // Other public members

  private:
    char *string_;  // the "real" string

    // Other private members
};
In String.cpp
// Define and initialize the static members  
int String::bytes_used_ = 0;
int String::created_ = 0;
int String::alive_ = 0;
Updated member functions:
Default constructor    Conversion Constructor
String::String()
{
    // Allocate minimal space
  string_ = new char[1]; 
  string_[0] = 0;     

  bytes_used_ += 1;
  created_++;
  alive_++;

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

  bytes_used_ += len + 1;
  created_++;
  alive_++;

  #ifdef PRINT
  std::cout << "Conversion constructor: " << cstr << std::endl;
  #endif
}
DestructorCopy Constructor
String::~String()
{
  #ifdef PRINT
  std::cout << "Destructor: " << string_  << std::endl;
  #endif

  delete [] string_; // release the memory
  
  alive_--;
}
String::String(const String& rhs)
{
  int len = (int)strlen(rhs.string_);
  string_ = new char[len + 1];
  std::strcpy(string_, rhs.string_);

  bytes_used_ += len + 1;
  created_++;
  alive_++;

  #ifdef PRINT
  std::cout << "Copy constructor: " << rhs.string_ << std::endl;
  #endif
}
Copy assignment operator
String& String::operator=(const String& rhs)
{
  #ifdef PRINT
  std::cout << "operator=: " << string_ << " = " << rhs.string_ << std::endl;
  #endif

  if (&rhs != this)
  {
    delete [] string_;
    int len = (int)strlen(rhs.string_);
    string_ = new char[len + 1];
    std::strcpy(string_, rhs.string_);
  
    bytes_used_ += len + 1;
  }
  return *this;
}
Sample test program:
#include <iostream> // cout, endl
#include "String.h" // String class

// Print static data
void print_stats()
{
  std::cout << "Strings created: " << String::created_ << ", ";
  std::cout << "Bytes used: " << String::bytes_used_ << ", ";
  std::cout << "Strings alive: " << String::alive_ << std::endl;
}

// Pass a copy, return a copy
String pass_by_val(String s)
{
  return s; // return a copy
}

// Pass a reference, return a reference
const String& pass_by_ref(const String& s)
{
  return s; // return a reference
}

void f1()
{
  String s1("Hello");
  print_stats(); // Strings created: 1, Bytes used: 6, Strings alive: 1

  String s2(s1);
  print_stats(); // Strings created: 2, Bytes used: 12, Strings alive: 2

  pass_by_val(s1);
  print_stats(); // Strings created: 4, Bytes used: 24, Strings alive: 2

  pass_by_ref(s1);
  print_stats(); // Strings created: 4, Bytes used: 24, Strings alive: 2

  s1 = "Goodbye";
  print_stats(); // Strings created: 5, Bytes used: 40, Strings alive: 2

  s2 = s1;
  print_stats(); // Strings created: 5, Bytes used: 48, Strings alive: 2

  String s3;
  print_stats(); // Strings created: 6, Bytes used: 49, Strings alive: 3
}

int main()
{
  print_stats(); // Strings created: 0, Bytes used: 0, Strings alive: 0
  f1();
  print_stats(); // Strings created: 6, Bytes used: 49, Strings alive: 0
}
Output: (without PRINT defined)
    1. Strings created: 0, Bytes used: 0, Strings alive: 0 2. Strings created: 1, Bytes used: 6, Strings alive: 1 3. Strings created: 2, Bytes used: 12, Strings alive: 2 4. Strings created: 4, Bytes used: 24, Strings alive: 2 5. Strings created: 4, Bytes used: 24, Strings alive: 2 6. Strings created: 5, Bytes used: 40, Strings alive: 2 7. Strings created: 5, Bytes used: 48, Strings alive: 2 8. Strings created: 6, Bytes used: 49, Strings alive: 3 9. Strings created: 6, Bytes used: 49, Strings alive: 0
Output: (with PRINT defined)
1. Strings created: 0, Bytes used: 0, Strings alive: 0

   Conversion constructor: Hello
2. Strings created: 1, Bytes used: 6, Strings alive: 1

   Copy constructor: Hello
3. Strings created: 2, Bytes used: 12, Strings alive: 2

   Copy constructor: Hello
   Copy constructor: Hello
   Destructor: Hello
   Destructor: Hello
4. Strings created: 4, Bytes used: 24, Strings alive: 2

5. Strings created: 4, Bytes used: 24, Strings alive: 2

   Conversion constructor: Goodbye
   operator=: Hello = Goodbye
   Destructor: Goodbye
6. Strings created: 5, Bytes used: 40, Strings alive: 2

   operator=: Hello = Goodbye
7. Strings created: 5, Bytes used: 48, Strings alive: 2

   Default constructor
8. Strings created: 6, Bytes used: 49, Strings alive: 3

   Destructor: 
   Destructor: Goodbye
   Destructor: Goodbye
9. Strings created: 6, Bytes used: 49, Strings alive: 0

Self-check: Given what we've been discussing for the past week, how would you get rid of the extra (read: unnecessary) construction in #6 above? (Hint: Add another function)