C++标准库类——string类

引言

在c++中,string类的引用极大地简化了字符串的操作和管理,相比 C 风格字符串(char*或char[]),`std::string` 提供了更高效和更安全的字符串操作。接下来让我们一起来深入学习string类吧!

1.string 的构造与初始化

string类 的几种常见的构造方式:
    - 默认构造:创建空字符串。
    - 通过 C 风格字符串初始化:`std::string s("Hello");`
    - 拷贝构造:从另一个字符串构造。
    - 指定字符和长度构造:`std::string s(5, 'A');

using namespace std;   
   string s1; // 空字符串
   string s2("Hello"); // 通过 C 风格字符串初始化
   string s3(s2); // 复制构造
   string s4(5, 'A'); // 重复字符构造 "AAAAA"`

2.string的迭代器支持

在 C++ 中,迭代器(Iterator)是一种抽象概念,用来遍历容器中的元素。std::string 作为标准库中的容器类,支持迭代器,使得我们可以像操作其他 STL 容器(如 vector、list 等)一样,使用迭代器来遍历和操作字符串。

(1)什么是迭代器?

迭代器可以理解为一个指针,它指向容器中的某个元素,通过迭代器我们可以逐个访问容器中的元素。不同于普通指针,迭代器具备更强的泛化性,能适用于不同的容器。对 std::string 来说,迭代器可以用于遍历字符串中的每个字符。

(2)std::string 的迭代器类型

string 提供了几种常见的迭代器类型:

string::iterator
正向迭代器,用于从字符串的起始位置向末尾遍历。
string::const_iterator
常量正向迭代器,和 iterator 类似,但它不允许修改迭代器指向的元素。

string::reverse_iterator
反向迭代器,用于从字符串末尾向前遍历。
string::const_reverse_iterator
常量反向迭代器,反向遍历并且不允许修改元素。

(3)常用的迭代器操作

begin():返回指向字符串第一个字符的正向迭代器。
end():返回指向字符串末尾之后位置的正向迭代器(即超尾迭代器,不指向实际元素)。

rbegin():返回指向字符串最后一个字符的反向迭代器。
rend():返回指向字符串第一个字符之前位置的反向迭代器。

cbegin() 和 cend():常量正向迭代器,保证字符串内容不可修改。

crbegin() 和 crend():常量反向迭代器。
#include <iostream>
#include <string>
using namespace std;


int main() {
    string str1 = "Hello World";

    // 使用正向迭代器遍历字符串
    string::iterator it1 = str1.begin();
    while (it1 != str1.end()) 
    {
        cout << *it1 << " ";  // 输出每个字符
        ++it1;
    }
    cout << endl;

    // 通过正向迭代器修改字符串
    string::iterator it2 = str1.begin();
    while (it2 != str1.end()) 
    {
        (*it2)++;//所有字符替换为下一个
        cout << *it2 << " ";// 输出修改后的字符串
        ++it2;
    }
    cout << endl;

    // 使用常量正向迭代器遍历字符串
    const string str2(str1);
    string::const_iterator it3 = str2.cbegin();
    while (it3 != str2.cend()) 
    {
        //*(it3)++;
        cout << *it3 << " ";  // 输出每个字符,但不能修改
        ++it3;
    }
    cout << endl;

    // 使用反向迭代器遍历字符串
    string::reverse_iterator rit = str1.rbegin();
    while (rit != str1.rend()) 
    {
        cout << *rit << " ";  // 反向输出字符串
        ++rit;
    }
    cout << endl;

    return 0;
}

3.常用成员函数

3.1.获取字符串长度

 - size()和 length()作用相同,都用于返回字符串的长度,返回类型都是size_t,

 - size_t本质上是无符号整数类型(unsigned integer type),专门用于表示 “对象大小” 或 “元素个数”,比如字符串长度、数组下标、容器大小等。。

#include <iostream>
using namespace std;

