1. 程式人生 > >Cpp Chapter 16: The string Class and the Standard Template Library

Cpp Chapter 16: The string Class and the Standard Template Library

(這已經是第二次部落格園吞我東西了,有點心態爆炸)

16.1 The string Class

) Constructing a string
Here is a table which shows the seven form of constructors that the string class has:

Constructor Description
string(const char * s) initializes a string object to the C-style string pointed to by s
string(size_type n, char c) initializes a string object of n elements, each initialized to c
string(const string & str) initializes a string object to the string object str(copy constructor)
string() creates a default string object of size 0
string(const char * s, size_type n) initializes a string object to the C-style string pointed to by s, continuing for n characters even it exceeds s
template string(Iter begin, Iter end) initializes a string to the values in the range [begin, end)
string(const string & str, size_type pos, size_type n = npos) initializes a string object to str, starting from pos and going to end or terminate with n characters

) The string class input
Typically, you have two ways of string input:
1

string stuff;
cin >> stuff;

In this way you read a word.
2

string stuff;
getline(cin, stuff);

In this way you read a line of characters.
An important feature of string class is that it automatically size the string object to hold the input characters.
The getline() function for the string class reads characters from input unless:
1 the end-of-file is encountered
2 the delimiting character(\n) is reached
3 the maximum possible number of characters is read, which is represented by string::npos
The operator>>() function reads up to the whitespace, which is typically ' ' and any characters that makes isspace() return true

Next comes a program that illustrates the features of file output:

// strfile.cpp -- read strings from a file
#include <iostream>
#include <string>
#include <cstdlib>
#include <fstream>

int main()
{
    using namespace std;
    ifstream fin;
    fin.open("tobuy.txt");
    if (fin.is_open() == false)
    {
        cerr << "Can't open file. Bye.\n";
        exit(EXIT_FAILURE);
    }
    string item;
    int count = 0;
    getline(fin, item, ':');
    while (fin)
    {
        ++count;
        cout << count << ": " << item << endl;
        getline(fin, item, ':');
    }
    cout << "Done\n";
    fin.close();
    return 0;
}

Noteworthy:
1 Typically, the file you tried to read should be in the same directory with the program. If not so, you can provide a full path name:

fin.open("C:\\CPP\\tobuy.txt");

The \\ here is an escape sequence, which represents one ''
2 You open a file with fin.open(path), and fin is an ifstream object. To get contents of the file, you use getline(fin, str, ':') where ':' stands for the delimiter here, which means that the input terminates when it reaches ':'
3 while(fin) loops as long as the input is not EOF and is good.

) Working with strings
1 You can compare strings with the overloaded relational operators, with one object being considered less if it occurs earlier in the machine collating sequence(ASCII).
2 You can determine the size of a string by length() or size():

if (snake1.length() == snake2.size())
    cout << "Both strings have the same length";

3 You can search for a substring or a character in a string"
size_type find(const string & str, size_type pos = 0) const finds the first occurence of str, starting from pos, and returning the position found or pos otherwise.
Next comes a program using the features of the string class, which is a game that lets you guess a word with a given length:

// hangman.cpp -- some string methods
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cctype>
using std::string;
const int NUM = 26;
const string wordlist[NUM] = {"apiary", "beetle", "cereal", "danger", "ensign", "florid", "garage", "health", "insult",
    "jackal", "keeper", "loaner", "manage", "nonce", "onset", "plaid", "quilt", "remote", "stolid", "train", "useful", "valid", "whence", "exnon", "yearn", "zipppy"};

