前言
本文主要介绍STL六大组件中的容器之一:string,在学习C++的过程中,我们要将C++视为一个语言联邦(摘录于Effective C++条款一)。如何理解这句话呢,我们学习C++,可将其分为四个板块;分别为C、Object-Oriented C++(面向对象的C++)、Template C++(模板)、STL。本文就介绍STL中的string
C语言字符串的局限性
C语言中的字符串处理依赖于字符数组和一系列标准库函数(如strcpy
, strcat
, strlen
等)。这些方法虽然基本能满足需求,但存在几个显著的缺点:
- 安全性问题:C语言中的字符串操作常常设计直接的内存操作问题,荣誉造成缓冲区的溢出、内存泄漏的等安全问题,例如strcpy函数在赋值数据时不会检查目标缓冲区的大小,这可能导致超出其容量的写操作。
- 效率低下:C语言的字符串操作通常需要手动管理字符串长度和内存,如需拼接字符串时,肯需要多次调用strlen来获取当前长度,然后再添加新内容,这样的操作效率较低。
- 使用不便:C语言的字符串需要程序员对内存非常小心谨慎的操作,容易出错且代码难以维护。
C++string类的优势
与C语言的基本字符串处理相比,C++的string
类提供了一个更安全、更高效、更易用的字符串操作方式:
- 自动内存管理:string类自动管理内存,使用者不需要关心内存分配和释放的问题,极大地降低了内存泄漏和缓冲区溢出的风险。
- 丰富的成员函数:string类内置了大量的成员函数,如append, find, replace, substr等,使得字符串的处理更为方便和直观。
- 动态大小:string对象可以根据需要动态增长和缩小,无需预先声明最大容量,这一点与静态大小的C语言字符数组形成鲜明对比。
- 操作符重载和迭代器支持:string类重载了多个操作符(如+、=等),支持迭代器,使得字符串操作更符合C++的对象操作习惯,也支持现代C++中的范围for循环和算法库函数。
- 兼容性和灵活性:尽管string以字节为单位进行操作,对于多字节字符集的支持可能有限,但它在大多数情况下能够兼容处理UTF-8等编码,尤其是在使用支持这些编码的库时。
string类
C++ 中提供了专门的头文件string(注意不是 string.h,这个是C风格字符串相关函数的头文件),来支持string类型。string类定义隐藏了字符串的数组性质,让我们可以像处理普通变量那样处理字符串。string对象和字符数组之间的主要区别是:可以将string对象声明为简单变量,而不是数组
标准库中的string类文档介绍
1.字符串时表示字符序列的类。
2.标准的字符串类提供了对此类对象的支持,其接口累死与标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3.string类是使用char(即作文他的字符类型,使用他的默认char_traits和分配器类型)
4.string类是basic_string模版类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作文basic_string分默认参数(根于更多漫步信息参考basic_string)。
5.注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(符UTF-8)的序列,这个类的缩影成员(如长度或大小)已经它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
字符串作为字符序列
string类本质上是一个用于表示字符序列的容器。它封装了字符数组的许多复杂操作,提供了一种更安全、更直观的方式来处理文本数据。用户不需要关心底层的字符数组和内存管理,所有这些都由string类自动处理。
接口与标准容器
string类的接口设计借鉴了标准库中STL容器的模式,例如vector和deque。这意味着string提供了类似于这些容器的多种成员函数,如迭代器的支持、元素访问、修改操作、容量查询等。这种设计使得string类即熟悉又易于使用对于已经熟悉其他C++标准容器的开发者。
string类的常用接口说明
string类对象的常见构造
(constructor)函数名称 | 功能说明 |
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(size_t n,char c) | string类对象中包含n个字符c |
string(const string& s) | 拷贝构造函数 |
string s1; // 构造空的string类对象s1
string s2("hello bit"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
string类对象的容量操作
函数名称 | 功能说明 |
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检查字符串释放为空串,是返回true,否返回false |
clear | 清空有效字符 |
reserve | 为字符串预留空间 |
resize | 将有效字符的个数改成n个,多出的空间用字符c填充 |
size()和length():
size_t size() const;
size_t length() const;
std::string str = "Hello, world!";
std::cout << "Length of string: " << str.length() << std::endl; // 输出 13
std::cout << "Size of string: " << str.size() << std::endl; // 同样输出 13
这两个函数都返回字符串当前的长度,即字符串中字符的数量,这两种并无相异,忧郁string出现的较早原因,当时没有STL其他容器,先出现了length,后来为了统一接口,与其他容器接口保持一致,因此出现了size。
capacity():
size_t capacity() const;
std::string str = "Test";
std::cout << "Capacity of string:" << str.capacity() << std::endl; // 输出分配的内存大小
capacity()函数返回为字符串分配的存储空间的大小,通常这个值大于或等于size()。这可以给出底层数组的空间大小,有助了解内存的使用情况。
empty:
bool empty() const;
std::string str;
std::cout << "Is the string empty? " << (str.empty() ? "Yes" : "No") << std::endl; // 输出 'Yes'
empty()函数检查字符串是否为空(即长度为0)。如果字符串为空,返回true,否则返回false。
clear:
void clear() noexcept;
std::string str = "Something";
str.clear(); // 清空字符串
std::cout << "String after clear: '" << str << "'" << std::endl; // 输出 ''
clear()函数删除字符串中的所有字符,使其长度变为0,这个函数不会改变容量的大小。
reserve:
void reserve(size_t n = 0);
std::string str;
str.reserve(100); // 为存储至少100个字符预分配内存
std::cout << "New capacity after reserve: " << str.capacity() << std::endl;
reserve()函数试图改变字符串的容量,即预先分配足够的内存来存储至少n个字符,避免多次增加字符串大小时的重复分配。
resize:
void resize(size_t n);
void resize(size_t n, char c);
std::string str = "Hello";
str.resize(10, 'x'); // "Helloxxxxx"
str.resize(3); // "Hel"
resize()函数更改字符串的长度,如果新的长度大于当前长度,新位置将于字符c填充(如果提供了c的话)。如果新的长度小于当前长度,多余的字符将被截断,空间大小不会被改变。
string类的访问及遍历操作
在std::string类中,访问和遍历字符串的方法包括使用下标操作符、迭代器和范围for循环。这些方法提供了灵活的方式来访问和遍历字符串中的字符。
函数名称 | 功能说明 |
operator[] | 返回pos位置的字符,const string类对象调用 |
begin + end | begin获取一个字符的迭代器+end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | 反向迭代器rbegin获取一个字符的迭代器+rend获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简介的范围for的新遍历方式 |
operator[]:
std::string str = "Hello, world!";
char ch1 = str[0]; // 访问第一个字符,ch1 = 'H'
char ch2 = str[7]; // 访问第八个字符,ch2 = 'w'
std::cout << "First character: " << ch1 << std::endl;
std::cout << "Eighth character: " << ch2 << std::endl;
// 修改字符串中的字符
str[5] = '!';
std::cout << "Modified string: " << str << std::endl; // 输出 "Hello! world!"
下标操作符operator[]允许你访问字符串中的特定位置的字符,这种访问方式是随机访问,时间复杂度为O(1)。
迭代器:
std::string str = "Hello";
std::cout << "Characters in string: ";
// 使用正向迭代器遍历字符串
for (auto it = str.begin(); it != str.end(); ++it) {
std::cout << *it << ' ';
}
std::cout << std::endl;
// 使用反向迭代器遍历字符串
std::cout << "Characters in reverse: ";
for (auto rit = str.rbegin(); rit != str.rend(); ++rit) {
std::cout << *rit << ' ';
}
std::cout << std::endl;
迭代器提供了一种方式按顺序访问容器中的每个元素。std::string支持正向和反向迭代器
范围for:
std::string str = "world";
std::cout << "Characters in string using range-for: ";
for (char c : str) {
std::cout << c << ' ';
}
std::cout << std::endl;
// 修改字符串中的字符(需要使用引用)
for (char &c : str) {
c = toupper(c); // 将字符转换为大写
}
std::cout << "Modified string: " << str << std::endl; // 输出 "WORLD"
范围for循环是C++11引入的一个特性,它允许简介的变量容器中的所有元素,在使用范围for循环遍历std::string时,可以直接访问每个字符。
string类对象的修改操作
类提供了多种方法来修改字符串的内容,这些方法包括添加、删除、插入和替换字符等操作,下面这些方法的详细介绍和使用实例。
函数名称 | 功能说明 |
push_back | 在字符串后尾插入字符c |
append | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
c_str | 返回c格式字符串 |
find + npos | 从字符串pos位置开始往后开始找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往后开始找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
push_back:
void push_back(char c);
std::string str = "Hello";
str.push_back('!');
std::cout << str << std::endl; // 输出 "Hello!"
push_back将一个字符追加到字符串的末尾。
append:
string& append(const string& str);
string& append(const string& str, size_t subpos, size_t sublen);
string& append(const char* s);
string& append(const char* s, size_t n);
string& append(size_t n, char c);
std::string str = "Hello";
str.append(" world", 6); // 附加了 " world" 中的前6个字符
std::cout << str << std::endl; // 输出 "Hello world"
append方法添加字符串、字符数组或多个相同字符到现有字符串的末尾。
operator+=:
void operator+=(const string& str);
void operator+=(char c);
void operator+=(const char* s);
std::string str = "Hello";
str += ", world!";
std::cout << str << std::endl; // 输出 "Hello, world!"
operator+=用于将字符串、字符或者c字符串附加到现有的std::string对象上。
c_str:
int main()
{
string s1("hello world");
cout << s1.c_str() << endl;
cout << s1.data() << endl;
return 0;
}
c_str返回string类中存储字符串的字符指针,在C语言中字符串是以'\0'结尾的一些字符的集合。但在C++中,string类不以'\0'结尾,而是根据有效空间的大小结束。本质作用是:将const string* 类型转化为const char* 类型。
find和npos:
// 从string拷贝给字符数组
// size_t copy (char* s, size_t len, size_t pos = 0) const;
char arr[] = "hello world";
string s1("xxxxxxxxxxxxxxxx");
s1.copy(arr, 6, 2);
cout << s1 << endl;
// 寻找某个字符串的起始位置
// size_t find (const string& str, size_t pos = 0) const;
string tmp("abc");
string s2("abbadabcdeabcd");
size_t pos1 = s2.find(tmp, 0);
// 从后往前找
//size_t rfind (const string& str, size_t pos = npos) const;
size_t pos2 = s2.rfind(tmp, s2.size() - 1);
cout << pos1 << endl;
cout << pos2 << endl;
find有很多函数重载,学会一种就可以了,size_t find(const string& s, int pos = 0);s是需要查找的字符串,pos是从那个位置查找,如果未找到则返回npos(npos表示size_t 的最大值)。
rfind:
string s1("fghasdabc");
cout << s1.rfind('a') << endl;
string s2("abc");
cout << s1.rfind(s2) << endl;
cout << s1.rfind('e') << endl;
rfind和find功能是一样的,不过一个是往前找,一个向后找的区别。
substr:
string s1("Hello World");
cout << s1 << endl;
string s2(s1.substr(0, 5));
cout << s1 << endl;
cout << s2 << endl;
在string中的pos位置截取n个字符,然后返回。
string类的深拷贝与浅拷贝问题
浅拷贝:
在类中如果用户没有显示实现而是由编译器自动生成的成员函数叫做默认成员函数,这样的成员函数有六个。默认成员函数中的拷贝构造函数和赋值运算符重载函数会以逐字节的方式将原对象的内容原封不动的拷贝或赋值给新的对象,如果对象中管理资源,最后就会导致多个对象共用一份资源,当其中一个对象销毁时会将该资源释放掉,其他对象再想操作该资源时就会发生访问违规,这便是浅拷贝。
namespace qx
{
class string
{
public:
string(const char* s="")
{
if (nullptr == s)
{
cout << "string():false" << endl;
return;
}
char* ptr = new char[strlen(s) + 1];
strcpy(_str, s);
}
//........
private:
char* _str;
};
}
int main()
{
qx::string s1("hello world!");
qx::string s2(s1);
return 0;
}
由于类对象销毁时调用析构函数,会先将s2对象销毁,再调用析构进行销毁s1对象,但是因为拷贝构造是浅拷贝,导致s1和s2指向同一块内存空间,销毁掉s2之后内存空间被释放,s1找不到改内存空间,最终违规访问。
深拷贝:
如果一个类中涉及到资源管理,其拷贝构造函数、赋值运算符重载以及析构函数都必须要显示给出,一般情况都是按照深拷贝方式提供。
//显示实现拷贝构造
string(const string& str)
:_str(nullptr)
{
string strTmp(str._str);
swap(_str, strTmp._str);
}
//显示实现赋值运算符重载
string& operator=(string& str)
{
if (this != &str)
{
string strTmp(str);
swap(_str, strTmp._str);
}
}
//显示实现析构函数
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
深拷贝即给每个对象独立分配资源,保证多个对象之间不会因为共享资源而造成多次释放导致程序崩溃的问题。
string总结
深入探讨C++标准库中的string类,从其构造函数、容量操作函数,到访问及遍历操作,以及类对象的修改操作。string类作为C++中处理字符串的核心工具,提供了丰富的接口来高效、安全地管理和操作字符串。掌握这些功能不仅可以提高编程效率,还能帮助开发者编写更加健壮和可维护的代码,有效地处理现代软件开发中的文本数据挑战。