Chapter Overview

The principal goal of this chapter is the development of a string class that corrects the misdemeanors (and even a felony or two) committed by primitive C strings. We will begin with a brief review/critique of selected topics on C strings, proceed to a detailed discussion of the notion of proper type, and then get down to the business of string objects.

C Strings

C strings suffer from all of the problems of primitive arrays plus a few extra problems unique to strings. All of these problems are of the nature of traps that victimize imperfect humans. These problems have two roots:

  1. Memory management
  2. Stop signs (null-termination)

Now, please understand, imperfect people can write perfect programs. The problem is that imperfect people do not write perfect programs all the time. Perfection in programming requires concentration; careful analysis, design, and implementation; and high levels of effort and concentration. All of us can fulfill these requirements some of the time. There is no person who can do it all of the time. C strings open doors of bad opportunity that wait for us when we are not performing perfectly.

Conceptually, a C string is a null-terminated array of characters. As a type, though, a C string is nothing more than a pointer to char. The mismatch between concept and type means there are assumptions that are not visible to or checkable by the preprocessor, compiler, or runtime system. This slide shows two hidden assumptions, together with some code that illustrates correct ways to instantiate the assumptions.

The effect of the code is to create two strings and output them to screen. Note how easy it would be for the programmer to create problems by (1) forgetting to allocate memory for str2 or (2) forgetting to null-terminate str2.

C String Functions

Here is a typical implementation of the output operator for C strings, an overload of operator <<():

ostream& operator << (ostream& os, char* str)
{
  int i;
  while (str[i] != '\0')
    os.put(str[i++]);
  return os;
}

For those of us who find such complex expressions cryptic, here is an alternative:

ostream& operator << (ostream& os, char* str)
{
  while (*str != '\0')
  {
    os.put(*str);
    ++str;
  }
  return os;
}

The null-termination assumption is used to terminate the loop. Null-termination refers to the conceptual constraint that strings have "stop signs" (the null character '\0') to indicate when the data in memory is no longer part of the string. Without a stop sign, the loop in this function would run indefinitely, or at least until incrementation resulted in an invalid memory address. Long before this occurred, the loop would be running through memory that wasn't ever part of the string, and perhaps not even owned by the running program. The result would be a screen full of random junk, at best.

Implicit in the process that copied str1 to str2 in the previous slide was an assumption that data was copying into memory that was reserved for the purpose. If memory had not been allocated correctly, the copy routine would not have any way to know, so it would copy data into memory that likely is used for some other important purpose, thus overwriting that memory and sabotaging that part of the program.

Almost anything. Crashing programs, random or unpredictable results, even system crashes can and do happen due to mistakes with strings. The secondary consequences can be huge: failed critical systems, incorrectly functioning medical devices, ...

C String Functions (cont.)

Here is another example, a possible implementation of strcpy():

void strcpy (char* str2, const char* str1)
{
  while (*str1 != '\0')
  {
    *str2 = *str1;
    ++str2;
    ++str1;
  }
  *str2 = '\0';
}

Note that:

Most of the other string functions prototyped in string.h suffer from similar defects. In addition to trapping many an unwary programmer, these defects have been exploited by unethical hackers for destructive and criminal purposes, including such notorious incidents as the Internet Worm and unauthorized takeover of department of defense systems. The legacy of C is a mixed one.

Concept of Proper Type

The concept of proper type is central to the good software engineering practice of encapsulation -- solving hard problems carefully, and then shielding clients from the difficulties using an interface. A proper type should have the following attributes.

Any data on which objects depend for correct function should be kept both valid and protected. For example, if there is a datum "size", whatever care is neccessary to maintain correct values for "size" should be exercised. The rule should be that "size" is guaranteed to have correct value when a method returns, assuming that "size" was correct when the method was called. (There may be times during the call of a method that "size" has a temporarily incorrect value.) Ensuring that every method implementation behaves correctly with respect to "size" ensures that "size" has a valid value. To guarantee that "size" has a correct value, we must also prevent tampering with its value by clients.

Behavior should be described, documented, and made available to clients through some kind of interface. Behavior implementation should be hidden or shielded from client access.

