Computer Science and Software Engineering University of Wisconsin - Platteville 3. Big3 Yan Shi CS/SE 2630 Lecture Notes The Big Three In C++, classes come with 3 special functions that are already written for you, known collectively as the big three. In many cases, you can accept the default behavior of these function. By default: Destructor: calls the destructors of all members/release all primitive members. Copy Constructor: applies copy constructors/assignments to each data member operator=: applies assignment operator to each data member Example: default class Cell { private: int value; public: int Get() { return value; } void Set( int x ) { value = x; } }; Default Big3! void main() { Cell a; a.Set(2); Cell b = a; Cell c(a); b.Set(8); c = b; a.Set(5); cout << a.Get() << " " << b.Get() << " " << c.Get() << endl; } Example: Default Big3 Cell( const Cell & c ) { value = c.value; } ~Cell(){ } Cell& operator=( const Cell &rhs ) { if ( this != & rhs ) // must check for self assignment! value = rhs.value; return *this; // return a reference to myself } this is a pointer to the object that the member function is being called on. a = b is the same as a.operator=(b) Pointers and the Big Three If a class contains pointers, we may have problem with defaults!! — we must delete pointers by ourselves. — The copy constructor and operator= only do “shallow copying”: giving us two class instances with a pointer to the same object. we expect “deep copying” We need to implement the Big Three by ourselves! Example: With Pointer Member class Cell { private: int *value; public: int Get() { return *value; } void Set( int x ) { *value = x; } }; Is this code correct? void main() { Cell a; a.Set(2); Cell b = a; Cell c(a); b.Set(8); c = b; a.Set(5); cout << a.Get() << " " << b.Get() << " " << c.Get() << endl; } No! value is not initialized! Example: With Pointer Member class Cell { private: int *value; public: Cell ( int x = 0 ) { value = new int(x); } int Get() { return *value; } void Set( int x ) { *value = x; } }; void main() { Cell a; a.Set(2); Cell b = a; Cell c(a); b.Set(8); c = b; a.Set(5); cout << a.Get() << " " << b.Get() << " " << c.Get() << endl; } Function overloading: If there is no parameter x, assume x = 0. Example: With Pointer Member class Cell { private: int *value; public: Cell ( int x = 0 ) { value = new int(x); } int Get() { return *value; } void Set( int x ) { *value = x; } }; void main() { Cell a; a.Set(2); Cell b = a; Cell c(a); b.Set(8); c = b; } // What is b’s value? a.Set(5); // Will b and c’s values change? cout << a.Get() << " " << b.Get() << " " << c.Get() << endl; // Is there any memory leak? Example: Implement Big3 Cell( const Cell & c ) { value = new int( *c.value ); } ~Cell(){ delete value; } Cell& operator=( const Cell &rhs ) { if ( this != & rhs ) // must check for self assignment! *value = *rhs.value; return *this; // return a reference to myself } Destructor Destructor is called when an object goes out of scope. A class can have only one destructor without any parameters. When there are pointer data members in a class, you should implement your own destructor to — delete all dynamically allocated memory space — other operations as needed Operator Overloading In C++, you can give special meanings to operators when they are used with user-defined classes. • • • • = (assignment operator) + - * (binary arithmetic operators) += -= *= (compound assignment operators) == != > < >= <= (comparison operators) Implement operator overloads by providing special member-functions in your classes. Assignment Operator= class MyClass { public: ... // the = operator don’t change the rhs, only the lhs // return a reference to allow operator chaining // why not returning const MyClass&? // to enable operations such as ( a = b ) = c // "If it's good enough for ints, // it's good enough for user-defined data-types." MyClass & operator=(const MyClass &rhs); ... } MyClass a, b, c, d; ... b = a; // Same as b.operator=(a); d = c = b = a; // assignment is right-associative. // same as d = ( c = ( b = a))); Assignment Operator= The typical sequence of operations: MyClass & operator=(const MyClass &rhs) { //0. Self assignment check! //1. Deallocate any memory that MyClass is using internally //2. Allocate some memory to hold the contents of rhs //3. Copy the values from rhs into this instance //4. return *this; } the member function needs to return a reference to the object. So, it returns *this, which returns what this points at (i.e. the object). (In C++, instances are turned into references, and vice versa, because references are treated as alternative names for instances, so C++ implicitly converts *this into a reference to the current instance.) Assignment Operator= You must check for self assignment!!! What happens if we do: MyClass obj; … obj = obj; obj will first release any memory it holds internally. This completely messes up the rest of the assignment operator's internals. Assignment Operator = Self assignment checking: MyClass & operator=(const MyClass &rhs) { // Check for self-assignment! if (this != &rhs) // Same object? { ... // Deallocate, allocate new space, copy values... } return *this; } Compound Assignment Operators += -= *= // similar prototype as operator= MyClass & MyClass::operator+=(const MyClass &rhs) { ... // Do the compound assignment work. return *this; } MyClass a, b; ... b += a; // Same as b.operator+=(a); Compound assignment operators are destructive operators: they update or replace the values on lhs of the assignment. In general, beware of self-assignment as well. Binary Arithmetic Operators + - * Binary arithmetic operators don’t modify either operand, but actually return a new value from the two arguments // pass a const reference, return a const MyClass value const MyClass MyClass::operator+(const MyClass &other) const { MyClass result = *this; result += other; //return MyClass(*this) += other; return result; } MyClass a, b, c; ... c = a + b; // Same as c = a.operator+(b); Use the compound operator (+=) implementation to implement binary (+) operator! Comparison Operator == and != == and != return true or false usually implement == first, then use == to implement != // pass a const reference, return a bool value bool MyClass::operator==(const MyClass &other) const { // compare the values and return a bool result } bool MyClass::operator!=(const MyClass &other) const { return !(*this == other); } MyClass a, b; ... if ( a == b )// Same as if ( a.operator==(b) ) ... Overloading << and >> // return a reference for ostream to allow recursive << // use friend keyword to access the private members of MyClass friend ostream& operator<<(ostream& out, const MyClass & m) { out << ...what to print m...; return out; } friend istream& operator>>(istream& in, MyClass & m) { // whatever code to read m: use in instead of cin return in; } MyClass a,b; cin >> a >> b; cout << a << b; Friend Functions A friend function is a function that is not a member of a class but has access to the class's private and protected members. — friend functions are not considered as class members; they are not in the class's scope, and they are not called using the dot notation. — A friend function is declared by the class that is granting access. — The friend declaration can be placed anywhere in the class declaration. It is not affected by the access control keywords. Operator overloading restrictions Cannot overload — scope resolution operator :: — member access operator ., .* — ternary conditional operator ?: Cannot create new operators After class exercise: How to implement a growable array? — Hint: define a growable array class; overload operator[] How to overload operator++?