C+±入门到精通【18】string类和字符串流处理的深入剖析
目录
一、简介
我们通过头文件<string>
来定义的string类型的变量,其实一个类模板特化basic_string<char>
的对象。类模板basic_string
定义在头文件<string>
中,并在命名空间std中使用typedef
或using声明
为basic_string<char>
设置一个别名string
所以我们才可以用关键字string来声明一个字符串对象。
初始化string对象
注意:
string text("HELLO");
与string text = "HELLO";
是等价的,后者并不是一个赋值操作,而是会隐式的调用拷贝构造函数进行对象初始化的过程。
这里的初始化和赋值的意义并不相同,初始化是在对象创建过程中进行资源的分配,如果初始化失败,则这个对象不存在。而赋值是对一个已存在的对象进行赋值操作,赋值操作失败,对象仍然存在。
区分它们有什么意义吗?有的,兄弟有的。如果将后者看作一个赋值操作,那么编译器会首先调用默认构造函数来创建一个值为空的string对象,然后再对该对象进行赋值操作,这相当于多进行一次“赋值”操作,增加了额外的开销。除此之外,那些没有提供重载的赋值运算符的对象,在使用赋值运算符时,会使用编译器提供的默认的按位赋值运算符(浅拷贝),此时可能会出现二次释放问题(源对象和赋值对象都释放资源的话,其实是对同一份资源进行释放),也可能会导致资源泄漏问题,在赋值过程中直接将源对象的值拷贝到赋值对象中,赋值对象中原来使用的资源并不会被自动释放,此时就产生了资源泄漏。
string对象并不以NULL结尾
与C风格的char*
字符串不同,string类不一定是用空字符表示终止的,实现在于使用的平台。
二、字符串的赋值和连接
先来看一段代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str1("cat");
string str2;
string str3;
str2 = str1; // 使用重载的赋值运算符将str1复制到str2
str3.assign(str1); // 使用成员函数将str1复制到str3
cout << "str1: " << str1
<< "\nstr2: " << str2
<< "\nstr3: " << str3 << "\n\n";
// 修改str2和str3
str2[0] = str3[2] = 'r';
cout << "After modification of str2 and str3:\n"
<< "str1: " << str1
<< "\nstr2: " << str2
<< "\nstr3: ";
// 使用成员函数at
for (size_t i = 0; i < str3.size(); ++i)
{
cout << str3.at(i);
}
string str4(str1 + "egroy"); // 拼接对象str1,并用该拼接字符串为str4初始化
string str5;
str3 += "pet";
str1.append("alog");
str5.append(str1, 4, str1.size() - 4);
cout << "\n\nAfter concatenation:\nstr1: " << str1
<< "\nstr2: " << str2
<< "\nstr3: " << str3
<< "\nstr4: " << str4
<< "\nstr5: " << str5 << endl;
}
运行结果:
上面代码中使用了string对象的多种成员函数,比如,at
函数,该函数会返回一个字符串指定位置字符的引用,并且该成员函数与使用下标运算符[]
最大的区别在于,成员函数版本会自动检查边界,如果出现越界访问,会抛出一个out_of_range
异常。
至于其他的成员函数可以cppreference网站中查看。
三、字符串的成员函数
对于string对象的比较,与C语言中字符串只能使用strcmp
或strncmp
之类的函数(在string中的比较成员函数为compare
)进行比较之外,还可以使用类模板中提供的重载关系运算符(如<
、>
)来进行比较。
值得一提的是成员函数compare提供两个子串的比较,例如,现在要比较对象str1和对象str2中的指定子串是否相同,str1.compare(2, 5, str3, 0, 5);
,这条语句表示比较字符串str1从下标为0的字符起的5个字符和字符串str3从下标为2的字符起的5个字符是否相等。还有str1.compare(0, str3.size(), str3);
则是比较了str1和str3这两个字符串从下标为0的位置起的前str3.size()个字符(如果指定的长度超出的str1的长度,则只比较到str1的终止位置)。
string类中也有获取子串的成员函数——substr
。该函数有两个参数,第一个参数指明子串的开始下标,第二个参数指明子串的长度。
string类中还提供了一个交换两个字符串的成员函数,swap
,它会交换调用函数的对象和参数指定的对象的值。这对实现字符串排序十分有用。例如,str1.swap(str2);
就将str1和str2这个两个字符串交换了位置。
四、string类的特征
string类提供了收集字符串的大小、长度、容量、最大长度和其他特性的成员函数。一个字符串的长度或大小是当前存储在这个字符串对象中的字符数目。一个字符串的容量是指在不获取更多内存的情况下字符串所能存储的最大字符数。
下面代码中展示了利用string类的各种成员函数获取字符串的各个特性的过程:
#include <iostream>
#include <string>
using namespace std;
void printStatistics(const string&);
int main()
{
string str1;
cout << "Statistics before input:\n" << boolalpha;
printStatistics(str1);
// string类中重载的 >> 运算符与C++标准中的 >> 行为相同
// 在遇到 空白字符 时会停止一次提取
// 输入一个句子 String testing
cout << "\n\nEnter a string: ";
cin >> str1;
cout << "The string entered was: " << str1;
cout << "\nStatistics after input:\n";
printStatistics(str1);
cin >> str1;
cout << "\n\nThe remaining string is: " << str1 << endl;
printStatistics(str1);
// 使用成员函数append在str1对象后面追加字符
str1 += "123456789abcdefghijklmnopqrstuvwxyz987654321";
cout << "\n\nstr1 is now: " << str1 << endl;
printStatistics(str1);
// 使用成员函数resize,重新设置字符串的长度(不是容量)
str1.resize(str1.size() + 10);
cout << "\n\nStats after resizing by (length + 10):\n";
printStatistics(str1);
// 观察当resize的空间大于当前容量时,是否会增加容量
str1.resize(str1.size() + 100);
cout << "\n\nTesting member function resize, obeserve capacity:\n";
printStatistics(str1);
cout << endl;
}
void printStatistics(const string& stringRef)
{
cout << "capacity: " << stringRef.capacity()
<< "\nmax size: " << stringRef.max_size()
<< "\nsize: " << stringRef.size()
<< "\nlength: " << stringRef.length()
<< "\nempty: " << stringRef.empty();
}
运行结果:
五、查找字符串中的子串和字符
string类中有下图所示的查找成员函数
前两个函数很容易理解,就是找调用成员函数的字符串中与参数指定的字符串相同的子串,一个找第一个,另一个找最后一个。
后面的成员函数,则是在参数中指定一个字符集,并在调用函数的对象中找第一个或最后一个与字符集中某个字符相同的字符。not版本,则是找第一个或最后一个不在集合中的字符。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("noon is 12 pm; midnight is not.");
int location;
// 找字符串str中的第一个 "is" 和最后一个 "is"
cout << "Original string:\n" << str
<< "\n\n(find) \"is\" was found at: " << str.find("is")
<< "\n(rfind) \"is\" was found at: " << str.rfind("is");
// 查找字符串str中第一个与字符集合 "ab;" 中相同的字符,这里是 ';'
location = str.find_first_of("ab;");
cout << "\n\n(find_first_of) found '" << str[location]
<< "' from the group \"ab;\" at: " << location;
// 查找字符串str中最后一个与字符集合 "nos" 中相同的字符,这里是 'o'
location = str.find_last_of("ab;");
cout << "\n\n(find_last_of) found '" << str[location]
<< "' from the group \"ab;\" at: " << location;
// 找到字符串中第一个不存在于参数指定的字符集合中的字符,这里为 ' '
location = str.find_first_not_of("noi s");
cout << "\n\n(find_first_not_of) found '" << str[location]
<< "'is not contained in \"noi s\" and was found at: " << location;
// 找到字符串中最后一个不存在于参数指定的字符集合中的字符,这里为 '.'
location = str.find_last_not_of("noi s");
cout << "\n\n(find_last_not_of) found '" << str[location]
<< "'is not contained in \"noi s\" and was found at: " << location;
// 找到字符串中第一个不存在于参数指定的字符集合中的字符,这里为没有
location = str.find_first_not_of("noon is 12 pm; midnight is not.");
cout << "\n\nfind_first_not_of(\"noon is 12 pm; midnight is not.\")"
<< " returned: " << location << endl;
}
运行结果:
在使用查找函数查找失败时,函数会返回std::string::npos
,这里是-1
。
六、在字符串中替换字符
string类中使用erase
函数来删除字符,使用replace
来替换字符。
下面是示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("The values in any left subtree"
"\nare less than the value in the"
"\nparent node and the values in"
"\nany right subtree are greater"
"\nthan the value in the parent node");
cout << "Original string:\n" << str << "\n\n";
// 删除参数指定位置之后的字符,如果有两个参数,第二个参数表示删除的字符个数
// 没有指定则默认删除到最后一个字符
str.erase(62);
cout << "Original string after erase:\n" << str
<< "\nAfter first replacement:\n";
size_t position = str.find(" "); // 找字符串中第一个空格字符
// 将字符串中所有的空格字符替换成 '.'
while (position != string::npos)
{
// 将从position下标开始的1个元素替换成'.'
str.replace(position, 1, ".");
position = str.find(" ", position + 1); // 从position + 1位置进行查找下一个空格
}
cout << str << "\nAfter second replacement:\n";
// 使用字符作为参数表示只找与该参数相同的字符
position = str.find('.');
while (position != string::npos)
{
// 将'.'起的两个字符替换成参数中指定字符串中下标为5开始的2个字符
// 也就是 ";;"
str.replace(position, 2, "xxxxx;;yyy", 5, 2);
position = str.find('.', position + 1);
}
cout << str << endl;
}
运行结果:
七、在字符串中插入字符
要想向一个string对象中插入字符,可以使用insert
成员函数,下面我们来看看示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str1("beginning end");
string str2("middle ");
string str3("12345678");
string str4("xx");
cout << "Initial strings:\nstr1: " << str1
<< "\nstr2: " << str2
<< "\nstr3: " << str3
<< "\nstr4: " << str4 << "\n\n";
// 在str1的下标10处插入str2
str1.insert(10, str2);
// 将str4.substr(0, string::npos)
// substr的第二个参数表示子串的长度,
// 如果它为string::npos,这里string类中的静态数据成员,它的值为-1,
// 它的含义取决于上下文,在这里表示str4.substr(0, size())
// 所以下面的语句表示将str4整个字符串插入到str3的下标为3字符处
str3.insert(3, str4, 0, string::npos);
cout << "Strings after insert:\nstr1: " << str1
<< "\nstr2: " << str2
<< "\nstr3: " << str3
<< "\nstr4: " << str4 << "\n\n";
}
运行结果:
八、转换成C风格的基于指针的char*字符串
前面我们提到了一个string的对象,并不一定是以空字符结尾,所以要将一个string对象转换成一个C风格的字符串,下面使用到的函数copy
和c_str
是非常重要的。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("STRINGS");
const char* ptr1 = nullptr;
size_t length = str.size();
char* ptr2 = new char[length + 1]; // 额外分配一个字节的空间用于存储char*字符串的结束空字符
// copy只有一个版本,
// 接收三个参数,第一个为一个char类型的指针,用于保存字符串
// 第二个为拷贝的子串的长度,第三个为拷贝开始位置
// 该函数不会自动在拷贝的字符串尾部添加空字符,需要手动添加
str.copy(ptr2, length, 0);
ptr2[length] = '\0'; // 手动为char*字符串添加结束字符
cout << "string str is " << str
<< "\nstr converted to a pointer-based string is "
<< str.c_str() << "\nptr1 is ";
// 在C++11之后,该函数一定返回一个带有空字符的字符串
ptr1 = str.data();
cout << ptr1 << "\nptr2 is " << ptr2 << endl;
}
运行结果:
九、迭代器
与容器一样string类中提供迭代器来访问字符串。迭代器提供类似指针运算的语法来访问单个字符,并且与指针一样,迭代器并不会进行范围检查。下面是关于string类中迭代器使用的示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str("Testing iterators");
// 使用作用域分辨运算符指明const_iterator是在string类中被定义
// const_iterator是string类中的一个嵌套类型
string::const_iterator iterator = str.begin();
cout << "str = " << str
<< "\n(Using iterator) str is: ";
while (iterator != str.end())
{
// 像使用指针一样使用 * 运算符来解引用一个迭代器来获得它指向的值
cout << *iterator;
++iterator; // 将迭代器指向下一个字符
}
cout << endl;
}
运行结果:
上面代码中循环可以使用基于范围的for语句来替换,因为string对象提供迭代器。
十、字符串流处理
类istringstream
支持从字符串输入,类ostringstream
支持输出到一个字符串。它们是由typedef
定义的别名,它们实际上是:
typedef basic_istringstream<char> istringstream;
typedef basic_ostringstream<char> ostringstream;
这两个类模板提供与istream
和ostream
及其他用于内存中格式化的成员函数一样的功能。
使用内存中格式化的程序必须包含<sstream>和<iostream>头文件
。
ostringstream的演示
示例代码:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
ostringstream outputString;
string str1("Output of several data types ");
string str2("to an ostringstream object:");
string str3("\n double: ");
string str4("\n int: ");
string str5("\naddress of int: ");
double doubleValue = 123.45678;
int intValue = 22;
outputString << str1 << str2 << str3 << doubleValue
<< str4 << intValue << str5 << &intValue;
// 调用basic_ostringstream类中的成员函数str,
// 该函数的作用是返回对象中的字符串
// 或是修改对象中的字符串(两个版本)
// 此处是返回对象中的字符串
cout << "outputString contains:\n" << outputString.str();
outputString << "\nmore characters added";
cout << "\n\nAfter additional stream insertions,\n"
<< "outputString contains:\n" << outputString.str() << endl;
outputString.str("Testing memeber function str");
cout << "\n\nUsing member function str(\"Testing memeber function str\") replace contents in outputString:\n"
<< outputString.str() << endl;
}
运行结果:
istringstream的演示
示例代码:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
string input("Input test 123 4.7 A");
// 将字符串对象input中内容读入流对象inputString中
istringstream inputString(input); // 默认模式为ios_base::in
string str1;
string str2;
int integerValue;
double doubleValue;
char characterValue;
// 将流中数据存入内存中
inputString >> str1 >> str2 >> integerValue >> doubleValue >> characterValue;
cout << "The following items were extracted\n"
<< "from the istringstream object:" << "\nstring: " << str1
<< "\nstring: " << str2 << "\n int: " << integerValue
<< "\ndouble: " << doubleValue << "\n char: " << characterValue;
long value;
// 尝试从一个空流中读取数据
inputString >> value;
// good函数继承自basic_ios类
// 如果最近一次流上的I/O操作成功,则返回true,反之则返回false
// 这里则是判断从inputString流中想要读取一个long类型的数据的操作是否成功
if (inputString.good())
{
cout << "\n\nlong value is: " << value << endl;
}
else
{
cout << "\n\ninputString is empty" << endl;
}
}
运行结果:
十一、C++11数值转换函数
将数值转化为字符串对象
C++11的to_string
函数(<string>
中包含)返回参数数值所代表的字符串。例如cout << str.to_string(100);
,这条语句将会以字符串的形式输出100
。
将字符串对象转化为数值
C++中提供了几个将字符串对象转化成数值的函数
它们接收三个参数第一个参数是要转化的字符串,第二个(nullptr)和第三个(10)都有默认实参,第二个是一个用于保存开始转化的位置的引用,如果为nullptr,则表示不记录开始位置;第三个表示转化的目标数值的进制。
这些函数会忽略字符串最前面的空白字符,直到遇到第一个非空白字符才开始转化,在转化时需要判断字符串的形式是否符合要求:
- 有正号(+)或负号(-);(没有也可以默认为正)
- 在第三个参数base为8或0(函数自动判断进制,前缀为0则为八进制,前缀为0x或0X则为十六进制,其他的为十进制)时,前缀为0;
- 前缀为0x或0X;
- 为一连串数字
其余情况皆出错,例如,字符串中第一个不为空白字符的字符是非数值字符(比如‘a’),此时会抛出invalid_argument
异常,如果返回的值超过了该类型对应的范围,将会抛出out_of_range
异常。