A proper type should behave like a native type, in that variables of that type can be declared (globally or locally), used, and allowed to go out of existence with no more work on the client's side. This should work for both static and dynamic variables.

Note that the notion of proper type addresses all of the weaknesses of C strings.

Proper Type as C++ Class

The student of C++ recognizes immediately that the C++ class provides a mechanism to create proper types. Note, it is not sufficient to say that a proper type is a class. In C++, we could say that a proper type is either a native type or a class that has been carefully designed and implemented to satisfy the following.

Data variables should be placed in non-public (protected or private) areas in the class definition. Access to this data should be carefully controlled through public methods that allow clients to know the value of data (via methods declared as const) and, where appropriate only, to change the value of data. When changes are allowed, the access methods should make sure that the client cannot invalidate the data.

Class behavior should be made available to clients through documented (or self-documenting) public methods. Implementation of methods should be out of the client's view and control. Method implementation process (plan, design, and code) should ensure that behavior is implemented as documented.

The class must have appropriately implemented constructors and destructor so that objects may go in and out of scope with no adverse effects on a client program. Copy constructor and assignment operator should follow the same criterion, or they should be disabled so that a client cannot use a copy constructor or assignment operator that doesn't work properly.

Class String Public Interface

We will start out with a wish list. (You may wonder "Who's wish is this, anyway?" It's mine, of course, but I hope you will play along and try to see the reasons one might desire these properties of String objects.) The first item should not be a surprise.

The library we will build, in general, will reside in the namespace fsu. See the appendix on namespaces in C++ for a brief discussion and [Dietel] for more depth on namespaces.

Proper type is clearly a desirable, even essential, trait. It will not be trivial to deliver, either. But the clients, including a lot of your own programs, will be very happy.

Thus, we will be able to compare two String objects for equality, non-equality, less than, greater than, and so on. There are six of these operators, none of which work for C strings.

The reason the comparison operators don't work for C strings is not that they are undefined. If you have a program fragment such as

it will compile and run. The problem is that it will not do what you want. The comparison is between two pointers, which are addresses, so the effect will be to activate the body of the if statement when the address of the first string is less than the address of the second string. A similar problem exists with the I/O operators. They are overloaded for the type char* to the extent possible, but the extent possible is limited by the inherent weaknesses of the type. For example, the line

will output the C string in memory beginning with the address str1. But, if the creator of that string forgot to null-terminate, that could be a very long string! Even more troubling, the input operation

will place the next white-space-delimited string into memory, starting at address str2, regardless of whether that memory has actually been allocated. This operation could, for example, write user input all over the code itself, and crash the program (perhaps at some time in the future when it would have no apparent relationship to this particular action).

To correct these problems, we wish for

Then code like the following would work just as a client would expect:

Exercise: Replace the declarations in the first line of code above with

and complete a program fragment that accomplishes the same task using C strings, include correct and safe memory management.

Continuing with the wish list:

Assignment is another feature that does not work for C strings. The bracket operator does work for C strings, and we would like it to continue to work for String objects. Ensuring against attempts to access non-existent elements would also be nice.

These builders will be useful in a variety of String applications.

There are a few more subtle client needs that also need our attention. The first has to do with reusing the large body of existing functions that analyze, or use without change, C strings in various ways. Examples include the C string library function strlen(), which returns the length of a string, and the C++ file management methods which take a filename parameter. It would be nice to make all of these avaliable for String objects as well. Overloading all of these would be a taxing effort, and almost certainly would result in an incomplete solution. A method Cstr() that returns the underlying C-string (as a const pointer) facilitates the use of such functions. Here is one example:

std::ifstream ifs;
ifs.open(s.Cstr()); // opens the file named by the string object s

Note: We could make all of these available to String objects by overloading an obscure operator called operator const char* (). It may be surprising to learn that this is an operator. Like a constructor, it has no return type, and like other operators, it can be overloaded for a new type. The effect of this overload is to provide an automatic type cast, or converter, for the compiler's use. It would give the compiler a way to make sense out of any substitution of String objects for any parameter of type const char*. Unfortunately, recent changes in the C++ implementation rules have made operator const char*() produce error messages due to ambiguous meaning of the bracket operator. Therefore we have retreated to the use of a method named Cstr() that accomplished the purpose via an explicit call.

Finally, it will be convenient to have a safe element return method that always has a value for any input index, whether or not the index is out of range:

Here is a complete set of operators and methods implied by this wish list is shown in the slide.

// comparison operators
int operator == (const String& s1, const String& s2);
int operator != (const String& s1, const String& s2);
int operator <  (const String& s1, const String& s2);
int operator <= (const String& s1, const String& s2);
int operator >= (const String& s1, const String& s2);
int operator >  (const String& s1, const String& s2);

// I/O operators (these will become friends during implementation)
std::ostream& operator << (std::ostream& os, const String& S);
std::istream& operator >> (std::istream& is, String& S);

class String
{
public:
  // constructors
  String           ();                       // construct a null string 
  String           (const char* cptr);       // construct a string around cptr
  String           (size_t size, char fill); // size sz and all characters = fill
  ~String          ();                       // destructor
  String           (const String& s);        // copy constructor
 
  // operators
  String&      operator =   (const String& s);  // assignment operator
  char&        operator []  (size_t n) ;        // returns REFERENCE to the character at place n
  const char&  operator []  (size_t n) const;   // const version

  // builders
  void  Wrap    (const char* cptr);        // wrap cptr up in a String
  void  GetLine (std::istream& in1);       // read/wrap entire line
  int   SetSize (size_t size, char fill);  // keep old data, fill extra spaces with fill character
  void  Clear   ();                        // make String empty (zero size)

  // data accessors 
  const char* Cstr() const; // returns bare C string for use as const char* function argument
  size_t Size    ()         const;
  size_t Length  ()         const;         // calls strlen(const char*)
  char   Element (size_t n) const;         // returns VALUE of the character at place n
                                             // (returns '\0' if n is out of range)
  ...
  }  ;

Be sure to study this interface carefully so that you understand every method and operator: what it does and why we want it.

Class String Implementation Plan

Because the comparison operators don't work properly for C strings, the library string.h provides the function int strcmp (const char*, const char*) whose return value codes the relationship between the two (presumptive) strings: strcmp() returns 0 if the strings are equal, a negative number if the first string precedes the second, and a positive number if the second precedes the first. (The order of precedence is lexicographic with ascii order in each string element.)

We can build on this idea to implement the comparison operators: define the function StrCmp() that takes two String object parameters and returns similarly coded information about the relative order of the two String objects. To control scope, we will make this function a member function. Because the function does not need an implicit object parameter, we make it static:

There may be occasional need to call this function directly as well, hence, we are making it public. This function can be used to implement all of the boolean operators, and because we have made it public, the boolean operators do not need friend status. The rest of the interface will either need friend status or be a member function. For these implementations, we need to consider the non-public data portion of the implementation.

The basic implementation plan is to maintain two private data members: an array data_ of type char, where we will store the underlying C string containing the character data, and an unsigned integer size_ that will be the size of the C string we can store in data_, that is, one less than the size of the array. (There will be a complication when the size is less than one, and we will have an exception to the rule in that case.) During implementation we will need to take special care to maintain these two data items in a consistent manner and to maintain a legal null-terminated C string in data_. Thus, we will have two private data items:

and our challenge is to maintain a valid C string (pointed to by) data_ whose length, as given by the C function strlen(), is equal to size_, and whose implementation details are hidden from the client.

Meeting this challenge is a complicated task, in many ways more complicated than the implementation of our generic vector class. Several private methods will assist in the organization of our implementation.

Clear() will safely convert the calling object to a null String, a step needed whenever the object is being re-instantiated by, for example, an assignment or an input operator. Clone() will make the calling object an exact duplicate of the parameter object. Assignment s1 = s2 will then consist of two steps: first s1.Clear() and second s1.Clone(s2).

Note that Clone() is an extremely dangerous operation. If called by a non-null String object s1, the result would be to orphan the memory allocated to S1 prior to the call. Therefore: Clone() is private. If there were significant client demand for Clear() it could be made public, and in some versions it is public. Clone() will always be protected or private.

Clearly we will need to do a lot of memory allocation in this implementation, so memory allocation is handled by the private member function NewCstr(). Because there are numerous ways that errors can occur, we also isolate the error handler into a private member function Error().

By isolating memory allocation and error handling, we accrue several advantages:

  1. Code is not repeated in various locations in the implementation.
  2. We can later change to an alternative memory allocator by changing this one method.
  3. Error handling is handled the same way for each memory allocation, so one good design ensures a uniformly applied good design.
  4. We can add more sophisticated exception handling later, again by modifying only one method.

Finally, because we are not certain of the behavior of strlen() and strcpy() on degenerate cases, we make versions of those as well. This completes the private methods list:

    static char*  NewCstr  (int n);
    static void   Error   (const char*);
    static int    StrLen  (const char*);
    static void   StrCpy  (char*, const char*);

We use the explicit keyword for any one-parameter constructor to prevent the compiler from using it as a type converter without the client's explicit call. Collecting all of these component details of the implementation plan, we arrive at the distributed version of the file xstring.h:

/*   xstring.h

     Definition of the String class;
     See also xranxstr.h, xstrcomp.h.
*/

#ifndef _XSTRING_H
#define _XSTRING_H

#include <iostream> // ostream class

namespace fsu
{

//--------------------
//    class String
//--------------------

  class String
  {
    // extraction and insertion operators
    friend std::ostream& operator << (std::ostream& os, const String& S);
    friend std::istream& operator >> (std::istream& is, String& S);

  public:
    // constructors
    String           ();                       // construct a null string 
    String           (const char* cptr);       // construct a string around cptr
    String           (size_t size, char fill); // size sz and all characters = fill
    ~String          ();                       // destructor
    String           (const String& s);        // copy constructor
 
    // operators
    String&      operator =   (const String& s);  // assignment operator
    char&        operator []  (size_t n) ;        // returns REFERENCE to the character at place n
    const char&  operator []  (size_t n) const;   // const version
    // operator const char* () const;           // auto conversion of String to const char*
    // this operator safely allows a String to be used for any const char* argument
    const char* Cstr() const; // returns bare C string for use as const char* function argument

    // builders
    void  Wrap    (const char* cptr);        // wrap cptr up in a String
    void  GetLine (std::istream& in1);       // read/wrap entire line
    int   SetSize (size_t size, char fill);  // keep old data, fill extra spaces with fill character
    void  Clear   ();                        // make String empty (zero size)

    // data accessors (const)
    size_t Size    ()         const;
    size_t Length  ()         const;         // calls strlen(const char*)
    char   Element (size_t n) const;         // returns VALUE of the character at place n
                                             // (returns '\0' if n is out of range)

    // String comparison function
    static int StrCmp (const String&, const String&);
    // modelled on the classic strcmp() in string.h
    // called by the boolean equality and order operators

    void Dump (std::ostream& os) const;
    // displays structural output for development and testing

  private:
    // variables
    char *   data_;
    size_t   size_;

    // methods
    void          Clone   (const String& s);
    static void   Error   (const char*);
    static int    StrLen  (const char*);
    static void   StrCpy  (char*, const char*);
    static char*  NewCstr (int n);
  }  ;

  // equality and order comparison operators
  int operator == (const String& s1, const String& s2);
  int operator != (const String& s1, const String& s2);
  int operator <  (const String& s1, const String& s2);
  int operator <= (const String& s1, const String& s2);
  int operator >= (const String& s1, const String& s2);
  int operator >  (const String& s1, const String& s2);

  // sum (concatenation) operator
  String operator + (const String&, const String&);

}   // namespace fsu
#endif

The file whose contents are quoted above is in the cpp distribution directory.

String::StrCmp

Helper methods are those whose primary purpose is facilitating the implementation of other methods and operators of the class. Typically, helper methods are protected (or private), although not necessarily. One of the fsu::String helper methods is public (StrCmp()) and another (Clear()) could be.

int String::StrCmp(const String& s1, const String& s2)
This method just sorts through the degenerate cases and then calls the classic strcmp().

int String::StrCmp(const String& s1, const String& s2)
// returns:   0 if s1 == s2
//            - if s1 < s2
//            + if s1 > s2
// using lexicographic ordering
{
  if      ((s1.Size() == 0) && (s2.Size() == 0))
    return 0;
  else if ((s1.Size() == 0) && (s2.Size() != 0))
    return -1;
  else if ((s1.Size() != 0) && (s2.Size() == 0))
    return 1;
  else
    return (strcmp(s1.data_, s2.data_));
} // end StrCmp()

We now turn to the implementation of the public member functions, a task made much more straighforward by helper method, and return to the helpers later.

String Implementation: Comparison Operators

These operators are all simple to implement using the helper StrCmp().

int operator == (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) == 0);
}