int main()
{
    using std::cout;
    using std::cin;
    using std::tolower;
    using std::endl;
    std::srand(std::time(0));
    char play;
    cout << "Will you play a word game? <y/n>? ";
    cin >> play;
    play = tolower(play);
    while (play == 'y')
    {
        string target = wordlist[std::rand() % NUM];
        int length = target.length();
        string attempt(length, '-');
        string badchars;
        int guesses = 6;
        cout << "Guess my secret word. It has " << length << " letters, and you guess\none letter at a time. You get " << guesses << " wrong guesses.\n";
        cout << "Your word: " << attempt << endl;
        while (guesses > 0 && attempt != target)
        {
            char letter;
            cout << "Guess a letter: ";
            cin >> letter;
            if (badchars.find(letter) != string::npos || attempt.find(letter) != string::npos)
            {
                cout << "You already guessed that. Try again.\n";
                continue;
            }
            int loc = target.find(letter);
            if (loc == string::npos)
            {
                cout << "Oh, bad guess!\n";
                --guesses;
                badchars += letter;
            }
            else
            {
                cout << "Good guess!\n";
                attempt[loc] = letter;
                loc = target.find(letter, loc + 1); // start searching from loc+1
                while (loc != string::npos)
                {
                    attempt[loc] = letter;
                    loc = target.find(letter, loc + 1);
                };
            }
            cout << "Your word: " << attempt << endl;
            if (attempt != target)
            {
                if (badchars.length() > 0)
                    cout << "Bad choices: " << badchars << endl;
                cout << guesses << " bad guesses left\n";
            }
        }
        if (guesses > 0)
            cout << "That's right!\n";
        else
            cout << "Sorry, the word is " << target << ".\n";
        cout << "Will you play another? <y/n> ";
        cin >> play;
        play = tolower(play);
    }

    cout << "Bye\n";
    return 0;
}

Noteworthy:
1 Recall that string.find(letter) returns string::npos if it doesn't find the required letter in it. So the program uses this sentence to test if the user's input is already guessed before:

if (badchars.find(letter) != string::npos || attempt.find(letter) != string::npos)

npos is a static member of string class, it marks the greatest amount of characters that a string could have, thus used in find() to indicate that the letter required isn't found in the string.
2 The program also used + for string concatenation, > for comparison of string and the features provided by string class.

) What else does the string class offer
The string class objects will automatically adjust its size, so it initially will allocate a larger memory for a string object, letting it to grow in size. When it grows to exceed the memory, the compiler allocates a block of memory that is twice the size of the previous one and copy the object contents to the new place.
The capacity() method returns the size of the current block, and the reserve() method allows you to request a minimum size for the block.
Here is a program illustrating the mentioned features about memory allocation:

// str2.cpp -- capacity() and reserve()
#include <iostream>
#include <string>
int main()
{
    using namespace std;
    string empty;
    string small = "bit";
    string larger = "Elephants are a girl's best friend";
    cout << "Sizes\n";
    cout << "\tempty: " << empty.size() << endl;
    cout << "\tsmall: " << small.size() << endl;
    cout << "\tlarger: " << larger.size() << endl;
    cout << "Capacities:\n";
    cout << "\tempty: " << empty.capacity() << endl;
    cout << "\tsmall: " << small.capacity() << endl;
    cout << "\tlarger: " << larger.capacity() << endl;
    empty.reserve(50);
    cout << "Capacity after empty.reserve(50): " << empty.capacity() << endl;
    return 0;
}

) Conversion between C-style string and string objects
In order to convert from C-style string to string objects, use the string constructor:

char a[2] = "ab";
string b = string(a);

In order to convert from string objects to C-style strings(e.g., opening file), use string.c_str():

string b = "haha.txt";
fout.open(b.c_str());

16.2 Smart Pointer Template Classes

) Why smart pointer?
Sometimes you might forget to append delete ptr after you declare a block of memory which is pointed to by ptr, or a function would terminate by throwing exception and skip the delete code. In both cases, it causes a memory leak. In order to solve this problem one for all, you may design a pointer which has a destructor that automatically free the memory it points to. That is just the idea of smart pointers, to be more specific, auto_ptr, unique_ptr and shared_ptr.

) Using smart pointers
In order to use smart pointers, you include memory header file and use the syntax of ordinary templates:

auto_ptr<double> pd(new double);
auto_ptr<string> pd(new string);

Note that smart pointers belong to the std namespace. Next comes code that uses smart pointers:

// smrtptrs.cpp -- using three kinds of smart pointers
// requires support of C++11 shared_ptr and unique_ptr
#include <iostream>
#include <string>
#include <memory>

class Report
{
private:
    std::string str;
public:
    Report(const std::string s) : str(s) {std::cout << "Object created!\n";}
    ~Report() {std::cout << "Object deleted!\n";}
    void comment() const {std::cout << str << "\n";}
};

