Operator overloading means that the operation performed by the operator depends on the type of operands provided to the operator. For example, (a) the bit left-shift operator << is overloaded to perform stream insertion if the left operand is a ostream object such as cout; (b) the operator * could means multiplication for two numbers of built-in types or indirection if it operates on an address. C++ lets you extend operator overloading to user-defined types (classes).
Operator overloading is similar to function overloading, where you have many versions of the same function differentiated by their parameter lists.
Overloaded Operators in the string class
As an example, the C++ string class (in header <string>) overloads these operators to work on string objects:
- String comparison (
==,!=,>,<,>=,<=): For example, you can usestr1 == str2to compare the contents of twostringobjects. - Stream insertion and extraction (
<<,>>): For example, you can usecout << str1andcin >> str2to output/inputstringobjects. - Strings concatenation (
+,+=): For example,str1 + str2concatenates twostringobjects to produce a newstringobject;str1 += str2appendsstr2intostr1. - Character indexing or subscripting
[]: For example, you can usestr[n]to get thecharat indexn; orstr[n] = cto modify thecharat indexn. Take note that[]operator does not perform index-bound check, i.e., you have to ensure that the index is within the bounds. To perform index-bound check, you can usestring'sat()member function. - Assignment (
=): For example,str1 = str2assignsstr2intostr1.
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* Test overloaded operators in the C++ string class (TestStringOverloadOperators.cpp) */ #include <iostream> #include <iomanip> #include <string> // needed to use the string class using namespace std; int main() { string msg1("hello"); string msg2("HELLO"); string msg3("hello"); // Relational Operators (comparing the contents) cout << boolalpha; cout << (msg1 == msg2) << endl; // false cout << (msg1 == msg3) << endl; // true cout << (msg1 < msg2) << endl; // false (uppercases before lowercases) // Assignment string msg4 = msg1; cout << msg4 << endl; // hello // Concatenation cout << (msg1 + " " + msg2) << endl; // hello HELLO msg3 += msg2; cout << msg3 << endl; // helloHELLO // Indexing cout << msg1[1] << endl; // 'e' cout << msg1[99] << endl; // garbage (no index-bound check) // cout << msg1.at(99) << endl; // out_of_range exception } |
Notes: The relational operators (==, !=, >, <, >=, <=), +, <<, >> are overloaded as non-member functions, where the left operand could be a non-string object (such as C-string, cin, cout); while =, [], += are overloaded as member functions where the left operand must be a string object. I shall elaborate later.
User-defined Operator Overloading
"operator" Functions
To overload an operator, you use a special function form called an operator function, in the form of operatorΔ(), where Δ denotes the operator to be overloaded:
return-type operatorΔ(parameter-list)
For example, operator+() overloads the + operator; operator<<() overloads the << operator. Take note that Δ must be an existing C++ operator. You cannot create you own operator.
Example: Overloading '+' Operator for the Point Class as Member Function
In this example, we shall overload the '+' operator in the Point class to support addition of two Point objects. In other words, we can write p3 = p1+p2, where p1, p2 and p3 are Point objects, similar to the usual arithmetic operation. We shall construct a new Point instance p3 for the sum, without changing the p1 and p2 instances.
Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* The Point class Header file (Point.h) */ #ifndef POINT_H #define POINT_H class Point { private: int x, y; // Private data members public: Point(int x = 0, int y = 0); // Constructor int getX() const; // Getters int getY() const; void setX(int x); // Setters void setY(int y); void print() const; const Point operator+(const Point & rhs) const; // Overload '+' operator as member function of the class }; #endif |
Program Notes:
- We overload the
+operator via a member functionoperator+(), which shall add this instance (left operand) with therhsoperand, construct a new instance containing the sum and and return it by value. We cannot return by reference a local variable created inside the function, as the local variable would be destroyed when the function exits. - The
rhsoperand is passed by reference for performance. - The member function is declared
const, which cannot modify data members. - The return value is declared
const, so as to prevent it from being used as lvalue. For example, it prevents writing(p1+p2) = p3, which is meaningless and could be due to misspelling(p1+p2) == p3.
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/* The Point class Implementation file (Point.cpp) */ #include "Point.h" #include <iostream> using namespace std; // Constructor - The default values are specified in the declaration Point::Point(int x, int y) : x(x), y(y) { } // Using initializer list // Getters int Point::getX() const { return x; } int Point::getY() const { return y; } // Setters void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } // Public Functions void Point::print() const { cout << "(" << x << "," << y << ")" << endl; } // Member function overloading '+' operator const Point Point::operator+(const Point & rhs) const { return Point(x + rhs.x, y + rhs.y); } |
Program Notes:
- The function allocates a new
Pointobject with the sums ofx's andy's, and returns this object byconstvalue.
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include "Point.h"
#include <iostream>
using namespace std;
int main() {
Point p1(1, 2), p2(4, 5);
// Use overloaded operator +
Point p3 = p1 + p2;
p1.print(); // (1,2)
p2.print(); // (4,5)
p3.print(); // (5,7)
// Invoke via usual dot syntax, same as p1+p2
Point p4 = p1.operator+(p2);
p4.print(); // (5,7)
// Chaining
Point p5 = p1 + p2 + p3 + p4;
p5.print(); // (15,21)
}
|
Program Notes:
- You can invoke the overloaded operator via
p1+p2, which will be translated into the dot operationp1.operator+(p2). - The
+operator supports chaining (cascading) operations, asp1+p2returns aPointobject.
Restrictions on Operator Overloading
- The overloaded operator must be an existing and valid operator. You cannot create your own operator such as ⊕.
- Certain C++ operators cannot be overloaded, such as
sizeof, dot (.and.*), scope resolution (::) and conditional (?:). - The overloaded operator must have at least one operands of the user-defined types. You cannot overload an operator working on fundamental types. That is, you can't overload the
'+'operator for twoints (fundamental type) to perform subtraction. - You cannot change the syntax rules (such as associativity, precedence and number of arguments) of the overloaded operator.
Overloading Operator via "friend" non-member function
Why can't we always use Member Function for Operator Overloading?
The member function operatorΔ() can only be invoked from an object via the dot operator, e.g., p1.operatorΔ(p2), which is equivalent to p1 Δ p2. Clearly the left operand p1 should be an object of that particular class. Suppose that we want to overload a binary operator such as * to multiply the object p1 with an int literal, p1*5 can be translated into p1.operator*(5), but 5*p1 cannot be represented using member function. One way to deal with this problem is only allow user to write p1*5 but not 5*p1, which is not user friendly and break the rule of commutativity. Another way is to use a non-member function, which does not invoke through an object and dot operator, but through the arguments provided. For example, 5*p1 could be translated to operator+(5, p1).
In brief, you cannot use member function to overload an operator if the left operand is not an object of that particular class.
"friend" Functions
A regular non-member function cannot directly access the private data of the objects given in its arguments. A special type of function, called friends, are allowed to access the private data.
A "friend" function of a class, marked by the keyword friend, is a function defined outside the class, yet its argument of that class has unrestricted access to all the class members (private, protected and public data members and member functions). Friend functions can enhance the performance, as they eliminate the need of calling public member functions to access the private data members.
Example: Overloading << and >> Operators of Point class using non-member friend Functions
In this example, we shall overload << and >> operators to support stream insertion and extraction of Point objects, i.e., cout << aPoint, and cin >> aPoint. Since the left operand is not a Point object (cout is an ostream object and cin is an istream object), we cannot use member function, but need to use non-member function for operator overloading. We shall make these functions friends of the Point class, to allow them to access the private data members directly for enhanced performance.
Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* The Point class Header file (Point.h) */ #ifndef POINT_H #define POINT_H #include <iostream> // Class Declaration class Point { private: int x, y; public: Point(int x = 0, int y = 0); int getX() const; // Getters int getY() const; void setX(int x); // Setters void setY(int y); friend std::ostream & operator<<(std::ostream & out, const Point & point); friend std::istream & operator>>(std::istream & in, Point & point); }; #endif |
Program Notes:
- Friends are neither
publicorprivate, and can be listed anywhere within the class declaration. - The
coutandcinneed to be passed into the function by reference, so that the function accesses thecoutandcindirectly (instead of a clone copy by value). - We return the
cinandcoutpassed into the function by reference too, so as to support cascading operations. For example,cout << p1 << endlwill be interpreted as(cout << p1) << endl. - In
<<, the reference parameterPointis declared asconst. Hence, the function cannot modify thePointobject. On the other hand, in>>, thePointreference is non-const, as it will be modified to keep the input. - We use fully-qualified name
std::istreaminstead of placing a "using namespace std;" statement in the header. It is because this header could be included in many files, which would include theusingstatement too and may not be desirable.
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/* The Point class Implementation file (Point.cpp) */ #include <iostream> #include "Point.h" using namespace std; // Constructor - The default values are specified in the declaration Point::Point(int x, int y) : x(x), y(y) { } // using member initializer list // Getters int Point::getX() const { return x; } int Point::getY() const { return y; } // Setters void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.x << "," << point.y << ")"; // access private data return out; } istream & operator>>(istream & in, Point & point) { cout << "Enter x and y coord: "; in >> point.x >> point.y; // access private data return in; } |
Program Notes:
- The function definition does not require the keyword
friend, and theClassName::scope resolution qualifier, as it does not belong to the class. - The
operator<<()function is declared as a friend ofPointclass. Hence, it can access the private data membersxandyof its argumentPointdirectly.operator<<()function is NOT a friend ofostreamclass, as there is no need to access the private member ofostream. - Instead of accessing private data member
xandydirectly, you could use public member functiongetX()andgetY(). In this case, there is no need to declareoperator<<()as a friend of thePointclass. You could simply declare a regular function prototype in the header.// Function prototype ostream & operator<<(ostream & out, const Point & point); // Function definition ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.getX() << "," << point.getY() << ")"; return out; }
Usingfriendis recommended, as it enhances performance. Furthermore, the overloaded operator becomes part of the extended public interface of the class, which helps in ease-of-use and ease-of-maintenance.
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream>
#include "Point.h"
using namespace std;
int main() {
Point p1(1, 2), p2;
// Using overloaded operator <<
cout << p1 << endl; // support cascading
operator<<(cout, p1); // same as cout << p1
cout << endl;
// Using overloaded operator >>
cin >> p1;
cout << p1 << endl;
operator>>(cin, p1); // same as cin >> p1
cout << p1 << endl;
cin >> p1 >> p2; // support cascading
cout << p1 << endl;
cout << p2 << endl;
}
|
The overloaded >> and << can also be used for file input/output, as the file IO stream ifstream/ofstream (in fstream header) is a subclass of istream/ostream. For example,
#include <fstream>
#include "Point.h"
using namespace std;
int main() {
Point p1(1, 2);
ofstream fout("out.txt");
fout << p1 << endl;
ifstream fin("in.txt"); // contains "3 4"
fin >> p1;
cout << p1 << endl;
}
Overloading Binary Operators
All C++ operators are either binary (e.g., x + y) or unary (e.g. !x, -x), with the exception of tenary conditional operator (? :) which cannot be overloaded.
Suppose that we wish to overload the binary operator == to compare two Point objects. We could do it as a member function or non-member function.
- To overload as a member function, the declaration is as follows:
class Point { public: bool operator==(const Point & rhs) const; // p1.operator==(p2) ...... };The compiler translates "p1 == p2" to "p1.operator==(p2)", as a member function call of objectp1, with argumentp2.
Member function can only be used if the left operand is an object of that particular class.
- To overload as a non-member function, which is often declared as a
friendto access the private data for enhanced performance, the declaration is as follows:class Point { friend bool operator==(const Point & lhs, const Point & rhs); // operator==(p1,p2) ...... };The compiler translates the expression "p1 == p2" to "operator==(p1, p2)".
Overloading Unary Operators
Most of the unary operators are prefix operators, e.g., !x, -x. Hence, prefix is the norm for unary operators. However, unary increment and decrement come in two forms: prefix (++x, --x) and postfix (x++, x--). We to a mechanism to differentiate the two forms.
Unary Prefix Operator
Example of unary prefix operators are !x, -x, ++x and --x. You could do it as a non-member function as well as member function. For example, to overload the prefix increment operator ++:
- To overload as a non-member
friendfunction:class Point { friend Point & operator++(Point & point); ...... };The compiler translates "++p" to "operator++(p)". - To overload as a member function:
class Point { public: Point & operator++(); // this Point ...... };The compiler translates "++p" to "p.operator++()".
You can use either member function or non-member friend function to overload unary operators, as their only operand shall be an object of that class.
Unary Postfix Operator
The unary increment and decrement operators come in two forms: prefix (++x, --x) and postfix (x++, x--). Overloading postfix operators (such as x++, x--) present a challenge. It ought to be differentiated from the prefix operator (++x, --x). A "dummy" argument is therefore introduced to indicate postfix operation as shown below. Take note that postfix ++ shall save the old value, perform the increment, and then return the saved value by value.
- To overload as non-member
friendfunction:class Point { friend const Point operator++(Point & point, int dummy); };The compiler translates "pt++" to "operator++(pt, 0)". Theintargument is strictly a dummy value to differentiate prefix from postfix operation. - To overload as a member function:
class Point { public: const Point operator++(int dummy); // this Point ...... };The compiler translates "pt++" to "pt.operator++(0)".
Example: Overloading Prefix and Postfix ++ for the Counter Class
Counter.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/* The Counter class Header file (Counter.h) */ #ifndef COUNTER_H #define COUNTER_H #include <iostream> class Counter { private: int count; public: Counter(int count = 0); // Constructor int getCount() const; // Getters void setCount(int count); // Setters Counter & operator++(); // ++prefix const Counter operator++(int dummy); // postfix++ friend std::ostream & operator<<(std::ostream & out, const Counter & counter); }; #endif |
Program Notes:
- The prefix function returns a reference to this instance, to support chaining (or cascading), e.g.,
++++cas++(++c). However, the return reference can be used as lvalue with unexpected operations (e.g.,++c = 8). - The postfix function returns a
constobject by value. Aconstvalue cannot be used as lvalue. This prevents chaining such asc++++. Although it would be interpreted as(c++)++. However,(c++)does not return this object, but an temporary object. The subsequent++works on the temporary object. - Both prefix and postfix functions are non-
const, as they modify the data membercount.
Counter.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* The Counter class Implementation file (Counter.cpp) */ #include "Counter.h" #include <iostream> using namespace std; // Constructor - The default values are specified in the declaration Counter::Counter(int c) : count(c) { } // using member initializer list // Getters int Counter::getCount() const { return count; } // Setters void Counter::setCount(int c) { count = c; } // ++prefix, return reference of this Counter & Counter::operator++() { ++count; return *this; } // postfix++, return old value by value const Counter Counter::operator++(int dummy) { Counter old(*this); ++count; return old; } // Overload stream insertion << operator ostream & operator<<(ostream & out, const Counter & counter) { out << counter.count; return out; } |
Program Notes:
- The prefix function increments the
count, and returns this object by reference. - The postfix function saves the old value (by constructing a new instance with this object via the copy constructor), increments the
count, and return the saved object by value. - Clearly, postfix operation on object is less efficient than the prefix operation, as it create a temporary object. If there is no subsequent operation that relies on the output of prefix/postfix operation, use prefix operation.
TestCounter.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include "Counter.h"
#include <iostream>
using namespace std;
int main() {
Counter c1;
cout << c1 << endl; // 0
cout << ++c1 << endl; // 1
cout << c1 << endl; // 1
cout << c1++ << endl; // 1
cout << c1 << endl; // 2
cout << ++++c1 << endl; // 4
// cout << c1++++ << endl; // error caused by const return value
}
|
Program Notes:
- Take note of the difference in
cout << c1++andcout << ++c1. Both prefix and postfix operators work as expected. ++++c1is allowed and works correctly.c1++++is disallowed, because it would produce incorrect result.
Example: Putting them together in Point Class
This example overload binary operator << and >> as non-member functions for stream insertion and stream extraction. It also overload unary ++ (postfix and prefix) and binary += as member function; and +, += operators.
Point.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
/* The Point class Header file (Point.h) */ #ifndef POINT_H #define POINT_H #include <iostream> class Point { private: int x, y; public: explicit Point(int x = 0, int y = 0); int getX() const; int getY() const; void setX(int x); void setY(int y); Point & operator++(); // ++prefix const Point operator++(int dummy); // postfix++ const Point operator+(const Point & rhs) const; // Point + Point const Point operator+(int value) const; // Point + int Point & operator+=(int value); // Point += int Point & operator+=(const Point & rhs); // Point += Point friend std::ostream & operator<<(std::ostream & out, const Point & point); // out << point friend std::istream & operator>>(std::istream & in, Point & point); // in >> point friend const Point operator+(int value, const Point & rhs); // int + Point }; #endif |
Point.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
/* The Point class Implementation file (Point.cpp) */ #include "Point.h" #include <iostream> using namespace std; // Constructor - The default values are specified in the declaration Point::Point(int x, int y) : x(x), y(y) { } // Getters int Point::getX() const { return x; } int Point::getY() const { return y; } // Setters void Point::setX(int x) { this->x = x; } void Point::setY(int y) { this->y = y; } // Overload ++Prefix, increase x, y by 1 Point & Point::operator++() { ++x; ++y; return *this; } // Overload Postfix++, increase x, y by 1 const Point Point::operator++(int dummy) { Point old(*this); ++x; ++y; return old; } // Overload Point + int. Return a new Point by value const Point Point::operator+(int value) const { return Point(x + value, y + value); } // Overload Point + Point. Return a new Point by value const Point Point::operator+(const Point & rhs) const { return Point(x + rhs.x, y + rhs.y); } // Overload Point += int. Increase x, y by value Point & Point::operator+=(int value) { x += value; y += value; return *this; } // Overload Point += Point. Increase x, y by rhs Point & Point::operator+=(const Point & rhs) { x += rhs.x; y += rhs.y; return *this; } // Overload << stream insertion operator ostream & operator<<(ostream & out, const Point & point) { out << "(" << point.x << "," << point.y << ")"; return out; } // Overload >> stream extraction operator istream & operator>>(istream & in, Point & point) { cout << "Enter x and y coord: "; in >> point.x >> point.y; return in; } // Overload int + Point. Return a new point const Point operator+(int value, const Point & rhs) { return rhs + value; // use member function defined above } |
TestPoint.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream>
#include "Point.h"
using namespace std;
int main() {
Point p1(1, 2);
cout << p1 << endl; // (1,2)
Point p2(3,4);
cout << p1 + p2 << endl; // (4,6)
cout << p1 + 10 << endl; // (11,12)
cout << 20 + p1 << endl; // (21,22)
cout << 10 + p1 + 20 + p1 << endl; // (32,34)
p1 += p2;
cout << p1 << endl; // (4,6)
p1 += 3;
cout << p1 << endl; // (7,9)
Point p3; // (0,0)
cout << p3++ << endl; // (0,0)
cout << p3 << endl; // (1,1)
cout << ++p3 << endl; // (2,2)
}
|
Implicit Conversion via Single-argument Constructor & Keyword "explicit"
In C++, a single-argument constructor can be used to implicitly convert a value to an object. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
Counter(int c = 0) : count(c) { }
// A single-argument Constructor which takes an int
// It can be used to implicitly convert an int to a Counter object
int getCount() const { return count; } // Getter
void setCount(int c) { count = c; } // Setter
};
int main() {
Counter c1; // Declare an instance and invoke default constructor
cout << c1.getCount() << endl; // 0
c1 = 9;
// Implicit conversion
// Invoke single-argument constructor Counter(9) to construct a temporary object.
// Then copy into c1 via memberwise assignment.
cout << c1.getCount() << endl; // 9
}
|
This implicit conversion can be confusing. C++ introduces a keyword "explicit" to disable implicit conversion. Nonetheless, you can still perform explicit conversion via type cast operator. For example,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream>
using namespace std;
class Counter {
private:
int count;
public:
explicit Counter(int c = 0) : count(c) { }
// Single-argument Constructor
// Use keyword "explicit" to disable implicit automatic conversion in assignment
int getCount() const { return count; } // Getter
void setCount(int c) { count = c; } // Setter
};
int main() {
Counter c1; // Declare an instance and invoke default constructor
cout << c1.getCount() << endl; // 0
// Counter c2 = 9;
// error: conversion from 'int' to non-scalar type 'Counter' requested
c1 = (Counter)9; // Explicit conversion via type casting operator
cout << c1.getCount() << endl; // 9
}
|
Example: The MyComplex Class
The MyComplex class is simplified from the C++ STL's complex template class. I strongly recommend that you study the source code of complex (in the complex header) - you can download the source code for GNU GCC.
MyComplex.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/* * The MyComplex class header (MyComplex.h) * Follow, modified and simplified from GNU GCC complex template class */ #ifndef MY_COMPLEX_H #define MY_COMPLEX_H #include <iostream> class MyComplex { private: double real, imag; public: explicit MyComplex (double real = 0, double imag = 0); // Constructor MyComplex & operator+= (const MyComplex & rhs); // c1 += c2 MyComplex & operator+= (double real); // c += double MyComplex & operator++ (); // ++c const MyComplex operator++ (int dummy); // c++ bool operator== (const MyComplex & rhs) const; // c1 == c2 bool operator!= (const MyComplex & rhs) const; // c1 != c2 // friends friend std::ostream & operator<< (std::ostream & out, const MyComplex & c); // out << c friend std::istream & operator>> (std::istream & in, MyComplex & c); // in >> c friend const MyComplex operator+ (const MyComplex & lhs, const MyComplex & rhs); // c1 + c2 friend const MyComplex operator+ (double real, const MyComplex & rhs); // double + c friend const MyComplex operator+ (const MyComplex & lhs, double real); // c + double }; #endif |
Program Notes:
- I prefer to list the
privatesection before thepublicsection in the class declaration to have a quick look at the internal of the class for ease of understanding. - I named the
privatedata membersrealandimag, that potentially crash with the function parameters. I resolves the crashes viathis->pointer if needed. Some people suggest to nameprivatedata members with a trailing underscore (e.g.,real_,imag_) to distinguish from the function parameters. As private members are not expose to the users, strange names are acceptable. The C++ compiler uses leading underscore(s) to name its variables internally (_xxxfor data members,__xxxfor local variables). - The constructor is declared
explicit. This is because a single-argument constructor can be used for implicit conversion, in this case, fromdoubletoMyComplex, e.g.,// Without explicit MyComplex c = 5.5; // Same as MyComplex c = (MyComplex)5.5;
The keywordexplicitdisables implicit conversion.// With explicit MyComplex c = 5.5; // error: conversion from 'double' to non-scalar type 'MyComplex' requested MyComplex c = (MyComplex)5.5; // Okay
Avoid implicit conversion, as it is hard to track and maintain. - The constructor sets the default value for
andrealimagto 0. - We overload the stream insertion operator
<<to print aMyComplexobject on aostream(e.g.,cout << c). We use a non-member friend function (instead of member function) as the left operand (cout) is not aMyComplexobject. We declare it as friend of theMyComplexclass to allow direct access of the private data members. The function return a reference of the invokingostreamobject to support cascading operation, e.g.cout << c << endl;. - We overload the prefix increment operator (e.g.,
++c) and postfix increment operator (e.g., c++) as member functions. They increases the real part by 1.0. Since both prefix and postfix operators are unary, a dummyintargument is assigned to postfixoperator++()to distinguish it from prefixoperator++(). The prefix operator returns a reference to this object, but the postfix returns a value. We shall explain this in the implementation. - We overload the plus operator
+to perform addition of twoMyComplexobjects, aMyComplexobject and adouble. Again, we use non-member friend function as the left operand may not be aMyComplexobject. The+shall return a new object, with no change to its operands. - As we overload the
+operator, we also have to overload+=operator. - The function's reference/pointer parameters will be declared
const, if we do not wish to modify the original copy. On the other hand, we omitconstdeclaration for built-in types (e.g., double) in the class declaration as they are passed by value - the original copy can never be changed. - We declare the return values of
+operator asconst, so that they cannot be used as lvalue. It is to prevent meaningless usages such as(c1+c2) = c3(most likely misspelling(c1 + c2) == c3). - We also declare the return value of
++asconst. This is to preventc++++, which could be interpreted as(c++)++. However, asC++return by value a temporary object (instead of the original object), the subsequent++works on the temporary object and yields incorrect output. But++++cis acceptable as++creturns this object by reference.
MyComplex.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
/* The MyComplex class implementation (MyComplex.cpp) */ #include "MyComplex.h" // Constructor MyComplex::MyComplex (double r, double i) : real(r), imag(i) { } // Overloading += operator for c1 += c2 MyComplex & MyComplex::operator+= (const MyComplex & rhs) { real += rhs.real; imag += rhs.imag; return *this; } // Overloading += operator for c1 += double (of real) MyComplex & MyComplex::operator+= (double value) { real += value; return *this; } // Overload prefix increment operator ++c (real part) MyComplex & MyComplex::operator++ () { ++real; // increment real part only return *this; } // Overload postfix increment operator c++ (real part) const MyComplex MyComplex::operator++ (int dummy) { MyComplex saved(*this); ++real; // increment real part only return saved; } // Overload comparison operator c1 == c2 bool MyComplex::operator== (const MyComplex & rhs) const { return (real == rhs.real && imag == rhs.imag); } // Overload comparison operator c1 != c2 bool MyComplex::operator!= (const MyComplex & rhs) const { return !(*this == rhs); } // Overload stream insertion operator out << c (friend) std::ostream & operator<< (std::ostream & out, const MyComplex & c) { out << '(' << c.real << ',' << c.imag << ')'; return out; } // Overload stream extraction operator in >> c (friend) std::istream & operator>> (std::istream & in, MyComplex & c) { double inReal, inImag; char inChar; bool validInput = false; // Input shall be in the format "(real,imag)" in >> inChar; if (inChar == '(') { in >> inReal >> inChar; if (inChar == ',') { in >> inImag >> inChar; if (inChar == ')') { c = MyComplex(inReal, inImag); validInput = true; } } } if (!validInput) in.setstate(std::ios_base::failbit); return in; } // Overloading + operator for c1 + c2 const MyComplex operator+ (const MyComplex & lhs, const MyComplex & rhs) { MyComplex result(lhs); result += rhs; // uses overload += return result; // OR return MyComplex(lhs.real + rhs.real, lhs.imag + rhs.imag); } // Overloading + operator for c + double const MyComplex operator+ (const MyComplex & lhs, double value) { MyComplex result(lhs); result += value; // uses overload += return result; } // Overloading + operator for double + c const MyComplex operator+ (double value, const MyComplex & rhs) { return rhs + value; // swap and use above function } |
Program Notes:
- The prefix
++increments the real part, and returns this object by reference. The postfix++saves this object, increments the real part, and returns the saved object by value. Postfix operation is clearly less efficient than prefix operation! - The
+operators use the+=operator (for academic purpose). - The friend functions is allow to access the private data members.
- The overloaded stream insertion operator
<<outputs "(real,imag)". - The overloaded stream extraction operator
>>inputs "(real,imag)". It sets thefailbitof theistreamobject if the input is not valid.
TestMyComplex.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
/* Test Driver for MyComplex class (TestMyComplex.cpp) */ #include <iostream> #include <iomanip> #include "MyComplex.h" int main() { std::cout << std::fixed << std::setprecision(2); MyComplex c1(3.1, 4.2); std::cout << c1 << std::endl; // (3.10,4.20) MyComplex c2(3.1); std::cout << c2 << std::endl; // (3.10,0.00) MyComplex c3 = c1 + c2; std::cout << c3 << std::endl; // (6.20,4.20) c3 = c1 + 2.1; std::cout << c3 << std::endl; // (5.20,4.20) c3 = 2.2 + c1; std::cout << c3 << std::endl; // (5.30,4.20) c3 += c1; std::cout << c3 << std::endl; // (8.40,8.40) c3 += 2.3; std::cout << c3 << std::endl; // (10.70,8.40) std::cout << ++c3 << std::endl; // (11.70,8.40) std::cout << c3++ << std::endl; // (11.70,8.40) std::cout << c3 << std::endl; // (12.70,8.40) // c1+c2 = c3; // error: c1+c2 returns a const // c1++++; // error: c1++ returns a const // MyComplex c4 = 5.5; // error: implicit conversion disabled MyComplex c4 = (MyComplex)5.5; // explicit type casting allowed std::cout << c4 << std::endl; // (5.50,0.00) MyComplex c5; std::cout << "Enter a complex number in (real,imag): "; std::cin >> c5; if (std::cin.good()) { // if no error std::cout << c5 << std::endl; } else { std::cerr << "Invalid input" << std::endl; } return 0; } |
Dynamic Memory Allocation in Object
If you dynamically allocate memory in the constructor, you need to provide your own destructor, copy constructor and assignment operator to manage the dynamically allocated memory. The defaults provided by the C++ compiler do not work for dynamic memory.
Example: MyDynamicArray
MyDynamicArray.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* * The MyDynamicArray class header (MyDynamicArray.h) * A dynamic array of double elements */ #ifndef MY_DYNAMIC_ARRAY_H #define MY_DYNAMIC_ARRAY_H #include <iostream> class MyDynamicArray { private: int size_; // size of array double * ptr; // pointer to the elements public: explicit MyDynamicArray (int n = 8); // Default constructor explicit MyDynamicArray (const MyDynamicArray & a); // Copy constructor MyDynamicArray (const double a[], int n); // Construct from double[] ~MyDynamicArray(); // Destructor const MyDynamicArray & operator= (const MyDynamicArray & rhs); // Assignment a1 = a2 bool operator== (const MyDynamicArray & rhs) const; // a1 == a2 bool operator!= (const MyDynamicArray & rhs) const; // a1 != a2 double operator[] (int index) const; // a[i] double & operator[] (int index); // a[i] = x int size() const { return size_; } // return size of array // friends friend std::ostream & operator<< (std::ostream & out, const MyDynamicArray & a); // out << a friend std::istream & operator>> (std::istream & in, MyDynamicArray & a); // in >> a }; #endif |
Program Notes:
- In C++, the you cannot use the same name for a data member and a member function. As I would like to have a public function called
size(), which is consistent with the C++ STL, I named the data membersize_with a trailing underscore, following C++'s best practices. Take note that leading underscore(s) are used by C++ compiler for its internal variables (e.g.,_xxxfor data members and__xxxfor local variables). - As we will be dynamically allocating memory in the constructor, we provide our own version of destructor, copy constructor and assignment operator to manage the dynamically allocated memory. The defaults provided by the C++ compiler do not work on dynamic memory.
- We provide 3 constructors: a default constructor with an optional size, a copy constructor to construct an instance by copying another instance, and a construct to construct an instance by copying from a regular array.
- We provide 2 version of indexing operators: one for read operation (e.g.,
a[i]) and another capable of write operation (e.g.,a[i] = x). The read version is declared as aconstmember function; whereas the write version return a reference to the element, which can be used as lvalue for assignment.
MyDynamicArray.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
/* The MyDynamicArray class implementation (MyDynamicArray.cpp) */ #include <stdexcept> #include "MyDynamicArray.h" // Default constructor MyDynamicArray::MyDynamicArray (int n) { if (n <= 0) { throw std::invalid_argument("error: size must be greater then zero"); } // Dynamic allocate memory for n elements size_ = n; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = 0.0; // init all elements to zero } } // Override the copy constructor to handle dynamic memory MyDynamicArray::MyDynamicArray (const MyDynamicArray & a) { // Dynamic allocate memory for a.size_ elements and copy size_ = a.size_; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = a.ptr[i]; // copy each element } } // Construct via a built-in double[] MyDynamicArray::MyDynamicArray (const double a[], int n) { // Dynamic allocate memory for a.size_ elements and copy size_ = n; ptr = new double[size_]; for (int i = 0; i < size_; ++i) { ptr[i] = a[i]; // copy each element } } // Override the default destructor to handle dynamic memory MyDynamicArray::~MyDynamicArray() { delete[] ptr; // free dynamically allocated memory } // Override the default assignment operator to handle dynamic memory const MyDynamicArray & MyDynamicArray::operator= (const MyDynamicArray & rhs) { if (this != &rhs) { // no self assignment if (size_ != rhs.size_) { // reallocate memory for the array delete [] ptr; size_ = rhs.size_; ptr = new double[size_]; } // Copy elements for (int i = 0; i < size_; ++i) { ptr[i] = rhs.ptr[i]; } } return *this; } // Overload comparison operator a1 == a2 bool MyDynamicArray::operator== (const MyDynamicArray & rhs) const { if (size_ != rhs.size_) return false; for (int i = 0; i < size_; ++i) { if (ptr[i] != rhs.ptr[i]) return false; } return true; } // Overload comparison operator a1 != a2 bool MyDynamicArray::operator!= (const MyDynamicArray & rhs) const { return !(*this == rhs); } // Indexing operator - Read double MyDynamicArray::operator[] (int index) const { if (index < 0 || index >= size_) { throw std::out_of_range("error: index out of range"); } return ptr[index]; } // Indexing operator - Writable a[i] = x double & MyDynamicArray::operator[] (int index) { if (index < 0 || index >= size_) { throw std::out_of_range("error: index out of range"); } return ptr[index]; } // Overload stream insertion operator out << a (as friend) std::ostream & operator<< (std::ostream & out, const MyDynamicArray & a) { for (int i = 0; i < a.size_; ++i) { out << a.ptr[i] << ' '; } return out; } // Overload stream extraction operator in >> a (as friend) std::istream & operator>> (std::istream & in, MyDynamicArray & a) { for (int i = 0; i < a.size_; ++i) { in >> a.ptr[i]; } return in; } |
Program Notes:
- Constructor: [TODO]
- Copy Constructor:
- Assignment Operator:
- Indexing Operator:
TestMyDynamicArray.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
/* Test Driver for MyDynamicArray class (TestMyDynamicArray.cpp) */ #include <iostream> #include <iomanip> #include "MyDynamicArray.h" int main() { std::cout << std::fixed << std::setprecision(1) << std::boolalpha; MyDynamicArray a1(5); std::cout << a1 << std::endl; // 0.0 0.0 0.0 0.0 0.0 std::cout << a1.size() << std::endl; // 5 double d[3] = {1.1, 2.2, 3.3}; MyDynamicArray a2(d, 3); std::cout << a2 << std::endl; // 1.1 2.2 3.3 MyDynamicArray a3(a2); // Copy constructor std::cout << a3 << std::endl; // 1.1 2.2 3.3 a1[2] = 8.8; std::cout << a1[2] << std::endl; // 8.8 // std::cout << a1[22] << std::endl; // error: out_of_range a3 = a1; std::cout << a3 << std::endl; // 0.0 0.0 8.8 0.0 0.0 std::cout << (a1 == a3) << std::endl; // true std::cout << (a1 == a2) << std::endl; // false const int SIZE = 3; MyDynamicArray a4(SIZE); std::cout << "Enter " << SIZE << " elements: "; std::cin >> a4; if (std::cin.good()) { std::cout << a4 << std::endl; } else { std::cerr << "Invalid input" << std::endl; } return 0; } |