
✨前言:string 是 C++ 中最常用的类之一,但你真的了解它的底层实现吗?本文将带你快速掌握 string 的核心用法,并深入模拟实现其关键功能,理解深浅拷贝等核心概念。 📖专栏:【C++成长之旅】
首先,对于string类的学习我们也可以参考: 【string类文档介绍】 然后,在此之前,我们在这里学习2个C++11的小语法,方便我们后面的学习:
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
return 0;
}用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&;
int main()
{
int x = 10;
auto y = &x;
auto* z = &x;
auto& m = x;
return 0;
}当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
//编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
auto cc = 3, dd = 4.0;
return 0;
}auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
// 不能做参数
void func2(auto a)
{
}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
return 3;
}auto不能直接用来声明数组
int main()
{
//编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
auto array[] = { 4, 5, 6 };
return 0;
}int main()
{
// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项,不然无法推导
auto e;
return 0;
}范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。 范围for可以作用到数组和容器对象上进行遍历 范围for的底层很简单,容器遍历实际就是替换为迭代器。
对于迭代器简单解释一下:
我们可以把迭代器在行为上看做是指针(因为它模拟指针的用法),但实际上它的实现不一定是指针,而是一个为了统一遍历各种数据结构而设计的抽象工具。
其实这种“统一接口”的特性,正是C++ STL(标准模板库)的基石。(慢慢体会)
#include<iostream>
#include <string>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
// C++98的遍历
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;
}
// C++11的遍历
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " " << endl;
string str("hello world");//用"hello world"构造了一个string
for (auto ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}有了上面两个的基础,我们在C++中遍历容器就比较方便。
对于string类我只讲解最常用的接口,因为各种原因导致string类有些冗余。
函数名称 | 功能说明 |
|---|---|
string() (重点) | 构造空的 string 类对象,即空字符串 |
string(const char* s) (重点) | 用 C-string 来构造 string 类对象 |
string(size_t n, char c) | string 类对象中包含 n 个字符 c |
string(const string& s) (重点) | 拷贝构造函数 |
void test()
{
string s1; // 构造空的 string 类对象 s1
string s2("hello world"); // 用 C 格式字符串构造 string 类对象 s2
string s3(s2); // 拷贝构造 s3
}
函数名称 | 功能说明 |
|---|---|
size (重点) | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty (重点) | 检测字符串是否为空串,是返回 true,否则返回 false |
clear (重点) | 清空有效字符 |
reserve (重点) | 为字符串预留空间** |
resize (重点) | 将有效字符的个数改成 n 个,多出的空间用字符填充 |
说明: 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会不会缩容是不确定的。 我们来看官方说法:

不确定的,也就是说reserve的参数小于string的底层空间总大小时,reserver会不会缩容是不确定的。但是:

