String Class Example
Spring 2002
Table of Contents
1. Source Code
Instructor Comments
Section 1. Source Code

This example is somewhat different from the approach we took in class. In particular, in this implementation I have made the decision not to allocate space if the String object is empty. This tends to make more work for us because we now have to check to size of the String before we deallocate memory. Also, I decided to use a typedef statement to define szType to hold the type of object used to hold the length of the String. This has an advantage in that I can modify the type quite easily (for example, if we decided that we needed to deal with really long strings, we could make szType by a long unsigned int instead of an unsigned int).

String.h
   1// String.h
   2// Author: t a y l o r@msoe.edu and section 6 of the
   3//         MSOE Spring 2001 CS183 course
   4// Date: 4-10-2001
   5// Purpose: Defines a String class which demostrates a simplified
   6//          version of the C++ standard string class.
   7// Modified: t a y l o r@msoe.edu, 4-8-02 => const operator[] now returns const ref
   8//                                   => now use szType typedef for object size
   9
  10#ifndef STRING_H
  11#define STRING_H

The above two lines (along with the #endif at the end of the header file) ensure that this class is not inadvertently declared twice.

This has the potential for happening if String.h were included twice in one source file. While this may seem unlikely, it really isn't. For example, suppose you had Date class that stored the day of the week as a String. You would need to include String.h in the Date.h header file. In the source file containing main, you would probably include String.h and Date.h since you would want to create String and Date objects inside main. Doing so results in String.h being included twice (once explicitly and another time by way of the Date.h file which included String.h in it).

  12
  13#include <iostream>
  14using std::istream;
  15using std::ostream;
  16
  17class String {
  18public:
  19  // Define szType to be the type of the object containing the size of the
  20  //  String
  21  typedef unsigned int szType;

This statement allows us to use szType in place of unsigned int. You should notice that this has been declared within the String class which means that if we want to use it outside of the String class member functions, we need to use its full name: String::szType.

  22
  23  // Default constructor
  24  String();
  25
  26  // Copy constructor
  27  String(const String& rhs);
  28
  29  // One argument constructor that takes a character array as the initial
  30  //  value for the string
  31  String(char str[]);
  32
  33  // Destructor
  34  ~String();
  35
  36  // Assignment operator
  37  String& operator=(const String& rhs);

Note that we return by reference instead of by value. We do this because it is more efficient and because we know that the reference will be valid even after the function call is complete. We know this because the only way we can call this function is to have a valid String object. It is a reference to this object that gets returned.

  38
  39  // Accessor returning the number of elements in the String
  40  szType size() const;

Here we use szType instead of unsigned int.

  41
  42  // Concatenate operator: adds the String passed, str, to the
  43  //  current String
  44  // e.g., String name("Jon"); String last("Dough");
  45  //       name += last; // "JonDough"
  46  String& operator+=(String rhs);
  47
  48  // Accessor that returns the (i+1)st element of the String
  49  const char& operator[](szType i) const;

We have two versions of the subscript operator. This function returns an rvalue, i.e., a value that can only appear on the right side of an assignment operator. This function gets called whenever we have a const String object. It is necessary because the non-const version cannot be called on a const String object. This actually happens quite frequently... consider the following function:

void printMe(const String& word)
{
  for(String::szType i=0; i<word.size(); ++i)
    cout << word[i];
  }
}

Because word is passed in by const reference, word can only call const member functions. Without this version of the subscript operator, the printMe function would not compile. It should also be noted that the size member function needs to be declared const for the same reason.

One final thing to notice is the way we had to specify String::szType. We have to do it this way because szType was declared within the String class and the printMe function is not part of the class.

  50
  51  // Mutator that returns a reference to the (i+1)st element of the String
  52  //  This version of the operator may be used as an lvalue
  53  char& operator[](szType i);

This version actually allows us to change the values of characters within the String object. This function returns a lvalue, i.e., a value that can appear on either the left or right side of an assignment operator.

  54
  55  // Inserts a String into the input stream, os
  56  void insertion(ostream& os) const;

It is not anticipated that this function would ever be called directly. However, we need it be declared public because it will be called by the auxillary function operator<<.

An alternate approach would be to declare the operator<< function to be a friend of the String class. This would be done by replacing the insertion member function prototype with the following:

      friend ostream& operator<<(ostream& os, const String& str);

This would allow operator<< to have access to the private members in the String class. We prefer writing the insertion function since we can declare the function a const function.

  57
  58private:
  59  szType numEl;
  60  char* letters;
  61};
  62
  63// Auxilliary insertion operator which allows String objects to be
  64//  inserted into an output stream
  65ostream& operator<<(ostream& os, const String& str);

This operator returns a reference to an ostream so that it is possible to multiple stream instertions on the same line. For example, if os is an ostream and a, b, and c are String objects,

  os << a << b << c;

will send a, b, and c to the output stream, os.

  66#endif
String.cpp
   1// String.cpp
   2// Author: t a y l o r@msoe.edu and section 6 of the
   3//         MSOE Spring 2001 CS183 course
   4// Date: 4-10-2001
   5// Purpose: Defines a String class which demostrates a simplified
   6//          version of the C++ standard string class.
   7// Modified: t a y l o r@msoe.edu, 4-8-02 => const operator[] now returns const ref
   8//                                   => now use szType typedef for object size
   9
  10#include "String.h"
  11#include <cassert>
  12
  13// t a y l o r@msoe.edu, 4-10-01
  14String::String() : numEl(0), letters(0)
  15{
  16  // Nothing else to do
  17}

