570 likes | 708 Views
Software Design and C++ Programming. Lecture 4 Operator Overloading and Streamed I/O. Contents. Introduction Operators in C++ Operator overload functions Member function v friend function overloading Converting between types Overloading the assignment operator
E N D
Software Design and C++ Programming Lecture 4 Operator Overloading and Streamed I/O
Contents • Introduction • Operators in C++ • Operator overload functions • Member function v friend function overloading • Converting between types • Overloading the assignment operator • Overloading the array subscripting operator • Example – A Date class • Introduction to streams • The ostream and istream classes • User defined input/ouput • Other features of streams
Introduction • Operator overloading is a powerful feature of C++ • It provides programmers with a concise notation for manipulating user defined objects • It is simple to implement given a few basic rules • It is also used in providing input/output capabilities via the Stream classes as we shall see in the next lecture
Operators in C++ • C++ has a rich collection of operators most of which are common to other programming languages • It is the standard arithmetic and logical operators • + - * / % & ! >> << || && == etc • It has thearray indexing and function evaluation operators • [] () • It has the assignment operators • = += -= *= /= &= |= etc
It has the auto increment and decrement operators • ++ -- • It has the pointer de-referencing and address of operators • * & • It has the memory management operators • new delete new[] delete[]
We can divide up the set of operators into unary and binary operators • A unary operator has one operand • Examples if (!x) {..} // unary operator !, operand x x++; // post-fix operator ++, operand x --x; / / pre-fix operator --, operand x int y=a[5]; // operator [], operand a
A binary operator has two operands • Examples int z=x+y; // binary operator +, operands x and y bool z=x&&y; // binary operator && operands x and y x+=y; // binary operator +=, operands x and y
Operator overload functions • In order to overload and operator op, a function operator op must be defined • operator+ to overload + • operator+= to overload += • operator[] to overload [] • etc • We can either provide member functions of a class or external functions (possibly friend functions of a class)
Restrictions on operator overloading • The precedence of an operator cannot be changed • The arity of an operator cannot be changed • We cannot redefine a binary operator to be unary or vice verse • The associativity of an operator cannot change • Whether it applies left to right or right to left
Example • We can define a complex class to represent a complex number class complex { private : double re, im; public : complex(double r,double i) { re = r; im = i;} friend complex operator +(complex, complex); friend complex operator -(complex); };
We have included 2 friend functions for overloading the binary + operator and unary - operator complex operator +(complex a,complex b) { return complex(a.re + b.re , a.im + b.im); } complex operator -(complex a) { return complex(-a.re, -a.im); }
We can also implement the overload functions as member functions • In this case, this replaces one of the arguments class complex { private : double re, im; public : complex(double r,double i) { re = r; im = i;} complex operator +(complex); complex operator -(); };
complex complex::operator +(complex a) { return complex(re + a.re , im + a.im); } complex complex::operator -() { return complex(-re, -im); }
The class can now be used as follows void main() { complex z1(3.0,2.0),z2(3.0,-5.0),z3,z4; z3 = z1 + z2; // overload operator+ z4=-z3; // overload operator- }
z1 + z2 is implemented as operator+(z1,z2) when a global function overload is used and z1.operator+(z2) when a member function overload is used • -z3 is implemented as operator-(z3) when a global function overload is used and z3.operator-() when a member function overload is used
Converting between types • It is often necessary to convert data of one type into data of another type • This is done implicitly by the compiler when we, for example, add an integer variable to a floating point variable • However, for user defined types, the compiler cannot know in advance how to do these conversions • Two mechanisms exist in C++ to convert data types • Conversion constructor • Conversion operator
Conversion constructor • Suppose we wanted to write the following simple piece of code void main() { complex z1,z2; z1 = z2 + 3.0; }
We could write an operator overload function operator+(complex,double) • But we would need to repeat this for every other operator (eg. -,*,/) • The solution is to write a complex->double conversion constructor complex :: complex(double d) { re = d; im = 0; }
z2+3.0 is now interpreted as : operator+(z2,complex(3.0)) • This will cause problems if we have implemented operator+() as a member function • z2+3.0 must be interpreted in the same way as 3.0+z2 (addition is commutative) • Member function implementation of z2+3.0 is z2.operator(complex(3.0)) • There is no equivalent implementation of 3.0+z2
Conversion operator • This is used to to convert an object of one class into an object of another class or into a built in type (int, float, char etc) • Sometimes called a cast operator as it essentially overloads the cast () operation • Declared as a member function of some class, operator X() specifies how to convert an object of this class into an object of type X
As a (silly) example, we could define a member function double() to convert a complex to a double by taking the real part class complex { private : double re, im; public : complex(double r,double i) { re = r; im = i;} operator double() {return re;} };
If we then cast a complex to a double, the conversion operator is called void main() { complex z1(1,0,2.0); double x=(double) z1; // calls double(), x=1.0 }
Overloading the assignment operator • A powerful feature of operator overloading is overloading the assignment operator = • This allows us to specify the action of the statement a=b for two objects a and b of some class • For our simple complex class, implementing operator= is not necessary • z1=z2 causes a default member-wise assignment to be implemented • z1.re=z2.re, z1.im=z2.im
We can see how assignment overload becomes important by considering a simple String class • Memory for the string is allocated dynamically in the constructor
class String { private: char *p; // pointer to string data int size; // length of string public: String (int sz) { p = new char[size = sz];} ~String( ); int getSize( ) { return size; } char* getp( ) { return p;} };
Without an overloaded assignment operator, assignment involves member-wise copy which doesn’t transfer the data • It just copies the pointers main( ) { string s1(3); // string of 3 chars string s2(3); // string of 3 chars s1 = s2; // are you sure? }
“a”, “b”, “c”, “a”, “b”, “c”, Before assignment s1.p s2.p “d”, “e”, “f”, After assignment s1.p “d”, “e”, “f”, s2.p
An assignment overload operatordefined asString& String::operator = (String& a) uses strcpy() to ensure data is copied String& String::operator = (String& a) { if (this != &a) // avoid s = s assignment { delete p; p = new char[size = a.getSize( )]; strcpy(p,a.getp( )); // copy *a.p into *p } return *this; }
“a”, “b”, “c”, “d”, “e”, “f”, Before assignment s1.p s2.p “d”, “e”, “f”, After assignment s1.p s2.p “d”, “e”, “f”,
Why does operator=(String) return a String reference? • Assignment as a right to left associativity • A statement like x=y=z is implemented as (x=(y=z)) • The result of y=z is a reference to object y • This then appears in the x=y assignment • This ‘trick’ is also extensively used in streamed i/o
Overloading the array subscripting operator • The [] operator normally used for array access can be overloaded • It enables us to design a safe array class where attempts to access beyond the bounds of the array are flagged
class SafeArray { private: int* data; int numPoints; public: SafeArray(int); int& operator[](int); };
int& SafeArray::operator[](int index) { if ((index<0)||(index>=numPoints)) { printf( “Index array out of range ”); exit(1); // exit program } return data[index]; }
Why does operator[] return a reference? • Allows array values to be set as well as accessed using the overload function • In other words, the returned value can be used as an lvalue void main() { SafeArray sa(10); // 10 point safe array int j=sa[3]; // Access value sa[5]=5; // Set value int k=sa[10]; // Out of range! }
Example. A Date class • We can design a Date class which uses overloaded ++ operator functions to add 1 to the day • Program statements such as date++ and ++date will then increment the date • We have to think carefully about the differences between the post and pre-increment overload functions
class Date { private: int day,month,year; void helpIncrement(); static const int days[]; public: Date(int d, int m, int y) {day=d;month=m;year=y;} Date& operator++(); // pre-increment Date operator++(int); // post-increment int endOfMonth(int) const; int leapYear(int) const; };
days[] is a convenience array for storing the days per month • helpIncrement() is a convenience function for helping to increment the day • endOfMonth() uses the days[] array to determine if a given day is the last in the month (taking into account leap years) const int Date::days[]= {0,31,28,31,30,31,30,31,31,30,31,30,31};
void Date::helpIncrement() { if (!endOfMonth(day)) ++day; else if (month<12) { ++month; day=1; } else { ++year; month=day=1; } }
Date& Date::operator++() { // Pre-increment operator helpIncrement(); return *this; // reference returned } Date Date::operator++(int) { //Post increment operator Date temp=*this; // current object state helpIncrement(); return temp; // can’t return reference }
The pre-increment operator overload function returns a reference so that pre-incremented Date objects can be used as lvalues (Date ++d=….) • We can’t do this for the post increment overload function as we can’t return a reference to a temporary local variable • In any case syntax such as Date d++=… is not allowed
Introduction to streams • C++ provides an extensive set of input/output capabilities • Stream based input/output relies extensively on operator overloading as well as being object oriented • Specific I/O routines are called depending on the data type of the object being input or output • Users can specify how to perform I/O for objects or programmer defined types
The ostream and istream classes • The iostream library provides the ostream class for stream-based output and the istream class for stream-based input • These classes provide overload functions for the << and >> operators • << - stream insertion operator for output • >> - stream extraction operator for input • Overload functions for the primitive types are provided • Overload functions can easily be provided for programmer defined classes
The ostream class class ostream { . . public : ostream& operator << (const char*); ostream& operator<<(char); ostream& operator<<(int); ostream& operator<<(double); ostream& operator<<(const void*); . . }
All C++ programs have access to an ostream object cout • ostream overload functions automatically called by matching the data types • The << operator has left-to-right associativity so multiple objects can be output in one statment
Example int x=10; cout << "x = " << x; // Outputs “x= 10” • Implemented as (cout.operator<<(“x=“)).operator<<(x); • Overload functions ostream.operator<<(char*) and ostream.operator<<(int) called
The istream class class istream { . . public : istream& operator >> (char*); istream& operator>>(char&); istream& operator>>(int&); istream& operator>>(double&); . . }
All C++ programs have access to an istream object cin • istream overload functions take reference arguments as they modify the arguments • The >> operator has left-to-right associativity so multiple objects can be input in one statement • operator>>() functions skips white space characters (blanks, tabs, newline, formfeed, carriage return) on the input stream in the same way as scanf() )
Example void main() { int i1, i2; float f; cout << "Input 2 ints and a float \n"; cin >> i1 >> i2 >> f; }
Using right to left associativity, the input is implemented as(((cin >> i1) >> i2) >> f); • operator>>(int&) is called twice and operator>>(float&) is called once • The values input must be separated by whitespace characters (blanks, tabs, cr) and not commas!
User defined input/ouput • We can add overloaded operator>>() and operator<<() functions to our own classes • They must be overloaded as friend functions of the class and not member functions • Obviously they are implemented as binary operators with the first operand cin or cout • We must remember to return a reference to an ostream or istream object