Introduction
Note: This tutorial assumes that you already have a moderate understanding of the C++ language. You should be able to write and compile simple programs, and you should be familiar with object-oriented programming and how classes work in C++.
Note: The complete source code for the examples in this tutorial can be downloaded at the end of the tutorial.
One of the powerful features of C++ is the ability to overload operators. Almost every operator in C++ can be overloaded to provide custom functionality that will give your classes greater flexibility and allow then to behave as other programmers would expect them to.
In this tutorial, you will learn what operators are, how they can be overloaded, and some common reasons for overloading various operators.
What is an operator in C++?
Operators are functions. Specifically, operators are special functions that are used to perform operations on data without directly calling methods each time. Common operators that you should be familiar with include +, -, *, /, <<, >>, and so on. All such operators can be used on primative data types in C++ to achieve certain results. For example, we can use the + operator to add together two numeric values, such as ints, floats, and doubles:
int result = 2 + 4;
In the code above, two operators are used: the = operator and the + operator. The + operator adds together the two integer values, returning the result. The = operator takes the resulting value on the right and assigns it to the variable on the left.
We also see operators used in other areas, such as writing formatted data to a stream:
cout << "Brownies are better with sprinkles.";
In the code above, the insertion operator is used to insert data into the output stream.
It is important to note that there are two important types of operators: unary and binary.
Unary operators are operators that act on a single piece of data. Look at the following code:
int myInt = 5;
myInt++; // ++ is a unary operator
myInt--; // -- is also a unary operator
In the above example, operator ++ and operator -- are unary operators, because they function on only one piece of data, in this case, the int variable myInt.
Binary operators are operators that act on a two pieces of data. Look at the following code:
float numerator = 5.376;
float denominator = 55.372;
float result = numerator / denominator; // "/" is a binary operator
result = denominator + numerator; // + is also a binary operator
In the above example, operator / and operator + are binary operators, because they function on two pieces of data, in this case, the float variables numerator and denominator.
What operators can be overloaded?
The following operators can be overloaded:
Source: Wikipedia
+ Unary Plus
+ Addition (Sum)
++ Prefix Increment
++ Postfix Increment
+= Assignment by Addition
- Unary Minus (Negation)
- Subtraction (Difference)
-- Prefix Decrement
-- Postfix Decrement
-= Assignment by Subtraction
** Multiplication (Product)
= Assignment by Multiplication
/ Division (Quotient)
/= Assignment by Division
% Modulus (Remainder)
%=** Assignment by Modulus
< Less Than
<= Less Than or Equal To
> Greater Than
>= Greater Than or Equal To
!= Not Equal To
== Equal To
! Logical Negation
&& Logical AND
|| Logical OR
<< Bitwise Left Shift
<<= Assignment by Bitwise Left Shift
>> Bitwise Right Shift
>>= Assignment by Bitwise Right Shift
~ Bitwise One's Complement
& Bitwise AND
&= Assignment by Bitwise AND
| Bitwise OR
|= Assignment by Bitwise OR
^ Bitwise XOR
^= Assignment by Bitwise XOR
= Basic Assignment
() Function Call
[] Array Subscript
* Indirection (Dereference)
& Address-of (Reference)
-> Member by Pointer
->* Member by Pointer Indirection
(type) Cast
, Comma
sizeof Size-of
typeid Type Identification
new Allocate Storage
new Allocate Storage (Array)
delete Deallocate Storage
delete Deallocate Storage (Array)
Why would I want to overload an operator?
Operators are often overloaded to allow custom types to behave like and be used like primative types. It gives your classes a lot of flexibility in terms what how they interact with the rest of your program. In a minute, we'll take a look at an example class and how we can use overloaded operators to enhance it's functionality.
Member vs. Non-member operators
Before we dig in, it is important to note that there are two groups of operators in terms of how they are implemented: member operators and non-member operators. The distinction between the two is the same as it is with methods and functions.
Member operators are operators that are implemented as member functions (methods) of a class.
Non-member operators are operators that are implemented as regular, non-member functions.
Some operators are required to be member operators, others must be non-member operators, and some can be both.
A basic example: Fraction
For the rest of this tutorial, we will be working with a custom class: Fraction. The class declaration for fraction looks like this:
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction( int num, int denom );
void print( const std::ostream& os ) const;
};
Fraction is a simple class that represents a number as a fraction containing a numerator and a denominator. These are set via the constructor as seen in the declaration above. This tutorial assumes basic knowledge of C++ and classes, so I won't show the definitions for the constructor and the print method. You can assume that print() will output the fraction in the form "2/5".
To create a new Fraction object, we would write the following:
Fraction f( 2, 5 ); // create the fraction "2/5"
To print the fraction to standard output, we would write:
f.print( std::cout );
As it stands right now, our Fraction class isn't very useful. There's no way to change the value of our fraction once it's been created, thus it will always be used as a constant. In a moment, we will fix this.
Syntax
The syntax for overloading operators is the same as it is for declaring a function or method:
return-type operator operator-to-overload ( parameters );
Since operators are functions, they can in essence be used in their functional form. For example:
int result = operator+( 2, 5 );
would do exactly the same thing as
int result = 2 + 5;
Obviously, we would never write the first form when writing programs, since it defeats the entire point of using operators in the first place. However, it is important to understand how the compiler treats operators so that we can more easily overload them.
Defining and overloading + and +=
The first operator we want to overload is the + operator. We want to be able to add other fractions to each other. In this case, we can use a member operator, so lets add its declaration to our class:
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction( int num, int denom );
void print( const std::ostream& os ) const;
Fraction operator+( const Fraction& other ) const;
};
Let's look at each piece of that declaration individually:
Fraction is the return type for this method. We are returning a new Fraction object containing the result of the addition operation. We do this to allow operator chaining, which allows us to write something like this:
// Declare four fractions
Fraction a(1, 2), b(2, 5), c(2, 3), d(8, 9);
// Add a, b, and c together and assign to d
// Operator chaining allows us to write a + b + c
d = a + b + c;
In the code above, the program will add a + b and return a new Fraction object containing the result. The result is then added with c, and the result of that operation is then assigned to d.
operator+ signifies the operator to be overloaded. Note that operator is a keyword in C++; whitespace between it and the actual operator to overload is optional.
const Fraction& other is the single parameter for this method. We take a constant reference to a Function object, because we don't want to ever modify the other Fraction, only the one being added to. We use a Fraction reference rather than a Fraction to avoid making an unnecessary copy each time we add.
Now let's take a look at the definition for this method:
Fraction Fraction::operator+( const Fraction& other ) const {
// Find the least common multiple of the denominators
int lcm = other.denominator;
while( lcm % denominator != 0 ) {
lcm += other.denominator;
}
// Return a new fraction with the results
return Fraction(
(lcm / denominator) * numerator +
(lcm / other.denominator) * other.numerator, lcm );
}
In the method, we find the LCM of the two denominators, and add in the appropriate values from the other function. As mentioned earlier, we return a reference to the current Fraction to allow chaining.
So now we have a way of adding two Fractions together. Fantastic! But wait, what if we want to add whole numbers to our Fractions? It's mighty inconvenient to have to create a new Fraction every time we want to add an integer amount to our Fraction objects. Let's go ahead and add another version of the + operator.
Add the declaration:
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction( int num, int denom );
void print( const std::ostream& os ) const;
Fraction operator+( const Fraction& other ) const;
Fraction operator+( int num ) const;
};
And define the method:
Fraction Fraction::operator+( int num ) const {
// Return a new fraction with the results
return Fraction( numerator + denominator * num, denominator );
}
Now that we've overloaded the + operator with another version accepting an integer, we are able to write things like this:
Fraction f( 3, 10 ); // Create the fraction "3/20"
f = f + 5 // f is now "53/10"
At this point, we can easily add the += operator to our class. Let's add the declarations to our class:
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction( int num, int denom );
void print( const std::ostream& os );
Fraction operator+( const Fraction& other ) const;
Fraction operator+( int val ) const;
Fraction& operator+=( const Fraction& other ) const;
Fraction& operator+=( int val ) const;
};
Notice that the declarations for += look very similar to +. Let's look at a few key differences.
This time, our return type is Fraction&, a reference to a Fraction object. We use this because the result of the += operation should be a reference back to the original object being added to. Again, this is to allow operator chaining. Consider the following code:
Fraction a(2, 5), b(6, 8), c(4, 7);
a += b += c;
In the code above, the value of c is added to the value of b. The new value of b is then added to the value of a. Returning references to the objects in the += operator allows us to modify each one in the chain as needed.
We also do not declare these methods as const, since they will directly be modifying their objects.
Now lets look at the code for our += operators:
Fraction& Fraction::operator+=( const Fraction& other ) {
// Use the + operator we created
Fraction temp( *this );
temp = temp + other;
numerator = temp.numerator;
denominator = temp.denominator;
// Return a reference to ourself
return *this;
}
In this implementation, very little code is required since we can use the + operator that we previously defined. The same is true for our "integer version":
Fraction& Fraction::operator+=( int val ) {
// Use the + operator we created
Fraction temp( *this );
temp = temp + val;
numerator = temp.numerator;
denominator = temp.denominator;
// Return a reference to ourself
return *this;
}
Now, we can write things like the following:
Fraction a(3, 5);
a += a; // Add the value of a to a (double it)
a += 4; // Add 4 to a
Taking things a step further: <<
Now that you're comfortable with implementing some basic member operators, lets take a look at an example of an operator that must be implemented as a non-member function: <<
The << operator is used for two things in C++: to perform a bitwise left shift, and to insert data into a output stream. We will only be dealing with the latter.
At the moment, we have a way of printing our class to an output stream: print(). We pass in an output stream and the fraction gets printed for us. For example:
Fraction f(2, 5);
f.print(std::cout);
Will output the following on standard output:
2/5
This is fine for minimal usage of the class. But what if we wanted to include our Fraction as part of a more complicated formatted insertion into a stream?
Fraction f(2, 5);
std::cout << "A string here " << f << " another string, etc." << std::endl;
If we want to be able to insert Fractions into streams in this manner, we must define the << operator, or insertion operator. Let's go ahead and declare it in our class:
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
Fraction( int num, int denom );
void print( const std::ostream& os );
Fraction operator+( const Fraction& other ) const;
Fraction operator+( int val ) const;
Fraction& operator+=( const Fraction& other ) const;
Fraction& operator+=( int val ) const;
};
ostream& operator<<( ostream& os, const Fraction& f );
But wait! The declaration for this function is outside of our class, that is, it's a non-member function! In fact, this operator must be operated as a non-member function. The reason is relatively straightforward once you understand how the operator is called. Remember that a line like the following:
Fraction f( 2, 5 );
std::cout << f;
is actually called like this:
operator<<( std::cout, f );
To write the << operator as a method of Fraction, the output stream would have to be the single argument to the method, and the Fraction object would have to be on the left hand side, like this:
Fraction f( 2, 5 );
f << std::cout;
This is obviously incorrect, since the output stream must always be on the left hand side of the insertion operator. Thus, << would have to be implemented as a member function of std::ostream. Since we cannot modify standard library classes, our only option is to implement << as a non-member function taking two arguments.
Let's look at the code for operator <<:
std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
os << f.numerator << '/' << f.denominator;
return os;
}
This implementation seems valid, but there's a problem. Numerator and denominator are private members of Fraction. Because of this, non-member functions such as our << operator do not have access to them. We could write accessors for the data members, however this is unnecessary and duplicates code. Looking back at our class declaration, we see that we already have a nice method to print out a Fraction to an output stream: print(). Since this method is public, let's go ahead and use it in our << operator:
std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
// Use the member function to print to the output stream
f.print( os );
return os;
}
Now we have one single piece of code, contained in our class, that determines how Fractions are displayed. If we ever want to change this, we simply change it in print(), and our << operator will sill work correctly.
Style and usage guidelines
One last thing to address on the subject of operator overloading is style and usage. It can be very tempting for beginners to start overloading all sorts of operators in their classes, sometimes for purposes not intended by that operator. For example, one might be creating a List class and choose to overload the increment operator (++) to automatically add an element to the list. This can be done, and yes, it will work. But it doesn't make sense. The ++ operator is not meant for this purpose, and even though it may seem convenient, this type of functionality should be avoided.
Conclusion
You should now have a solid understanding of how operators work in C++ and how they can be defined and overloaded to allow your classes to function more like primative data types. You should understand the different between unary and binary operators, and well as when to implement operators as member functions versus non-member functions.
I always welcome questions or feedback about this tutorial. Simply post a reply or PM me; I'm glad to help! Here is the complete source code for each file:
Fraction.cpp
#include <iostream>
#include "Fraction.h"
Fraction::Fraction( int num, int denom )
: numerator( num ), denominator( denom ) {
// Make sure the denominator isn't zero. This isn't the best
// way to go about it, but it work sufficiently for this example.
if( denominator == 0 ) {
numerator = 0;
denominator = 1;
}
// The numerator is the only value allowed to be negative
if( (denominator < 0 && numerator > 0) ) {
numerator = -numerator;
}
else if( (denominator < 0 && numerator < 0) ) {
numerator = -numerator;
denominator = -denominator;
}
}
void Fraction::print( std::ostream& os ) const {
// Print the numerator, a slash, and the denominator
os << numerator << '/' << denominator;
}
Fraction Fraction::operator+( const Fraction& other ) const {
// Find the least common multiple of the denominators
int lcm = other.denominator;
while( lcm % denominator != 0 ) {
lcm += other.denominator;
}
// Return a new fraction with the results
return Fraction(
(lcm / denominator) * numerator +
(lcm / other.denominator) * other.numerator, lcm );
}
Fraction Fraction::operator+( int num ) const {
// Return a new fraction with the results
return Fraction( numerator + denominator * num, denominator );
}
Fraction& Fraction::operator+=( const Fraction& other ) {
// Use the + operator we created
Fraction temp( *this );
temp = temp + other;
numerator = temp.numerator;
denominator = temp.denominator;
// Return a reference to ourself
return *this;
}
Fraction& Fraction::operator+=( int val ) {
// Use the + operator we created
Fraction temp( *this );
temp = temp + val;
numerator = temp.numerator;
denominator = temp.denominator;
// Return a reference to ourself
return *this;
}
std::ostream& operator<<( std::ostream& os, const Fraction& f ) {
// Use the member function to print to the output stream
f.print( os );
return os;
}
Fraction.h
#ifndef FRACTION_H
#define FRACTION_H
#include <iostream>
class Fraction {
private:
int numerator;
int denominator;
public:
/* Constructor */
Fraction( int num, int denom );
/* Prints this Fraction to an output stream */
void print( std::ostream& os ) const;
/* Overloaded operators for adding fractions */
Fraction operator+( const Fraction& other ) const;
Fraction operator+( int val ) const;
Fraction& operator+=( const Fraction& other );
Fraction& operator+=( int val );
};
/* Non-member operator for output stream insertion */
std::ostream& operator<<( std::ostream& os, const Fraction& f );
#endif
fractiontest.cpp
#include <iostream>
#include "Fraction.h"
/* To compile and run with g++:
g++ -o fractiontest fractiontest.cpp Fraction.cpp
./fractiontest
*/
using namespace std;
int main() {
Fraction fraction1( 2, 5 );
cout << "fraction1 is " << fraction1 << endl;
Fraction fraction2( 3, 4 );
cout << "fraction2 is " << fraction2 << endl;
cout << "Added together, we get " << (fraction1 + fraction2) << endl;
fraction1 += 2;
cout << "Adding 2 to fraction1, we get " << fraction1 << endl;
return 0;
}
README.txt
To compile the code using g++ on *nix, extract the files and run:
g++ -o fractiontest fractiontest.cpp Fraction.cpp
To run the test program, use the following command:
./fractiontest
Questions/comments can directed towards me at ozzu.com. Send a PM
to member "spork".
This page was published on It was last revised on