9.15.4.3. String Class Example

The final example class, named String, is logically similar to the previous CString class but with a different way of representing the string's length. Both manage textual data as a sequence of characters saved in a dynamically allocated array. But the former represents the string's length with a member variable, while the latter uses null-terminated C-strings. Furthermore, because the array isn't null-terminated, the String member functions must replace the C-string functions used by the CString functions with loops and array operations.

Continuing the practice I started in the CString example, I'm limiting the descriptions accompanying the C++ functions. My goal is to provide you an opportunity to practice and refine your elaboration skills. If you find statements or expressions you don't understand, ask yourself, "What do I need to review?" Have you studied the "Review" lists, the links in this, and previous examples? Solving problems with computer programs demands constant review because complex concepts inevitably build on simple ones.

String Class Member Variables
class String
{
    private:
	char*   text = nullptr;
        size_t  length = 0;
	size_t  capacity = 0;
};
  •  text  - character pointer pointing to an array allocated on the heap.
  •  length  - the number of characters in the string.
  •  capacity  - the number of elements in the allocated array.
Empty String String With Content
An empty String object. The member variable 'text' points to an array 15 characters long. The member variables 'length' and 'capacity' store the number of characters and total number of elements in the array, respectively. A String object that contains the string 'Hello' in elements 0 through 4. The member variables length and capacity are 5 and 15, respectively.
The String class. The String class, which we must not confuse with C++'s string class, relies on three member variables, as illustrated here. Like the previous CString class, the String class manages textual data as a sequence of characters saved in a dynamically allocated array, enabling String objects to change their capacity as needed.

Member functions that create new String objects or modify this object must generally update all three member variables.

Aside from changing the class name from CString to String, adding the length member variable is the only between this and the previous class specification. Please see the complete class in the source code at the bottom of the page.

String Constructors And Destructor

inline String::String()
    : capacity(15),
    text(new char[15]) {}
     
     
inline String::String(size_t cap)
    : capacity(cap),
    text(new char[cap]) {}
     
     
inline String::~String()
{
    if (text != nullptr)
        delete[] text;
}
(a)(b)(c)
String::String(char c)
{
    length = capacity = 1;
    text = new char[capacity];
    text[0] = c;
}
 
 
 
String::String(const char* s)
{
    while (s[capacity])
        capacity++;
    length = capacity;
    text = new char[capacity];
    for (int i = 0; s[i]; i++)
        text[i] = s[i];
}
String::String(const String& cs)
{
    capacity = cs.capacity;
    length = cs. length;
    text = new char[capacity];
    for (int i = 0; i < cs.length; i++)
        text[i] = cs.text[i];
}
 
(d)(e)(f)
String constructors and destructor: building and destroying objects. Please notice that constructors (a), (b), and (e) rely on the member variable initialization performed in the class specification (Figure 1).
  1. The default constructor.
  2. A general (not conversion) constructor.
  3. The destructor.
  4. A conversion constructor: character to String.
  5. A conversion constructor: C-string to String. Relies on C++ treating '\0' (the null termination character) as false.
  6. The copy constructor.

String Access Functions

inline size_t String::get_length() const
{
	return length;
}
inline size_t String::get_capacity() const
{
	return capacity;
}
(a)(b)
char& String::at(int index)
{
	if (index < 0 || index >= length)
		throw "index out of bounds";
	return text[index];
}
(c)
String access functions. "Access function" is a collective term for both getter and setter functions. As illustrated here, access functions are often small and relatively straightforward.
  1. We named the corresponding CString function length. Can you see why we changed the name in the String class? Hint: notice the change in the function's body.
  2. This String is identical to the CString version.
  3. Can you spot the single change from the CString version? The function still returns a reference, enabling it to serve as both a getter and a setter:
    • cout << cs.at[4] << endl;
    • cs.at[4] = 'X';

String I/O Functions

Throughout the semester, we've used the functions, operators, and classes declared in <iostream> whenever our programs needed to write output to or read input from the console. Even the C++ string class's I/O functions rely on <iostream> features. However, all these I/O functions depend on an established, well-known string representation. None of them "know about" our String class. Furthermore, basing the String class on a non-terminated character array implies that we can't use the C-string functions. This situation leaves us with two alternatives to implement the String I/O functions.