The above function makes use of an initializer list. It is the preferred method for assigning values to data members as it gives the data member a value when it is created rather than creating it with some invalid value and then going back and giving it the appropriate value.

  18
  19// t a y l o r@msoe.edu, 4-10-01
  20// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
  21String::String(const String& rhs) : numEl(rhs.numEl), letters(0)
  22{
  23  if(rhs.numEl) { // rhs is not an empty String
  24    letters = new char[numEl];

This statement allocates space for a character array with numEl elements and causes letters to point to it. Note that if numEl = 0, then no memory is allocated.

  25  }
  26  for(szType i=0; i<numEl; ++i) {
  27    letters[i] = rhs.letters[i];
  28  }
  29}
  30
  31// t a y l o r@msoe.edu, 4-10-01
  32// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
  33String::String(char str[]) : numEl(0), letters(0)
  34{
  35  while(str[numEl]!='\0') {
  36    ++numEl;
  37  }

This function is slightly more complicated that the copy constructor because here we don't know how many characters are in str. We need to loop through the character array until we find the null character, '\0'.

  38  if(numEl) { // not an empty String
  39    letters = new char[numEl];
  40  }
  41  for(szType i=0; i<numEl; ++i) {
  42    letters[i] = str[i];
  43  }
  44}
  45
  46// t a y l o r@msoe.edu, 4-10-01
  47String::~String()
  48{
  49  if(numEl) {
  50    delete [] letters;
  51  }
  52}
  53
  54// t a y l o r@msoe.edu, 4-10-01
  55String& String::operator=(const String& rhs)
  56{
  57  // Check for self-assignment
  58  if(this!=&rhs) {

The if statement above makes sure that we don't assign an object to itself. this is a pointer to the calling object, i.e., it is the address in memory where the calling object resides. rhs is the object whose value we want to copy and &rhs the address in memory where it is stored. If this and &rhs are the same, then they are both stored in the same place in memory and must be the same object. In this case, not checking for self-assignment is inefficient. Suppose that A is a String object and we do the following: A = A;. This function is called with the calling object being A and the object passed in, rhs, also being A. Therefore, numEl and rhs.numEl are the same and letters and rhs.letters both point to the same character array. Without the self assignment check, we would be forced to copy all of the letters into the same location, clearly inefficient.

It is important to check for self assignment whenever implementing the assignment operator for a class that has dynamically allocated memory. In this case are implementation would be less efficient; however, there are situations where it can be down right disastrous. For example, we could remove the if statement immediately following this comment and still have a valid implementation of the assignment operator (it's just a little less efficient when assigning two Strings of the same length equal to each other); however, now if we were to remove the self assignment check as well, we would have major problems. Can you see why?

Removing the two if statements would result in us deallocating the memory used to store the calling object. We would then allocate new memory to store the character array from rhs, but when we tried to copy the characters from rhs we would discover that they are no longer there (we just deleted them).

  59    if(numEl!=rhs.numEl) {
  60      numEl = rhs.numEl;
  61      delete [] letters;
  62      if(rhs.numEl) {
  63        letters = new char[numEl];
  64      }
  65    }
  66    for(szType i=0; i<numEl; ++i) {
  67      letters[i] = rhs.letters[i];
  68    }
  69  }
  70  return *this;
  71}
  72
  73// t a y l o r@msoe.edu, 4-10-01
  74// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
  75String::szType String::size() const
  76{
  77  return numEl;
  78}
  79
  80// t a y l o r@msoe.edu, 4-10-01
  81// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
  82String& String::operator+=(String rhs)
  83{
  84  if(numEl+rhs.numEl) {
  85    char* ptr = new char[numEl+rhs.numEl];
  86    szType i=0;
  87    for(; i<numEl; ++i) {
  88      ptr[i] = letters[i];
  89    }
  90    for(i=0; i<rhs.numEl; ++i) {
  91      ptr[numEl+i] = rhs.letters[i];
  92    }
  93    delete [] letters;
  94    letters = ptr;
  95    numEl += rhs.numEl;
  96  }
  97  return *this;
  98}

Here we add the contents of rhs to the calling object. This requires us to allocate enough memory to store both Strings, copy the contents of the calling String to the newly allocated memory, then copying the contents of rhs, deallocating the memory that stored the original calling object, and make the letters data member point to the newly allocated memory.

  99
 100// t a y l o r@msoe.edu, 4-10-01
 101// Modified: t a y l o r@msoe.edu, 4-8-02 => now returns const ref
 102//                                   => now uses szType typedef
 103const char& String::operator[](String::szType i) const
 104{
 105  assert(i<numEl);  // MSVC++ does not do this check
 106  return letters[i];
 107}
 108
 109// t a y l o r@msoe.edu, 4-10-01
 110// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
 111char& String::operator[](String::szType i)
 112{
 113  assert(i<numEl);  // MSVC++ does not do this check
 114  return letters[i];
 115}
 116
 117// t a y l o r@msoe.edu, 4-10-01
 118// Modified: t a y l o r@msoe.edu, 4-8-02 => now uses szType typedef
 119void String::insertion(ostream& os) const
 120{
 121  for(szType i=0; i<numEl; ++i) {
 122    os << letters[i];
 123  }
 124}
 125
 126// t a y l o r@msoe.edu, 4-10-01
 127ostream& operator<<(ostream& os, const String& str)
 128{
 129  str.insertion(os);
 130  return os;
 131}
Copyright   2001 Dr. Christopher C. Taylor t a y l o r@m s o e.e d u Last updated: Tue Apr 9 18:44:32 2002