跳到主要内容
C++ std::string 类常用接口与模拟实现 | 极客日志
C++ 算法
C++ std::string 类常用接口与模拟实现 C++ std::string 类涵盖常用接口、底层结构及模拟实现。内容涉及 auto 关键字与范围 for 循环的应用,string 对象的构造、容量、访问及修改操作,VS 与 g++ 下结构差异,浅拷贝与深拷贝机制,以及写时拷贝概念。结合 OJ 题目展示实际应用,强调内存管理的重要性。
链路追踪 发布于 2026/3/21 更新于 2026/6/1 16 浏览C++ std::string 类详解
1. 为什么学习 string 类?
1.1 C 语言中的字符串
C 语言中,字符串是以'\0'结尾的一些字符的集合。为了操作方便,C 标准库中提供了一些 str 系列的库函数,但是这些库函数与字符串是分离开的,不太符合 OOP 的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
在 OJ 中,有关字符串的题目基本以 string 类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用 string 类,很少有人去使用 C 库中的字符串操作函数。
2. 标准库中的 string 类
2.1 string 类 (了解)
在使用 string 类时,必须包含 #include <string> 头文件以及 using namespace std;。
2.2 auto 和范围 for
auto 关键字
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量。后来这个不重要了。C++11 中,标准委员会变废为宝赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加&。当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用。auto 不能直接用来声明数组。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
int func1 () { return 10 ; }
void func2 (auto a) {}
auto func3 () { return 3 ; }
int main () {
int a = 10 ;
b = a;
c = ;
d = ();
cout << (b). () << endl;
cout << (c). () << endl;
cout << (d). () << endl;
x = ;
y = &x;
* z = &x;
& m = x;
cout << (x). () << endl;
cout << (y). () << endl;
cout << (z). () << endl;
;
}
auto
auto
'a'
auto
func1
typeid
name
typeid
name
typeid
name
int
10
auto
auto
auto
typeid
name
typeid
name
typeid
name
return
0
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main () {
std::map<std::string, std::string> dict = {
{ "apple" , "苹果" },
{ "orange" , "橙子" },
{"pear" ,"梨" }
};
auto it = dict.begin ();
while (it != dict.end ()) {
cout << it->first << ":" << it->second << endl;
++it;
}
return 0 ;
}
范围 for 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号' :'分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。本质还是迭代器。范围 for 可以作用到数组和容器对象上进行遍历。范围 for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main () {
int array[] = { 1 , 2 , 3 , 4 , 5 };
for (int i = 0 ; i < sizeof (array) / sizeof (array[0 ]); ++i) {
array[i] *= 2 ;
}
for (int i = 0 ; i < sizeof (array) / sizeof (array[0 ]); ++i) {
cout << array[i] << endl;
}
for (auto & e : array) e *= 2 ;
for (auto e : array) cout << e << " " << endl;
string str ("hello world" ) ;
for (auto ch : str) {
cout << ch << " " ;
}
cout << endl;
return 0 ;
}
2.3 string 类的常用接口说明
1. string 类对象的常见构造 #include <iostream>
#include <string>
using namespace std;
void test1 () {
string s1;
string s2 ("hello,world" ) ;
string s3 (3 , 'a' ) ;
string s4 = s2;
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}
int main () {
test1 ();
return 0 ;
}
2. string 类对象的容量操作 函数名称 功能说明 size(重点)返回字符串有效字符长度 length返回字符串有效字符长度 capacity返回空间总大小 empty(重点)检测字符串是否为空串,是返回 true,否则返回 false clear(重点)清空有效字符 reserve(重点)为字符串预留空间 resize(重点)将有效字符的个数改成 n 个,多出的空间用字符 c 填充
#include <iostream>
#include <string>
using namespace std;
int main () {
string s1 = "hello,world!" ;
cout << s1. size () << endl;
cout << s1.l ength() << endl;
cout << s1. empty () << endl;
cout << "s1:" << " " << s1 << endl;
cout << "clear 前的实际空间个数" << endl;
cout << s1. capacity () << endl;
cout << "clear" << endl;
s1. clear ();
cout << "clear 后的空间个数" << endl;
cout << s1. capacity () << endl;
cout << s1. empty () << endl;
cout << "reserve 后的实际空间个数" << endl;
s1. reserve (20 );
cout << s1. capacity () << endl;
cout << s1. size () << endl;
cout << "resize" << endl;
s1. resize (10 , 'a' );
cout << s1 << endl;
return 0 ;
}
注意 : size() 与 length() 方法底层实现原理完全相同,引入 size() 的原因是为了与其他容器的接口保持一致,一般情况下基本都是用 size()。早期编写 string 时实现的 length 后面 stl 参考 string 模板类的实现时,stl 中的其他容器用的都是 size(),为了统一 string 也补充了 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 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。不同的编译器改变因 resize 导致的扩容方式可能不同。
reserve(size_t res_arg=0):为 string 预留空间,不改变有效元素个数,当 reserve 的参数小于 string 的底层空间总大小时,reserver 不会改变容量大小。
3. string 类对象的访问及遍历操作 #include <iostream>
#include <string>
using namespace std;
int main () {
string s;
s += "hello,world" ;
cout << s << endl;
cout << "范围 for 遍历 s:" << endl;
for (auto & e : s) {
cout << e << " " ;
}
cout << endl << endl << endl;
cout << "正向迭代器遍历 s:" << endl;
for (auto i = s.begin (); i != s.end (); i++) {
cout << *i << " " ;
}
cout << endl;
cout << endl;
cout << endl;
cout << "逆向迭代器遍历 s" << endl;
for (auto i = s.rbegin (); i != s.rend (); i++) {
cout << *i << " " ;
}
cout << endl;
return 0 ;
}
4. string 类对象的修改操作 函数名称 功能说明 push_back在字符串末尾插入字符 c。例如 string s = "abc"; s.push_back('d');,执行后 s 为 "abcd" append在字符串后追加一个字符串。比如 string s = "hello"; s.append(" world");,结果 s 是 "hello world" operator+=(重点)在字符串后追加字符串 str,用法和 append 类似但更简洁,如 string s = "I "; s += "love C++";,s 变为 "I love C++" c_str(重点)返回 C 格式字符串(const char* 类型),可用于兼容 C 语言接口,比如 printf("%s", s.c_str());(s 是 string 对象) find + npos(重点)从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置(若找不到返回 string::npos)。例如 string s = "abcabc"; size_t pos = s.find('b', 2);,从索引 2 开始找 'b',会返回索引 3 rfind从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置。比如 string s = "abcabc"; size_t pos = s.rfind('b', 4);,从索引 4 往前找 'b',会返回索引 3 substr在字符串中从 pos 位置开始,截取 n 个字符并返回(若不指定 n,则截取到字符串末尾)。例如 string s = "abcdefg"; string sub = s.substr(2, 3);,sub 为 "cde"
#include <iostream>
#include <string>
using namespace std;
int main () {
string s1;
s1. push_back ('c' );
char a = 'd' ;
s1. push_back (a);
cout << s1 << endl;
return 0 ;
}
find:
npos:
string 找不到对象时 返回 npos
#include <iostream>
#include <string>
using namespace std;
int main () {
string s = "hello,world" ;
string s2 = "llo" ;
cout << s.find (s2) << endl;
cout << s.find ("wor" ) << endl;
cout << s.find ("wor" , 5 , 2 ) << endl;
cout << s.find ('l' ) << endl;
cout << s.find ('l' , 5 ) << endl;
cout << s.find ("www" ) << endl;
return 0 ;
}
#include <iostream>
#include <string>
using namespace std;
int main () {
string s = "hello,world" ;
cout << s.rfind ('l' ) << endl;
return 0 ;
}
#include <iostream>
#include <string>
using namespace std;
int main () {
string s = "hello,world" ;
cout << s.substr (2 , 3 ) << endl;
return 0 ;
}
注意 : 在 string 尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下 string 类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。对 string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好。减少后续不断添加字符导致的空间开辟浪费的性能。
5. string 类非成员函数 输入输出的运算符重载 在本文下方模拟 string 类时实现。
这里 getline 该函数接口挺重要的也比较常用,希望大家能够熟练掌握。
getline 不是 string 类的封装函数 其在 std 命名空间里 想要用的话在 std 中直接调用即可
#include <iostream>
#include <string>
using namespace std;
int main () {
string s;
cout << " 直接 cin 输入字符串(里面包含空格)的展现效果:" << endl;
cin >> s;
cout << s << endl;
cout << "getline 方式实现含特殊字符的字符串:" << endl;
getline (cin, s);
cout << s << endl;
return 0 ;
}
#include <iostream>
#include <string>
using namespace std;
int main () {
string s;
cout << "getline 输入:" << endl;
getline (cin, s, '*' );
cout << s << endl;
return 0 ;
}
为什么第一个 getline 没有调用 getline 输入呢?在第一个代码中,cin >> s 会在输入缓冲区留下未读取的内容(包括空格和换行符),而 endl 只负责刷新输出缓冲区,对输入缓冲区没有任何影响 。因此必须使用 cin.ignore() 来专门处理输入缓冲区的残留数据。而第二个代码 iostream 流中 即键盘文件中没有信息 getline 则会调用 以 cin 输入标准流的方式 将信息输入到键盘文件中 从而读取数据到 s 中。
上面的几个接口大家了解一下,下面的 OJ 题目中会有一些体现他们的使用。string 类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
6. vs 和 g++ 下 string 结构的说明
注意:下述结构是在 32 位平台下进行验证,32 位平台下指针占 4 个字节。
string 总共占 28 个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义 string 中字符串的存储空间:当字符串长度小于 16 时,使用内部固定的字符数组来存放 (内存池)。内存池这里不做阐述 后续文章中会有该知识点的学习。当字符串长度大于等于 16 时,从堆上开辟空间。
union _Bxty {
value_type _Buf[_BUF_SIZE];
pointer _Ptr;
char _Alias[_BUF_SIZE];
} _Bx;
这种设计也是有一定道理的,大多数情况下字符串的长度都小于 16,那 string 对象创建好之后,内部已经有了 16 个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个 size_t 字段保存字符串长度,一个 size_t 字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其他事情。
故总共占 16+4+4+4=28 个字节。
G++ 下,string 是通过写时拷贝实现的,string 对象总共占 4 个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:空间总大小字符串有效长度引用计数。
struct_Rep_base{
size_type _M_length;
size_type _M_capacity;
_Atomic_word _M_refcount;
};
7. string 函数接口的应用 class Solution {
public :
bool isLetter (char ch) {
if (ch >= 'a' && ch <= 'z' ) return true ;
if (ch >= 'A' && ch <= 'Z' ) return true ;
return false ;
}
string reverseOnlyLetters (string S) {
if (S.empty ()) return S;
size_t begin = 0 , end = S.size () - 1 ;
while (begin < end) {
while (begin < end && !isLetter (S[begin])) ++begin;
while (begin < end && !isLetter (S[end])) --end;
swap (S[begin], S[end]);
++begin;
--end;
}
return S;
}
};
class Solution {
public :
int firstUniqChar (string s) {
int count[256 ] = { 0 };
int size = s.size ();
for (int i = 0 ; i < size; ++i) count[s[i]] += 1 ;
for (int i = 0 ; i < size; ++i)
if (1 == count[s[i]]) return i;
return -1 ;
}
};
#include <iostream>
#include <string>
using namespace std;
int main () {
string line;
while (getline (cin, line)) {
size_t pos = line.rfind (' ' );
cout << line.size () - pos - 1 << endl;
}
return 0 ;
}
class Solution {
public :
bool isLetterOrNumber (char ch) {
return (ch >= '0' && ch <= '9' ) || (ch >= 'a' && ch <= 'z' ) || (ch >= 'A' && ch <= 'Z' );
}
bool isPalindrome (string s) {
for (auto & ch : s) {
if (ch >= 'a' && ch <= 'z' ) ch -= 32 ;
}
int begin = 0 , end = s.size () - 1 ;
while (begin < end) {
while (begin < end && !isLetterOrNumber (s[begin])) ++begin;
while (begin < end && !isLetterOrNumber (s[end])) --end;
if (s[begin] != s[end]) {
return false ;
}
else {
++begin;
--end;
}
}
return true ;
}
};
class Solution {
public :
string addStrings (string num1, string num2) {
int end1 = num1. size () - 1 ;
int end2 = num2. size () - 1 ;
int value1 = 0 , value2 = 0 , next = 0 ;
string addret;
while (end1 >= 0 || end2 >= 0 ) {
if (end1 >= 0 ) value1 = num1[end1--] - '0' ;
else value1 = 0 ;
if (end2 >= 0 ) value2 = num2[end2--] - '0' ;
else value2 = 0 ;
int valueret = value1 + value2 + next;
if (valueret > 9 ) {
next = 1 ;
valueret -= 10 ;
}
else {
next = 0 ;
}
addret += (valueret + '0' );
}
if (next == 1 ) {
addret += '1' ;
}
reverse (addret.begin (), addret.end ());
return addret;
}
};
关于 string 的题目有很多,大家可以去刷题网站巩固,这里不做过多阐述了。
3. string 类的模拟实现
3.1 经典的 string 类问题 上面已经对 string 类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现 string 类,最主要是实现 string 类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下 string 类的实现是否有问题?
class String {
public :
String (const char * str = "" ) {
if (nullptr == str) {
assert (false );
return ;
}
_str = new char [strlen (str) + 1 ];
strcpy (_str, str);
}
~String () {
if (_str) {
delete [] _str;
_str = nullptr ;
}
}
private :
char * _str;
};
void TestString () {
String s1 ("hello bit!!!" ) ;
String s2 (s1) ;
}
程序结束时 s1,s2 所指向的空间被连续释放了两次!!!
说明:上述 String 类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 构造 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2 共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
3.2 浅拷贝 浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
3.3 深拷贝 如果一个类中涉及到资源的管理 ,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出 。一般情况都是按照深拷贝方式提供。
3.3.1 传统版写法的 String 类 class String {
public :
String (const char * str = "" ) {
if (nullptr == str) {
assert (false );
return ;
}
_str = new char [strlen (str) + 1 ];
strcpy (_str, str);
}
String (const String& s) :_str(new char [strlen (s._str) + 1 ]) {
strcpy (_str, s._str);
}
String& operator =(const String& s) {
if (this != &s) {
char * pStr = new char [strlen (s._str) + 1 ];
strcpy (pStr, s._str);
delete [] _str;
_str = pStr;
}
return *this ;
}
~String () {
if (_str) {
delete [] _str;
_str = nullptr ;
}
}
private :
char * _str;
};
3.3.2 现代版写法的 String 类 class String {
public :
String (const char * str = "" ) {
if (nullptr == str) {
assert (false );
return ;
}
_str = new char [strlen (str) + 1 ];
strcpy (_str, str);
}
String (const String& s) :_str(nullptr ) {
String strTmp (s._str) ;
swap (_str, strTmp._str);
}
String& operator =(String s) {
swap (_str, s._str);
return *this ;
}
~String () {
if (_str) {
delete [] _str;
_str = nullptr ;
}
}
private :
char * _str;
};
3.4 写时拷贝 (了解)
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为 1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源
3.5 string 类的模拟实现 这里简单的模拟数据类型为 char 的 string,但是库里面的是满足各种编码的模板类。
#pragma once
#include <iostream>
#include <cstring>
#include <cassert>
using namespace std;
class mystring {
public :
mystring (const char * s = "" ) {
if (s == nullptr ) {
assert (false );
return ;
}
_str = new char [strlen (s) + 1 ];
strcpy (_str, s);
_capacity = _size = strlen (s);
}
mystring (const mystring& s) {
_str = new char [s._capacity + 1 ];
strcpy (_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
~mystring () {
delete [] _str;
_str = nullptr ;
_size = _capacity = 0 ;
}
mystring& operator =(const mystring& s) {
delete [] _str;
_str = new char [strlen (s._str) + 1 ];
strcpy (_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this ;
}
mystring operator +(const mystring& s) const {
mystring strtmp = *this ;
int newlenth = strlen (s._str) + strlen (_str) + 1 ;
if (newlenth > strtmp._capacity) {
char * newchar = new char [newlenth * 2 ];
strcpy (newchar, strtmp._str);
delete [] strtmp._str;
strtmp._str = newchar;
strtmp._capacity = newlenth * 2 ;
}
strcat (strtmp._str, s._str);
strtmp._size = newlenth - 1 ;
return strtmp;
}
mystring& operator +=(const mystring& s) {
mystring strtmp = s;
*this = (*this + strtmp);
return *this ;
}
friend ostream& operator <<(ostream& out, const mystring s);
friend istream& operator >>(istream& in, mystring& s);
void swap (mystring& s) {
std::swap (_str, s._str);
std::swap (_size, s._size);
std::swap (_capacity, s._capacity);
}
void print () const {
for (int i = 0 ; i < _size; i++) {
printf ("%c" , _str[i]);
}
printf ("\n" );
}
private :
char * _str;
int _size;
int _capacity;
};
ostream& operator <<(ostream& out, const mystring s) {
s.print ();
return out;
}
istream& operator >>(istream& in, mystring& s) {
int i = 0 ;
char ch;
while (in.get (ch) && ch != ' ' && ch != '\n' ) {
s._str[i++] = ch;
}
s._str[i] = '\0' ;
s._size = i;
return in;
}
结语 从用 char* 手动计算长度、反复处理内存溢出的'踩坑',到用 std::string 一行代码实现拼接、查找的'丝滑',再到亲手拆解其动态扩容、深拷贝的底层逻辑 —— string 类的学习,本质是一场对 C++'封装思想'的实践:它把复杂的内存管理藏在底层,把简洁的接口留给开发者,既解决了 C 语言字符串的痛点,又为后续 STL 容器的学习打下了'容器 + 迭代器 + 接口'的认知基础。
或许你现在对'小字符串优化(SSO)''迭代器失效'等细节仍有疑惑,或许在模拟实现时曾为深拷贝与浅拷贝的边界纠结 —— 但没关系,技术的精进本就是'先用透,再深究'的过程。后续可以试着用 string 结合 STL 算法(如 sort 排序字符串、find_if 筛选字符),或是在项目中用 stringstream 处理复杂字符串转换,让知识在实践中落地。
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online