9.15.4.2. CString Class Example

Review
Reviewed in the LPString Example Additional CString Review Concepts

Throughout my first career as a software engineer, some of my tasks always involved writing and maintaining programs written in C. Midway through that career, I began writing and maintaining object-oriented programs written in C++. That experience allowed me to teach C++ to my colleagues within and without my main employment. At this time, experienced C programmers were the most frequent C++ students. So, creating a string class based on C-strings was a natural object-oriented programming example.

Creating a string class based on C-strings is still instructionally beneficial. It gives new C++ programmers additional experience using the C-string functions and demonstrates many object-oriented features presented in the current chapter. The previous example emphasized pictures for solving problems and converting the solutions to working C++ functions. This example encourages you to use the C-string functions' documentation and practice your elaboration skills. As you study each function, try to explain to yourself what each statement does. You can put yourself in an authentic mindset by imagining that you are in a formal code review and must explain to your peers how each function operates and why you have implemented it as it appears in the example.

As we transition our string class from length-prefixed to C-style strings, please consider the following issues:

Now we now have sufficient background to outline our class, which we name CString to emphasize its reliance on C-strings.

CString Class Member Variables
class CString
{
    private:
	char*   text = nullptr;
	size_t  capacity = 0;
};
  •  text  - character pointer pointing to an array allocated on the heap.
  •  capacity  - size type (an unsigned integer) that saves the number of elements in the allocated array.
Empty String String With Content
An empty CString object. The member variable text points to an array 15 characters long. The first character in the array is the null-termination character. The member variable capacity stores the number of elements in the array: 15. A CString object that contains the string 'Hello' in elements 0 through 4; element 5 is the null-termination character. The array is still 15 elements long, which is the value saved in the variable capacity.
CString: A C-string based class. Embedding a C-string in a class shifts the responsibility for the error-prone memory management from the application to the class programmer. It also "hides" the logic and operations, making the string dynamic (able to change its capacity as needed). Once the class programmer writes and verifies the constructors, destructor, and algorithmic member functions, application programmers can use them without regard to their complexity.

Member functions that create new CString objects or modify this object must update both member variables.

Throughout the text, I've referred to C-style strings as C-strings. I named the class CString to distinguish it from the other string examples in this chapter. Although the two names are similar and related, they denote different string representations.
#pragma once

#include <cstring>
using namespace std;

class CString
{
    private:
        char*  text = nullptr;
        size_t capacity = 0;

    public:
        // constructors and destructor
        CString();
	CString(size_t cap);
	CString(char c);
        CString(const char* s);
        CString(const CString& s);
        ~CString(); 

        // access
        size_t length() const;
        size_t get_capacity() const;
        char& at(int index);

        // i/o
        static void print(const char* line);
        void print() const;
        void println() const;
        void readln(size_t n);

        // modify "this"
        void append(const CString& s);
        void insert(const CString& s, int index);
        void clear();

        // new CString
        CString copy() const;
        CString concat(const CString& s) const;
        CString substring(int index, size_t length) const;

        // ordering
        bool equals(const CString& s) const;
        int order(const CString& s) const;

    private:
        void grow(size_t new_capacity);
};
  • Needed by the inline functions using C-string functions
  • A Cstring has two member variables: text and capacity
  • Default constructor
  • General (not conversion) constructor
  • Conversion constructor
  • Conversion constructor
  • Copy constructor
  • Destructor
  • Number of saved characters
  • Total number of characters
  • Reference to one character: can be an l-value or an r-value
  • Print line to console
  • Print this string to console
  • Print this string to console with new-line
  • Read a line (max n characters) from console
  • Append s to the end of this
  • Insert s into this at index
  • Logically remove all characters from this string
  • Make a copy of this string
  • Make a new string by joining this and s
  • Make a new string by by extracting a substring from this
  • Two strings are equal if & only if their contents are the same
  • ASCIIbetically order two strings
  • A helper function increasing a string's capacity
The CString class specification and inline functions. The CString member functions use C-string library functions whenever possible. This approach makes most member functions small enough that we can appropriately implement them as inline functions. Putting a function's body in the class specification automatically makes it an inline function. But we can also use the inline keyword if we want to keep the class specification compact (or narrow as needed here). Choosing to make a "small" function (three or four statements) inline or not is often a matter of personal taste, but we should not inline "large" functions. Inlining a function, either by putting it in the class or outside with the inline keyword, is just a suggestion, which the compiler may accept or reject.

