Preface
This tutorial contains three main chapters. The first chapter addresses simple straightforward issues, which are not obscured by high demand code or complex issues. Chapter two will build upon chapter one and addresses a few optimisations. The simplicity of operator overloading remains present, although a bit more concentration will be needed. I've chosen for a style where chapters increase in difficulty, because operator overloading is a "hot issue" occasionally I've noticed. It is sometimes considered difficult to implement, and therefore even some professional programmers don't like to use them. But it is not difficult at all. I think that various aspects of the C++ language have only meaning when placed into the right context and that various aspects of the C++ language needs to be combined with each other to demonstrate its potential. Common sense and a bit of study are here the keywords. This is where chapter three comes into play. Many (potential) coders who study the ways of operator overloading are less seasoned C++ coders. Young students are an example. A recap of certain aspects is in its place I think, so the lesser seasoned coder has the chance to review relevant topics to this tutorial without opening additional books all the time.
I will do that by demonstrating a
return by reference function and some basic character handling (chapter three). Furthermore, the student should read the tutorial that covers the "this" keyword, also to be found on this site. After the short return-by-reference fresh up, I will present you the first and very simple case to introduce the student to the concept. After familiarizing ourselves we will build our own minimal "string class" to demonstrate more complex topics, starting in chapter three. I've chosen for the ever-present String class so that the students receive some basic "string handling" skills as well. I've noticed that many problems are related to a lack of string/char handling. Along the way we will encounter problems to solve and demonstrate the correct implementation(s) of operator overloading.
However, this tutorial doesn't pretend (nor does it
intend) to be complete or highly accurate. I've chosen for an informal style. Detailed explanations of the mechanics and internal workings behind the topics in this tutorial can be found in many places on the internet though.
- Prerequisites (for all chapters):
- Basic knowledge and handling of classes and its instances (objects).
- Basic understanding and handling of pointers.
- Basic understanding and handling of functions. This includes passing parameters by value and by reference.
- Understanding the "friend" keyword. This tutorial will explain its purpose many times though.
- Understanding of the "this" keyword. A tutorial dedicated to this keyword can be found on this site as well.
- Understanding "initialising" in a constructor (vs. assignment in the body of a constructor).
- Understanding the basics of a copy constructor.
- Basic understanding and handling of returning by reference. Note that we will review this topic in short.
There will be many final code listings presented here which are complete and ready to be copied and run into your console project.
Only one *.cpp file is required.
*** Chapter One *** Returning by reference. A recap.
There is no way one can decently understand and implement operator overloading without understanding the basics of a function that returns a reference. Observe the little code below. It will be followed by a short remark only. Refer to your study book if this topic is completely new to you.
- Step(1)
int& SomeFunction enables SomeFunction to be used as a lefthandside value.
Code:
#include <iostream>
using namespace std;
int& SomeFunction(int& a, int& b, int& c)
{
// Step(2) Variable "c" can change the variable it points to.
return c;
}
int main()
{
int MyInt_1 =7, MyInt_2=8, MyInt_3=9;
/* Step(3)
* The assignment below basically means: MyInt3 = 230;
* Why? Because "c" is returned by SomeFunction, AND
* MyInt_3 is passed to "int& c", AND
* there is an ampersand (&) in "int& SomeFunction".
*/
//Somefunction is lhs because of "int&" as return type!
SomeFunction(MyInt_1, MyInt_2, MyInt_3)=230;
cout<<MyInt_1<<endl;
cout<<MyInt_2<<endl;
cout<<MyInt_3<<endl;
return 0;
} The output for
MyInt_3 is 230! Not 9.
So how do you change
MyInt_2? Why easy. Just return
b instead of
c in
SomeFunction(int& a, int& b, int& c).
That's it. Now we move to our first example of operator overloading.
Overloading the assignment operator (+).
Let's define a simple class to create complex numbers. We will also define a method to add up complex numbers. And we will implement a method to print the final results:
Code:
#include <iostream>
using namespace std;
class Complex
{
public:
Complex Add(const Complex& c) const
{
Complex temp;
temp.m_re = m_re + c.m_re; temp.m_im = m_im + c.m_im;
return temp;
}
void print() const
{
cout<<"("<<m_re<<" , "<<m_im<<")";
}
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result = A.Add(B);
Result.print();
cout<<endl;
return 0;
} Result=A.Add(B) doesn't look really neat. It would be nice if we could implement such a thing as
Result=A+B.
This is where operator overloading comes into play. There is an operator "+" defined for (lets say) integers, and floats and doubles. But now we need to define our own "+" for our Complex class. since "+" already exists, we need to overload it, to adapt it to our special needs.
A new keyword: operator+
Basically we want to replace
Result=A.Add(B) with
Result=A.operator+(B). If we can manage that, we can also write
Result=A+B. That's the bottom line of operator overloading. There is nothing much to it.
But there are some "rules of engagement". We will discuss them when it's the right time to do so. First lets implement our overloaded operator "+". We just need to substitute our original
Add() code with
operator+() code.
So here is how the transformation looks like:
Code:
ORIGINAL CODE SUBSTITUED CODE Complex Add(const Complex& c) Complex operator+(const Complex& rhs)
Complex temp; Complex temp;
temp.m_re=m_re+c.m_re; temp.m_im=m_im+rhs.m_im;
temp.m_im=m_im+c.m_im; temp.m_re=m_re+rhs.m_re;
return temp; return temp;
That's all there is up to it. But we
could make our code a bit more efficient, and certainly a bit neater. Lets do that later because we're eager to find out if this new code actually works. Here is the complete code listing. You can copy it, paste it and run it:
Code:
#include <iostream>
using namespace std;
class Complex
{
public:
Complex operator+(const Complex& rhs) const
{
Complex temp;
temp.m_im = m_im + rhs.m_im;
temp.m_re = m_re + rhs.m_re;
return temp;
}
Complex Add(const Complex& c) const
{
Complex temp;
temp.m_re=m_re+c.m_re;
temp.m_im=m_im+c.m_im;
return temp;
}
void print() const
{
cout<<"("<<m_re<<" , "<<m_im<<")";
}
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
//line below: basically we just execute Result=A.operator+(B);
Result=A+B;
Result.print();
cout<<endl;
Result = A.Add(B);
Result.print();
cout<<endl;
return 0;
} Efficiency and neatness
The line
Complex temp makes a call to the default constructor
Complex() : m_re(0), m_im(0) {}. The default constructor doesn't do anything relevant in our case. That is a typical disadvantage of temporary variables. It slows down the execution time, and can slow down the execution time quite a lot in more complex programs. We would like to avoid the creation of temporary variables as much as possible. That's easy enough in this case, since we have another constructor that does a good job on initialising the two member variables:
Complex(double r, double i): m_re(r), m_im(i) {}.
We will find out how to use this efficient constructor by using common sense. Here is an analysis on this interesting little puzzle:
1) Efficiency: bypassing the useless default constructor
The other constructor takes two arguments and is suitable for our needs. So use it like this:
Code:
Complex operator+(const Complex& rhs) const
{
Complex temp(m_re + rhs.m_re, m_im + rhs.m_im);
return temp;
} So far so good because the default constructor (which didn't help at all in the operator overloading code) is not used anymore. Instead, the other constructor is put to good work.
2) Neat code: exploiting the properties of a constructor
We don't even need a
user defined temporary variable. If one calls the constructor
directly, the constructor creates one implicitly. So the code below actually returns an implicit made temporary:
Code:
Complex operator+(const Complex& rhs ) const
{
return Complex(m_re + rhs.m_re, m_im + rhs.m_im);
} It doesn't get more efficient than this. Below is our final listing:
Code:
#include <iostream>
using namespace std;
class Complex
{
public:
Complex operator+(const Complex& rhs) const
{
return Complex(m_re + rhs.m_re, m_im + rhs.m_im);
}
Complex Add(const Complex& c) const
{
return Complex(m_re + c.m_re, m_im + c.m_im);
}
void print() const
{
cout<<"("<<m_re<<" , "<<m_im<<")";
}
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result=A+B;
Result.print();
cout<<endl;
Result = A.Add(B);
Result.print();
cout<<endl;
return 0;
} Copy it, paste it, run it, and experiment with it. From now on it will get slightly harder. Well, not really harder, as long as you follow the rules of engagement, which will be discussed when it is time to do so. And the overloaded "+" will be overloaded in a different way later (chapter two), to avoid problems that are not relevant now. If you understand what we did until now, then you're definably on your way. Also be aware that our "constructor-efficiency" is going to be used in our custom String class.
Our "constructor trick" is our first rule of engagement!
That's why I asked you to play a bit with our current code. To get familiar with this concept.
Adding an inserter operator: ostream operator<<(ostream* os)
We would like to write our output to screen like this:
cout<<Complex;
Therefore we need to overload
<<. This requires a special technique.
Our second rule of engagement:
- Inserters/extractors SHOULD be defined OUTSIDE the class.
- Inserters/extractors MUST accept two types, as a consequence or the previous rule.
- Inserters/extractors MUST be returned by reference.
Note: we need to use the friend keyword to access private members of a class when using a non-member function. In our case we need to access the private members m_re and m_im.
Here is the
skeleton for the inserter:
Code:
//Observe ostream&, and notice how it accepts two arguments: os and T.
ostream& operator<<(ostream& os, const T& )
{
os<<//add inserter code here
return os; //Always return inserter/extractor.
} For the Complex class, the code looks like this:
Code:
ostream& operator<<(ostream& os, const Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
} Do you notice the resemblance with
print() const?
And don't forget to mark this function as a friend of class Complex. This way
operator<< can access the private members:
Code:
private:
friend ostream& operator<<(ostream& os, const Complex& c);
So remove all the redundant code and show only the new code with this complete listing below:
Code:
#include <iostream>
using namespace std;
class Complex
{
private:
friend ostream& operator<<(ostream& os, const Complex& c);
public:
Complex operator+(const Complex& rhs) const
{
return Complex(m_re + rhs.m_re, m_im + rhs.m_im);
}
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
ostream& operator<<(ostream& os, const Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
}
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result=A+B;
cout<<Result<<endl;
return 0;
} That's it. And defining an extractor is just as easy. Here is the complete listing straight away:
Code:
#include <iostream>
using namespace std;
class Complex
{
private:
friend ostream& operator<<(ostream& os, Complex& c);
friend istream& operator>>(istream& is, Complex& c);
public:
Complex operator+(const Complex& rhs) const
{
return Complex(m_re + rhs.m_re, m_im + rhs.m_im);
}
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
ostream& operator<<(ostream& os, Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
}
istream& operator>>(istream& is, Complex& c)
{
is>>c.m_re>>c.m_im;
return is; //Always return inserter/extractor.
}
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result=A+B;
cout<<Result<<endl;
cout<<"Enter re and im, separated by a space. E.g.: 12 44.3"<<endl;
cin>>Result;
cout<<Result<<endl; //Observe how "<<" works on "Result" and "endl".
return 0;
} Basically, you could make the inserter and extractor operators as fancy as you could think of.
Can you implement an extractor that accepts a user-input like: (32,87). Or perhaps a user input like: 32+87i. You have to remove
the braces, the + and the "i", and convert 23 and 87 from char(s) to double(s). Searching for specific patterns in strings is called
parsing.
Did you notice the ampersand (&) right after ostream and istream?
This ampersand I mean: ostream
& operator<<...
We do this to allow chaining of the operator. Besides using it in inserters/extractors, you will be using this for every overloaded
operator that has to do with assignment. At least every operator related to
=. But not
==, since this is not an assignment operator, but a comparison operator. Chapter two demonstrates the mechanics behind returning by reference by overloading
operator+=. Chapter three will cover the
need for an overloading
operator=.
*** Chapter Two *** Two final approaches on operator+.
We defined the inserter and extractor operators outside the class. That's
almost mandatory for these operators. These operators could have been defined as a member of the class, but then using these operators wouldn't be as convenient as it is now. That's why this tutorial doesn't spend time on defining it outside the class. But defining
operator+ outside the class is not mandatory. Nevertheless you should get used to define it as a
free function. It will offer us benefits which will be clear in chapter two. For now, you will learn how to define it as a free function. You could stop reading and try it your self. Here is your hint: make it accept two arguments, instead of one like we did earlier. Below follows the solution so you might want to stop reading right here.
In function
int main() Our original
operator+ was called by object
A whilst object
B was passed as the argument to
operator+. Therefore
B is the right hand side value. In the case of
passing objects as arguments it doesn't matter whether
A or
B is the right hand side value or the calling object. Lets write a
main() where it shows that in the case of passing
objects as parameters, it doesn't matter which
object is the right hand side value. After that I'll demonstrate code in
main() where it shows why it is relevant to take left -and right hand side
entities into account.
First the code below where it doesn't matter since only
objects are used:
Code:
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result=A+B;
cout<<Result<<endl;
//It doesn't matter who is the right hand value.
Result=B+A;
cout<<Result<<endl;
return 0;
} And now the code where it does matter:
Code:
int main()
{
String A(" C++ "), Result;
Result=A+"unlimited";
cout<<Result<<endl;
//What means: "unlimited".operator+(A)
Result="unlimited"+A;
//Line below will never be executed.
cout<<Result<<endl;
return 0;
} By intuition
Result="unlimited"+A; should be perfectly valid. This is how we usually work with strings. But C++ demands that only
objects may call member functions.
"unlimited" is not an object but a
type! No wonder this code doesn't work (and causes errors). To enable this functionality
operator+ should not be a member function. And therefore we have to define it as a free function. We did this already with the overloaded inserter and extractor operators. Instead of one argument, it needs to accept two arguments since there will be no object calling. This is how the declaration would look like:
Complex operator+(const Complex& lhs, const Complex& rhs)
And below you see the final implementation:
Code:
Complex operator+(const Complex& lhs, const Complex& rhs)
{
return Complex(lhs.m_re + rhs.m_re, lhs.m_im + rhs.m_im);
} But
m_re and
m_im are
private members of the class so we need to tell the class that
operator+ can be considered as a friend:
Code:
class Complex
{
private:
friend Complex operator+(const Complex& lhs, const Complex& rhs);
...
}; Below follows the complete working code:
Code:
#include <iostream>
using namespace std;
class Complex
{
private:
friend Complex operator+(const Complex& lhs, const Complex& rhs);
friend ostream& operator<<(ostream& os, Complex& c) ;
friend istream& operator>>(istream& is, Complex& c) ;
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
Complex operator+(const Complex& lhs, const Complex& rhs)
{
return Complex(lhs.m_re + rhs.m_re, lhs.m_im + rhs.m_im);
}
ostream& operator<<(ostream& os, Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
}
istream& operator>>(istream& is, Complex& c)
{
is>>c.m_re>>c.m_im;
return is; //Always return inserter/extractor.
}
int main()
{
Complex A(12.347, 15), B(-2, 5.45), Result;
Result = A + B;
cout<<Result<<endl;
cout<<"Enter re and im, separated by a space. E.g.: 12 44.3"<<endl;
cin>>Result;
cout<<Result<<endl;
return 0;
} What we don't like is the fact that our non-member functions (free functions) have access to the private members of our class. One should keep the concept of privacy intact as much as possible. In the case of inserters and extractors there is not much choice. There is a choice but using
<< and >> becomes ugly as previously mentioned, so we don't even bother. But in the case of
operator+ we do. The less intrusive way is by calling a
copy constructor and an overloaded
+= operator.
About the copy constructor
Chapter three will cover the copy constructor in detail. A copy constructor will also be implemented in that chapter. What you need to know right now is that the copy constructor is automatically called when two objects of the same class are used to initialise each other. Here is a situation where a copy constructor is (automatically) called:
Code:
{
Complex A(B); //Calls copy ctor.
} If
class members use
dynamically allocated memory, then one needs to implement an own copy constructor (so the default copy constructor is overridden) to avoid undefined behaviour, as a result of double deletion of data. Our Complex class doesn't have such members (that are using dynamically allocated memory), so we don't have to define one in this case.
Our constructor
Complex(double r, double i): m_re(r), m_im(i) { } offers good quality initialisation for our purposes. Once again, chapter three will cover this aspect in detail.
So right now we need to implement
operator+= only.
Implementing operator+=
It would be nice if we could write something like this:
A+= B;
We can write this function as a member of our class. But there are specific rules of engagement to follow. Let's go back to a statement from chapter one:
Quote:
Did you notice the ampersand (&) right after ostream and istream? This ampersand I mean: ostream & operator<<...
We do this to allow chaining of the operator. Besides using it in inserters/extractors, you will be using this for every overloaded operator that has to do with assignment. At least every operator related to =. But not ==, since this is not an assignment operator, but a comparison operator. Chapter two demonstrates the mechanics behind this.
|
Let's find out about this statement. We will implement a simple approach first and as we move along, our demands for quality will grow. And exactly this is where the statement comes into play. The most straightforward approach that
seems to work well is this:
Code:
void Complex::operator+=(const Complex& c)
{ m_re = m_re + c.m_re; m_im = m_im + c.m_im; }
int main()
{
Complex A(12.347, 15);
A+=A;
cout<<A<<endl;
return 0;
} Observe that our code returns void.
Don't forget to declare this method
public in our class by the way. But what if we try to chain it like:
A+=A+=A; or like this:
A+=A+=B; We'd certainly expect chaining to work since the original
+= works this way as well, on built-in types. To understand what's going on you have to realize how
A+=A+=B; is substituted by the compiler:
A+=A+=B substitutes to
A.operator+=( A.operator+=(B) ).
So
A.operator+=(B) is executed first. But the method we implemented returns a
void. So the second execution
yields:
A.operator+=(void).
And that's not legal. There is another problem that offers a solution which solves our first problem straight away.
Realize that
(A+=A)+=B is NOT the same as
A+=A+=B (or
A+=(A+=B)).
(A+=A)+=B means
(A.operator+= A).operator+=(B).
So
(A.operator+= A) is executed first in this case. But the method we implemented returns a
void like I mentioned before. So the second execution yields:
(void).operator+=(B) which is illegal as well. We have to find a solution for this problem, and here is where
returning by reference and the
this keyword comes into play. Lets examine
(A+=A)+=B.
A+=A shouldn't return a void but the object
A, which will make the second execution yield
A.operator+=(B) instead of
void.operator+=(B). Here is our template for such cases:
Code:
@1 operator+=(const Complex& c)
{
m_re = m_re + c.m_re; m_im = m_im + c.m_im;
return @2;
} @1 and
@2 are our mystery guests.
We want to return the calling object and since
(A+=A) actually means
(A.operator(A) it this substitution that should return A. If
A is returned then the next call is the valid call
A.operator(B). And if this was the case:
OPQ+=XYZ then obviously
OPQ should have been returned.
Here is our first rule of engagement when it comes to overloading operators related to assignment: Return the calling object itself.
Return the
this keyword.
So here is our modified template:
Code:
@1 operator+=(const Complex& c)
{
m_re = m_re + c.m_re; m_im = m_im + c.m_im;
return *this;
} Important note: the keyword this is only the pointer to the calling object. It is not the calling object itself! *this is the actual object!
There is another mystery guest left:
@1.
One could think
Complex is the object that should be returned. So our method looks like this:
Code:
Complex operator+=(const Complex& c)
{
m_re = m_re + c.m_re; m_im = m_im + c.m_im;
return *this;
} But now the method returns by
value. Returning by value results in a temporary variable that disappears when the method goes out of scope. You can test what happens when we implement the code above. Here is a complete, working, and
almost correct listing. So you can test it your self:
Code:
#include <iostream>
using namespace std;
class Complex
{
private:
friend Complex operator+(const Complex& lhs, const Complex& rhs);
friend ostream& operator<<(ostream& os, Complex& c) ;
friend istream& operator>>(istream& is, Complex& c) ;
public:
Complex operator+=(const Complex& c);
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
Complex operator+(const Complex& lhs, const Complex& rhs)
{
return Complex(lhs.m_re + rhs.m_re, lhs.m_im + rhs.m_im);
}
Complex Complex::operator+=(const Complex& c)
{
m_re = m_re + c.m_re; m_im = m_im + c.m_im;
return *this;
}
ostream& operator<<(ostream& os, Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
}
istream& operator>>(istream& is, Complex& c)
{
is>>c.m_re>>c.m_im;
return is; //Always return inserter/extractor.
}
int main()
{
Complex A(12.347, 15), B(-2, 5.45);
A+=A+=B;
cout<<A<<endl;
return 0;
} Note that
A+=A+=B is the same as
A+=(A+=B).
So the output should be (20.694 , 40.9). The output yields this value indeed so everything seems to be working fine. But lets try
(A+=A)+=B. The correct output should be ( (12.347 + 12.347 - 2) , (15 + 15 + 5.45) ) = (22.694 , 35.45).
But
(A+=A)+=B results in (24.693 , 30)!
B seems to be ignored. Only
(A+=A) is being evaluated so it seems. Here is what actually happened:
- B has been evaluated but Complex operator+= returns by value. Therefore...
- ... (A+=A) is stored in a (internal) temporary variable. Therefore...
- ... B is added to this internal temporary variable. Therefore...
- ... when the method is done processing, the temporary variable goes out of scope. Therefore...
- ... the compiler destroys the temporary variable.
We could have simulated this situation (more or less) by defining
Complex operator+= this way:
Code:
Complex Complex::operator+=(const Complex& c)
{
Complex internal_temp;
internal_temp=c;
m_re = m_re + internal_temp.m_re;
m_im = m_im + internal_temp.m_im;
return *this;
} B is added to
internal_temp by executing
internal_temp.operator+=(B). Then
internal_temp goes out of scope.
If return by value isn't the answer then what is?
To avoid the automatic creation of internal temporaries by the compiler, one needs to return by reference.
Same thing for "normal" functions. So it goes for operator overloading as well. Thus the answer we were seeking is:
Complex& Complex::operator+=(const Complex& c). And the rest of the code is familiar:
Code:
Complex& Complex::operator+=(const Complex& c)
{
m_re = m_re + c.m_re;
m_im = m_im + c.m_im;
return *this;
} Below follows the
correct and complete code. You can copy it, paste it into your IDE and run it.
Code:
#include <iostream>
using namespace std;
class Complex
{
private:
friend Complex operator+(const Complex& lhs, const Complex& rhs);
friend ostream& operator<<(ostream& os, Complex& c) ;
friend istream& operator>>(istream& is, Complex& c) ;
public:
Complex& operator+=(const Complex& c);
public:
Complex(double r, double i): m_re(r), m_im(i) {}
Complex() : m_re(0), m_im(0) {}
private:
double m_re, m_im;
};
Complex operator+(const Complex& lhs, const Complex& rhs)
{
return Complex(lhs.m_re + rhs.m_re, lhs.m_im + rhs.m_im);
}
Complex& Complex::operator+=(const Complex& c)
{
m_re = m_re + c.m_re;
m_im = m_im + c.m_im;
return *this;
}
ostream& operator<<(ostream& os, Complex& c)
{
os<<"("<<c.m_re<<" , "<<c.m_im<<")";
return os; //Always return inserter/extractor.
}
istream& operator>>(istream& is, Complex& c)
{
is>>c.m_re>>c.m_im;
return is; //Always return inserter/extractor.
}
int main()
{
Complex A(12.347, 15), B(-2, 5.45);
(A+=A)+=B;
cout<<A<<endl;
return 0;
} Combining the copy ctor with an overloaded += to redefine operator+
Our previous code for the overloaded addition operator was this:
Code:
{
return Complex(lhs.m_re + rhs.m_re, lhs.m_im + rhs.m_im);
} This is possible because the constructor we are using will create an internal temporary variable. And it is this variable we are returning. First this internal temporary is initialized with rhs and then the new value of the internal temporary is added with lhs. Then the internal temporary is returned. It looks a bit like this:
Code:
{
//Phase 1:
Complex @temp;
@temp=lhs;
//Phase 2:
@temp=@temp+rhs;
return @temp;
} - Regarding
Phase 1:
We can easily shorten these two statements by using a copy constructor:
Code:
{
//Phase 1:
Complex @temp(lhs);
} - Regarding
Phase 2:
In this line the overloaded addition operator is being used as well as a
default assignment operator. This is exactly the same as a single += statement, so lets use it so the need for the
intrusive addition operator is eliminated. Note that the overloaded += operator is not intrusive (it is a member of the class):
Code:
Complex operator+(const Complex& lhs, const Complex& rhs)
{
Complex temp(lhs); //Calls DEFAULT copy ctor.
temp+=rhs; //Calls overloaded +=
return temp;
} You may replace your old code for the addition operator and implement a
main() to test it. Remember, you can remove now this line completely:
friend Complex operator+(const Complex& lhs, const Complex& rhs);
A simple main function to test is this:
Code:
int main()
{
Complex A(12.347, 15), B(-2, 5.45);
(A+=A)+=B;
cout<<A<<endl;
A=A+B;
cout<<A<<endl;
return 0;
} With this, we end the case where our Complex class is being used. Up to slightly more advanced issues.
Complex values can also be multiplied and divided. Should operator* and operator/ be a method or defined outside the class and tagged as a friend of the Complex class? Why? Do you need some understanding of mathematics with complex numbers to solve this question? Multiplying two complex numbers, what sort of result will we get? So if we need to define a multiplication operator, what would its return type be? *** Chapter Three ***
In this chapter we will create a custom String class and address some advanced issues. We will put the copy constructor to a good use for example, and show how it can be related to operator overloading. Actually one should think twice before developing a class when standard C++ already offers a similar class. Nevertheless I've chosen for this approach because a string class involves working with pointers. And in such cases more technique is needed than presented in our Complex number class. Perhaps not more technique, but definably a bit more thought in design. This part of the Operator Overloading tutorial continues to thrive upon our "basic instinct" on how an operator should behave. In the end we will achieve our demands by using common sense and combining several topics we've learned in
C++ primers. The simplicity behind the concept of operator overloading remains though. Only a bit more of code and know-how is needed to make things work because of the presence of pointers. Many things will come together in this chapter.
First a recap on basic (primitive) string handling. The recap is not a tutorial. Only a reminder of some of the basics.
Primitive character and pointer handling (a recap).
Do you remember that
char* szMyCharVariable="mytext"is exactly the same as
char szMyCharVariable[]="mytext"? One does this when the size of the assigned string is unknown in advance. Notice that these variables are
arrays, which hold c-style characters! Observe the code below:
Code:
#include <iostream>
using namespace std;
int main()
{
char* szHello = "Hello";
char szHowdy[] = "Everybody!";
return 0;
} Using the code above, we would like to show "Hello everybody!" to our console output, but first we would like to merge these two strings into a third string called "szGreetAll". This is what I encounter often when students ask for help:
Code:
#include <iostream>
using namespace std;
int main()
{
char* szHello = "Hello";
char szHowdy[] = "Everybody";
char* szGreetAll = szHello+szHowdy; //It doesn't work this way.
cout<<szGreetAll<<endl;
return 0;
} The code above doesn't show proper character handling.
First of all
szHello+szHowdy;. You are merely trying to add two pointers. Remember that these are
arrays. With the code above one is merely trying to add the starting addresses of two arrays. That is illegal to start with, besides the fact that that wasn't the intention.
And secondly one shouldn't use the assignment operator on char(s) at all.
Some try to merge strings by using a for-loop (or a while loop) and iterate through all the elements. But that is not efficient. There are special methods for assigning characters to characters:
strcpy is the most important one. And adding characters to form a bigger string offers another method:
strcat. I'm sure you've read here and there that strings are represented as arrays which hold characters. So to add szHello and szHowdy, you need a third character array that can hold both characters. Now you will find out how to calculate the size of
szGreetAll. Yes we can simply count the number of characters in this case, but often we don't have that luxury. So lets review the way to actually calculate the needed size by ourselves, properly allocate the memory and assign and concatenate strings. Also observe the proper usage of
delete[](square brackets, more on that later):
Code:
#include <iostream>
using namespace std;
int main()
{
char* szHello = "Hello";
char szHowdy[]="Everybody";
//strlen() returns the size in bytes, so we need to add +1 later.
int len = strlen(szHello);
int len2 = strlen(szHowdy);
len+=len2;
//So szGreetAll has the size of szHello and szHowdy PLUS 2.
//...for the extra space we insert between szHello and szHowdy!
char* szGreetAll=new char[len+2];
//Line below: the way to copy and merge strings.
strcpy(szGreetAll, szHello); //copy szHello to our buffer.
strcat(szGreetAll, " "); //Merge it with an extra space.
strcat(szGreetAll, szHowdy); //And then glue szHello to the end of the buffer.
cout<<szGreetAll<<endl;
delete[] szGreetAll;
return 0;
} - Remember to use:
- strlen()
- strcpy()
- strcat()
So far a reminder on character handling. We will use these techniques in our custom defined String class later.
Our String class. Important: Note that our String class is written with a capitol "S". While the original string class shipped with C++ is written with a lowercase "s"! The Vision Quote:
|
We would like to implement a minimal String class to demonstrate the key issues regarding operator overloading.
|
Note how we didn't demand for a complete and functional String class. We only want to implement entities wich are relevant to study more complex but important issues.
We would like to implement a String class so that:
- Note that A, B and C are all objects of class String.
- We could write String A("This is a string.").
- We could write A="This is still a string!".
- We could write String A(B).
- We could write A=B.
- Concatenate (merge) Strings like A=B+C.
- concatenate Strings like (A+=B)+=C.
- concatenate a String like A+="Another string here".
- Compare Strings like A==B.
- Compare a String with a c-style string like A=="Yup, a string I am".
- Compare a c-style string with a String like "Guess what, I am string" == A.
- Account for explicit conversion from String to const char*.
Implementing our String class.
We will start simple, and then we will explore what we have:
Code:
class String
{
public:
int length() const { return strlen(m_pChars); }
public:
~String ();
String(const char* = "");
String(const String&);
private:
char *m_pChars;
};
//Acts like a conversion operator: 'c-style char' to 'String'
String::String(const char *s)
{
// Need to copy the given string. if s is zero, make a null string.
if (s)
{
int len = strlen(s) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s);
}
else
{
m_pChars = new char[1];
*m_pChars = '\0';
}
}
String::String(const String& s)
{
m_pChars = new char[strlen(s.m_pChars) + 1];
strcpy(m_pChars, s.m_pChars);
}
String::~String() { delete[] m_pChars; } Our String class is nothing else but a data structure, implemented as a class. It has no functionality we could use.
At our best we could write:
Code:
int main()
{
String A="Hello";
String B(A);
//Or String B=A;
return 0;
} - Exploring our String class
- String::String(const char *s) is a constructor.
This one takes one argument. Therefore this constructor is automatically a conversion operator from a "c-style string" to our
String class. That is relevant because in code like A="Hello", "Hello" isn't a String object but a c-style string.
Also notice that our class uses dynamically allocated memory. This will force us to take two specific steps to implement our
class properly! - String B=A (in main() )uses the default "=" operator. That is not good.
The reason is that the members of our class use dynamically allocated memory. In these cases we should be weary of memory leaks. Here is a typical situation where memory leaks arise:
Code:
void SomeFunction(const String& s)
{
String B("B will be overwritten, but I will remain in memory!");
B=s;
//Here 'B' goes out of scope: destructor called. Data gone.
}
int main()
{
String A("Hello");
SomeFunction(A);
//Line below produces wrong output. A's pointer points to ???
cout<<A.length()<<endl;
return 0;
} First of all, in B=s the object B was never deleted (the destructor wasn't called), before we assigned the new value to it. So the portion of the memory - which holds "B will be overwritten, but I will remain in memory!" - is wasted for good as soon as we execute this line of code.
After executing that line, both A and B now contain a pointer to the same data. Once B goes out of scope, its destructor will be called to delete the data in m_pChars. So what data does A hold? That is undefined. So it could be anything. Therefore I should have overloaded operator=. I will do that later. - We have a copy constructor: String::String(const String& s).
This is the second requirement when a class dynamically allocates memory. The reason for its requirement is to prevent double deletion of dynamically allocated memory.
Lets change main() to demonstrate the situation.
Code:
int main()
{
String A("Hello");
String C=A;
return 0;
} This code will result in undefined behaviour: during the execution of the return statement the destructor for both C and A are called. They both point to the same data. So the first destruction goes as planned but the second destructor call will result in undefined behaviour - on my system I had a crash -, since that data just doesn't exist anymore (to put it very informal).
Single step this code while debugging the destructor to verify this. Use the first implementation of the String class. Then change main to the code above. See what happens when you disable (just out comment it) the copy-constructor. - We have a destructor: m_pChars dynamically allocates memory, therefore we need a destructor to free the allocated memory.
Notice how I used delete[]. Use the square brackets because we allocated memory by coding m_pChars = new char[...]. The square bracket is called the subscript operator. For every new ...[ ] we want a corresponding delete[]. For every new we must use delete without the subscript operator. Failing to comply with this rule results in undefined behaviour. So it might look your program still works if you don't comply (undefined, remember?), but if an error finally shows up in a more complex situation, you'll have a major problem.
It is not always true that for every new/new[] one MUST implement a delete/delete[]. In certain cases there is no deletion needed. The reason for this is beyond the scope of this tutorial, so that won't be covered here. But for the sake of correctness I've decided to mention it. So what did we achieve so far?
A lot indirectly. What I want to demonstrate is that programming
operator overloading is nothing else but using a healthy amount of common sense. When it comes to operator overloading, many are easily tempted to interpret at it as a special skill. But that is not the case. Starting from
part one we have reviewed many aspects of C++, which are covered in every school or book (primer). But now things come together. What is left for us in this part of the tutorial is to demonstrate common sense. Everything else (like operator overloading) comes naturally -it will be more or less a logic
follow up on what we see. Here we go.
Expanding our String class.
The need for an overloaded
operator= was already mentioned.
Stop reading! Close your eyes. Can you see the need for an overloaded assignment operator in the cases where classes dynamically allocate memory on members?
Do you remember how we overloaded
+= in our Vector class? We needed the
this keyword and we had to
return by reference.
Chapter two covered the reasons and mechanics: the need for chaining. Well, not "need", but by intuition, chaining is not a luxury at all.
- Here is a wrap up what we want to achieve right now:
- Allow chaining.
- Making sure the code doesn't cause memory leaks.
Basically the snippet below should work without errors after implementing the (required) assignment operator:
Code:
void SomeFunction(const String& s)
{
String B("Okidoky");
B=s;
}
int main()
{
String A("Hello");
SomeFunction(A);
String C=A;
return 0;
}
a) It is the 4th item of our needs as stated right after defining our one-line
vision.
b)
And it is one of the two mandatory requirements to avoid crashes by bad memory management.
Note: the other requirement (copy constructor) is already implemented.
First the design, then the complete working code for you to try out. The skeleton for
operator=:
Code:
String@1 String::operator=(const String& s)
{
if (this != &s)
{
...
}
return @2;
} The
mystery guests aren't so mystery anymore because we covered them in detail in
part two. So try to figure it
out for yourself. Below I will fill in the mystery guests without explanation.
But something new is presented.
A new rule of engagement. In case of overloading the assignment operator, we
must check for
self assignment.
Example 1:
A = A
Example 2:
A = B (
if B is a reference to
A!) A tricky one. This is called
aliasing.
There are two good reasons to cope with possible self-assignment:
- Efficiency
- Correctness (avoiding crashes and bugs again)
Efficiency
This is the lesser (and easier) part. Assignment basically means: change the
state of the object. Assignment to self doesn't change the state at all, so to prevent executing more code than is needed, we have to be weary for self assignment.
Correctness (part 1 of 2)
If "assignment" means that we are changing the state of an object (by changing its properties in our case), then it means we need to assign a new property to the object. We have to find out how this is done in the first place. So never mind
correctness for now, and let us first implement a seemingly functional
operator=. Once we have such an implementation, we can move back a little and find out about correctness in detail.
Moving on with expanding our String class: operator=
Assigning boils down to changing the value of
m_pChars in our specific case. But
m_pChars always contains data. The constructor
String::String(const char *s) will make sure of that: If
m_pChars does not contain user-defined data, then this variable holds an
empty string. Note that the memory for this data is dynamically allocated. Therefore, assigning this variable with new data just like that, will keep the old data still present in memory. It becomes inaccessible and so that part of memory is lost for good. So we delete the old data first correctly. Remember,
m_pChars is an
array of c-style characters so use the proper delete (in this incomplete code snippet):
Code:
String& String::operator=(const String& s)
{
{
delete[] m_pChars;
}
return *this;
} The mystery guests are finally filled in as well: a return by reference for chaining, and *this to return an object.
Question: why do we need to return the object again?
At the very beginning of this chapter we discussed how to deal with assigning, copying and setting up new string data for an array:
- Calculate the needed size.
- Allocate the new memory.
- Use strcpy() when copying or assigning data.
So here are the next three lines:
Code:
String& String::operator=(const String& s)
{
{
delete[] m_pChars;
int len = strlen(s.m_pChars) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s.m_pChars);
}
return *this;
} This is a fully functional implementation, but it is buggy. I'll present the complete code for you to copy so we can quickly set up a test in main():
Code:
#include <iostream>
using namespace std;
class String
{
public:
String& operator=(const String&);
public:
~String ();
String(const char* = "");
String(const String&);
private:
char *m_pChars;
};
//Acts like a conversion operator: 'c-style char' to 'String'
String::String(const char *s)
{
// need to copy the given string. If s is zero, make a null string!
if (s)
{
int len = strlen(s) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s);
}
else
{
m_pChars = new char[1];
*m_pChars = '\0';
}
}
String::String(const String& s)
{
m_pChars = new char[strlen(s.m_pChars) + 1];
strcpy(m_pChars, s.m_pChars);
}
String::~String() { delete[] m_pChars; }
String& String::operator=(const String& s)
{
{
delete[] m_pChars; // first delete old
int len = strlen(s.m_pChars) + 1; // +1 for '\0'
m_pChars = new char[len];
strcpy(m_pChars, s.m_pChars);
}
return *this;
} And here is the main() so we can do a test:
Code:
int main()
{
String A("Hello");
String B;
B=A;
return 0;
} Note that String B=A; is the same as String B(A);. So that is not the way to test for assignment!
The code is buggy and delivers an
undefined result. We should test it by debugging. Insert a
breakpoint at
String B=A and single step the code. You will see that the contents of B are cleared first, then the contents of
A are copied into
B. Nothing surprising here. It seems to work correctly.
Correctness (part 2 of 2)
But what happens if we do
A=A. Change your main()and run it first. No errors are given whilst compiling and running. But this code results in faulty execution definably.
Debug your code and discover the following:
- “A” is this and “A” is s.
Therefore they are their aliases. - this.m_pChars is deleted, therefore s.m_pChars is deleted!
Once again, they are their aliases. Aliasing comes in many shapes and flavours and the programmer always needs to be aware of that in any program. - So if the data is deleted, what are we copying to self? Rubbish. What memory size were we dynamically allocating? Undefined. Undefined, therefore it could have easily allocated 10 Kbytes instead of the needed 6 Bytes! There goes our memory.
So we need to check if object
*this is an alias of object
s. We do that by comparing the
addresses of the objects, since the
identity of an object is defined by its address. If the addresses are the same, the two identities are the same. And that's what we are interested in right now.
The address of
*this is
this and the address of
s is
&s.
So the code to test for self-assignment would look like:
if ( this == &s ). If that returns the value
true then
we should
return *this (the object itself) straight away so actual assignment doesn't take place.
Here is how I defined the code (I made it quite some time ago in preparation of this tutorial):
Code:
String& String::operator=(const String& s)
{
if (this != &s)
{
delete[] m_pChars; // first delete old
int len = strlen(s.m_pChars) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s.m_pChars);
}
return *this;
} The object is always returned and if the check for self-assignment fails then assignment can be done.
Once again, the final,
correct and complete listing below, and then we will implement an inserter because we would like to see some results on screen:
Code:
include <iostream>
using namespace std;
class String
{
public:
String& operator=(const String&);
public:
~String ();
String(const char* = "");
String(const String&);
private:
char *m_pChars;
};
//Acts like a conversion operator: 'c-style char' to 'String'
String::String(const char *s)
{
// need to copy the given string. If s is zero, make a null string!
if (s)
{
int len = strlen(s) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s);
}
else
{
m_pChars = new char[1];
*m_pChars = '\0';
}
}
String::String(const String& s)
{
m_pChars = new char[strlen(s.m_pChars) + 1];
strcpy(m_pChars, s.m_pChars);
}
String::~String() { delete[] m_pChars; }
String& String::operator=(const String& s)
{
if (this != &s)
{
delete[] m_pChars; // first delete old
int len = strlen(s.m_pChars) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s.m_pChars);
}
return *this;
}
int main()
{
String A("Hello");
A=A;
return 0;
} Moving on with expanding our String class: operator<<
We covered certain rules of engagement for overloading the inserter and extractor operator:
must be defined outside the class
and therefore use the
friend keyword to allow that function to access the private members of the class. Everything else is straightforward so here is the code without delay or explanation:
Code:
//MUST be defined outside class; So don't forget to make it a friend in the class that uses it.
ostream& operator<<(ostream& os, String& s)
{
os<<s.m_pChars;
return os; //Always return inserter/extractor.
} And mark it in the class as a buddy of that class:
Code:
private:
friend ostream& operator<<(ostream& os, String& s);
Check out
cout<<A<<endl; in main(). It works.
Moving on with expanding our String class: operator==
Suppose we have a String A and a String B. We would like to check if they contain the same string. It boils down to check whether their
m_pChars contains the same data.
m_pChars is a c-style string array. C++ offers a function to check if c-style string arrays contain the same data:
strcmp(). This function returns:
- < 0: string1 less than string2
- > 0: string1 greater than string2
- 0 : string1 identical to string2
Which makes our overloaded == quite simple. Notice how only common sense is used. Nothing fancy about overloading operators at all:
Code:
int operator==(const String& lhs, const String& rhs)
{
return (strcmp(lhs.m_pChars,rhs.m_pChars) == 0);
} And yes, make it a friend in the class, since this function is defined outside the class as well:
Code:
private:
friend int operator==(const String& lhs, const String& rhs);
Is there a special reason to define it outside the class (so it takes two arguments instead of one)? Does it make something work what otherwise won't work? If so, Why is that?
Hint: we covered the relevant aspects somewhere in this tutorial. And definably check out our vision again!
Below is just a simple main() for you to work with.
Code:
int main()
{
String A("Hello!");
A=A;
cout<<A<<endl;
String B="How are you?";
cout<<(B == A)<<endl;
return 0;
} Change B in "Hello!", then run the code again. The output should show "1" instead of "0". Moving on with expanding our String class: operator+=
We can easily make this a member of the class (just like operator=) as long as we return by reference and return the calling object
*this. The rest is a matter of using
strcpy,
strcat and naturally determining the new memory sizes:
Code:
String& String::operator+=(const String& s)
{
char* buffer = new char[strlen(m_pChars) + strlen(s.m_pChars) + 1];
strcpy(buffer, m_pChars);
strcat(buffer, s.m_pChars);
delete[] m_pChars;
m_pChars = buffer;
return *this;
} You may implement (add) this code in your class. There is no need to list the complete code again. But here is the updated main():
Code:
int main()
{
String A("Hello! ");
A=A;
cout<<A<<endl;
String B="How are you?";
cout<<("Hello!" == A)<<endl;
A+=A;
cout<<A<<endl;
return 0;
} This won’t work: "Oops!"+=A; Is that reasonable or even logical?
Why? Could you use a reference to compare our String class with something else? Would you personally change code so it will work if you could? "Hello!" == A works. Just as A == "Hello!". If you answered the question I asked before, then you know why both lines work, even though"Hello!" is not an object. Moving on with expanding our String class: operator+
Do you remember how I introduced a third way of implementing the + operator? The point was to make clear that we can use the copy constructor as an aid to avoid intrusion using the
friend keyword, if the copy constructor provides us proper initialisation.
Notice that we need an overloaded
+= but luckily we just defined one. We will define a straightforward overloaded
operator+ first and then check whether it can be optimised by using a constructor we currently have. And there are even more issues going on.
Code:
String String::operator+(const String& s)
{
char *buffer = new char[(strlen(m_pChars)) + strlen(s.m_pChars) + 1];
strcpy(buffer, m_pChars);
strcat(buffer, s.m_pChars);
return buffer;
} You should know by now what's going on here.
- Assign the proper amount of memory for buffer. The proper amount is the amount the calling object occupies added to the amount, which the argument object occupies. Plus one for the terminating character.
Have you figured out yet that strcpy() and strcat() overwrites the terminating character?
- Copy the contents of the calling object to the buffer.
- Merge the contents of the argument object with the buffer.
- Return the buffer.
We have a problem:
The following two lines will work:
-
A+B;
-
A+"Something for nothing.";
But this won't work:
"Something for nothing."+A;
We know by now that the compiler substitutes our code with this:
"Something for nothing.".operator+(A);
But
"Something for nothing." is not an object but an integral
type. So to make this happen, we need to define the
function outside the class as well:
Code:
String operator+(const String& lhs, const String& rhs)
{
char *buffer = new char[(strlen(lhs.m_pChars)) + strlen(rhs.m_pChars) + 1];
strcpy(buffer, lhs.m_pChars);
strcat(buffer, rhs.m_pChars);
return buffer;
} Make it a friend in the class. You know by now how to do that.
And all this will work now correctly:
Code:
int main()
{
String A("Hello! ");
A=A;
cout<<A<<endl;
String B="How are you?";
cout<<(B == A)<<endl;
A="Goodday sir. "+B;
cout<<A<<endl;
return 0;
} And now it is time to find out if the copy constructor can be used to avoid intrusion by the addition operator. Only common sense or logic will help us here. So lets analyze what we have right now and see how we can combine the current code into non-intrusive code. It boils down to removing one
friend keyword. The less friends the better!
Our operator+ char *buffer = new char[(strlen(lhs.m_pChars)) + strlen(rhs.m_pChars) + 1]
strcpy(buffer, lhs.m_pChars)
strcat(buffer, rhs.m_pChars)
return buffer The Copy Constructor m_pChars = new char[strlen(s.m_pChars) + 1]
strcpy(m_pChars, s.m_pChars) The operator+= char* buffer = new char[strlen(m_pChars) + strlen(s.m_pChars)+1]
strcpy(buffer, m_pChars)
strcat(buffer, s.m_pChars)
*skipped*
*skipped*
return *this
These similarities we can use alright:
Code:
String operator+(const String& lhs, const String& rhs)
{
String buffer(lhs); // calls copy ctor
buffer += rhs; // calls our overloaded +=
return buffer;
} Note how
operator+ delegates all the work to the existing overloaded
operator+= and to the copy constructor. So now the addition operator doesn't have to be a
friend anymore. So here is our very final and correct String class code listing:
Code:
#include <iostream>
using namespace std;
class String
{
private:
friend int operator==(const String& lhs, const String& rhs);
friend ostream& operator<<(ostream& os, String& s);
public:
String& operator=(const String&);
String& operator+=(const String&);
public:
~String ();
String(const char* = "");
String(const String&);
private:
char *m_pChars;
};
//Acts like a conversion operator: 'c-style char' to 'String'
String::String(const char *s)
{
// need to copy the given string. If s is zero, make a null string!
if (s)
{
int len = strlen(s) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s);
}
else
{
m_pChars = new char[1];
*m_pChars = '\0';
}
}
String::String(const String& s)
{
m_pChars = new char[strlen(s.m_pChars) + 1];
strcpy(m_pChars, s.m_pChars);
}
String::~String() { delete[] m_pChars; }
String& String::operator=(const String& s)
{
if (this != &s)
{
delete[] m_pChars; // first delete old
int len = strlen(s.m_pChars) + 1;
m_pChars = new char[len];
strcpy(m_pChars, s.m_pChars);
}
return *this;
}
//MUST be defined outside class; So don't forget to make it a friend in the class that uses it.
ostream& operator<<(ostream& os, String& s)
{
os<<s.m_pChars;
return os; //Always return inserter/extractor.
}
int operator==(const String& lhs, const String& rhs)
{
return (strcmp(lhs.m_pChars,rhs.m_pChars) == 0);
}
String& String::operator+=(const String& s)
{
char* buffer = new char[strlen(m_pChars) + strlen(s.m_pChars) + 1];
strcpy(buffer, m_pChars);
strcat(buffer, s.m_pChars);
delete[] m_pChars;
m_pChars = buffer;
return *this;
}
String operator+(const String& lhs, const String& rhs)
{
String buffer(lhs); // calls copy ctor
buffer += rhs; // calls our overloaded +=
return buffer;
}
int main()
{
String A("Hello! ");
A=A; //Test for self-assignment.
String B="How are you?";
cout<<("Hello! " == A)<<endl; //Hey... what's this? It works :)
A+=B;
cout<<A<<endl;
return 0;
} Tutorial ends here. A tutorial is never finished. So I will maintain this tutorial and revise it on a regular basis. But I need your help.
Mail me to valmont_programming@hotmail.com for your comments. Without your comments a tutorial is never good enough.