题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。
作者: tyc611, 2007-01-13
本文主要讨论C++ 标准库中string类的使用。
如果文中有错误或遗漏之处,敬请指出,谢谢!
说明:本文为了方便叙述,把标准库里的模板类型仅用于char类型进行讨论,下面所有函数原型都是简化后的。
先介绍一些后面要用到的基本东西。
字符序列的几种构造方式
- c —— 只有一个字符c的序列
- n, c —— n个重复字符c的序列
- s —— 以指针s开始的并以'\0'字符结束的C字符串序列
- s, n —— 以指针s开始的n个字符构成的字符序列
- str —— str为string类对象的字符序列
- str, pos, n —— 为string类对象str的子串:从pos位置开始的n个字符或到str结束(如果提前到达str串尾)的字符序列
- first, last —— 字符序列:[first, last)(注意:这里及后面的[a, b)表示从a到b,包括a,但不包括b)
如果上面的位置参数(如上面的pos)超过了字符串尾,那么在调用string成员函数时将抛出一个异常class out_of_range。
string对象的下标是从0开始。
string::size_type类型
string::size_type类型是表示string类中字符个数的大小类型,这是string类定义的一个配套类型(companion type)。通过使用这些配套类型,库类型的使用就能与机器无关(machine-independent)。
size_type定义为unsigned型(unsigned int or unsigned long),可以保证能够存储任意string对象的长度。string::size_type默认是std::size_t类型。
string::npos
static const size_type string::npos = -1; //npos(no position)作为查找失败的标志 这里作一下解释:在后面将看到,很多默认参数用了npos,而size_type一般被声明为unsigned,也就是说这儿的-1实质上是一个很大的正数。
为了方便讨论,在后面的叙述中作如下约定:把调用成员函数的那个对象所对应的字符串序列称为控制字符序列;把操作数或参数所对应的字符串序列称为操作字符序列。
string类对象的构造
简化构造函数原型如下(注意,为了简便,把模板中最后一个默认参数省略了):
explicit basic_string();
string(const char *s); string(const char *s, size_type n); string(const string& str); string(const string& str, size_type pos, size_type n); string(size_type n, E c); string(const_iterator first, const_iterator last);
|
应用这些构造函数举例如下:
int main() { string s1; // default constructor, null string const char lit[] = "hello, world"; string s2(lit); // value is "hello, world" string s3(lit, 5); // value is "hello" string s4(s2); // value is "hello, world" string s5(s2, 2, 6); //value is "llo, w", 6 chars from index 2 string s6(s2, 2, 20);//value is "llo, world" string s7(5, 'c'); // value is "ccccc" string s8(lit+1, lit+5); // value is "ello," string s9(s2.begin(), s2.end()-1); // value is "hello, worl" cout<<s1<<'\n'<<s2<<'\n'<<s3<<'\n'<<s4<<'\n'<<s5<<'\n' <<s6<<'\n'<<s7<<'\n'<<s8<<'\n'<<s9<<endl; return 0; }
|
string对象的操作
字符串比较
支持六种关系运算符(==、!=、>、>=、<、<=),其采用字典排序策略(与C中字符串比较策略完全一样)。这六个关系运算符是非成员的重载运算符。而这些运算符都支持三种操作数组合:string op string、string op const char*、const char* op string(其中op是前面六种关系运算符中任意一种)。解释:提供运算符的三种重载版本主要是从效率角度考虑的,其避免了临时string对象的产生。
另外,string类还提供了各种重载版本的成员函数compare来比较,简化函数原型为:
int compare(const string& str) const; int compare(size_type p0, size_type n0, const string& str); int compare(size_type p0, size_type n0, const string& str, size_type pos, size_type n); int compare(const char* s) const; int compare(size_type p0, size_type n0, const char* s) const; int compare(size_type p0, size_type n0, const char* s, size_type n) const;
|
其中,子序列[p0, p0 + n0)是控制字符序列的子序列,[pos, pos + n)是操作字符序列的子序列。如果没有指定比较范围就是整个字符串参与比较。
返回值:如果调用该函数的对象的比较序列小于操作数比较序列,则返回负数;若相等,则返回0;否则,返回正数。
应用举例如下:
int main() { string str1 = "string: apple"; string str2 = "string: orange"; string str3 = "string"; const char s[] = "string: banana"; if (str1.compare(str2) < 0) // true cout<<"1. "<<str1<<" < "<<str2<<endl; if (str1.compare(0, 6, str3) == 0) //true cout<<"2. "<<str1<<"'s first 6 chars are the same to "<<str3<<endl; if (str1.compare(0, 8, str2, 0, 8) == 0) //true cout<<"3. "<<str1<<" has the same first 8 chars to "<<str2<<endl; if (str1.compare(s) < 0) //true cout<<"4. "<<str1<<" < "<<s<<endl; if (str1.compare(0, 8, s) < 0) //true cout<<"5. "<<str1<<"'s first 8 chars are less than "<<s<<endl; if (str2.compare(0, 8, s, 8) == 0) cout<<"6. "<<str1<<" has the same first 8 chars to "<<s<<endl; return 0; }
|
字符串相加
针对string类提供了非成员重载operator+,支持string对象之间、string对象与const char*对象之间、string对象与char对象之间相加,并且operator + 两边的操作数的任意顺序都支持。简化函数原型如下:
string operator+ (const string& lhs, const string& rhs); string operator+ (const string& lhs, const char *rhs); string operator+ (const string& lhs, char rhs); string operator+ (const char *lhs, const string& rhs); string operator+ (char lhs, const string& rhs);
|
字符串赋值
字符串赋值有两种方式:一是利用成员重载运算符operator=;另外就是使用成员重载函数assign可以更加灵活地处理。这里只提供简化函数原型供参考:
string& operator=(char c); string& operator=(const char *s); string& operator=(const string& rhs); string& assign(const char *s); string& assign(const char *s, size_type n); string& assign(const string& str, size_type pos, size_type n); string& assign(const string& str); string& assign(size_type n, char c); string& assign(const_iterator first, const_iterator last);
|
字符串追加
字符串追加同样有两种方式:一是operator+=;另外就是成员函数append。简化函数原型如下:
string& operator+=(char c); string& operator+=(const char *s); string& operator+=(const string& rhs);
string & append(const char *s); string& append(const char *s, size_type n); string& append(const string& str, size_type pos, size_type n); string& append(const string& str); string& append(size_type n, char c); string& append(const_iterator first, const_iterator last); |
读取子串
获取某个下标处的字符:一是用at成员函数;另外就是用operator[]。获取子串,可以用成员函数c_str及substr,还有成员函数data和copy。简化函数原型如下:
reference operator[](size_type pos); const_reference operator[](size_type pos) const; reference at(size_type pos); const_reference at(size_type pos) const;
const char *c_str() const; const char *data() const; string substr(size_type pos = 0, size_type n = npos) const; size_type copy(char *s, size_type n, size_type pos = 0) const;
|
注意:若at函数的参数pos无效,则抛出异常out_of_range;但如果operator[]的参数pos无效,则属于未定义行为。所以at比operator[]更加安全。
其中,copy返回实际拷贝的字符数。
替换子串
成员函数replace实现替换某个子串。简化函数原型如下:
string& replace(size_type p0, size_type n0, const char *s); string& replace(size_type p0, size_type n0, const char *s, size_type n); string& replace(size_type p0, size_type n0, const string& str); string& replace(size_type p0, size_type n0, const string& str, size_type pos, size_type n); string& replace(size_type p0, size_type n0, size_type n, char c); string& replace(iterator first0, iterator last0, const char *s); string& replace(iterator first0, iterator last0, const char *s, size_type n); string& replace(iterator first0, iterator last0, const string& str); string& replace(iterator first0, iterator last0, size_type n, char c); string& replace(iterator first0, iterator last0, const_iterator first, const_iterator last);
|
这里,可能需要用到这几个函数得到整个字符序列:
const_iterator begin() const; iterator begin(); const_iterator end() const; iterator end();
|
插入字符串
成员函数insert实现在某点处插入字符串。简化函数原型如下:
string& insert(size_type p0, const char *s); string& insert(size_type p0, const char *s, size_type n); string& insert(size_type p0, const string& str); string& insert(size_type p0, const string& str, size_type pos, size_type n); string& insert(size_type p0, size_type n, char c); iterator insert(iterator it, char c); void insert(iterator it, const_iterator first, const_iterator last); void insert(iterator it, size_type n, char c);
|
注意:insert函数是在插入点(p0 or it)之前插入字符串。
删除子串
成员函数 erase实现删除某个子串。简化函数原型如下:
iterator erase(iterator first, iterator last); iterator erase(iterator it); string& erase(size_type p0 = 0, size_type n = npos);
|
如果指定删除的字符个数比字符串中从指定位置开始的剩余字符个数还多,那么只有这些字符被删除。
查找子串
查找子串有六种方式,分别有五类成员函数与之应。
- find 查找控制字符序列中与操作字符序列匹配的第一个子串,并返回子串的起始位置;
- rfind 查找控制字符序列中与操作字符序列匹配的最后一个子串,并返回该子串的起始位置,相当于逆向查找;
- find_first_of 查找控制字符序列中第一个出现在操作字符序列中的字符的位置,并返回该位置;
- find_first_not_of查找控制字符序列中第一个不出现在操作字符序列中的字符的位置,并返回该位置;
- find_last_of 查找控制字符序列中最后一个出现在操作序列中的字符的位置,并返回该位置;
- find_last_not_of 查找控制字符序列中最后一个不出现在操作字符序列中的字符位置,并返回该位置;
如果这些函数查找失败,则返回string::npos。
其中,find函数的简化函数原型如下:
size_type find(char c, size_type pos = 0) const; size_type find(const char *s, size_type pos = 0) const; size_type find(const char *s, size_type pos, size_type n) const; size_type find(const string& str, size_type pos = 0) const;
|
另外的五个函数的函数原型和find的函数原型类型类似,区别在于,如果是是逆序查找的函数(rfind, find_last_of, find_last_not_of),则pos参数默认值为npos。
其它成员函数和友元函数
size_type capacity() const; // 返回当前字符串的存储空间大小 >= size()
void reserve(size_type n = 0);// 保证capacity()>=n
bool empty() const; // 若字符串为空,返回true size_type size() const; // 返回字符串长 size_type length() const; // 等于 size() size_type max_size() const; //返回string类中字符串的最大长度 void resize(size_type n, char c = ' '); //若长度不够,则用c填充加长的部分;保证size()返回n void swap(string& str); //两string对象交换,能在常数时间内完成(必须是使用相同allocator的两对象,这里都使用的默认的)
// 其它非成员函数 istream& getline(istream& is, string& str); istream& getline(istream& is, string& str, char delim);
ostream& operator<<(ostream& os, const string& str); istream& operator>>(istream& is, const string& str);
|
其中,istream& getline(istream& is, string& str); 相当于 istream& getline(istream& is, string& str, char delim = '\n');
getline函数在下列三种情况下结束提取:
1)遇到文件结束符;
2)遇到分隔符delim。如果第一个就是分隔符,str为空串(并且该分隔符被从流中读出丢弃),但istream测试为真;
3)如果已经提取了istream.max_size()个字符,那么提取结束,并且将调用istream.setstate(ios_base::failbit),即此时返回的istream测试为假。
如果函数没有提取到字符(包括分隔符),那么将调用istream.setstate(failbit),此时测试istream为假。
默认情况下, istream& operator>>(istream& is, const string& str);在下列三种情况下结束提取: 1)遇到文件结束符; 2)遇到空白字符(空格、Tab、换行); 3)如果已经提取了is.max_size()个字符,或者提取了is.width()(非0情况下)个字符。 如果没有提取到任何非文件结束符的字符(包括空白字符),那么将调用istream.setstate(failbit),此时测试istream为假。
例如,看看下面的循环:
while(cin >> word) { cout << "word read is: " << word << '\n'; }
要中止上面的循环应该用文件结束符: Win——Ctrl+Z Unix——Ctrl+D 并且,应该是输入行的第一个字符就是文件结束符,然后回车才能结束循环。
应用举例
替换string对象中所有子串
在上面string类的成员函数中,没有替换string对象中所有子串的方法。我们可以利用find和replace这两个成员函数来实现该目标,代码如下:
string& replaceAll(string& context, const string& from, const string& to) { // replace all substring 'from' with substring 'to' in string context string::size_type curPos = 0; string::size_type foundPos = 0; while ((foundPos = context.find(from, curPos)) != string::npos) { context.replace(foundPos, from.size(), to); curPos = foundPos + to.size(); } return context; }
|
除了上面这个通用方法,对于特殊的需求,我们还可以有更简单的方法。如果我们只需要用一个字符替换string对象中的另一个字符,则可以用标准库提供的模板类算法replace实现该目标,其原型如下:
template void replace(FwdIt first, FwdIt last, const T& vold, const T& vnew);
把该算法应用string对象,可以这样使用:
replace(s.begin(), s.end(), 'F', 'T');
这样就可以把string对象s中所有的F字符替换为T字符。
提取网页文件中的E-mail地址
如果要抓取互联网上的E-mail地址(如WebSpider),需要提取抓取到的网页文件里的URL和E-mail地址。假如这里需要提取E-mail地址,那么可以利用string类方便的进行提取工作。下面提供了一个实现该功能的源码示例(从我的一个程序里截取出来的):
void RetrieveEmailFromPage(const string& page, const string& url) { int posCur = 0, posStart, posAt, posEnd, posTmp; string email; string tags = "._-" "abcdefg" "hijklmn" "opqrst" "uvwxyz" "ABCDEFG" "HIJKLMN" "OPQRST" "UVWXYZ" "0123456789";
while (true) { if ((posAt = page.find('@', posCur)) != string::npos) { posStart = page.find_last_not_of(tags, posAt - 1) + 1; posEnd = page.find_first_not_of(tags, posAt + 1); posTmp = page.find('.', posAt + 1); // domain has at least one dot "xxx.yy"
if (posTmp != string::npos && posTmp < posEnd - 1 && posStart != posAt && posEnd != posAt + 1) { email = page.substr(posStart, posEnd - posStart); AddEmail(email, url); // add the email to special data struct } posCur = posEnd; } else { return; } }//while }
|
忽略大小的字符串比较
string类没有提供忽略大小写的比较。我们可以利用basic_string模板类来实现。basic_string的特化类的比较是通过其模板参数char_traits定义的。所以我们需要重新实现一个忽略大小写的char_traits类,然后用它特化basic_string。下面分别实现了窄字符和宽字符版本。
源程序如下:
//: ichar_traits.h
#ifndef ICHAR_TRAITS_H #define ICHAR_TRAITS_H
#include <string> #include <cctype> #include <cwctype> #include <ostream>
/////////////////////////////////////////////////////////////////////// using std::toupper;
struct ichar_traits: std::char_traits <char> { static bool eq(char c1, char c2) { return toupper(c1) == toupper(c2); } static bool lt(char c1, char c2) { return toupper(c1) < toupper(c2); } static int compare(const char* str1, const char* str2, size_t n) { if (str1 == NULL) return -1; else if(str2 == NULL) return 1; for (; n-- > 0; ++str1, ++str2) { if (toupper(*str1) < toupper(*str2)) return -1; else if(toupper(*str1) > toupper(*str2)) return 1; } return 0; } static const char* find(const char* str, size_t n, char c) { for(; n-- > 0; ++str) { if (toupper(*str) == toupper(c)) return str; } return NULL; } };
typedef std::basic_string<char, ichar_traits> istring;
inline std::ostream& operator<< (std::ostream& os, const istring& s) { return os<<s.c_str(); }
/////////////////////////////////////////////////////////////////////////////// using std::towupper;
struct iwchar_traits: std::char_traits <wchar_t> { static bool eq(wchar_t c1, wchar_t c2) { return towupper(c1) == towupper(c2); } static bool lt(wchar_t c1, wchar_t c2) { return towupper(c1) < towupper(c2); } static int compare(const wchar_t* str1, const wchar_t* str2, size_t n) { if (str1 == NULL) return -1; else if(str2 == NULL) return 1; for (; n-- > 0; ++str1, ++str2) { if (towupper(*str1) < towupper(*str2)) return -1; else if(towupper(*str1) > towupper(*str2)) return 1; } return 0; } static const wchar_t* find(const wchar_t* str, size_t n, wchar_t c) { for(; n-- > 0; ++str) { if (towupper(*str) == towupper(c)) return str; } return NULL; } }; typedef std::basic_string<wchar_t, iwchar_traits> iwstring;
inline std::wostream & operator<< (std::wostream & os, const iwstring& s) { return os<<s.c_str(); }
#endif
|
测试代码如下:
#include <iostream> #include <cassert> #include "ichar_traits.h"
using namespace std;
int main() { istring first = "string"; istring second = "STRING"; assert(first == second); assert(first.compare(second) == 0); assert(first.find('r') == 2); assert(first.find('I') == 3); assert(first.find("Ing") == 3); assert(first.find("No") == string::npos); cout<<"First: "<<first<<", Second: "<<second<<endl; iwstring wfirst = L"string"; iwstring wsecond = L"STRING"; assert(wfirst == wsecond); assert(wfirst.compare(wsecond) == 0); assert(wfirst.find(L'r') == 2); assert(wfirst.find('I') == 3); assert(wfirst.find(L"Ing") == 3); assert(wfirst.find(L"No") == string::npos); wcout<<L"FirstW: "<<wfirst<<L", SecondW: "<<wsecond<<endl; return 0; }
|
其它
至于为什么要用建议用string类来代替以前c形式的字符串,除了安全、方便等原因外。还有一个很重要的原因,那就是string类可以实现写时拷贝(这由编译器决定是否予与实现),这样如果程序中有较多的字符串拷贝操作的话,那么可以大大提高程序的效率,并减少存储空间的使用。
写时拷贝(copy-on-write)策略:string类低层采用引用计数实现只有当字符串被修改时才创建各自的拷贝。这样,当字符串只是作为值参数或在其他情形下使用时,这种方法能够节省时间和空间。在多线程编程中,几乎不可能安全地使用引用计数来实现(参阅Herb Sutter的《More Exceptional C++》)。
另一个建议就是:如果同一string类对象的修改操作比较频繁,那么可以先调用reserve()确保足够的空间,以避免后面的修改过程中重新分配内存,提高效率。
另外,如果直接从流输入string类对象,默认情况只能得到以一个单词(即不能有空格)。如果要得到一行,一是可以设置输入流的格式(详见关于输入输出流的内容),另一个更简单的方法是使用上面提到的getline函数。
如果文中有错误或遗漏之处,敬请指出,谢谢!
参考文献:
[1] Thinking in C++(Volume Two, Edition 2)
[2] C++ Primer(Edition 4)
[3] International Standard:ISO/IEC 14882:1998
| | |