First, we could use loops, array indexing, and single-character I/O. Many String functions follow this basic pattern, so the approach is familiar. However, the second alternative provides an authentic but simplified example of how "real" I/O functions work. Library output functions, including the inserter (<<), convert numbers to strings and format the strings for output. But they can't write the final string to the console. The operating system is responsible for managing the computer's resources, including the console, so programs must access those resources through low-level system calls. Input functions operate similarly.

WindowsUnix / Linux / macOS
  • #include <io.h>
  • int _write(int fd, const void* buffer, size_t count)
  • int _read(int fd, void* buffer, size_t count)
  • #include <unistd.h>
  • int write(int fd, const void* buffer, size_t count)
  • int read(int fd, void* buffer, size_t count)
I/O system calls. Running programs transfer character data to and from the console by making system calls. The calls transfer the characters in a program-defined array or buffer, but they are not C-strings because they are not null-terminated. Various operating systems have different system calls and associated header files, but the four most common desktop systems have similar calls with similar parameters: The functions return the number of characters transferred or -1 on failure. For a better understanding of void*, please see void-pointers. For more detail about the system calls, please see _write, write, _read, and read.

Our String I/O functions are not as sophisticated as the "real" I/O functions. They don't handle numeric data or provide any formatting. However, they demonstrate where a function would do the "heavy lifting" (the difficult or complex operations) of converting and formatting the data compared to where it makes the system call to transfer the text.

void String::print(const char* line)
{
    int i = 0;
    for (; line[i]; i++)
        ;
    _write(1, line, i);			// Windows
    //write(1, line, i);		// Linux
}
void String::print() const
{
    _write(1, text, length);		// Windows
    //write(1, text, length);		// Linux
}
 
 
 
(a)(b)
void String::println() const
{
    char new_line = '\n';

    print();
    _write(1, &new_line, 1);		// Windows
    //write(1, &new_line, 1);		// Linux
}
void String::readln(size_t n)
{
    if (n >= capacity)
        grow(n);

    length = _read(0, text, n);		// Windows
    //length = read(0, text, n);	// Linux
}
(c)(d)
String I/O functions.
  1. Counts the characters in the C-string line and writes them to the console output.
  2. Prints a CString object to the console but leaves the cursor at the end of the text. We can't take the address of a constant, so we save the newline in a variable and pass its address to the system call.
  3. Calls (b) and then prints a new line to move the cursor to the beginning of the next line.
  4. Ensures the String object has enough space to save n characters. The read operations return before reading n characters when they read a carriage return or end-of-file marker. The number of characters becomes the string's length.
We can make the functions more robust or secure by testing the system calls' returned values and throwing an exception if they return a -1. We can also detect and report an error if the write functions write fewer characters than requested.

Functions Modifying this Object

inline void String::clear()
{
    length = 0;
}
void String::insert(const String& s, int index)
{
    if (index < 0 || index > length)
        throw "index location is too large";
    if (length + s.length >= capacity)
        grow(length + s.length);

    for (int i = length - index; i >= 0; i--)	
        text[index + s.length + i] = text[index + i];

    for (int i = 0; i < s.length; i++)
        text[index + i] = s.text[i];

    length += s.length;
}
(a)
void String::append(const String& s)
{
    if (length + s.length > capacity)
        grow(length + s.length);

    for (int i = 0; i < s.length; i++)
        text[length++] = s.text[i];
}
(b)(c)
String functions that modify this object.
  1. A String is logically empty when its length is 0 without changing the contents of the text array.
  2. Validates this string's length and copies s to the end of it.
  3. First, the function validates that s is inbounds and this string has enough space. Next, it shifts the end of the string to the right and copies s into this string at position index. The function finishes by updating this string's length.

Functions Returning A String Object

String String::copy() const
{
    String local(capacity);

    local.length = length;
    for (int i = 0; i < length; i++)
        local.text[i] = text[i];

    return local;
}
 
 
String String::concat(const String& s) const
{
    String local(length + s.length);

    local.length = length + s.length;
    for (int i = 0; i < length; i++)
        local.text[i] = text[i];
    for (int i = 0; i < s.length; i++)
        local.text[length + i] = s.text[i];

    return local;
}
(a)(b)
String String::substring(int index, size_t len) const
{
    if (index < 0 || index > length)
        throw "index location is out of bounds";
    if (index + len > length)
        throw "\"length\" is too long";

    String local(len);

    local.length = len;
    for (int i = 0; i < len; i++)
        local.text[i] = text[index + i];
    return local;
}
(c)
Functions that create, fill, and return a new String object. Each function in this group defines a local variable (object) named local, operates on it, and returns it. Before returning the local object, each function saves values in its member variables length, capacity, and text array.
  1. Copies all data saved in this object to local.
  2. Creates a local string with sufficient capacity to save both strings' contents. Copies this and then s to the new string.
  3. Validates that index and the following len characters are inbounds. The function creates a local String and copies len characters from this to it. Notice that the parameter len and the member length are different variables.