int main()
{
	string s = "Hello";
	cout << s.size() << endl;//5
	cout << s.length() << endl;//5
	return 0;
} 

两者没有功能上的区别,提供两个概念主要是为了与开发者的习惯保持一致,size() 更符合容器类的术语,length() 更符合处理字符串的自然语言概念。

3.2. 访问和修改字符串

std::string::at() 是 C++ 标准库中std::string类的成员函数,用于安全地访问字符串中的指定字符。它会根据提供的索引返回该索引位置的字符同时会进行越界检查,如果索引超出了字符串的范围,则会抛出 `std::out_of_range` 异常。

相比直接使用下标运算符 `[]`,`at()` 的优势在于它提供了边界检查,从而避免访问无效位置导致未定义行为。

 3.3.1函数原型

char& at(size_t pos);
const char& at(size_t pos) const;

- pos:要访问的字符的索引,size_t类型。
- 返回值:pos位置处的字符。如果at()被用于常量字符串,它返回const char&,否则返回 char&。

3.2.2.示例代码

#include <iostream>
using namespace std;

int main() {
	string str = "Hello, World!";

	// 使用 at() 访问字符串中的字符
	cout << "字符串的第一个字符:" << str.at(1) << endl; // 输出 'e'

	// 修改字符串中的字符
	str.at(7) = 'C'; // 将 'W' 修改为 'C'
	cout << "修改后的字符串:" << str << endl;

	try 
	{
		// 尝试访问超出范围的字符,抛出异常
		char ch = str.at(20);
		cout << ch << endl;
	}
	catch (const out_of_range& e) {
		// 捕获 out_of_range 异常
		cout << "Out of range error:" << e.what() << endl;
	}

	return 0;
}

 
3.3. 拼接字符串

在 C++ 中,拼接字符串的方式有很多,其中比较常用的方式包括使用 `+` 运算符(通过 `operator+` 重载实现)以及 `append()` 函数。虽然它们的作用都是拼接字符串,但它们有不同的使用场景和性能特点。

3.3.1.使用 + 运算符(operator+)

+ 运算符是 C++ 中常用的拼接字符串的方法。它通过重载 `operator+` 来实现两个字符串的拼接操作。

int main() 
{
	string str1 = "Hello";
	string str2 = "World";

	// 使用 + 运算符拼接字符串
	string result = str1 + ", " + str2 + "!";

	cout << "Result: " << result << endl; // 输出 "Hello, World!"

	return 0;
}

3.3.2.使用append()方法

append()是 std::string 类的成员函数,用于将另一个字符串或字符拼接到当前字符串的末尾。相比于 + 运算符,append() 更加灵活,允许指定拼接的范围,甚至拼接部分字符串。

string (1)	
string& append (const string& str);

substring (2)	
string& append (const string& str, size_t subpos, size_t sublen);

c-string (3)	
string& append (const char* s);

buffer (4)	
string& append (const char* s, size_t n);

fill (5)	
string& append (size_t n, char c);

range (6)	
template <class InputIterator>
   string& append (InputIterator first, InputIterator last);

initializer list(7)	
string& append (initializer_list<char> il);
#include <iostream>
using namespace std;

int main() 
{
	string str1 = "Hello";
	string str2 = "World";

	// 使用 append() 拼接字符串
	str1.append(", ").append(str2).append("!");//从str1的“,”开始拼接到str2的“!”
	cout << "Result: " << str1 << endl; // 输出 "Hello, World!"

	return 0;
}

 

3.3.3.+与append()的对比

1.可读性:
- + 运算符更加直观,拼接字符串时看起来像数学加法,代码简洁、易读,尤其是在拼接多个字符串时。
- append()在一些场景下也很方便,尤其是需要逐步对同一个字符串进行修改时。

2 .性能:
- + 运算符会创建临时字符串对象。在连续使用 + 运算符时,C++ 可能会创建多个临时对象,特别是当涉及多个拼接操作时,临时对象会占用额外的内存,影响性能。

 string result = str1 + str2 + str3;  // 可能生成多个临时对象