int main()
{
    {
        std::auto_ptr<Report> ps (new Report("using auto_ptr"));
        ps->comment();
    }
    {
        std::shared_ptr<Report> ps (new Report("using shared_ptr"));
        ps->comment();
    }
    {
        std::unique_ptr<Report> ps (new Report("using unique_ptr"));
        ps->comment();
    }
    return 0;
}

Noteworthy:
1 You could just use smart pointer as ordinary pointers. It supports derefrencing*, call members'->', and assign to other pointers.
2 Avoid letting smart pointers point to non-heap memory:

string vacation("oh");
shared_ptr<string> pvac(vacation);

When pvac expires, it would delete vacation, which is not right.

) Smart pointer consideration
Consider:

vocation = ps;

When you let two smart pointers point to a same chunk of memory, it will free it twice when they both expired, which would raise an error. There are strategies that solve this:
1 Design the assignment to do deep copy
2 Take the concept of ownership, meaning that only the pointer that owns the memory could delete it with its destructor. This is the strategy used for auto_ptr and unique_ptr.
3 Create an even smarter pointer that keeps track of how many pointer point to a particular object. This is called reference counting. shared_ptr uses this strategy.
Next comes an example that auto_ptr works poorly:

// fowl.cpp -- auto_ptr a poor choice
#include <iostream>
#include <string>
#include <memory>

int main()
{
    using namespace std;
    auto_ptr<string> films[5] = {auto_ptr<string> (new string("A")),
                                auto_ptr<string> (new string("B")),
                                auto_ptr<string> (new string("C")),
                                auto_ptr<string> (new string("D")),
                                auto_ptr<string> (new string("E"))};
    auto_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership

    cout << "The nominees for best avian baseball film are\n";
    for (int i = 0; i < 5; i++)
        cout << *films[i] << endl;
    cout << "The winner is " << *pwin << "!\n";
    cin.get();
    return 0;
}

This program will crash due to the sentence:

pwin = films[2];

As auto_ptr incorporates the idea of ownership, this assignment passes ownership from films[2] to pwin. After an auto_ptr gives up the ownership of an object, it no longer provides access to it. So when you later do cout << *films[2]; it will find a null-pointer and leads to a crash.
But if you use shared_ptr here, the program runs properly because both pointers retain access to the object.

---恢復內容結束---

## 16.1 The string Class
) Constructing a string
Here is a table which shows the seven form of constructors that the string class has:

Constructor Description
string(const char * s) initializes a string object to the C-style string pointed to by s
string(size_type n, char c) initializes a string object of n elements, each initialized to c
string(const string & str) initializes a string object to the string object str(copy constructor)
string() creates a default string object of size 0
string(const char * s, size_type n) initializes a string object to the C-style string pointed to by s, continuing for n characters even it exceeds s
template string(Iter begin, Iter end) initializes a string to the values in the range [begin, end)
string(const string & str, size_type pos, size_type n = npos) initializes a string object to str, starting from pos and going to end or terminate with n characters

) The string class input
Typically, you have two ways of string input:
1

string stuff;
cin >> stuff;

In this way you read a word.
2

string stuff;
getline(cin, stuff);

In this way you read a line of characters.
An important feature of string class is that it automatically size the string object to hold the input characters.
The getline() function for the string class reads characters from input unless:
1 the end-of-file is encountered
2 the delimiting character(\n) is reached
3 the maximum possible number of characters is read, which is represented by string::npos
The operator>>() function reads up to the whitespace, which is typically ' ' and any characters that makes isspace() return true

Next comes a program that illustrates the features of file output:

// strfile.cpp -- read strings from a file
#include <iostream>
#include <string>
#include <cstdlib>
#include <fstream>

int main()
{
    using namespace std;
    ifstream fin;
    fin.open("tobuy.txt");
    if (fin.is_open() == false)
    {
        cerr << "Can't open file. Bye.\n";
        exit(EXIT_FAILURE);
    }
    string item;
    int count = 0;
    getline(fin, item, ':');
    while (fin)
    {
        ++count;
        cout << count << ": " << item << endl;
        getline(fin, item, ':');
    }
    cout << "Done\n";
    fin.close();
    return 0;
}

Noteworthy:
1 Typically, the file you tried to read should be in the same directory with the program. If not so, you can provide a full path name:

fin.open("C:\\CPP\\tobuy.txt");