CString Constructors And Destructor

inline CString::CString()
    : capacity(15), text(new char[15])
{
    text[0] = '\0';
}

 
inline CString::CString(size_t cap)
    : capacity(cap), text(new char[cap])
{
    text[0] = '\0';
}

 
inline CString::CString(char c)
{
    capacity = 2;
    text = new char[2];
    text[0] = c;
    text[1] = '\0';
}
(a)(b)(c)
inline CString::CString(const char* s)
{
    capacity = strlen(s) + 1;
    text = new char[capacity];
    strcpy(text, s);
}
inline CString::CString(const CString& cs)
{
    capacity = cs.capacity;
    text = new char[capacity];
    strcpy(text, cs.text);
}
inline CString::~CString()
{
    if (text != nullptr)
        delete[] text;
}
 
(d)(e)(f)
CString constructors and destructor: building and destroying objects. This figure and those following describe a function's behavior (especially its unexpected behavior), but they do not elaborate what each statement does. Working in small groups, take turns elaborating successive functions to each other.
  1. Many C++ default string constructors create an empty string with an initial capacity of 15 characters. Imagine a program that creates a new string and adds characters to it one at a time. Enlarging a string requires three steps. First, the program must allocate memory for a new array; next, it copies the contents of the old array to the new one and deallocates the old array. It's inefficient to do this for each character added to the array, so the default constructor gives the new string a little "growing room." We copy this behavior with the CString default constructor.
  2. Although this constructor has a single, non-reference parameter, it isn't a conversion constructor - a fact that can cause some trouble. C++ automatically converts integers to characters; if the integer is too large to fit in a character, it only uses the least-significant 8 bits. So, CSstring cs(25); may be a call to (b) or to (c). Client code must disambiguate the call with a typecast: CString cs((size_t)25);. The C++ string class avoids the problem by not defining a similar constructor.
  3. A conversion constructor that converts a single character to a CString object.
  4. A conversion constructor that converts a C-string to a CString object.
  5. The copy constructor that makes a new CString object by copying an existing one.
  6. The CString destructor destroys an object by deallocating heap memory. Recall that destructors do not have parameters, implying they cannot be overloaded.

CString Access Functions

inline size_t CString::length() const
{
    return strlen(text);
}
inline size_t CString::get_capacity() const
{
    return capacity;
}
(a)(b)
char& CString::at(int index)
{
    if (index < 0 || index > strlen(text) - 1)
        throw "index out of bounds";
    return text[index];
}
(c)
CString 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. As a getter function, we could have name it get_length or getLength, but we choose to follow the C++ string example.
  2. An example of a typical getter.
  3. The function verifies the index is inside this string and throws an exception if it isn't. The function returns a reference, enabling it to serve as both a getter and a setter:
    • cout << cs.at[4] << endl;
    • cs.at[4] = 'X';

CString I/O Functions

inline void CString::print(const char* line)
{
    cout << line;
}
inline void CString::print() const
{
    cout << text;
}
(a)(b)
inline void CString::println() const
{
    print();
    cout << '\n';
}

 
void CString::readln(size_t n)
{
    if (n >= capacity)
        grow(n + 1);

    cin.getline(text, n);
}
(c)(d)
CString I/O functions.
  1. print(char*) is static, but the "static" keyword only appears with the function prototype in the class specification. Static functions do not have a this pointer and are not bound to an object when called. See client.cpp at the bottom of the page for examples.
  2. Prints a CString object to the console but leaves the cursor at the end of the text.
  3. Calls (b) and then prints an endl to move the cursor to the beginning of the next line.
  4. The general string ADT that we outlined previously included a parameterless readln() function. We could implement it in the CString class (see the next figure), but it isn't practical. So, for simplicity, we follow the C-string getline example and define a parameter that is the maximum number of characters the function will read.
