Following on from my investigations of C++ classes here:
http://www.gammon.com.au/forum/?id=4420
I have been doing more work to find out the optimal way of making a new C++ class. Questions spring to mind:
- Do I need a constructor?
- Do I need a default constructor?
- Do I need a copy constructor?
- Do I need an assignment operator?
- How should comparisons be implemented?
- What to do in the destructor?
- What should be declared const?
- When do things need to return references to themselves?
To try to answer these I have made a simple "string" class. I know there are lots of string classes around, and this one certainly won't be perfect from the point of view of features or efficiency. But string classes are complex enough that they can demonstrate various principles.
Underneath I will paste each method (constructors etc.) and explain why each one is done in the way it is.
Helpful corrections welcomed.
Function profiling
First, for function profiling, we will include this:
#include <iostream>
#include "ProfileTimer.h"
using namespace std; // make things less cluttered
ProfileTimer.h will contain this:
#define PROFILE_FUNCTION ProfileTimer t (__PRETTY_FUNCTION__)
// for timing stuff
class ProfileTimer
{
const char * sReason_;
static int indent_;
public:
ProfileTimer (const char * reason); // constructor
~ProfileTimer (); // destructor
}; // end of class ProfileTimer
And we make a ProfileTimer.cpp file:
#include <iostream>
#include "ProfileTimer.h"
int ProfileTimer::indent_ = 0;
// constructor
ProfileTimer::ProfileTimer (const char * reason) : sReason_ (reason)
{
sReason_ = reason;
for (int i = 0; i < indent_; i++)
std::cout << " ";
std::cout << "-> Entering: " << sReason_ << std::endl;
indent_++;
}
// destructor
ProfileTimer::~ProfileTimer ()
{
indent_--;
for (int i = 0; i < indent_; i++)
std::cout << " ";
std::cout << "<- Leaving : " << sReason_ << std::endl;
}
This lets us put PROFILE_FUNCTION at the start of every method, and have output on standard output when we enter and leave the method. Later on we can change the define to eliminate the debugging info, eg.
#define PROFILE_FUNCTION // no debugging now
This is the sort of debugging we get:
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
The indentation shows nested calls. Sometimes in the examples I will add in extra blank lines manually to make it clearer the different parts of code.
Class declaration
Let's start with the minimal declaration. We need a buffer to hold the string, and we will use an unsigned int as the string length, that way we know the length without having to do strlen all the time, and we can handle 8-bit clean strings.
My naming convention is that member variables have a trailing backslash.
// declaration
class myString {
// member variables
char * buf_;
unsigned int length_;
// private methods ...
public:
// public methods ...
}; // end class myString
Constructors
Constructors are needed to initialize data (eg. set length_ to zero and buf_ to NULL). Constructors do not return any value, not even void, as they are automatically called by the compiler.
Warning: Although constructors automatically call the constructors for objects inside this object (first), "simple" variables like int or long are not initialized to zero. You must explicitly initialize them.
We need a number of things:
- Default constructor. This is called when you don't supply any arguments, eg.
- Constructor which takes a const char *. This lets us supply a literal, eg.
myString a ("hello");
myString a = "hello";
- Copy constructor. This is called when we copy one myString to another, eg.
myString a;
myString b (a); // copy constructor
myString a = b; // also copy constructor
Common initialization
I wanted this string class to be able to be used intermixed with ordinary null-terminated strings, so it has an casting operator (described later). For that to work easily I need to store enough room for the string, plus one extra byte for the null terminator. Thus even a zero-length string needs storage (for the terminator). Since all of the constructors need to allocate memory, it saves effort if they can share common code. Now one constructor cannot (usefully) call another, so we make a private "init" method, whose job it is to allocate the memory appropriately.
// one constructor cannot call another so they share a private initialization function
// also called from operator=
void myString::init (const char * s, const unsigned int length)
{
PROFILE_FUNCTION;
// on an append or assignment a buffer might already exist so get rid of it
free(buf_);
if (s == NULL)
length_ = 0;
else
length_ = length;
// allocate memory for string and null-terminator
buf_ = (char *) malloc (length_ + 1);
// on failure, set length to zero and give up
if (buf_ == NULL)
{
length_ = 0; // object is now a "zombie"
return;
}
// copy string in
if (length_ > 0)
memcpy(buf_, s, length_);
buf_ [length_] = 0; // terminating null
} // end of myString::init
We use memcpy here rather than strcpy, to make sure that if a string has an 0x00 byte it, the whole string is copied.
These constructors do not throw exceptions (partly because I wanted to use them on the Arduino where you do not have exceptions). However if the memory was not available this would be a good place to throw one.
Default constructor
Below is the default constructor. It takes an argument but the argument has a default. It is declared thus:
myString (const char * s = "");
All it does is work out the length of the string, and call the init function:
// constructor from null-terminated string
// calls init with calculated length
myString::myString (const char * s) : buf_ (NULL)
{
PROFILE_FUNCTION;
init (s, s ? strlen (s) : 0);
} // end of myString::myString (constructor)
Note that it also sets buf_ to NULL (first) because init will attempt to free an existing buffer.
Example of use:
cout << endl << "------Create a -----" << endl;
myString a;
cout << endl << "------Create b -----" << endl;
myString b ("foo");
cout << endl << "------Create c -----" << endl;
myString c = "bar";
Debug output:
------Create a -----
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
------Create b -----
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
------Create c -----
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
Note how all three forms of creating a myString object use the default constructor.
Constructor with string and length
To allow for strings with imbedded 0x00 bytes, we also provide a constructor that takes a string and a length:
// constructor with string AND length
// calls init passing both
myString::myString(const char * s, const unsigned int length) : buf_ (NULL)
{
PROFILE_FUNCTION;
init (s, length);
} // end of myString::myString (constructor)
Example of use:
cout << endl << "------Create d -----" << endl;
myString d ("foobar", 6);
Debug output:
------Create d -----
-> Entering: myString::myString(const char*, unsigned int)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*, unsigned int)
Copy constructor
The "copy constructor" is called when the compiler want to copy an object from another instance of that type.
// copy constructor calls init from data from source string
myString::myString (const myString & rhs) : buf_ (NULL)
{
PROFILE_FUNCTION;
init (rhs.buf_, rhs.length_);
} // end of myString::myString (constructor)
Note that it is possible to do self-copy, like this:
For this reason we make sure that buf_ is NULL, and in init we do not copy from the supplied buffer if it is NULL.
The rhs (right-hand-side) argument is const because we do not change it, we just copy from it.
Example of use:
cout << endl << "------Create e -----" << endl;
myString e (b); // using b created earlier
Debug output:
------Create e -----
-> Entering: myString::myString(const myString&)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const myString&)
Destructor
Although you can have multiple constructors, you only ever have one destructor because the compiler calls that automatically.
Destructors typically free any used resources. Nowadays the free function is defined to do nothing if passed a NULL argument, hence no test for if the buffer was ever allocated.
// destructor
// free any used resources
myString::~myString ()
{
PROFILE_FUNCTION;
free(buf_);
}
Casting operators
You can provide "casting" operators, which let the compiler convert your object to another type.
// returns an immutable string that can be printed etc.
myString::operator const char * () const
{
PROFILE_FUNCTION;
// if allocation previously failed, just return an empty string
if (buf_ == NULL)
return "";
return buf_;
} // end of myString::operator const char *
The function is declared const because it does not modify the object.
Example of use:
cout << endl << "b is: " << b << endl << endl;
Debug output:
-> Entering: myString::operator const char*() const
<- Leaving : myString::operator const char*() const
b is: foo
Assignment operator
The operator= (assignment) is used by the compiler to copy one instance of an object to another.
This is not const, because the original contents of the object are replaced by the "right-hand-side". The rhs however is const because it does not change.
An important thing to check for is self-assignment. (eg. a = a;) If we do not test for this we are likely to get memory leaks, as we create a new copy when really it is the same object.
The operator= returns a reference to itself, so that assignments can be chained (eg. a = b = c; )
// operator =
// copies from RHS of assignment
// returns reference to itself so you can do: a = b = c;
// does not modify source string hence that is const
// since we are copying on top of existing string init gets rid of old one if present
myString & myString::operator= (const myString & rhs)
{
PROFILE_FUNCTION;
// gracefully handle self-assignment (eg. a = a;)
if (this == &rhs )
return *this;
init (rhs.buf_, rhs.length_);
return *this;
} // end of myString::myString & operator=
Example of use:
cout << endl << "------Assignment -----" << endl;
a = b; // assign it
Debug output:
------Assignment -----
-> Entering: const myString& myString::operator=(const myString&)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : const myString& myString::operator=(const myString&)
Self-reference test:
cout << endl << "------Self-reference -----" << endl;
a = a; // assign it
Debug output:
------Self-reference -----
-> Entering: myString& myString::operator=(const myString&)
<- Leaving : myString& myString::operator=(const myString&)
Note that we did not call init, so the test worked.
Arithmetic
String classes are not very good for demonstrating arithmetic, but we can at least add strings together (concatenate them). So first let's do an append to an existing string.
Since we are appending to ourselves the function is not const. However the string being appended is const as that doesn't change.
The operator= returns a reference to itself, so that assignments can be chained (eg. a = b += c; )
// operator +=
// appends to existing string, hence non-const function
myString & myString::operator+= (const myString & rhs)
{
PROFILE_FUNCTION;
// do nothing if nothing to append
// also do nothing if we don't have a buffer
if (rhs.length_ == 0 || buf_ == NULL)
return *this;
// try to grow buffer
char * temp = (char *) realloc(buf_, length_ + rhs.length_ + 1);
// if unable, keep existing buffer, don't change length
if (temp == NULL)
return *this;
// got memory (original part would be copied by realloc)
// copy new string
// this should work OK for appending to ourselves, because
// we have already changed buf but are still using the old length.
buf_ = temp;
memcpy(&buf_ [length_], rhs.buf_, rhs.length_);
// adjust string length, append null
length_ += rhs.length_;
buf_ [length_] = 0; // terminating null
return *this;
} // end of myString::operator+=
This attempts to grow its buffer (the realloc copies the original part) and then uses memcpy to append the rest. If we can't get memory we restore back to the original buffer.
Example of use:
cout << endl << "------Concatenate -----" << endl;
a += " my friends";
cout << endl << "a is: " << a << endl << endl;
Debug output:
------Concatenate -----
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
-> Entering: myString& myString::operator+=(const myString&)
<- Leaving : myString& myString::operator+=(const myString&)
-> Entering: myString::~myString()
<- Leaving : myString::~myString()
-> Entering: myString::operator const char*() const
<- Leaving : myString::operator const char*() const
a is: foo my friends
Notice the calls to myString constructor? The compiler must have created a temporary myString object, in order to concatenate it with the string. Then operator+= was called passing in this temporary object. The addition was done, and then the destructor called for the temporary string.
Example of use (concatenating two myString objects):
cout << endl << "------Concatenate -----" << endl;
a += b;
cout << endl << "a is: " << a << endl << endl;
Debug output:
------Concatenate -----
-> Entering: myString& myString::operator+=(const myString&)
<- Leaving : myString& myString::operator+=(const myString&)
-> Entering: myString::operator const char*() const
<- Leaving : myString::operator const char*() const
a is: foofoo
This time we didn't need the temporary object, so it was easier to do.
Addition
Now that we have an operator+= it is easy to add two strings together. In this case we need to make a temporary string. This is because if we do:
... we don't want to change either b or c. Thus we need a new object.
// operator +
// creates a new string, hence const function
// note that the temporary is copied on the return (it is not a reference)
// hence the compiler will do an assignment of the temporary object here
// when taking the return value.
myString myString::operator+ (const myString & rhs) const
{
PROFILE_FUNCTION;
myString temp = *this;
temp += rhs;
return temp;
} // end of myString::operator+
The method is declared const, because it does not change the left-hand-side, and the right-hand-side (rhs) is also const. The function does not return a reference to the temporary variable. This is because that will go out of scope as soon as we exit the function. It has to return a copy so that it is still valid once the function exits.
To save duplicating all the concatenation code, once we have the temporary object we just use operator+= to add the new string to it.
Example of use:
cout << endl << "------Create f -----" << endl;
myString f;
cout << endl << "------Add -----" << endl;
f = a + b;
cout << endl << "f is: " << f << endl << endl;
Debug output:
------Create f -----
-> Entering: myString::myString(const char*)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const char*)
------Add -----
-> Entering: myString myString::operator+(const myString&) const
-> Entering: myString::myString(const myString&)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString::myString(const myString&)
-> Entering: myString& myString::operator+=(const myString&)
<- Leaving : myString& myString::operator+=(const myString&)
<- Leaving : myString myString::operator+(const myString&) const
-> Entering: myString& myString::operator=(const myString&)
-> Entering: void myString::init(const char*, unsigned int)
<- Leaving : void myString::init(const char*, unsigned int)
<- Leaving : myString& myString::operator=(const myString&)
-> Entering: myString::~myString()
<- Leaving : myString::~myString()
-> Entering: myString::operator const char*() const
<- Leaving : myString::operator const char*() const
f is: foofoofoo
After the "------Add -----" line you can see the temporary object being created, then the operator+= is done to add to it, and then operator+ exits. Interestingly, the compiler seems to have been smart enough not to make a copy of that temporary object but rather to "hang onto it". So that temporary object is assigned to f, and then the temporary object is destroyed. Finally we print the results.
Information about the string
We have a couple of trivial functions that return data about the string. They are both const because they don't modify it.
// return size of string
// is const because it doesn't change the object
unsigned int myString::size( ) const
{
PROFILE_FUNCTION;
return length_;
} // end of myString::size
// return whether memory allocated OK
// is const because it doesn't change the object
bool myString::valid( ) const
{
PROFILE_FUNCTION;
return buf_ != NULL;
} // end of myString::valid
Comparisons
Finally let's do some string comparisons. First let's look at the declarations so we know what we are planning to implement:
// comparisons
bool operator< (const myString & rhs) const;
bool operator< (const char * rhs) const;
bool operator> (const myString & rhs) const;
bool operator> (const char * rhs) const;
bool operator<= (const myString & rhs) const;
bool operator<= (const char * rhs) const;
bool operator>= (const myString & rhs) const;
bool operator>= (const char * rhs) const;
bool operator!= (const myString & rhs) const;
bool operator!= (const char * rhs) const;
bool operator== (const myString & rhs) const;
bool operator== (const char * rhs) const;
Unfortunately we need two versions of each one, or the compiler complains about ambiguities. The first compares a myString to a myString, the second compares a myString to an ordinary const char *.
The interesting thing about comparisons is that we really only need to think hard about operator< (that is, compare-less-than). All the others can be derived from that.
For example:
- a > b == b < a
- a >= b == !(a < b)
- a <= b == !(b < a)
- a == b == !((a < b) || (b < a))
- a != b == (a < b) || (b < a)
Notice how we can express every other comparison relationship in terms of a < b (or b < a). In all cases the < operator is all we need.
Less-than
We'll do the hard one first: less-than.
We need a couple of tests in case either string did not have a buffer allocated (due to lack of memory).
Once past that, we then compare for the smaller lengths. If equal for the common length, we then work out which one is longer, as the shorter buffer must be less than the longer one.
For the const char * comparison, we just use a normal strcmp.
// compare less with another myString
bool myString::operator< (const myString & rhs) const
{
PROFILE_FUNCTION;
// if other buffer does not exist, we must be > it so return false
// and if both don't exist then we are equal
// (if buffer does not exist length will be zero)
if (rhs.length_ == 0)
return false;
// buffer does not exist, assume empty string, in which case it is less
// (if buffer does not exist length will be zero)
if (length_ == 0)
return true;
// compare for length of smaller buffer
int result = memcmp (buf_, rhs.buf_,
(length_ < rhs.length_) ? length_ : rhs.length_);
// if identical for matching lengths, smaller one is less
// note, if equal length, the result is not less (it is equal)
if (result == 0)
return (length_ < rhs.length_) ? true : false;
return result < 0;
} // end of myString::operator<
// compare less with a const char *
// needed to avoid compiler ambiguities
// also this does a straight strcmp, so it stops at the first null
bool myString::operator< (const char * rhs) const
{
PROFILE_FUNCTION;
// if other buffer does not exist, we must be > it so return false
// and if both don't exist then we are equal
if (rhs == NULL || rhs [0] == 0)
return false;
// buffer does not exist, assume empty string, in which case it is less
// (if buffer does not exist length will be zero)
if (length_ == 0)
return true;
return strcmp (buf_, rhs) < 0;
} // end of myString::operator<
Greater-than
That is simple, we just reverse the comparison order and call less-than.
// compare greater with another myString
// just reverse operand order and call compare<
bool myString::operator> (const myString & rhs) const
{
PROFILE_FUNCTION;
return rhs < *this;
} // end of myString::operator>
// compare greater with a const char *
// needed to avoid compiler ambiguities
// just reverse operand order and call compare<
bool myString::operator> (const char * rhs) const
{
PROFILE_FUNCTION;
return rhs < *this;
} // end of myString::operator>
Less-than or equal
This is the inverse of greater than. So we do the same thing greater-than did, and negate the result.
// compare less-or-equal with another myString
// inverse of greater than
bool myString::operator<= (const myString & rhs) const
{
PROFILE_FUNCTION;
return !(rhs < *this);
} // end of myString::operator<=
// compare less-or-equal with a const char *
// needed to avoid compiler ambiguities
// inverse of greater than
bool myString::operator<= (const char * rhs) const
{
PROFILE_FUNCTION;
return !(rhs < *this);
} // end of myString::operator<=
Greater-than or equal
This is simply the inverse of the result from less-than.
// compare greater-or-equal with another myString
// inverse of less than
bool myString::operator>= (const myString & rhs) const
{
PROFILE_FUNCTION;
return !(*this < rhs);
} // end of myString::operator>=
// compare greater-or-equal with a const char *
// needed to avoid compiler ambiguities
// inverse of less than
bool myString::operator>= (const char * rhs) const
{
PROFILE_FUNCTION;
return !(*this < rhs);
} // end of myString::operator>=
Not equal
If the strings are either less-than, or greater-than each other they cannot be equal.
A quick test for lengths saves having to do two compares if the strings are not equal length.
// compare not equal with another myString
// if less or greater, it can't be equal
bool myString::operator!= (const myString & rhs) const
{
PROFILE_FUNCTION;
// efficiency test - if different lengths, cannot be equal
if (length_ != rhs.length_)
return true;
return (*this < rhs) || (rhs < *this);
} // end of myString::operator!=
// compare not equal with a const char *
// needed to avoid compiler ambiguities
// if less or greater, it can't be equal
bool myString::operator!= (const char * rhs) const
{
PROFILE_FUNCTION;
return (*this < rhs) || (rhs < *this);
} // end of myString::operator!=
Equal
The strings are equal if they are neither less than, nor greater than, each other.
A quick test for lengths saves having to do two compares if the strings are not equal length.
// compare equal with another myString
// inverse of not equal
bool myString::operator== (const myString & rhs) const
{
PROFILE_FUNCTION;
// efficiency test - if different lengths, cannot be equal
if (length_ != rhs.length_)
return false;
return !((*this < rhs) || (rhs < *this));
} // end of myString::operator==
// compare equal with a const char *
// needed to avoid compiler ambiguities
// inverse of not equal
bool myString::operator== (const char * rhs) const
{
PROFILE_FUNCTION;
return !((*this < rhs) || (rhs < *this));
} // end of myString::operator==
Finished class declaration
For file myString.h:
// declaration
class myString {
// member variables
char * buf_;
unsigned int length_;
// called from both constructors
void init (const char * s, const unsigned int length);
public:
// constructors
// default (with no arguments) will be handled by default value below
myString (const char * s = "");
myString (const char * s, const unsigned int length);
// copy constructor
// copies member variables
myString (const myString & rhs);
~myString (); // destructor
// return a string
operator const char * () const;
// operator =
// copies from RHS of assignment
// (changes object)
myString & operator= (const myString & rhs);
// operations on the string which change it (eg. a += "foo"; )
myString & operator+= (const myString & s);
// operations on the string which DO NOT change it (eg. a = b + "foo"; )
myString operator+ (const myString & n) const;
// get size
unsigned int size () const;
// got memory OK?
bool valid () const;
// comparisons
bool operator< (const myString & rhs) const;
bool operator< (const char * rhs) const;
bool operator> (const myString & rhs) const;
bool operator> (const char * rhs) const;
bool operator<= (const myString & rhs) const;
bool operator<= (const char * rhs) const;
bool operator>= (const myString & rhs) const;
bool operator>= (const char * rhs) const;
bool operator!= (const myString & rhs) const;
bool operator!= (const char * rhs) const;
bool operator== (const myString & rhs) const;
bool operator== (const char * rhs) const;
}; // end class myString
Finished class implementation
For file myString.cpp:
#include <iostream>
#include "myString.h"
#include "ProfileTimer.h"
using namespace std; // make things less cluttered
// implementation
// ----------------------------- CONSTRUCTORS ------------------------------
// one constructor cannot call another so they share a private initialization function
// also called from operator=
void myString::init (const char * s, const unsigned int length)
{
PROFILE_FUNCTION;
// on an append or assignment a buffer might already exist so get rid of it
free(buf_);
if (s == NULL)
length_ = 0;
else
length_ = length;
// allocate memory for string and null-terminator
buf_ = (char *) malloc (length_ + 1);
// on failure, set length to zero and give up
if (buf_ == NULL)
{
length_ = 0; // object is now a "zombie"
return;
}
// copy string in
if (length_ > 0)
memcpy(buf_, s, length_);
buf_ [length_] = 0; // terminating null
} // end of myString::init
// constructor from null-terminated string
// calls init with calculated length
myString::myString (const char * s) : buf_ (NULL)
{
PROFILE_FUNCTION;
init (s, s ? strlen (s) : 0);
} // end of myString::myString (constructor)
// constructor with string AND length
// calls init passing both
myString::myString(const char * s, const unsigned int length) : buf_ (NULL)
{
PROFILE_FUNCTION;
init (s, length);
} // end of myString::myString (constructor)
// copy constructor calls init from data from source string
myString::myString (const myString & rhs) : buf_ (NULL)
{
PROFILE_FUNCTION;
if (this == &rhs )
cout << "copying self" << endl;
init (rhs.buf_, rhs.length_);
} // end of myString::myString (constructor)
// ----------------------------- DESTRUCTOR ------------------------------
// destructor
// free any used resources
myString::~myString ()
{
PROFILE_FUNCTION;
free(buf_);
}
// ----------------------------- CASTING OPERATORS ------------------------------
// returns an immutable string that can be printed etc.
myString::operator const char * () const
{
PROFILE_FUNCTION;
// if allocation previously failed, just return an empty string
if (buf_ == NULL)
return "";
return buf_;
} // end of myString::operator const char *
// ----------------------------- ACTION OPERATORS ------------------------------
// operator =
// copies from RHS of assignment
// returns reference to itself so you can do: a = b = c;
// does not modify source string hence that is const
// since we are copying on top of existing string init gets rid of old one if present
myString & myString::operator= (const myString & rhs)
{
PROFILE_FUNCTION;
// gracefully handle self-assignment (eg. a = a;)
if (this == &rhs )
return *this;
init (rhs.buf_, rhs.length_);
return *this;
} // end of myString::myString & operator=
// operator +=
// appends to existing string, hence non-const function
myString & myString::operator+= (const myString & rhs)
{
PROFILE_FUNCTION;
// do nothing if nothing to append
// also do nothing if we don't have a buffer
if (rhs.length_ == 0 || buf_ == NULL)
return *this;
// try to grow buffer
char * temp = (char *) realloc(buf_, length_ + rhs.length_ + 1);
// if unable, keep existing buffer, don't change length
if (temp == NULL)
return *this;
// got memory (original part would be copied by realloc)
// copy new string
// this should work OK for appending to ourselves, because
// we have already changed buf but are still using the old length.
buf_ = temp;
memcpy(&buf_ [length_], rhs.buf_, rhs.length_);
// adjust string length, append null
length_ += rhs.length_;
buf_ [length_] = 0; // terminating null
return *this;
} // end of myString::operator+=
// operator +
// creates a new string, hence const function
// note that the temporary is copied on the return (it is not a reference)
// hence the compiler will do an assignment of the temporary object here
// when taking the return value.
myString myString::operator+ (const myString & rhs) const
{
PROFILE_FUNCTION;
myString temp = *this;
temp += rhs;
return temp;
} // end of myString::operator+
// return size of string
// is const because it doesn't change the object
unsigned int myString::size( ) const
{
PROFILE_FUNCTION;
return length_;
} // end of myString::size
// return whether memory allocated OK
// is const because it doesn't change the object
bool myString::valid( ) const
{
PROFILE_FUNCTION;
return buf_ != NULL;
} // end of myString::valid
// ----------------------------- COMPARISONS ------------------------------
// compare less with another myString
bool myString::operator< (const myString & rhs) const
{
PROFILE_FUNCTION;
// if other buffer does not exist, we must be > it so return false
// and if both don't exist then we are equal
// (if buffer does not exist length will be zero)
if (rhs.length_ == 0)
return false;
// buffer does not exist, assume empty string, in which case it is less
// (if buffer does not exist length will be zero)
if (length_ == 0)
return true;
// compare for length of smaller buffer
int result = memcmp (buf_, rhs.buf_,
(length_ < rhs.length_) ? length_ : rhs.length_);
// if identical for matching lengths, smaller one is less
// note, if equal length, the result is not less (it is equal)
if (result == 0)
return (length_ < rhs.length_) ? true : false;
return result < 0;
} // end of myString::operator<
// compare less with a const char *
// needed to avoid compiler ambiguities
// also this does a straight strcmp, so it stops at the first null
bool myString::operator< (const char * rhs) const
{
PROFILE_FUNCTION;
// if other buffer does not exist, we must be > it so return false
// and if both don't exist then we are equal
if (rhs == NULL || rhs [0] == 0)
return false;
// buffer does not exist, assume empty string, in which case it is less
// (if buffer does not exist length will be zero)
if (length_ == 0)
return true;
return strcmp (buf_, rhs) < 0;
} // end of myString::operator<
// compare greater with another myString
// just reverse operand order and call compare<
bool myString::operator> (const myString & rhs) const
{
PROFILE_FUNCTION;
return rhs < *this;
} // end of myString::operator>
// compare greater with a const char *
// needed to avoid compiler ambiguities
// just reverse operand order and call compare<
bool myString::operator> (const char * rhs) const
{
PROFILE_FUNCTION;
return rhs < *this;
} // end of myString::operator>
// compare less-or-equal with another myString
// inverse of greater than
bool myString::operator<= (const myString & rhs) const
{
PROFILE_FUNCTION;
return !(rhs < *this);
} // end of myString::operator<=
// compare less-or-equal with a const char *
// needed to avoid compiler ambiguities
// inverse of greater than
bool myString::operator<= (const char * rhs) const
{
PROFILE_FUNCTION;
return !(rhs < *this);
} // end of myString::operator<=
// compare greater-or-equal with another myString
// inverse of less than
bool myString::operator>= (const myString & rhs) const
{
PROFILE_FUNCTION;
return !(*this < rhs);
} // end of myString::operator>=
// compare greater-or-equal with a const char *
// needed to avoid compiler ambiguities
// inverse of less than
bool myString::operator>= (const char * rhs) const
{
PROFILE_FUNCTION;
return !(*this < rhs);
} // end of myString::operator>=
// compare not equal with another myString
// if less or greater, it can't be equal
bool myString::operator!= (const myString & rhs) const
{
PROFILE_FUNCTION;
// efficiency test - if different lengths, cannot be equal
if (length_ != rhs.length_)
return true;
return (*this < rhs) || (rhs < *this);
} // end of myString::operator!=
// compare not equal with a const char *
// needed to avoid compiler ambiguities
// if less or greater, it can't be equal
bool myString::operator!= (const char * rhs) const
{
PROFILE_FUNCTION;
return (*this < rhs) || (rhs < *this);
} // end of myString::operator!=
// compare equal with another myString
// inverse of not equal
bool myString::operator== (const myString & rhs) const
{
PROFILE_FUNCTION;
// efficiency test - if different lengths, cannot be equal
if (length_ != rhs.length_)
return false;
return !((*this < rhs) || (rhs < *this));
} // end of myString::operator==
// compare equal with a const char *
// needed to avoid compiler ambiguities
// inverse of not equal
bool myString::operator== (const char * rhs) const
{
PROFILE_FUNCTION;
return !((*this < rhs) || (rhs < *this));
} // end of myString::operator==
Completed main program
#include <iostream>
#include "myString.h"
#include "ProfileTimer.h"
using namespace std; // make things less cluttered
// ----------------------------- MAIN PROGRAM ------------------------------
int main (int argc, char * const argv[]) {
cout << endl << "------Creating objects-----" << endl;
// null-terminated string test
cout << endl << "------Create a -----" << endl;
myString a ("hello world");
// copy constructor test
cout << endl << "------Create b -----" << endl;
myString b (a);
// assignment test (note! actually use copy constructor judging by debug)
cout << endl << "------Create c -----" << endl;
myString c = a;
// assignment test
cout << endl << "------Create d -----" << endl;
myString d = a = b;
// concatenation (+=)
cout << endl << "------Concatenate -----" << endl;
a += " my friends";
cout << endl << "a is: " << a << endl << endl;
cout << endl << "------Create e -----" << endl;
myString e;
cout << endl << "------Add -----" << endl;
e = a + b;
cout << endl << "e is: " << e << endl << endl;
cout << endl << "-----Displaying contents------" << endl;
cout << endl << "b is: " << b << endl << endl;
cout << endl << "c is: " << c << endl << endl;
cout << endl << "d is: " << d << endl << endl;
cout << endl << "-----Comparisons------" << endl;
cout << endl << "a is < 'foo' = " << (a < "foo") << endl << endl;
cout << endl << "a is < b = " << (a < b) << endl << endl;
cout << endl << "a is > 'foo' = " << (a > "foo") << endl << endl;
cout << endl << "a is > b = " << (a > b) << endl << endl;
cout << endl << "a is >= 'foo' = " << (a >= "foo") << endl << endl;
cout << endl << "a is >= b = " << (a >= b) << endl << endl;
cout << endl << "a is <= 'foo' = " << (a <= "foo") << endl << endl;
cout << endl << "a is <= b = " << (a <= b) << endl << endl;
cout << endl << "a is == 'foo' = " << (a == "foo") << endl << endl;
cout << endl << "a is == b = " << (a == b) << endl << endl;
cout << endl << "a is != 'foo' = " << (a != "foo") << endl << endl;
cout << endl << "a is != b = " << (a != b) << endl << endl;
cout << endl << "'foo' is < a = " << ("foo" < a) << endl << endl;
cout << endl << "'foo' is > a = " << ("foo" > a) << endl << endl;
cout << endl << "'foo' is <= a = " << ("foo" <= a) << endl << endl;
cout << endl << "'foo' is >= a = " << ("foo" >= a) << endl << endl;
cout << endl << "'foo' is == a = " << ("foo" == a) << endl << endl;
cout << endl << "'foo' is != a = " << ("foo" != a) << endl << endl;
cout << endl << "-----Exiting program------" << endl;
return 0;
} // end of main
|