The \\ here is an escape sequence, which represents one ''
2 You open a file with fin.open(path), and fin is an ifstream object. To get contents of the file, you use getline(fin, str, ':') where ':' stands for the delimiter here, which means that the input terminates when it reaches ':'
3 while(fin) loops as long as the input is not EOF and is good.

) Working with strings
1 You can compare strings with the overloaded relational operators, with one object being considered less if it occurs earlier in the machine collating sequence(ASCII).
2 You can determine the size of a string by length() or size():

if (snake1.length() == snake2.size())
    cout << "Both strings have the same length";

3 You can search for a substring or a character in a string"
size_type find(const string & str, size_type pos = 0) const finds the first occurence of str, starting from pos, and returning the position found or pos otherwise.
Next comes a program using the features of the string class, which is a game that lets you guess a word with a given length:

// hangman.cpp -- some string methods
#include <iostream>
#include <string>
#include <cstdlib>
#include <ctime>
#include <cctype>
using std::string;
const int NUM = 26;
const string wordlist[NUM] = {"apiary", "beetle", "cereal", "danger", "ensign", "florid", "garage", "health", "insult",
    "jackal", "keeper", "loaner", "manage", "nonce", "onset", "plaid", "quilt", "remote", "stolid", "train", "useful", "valid", "whence", "exnon", "yearn", "zipppy"};

int main()
{
    using std::cout;
    using std::cin;
    using std::tolower;
    using std::endl;
    std::srand(std::time(0));
    char play;
    cout << "Will you play a word game? <y/n>? ";
    cin >> play;
    play = tolower(play);
    while (play == 'y')
    {
        string target = wordlist[std::rand() % NUM];
        int length = target.length();
        string attempt(length, '-');
        string badchars;
        int guesses = 6;
        cout << "Guess my secret word. It has " << length << " letters, and you guess\none letter at a time. You get " << guesses << " wrong guesses.\n";
        cout << "Your word: " << attempt << endl;
        while (guesses > 0 && attempt != target)
        {
            char letter;
            cout << "Guess a letter: ";
            cin >> letter;
            if (badchars.find(letter) != string::npos || attempt.find(letter) != string::npos)
            {
                cout << "You already guessed that. Try again.\n";
                continue;
            }
            int loc = target.find(letter);
            if (loc == string::npos)
            {
                cout << "Oh, bad guess!\n";
                --guesses;
                badchars += letter;
            }
            else
            {
                cout << "Good guess!\n";
                attempt[loc] = letter;
                loc = target.find(letter, loc + 1); // start searching from loc+1
                while (loc != string::npos)
                {
                    attempt[loc] = letter;
                    loc = target.find(letter, loc + 1);
                };
            }
            cout << "Your word: " << attempt << endl;
            if (attempt != target)
            {
                if (badchars.length() > 0)
                    cout << "Bad choices: " << badchars << endl;
                cout << guesses << " bad guesses left\n";
            }
        }
        if (guesses > 0)
            cout << "That's right!\n";
        else
            cout << "Sorry, the word is " << target << ".\n";
        cout << "Will you play another? <y/n> ";
        cin >> play;
        play = tolower(play);
    }

    cout << "Bye\n";
    return 0;
}

Noteworthy:
1 Recall that string.find(letter) returns string::npos if it doesn't find the required letter in it. So the program uses this sentence to test if the user's input is already guessed before:

if (badchars.find(letter) != string::npos || attempt.find(letter) != string::npos)

npos is a static member of string class, it marks the greatest amount of characters that a string could have, thus used in find() to indicate that the letter required isn't found in the string.
2 The program also used + for string concatenation, > for comparison of string and the features provided by string class.

) What else does the string class offer
The string class objects will automatically adjust its size, so it initially will allocate a larger memory for a string object, letting it to grow in size. When it grows to exceed the memory, the compiler allocates a block of memory that is twice the size of the previous one and copy the object contents to the new place.
The capacity() method returns the size of the current block, and the reserve() method allows you to request a minimum size for the block.
Here is a program illustrating the mentioned features about memory allocation:

// str2.cpp -- capacity() and reserve()
#include <iostream>
#include <string>
int main()
{
    using namespace std;
    string empty;
    string small = "bit";
    string larger = "Elephants are a girl's best friend";
    cout << "Sizes\n";
    cout << "\tempty: " << empty.size() << endl;
    cout << "\tsmall: " << small.size() << endl;
    cout << "\tlarger: " << larger.size() << endl;
    cout << "Capacities:\n";
    cout << "\tempty: " << empty.capacity() << endl;
    cout << "\tsmall: " << small.capacity() << endl;
    cout << "\tlarger: " << larger.capacity() << endl;
    empty.reserve(50);
    cout << "Capacity after empty.reserve(50): " << empty.capacity() << endl;
    return 0;
}

) Conversion between C-style string and string objects
In order to convert from C-style string to string objects, use the string constructor:

char a[2] = "ab";
string b = string(a);

In order to convert from string objects to C-style strings(e.g., opening file), use string.c_str():

string b = "haha.txt";
fout.open(b.c_str());

16.2 Smart Pointer Template Classes

) Why smart pointer?
Sometimes you might forget to append delete ptr after you declare a block of memory which is pointed to by ptr, or a function would terminate by throwing exception and skip the delete code. In both cases, it causes a memory leak. In order to solve this problem one for all, you may design a pointer which has a destructor that automatically free the memory it points to. That is just the idea of smart pointers, to be more specific, auto_ptr, unique_ptr and shared_ptr.

) Using smart pointers
In order to use smart pointers, you include memory header file and use the syntax of ordinary templates:

auto_ptr<double> pd(new double);
auto_ptr<string> pd(new string);

Note that smart pointers belong to the std namespace. Next comes code that uses smart pointers:

// smrtptrs.cpp -- using three kinds of smart pointers
// requires support of C++11 shared_ptr and unique_ptr
#include <iostream>
#include <string>
#include <memory>

class Report
{
private:
    std::string str;
public:
    Report(const std::string s) : str(s) {std::cout << "Object created!\n";}
    ~Report() {std::cout << "Object deleted!\n";}
    void comment() const {std::cout << str << "\n";}
};

int main()
{
    {
        std::auto_ptr<Report> ps (new Report("using auto_ptr"));
        ps->comment();
    }
    {
        std::shared_ptr<Report> ps (new Report("using shared_ptr"));
        ps->comment();
    }
    {
        std::unique_ptr<Report> ps (new Report("using unique_ptr"));
        ps->comment();
    }
    return 0;
}

Noteworthy:
1 You could just use smart pointer as ordinary pointers. It supports derefrencing*, call members'->', and assign to other pointers.
2 Avoid letting smart pointers point to non-heap memory:

string vacation("oh");
shared_ptr<string> pvac(vacation);

When pvac expires, it would delete vacation, which is not right.

) Smart pointer consideration
Consider:

vocation = ps;

When you let two smart pointers point to a same chunk of memory, it will free it twice when they both expired, which would raise an error. There are strategies that solve this:
1 Design the assignment to do deep copy
2 Take the concept of ownership, meaning that only the pointer that owns the memory could delete it with its destructor. This is the strategy used for auto_ptr and unique_ptr.
3 Create an even smarter pointer that keeps track of how many pointer point to a particular object. This is called reference counting. shared_ptr uses this strategy.
Next comes an example that auto_ptr works poorly:

// fowl.cpp -- auto_ptr a poor choice
#include <iostream>
#include <string>
#include <memory>

int main()
{
    using namespace std;
    auto_ptr<string> films[5] = {auto_ptr<string> (new string("A")),
                                auto_ptr<string> (new string("B")),
                                auto_ptr<string> (new string("C")),
                                auto_ptr<string> (new string("D")),
                                auto_ptr<string> (new string("E"))};
    auto_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership

    cout << "The nominees for best avian baseball film are\n";
    for (int i = 0; i < 5; i++)
        cout << *films[i] << endl;
    cout << "The winner is " << *pwin << "!\n";
    cin.get();
    return 0;
}

This program will crash due to the sentence:

pwin = films[2];

As auto_ptr incorporates the idea of ownership, this assignment passes ownership from films[2] to pwin. After an auto_ptr gives up the ownership of an object, it no longer provides access to it. So when you later do cout << *films[2]; it will find a null-pointer and leads to a crash.
But if you use shared_ptr here, the program runs properly because both pointers retain access to the object.