int operator != (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) != 0);
}

int operator >  (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) > 0);
}

int operator >=  (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) >= 0);
}

int operator <  (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) < 0);
}

int operator <=  (const String& s1, const String& s2)
{
  return (String::StrCmp(s1, s2) <= 0);
}

Equality is typical. The value returned by StrCmp(s1, s2) is tested for equality with zero, which is equivalent to testing whether s1 and s2 have the same elements.

String Implementation: I/O Operators

The output operator only needs to call the version that already exists for C strings:

std::ostream& operator << (std::ostream& os, const String& s)
{
  os << Cstr();
  return os;
}

Implementing the input operator istream& operator >> (istream& is, const String& s) is more complex. It helps to recall exactly what we expect of this operator. It should:

  1. Skip clearspace up to the first non-clearspace character in the istream is
  2. Read the string that begins with that character and ends at the next clearspace character in is
  3. Put the resulting string in the String object s
  4. Set size of scorrectly
  5. Return (a reference to) the stream object is

This task is made more complex by the lack of knowledge of how many characters the incoming string might have. We will need to use a temporary buffer to hold characters as they are read, and we will need to make this buffer larger when necessary. We need some way to decide how big to make the buffer initially and some plan on how to make it grow. A file-scope constant is a good solution:

static const size_t initBuffSize = 25;
// used as initial size for local buffer by operator >>() and method GetLine()

