深入解析:c++的STL:string类与string类的手动基础实现
点击跳转快捷目录
- 简介
- std::string的基础与使用
- string的实现
- 基本使用示例
- 底层实现my::string(不涉及SSO/内存池优化)
- 代码整体(直接复制可用的代码)
- string.h
- string.cpp
- 代码讲解
- 为什么不写默认构造函数string()?
- 带参构造string(const char* str)为什么不用初始化列表?
- 没看懂拷贝构造函数和赋值运算符重载?
- string(const char* str)为什么使用strcpy而不使用my::string::swap?
- assert()是什么?
- reserve()中为什么使用memcpy而不是strcpy?
- 流运算符重载
- 总结:为什么 std::string 如此重要?
简介
在 C++ 的标准模板库(STL)中,std::string
是最常用、最重要的类之一。
它封装了字符数组的复杂操作,为开发者提供了安全、便捷、功能强大的字符串处理能力。相比于传统的 C 风格字符串(即以 \0
结尾的 char*
数组),std::string
自动管理内存、支持动态扩展、提供丰富的成员函数,极大地提升了开发效率和代码安全性。
本文将从 std::string
的基本使用入手,逐步深入其底层实现原理,帮助读者会用、理解。
提示:由于std::string的底层实现并非简单地开辟空间,而是涉及内存池、SSO等优化技术,为了让有一定c++基础但是没有深入了解的读者也能看懂,本文的代码实现部分不涉及内存池等优化技术!
std::string的基础与使用
string的实现
std::string 是 C++ 标准库中定义的一个类模板实例化类型,其完整定义为:typedef std::basic_string<char> string;
它位于头文件 <string>
中,是一个模板类 basic_string
对 char
类型的特化。除了 string,标准库还提供了:
typedef std::basic_string<wchar_t> wstring; // 宽字符字符串typedef std::basic_string<char16_t> u16string; // UTF-16typedef std::basic_string<char32_t> u32string; // UTF-32
- 这里提及的宽字符、UTF等,对我们了解string没有太大帮助,不了解字符编码可以无视。如果没有兴趣可以不了解,感兴趣可以进行网络搜索或询问AI等
- 动态大小:可根据需要自动增长或缩减。
- 自动内存管理:无需手动分配或释放内存。
- 丰富的接口:支持拼接、查找、替换、比较、子串等操作。
- 与 C 风格字符串兼容:可通过
c_str()
获取const char*
。 - 迭代器支持:可与 STL 算法无缝结合。
std::string
本质上是一个动态数组容器,专门用于存储和操作字符序列。它具备以下核心特性:基本使用示例
以下为简单使用示例 (这些简单的部分就不过多赘述了)
#include <iostream>#include <string>using namespace std;int main() {// 构造string s1 = "Hello";string s2("World");// 拼接string s3 = s1 + ", " + s2 + "!";// 长度与访问cout << "Length: " << s3.size() << endl;cout << "First char: " << s3[0] << endl;// 查找与子串size_t pos = s3.find("World");if (pos != string::npos) {string sub = s3.substr(pos, 5);cout << "Found: " << sub << endl;}// 比较if (s1 < s2) {cout << s1 << " < " << s2 << endl;}return 0;}
底层实现my::string(不涉及SSO/内存池优化)
代码整体(直接复制可用的代码)
string.h
头文件: string.h
#define _CRT_SECURE_NO_WARNINGS 1 //Visual Studio防报错用
#pragma once
#include<iostream>#include<string.h>#include<assert.h>using namespace std;namespace my{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;string(const char* str = "");~string();string(const string& s);string& operator=(string tmp);const char* c_str() const;void swap(string& s);size_t size() const;char& operator[](size_t i);const char& operator[](size_t i) const;void reserve(size_t n);void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);string& insert(size_t pos, char ch);string& insert(size_t pos, const char* str);string& erase(size_t pos = 0, size_t len = npos);void pop_back();size_t find(char ch, size_t pos = 0) const;size_t find(const char* str, size_t pos = 0) const;string substr(size_t pos, size_t len = npos) const;void clear();bool operator<(const string& s) const;bool operator<=(const string& s) const;bool operator>(const string& s) const;bool operator>=(const string& s) const;bool operator==(const string& s) const;bool operator!=(const string& s) const;private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;public:static const size_t npos;};ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);istream& getline(istream& in, string& s, char delim = '\n');void swap(string& x, string& y);}
string.cpp
实现文件: string.cpp
#include "string.h"
namespace my
{
const size_t string::npos = -1;
string::string(const char* str)
: _size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string::string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
string& string::operator=(string tmp)
{
swap(tmp);
return *this;
}
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return _str + _size;
}
const char* string::c_str() const
{
return _str;
}
size_t string::size() const
{
return _size;
}
char& string::operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& string::operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* str = new char[n + 1];
memcpy(str, _str, _size + 1);
delete _str;
_str = str;
_capacity = n;
}
}
void string::push_back(char ch)
{
if (_size >= _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity) {
size_t newCapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newCapacity);
}
memcpy(_str + _size, str, len + 1);
_size += len;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
string& string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size >= _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newCapacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity) {
size_t newCapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
reserve(newCapacity);
}
int end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
--end;
}
memcpy(_str + pos, str, len);
_size += len;
return *this;
}
string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos || len >= (_size - pos))
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t i = pos + len;
memmove(_str + pos, _str + i, _size + 1 - i);
_size -= len;
}
return *this;
}
void string::pop_back()
{
assert(_size > 0);
--_size;
_str[_size] = '\0';
}
size_t string::find(char ch, size_t pos) const
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t string::find(const char* str, size_t pos) const
{
const char* p1 = strstr(_str + pos, str);
if (p1 == nullptr)
return npos;
else
return p1 - _str;
}
string string::substr(size_t pos, size_t len) const
{
if (len == npos || len >= _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; i++)
{
ret += _str[pos + i];
}
return ret;
}
void string::clear()
{
_size = 0;
_str[0] = '\0';
}
bool string::operator<(const string& s) const
{
size_t i1 = 0, i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] < s[i2])
return true;
else if (_str[i1] > s[i2])
return false;
else {
++i1; ++i2;
}
}
return i2 < s._size;
}
bool string::operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool string::operator>(const string& s) const
{
return !(*this <= s);
}
bool string::operator>=(const string& s) const
{
return !(*this < s);
}
bool string::operator==(const string& s) const
{
size_t i1 = 0, i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] != s[i2])
return false;
else {
++i1; ++i2;
}
}
return i1 == _size && i2 == s._size;
}
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
ostream& operator<<(ostream& out, const string& s)
{
//out << s.c_str(); //无法访问'\0'后的数据
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char buff[128];
int i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
++i;
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
istream& getline(istream& in, string& s, char delim)
{
s.clear();
char buff[128];
int i = 0;
char ch = in.get();
while (ch != delim)
{
buff[i] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
++i;
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
void swap(string& x, string& y)
{
x.swap(y);
}
}
代码讲解
为什么不写默认构造函数string()?
如果我们手动写默认构造,那么会写成下面这样:
string::string()
: _str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{
}
但是我们已经实现了带参构造函数,因此我们可以通过给带参构造函数缺省值的方式,实现两个构造函数的合并
string::string(const char* str = "")
: _size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
这样,如果是无参构造,就可以直接使用""
来进行构造
带参构造string(const char* str)为什么不用初始化列表?
可能有人会疑惑,为什么这里不直接写为全初始化列表:
string::string(const char* str)
:_str(new char[strlen(str) + 1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
我们不难发现,如果使用这种方法,会调用3次strlen
函数,但是strlen
函数的时间复杂度是n
,这意味着每次调用都要遍历一次字符串str
,这会造成极大的时间浪费。
但是我们可以发现,strlen
的值保存在了_size
之中,因此我们可以将_size
作为参数进行传递。
这时,可能就会有同学这样想了:调换初始化列表的顺序不就好了吗?
string::string(const char* str)
:_size(strlen(str))
,_str(new char[_size + 1])
,_capacity(_size)
{
strcpy(_str, str);
}
但是很疑惑,如果试着运行一下,会发现代码是错误的!
因为对于初始化列表而言,初始化顺序与声明顺序相同,我们的声明顺序是
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
因此初始化顺序依次为_str
,_size
,_capacity
(如果不太理解可以看看这里~)
CSDN文章:C++初始化列表是什么?有什么“隐藏坑”?初始化列表的顺序问题
当然,改变声明顺序也是解决办法之一,即将_size
的声明放到最前面,变成
private:
size_t _size = 0;
char* _str = nullptr;
size_t _capacity = 0;
但是这样会大大增加代码的耦合性,这是编程之大忌!
因此,这个时候就只能放弃全部使用初始化列表,只将_size
放在初始化列表之中,其余部分手动实现
string::string(const char* str)
: _size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
strcpy(_str, str);
}
(可能有同学会疑惑,初始化列表、缺省值等,谁的优先级更高呢?可以看看我写的另一篇文章哦~)
CSDN文章:C++类中成员变量走初始化列表的逻辑
没看懂拷贝构造函数和赋值运算符重载?
文章里提供的拷贝构造函数和赋值运算符重载是经过优化后的版本,虽然不会提高运行效率,但是代码简洁性会大大提高!
具体如何优化的在下面这里,里面涉及的例子就是本文的my::string
:
CSDN文章:C++拷贝构造函数与operator=()的现代简洁写法,如何“优雅”地拷贝构造与赋值
string(const char* str)为什么使用strcpy而不使用my::string::swap?
在上一个问题中,我们已经写了my::string::swap
函数来实现对一个新对象的赋值(点击上一个问题的蓝色链接即可看到),那么在这里为什么不调用swap
来进行更"优雅"的操作呢?
- 原因有两个
1.类型不同:
_str
的类型是char*
,而str
的类型是const char*
,二者类型不同2.存储位置不同
即使二者类型相同,二者的存储空间位置也不同str
指向的内存空间是常量区
,值是不能修改的_str
指向的内存空间是堆
,值是可以修改的
assert()是什么?
assert
是c/c++都有的断言报错,可以判断代码运行到此处时是否符合括号内的条件语句,是debug时的好帮手!
不清楚的话可以看看这里~
CSDN文章:assert断言(如何让程序在不满足条件时报错)
reserve()中为什么使用memcpy而不是strcpy?
在正常的使用情况下,使用strcpy()其实并没有什么问题,因为一个字符串正常插入数据,结尾以\0
结束,我们的代码逻辑可以很好地处理这种情况。
但是,为了代码的健壮性/鲁棒性,我们不得不考虑一种情况:
用户会不会插入\0
这个字符?
例如:
void test()
{
my::string s("hello");
s.push_back('\0');
s += "world";
cout << s <<endl;
}
调用这个函数后,如果使用strcpy
,我们会发现,打印结果为:
hello
但是在模板库std::string
,会发现能正常打印出结果:
helloworld
原因分析
对于strcpy
函数而言,看到\0
就意味着字符串的结束,并不会考虑这个\0
是否属于字符串内容的一部分,因此我们不应当使用strcpy
,而是使用memcpy
来手动设置拷贝_size + 1
个字符,这样才能确保用户插入的\0
以及后面的内容不会在拷贝中丢失!
补充
在append()
方法中,使用strcpy
和memcpy
均可
流运算符重载
可能部分同学会疑惑:流运算符为何会被重载到全局?
我写了两篇关于这个问题的文章,第一篇比较全面但是较长,想要快速理解可以直接看第二篇~
C++流运算符重载为何不能作为类的成员函数?
C++流运算符重载为何不能作为类的成员函数?快速理解!
总结:为什么 std::string 如此重要?
特性 | 说明 |
---|---|
安全性 | 避免缓冲区溢出,自动管理内存 |
易用性 | 支持 + 、=、 == 等运算符重载 |
功能丰富 | 提供 find 、replace 、substr 等 50+ 成员函数 |
性能优化 | SSO、移动语义、预留容量 (reserve ) 等机制 |
标准兼容 | 与 STL 容器、算法、迭代器无缝集成 |
(文章如有错漏,欢迎在评论区/私信指出!)
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/940935.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!