A picture showing the data structures implemented in the adjacent code. The pointer variable 'blocks' points to an array of pointers. Each element in the 'blocks' array points to an array of characters. The structure forms a two-dimensional array or table: the 'blocks' array represents the rows, and each character array is a table row. The variable 'count' is the number of rows used, and 'i' is the number of characters in the current row.
void CString::readln()
{
    const int NBLOCKS = 1024;
    const int BLKSIZE = 512;
    char** blocks = new char*[NBLOCKS];
    int count = 0;

    for (; count < NBLOCKS; count++)
    {
        blocks[count] = new char[BLKSIZE];
        for (int i = 0; i < BLKSIZE - 1; i++)
        {
            int c = cin.get();
            if (c != '\n' && c != EOF)
                blocks[count][i] = c;
            else
            {
                blocks[count][i] = '\0';
                goto done;
            }
        }
        blocks[count][BLKSIZE - 1] = '\0';
    }

    done:
    delete[] text;
    capacity = count * NBLOCKS + strlen(blocks[count]) + 1;
    text = new char[capacity];
    text[0] = '\0';
    for (int i = 0; i <= count; i++)
    {
        strcat(text, blocks[i]);
        delete[] blocks[i];
    }
    delete[] blocks;
}
The CString readln() function. I didn't include the parameterless readln() function in the class specification because it's not a practical function. But it is a good example of "nuts and bolts" programming - solving a programming problem even when there isn't an elegant or efficient solution.

The amount of memory the operating system makes available to a program is the one constraint on a CString's capacity. A program can allocate what is needed with the new operator if there is sufficient memory. But the program doesn't "know" how much memory to allocate until the full CString contents are in memory. We solve the problem by allocating and filling blocks of memory as needed and assembling the final CString after the program reads all the text. The get function, first introduced in the wc.cpp example, reads characters from the console one at a time.

This solution still fails to read an arbitrarily long string. Nevertheless, we can adjust the symbolic constants to make the function read strings as long as we need. If we replace the blocks array with a linked list, the function can continue reading text until it exhausts its available memory. While this version is a good overall solution, it's a more appropriate topic for a Data Structures And Algorithms course.

CString Process Functions

Process functions perform general operations on an object's mmember variables. They include non-private member functions not included in other labeled categories. See The UML Class Symbol.

Functions Modifying this Object

Three CString functions deliver the results of their operations to the client program by updating this object - the object bound to the function at call time.

void CString::append(const CString& s)
{
    if (strlen(text) + strlen(s.text) >= capacity)
        grow(strlen(text) + strlen(s.text) + 1);

    strcat(text, s.text);
}
inline void CString::clear()
{
    text[0] = '\0';
}


 
(a)(b)
The CString append and clear functions. The first two functions altering this string are small and simple.
  1. Checks this string's capacity and increases or grows it if necessary. The C-string strcat completed the append operation.
  2. Makes the string logically empty by placing a null-termination character at the beginning of the text array. The function does not alter the string's capacity.
At the top, the picture shows two CStrings, this ('Hello word') and s ('new '), before the insert operation. The insertion takes place at index location 6. At the bottom, the picture shows 'world' shifted to the right 4 places and 'new ' inserted at index location 5.
void CString::insert(const CString& s, int index)
{
    if (index < 0 || index > strlen(text))                    // validate index is inbounds
        throw "index location is out of bounds";
    if (strlen(text) + strlen(s.text) >= capacity)            // grow "this" string if needed
        grow(strlen(text) + strlen(s.text) + 1);

    memmove(&text[index + strlen(s.text)], &text[index], strlen(text) - index + 2);
    memcpy(&text[index], s.text, strlen(s.text));
}
The CString insert function. The third function in this category, insert, is more complex than the previous two. The picture helps us understand the relationships between the three components: two CString objects and the integer index. The first if-statement verifies that the index is within this string or at the end, and throws an exception if it isn't. The second if-statement verifies that this string's capacity is large enough to hold both strings and grows it if necessary. It's easier to understand how the function works if we can see the intermediate steps. All three text arrays in the picture are the same array before, during, and after the insert operation.
  1. The initial situation a the beginning of the function call.
  2. The C-string memmove function performs the shift operation. strlen(text) - index + 1 is the number of characters the function moves or shifts, and we add one more to account for the null-termination character. Notice that memmove doesn't erase the characters in locations 6 through 9.
  3. The memcopy function copies s to this, overwriting "worl" in locations 6 through 9.
We need several test cases to validate this function. I let s be an empty string, a string with one character, the case illustrated here, and a long string.

Functions Returning A CString Object

The three functions in this category return a new CString representing the results of their operations. Each function defines a local CString object, named local, operates on it, and returns it when its work is complete.

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

    strcpy(local.text, text);

    return local;
}
 