We start by declaring and initializing two local variables, currSize = 0 and buffSize = initBuffSize: currSize will keep track of the number of characters read from the stream, and buffSize will keep track of the size of the buffer in which we will temporarily store the read characters. Before allocating any memory, we check to see whether the stream actually has any characters in it by attempting to read a non-clearspace character into char x. If this fails, we return the stream with S unchanged. NOTE that the use of operator >>() for type char skips the clearspace for us!

Normally, there will be non-clearspace characters in the stream, so we proceed to read them one at a time into the buffer. The first has been read and is the value of x. The loop runs conditioned on x not being a clearspace character (blank, tab, or end-of-line) and not being at the end of the stream. The loop is a straightforward character read into the buffer, with time-out declared when the buffer is full.

During the time out, a lot of work must be done: memory allocated for a new buffer, data copied from old buffer to new buffer, old buffer de-allocated, and buffer renamed to the new address. This is an expensive time out. To keep the number of time outs small, we double the size of the buffer each time one is called.

When the loop terminates, we have to get the data into the String object S (using the handy Wrap() method), de-allocate the temporary buffer, and return the input stream.

std::istream& operator >> (std::istream& is, String& s)
{
  size_t currSize = 0, buffSize = initBuffSize;

  // skip clearspace
  char x;
  is >> x;
  if (is.fail() || is.eof())
  {
    return is;
  }

  // space for temporary char storage
  char* buffer = String::NewCstr(buffSize);

  // insert x and continue reading contiguous non-clearspace
  buffer[currSize++] = x;
  x = is.peek();
  while ((x != ' ') && (x != '\n') && (x != '\t') && (!is.eof()))
  {
    if (currSize == buffSize)  // need more buffer 
    {
      buffSize *= 2;
      char* newbuffer = String::NewCstr(buffSize);
      for (size_t i = 0; i < currSize; ++i)
      {
        newbuffer[i] = buffer[i];
      }
      delete [] buffer;
      buffer = newbuffer;
    }
    buffer[currSize++] = x;
    is.get();
    x = is.peek();
  }
  buffer[currSize] = '\0';
  s.Wrap(buffer);
  delete [] buffer;
  return is;
}

