文章目录
- 一、string类的发展历史
- 二、string的使用
- 三、string的练习
- 四、总结
一、string类的发展历史
在C++ 的早期版本中,处理字符串主要依赖于 C 风格的字符数组。但这种方式存在诸多不便,如手动管理内存、容易出现缓冲区溢出等问题。随着 C++ 标准的不断演进,string 类逐渐成为处理字符串的更安全和便捷的方式。
在 C++98 标准中,引入了 std::string 类,它提供了自动内存管理、丰富的操作方法,如字符串连接、查找、替换等。这使得字符串的操作更加直观和安全。例如,在 C 风格中连接两个字符串可能需要手动分配足够的内存并复制字符,而使用 std::string 可以简单地通过 + 运算符实现。
C++11 标准进一步增强了 string 的功能,例如提供了更高效的移动语义,使得字符串的传递和赋值更加高效。
C++17 及之后的标准也对 string 的性能和功能进行了一些优化和改进。
总的来说,C++ 中 string 的发展历史是一个不断改进和完善的过程,旨在为开发者提供更强大、更安全和更高效的字符串处理工具。
二、string的使用
(1).string类对象的常见构造
void Teststring()
{
string s1; // 构造空的string类对象s1
string s2("hello Tu"); // 用C格式字符串构造string类对象s2
string s3(s2); // 拷贝构造s3
}
(2).string类对象的容量操作
void Teststring1()
{
// 注意:string类对象支持直接用cin和cout进行输入和输出
string s("hello, TU!!!");
cout << s.size() << endl;
cout << s.length() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s.clear();
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
// “aaaaaaaaaa”
s.resize(10, 'a');
cout << s.size() << endl;
cout << s.capacity() << endl;
// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
// "aaaaaaaaaa\0\0\0\0\0"
// 注意此时s中有效字符个数已经增加到15个
s.resize(15);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
// 将s中有效字符个数缩小到5个
s.resize(5);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << s << endl;
}
-
size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一
致,一般情况下基本都是用size()。 -
clear()只是将string中有效字符清空,不改变底层空间大小。
-
resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字
符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的
元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大
小,如果是将元素个数减少,底层空间总大小不变。 -
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于
string的底层空间总大小时,reserver不会改变容量大小。
(3)string类对象的访问及遍历操作
void Teststring2()
{
string s("hello TU");
// 3种遍历方式:
// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
// 另外以下三种方式对于string而言,第一种使用最多
// 1. for+operator[]
for (size_t i = 0; i < s.size(); ++i)
cout << s[i] << endl;
// 2.迭代器
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << endl;
++it;
}
// string::reverse_iterator rit = s.rbegin();
// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
auto rit = s.rbegin();
while (rit != s.rend())
cout << *rit << endl;
// 3.范围for
for (auto ch : s)
cout << ch << endl;
}
(4)string类对象的修改操作
// 测试string:
// 1. 插入(拼接)方式:push_back append operator+=
// 2. 正向和反向查找:find() + rfind()
// 3. 截取子串:substr()
// 4. 删除:erase
void Teststring3()
{
string str;
str.push_back(' '); // 在str后插入空格
str.append("hello"); // 在str后追加一个字符"hello"
str += 'b'; // 在str后追加一个字符'b'
str += "it"; // 在str后追加一个字符串"it"
cout << str << endl;
cout << str.c_str() << endl; // 以C语言的方式打印字符串
// 获取file的后缀
string file("string.cpp");
size_t pos = file.rfind('.');
string suffix(file.substr(pos, file.size() - pos));
cout << suffix << endl;
// npos是string里面的一个静态成员变量
// static const size_t npos = -1;
// 取出url中的域名
string url("http://www.cplusplus.com/reference/string/string/find/");
cout << url << endl;
size_t start = url.find("://");
if (start == string::npos)
{
cout << "invalid url" << endl;
return;
}
start += 3;
size_t finish = url.find('/', start);
string address = url.substr(start, finish - start);
cout << address << endl;
// 删除url的协议前缀
pos = url.find("://");
url.erase(0, pos + 3);
cout << url << endl;
}
- 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般
情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。 - 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
(5) string类非成员函数
getline 函数的一般使用形式
#include <iostream>
#include <string>
int main() {
std::string str;
std::cout << "请输入一行字符串: ";
std::getline(std::cin, str);
std::cout << "您输入的字符串是: " << str << std::endl;
return 0;
}
在上述代码中,std::getline(std::cin, str) 表示从标准输入中读取一行内容,并将其存储到 str 字符串中。
getline 函数的一个重要特点是它可以读取包含空格的字符串,而不像 cin >> str 那样在遇到空格时就停止读取。
例如,如果用户输入 “Hello World” ,使用 cin >> str 时,str 只会被赋值为 “Hello” ,而使用 getline 则可以完整地将 “Hello World” 存储到 str 中。
relational operators函数的一般使用形式
#include <iostream>
#include <vector>
#include <string>
int main ()
{
std::string foo = "alpha";
std::string bar = "beta";
if (foo==bar) std::cout << "foo and bar are equal\n";
if (foo!=bar) std::cout << "foo and bar are not equal\n";
if (foo< bar) std::cout << "foo is less than bar\n";
if (foo> bar) std::cout << "foo is greater than bar\n";
if (foo<=bar) std::cout << "foo is less than or equal to bar\n";
if (foo>=bar) std::cout << "foo is greater than or equal to bar\n";
return 0;
}
这段 C++ 代码主要是对两个字符串 foo(值为 “alpha”)和 bar(值为 “beta”)进行关系比较,并根据比较结果输出相应的信息。
由于 “alpha” 和 “beta” 在字典序上不同,且 “alpha” 小于 “beta”,所以输出结果为:
foo and bar are not equal
foo is less than bar
foo is less than or equal to bar
(6)vs和g++下string结构的说明
1.vs下string的结构
string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:
- 当字符串长度小于16时,使用内部固定的字符数组来存放
- 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{ // storage for small buffer or pointer to larger one
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内
部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
- 其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
- 最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。
2.g++下string的结构
G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:
- 空间总大小
- 字符串有效长度
- 引用计数
struct _Rep_base
{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
- 指向堆空间的指针,用来存储字符串。
三、string的练习
1.反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
我们使用两个指针,一个指向字符串的开头 left,一个指向字符串的末尾 right。然后通过一个循环,不断交换这两个指针所指向的字符,直到两个指针相遇或者交错。这样就实现了字符串的反转,而且没有使用额外的存储空间,只在原字符串上进行操作,空间复杂度为 O(1)。
class Solution {
public:
void reverseString(vector<char>& s) {
int left=0,ringt=s.size()-1;
while(left<ringt)
{
swap(s[left++],s[ringt--]);
}
}
};
2.字符串相加
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。
你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。
思路如下:
我们从两个字符串的末尾开始逐位相加。使用两个指针 i 和 j 分别指向 num1 和 num2 的最后一位。同时设置一个进位 carry 初始化为 0 。
在每次循环中,如果指针还没有超出字符串范围,就取出当前位的数字进行相加,再加上进位。计算出当前位的和后,得到新的进位,并将当前位的数字(取模 10 )添加到结果字符串中。
如果指针超出了字符串范围,就认为当前位为 0 。
最后,因为我们是从低位向高位计算并添加到结果字符串的,所以需要将结果字符串反转,得到最终正确的结果。
class Solution {
public:
string addStrings(string num1, string num2) {
int end1=num1.size()-1,end2=num2.size()-1;
int next =0;
string str;
while(end1>=0||end2>=0)
{
int val1= end1 >= 0 ? num1[end1--]-'0':0;
int val2= end2 >= 0 ? num2[end2--]-'0':0;
int sum =val1+val2+next;
next = sum / 10;
sum = sum % 10;
str.insert(str.begin(),'0'+sum);
}
if(next==1)
{
str.insert(str.begin(),'1');
}
return str;
}
};
3.字符串中的第一个唯一字符
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
1.int cont[26]={0};:创建了一个大小为 26 的整数数组 cont,用于统计每个小写字母出现的次数,并初始化为 0。
2.for(auto ch :s):这是一个范围 for 循环,遍历字符串 s 中的每个字符 ch。
cont[ch-‘a’]++;:通过 ch - ‘a’ 将字符转换为对应的索引(例如,‘a’ - ‘a’ = 0,‘b’ - ‘a’ = 1 等),然后将对应索引位置的计数加 1,从而统计每个字符出现的次数。
3.第二个 for 循环:再次遍历字符串 s。
4.if(cont[s[i]-‘a’]==1):对于当前位置的字符,通过同样的方式将其转换为索引,然后检查其在 cont 数组中的计数是否为 1。如果是,则返回当前位置的索引 i。
如果整个字符串遍历完都没有找到计数为 1 的字符,就返回 -1。
这种方法的时间复杂度为 O(n),空间复杂度为 O(26),主要是通过一次遍历统计字符出现次数,再一次遍历查找第一个不重复的字符,避免了使用复杂的数据结构,提高了效率。
class Solution {
public:
int firstUniqChar(string s) {
int cont[26]={0};
for(auto ch :s)
{
cont[ch-'a']++;
}
for(size_t i=0;i<s.size();i++)
{
if(cont[s[i]-'a']==1)
{
return i;
}
}
return -1;
}
};
四、总结
std::string 是 C++ 标准库中用于处理字符串的重要类。
内存管理:
自动管理内存,无需手动分配和释放,避免了内存泄漏和缓冲区溢出的风险。动态调整字符串的存储空间,以适应字符串内容的变化。
丰富的操作:
支持字符串的连接,通过 + 运算符可以方便地将两个字符串拼接在一起。提供查找、替换、比较等操作,如 find() 、 replace() 、 compare() 。可以获取字符串的长度、子串等。
高效性:
在 C++11 及以后的标准中,利用移动语义提高了字符串对象的传递和赋值效率。
迭代访问:可以使用迭代器遍历字符串中的每个字符。