此函数不会影响字符串长度,也无法修改其内容。其实可以看出来reserve()函数还是很温柔的,它只是建议保留n个空间。
函数名称 | 功能说明 |
|---|---|
operator[] (重点) | 返回 pos 位置的字符,const string 类对象调用 |
begin + end | begin 获取第一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 (左闭右开) |
rbegin + rend | rbegin 获取最后一个字符的迭代器,rend 获取第一个字符前一个位置的迭代器 |
范围for (C++11) | C++11 支持更简洁的范围 for 的新遍历方式 |
#include<string>
#include<iostream>
using namespace std;
int main()
{
string str = "hello world";
// 1. operator[] 访问
for (size_t i = 0; i < str.size(); ++i)
{
cout << str[i] << " ";
}
cout << endl;
// 2. 迭代器遍历
for (auto it = str.begin(); it != str.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
// 3. 反向迭代器遍历
for (auto rit = str.rbegin(); rit != str.rend(); ++rit)
{
cout << *rit << " ";
}
cout << endl;
// 4. 范围for遍历 (C++11)
for (char ch : str)
{
cout << ch << " ";
}
cout << endl;
return 0;
}
函数名称 | 功能说明 |
|---|---|
push_back | 在字符串后尾插字符 c |
append | 在字符串后追加一个字符串 |
operator+= (重点) | 在字符串后追加字符串 str |
c_str (重点) | 返回 C 格式字符串 (const char* 类型) |
find + npos (重点) | 从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置 |
rfind | 从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置 |
substr | 在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回 |
std::string str = "hello";
// 1. push_back - 尾插字符
str.push_back('!'); // str becomes "hello!"
// 2. append - 追加字符串
str.append(" world"); // str becomes "hello! world"
// 3. operator+= - 追加字符串(更常用)
str += " C++"; // str becomes "hello! world C++"
// 4. c_str - 返回C格式字符串
const char* cstr = str.c_str(); // 可用于C语言接口
// 5. find - 查找字符/字符串
size_t pos = str.find('o'); // 返回第一个'o'的位置
if (pos != std::string::npos)
{ // npos表示未找到
std::cout << "Found at: " << pos << std::endl;
}
// 6. rfind - 反向查找
size_t rpos = str.rfind('o'); // 从后往前找第一个'o'
// 7. substr - 截取子串
std::string sub = str.substr(0, 5); // 截取前5个字符:"hello"对于npos解释:

它的字面意思是 “not a position” 或 “no position”,用于表示无效的或未找到的位置。 由于 size_t 是无符号类型,-1 会变成该类型能表示的最大值,即0xFFFFFFFF,我们的string的大小不可能是npos,就用npos表示无效的或未找到的位置 简单来说,npos 就是一个官方定义的、用来判断字符串查找操作是否成功的“失败标志”。
注意(习惯):
函数名称 | 功能说明 |
|---|---|
operator+ | 字符串拼接,但尽量少用,因为传值返回导致深拷贝,效率低 |
operator>> (重点) | 输入运算符重载(用于从流中提取字符串) |
operator<< (重点) | 输出运算符重载(用于将字符串插入到流中) |
getline (重点) | 获取一行字符串(可指定分隔符) |
relational operators (重点) | 大小比较(包括 ==, !=, <, <=, >, >= 等) |
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = "Hello";
string s2 = "World";
// 1. operator+ (不推荐频繁使用)
string s3 = s1 + " " + s2; // 产生临时对象,深拷贝效率低
cout << s3 << endl; // Output: Hello World
// 2. operator>> 输入
string input;
cout << "Please enter a string: ";
cin >> input; // 遇到空格停止
cout << "You entered: " << input << endl;
// 3. operator<< 输出
cout << s1 << " " << s2 << endl; // Output: Hello World
// 4. getline 获取一行(推荐)
cin.ignore(); // 清除输入缓冲区
cout << "Please enter a line: ";
getline(cin, input); // 读取整行,包括空格
cout << "You entered: " << input << endl;
// 5. relational operators 比较
if (s1 == s2) {
cout << "Strings are equal" << endl;
} else if (s1 < s2) {
cout << s1 << " is less than " << s2 << endl;
} else {
cout << s1 << " is greater than " << s2 << endl;
}
return 0;
}对于string类的接口还有很多,上面的几个接口大家了解一下,后续慢慢练习就熟悉了。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查前面说过的文档即可。
上面已经对string类进行了简单的介绍,我们只要能够正常使用即可。但是在面试中,面试官总喜欢让我们自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。 string的底层我们可以看作是:

