【C++基础】std::string详解
std::string 是 C++ 标准库提供的用于处理字符串的类。它在 头文件中定义。std::string 提供了一种灵活、高效的字符串表示方式,相比于 C 语言中的字符串表示(使用字符数组或指针),std::string 更易于使用,更安全,并且提供了许多便捷的操作。
一、底层实现
std::string
类的底层实现通常是一个动态分配的字符数组(即堆上的内存),并且该数组的长度可以动态地增长和收缩以适应字符串的长度变化。
1.1字符数组
std::string
对象通常包含了一个指向堆上分配的内存的指针,用于存储较长的字符串。较短的字符串可能会直接存在对象本身的内部缓冲区。有些实现还会使用一个独立的缓冲区来存储字符串内容。
std::strig str = "123";
const char* cstr = str.c_str();
// 输出 C 风格字符串
std::cout << "C-style string: " << cstr << std::endl;
无论std::string底层是怎么优化的,或者string本身是存在堆区还是栈区。c_str
总是可以拿到内部存储的C风格字符串,即字符串的字符数组的首地址。
使用 c_str()
函数时需要注意以下几点:
1、 生命周期问题
c_str()
返回的 C 风格字符串指针指向 std::string
内部的字符数组,这个指针的有效性与 std::string
对象的生命周期相关联。如果在 std::string
对象被销毁之后仍然使用 c_str()
返回的指针,将导致未定义的行为。因此,在使用 c_str()
返回的指针时,要确保对应的 std::string
对象仍然有效。
2、 只读属性
c_str()
返回的指针指向的是一个只读的 C 风格字符串,这意味着不能通过这个指针来修改 std::string
对象中字符串的内容。如果尝试修改 c_str()
返回的指针指向的字符数组内容,将导致未定义的行为。
3、 空指针检查
在某些情况下,c_str()
可能会返回一个空指针(nullptr
),例如当 std::string
对象为空时。在使用 c_str()
返回的指针之前,应该进行空指针检查,以避免出现空指针解引用错误。
4、 数据不稳定
由于 c_str()
返回的指针指向的字符数组可能随着 std::string
对象的操作而被修改或重新分配,因此在调用 c_str()
之后,如果对 std::string
进行了可能导致重新分配内存的操作(如添加字符、删除字符、修改字符串内容等),那么之前返回的指针就可能会失效。
如果需要用到c风格的字符串,建议拷贝出来使用。
#include <iostream>
#include <string>
#include <cstring>
int main() {
std::string str = "Hello";
const char* cstr = str.c_str();
// 拷贝到另一个字符数组
char buffer[20]; // 假设足够大
strcpy_s(buffer, sizeof(buffer), cstr);
// 使用拷贝后的字符数组
std::cout << buffer << std::endl;
return 0;
}
1.2长度和容量管理
除了存储字符数组外,std::string
还会记录字符串的当前长度和缓冲区的容量。使用length()
或 size()
方法,获得字符串中字符的数量。
长度表示字符串当前实际包含的字符数,而容量表示分配给字符数组的内存空间大小。当字符串的长度超过容量时,std::string
会重新分配更大的内存空间,并将原有的字符复制到新的内存位置。
大多数C++标准库会采用指数级增长,每次扩展缓冲区大小都是前一次大小的倍数,可以保障操作的时间复杂度为常数级,有效避免频繁地调整缓冲区大小带来的性能损耗,减少内存碎片化的问题。另外还有线性增长策略,每次扩展缓冲区大小都是固定值,这样扩展速度比较慢,但避免了大量的内存浪费情况。
#include <iostream>
#include <string>
using namespace std;
int main()
{ // 使用的gcc version 13.2.0 (GCC)默认的capacity是15,线性增长。
string str = "";
cout << str.size() << endl; // 0
cout << str.capacity() << endl; // 15
str += "1234654646416456544645";
cout << str.size() << endl; // 22
cout << str.capacity() << endl; // 30
return 0;
}
1.3 动态内存分配
std::string
类内部会自动使用 new
和 delete
,或者动态内存分配和释放函数(如 malloc()
和 free()
),来管理字符串的字符数组,封装了内存管理的细节,不用担心内存管理的问题。
向 std::string
对象赋值一个 C 风格字符串或者另一个 std::string
对象时,std::string
类会负责分配足够大的内存来存储字符串的内容,并将字符串复制到这块内存中。这个内存通常是在堆上动态分配的,因此你不需要担心栈空间的限制。
当 std::string
对象的生命周期结束时,它会自动释放所分配的内存。不需要手动调用 delete
或 free()
来释放字符串的内存,因为 std::string
类会在对象销毁时自动调用相应的析构函数来释放内存。
1.4 空终止符
与 C 语言中的字符串类似,std::string
的底层实现也会在字符数组的末尾添加一个空终止字符(‘\0’),以表示字符串的结束。这样可以使得 std::string
类的接口与标准 C 字符串函数兼容。但是std::string
的size()
和length()
不会统计('\0')
。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123";
cout << str.size() << endl; // 3
cout << str.length() << endl; // 3
return 0;
}
二、成员函数
std::string
类提供了公开的成员函数来访问内部的数据。直接使用str.
调用
append()
:在字符串末尾添加字符或字符串。
std::string str = "1";
std::cout<<str<<std::endl; // 1
str.append("2");
std::cout<<str<<std::endl; // 12
assign()
:用新的字符串替换原有内容。
std::string str = "1";
std::cout<<str<<std::endl; // 1
str.assign("2");
std::cout<<str<<std::endl; // 2
at()
:返回指定位置的字符。
std::string str = "123";
std::cout<<str.at(1)<<std::endl; // 2
back()
:返回最后一个字符。
std::string str = "123";
std::cout<<str.back()<<std::endl; // 3
begin()
:返回指向字符串第一个字符的迭代器。end()
:::返回自字符串最后一个字符后一个位置的迭代器cbegin()
:返回指向字符串第一个字符的常量迭代器。cend()
:返回指向字符串最后一个字符之后的位置的常量迭代器。crbegin()
:返回指向字符串最后一个字符的常量逆向迭代器。crend()
:返回指向字符串第一个字符之前的位置的常量逆向迭代器。- .
rbegin()
:返回指向字符串最后一个字符的逆向迭代器。 - .
rend()
:返回指向字符串第一个字符之前的位置的逆向迭代器。
std::string str = "123";
for (auto it = str.begin(); it != str.end(); it++) {
std::cout<<*it<<std::endl; // 1 2 3
}
for (auto it = str.cbegin(); it != str.cend(); it++) {
std::cout<<*it<<std::endl; // 1 2 3
}
for (auto it = str.rbegin(); it != str.rend(); it++) {
std::cout<<*it<<std::endl; // 3 2 1
}
capacity()
:返回字符串当前的容量。
std::string str = "123";
std::cout<<str.capacity()<<std::endl; // 15
- .
clear()
:清空字符串。
str.clear();
compare()
:比较两个字符串。(ASCII字典顺序)
std::string str1 = "1";
std::string str2 = "2";
std::string str3 = "3";
std::cout<<str2.compare(str1)<<std::endl; // > 1
std::cout<<str1.compare(str2)<<std::endl; // < -1
std::cout<<str2.compare(str2)<<std::endl; // = 0
copy()
:复制字符到字符数组中。
std::string str = "123";
char c[20];
str.copy(c,3,0);
c[3] = '\0';
std::cout<<str; // 123
data()
:返回指向字符串第一个字符的指针。c_str()
:返回指向字符串第一个字符的指针。
std::string str = "123";
char * c1 = str.data();
const char * c2 = str.c_str();
std::cout<<c1<<std::endl; // 123
std::cout<<c2<<std::endl; // 123
empty()
:检查字符串是否为空。
std::string str = "123";
std::cout<<str.empty()<<std::endl; // 0
str.assign("");
std::cout<<str.empty()<<std::endl; // 1
erase()
:删除字符串中的指定位置或指定范围的字符
# test erase :
string str = "0123456789";
cout << str << endl; // 0123456789
str.erase(2); // 删除索引2及以后的字符
cout << str << endl; // 01
str = "0123456789";
str.erase(2,3); // 删除从索引2起的3个字符
cout << str << endl; // 0156789
find()
:查找子字符串。
std::string str1 = "123";
if (str1.find('2') != std::string::npos) {
std::cout<<"find 2"<<std::endl; // find 2
}
if (str1.find("4") != std::string::npos) {
std::cout<<"find 4"<<std::endl;
}
front()
:返回第一个字符。
std::string str = "123";
std::cout<<str.front(); // 1
insert()
:在指定位置插入字符或字符串。
std::string str = "0123456";
str.insert(0,"a");
std::cout<<str<<std::endl; // a0123456
const char* str1 = "bcd";
str.insert(2,str1);
std::cout<<str<<std::endl; // a0bcd123456
str.insert(2,str1,2);
std::cout<<str<<std::endl; // a0bcbcd123456
str.insert(4,2,'Q');
std::cout<<str<<std::endl; // a0bcQQbcd123456
length()
或size()
:返回字符串中字符的数量。
std::string str = "0123456";
std::cout<<str.size()<<std::endl; // 7
std::cout<<str.length()<<std::endl; // 7
pop_back()
:删除最后一个字符。
std::string str = "0123";
std::cout<<str<<std::endl; // 0123
str.pop_back();
std::cout<<str<<std::endl; // 012
push_back()
:在字符串末尾添加一个字符。
std::string str = "0123";
std::cout<<str<<std::endl; // 0123
str.push_back('4');
std::cout<<str<<std::endl; // 01234
replace()
:替换字符串中的部分内容。
std::string str = "012345";
std::cout<<str<<std::endl; // 012345
str.replace(2,3,"ABC");
std::cout<<str<<std::endl; // 01ABC5
reserve()
:为字符串预留空间。
std::string str = "012345";
std::cout<<str.capacity()<<std::endl; // 15
str.reserve(50);
std::cout<<str.capacity()<<std::endl; // 50
resize()
:更改字符串的大小。
std::string str = "012345";
str.resize(3);
std::cout<<str.size()<<std::endl; // 3
std::cout<<str<<std::endl; // 012
shrink_to_fit()
:释放多余的内存空间。
std::string str = "012345";
std::cout<<str.capacity()<<std::endl; // 15
str.shrink_to_fit();
std::cout<<str.capacity()<<std::endl; // 15
str.reserve(50);
std::cout<<str.capacity()<<std::endl; // 50
str.shrink_to_fit();
std::cout<<str.capacity()<<std::endl; // 15
substr()
:提取子字符串。
std::string str = "012345";
std::cout<<str.substr(2,3)<<std::endl; // 234
swap()
:交换字符串内容
std::string str1 = "123";
std::string str2 = "456";
std::cout<<str1<<std::endl; // 123
std::cout<<str2<<std::endl; // 456
str1.swap(str2);
std::cout<<str1<<std::endl; // 456
std::cout<<str2<<std::endl; // 123
三、构造函数与析构函数
在 C++ 中,std::string
类的构造函数和析构函数负责创建和销毁字符串对象,并管理其内存资源。
构造函数:
std::string
类具有多个构造函数,可以用不同的方式创建字符串对象:
1、 默认构造函数:创建一个空字符串对象。
std::string str; // 创建一个空字符串对象
2、 使用 C 风格字符串构造:使用以 null 结尾的字符数组来初始化字符串对象。
const char* cstr = "Hello";
std::string str(cstr); // 使用 C 风格字符串来构造字符串对象
3、 使用字符和长度构造:使用字符数组和长度来初始化字符串对象。
const char chars[] = {'H', 'e', 'l', 'l', 'o'};
std::string str(chars, 5); // 使用字符数组和长度来构造字符串对象
4、使用重复字符构造:使用指定的字符重复指定次数来构造字符串对象。
std::string str(5, 'H'); // 使用指定的字符重复指定次数来构造字符串对象
5、 使用另一个字符串构造:使用另一个字符串对象来初始化当前字符串对象。
std::string str1 = "Hello";
std::string str2(str1); // 使用另一个字符串对象来构造当前字符串对象
析构函数:
std::string
类的析构函数负责在对象销毁时释放其内存资源。通常情况下,析构函数会自动调用,但如果需要手动释放内存,也可以使用 std::string
对象的 clear()
函数来清空字符串,并释放其内存资源。
std::string str = "Hello";
str.clear(); // 清空字符串并释放内存资源
总的来说,std::string
类的构造函数用于创建字符串对象,而析构函数则用于销毁对象并释放内存资源,从而确保在对象生命周期结束时资源能够得到正确地释放。
四、运算符重载
在C++中,std::string
类重载了一系列运算符,使得对字符串的操作更加方便和直观。以下是一些常用的字符串运算符重载:
1、 赋值运算符 (=
):用于将一个字符串赋值给另一个字符串。
std::string str1 = "Hello";
std::string str2;
str2 = str1; // 将 str1 赋值给 str2
2、 加法运算符 (+
):用于将两个字符串连接起来。
std::string str1 = "Hello";
std::string str2 = "World";
std::string result = str1 + str2; // 将 str1 和 str2 连接起来赋值给 result
3、 比较运算符 (==
, !=
, <
, >
, <=
, >=
):用于比较两个字符串的大小关系。(ASCII字典顺序)
std::string str1 = "Hello";
std::string str2 = "World";
if (str1 == str2) {
// 字符串相等
} else if (str1 < str2) {
// str1 小于 str2
} else {
// str1 大于 str2
}
4、 下标运算符 ([]
):用于访问字符串中的单个字符。
std::string str = "Hello";
char ch = str[0]; // 访问字符串的第一个字符
5、 复合赋值运算符 (+=
):用于将一个字符串连接到另一个字符串的末尾。
std::string str = "Hello";
str += " World"; // 在字符串末尾添加 " World"
6、 输入输出运算符 (<<
, >>
):用于将字符串输入到流中或从流中输出字符串。
std::string str;
std::cin >> str; // 从标准输入流中读取字符串
std::cout << str; // 输出字符串到标准输出流
这些运算符重载使得对字符串的操作更加直观和方便,使得字符串的处理更类似于基本数据类型的操作。
五、字符串的性能问题
在处理大量字符串时,可能会面临一些性能问题,主要涉及以下几个方面:
1、 内存分配和释放开销:字符串的动态内存分配和释放可能会导致性能下降。频繁地创建、销毁和修改字符串对象可能会引起内存碎片问题,增加内存分配和释放的开销。为了减少这种开销,可以考虑使用预留空间的方式避免频繁的内存重新分配。
#include <iostream>
#include <string>
int main() {
std::string str;
// 预留10个字符的空间
str.reserve(10);
// 向字符串中添加字符
str += "Hello";
// 打印字符串的长度和容量
std::cout << "Length: " << str.length() << std::endl; // 输出:5
std::cout << "Capacity: " << str.capacity() << std::endl; // 输出:10
return 0;
}
2、 拷贝和赋值开销:字符串的拷贝和赋值操作可能会涉及大量的内存复制,特别是对于较长的字符串。频繁地执行这些操作会影响程序的性能。可以使用引用或指针来避免不必要的拷贝,以及使用移动语义(move semantics)来避免不必要的内存复制。
// 使用指针传递字符串参数
void processString1(const std::string* strPtr) {
// 处理字符串
}
// 使用引用传递字符串参数
void processString2(const std::string& strRef) {
// 处理字符串
}
std::string str1 = "Hello";
std::string str2 = std::move(str1); // 使用移动语义将 str1 移动到 str2
// 函数接受一个字符串参数,使用移动语义
void processString3(std::string&& str) {
// 处理字符串
std::cout << "Processing string: " << str << std::endl;
}
int main() {
processString1("Hello");
processString2("Hello");
processString3("Hello");
return 0;
}
3、 字符串连接的性能问题:当需要将多个字符串连接成一个较长的字符串时,多次执行字符串连接操作可能会导致性能下降。这是因为+
运算符在每次连接都需要重新分配内存,并复制原始字符串的内容。为了提高性能,可以考虑使用 std::string
的 append()
函数或 +=
运算符,以及使用 std::stringstream
类型来代替直接连接字符串。
// 直接连接字符串指的是使用 `+` 运算符将两个字符串连接起来。
std::string str1 = "Hello";
std::string str2 = "World";
std::string result = str1 + str2;
append()
函数和 +=
运算符都不会引起额外的内存分配,因为它们在连接字符串时可以利用已有的内存空间。
也可以使用std::stringstream类,它在内存中创建一个字符串流(string stream)
#include <iostream>
#include <sstream>
int main() {
// 格式化输出
std::stringstream ss;
int num = 123;
ss << "The number is: " << num;
std::string result = ss.str(); // 获取格式化后的字符串
std::cout << result << std::endl; // 输出 "The number is: 123"
// 解析字符串
std::string input = "3.14 42 Hello";
std::stringstream ss2(input);
double pi;
int answer;
std::string greeting;
ss2 >> pi >> answer >> greeting; // 解析字符串中的数据
std::cout << "Pi: " << pi << ", Answer: " << answer << ", Greeting: " << greeting << std::endl;
return 0;
}
更多推荐
所有评论(0)