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 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:
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.
// static: char str1 [11]; // static array of char str1[10] = '\0'; // null-terminate strcpy (str1, "abcdefghij"); // define the chars in the string // dynamic: char* str2; // pointer to char str2 = new char [11]; // dynamic array of char str2[10] = '\0'; // null-terminate strcpy (str2, str1); // define // operator <<(): cout << str1 << ' ' << str2 << '\n';
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.
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, ...
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.
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.
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.
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
char * str1, * str2; // yada dada if (str1 < str2) // yada dada
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
cout << str1;
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
cin >> str2;
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:
String s1, s2; cin >> s1 >> s2; if (s1 <= s2) cout << s1; else cout << s2;
Exercise: Replace the declarations in the first line of code above with
char *s1, *s2;
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 sNote: 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:
char Element (size_t n) const; // returns the character at place n // (returns '\0' if n 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.
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:
static int StrCmp (const String&, const String&);
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:
private: // data char * data_; size_t size_;
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.
// methods void Clear (); void Clone (const String& s);
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:
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.
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.
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.
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:
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.
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); }
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.
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; }
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
int strlen (const char*);
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
S[j] = x;
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]; }
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); }