CString CString::concat(const CString& s) const
{
    CString local(strlen(text) + strlen(s.text) + 1);

    strcpy(local.text, text);
    strcat(local.text, s.text);

    return local;
}
(a)(b)
The CString copy and concat functions. Three functions, the CString(size_t) constructor and the C-string strcpy and strcat functions, do all the work.
  1. Defines a local variable whose capacity is the same as this string's, copies this text to the local variable, and the returns it.
  2. Defines a local variable whose capacity is the sum of this and s. The C-string functions copy this to local and then concatenates local and s before retuning local.
Initially, this CString is 'Hello world'. The program creates an empty local CSstring whose capacity is length+1. In the example, the function copies 'world' from this string to the local string and adds a null-termination character at the end.
CString CString::substring(int index, size_t length) const
{
    if (index < 0 || index > strlen(text))
        throw "index location is too large";
    if (index + length > strlen(text))
        throw "\"length\" is too long";

    CString local(length + 1);
    strncpy(local.text, &text[index], length);
    local.text[length] = '\0';
    return local;
}
(a)(b)
The CString substring function. The substring function is more complex than most of the CString functions, so we again turn to a picture to help establish the relationships between the two objects and the two integer parameters. The function verifies that the beginning and end of the substring, index and index + length respectively, are inside this string. It throws an exception if either endpoint is out of bounds. The strncpy function copies length characters from this string to local; substring null-terminates and returns the copy.

CString Comparison Functions

The two functions in this category compare two CString objects: this and the parameter s.

bool CString::equals(const CString& s) const
{
    return !strcmp(text, s.text);
}
int CString::order(const CString& s) const
{
    return strcmp(text, s.text);
}
(a)(b)
The CString equals and order functions. The C-string library has a function, strcmp, that compares two C-strings. The library documentation typically describes it as an ordering function: Given two C-strings, it determines their relative alphabetical (or more accurately, their ASCIIbetical) ordering. But the library does not define an equals function.
  1. When its two C-string parameters are identical (contain the same characters), the strcmp function returns 0, indicating that the strings have the same ordering. C++ treats 0 as false, which the negation operator, !, converts to true.
  2. Expressing the relative order of two strings as -1, 0, or 1 is a common encoding used in many programming languages (for example, Java's Comparable.compareTo method). So, the CString order function only needs to return the value produced by the strcmp function

The CString Helper Function

In chapter 6, I claimed that functions influence how software developers think about and solve problems. And further that developers decompose large complex functions into smaller, more manageable ones. These observations remain true for member functions. Developers often decompose large member functions into smaller helper functions and make them private because they help the other member functions rather than forming a complete service.

void CString::grow(size_t new_capacity)
{
    char* temp = new char[new_capacity];

    strcpy(temp, text);
    delete[] text;
    text = temp;
    capacity = new_capacity;
}
 
void CString::set_capacity(size_t new_capacity)
{
    char* temp = new char[new_capacity];

    strncpy(temp, text, new_capacity - 1);
    temp[new_capacity - 1] = '\0';
    delete[] text;
    text = temp;
    capacity = new_capacity;
}
(a)(b)
The CString grow function. Helper functions embody code used by one or more member functions. Class designers exclude them from the class's public interface because they don't represent a complete service that a client program should access directly.
  1. Several of the member functions described above can increase a CString object's capacity beyond its allocated memory. When this happens, the member functions call the grow function to increase the object's capacity. Putting this code in the grow function eliminates duplicate code from calling functions.
  2. Imagine that the class designer wishes to make the function a setter rather than a helper. A setter can truncate or shrink an object's capacity in addition to increasing or growing it. It takes three modest steps to complete the conversion:
    1. Name the function appropriately - give it a more general name
    2. Replace strcpy with strncpy, which copies one C-string to another or a specified number of characters - whichever is the shortest
    3. Insert a null-termination character in the copied string
    4. Move the function from a class's private section to a public one
Both functions demonstrate a problem shared by many dynamic data structures - C++ strings and vectors and Java ArrayLists and Vectors. Whenever a program changes the capacity of an array-based data structure, it must allocate a new array with the desired capacity, copy the old array to the new one, and deallocate the old array. The copy step becomes time-expensive for large structures.

Downloadable Code

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