目录
引言
一、C语言字符串
1.1 创建 C 字符串
1.2 字符串长度
1.3 字符串拼接
1.4 比较字符串
1.5 复制字符串
二、C++字符串string类
2.1 解释
2.2 string构造函数
2.2.1 string() 默认构造函数
2.2.2 string(const char* s) 从 C 风格字符串构造
2.2.3 string(const string& str) 拷贝构造函数
2.2.4 string(int n, char c) 从重复字符构造
2.3 string类对象的容量操作函数
2.3.1 size()与length() 获取字符串的有效字符长度
2.3.2 capacity()
2.3.3 empty() 判断是否为空串
2.3.4 clear() 清空有效字符
2.3.5 reserve()
2.3.6 resize()
2.4 string类对象的访问及遍历操作
2.4.1 operator[]
2.4.2 迭代器访问
① 正向迭代器
②反向迭代器
疑惑
2.4.3 范围for循环
拓展
引言
当我们踏入编程的神秘世界,就像是探险家踏上未知的大陆。C 字符串犹如传统的地图,指引着我们从简单中发现力量,然而小心不经意的错误,就像深不见底的陷阱。🗺️🧩
而在这个奇幻的编码森林中,
std::string
类则犹如一颗闪亮的宝石,散发出现代的光辉。它是那把能解锁高级魔法的钥匙,让我们能够探索更广阔的创造领域。✨🔑
本文将引导你踏上探索之旅,穿越时间的长河,探索 C 字符串 🕰️ 和 C++
std::string
类 🚀 的异同。无论你是选择继续弹奏古老的旋律,还是驾驭现代的编码风潮,都能在这里找到属于你的启示。一起开启这段奇妙的编程之旅吧!🌌🌈
一、C语言字符串
当在 C 语言中处理字符串时,主要是使用字符数组来表示字符串,通常被称为 C 风格字符串。C 字符串是以 null 结尾的字符数组,使用字符指针来访问字符串的各个字符。以下是关于 C 字符串的一些基本概念和操作:
1.1 创建 C 字符串
C 字符串是由字符数组构成的,以 null 终止(也就是以字符 '\0' 表示字符串的结束)。
char str[] = "Hello, world!"; // 创建并初始化 C 字符串
在上述代码中,char str[] = "Hello, world!";
创建了一个字符数组 str
并初始化为 "Hello, world!"。这个字符数组的大小会自动根据初始化的内容进行分配,且在该变量的作用域结束后会自动释放,不需要显式地进行内存释放操作。
接下来我们介绍C中需要手动释放的案例:
#include <stdio.h>
#include <string.h>
int main() {
// 创建一个字符数组并初始化为"Hello"
char *c_str = (char *)malloc(6 * sizeof(char));
printf("C String: %s\n", c_str);
// 错误:忘记释放分配的内存,导致内存泄漏
// free(c_str);
return 0;
}
1.2 字符串长度
C 字符串的长度是不包括 null 终止符的字符数。
char str[] = "Hello, world!";
int length = strlen(str); // 获取字符串长度(不包括 null 终止符)
1.3 字符串拼接
在 C 语言中,拼接字符串需要使用库函数 strcat()
。注意,这需要预先分配足够大的内存来存放拼接后的字符串。
char str1[] = "Hello, ";
char str2[] = "world!";
char result[20]; // 预分配足够大的内存
strcpy(result, str1); // 复制第一个字符串
strcat(result, str2); // 拼接第二个字符串
1.4 比较字符串
使用库函数 strcmp()
可以比较两个字符串是否相等。
char str1[] = "apple";
char str2[] = "banana";
int result = strcmp(str1, str2); // 比较字符串,返回 0 表示相等
1.5 复制字符串
使用库函数 strcpy()
可以复制一个字符串到另一个字符串。
char source[] = "Copy this.";
char destination[20]; // 预分配足够的内存
strcpy(destination, source); // 复制 source 到 destination
二、C++字符串string类
当我们进入C++的世界,我们会遇到一个强大而便捷的工具,那就是
std::string
类。与C语言的字符数组不同,C++中的std::string
为我们提供了更加安全和高效的字符串处理方式,让我们在编码的旅途中更加游刃有余。😊🎉
std::string
类是C++标准库中的一员,它被设计用来处理字符串,不仅能够存储文本数据,还提供了丰富的方法来操作字符串,避免了C语言中处理字符串时的许多繁琐问题。使用std::string
,我们可以更专注于业务逻辑而不必担心内存管理和其他细节。✨💼
下面让我们一起来探索一下std::string
的基本用法和一些特性,看看它是如何让我们摆脱繁琐的字符数组操作,更加轻松地处理字符串任务的。无论你是新手还是经验丰富的开发者,std::string
都会成为你编程工具箱中的一把利器。🔧🚀
2.1 解释
介绍C++string之前,补充一些知识点!
string本质:
-
string是C++风格的字符串,而string本质上是一个类。
string和char * 区别:
-
char * 是一个指针。
-
string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。
-
std::string str; // 创建一个空字符串
2.2 string构造函数
如图所示是cplusplus官网中string类的全部构造函数。
笔者将介绍其中主要使用的几种。
2.2.1 string() 默认构造函数
std::string str; // 创建一个空字符串
2.2.2 string(const char* s) 从 C 风格字符串构造
const char* cstr = "Hello, world!";
std::string str(cstr); // 从 C 风格字符串构造
2.2.3 string(const string& str) 拷贝构造函数
使用一个string对象初始化另一个string对象。
std::string original = "Hello, world!";
std::string str(original); // 从另一个字符串构造
2.2.4 string(int n, char c) 从重复字符构造
std::string str(5, 'X'); // 创建由 5 个 'X' 组成的字符串
总结:string的多种构造方式没有可比性,灵活使用即可。
2.3 string类对象的容量操作函数
2.3.1 size()与length() 获取字符串的有效字符长度
在 std::string
类中,size()
和 length()
都是用来获取字符串的长度的成员函数,它们在功能上是等价的,可以互换使用。
std::string str = "Hello, world!";
int len = str.size(); // 获取字符串长度
std::string str = "Hello, world!";
int len = str.length(); // 获取字符串长度
这两个函数都返回一个 size_t
类型的值,表示字符串的字符数(不包括 null 终止符 \0
)。它们的实际操作是相同的,只是函数名不同,你可以根据个人偏好选择使用哪个。
需要注意的是,size_t
是无符号整数类型,因此在使用 size()
或 length()
返回的值时,要确保不会与负数进行比较或运算,以避免不必要的问题。
2.3.2 capacity()
在 std::string
类中,capacity()
是一个成员函数,用于获取字符串对象内部分配的存储容量(内存大小),而不仅仅是字符串的长度。这个函数对于了解和优化内存分配非常有用。
当你创建一个 std::string
对象时,它会分配一块内存来存储字符串数据。capacity()
函数返回的值是这块内存能够容纳的字符数量(不包括 null 终止符 \0
)。
std::string str = "Hello";
std::cout << "String length: " << str.length() << std::endl; // 输出 5
std::cout << "String capacity: " << str.capacity() << std::endl; // 输出至少 5
在上面的例子中,
str
的长度是 5(包括 "Hello" 的字符),而capacity()
函数可能会返回至少 5,但实际上会分配更多的内存,以便在需要时能够追加更多字符而不需要频繁重新分配内存。
2.3.3 empty() 判断是否为空串
在 std::string
类中,empty()
是一个成员函数,用于检查字符串是否为空,即字符串中是否没有字符。
std::string str1 = "Hello";
std::string str2;
bool is_str1_empty = str1.empty(); // 返回 false,因为 str1 不为空
bool is_str2_empty = str2.empty(); // 返回 true,因为 str2 为空
如上所示,empty()
函数会返回一个布尔值,表示字符串是否为空。如果字符串中没有字符(即长度为 0),则返回 true
,否则返回 false
。
2.3.4 clear() 清空有效字符
在 std::string
类中,clear()
是一个成员函数,用于清空字符串中的内容,使字符串变为空字符串。
std::string str = "Hello, world!";
std::cout << "Before clear: " << str << std::endl;
str.clear(); // 清空字符串
std::cout << "After clear: " << str << std::endl; // 输出空字符串
在上述示例中,调用 str.clear()
后,字符串 str
的内容将被清空,变成了一个空字符串。注意,clear()
函数会保留内存分配,以便在后续操作中可能需要添加字符时不必重新分配内存。
2.3.5 reserve()
在 std::string
类中,reserve()
是一个成员函数,用于预分配字符串对象的内存空间,以便存储未来可能的字符。这个函数在需要高效管理内存分配和避免频繁的重新分配时非常有用。
std::string str;
std::cout << "Before reserve: Capacity = " << str.capacity() << std::endl;
str.reserve(100); // 预分配至少能容纳 100 个字符的内存
std::cout << "After reserve: Capacity = " << str.capacity() << std::endl;
在上面的例子中,我们首先创建了一个空字符串 str
,然后使用 reserve()
函数预分配了至少能容纳 100 个字符的内存。这样,在后续添加字符时,不会频繁地进行内存重新分配,从而提高性能。
需要注意的是,reserve()
函数并不会改变字符串的长度,只是在内部分配足够的内存,以便在需要时可以高效地添加字符。如果你知道字符串的大致长度,使用 reserve()
可以帮助你减少内存重新分配的次数。
疑问:
如果参数比我现在的有效字符数还要小呢?如何处理?
如果给
reserve()
函数的参数值太小,它会根据情况进行内存分配,以确保能够容纳至少指定数量的字符。如果指定的值比当前字符串长度要小,那么reserve()
将会忽略这个请求,因为当前内存已经足够容纳现有的字符。
以下是一个示例,演示了当给 reserve()
函数传递一个较小值时的行为:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
std::cout << "Before reserve: Capacity = " << str.capacity() << std::endl;
str.reserve(5); // 预分配至少能容纳 5 个字符的内存
std::cout << "After reserve: Capacity = " << str.capacity() << std::endl;
return 0;
}
在这个示例中,尽管我们传递了 reserve(5)
,字符串的当前长度已经是 13("Hello, world!"),因此 reserve()
函数会保留当前的内存分配,不会将内存缩减到只能容纳 5 个字符,以保持已有的内容。
所以,如果 reserve()
函数的参数值比当前字符串长度小,函数会尽可能保留当前的内存分配,以适应现有的字符串内容。这种行为确保了已有内容不会被截断或丢失。
2.3.6 resize()
在 std::string
类中,resize()
是一个成员函数,用于改变字符串的长度,同时可以选择如何填充新增的部分。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello";
std::cout << "Before resize: " << str << std::endl;
str.resize(10, '!'); // 将字符串长度改变为 10,多出部分用 '!' 填充
std::cout << "After resize: " << str << std::endl;
return 0;
}
在上面的例子中,我们首先创建了一个字符串 str
,然后使用 resize()
函数将其长度改变为 10。如果指定的新长度大于当前长度,那么多出的部分将使用指定的字符(在这个例子中是 '!'
)进行填充。
如果指定的新长度小于当前长度,字符串将被截断,保留前面的字符,而多余的字符将被删除。
需要注意的是,resize()
函数可以接受两个参数:新的长度和用于填充的字符。如果只提供新的长度,函数会用 null 字符('\0'
)填充新增部分。
2.4 string类对象的访问及遍历操作
2.4.1 operator[]
在 std::string
类中,operator[]
是一个用于访问字符串中特定位置字符的运算符重载。通过这个运算符,你可以直接访问字符串中的单个字符,就像访问数组元素一样。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
char firstChar = str[0]; // 获取第一个字符 'H'
char sixthChar = str[5]; // 获取第六个字符 ','
std::cout << "First character: " << firstChar << std::endl;
std::cout << "Sixth character: " << sixthChar << std::endl;
return 0;
}
在上面的例子中,我们通过 str[0]
和 str[5]
分别获取了字符串 str
的第一个和第六个字符。需要注意的是,字符串的索引是从 0 开始的,因此第一个字符的索引是 0。
如果使用 operator[]
访问超出字符串长度的索引,将会导致未定义的行为,因此在使用时需要确保索引的范围是有效的。
2.4.2 迭代器访问
① 正向迭代器
在
std::string
类中,你可以使用begin()
函数获取一个指向字符串开头的迭代器,使用end()
函数获取一个指向字符串结尾的迭代器的下一个位置。这些迭代器可以用于遍历字符串中的字符。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
// 使用迭代器遍历字符串
for (std::string::iterator it = str.begin(); it != str.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 使用 const 迭代器遍历字符串(不修改字符串内容)
for (std::string::const_iterator it = str.begin(); it != str.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
在上述示例中,我们使用 begin()
和 end()
函数获取了迭代器,然后使用迭代器遍历字符串中的字符。需要注意的是,end()
函数返回的迭代器指向字符串的结尾的下一个位置,因此在使用时应该注意边界。
如果你不需要修改字符串内容,建议使用 const
版本的迭代器(const_iterator
),以便在遍历过程中不意外地修改字符串内容。
②反向迭代器
在 C++11 及更高版本中,
std::string
提供了rbegin()
和rend()
成员函数,它们分别返回一个反向迭代器,用于从字符串末尾向开头遍历字符串的字符。这对于需要从末尾开始遍历字符串的场景非常有用。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
// 使用反向迭代器遍历字符串
for (std::string::reverse_iterator it = str.rbegin(); it != str.rend(); ++it) {
std::cout << *it << " ";
}
return 0;
}
在上述示例中,我们使用
rbegin()
和rend()
函数获取了反向迭代器,然后使用迭代器遍历字符串中的字符。rbegin()
返回一个指向最后一个字符的迭代器,rend()
返回一个指向第一个字符前面的位置的迭代器。需要注意的是,反向迭代器遍历的顺序是从字符串末尾向开头,因此输出结果将会从右往左显示字符串的字符。
当使用 const
版本的迭代器遍历字符串时,可以使用 crbegin()
和 crend()
成员函数。这些函数返回的是 const
版本的反向迭代器,用于遍历字符串的字符,同时不允许修改字符串内容。
#include <iostream>
#include <string>
int main() {
const std::string str = "Hello, world!";
// 使用 const 反向迭代器遍历字符串
for (std::string::const_reverse_iterator it = str.crbegin(); it != str.crend(); ++it) {
std::cout << *it << " ";
}
return 0;
}
总之,crbegin()
和 crend()
是用于使用 const
反向迭代器遍历 std::string
字符的函数,适用于不允许修改字符串内容的场景。
疑惑
为什么反向迭代器更新也是使用递增运算符呢?
😄 良好的设计和一致性是C++标准库的重要支柱之一。在整个库中,无论是正向迭代器还是反向迭代器,它们都秉承着相似的操作方式,这样使用者能够更加轻松地理解和使用。
在C++中,递增操作符
++
的含义异常清晰:它让迭代器指向容器中的下一个元素。这一操作无论是用于正向迭代器还是反向迭代器,都保持一致。通过维持这种一致性,C++标准库让不同类型的迭代器都能够使用相似的方式操作,这极大地降低了学习和使用的难度。
举例来说,当你使用反向迭代器遍历字符串时,++
操作将迭代器指向前一个字符,这样一来,正向迭代器和反向迭代器的操作行为就得以保持一致。这种一致性不仅增强了代码的可读性,还有助于提升代码的维护性。
总之,一致性和清晰的设计在C++标准库中扮演着重要角色,迭代器的 ++
操作在不同类型的迭代器之间始终保持一致。这种一致性大大方便了使用者在容器中操作元素时的思维切换。🚀📘
2.4.3 范围for循环
C++11 引入了基于范围的
for
循环(Range-basedfor
loop),可以方便地遍历容器(包括字符串)中的元素,包括std::string
。
下面是如何使用范围 for
循环来遍历 std::string
中的字符的示例:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
// 使用范围 for 循环遍历字符串中的字符
for (char c : str) {
std::cout << c << " ";
}
return 0;
}
在上述示例中,我们使用范围 for
循环直接遍历了字符串 str
中的字符,并将每个字符输出到控制台。范围 for
循环会自动迭代容器中的每个元素,并将其赋值给指定的变量。
需要注意的是,范围 for
循环中的变量类型应该与容器中的元素类型匹配。在遍历 std::string
时,可以使用 char
类型来匹配字符串中的字符。
拓展
当使用范围 for
循环遍历 std::string
中的字符时,你可以使用 auto
或者 const auto
来更简洁地声明循环变量。这可以让代码更加清晰,同时减少了类型匹配的烦恼。
与此同时,必要时还可以加上&。
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, world!";
// 使用 auto 遍历字符串中的字符
for (auto c : str) {
std::cout << c << " ";
}
std::cout << std::endl;
// 使用 const auto & 遍历字符串中的字符
for (const auto &c : str) {
std::cout << c << " ";
}
return 0;
}
在第一个范围 for
循环中,我们使用 auto
来自动推断循环变量的类型,这里会自动推断为 char
。在第二个范围 for
循环中,我们使用 const auto &
,它将循环变量声明为对字符的常量引用。
使用 const auto &
可以减少拷贝,特别是当处理容器内元素较大时,效率会有所提高。而使用 auto
则会进行元素的拷贝。