- append()则直接修改原字符串对象,不会生成临时对象,因此在多次拼接操作时性能更优。例如,如果你需要不断往同一个字符串中追加内容,`append()` 会比 `+` 更高效。

3. 灵活性
- + 运算符简单直观,但只支持完整字符串或字符的拼接,不能直接拼接部分字符串。
- append() 则可以指定拼接字符串的子串或部分字符,这在需要精细控制拼接内容时非常有用。
  示例:拼接部分字符串

  string str1 = "Hello";
  string str2 = "World!!!";
  
  // 拼接 str2 的前 5 个字符
  str1.append(str2, 0, 5);  // 拼接 "World"
  
  cout << str1 << endl;  // 输出 "HelloWorld"

3.3.4.append() 的其他用法

1. 拼接字符串:
str1.append(str2);  // 将 str2 拼接到 str1 后面
 
2. 拼接子串:
str1.append(str2, pos, len);  // 将 str2 中从位置 pos 开始的 len 个字符拼接到 str1
 
3. 拼接字符数组:
str1.append("World", 5);  // 将 "World" 的前 5 个字符拼接到 str1
 
4. 拼接单个字符:
str1.append(1, '!');  // 将字符 '!' 拼接到 str1 后面

3.4.查找、替换与截取

可以通过结合find()、substr()以及replace()函数,实现字符串的查找、截取和替换。

3.4.1.查找子串

string (1)	
size_t find (const string& str, size_t pos = 0) const noexcept;
c-string (2)	
size_t find (const char* s, size_t pos = 0) const;
buffer (3)	
size_t find (const char* s, size_t pos, size_type n) const;
character (4)	
size_t find (char c, size_t pos = 0) const noexcept;

如前面所述,使用`find()`函数可以查找子串的位置。

#include <iostream>
using namespace std;

int main() {
	string str = "Hello, World!";
	string toFind1 = "Worlda";
	string toFind2 = "World";

	size_t found1 = str.find(toFind1);
	size_t found2 = str.find(toFind2);
	if (found1 != string::npos) {
		cout << "Found at position: " << found1 << endl;
	}
	else {
		cout << "Not found" << endl;
	}
	if (found2 != string::npos) {
		cout << "Found at position: " << found2 << endl;
	}
	else {
		cout << "Not found" << endl;
	}

	return 0;
}

3.4.2.截取字符串

使用`substr()`可以从查找到的位置开始截取指定长度的子串。

string substr (size_t pos = 0, size_t len = npos) const;
#include <iostream>
using namespace std;

int main() {
	string str = "Hello, World!";
	size_t start = 7; // 从第7个字符开始
	size_t length = 5; // 截取5个字符

	string sub = str.substr(start, length);
	cout << "截取的字符串: " << sub << endl;

	return 0;
}

3.4 3.替换字符串

使用replace()函数可以替换子串。replace()的使用方法是:

string.replace(position, length, new_string);

其中position是替换的起始位置,length是要替换的字符长度,new_string是用于替换的字符串。

#include <iostream>
using namespace std;

int main() {
	string str = "Hello, World!";
	string toReplace = "World";
	string replacement = "C++";

	size_t found = str.find(toReplace);
	if (found != string::npos) 
	{
		// 在位置 found 替换长度为 toReplace.length() 的子串
		str.replace(found, toReplace.length(), replacement);
	}

	cout << "替换后的字符串:" << str << endl;

	return 0;
}

3.5.其他成员函数

string的成员函数太多,我们在此就不详细说明了,其他还有很多重要的成员函数,我们简单介绍一下功能,其它还需要小伙伴们自己深入学习。

3.5.1.string 类对象的容量操作

size:返回字符串有效字符长度。
length:返回字符串有效字符长度。
capacity:返回空间总大小。
empty:检测字符串是否为空串,是返回true,否则返回false。
clear:清空有效字符。
reserve:为字符串预留空间。
resize:将有效字符的个数改成n个,多出的空间用字符c填充。