String Comparison Functions

The C++ string class has a rich set of operators supporting the full range of comparison operations. In a subsequent chapter, we'll see how to add these operators to our String class. But for now, two example functions demonstrate the character-by-character comparisons on which string comparisons depend. The functions compare simple, one-byte characters by their ASCII codes, so upper case letters precede lower case, with punctuation characters intermixed. And C++ follows the "nothing comes before something" rule, so "X" comes before "X*" where * can be any character.

bool String::equals(const String& s) const
{
    if (length != s.length)
        return false;
    for (int i = 0; i < length; i++)
        if (text[i] != s.text[i])
            return false;

    return true;
}
 
 
 
 
 
 
 
 
 
String s1("hello world");
String s2("hello world");
s1.equals(s2)		// equals

String s3("hello world");
String s4("hello Alice");
s3.equals(s4)		// not equals

String s5("hello world");
String s6;
s5.equals(s6)		// not equals

String s7("apple");
String s8("zebra");
s7.equals(s8)		// not equals

String s9("a");
String s10("aa");
is9.equals(s10)		// not equals
(a)(b)
int String::order(const String& s) const
{
    for (int i = 0; i < length && i < s.length; i++)
        if (text[i] < s.text[i])
            return -1;
        else if (text[i] > s.text[i])
            return 1;

    if (length == s.length)
        return 0;
    else if (length < s.length)
        return -1;
    else
        return 1;
}
 
 
 
 
String s1 = "apple";
String s2 = "apple";
s1.order(s2)		// 0
s2.order(s1)		// 0

String s1 = "apple";
String s2 = "zebra";
s1.order(s2)		// -1
s2.order(s1)		//1

String s1 = "apple";
String s2 = "appl";
s1.order(s2)		// 1
s2.order(s1)		// -1

String s1 = "apple";
String s2 = "applx";
s1.order(s2)		// -1
s2.order(s1)		// 1
  • The ordering rule:
  • s1 comes before s2: return -1
  • s1 & s2 are the same: return 0
  • s1 comes after s2: return 1
(c)(d)(e)
The String equals and order functions.
  1. For two String objects to be equal, they must have the same length, and all corresponding characters must be the same (including having the same case).
  2. The example is excerpted from client.cpp and illustrates the narrow conditions needed for two strings to be equal.
  3. The for-loop attempts to order two String objects based on the relative order of their constituent characters. If the function cannot order the strings based only on their characters, it uses their lengths to complete the ordering.
  4. Adapted from client.cpp, the examples demonstrate how the ordering function denotes the ordering of two strings.
  5. The ordering rule is the same one used by strcmp and Java's Comparator.compare method.

The String Helper Function

void String::grow(size_t new_capacity)
{
    char* local = new char[new_capacity];

    for (int i = 0; i < length; i++)
        local[i] = text[i];
    delete[] text;
    text = local;
    capacity = new_capacity;
}
 
void String::set_capacity(size_t new_capacity)
{
    char* local = new char[new_capacity];

    length = (new_capacity < length) ? new_capacity : length;
    for (int i = 0; i < length; i++)
        local[i] = text[i];
    delete[] text;
    text = local;
    capacity = new_capacity;
}
(a)(b)
The String grow and set_capacity function. The code for these functions illustrates why changing a string's capacity is relatively expensive. Whenever the program changes a string's capacity, it must allocate a new array, copy the contents of the old to the new array, and deallocate the old array. The String class uses a loop and array indexing in place of the strcpy and strncpy functions.
  1. The String version of the grow function is logically identical to the CString version, and the only difference in their implementations is how they copy the characters from the old to the new array.
  2. Converting the grow helper function to the more general setter function set_capacity requires three changes. First, rename the function appropriately. Next, move it from a private to a public section. Finally, check this string's length, highlighted in yellow. If the string's new capacity is less than its previous length - if the function shrinks the string - it only copies the number of characters that fit in the smaller string.

Downloadable Code

View1Download
String.h String.h
String.cpp String.cpp
client.cpp client.cpp
1 The behavior of these links depends on your browser and desktop configuration.