The input operator has hands down the most complex task of anything in the interface of fsu::String.

String Implementation: Constructors and Destructor

The default constructor initializes a String object with size zero:

String::String() : data_(0), size_(0)
{}

A second constructor initializes a String object with a C string:

String::String(const char* Cptr)  :  data_(0), size_(0)  
{
  Wrap(Cptr);
}

The destructor deletes allocated memory if necessary:

String::~String()
{
  if (data_) delete [] data_;
}

The copy constructor uses a call to Clone(s) to copy the parameter object s:

String::String(const String& s)
{
  Clone(s);
}


String Implementation: Member Operators

The String assignment operator illustrated here follows the classic pattern: avoid self-assignment, get rid of current data, and then recreate as a clone of the incoming parameter.

String& String::operator = (const String& s)
{
  if (this != &s)
  {
    Clear();
    Clone(s);
  }
  return *this;
}

A refinement of the implementation avoids the unnecessary step of memory re-allocation in the case where the two strings have the same size. This is the version that will be distributed:

String& String::operator = (const String& s)
{
  if (this != &s)
  {
    if (size_ != s.size_)
    {
      Clear();
      Clone(s);
    }
    else if (size_ != 0)
    {
      StrCpy (data_, s.data_);
    }
  }
  return *this;
}

The bracket operator implements a safety check by first checking the incoming index parameter for validity, then return (a reference to) the element at that index. (See the discussion of method Element() below.)