注意事项
 - 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的底层空间总大小时,reserve不会改变容量大小。

3.5.2.string类对象的访问及遍历操作 

operator[](重点):返回字符串中 pos 位置的字符,供 const string 类对象调用。

begin + end:
begin 获取指向字符串第一个字符的迭代器
end 获取指向最后一个字符下一个位置的迭代器。

rbegin + rend:
rbegin 获取指向字符串最后一个字符的反向迭代器
rend 获取指向第一个字符前一个位置的反向迭代器。

范围 for:C++11 支持的更简洁的字符串遍历方式。

3.5.3.string类对象的修改操作

push_back:在字符串末尾插入字符 c。
append:在字符串后追加一个字符串。
operator+=(重点):在字符串后追加字符串 str。
c_str(重点):返回 C 格式字符串。
find + npos(重点):从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置。
rfind:从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置。
substr:在字符串中从 pos 位置开始,截取 n 个字符并返回。

注意事项
 - 在字符串尾部追加字符时,s.push_back(c)、s.append(1, c)、s += 'c' 三种实现方式差异不大,一般情况下 string 类的 += 操作用得比较多,+= 操作不仅可以连接单个字符,还可以连接字符串。

 - 对字符串进行操作时,如果能够大概预估到要放多少字符,可以先通过 reserve 把空间预留好。

3.5.4.string类非成员函数 

operator+:尽量少用,因为传值返回,会导致深拷贝,效率较低。
operator>>(重点):是输入运算符的重载,用于从输入流读取数据。
operator<<(重点):是输出运算符的重载,用于向输出流写入数据。
getline(重点):用于获取一行字符串。
relational operators(重点):用于进行字符串的大小比较。

更多string的成员函数:string - C++ Reference (cplusplus.com)

4.auto和范围for

1.auto

(1)auto是c++11引入的一种类型推导关键字,允许编译器自动推导变量的类型,它通过从变量的初始化表达式中推出具体类型auto声明的变量必须由编译器在编译时期推导而得

(2)用auto声明指针类型时,用autoauto*没有任何区别,但用auto声明引用类型时则必须加&。

(3)当在同一行声明多个变量时这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量

(4)auto不能作为函数的参数可以做返回值,但是建议谨慎使用

(5)auto不能直接用来声明数组

(6)使用auto声明的变量声明时必须初始化。

#include <iostream>
using namespace std;


int func1()
{
	return 10;
}
// 不能做参数
//void func2(auto a)
//{}

// 可以做返回值,但是建议谨慎使用
auto func3()
{
	return 3;
}
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = func1();
	// 编译报错,必须初始化
	//auto e;
	cout << typeid(b).name() << endl; // 输出int
	cout << typeid(c).name() << endl; // char
	cout << typeid(d).name() << endl; // int
	int x = 10;
	auto y = &x;
	auto* z = &x;
	auto& m = x;
	cout << typeid(x).name() << endl; // int
	cout << typeid(y).name() << endl; // int*
	cout << typeid(z).name() << endl; // int*
	cout << typeid(m).name() << endl; // int
	auto aa = 1, bb = 2;

	// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
	//auto cc = 3, dd = 4.0;
	
	// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
	//auto array[] = { 4, 5, 6 };

	string s1("hellow world");
	//string::reverse_iterator rit = s1.begin();
	auto rit = s1.begin();
	while (rit != s1.end())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;

	return 0;
}

2.范围for 

范围for是C++11引入的一种简化的遍历容器元素的方式,它极大地简化了传统的基于索引或迭代器的循环结构,是代码更加简洁明了。

基本语法:

for (declaration : range_expression) 
{
    // 循环体
}

declaration:表明元素的声明,可以是值、引用或常量引用。

range_expression:表示一个可遍历的对象。

示例代码:

int main()
{
	//数组三种遍历方式
    //下标[],迭代,返回for
	//自动取容器的数据赋值给左边的值
	//自动++,自动判断结束
	//原理:范围函数底层是迭代器
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	//修改
	for (auto& ch : s1)
	{
		ch++;
	}
	cout << endl;
   
    //修改后的字符串
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	int a[] = { 1,2,3,4,5,6,7 };
	for (auto i : a)
	{
		cout << i << " ";
	}

	return 0;
}

5.string内存管理与性能

C++ 提供了标准库中的 `std::string` 类来简化字符串操作,但其内存管理背后有许多细节值得注意。以下是内存管理与性能优化相关的几个关键点:

5.1.std::string内存管理机制

(1)动态内存分配:std::string是基于动态内存分配的。它会在需要时自动扩展容量。这意味着当字符串增长时,可能会重新分配内存并复制已有的内容到新位置。这种操作代价较高,特别是在频繁追加内容时。

(2)内存释放:当 string 对象超出作用域或被销毁时,其占用的内存会自动被释放。这是通过 C++的自动内存管理机制(RAII,Resource Acquisition Is Initialization)实现的。

void someFunction() {
    std::string localString = "Local string";
    // 做一些其他操作
} // localString 在这里被销毁,其内存被释放。

(3)小字符串优化(SSO, Small String Optimization):为了避免频繁的动态分配开销,许多现代的 C++ 实现采用了小字符串优化机制。当字符串很短(通常少于 15-23 个字符,取决于实现)时,std::string 会直接在栈上存储数据,而不需要堆内存分配。

(4)容量管理:`std::string` 通常会分配比实际需要的更多内存,以避免频繁的重新分配。开发者可以通过 `reserve()` 函数预分配足够的内存空间,避免多次分配。

5.2.性能优化技巧

(1)预分配内存:如果你知道字符串的大致大小,可以使用 `reserve()` 函数预分配内存,减少多次动态内存分配。例如,在大量 `push_back` 操作前调用 `reserve()`。

(2)避免不必要的复制:尽量避免在不必要的情况下拷贝 `std::string` 对象。可以通过使用引用(如 `const std::string&`)或 C++11 之后的右值引用来减少复制的开销。

  void process(const std::string& str);  // 避免拷贝
  void process(std::string&& str);       // 接收右值引用,支持移动语义

(3)使用 `std::move` 优化:C++11 引入了移动语义,可以通过 `std::move()` 转移字符串的所有权,而不再拷贝数据。对于需要返回大字符串的函数,移动语义可以显著提高性能。

  std::string create_large_string() {
      std::string str = "large_string_data";
      return std::move(str);  // 避免拷贝
  }

(4)拼接字符串时注意效率:在拼接大量字符串时,直接使用 `+` 可能导致多次内存分配和拷贝。为了提高效率,可以考虑使用 `std::ostringstream` 或 `std::string::append()` 来减少不必要的开销。

  std::ostringstream oss;
  oss << "Hello" << " " << "World!";
  std::string result = oss.str();

5.3.常见的性能陷阱

(1)频繁的小字符串拼接:频繁的小字符串拼接会导致大量的内存分配和释放操作。可以通过 `reserve()` 或使用合适的数据结构(如 `std::ostringstream`)来减少开销。

(2)返回大字符串:如果一个函数返回一个大字符串对象,而没有使用移动语义(C++11 之前),会导致不必要的拷贝。C++11 之后,编译器会优化返回值,但显式使用 `std::move` 更好地表达意图。

(3)过度使用std::string的临时对象:在某些场景下,频繁生成临时的 `std::string` 对象会导致性能下降。可以通过优化临时对象的使用或通过 `std::string_view` 来避免不必要的临时对象生成。

总结
C++ 的 `std::string` 提供了许多便捷的功能,但在需要处理大量数据或高性能场景下,注意其内存管理机制至关重要。通过合理使用预分配、避免不必要的拷贝、利用移动语义等技术,开发者可以大幅度提高字符串操作的效率。

评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值