当然,也可以更为简化,但是,我们为了方便书写代码,就可以像我这种,下面我们来完成string类的实现:
可以根据下面头文件中的声明来试着实现一下: string.h:
class string
{
friend std::ostream& operator<<(std::ostream& _out, const sxn::string& s);
friend std::istream& operator>>(std::istream& _in, sxn::string& s);
public:
//迭代器重命名
typedef char* iterator;
public:
string(const char* str = "");
string(const string& s);
//string& operator=(const string& s);
string& operator=(string s);
~string();
// iterator
iterator begin();
iterator end();
// modify
void swap(string& s);
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
//relational operators
bool operator<(const string& s);
bool operator==(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str = NULL;
size_t _capacity = 0;
size_t _size = 0;
};这里就有个问题:拷贝构造和赋值重载需要我们自己实现吗?

回答是,要,必须要。
这里就涉及到了深浅拷贝的问题,前面的文章也说过了,但是都没有实操的机会,这里真好赶上了,就展开再说说,就以拷贝构造为例,假如是编译器自己生成的,就是浅拷贝/值拷贝,会导致:

这是我们想要的结果吗,不是,它还会导致在调用析构函数的时候,会调用两次,出现问题。 所以这里我们就要实现深拷贝,深拷贝之后要达到的效果:

其实也很好理解,因为编译器不知道你这里到底指向资源还是干什么,它只知道在这块空间上放着_str、_size、_capacity这三个,所以它只能完成浅拷贝。
有了深浅拷贝的理解,这些代码就很好完成了。
#include"string.h"
const size_t string::npos = -1;
//构造函数
string::string(const char* str)
:_str(new char[strlen(str) + 1])
{
_size = strlen(str);
strcpy(_str, str);
_capacity = _size;
// std::cout << "string(const char* str)" << std::endl;
}
//拷贝构造函数
/*string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}*/
//现代写法
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//析构函数
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity;
//std::cout << "~string()" << std::endl;
}
// iterator
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
//赋值
/*string& string::operator=(const string& s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}*/
//现代写法
/*string& string::operator=(const string& s)
{
string tmp(s._str);
swap(tmp);
return *this;
}*/
string& string::operator=(string s)
{
swap(s);
return *this;
}
// modify
//尾插
void string::push_back(char c)
{
reserve(_size + 1);
_str[_size++] = c;
}
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
//追加
void string::append(const char* str)
{
int len = strlen(str);
reserve(_size + len);
strcpy(_str + _size, str);
_size = _size + len;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::clear()
{
_size = 0;
_str[0] = '\0';
}
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
const char* string::c_str()const
{
return _str;
}
/////////////////////////////////////////////////////////////
// capacity
size_t string::size()const
{
return _size;
}
size_t string::capacity()const
{
return _capacity;
}
bool string::empty()const
{
return _size == 0;
}
void string::resize(size_t n, char c)
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
while (_size < n)
{
_str[_size++] = c;
}
_str[_size] = '\0';
}
}
void string::reserve(size_t n)
{
static size_t count = 0;
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
std::cout << "reserve->" << ++count << std::endl;
}
}
/////////////////////////////////////////////////////////////
// access
char& string::operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& string::operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
/////////////////////////////////////////////////////////////
//relational operators
bool string::operator<(const string& s)
{
int i = 0;
int j = 0;
while (i < _size && j < s._size)
{
if (_str[i++] >= s._str[j++])
{
return false;
}
}
if (i < _size && j == s._size)
return false;
return true;
}
bool string::operator==(const string& s)
{
int i = 0;
int j = 0;
while (i < _size && j < s._size)
{
if (_str[i++] != s._str[j++])
{
return false;
}
}
if (i != _size || j != s._size)
return false;
return true;
}
bool string::operator<=(const string& s)
{
return *this == s || *this < s;
}
bool string::operator>(const string& s)
{
return !(*this <= s);
}
bool string::operator>=(const string& s)
{
return *this == s || *this > s;
}
bool string::operator!=(const string& s)
{
return !(*this == s);
}
// 返回c在string中第一次出现的位置
size_t string::find(char c, size_t pos) const
{
if (pos >= _size)
return npos;
while (pos < _size)
{
if (_str[pos] == c)
return pos;
}
return npos;
}
// 返回子串s在string中第一次出现的位置
size_t string::find(const char* s, size_t pos) const
{
char* str = strstr(_str + pos, s);
if (str == nullptr)
{
return npos;
}
return str - _str;
}
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& string::insert(size_t pos, char c)
{
assert(pos <= _size);
reserve(_size + 1);
size_t i = _size + 1;
while (i > pos)
{
_str[i] = _str[i - 1];
--i;
}
_str[pos] = c;
++_size;
return *this;
}
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len == 0)
return *this;
reserve(_size + len);
size_t i = _size + len;
while (i >= pos + len)
{
_str[i] = _str[i - len];
--i;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
// 删除pos位置上的元素,并返回该元素的下一个位置
string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
int i = 0;
while (len <= _size)
{
_str[pos + i] = _str[pos + len];
i++;
len++;
}
_size = pos + i - 1;
}
return *this;
}
//输入输出
std::ostream& operator<<(std::ostream& _out, const sxn::string& s)
{
int n = 0;
while (n < s._size)
{
_out << s._str[n++];
}
return _out;
}
/* std::istream& operator>>(std::istream& _in, sxn::string& s)
{
s.clear();
char ch = (char)_in.get();
while (ch != '\n')
{
s.reserve(++s._size);
s._str[s._size-1] = ch;
s._str[s._size] = '\0';//不写这个reserve函数中的strcpy()拷贝就会出错
ch = _in.get();
}
s._str[s._size] = '\0';
return _in;
}*/
std::istream& operator>>(std::istream& _in, sxn::string& s)
{
s.clear();
//用一个数组做缓冲,减少开辟内存的时间
const int N = 5;
char buffer[N];
//要使用get函数才行,因为cin以及输入时将空格忽视
int ch = _in.get();
int i = 0;
while (ch != '\n')
{
//先写入buffer
if (i < N)
{
buffer[i++] = ch;
}
else
{
s.reserve(s._size + N);
strncpy(s._str + s._size, buffer, N);
s._size += N;
s._str[s._size] = '\0';
i = 0;
}
ch = _in.get();
}
if (i > 0)
{
s.reserve(s._size + i + 1);
strncpy(s._str + s._size, buffer, i + 1);
s._size += i + 1;
s._str[s._size] = '\0';
}
return _in;
}在这里再提一个点:

可以看看区别,巧妙:




函数之后,s自动析构,只能说,完美。
对于string类的模拟实现不仅仅是一个编程练习,更是理解C++面向对象、资源管理和运算符重载等核心概念的绝佳范例,要时间都可以练习练习。
记住,真正掌握一个类,不仅要会用,更要知其所以然。正是我们迈向C++高手之路的重要一步!