char& String::operator [] (size_t n)
{
  if ((size_ == 0) || (n >= size_))
  {
    Error("index out of range");
  }
  return data_[n];
}

const char* String::Cstr() const 
{
  return str;
}

The implementation of method Length() below serves as an example of this operation in action.

String Implementation: Builders

The Wrap() method gives clients an easy way to convert C strings to String objects. The implementation is straightforward:

void String::Wrap(const char* Cptr)
{
  Clear();
  if (Cptr)
  {
    size_ = StrLen(Cptr);
    data_ = NewCstr(size);
    StrCpy(data_, Cptr);
  }
}

Note that we could use almost identical code to overload the assignment operator with the following header:

which would serve the same purpose as Wrap(). This is an instance where overloading could lead to confusion and errors, which is why we decided on Wrap(), a method with a self-descriptive name.

The implementation of the method GetLine (istream& is) is almost identical to the implementation of the input operator, except that we test only for end-of-line and end-of-file, ignoring space and tab:

void String::GetLine (istream& is)
{
  size_t currSize = 0, buffSize = initBuffSize;
  char* buffer;
  buffer = new char [buffSize + 1];
  char x = is.get();
  while ((x != '\n') && (!is.eof()))
  {
    if (currSize == buffSize)  // need more buffer 
    {
      buffSize *= 2;
      char* newbuffer = new char [buffSize + 1];
      for (size_t i = 0; i < currSize; ++i)
      {
        newbuffer[i] = buffer[i];
      }
      delete [] buffer;
      buffer = newbuffer;
    }
    buffer[currSize++] = x;
    x = is.get();
  }
  buffer[currSize] = '\0';
  Wrap(buffer);
  delete [] buffer;
}

SetSize() is implemented using the techniques developed already in the other String operations that require re-sizing:

int String::SetSize (size_t size, char fill)
{
  if (data_ == 0)
    {
      data_ = NewCstr(size);
      if (data_ == 0) return 0;
      size_ = size;
      for (size_t j = 0; j < size_; ++j)
        data_[j] = fill;
      return 1;
    }

  if (size != size_)
    {
      char* newdata = NewCstr(size);
      if (newdata == 0) return 0;
      size_t i;
      if (size < size_)
        {
          for (i = 0; i < size; ++i)
            newdata[i] = data_[i];
        }
      else
        {
          for (i = 0; i < size_; ++i)
            newdata[i] = data_[i];
          for (i = size_; i < size; ++i)
            newdata[i] = fill;
        }
      delete [] data_;
      data_ = newdata;
      size_ = size;
    }
  return 1;
}


String Implementation: Data Accessors

These three methods are straightforward in both meaning and implementation, but together they present some interesting insights. Size() as expected returns the value of the private datum size_, while Length() returns the length of the C string datum data_. (The behavior of Length() is not apparent from the implementation on the slide, until you remember that the prototype for strlen() is

The overload of operator const char* will allow the compiler to substitute data_ as the parameter.) This implementation of the String class is designed and coded to keep these two numbers equal, but these efforts could be circumvented by a client. If a client is in doubt about the continued validity of a String object S, it might be wise to check whether S.Size() and S.Length() return the same value.

