Default Class Behavior
We've been assigning StopWatch objects and initializing them without any regards to how this is done:The compiler made it for us.// 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
struct Foo { int a, b, c; }; Foo f1 = {1, 2, 3}; // initialization Foo f2 = f1; // initialization Foo f3; // uninitialized f3 = f1; // assignment
So these mean the same thing:Foo& Foo::operator=(const Foo& rhs) { a = rhs.a; b = rhs.b; c = rhs.c; return *this; // Allows chaining: f1 = f2 = f3 etc... }
// Infix notation f3 = f1; // Functional notation f3.operator=(f1);
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.)
Foo::Foo(const Foo& rhs) : a(rhs.a), b(rhs.b), c(rhs.c) { }
Foo f1; // default constructor Foo f2(f1); // copy constructor
Pass by value | Return by value |
---|---|
void SomeFn1(Foo foo) { // do something with foo... } void f3() { Foo f1; SomeFn1(f1); // pass by value } |
Foo SomeFn2() { Foo f; return f; // return by value } void f4() { Foo f1 = SomeFn2(); } |
Default constructor | Default destructor |
---|---|
Foo::Foo() { } |
Foo::~Foo() { } |
Default assignment operator | Default 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) { } |
Class definition | Some 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_; } |
Output:void f6() { Student john("john", 20, 3, 3.10f); Student billy("billy", 21, 2, 3.05f); billy = john; // Assignment }
The output makes it obvious that there is a problem. On my 64-bit Linux system, I get this message.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
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:
Declaration | Implementation |
---|---|
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; } |
Default assignment operator | Our 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 code | Output |
---|---|
// 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:
There are other ways to prevent problems with self-assignment, but at this point, this is easier.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; }
A similar problem exists with the default copy constructor:
Client code | Default 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_) { } |
Declaration | Implementation (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:
Private helper function | Calling the helper function |
---|---|
// Private copy method void Student::copy_data(const Student& rhs) { set_login(rhs.login_); set_age(rhs.age_); set_year(rhs.year_); set_GPA(rhs.GPA_); } |
// Constructor // Explicit copy constructor Student::Student(const Student& student) { copy_data(student); } // Explicit assignment operator Student& Student::operator=(const Student& rhs) { // Check for self-assignment if (&rhs != this) copy_data(rhs); return *this; } |
The copy constructor calls this method:void Student::set_login(const char* login) { // Delete "old" login (THIS IS A POTENTIAL PROBLEM) delete [] login_; // Allocate new one int len = (int)std::strlen(login); login_ = new char[len + 1]; std::strcpy(login_, login); }
void Student::copy_data(const Student& rhs) { // What is the value of login_ when the constructors call this method? set_login(rhs.login_); set_age(rhs.age_); set_year(rhs.year_); set_GPA(rhs.GPA_); }
Initializing in the body | Using the member initializer list |
---|---|
// Constructor Student::Student(const char * login, int age, int year, float GPA) { login_ = 0; set_login(login); set_age(age); set_year(year); set_GPA(GPA); } // Explicit copy constructor Student::Student(const Student& student) { login_ = 0; copy_data(student); } |
// Constructor Student::Student(const char * login, int age, int year, float GPA) : login_(0) { set_login(login); set_age(age); set_year(year); set_GPA(GPA); } // Explicit copy constructor Student::Student(const Student& student) : login_(0) { copy_data(student); } |
As a rule, if you use new in your constructor, you will need to create
|
Creating a String Class
Once these methods are implemented, this trivial program will work:#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) };
Output:void f1() { String s("Hello"); std::cout << s << std::endl; }
Implementations so far:Conversion constructor: Hello Hello Destructor: Hello
#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; } |
#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; } |
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; }
Notice the two different uses of new and delete in the program: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
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:This is the output:void foo() { String one("Pascal"); String two(one); cout << one << endl; cout << two << endl; }
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?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
Here's another similar use of the class:
Output:void PrintString(String s) { cout << s << endl; } void f3() { String str("Pascal"); PrintString(str); }
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:
Output:void f5() { String one("Pascal"); String two; two = one; cout << one << endl; cout << two << endl; }
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
Implementations: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 };
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; } |
Copy test | Output |
---|---|
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 test | Output |
---|---|
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 |
Enhancing the String Class
There are many features and functions we could add to the String class to make it more useable.
Declaration | Implementation |
---|---|
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_); } |
Test | Output |
---|---|
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 |
We need to include this:
#include <cctype> // islower, isupper
Convert to uppercase | Convert 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'; } |
Test | Output |
---|---|
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 |
Output: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; }
Declaration:Conversion constructor: One Conversion constructor: Two One is before Two Two is not before One One is not before One Destructor: Two Destructor: One
Implementation:bool String::operator<(const String& rhs) const;
Implementing these operators is also trivial: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; }
What about this?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;
String s1("Digipen"); if ("Hello" < s1) std::cout << "Hello is less" << std::endl; else std::cout << "Hello is NOT less" << std::endl;
We could require the user to do this:In function void f4(): error: no match for operator< (operand types are const char [6] and String) if ("Hello" < s1) ^
if (String("Hello") < s1)
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:Like the other operators, this is also trivial:String s1("Digipen"); // c should have the value 'g' char c = s1[2];
Declaration | Implementation |
---|---|
class String { public: char operator[](int index) const; }; |
char String::operator[](int index) const { return string_[index]; } |
Sample usage | Output |
---|---|
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 |
char String::operator[](int index) const { // Validate the index if ( (index >= 0) && (index < size()) ) return string_[index]; else return string_[0]; // What to return??? This is a BIG problem // that we'll postpone for now. (EH) }
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 |
Return a reference | Compiles and runs |
---|---|
char& String::operator[](int index) const { return string_[index]; } |
Output: Cello |
Try a const object | Compiles 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 |
Modify const object | No problemo |
---|---|
void f6() { const String s1("Hello"); // Change the const object s1[0] = 'C'; } |
Output: Cello |
Return a const reference | Compiler error as expected |
---|---|
const char& String::operator[](int index) const { return string_[index]; } |
error: assignment of read-only location |
void f4() { String s1("Hello"); // Compiler error: assignment of read-only location s1[0] = 'C'; std::cout << s1 << std::endl; }
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: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'; }
and the implementations:const char& operator[](int index) const; // for r-values char& operator[](int index) const; // for l-values
const char& String::operator[](int index) const { return string_[index]; } |
char& String::operator[](int index) const { return string_[index]; } |
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
Example code now works as expected:The const at the end is part of the method's signature and the compiler uses it to distinguish between the two methods.
Notes: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]; }
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 code | Output |
---|---|
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 |
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:
would produce something like this in memory:String s1("foo"); String s2("bar"); String s3("baz"); s1.foo = 10; s2.foo = 20; s3.foo = 30;
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.
class String { public: // Other public members... int foo; // non-static static int bar; // static private: char *string_; // the "real" string };
// Accessing a static member with the scope resolution operator int i = String::bar; std::cout << i << std::endl;
Header file (.h) | Implementation file (.cpp) |
---|---|
class String { public: String(); // default ctor String(const String& rhs); // copy ctor ~String(); // dtor // declaration static int bar; // static // etc... }; |
#include "String.h" // Initialize outside of the class (Definition) int String::bar = 0; String::String() { string_ = new char[1]; string_[0] = 0; } String::~String() { delete [] string_; } // etc... |
Each object has a separate storage area for foo, but bar is shared between them:/cygdrive/h/temp/ccZm55jF.o:main.cpp:(.text+0xf8d): undefined reference to 'String::bar' collect2: ld returned 1 exit status
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:
Declarations | Definitions |
---|---|
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; } |
You can access static members through an object as well:void f8() { // Accessing a static member int i = String::get_bar(); // Error, private now i = String::bar; }
void f9() { String s1("foo"); int x = s1.get_bar(); // Access through object int y = String::get_bar(); // Access through class }
class String { public: // Other public members... // const can be initialized in the class const static int foo = 47; private: // Other private members... // const can be initialized in the class const static char bar = 'B'; };