Register forum user name Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.

Due to spam on this forum, all posts now need moderator approval.

 Entire forum ➜ Programming ➜ General ➜ Making a C++ class - walkthrough

Making a C++ class - walkthrough

It is now over 60 days since the last post. This thread is closed.     Refresh page


Posted by Nick Gammon   Australia  (23,158 posts)  Bio   Forum Administrator
Date Sat 14 May 2011 06:36 AM (UTC)
Message
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.

    
    myString a;
    


  • 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:


myString e (e);


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:


a = b + c


... 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


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,158 posts)  Bio   Forum Administrator
Date Reply #1 on Sun 15 May 2011 12:38 AM (UTC)

Amended on Sun 15 May 2011 12:39 AM (UTC) by Nick Gammon

Message
For completeness I'll illustrate arithmetic. This was done with another experimental class (called num).

First the "arithmetic on self" operations. These change the current object, so they just modify the object's own member variable and return a reference to itself (so operations can be chained):


  // operations on the number which change it (eg. a += 5; )
  num & operator+= (const num & n) { n_ += n.n_; return *this; }
  num & operator-= (const num & n) { n_ -= n.n_; return *this; }
  num & operator/= (const num & n) { n_ /= n.n_; return *this; }
  num & operator*= (const num & n) { n_ *= n.n_; return *this; }


Next arithmetic that does not change the object (eg. a = b + c). This requires a temporary object to be created, and because this must exist after we return, we cannot return a reference to it:


  // operations on the number which do not change it (eg. a = b + 5; )
  num operator+ (const num & n) const { num temp = *this; temp += n; return temp; };
  num operator- (const num & n) const { num temp = *this; temp -= n; return temp; };
  num operator/ (const num & n) const { num temp = *this; temp /= n; return temp; };
  num operator* (const num & n) const { num temp = *this; temp *= n; return temp; };


And the same again where an int is supplied as an argument, rather than another object:


  num operator+ (const int n) const { num temp = *this; temp += n; return temp; };
  num operator- (const int n) const { num temp = *this; temp -= n; return temp; };
  num operator/ (const int n) const { num temp = *this; temp /= n; return temp; };
  num operator* (const int n) const { num temp = *this; temp *= n; return temp; };


Finally the "add/subtract one" operations. These can be prefix (++a) or postfix (a++).

Prefix is simpler because the add/subtract is done before returning the result, so we can change the object itself (and thus can return a reference to ourselves):


// prefix operations
  num & operator++ () { ++n_; return *this; }
  num & operator-- () { --n_; return *this; }


Postfix is harder because although we are adding/subtracting one, we must return the original value. We do this by making a temporary copy, changing ourselves, but returning the copy. Again note that we cannot return by reference or the copy would go out of scope:


// postfix operations (cannot return by reference)
  // we make a temporary object, change our current object, return the temporary one
  // if we returned a reference it would cease to exist, so we have to return a copy
  num operator++ (int) { num temp = *this; ++n_; return temp; }
  num operator-- (int) { num temp = *this; --n_; return temp; }


The (int) after the operator++ or operator-- tells the compiler that we are doing the postfix operator rather than the prefix operator.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


11,976 views.

It is now over 60 days since the last post. This thread is closed.     Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.