The method Element() takes an unsigned integer parameter and returns a value of type char, a character. Note the distinction with the bracket operator which returns a value of type char&, a reference to a character. The advantage of returning a reference is that it can be used on the left of an assignment, changing the value of the element itself, as in

That is, S[j] is an L-value. The disadvantage of returning a reference is that when the index is out of range, there is no choice other than to throw an error message (or allow access to unallocated memory).

The advantage of returning a value is that a value can always be returned: the string element str[i] if the index is in range and the null character '\0' otherwise. The disadvantage of returning a value is that the returned value cannot be used on the left of an assignment statement. (That is, S.Element(j) is an R-value but not an L-value.) When looping through the elements of a String object, with no need to write to these elements, the Element() method can be useful. In effect, the Element() method provides stop signs for loop termination where the bracket operator would fail.

size_t String::Size() const
{
  return size_;
}

size_t String::Length () const
{
  return strlen (data_);
}

char String::Element(size_t n) const
{
  if ((size_ == 0) || (n >= size_))
    return '\0';
  else
    return data_[n];
}


String Helper Methods

Helper methods are those whose primary purpose is facilitating the implementation of other methods and operators of the class. Typically, helper methods are protected (or private), although not necessarily. One of the fsu::String helper methods is public (StrCmp()), which we have already discussed. Another is Clear(): if the String has storage allocated, delete it, and set the storage pointer and size to zero.

void String::Clear()
{
  if (data_)
  {
    delete [] data_;
    data_ = 0;
    size_ = 0;
  }
}

void String::Clone(const String& s)
Clone() makes *this a copy of the parameter s, starting with size_. (The criterion for a copy is equality: after cloning, *this == s must return true. Note, this does not necessarily imply the two String objects are identical. For example, they might have different amounts of memory allocated.) If size_ is zero, no elements of s need to be copied, so no memory needs to be allocated. If size_ is non-zero, sufficient memory must be allocated and the appropriate string copy made.

void String::Clone(const String& s)
// Dangerous -- take care not to apply to *this !
{
  size_ = s.Size();
  if (size_ > 0)
  {
    data_ = NewCstr(size_);
    StrCpy (data_, S.data_);
  }
  else
  {
    data_ = 0;
  }
} // end Clone()

char* String::NewCstr(int n)
A memory allocation is requested from the runtime system. If the request is granted the address is returned; otherwise, Error() is called. This method could be changed to a non-default allocator if desired.

char* String::NewCstr(int n)
// creates a new (C) string of size n (array size = n+1)
// with memory allocation error message
{
  char* Cptr;
  if (n >= 0)
  {
    Cptr = new char [n + 1];
    if (Cptr == 0)
    {
      Error("memory allocation failure");
    }
    Cptr[n] = '\0';
  }
  else
    Cptr = 0;
  return Cptr;
} // end NewCstr()

int String::StrLen(const String&)
A classic implementation of C string length function.

int String::StrLen(const char* s)
// a version of strlen()
{
  int len = 0;
  if (s != 0)
  {
    for (len = 0; s[len] != '\0'; ++len){}
  }
  return len;
} // end StrLen()

void String::StrCpy(char* s1, const char* s2)
A classic implementation of C string copy function, with a safety check on the validity of the operands.

void String::StrCpy(char* s1, const char* s2)
// a version of strcpy(); DOES NOT CHECK OPERAND ARRAY SIZES
{
  if (s2 != 0)
  {
    if (s1 != 0)
    {
      int i;
      for (i = 0; s2[i] != '\0'; ++i)
        s1[i] = s2[i];
      s1[i] = '\0';
    }
    else
    {
      Error("StrCpy() operand 0");
    }
  }
} // end StrCpy()

void String::Error(const char* msg)
This method body is quite simple. It would be a convenient place from which to throw exceptions, which would make the body more complex.

void String::Error(const char* msg)
{
  cerr << "** String error: " << msg << '\n';
  exit (EXIT_FAILURE);
}