文章目录
- 前言
- 一、string类的常用接口
-
- [1. string类对象的常见构造](#1. string类对象的常见构造)
- 2.迭代器(iterator)
- 3.string类对象的容量操作
- 4.string类对象的修改操作
- 5.string类非成员函数
- 二、string的模拟实现
- 三、auto和范围for
- 总结
前言
在日常生活中呢?经常会用到字符串,比如:身份证号码,电话号码,家庭住址等。所以为了方便管理,就单独搞一份管理字符串的类型出来。所以就有了string类
这里小编会从两个方向来了解string:
- string类的常用的接口(这里主要是最常用的接口,因为string的接口太多了,大概有一百多个接口)
- string的模拟实现
注意:在使用string类时,必须包含#include头文件以及using namespace std;
一、string类的常用接口
想学好string除外还要,还要结合文档来学习https://legacy.cplusplus.com/reference/clibrary/
这个呢是C语言的一个标准库。我们可以通过这里看到string的全部成员函数以及相关介绍。
接下来接结合这个文档来看一下string都有哪些常用的接口吧!
1. string类对象的常见构造

- string():构造一个空的string类的对象,即空字符串。
- string (const string& str):拷贝构造函数。
- string (const string& str, size_t pos, size_t len = npos):复制str从字符pos开始的部分并跨越len字符(或直到str,如果str太短或len为string::npos)。
- string (const char* s);用C-string来构造string类对象.
- string (const char* s, size_t n);从s指向的字符数组中复制前n个字符。
- string (size_t n, char c);用n个字符来初始化。
具体用法:
cpp
#include<iostream>
#include<string.h>
using namespace std;
int main()
{
string s1;//构造一个空的的string对象
cout << s1 << endl;
string s2("hello world");//用C格式字符串构造string类对象s2
cout << s2 << endl;
string s3(s2, 6, 100);//当我们给的参数大于npos的时候,编译器会以npos来运行
cout << s3 << endl;
//string s3(s2, 6);//如果不传参数,他也会按照npos来运行
//cout << s3 << endl;
//string s3(s2, 6, 4);//如果参数小于npos,那么编译器 就会按照传的直来运行
//cout << s3 << endl;
string s4(s2);/// 拷贝构造s3
cout << s4 << endl;
string s5("hello world", 5);
cout << s5 << endl;
string s6(5, 'x');//用5个'x'来初始化字符串
cout << s6 << endl;
return 0;
}

2.迭代器(iterator)
迭代器是一种访问容器内元素的接口,它允许我们在不暴露容器内部表示的情况下遍历容器中的所有元素。在C++中,迭代器是一个重要的组成部分,尤其是在STL(标准模板库)中,迭代器提供了一种方法来访问容器中的元素,就像指针一样,但比指针更加通用和强大。
begin是字符串起始的位置,end是字符串"\0"的位置。然而rbegin和rend则是反向迭代器。
具体这样用呢?看代码:
cpp
#include<iostream>
#include<string.h>
using namespace std;
int main()
{
string s("hello world");
cout << s << endl;
string::iterator it = s.begin();//反向迭代器只需要把begin换成rbegin。end也是要换成rend。
while (it != s.end())
{
cout << *it<<" ";
it++;
}
return 0;
}

3.string类对象的容量操作
- size() :返回字符串的有效长度
- length():返回字符串的有效长度
- capacity():返回总空间的大小
- enmpty();检测字符串释放为空串,是返回true,否则返回false
- clear():清空有效字符
- reserve(size_t n ):开辟n个字符大小的空间
- 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的底层空间总大小时,reserver不会改变容量大小。
4.string类对象的修改操作
- push_bank(char c):在字符串后面尾插一个字符c
- append(const char *str):在字符串后面追加一个字符串str
- opertaor+=()在字符串后面追加一个字符串或者字符
- c_str():返回字符串
- find :返回查找字符在字符串中位置(下标)
- substr():在str中从pos位置开始,截取n个字符,然后将其返回
注意:
- 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
- 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留
好。
5.string类非成员函数

以上就是常见的string的接口了。如果还有其他的话那就去查文档吧!上面有链接的。这里小编带着大家来看看string的底层(模拟实现)。这里我不一一写出来了。
二、string的模拟实现
这里我们需要创建三个文件: string.cpp string.h test.cpp 。这里我们用到了一个namespace和一个类来封装一下。具体代码请看下面:
1.string.h文件
c
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
namespace S
{
class string
{
public:
//string()//默认构造函数
// :_str(new char [1]{'\0'})
// , _size(0)
// ,_capacity(0)
//{}
//短小且频繁使用的函数,可以直接定义到类里面,默认是内联函数(inline)
//初始化,这里给"\0"是为了防止传的是空的字符。如果不写并且传的是空的话,
//那么后面在c_str哪里返回时就要解引用,但是不能对空指针解引用,所以这里就会报错。
//当然这里也可以不用这样子写。可以用上面被注释掉的代码。但是这样要方便一点
string(const char* s = "\0")
{
_size = strlen(s);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, s);
}
~string()//析构函数
{
if (_str)
{
delete[]_str;
_str = nullptr;
_size = _capacity = 0;
}
}
string(const string& str)//拷贝构造
{
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
const char* c_str()//取到_str中的数据
{
return _str;
}
size_t size()const//返回字符串中的有效大小
{
return _size;
}
char& operator[](size_t pos)//返回pos的数据,可以修改的
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const//只返回pos的数据,不能修改
{
assert(pos < _size);
return _str[pos];
}
typedef char* iterator;//迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;//cosnt_iterator迭代器
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
void clear()//清空有效字符
{
_str[0] = '\0';
_size = 0;
}
//上面的代码比较短,比较简单,所以就可以在类里面实现。默认是内联(inline),
//这里我们重点讲一下以下这些成员函数吧。
void reserve(size_t n);
void push_bank(char ch);
void append(const char *str);
string& operator+= (char ch);
string& operator+=(const char *_str);
string& operator=(const string& s);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len= npos);
size_t find(char ch, size_t pos = 0);
string substr(size_t pos, size_t len=npos);
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos=-1;
};
ostream& operator<<(ostream& out, const string& s);
istream& operator >> (istream& in, string& s);
void test_string5();
}
2.string的相关成员函数的实现:
- void reserve(size_t n) :开辟n个字符大小的空间,开空间的顺序是:先开好n个新的空间(tmp),然后在把tmp拷贝给_str,然后在释放掉旧的空间(_str),在让_str指向新的空间tmp,最后再把空间容量改成n就行了
cpp
void string::reserve(size_t n)//扩容
{
if (n > _capacity)//这里是为了这个函数单独使用
{
char* tmp = new char[n + 1];//多开一个空间
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
2.void push_bank(char ch);在字符串的后面插入一个字符。这里需要注意的是_str[_size] = '\0',如果这里不写这段代码的话。那么程序运行的结果就会跟我们预想的不一样。这里是因为我们在原来的字符串'\0'插入了一个字符ch。都知道字符串是以'\0'结束的。如果这里不在新的字符串后面插入'\0',那么运行的结果就是在新的字符串后面会出现随之值直到把空间装满。
cpp
void string::push_bank(char ch)//插入
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//这里特别要注意
}
void append(const char str);在字符串后面追加一个字符串,这里需要注意的即使扩容哪里。如果len+_size小于2_capacity,要多少空间就给多大的空间。如果大于就按2*_capacity来扩容
cpp
void string::append(const char* str)//在字符串之后添加一个字符串
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//小于2*_capacity,要多少空间就给多大的空间。如果大于就按2*_capacity来扩容
reserve(len + _size < 2 * _capacity ? len + _size : 2 * _capacity);
}
strcpy(_str + _size, str);
/*for (int i = 0; i < len; i++)//这里的效果跟strcpy是一样的
{
_str[_size+i] = str[i];
}
_str[_size+len] = '\0';
_size += len;*/
}
string& operator+= (char ch);
string& operator+=(const char *_str);
由于这两个函数比较简单所以就全部写出来:
cpp
string& string::operator+= (char ch)//+=一个字符
{
push_bank(ch);
return *this;
}
string& string ::operator+=(const char* _str)//+=一个字符串
{
append(_str);
return *this;
}
string& operator=(const string& s);赋值运算符重载,其实这里跟拷贝构造差不多,但是这里要考虑一下自己给自己赋值的情况。虽然运行会报错,但是这样的语法是正确的。所以这里要处理这里情况。
cpp
string& string ::operator=(const string& s)//赋值
{
if (this!= &s)//这里就是小心自己给自己赋值的情况
{
delete[]_str;//先释放掉原来的空间
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
void insert(size_t pos, char ch);在指定的位置插入一个字符ch。这里代码运行的原理就是挪动数据。跟之前顺序表哪里一样的。
cpp
void string ::insert(size_t pos, char ch)//在pos点插入一个字符
{
assert(pos < _size);
if (_size = _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t end = _size+1;//这里从'\0'的下一个位置开始插入,防止pos<=end在比较的过程中的隐式类型转换
while (pos<end)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void insert(size_t pos, const char* str);在pos点插入一个字符串。原理还是挪动数据,只不过这里是挪动str的大小的数据。
cpp
void string:: insert(size_t pos, const char* str) //在pos插入一个字符串
{
assert(pos < _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size < 2 * _capacity ? len + _size : 2 * _capacity);
}
size_t end = len + _size;
while (pos+len-1 < end)//这里要仔细思考,建议画图了解,跟上面插入一个字符的相似
{
_str[end]= _str[end - len];
--end;
}
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void erase(size_t pos, size_t len= npos):删除pos之后的len个字符。这个要分两种情况,一种是想删除的数据的大小要大于或等于字符串的大小。第二种就是小于字符串的大小的。npos是最大有效数据的大小。
cpp
void string:: erase(size_t pos, size_t len )//删除pos之后的len个字符
{
assert(pos<_size);
if (len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos + len; i <= _size; i++)//挪动数据
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t find(char ch, size_t pos = 0);查找。找到了就返回对应的位置。没有找到就返回npos
cpp
size_t string::find(char ch, size_t pos)//查找
{
for (int i = pos; i < _size; i++)
{
if (ch == _str[i])
return i;
}
return npos;
}
string subtr(size_t pos, size_t len=npos);拷贝pos到len之间的数据。这里分两种情况。len大于或等于_size-pos。那就要重新更新一下len。len的大小就是_size-pos。另一种则是小于。大小就是len。
cpp
string string ::substr(size_t pos, size_t len )//拷贝pos到len之间的数据
{
assert(pos <= _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);//提前开好空间,提高效率
for (int i = 0; i <len; i++)
{
sub += _str[i + pos];
}
return sub;
}
注意:这里我们在调用这个函数的时候,一定要写拷贝构造函数。因为我们这里是传值传参,那就要调用拷贝构造。当我们没有显示写拷贝构造的时候,编译器会自动生成一个拷贝构造函数。但是这里是自定义类型,所以这里当没有显示写拷贝构造时,默认为浅浅拷贝。自定义类型的浅拷贝呢会存在一个问题就是这里会调用两次析构函数。同一块空间是不可以同时析构两次的。这里相当于野指针一样。
这里sub会拷贝到这个临时对象中,然后在拷贝给ret,这里有的编译器这里会优化。但是这里sub是出了作用域就会销毁。销毁了是不是他指向的空间都被销毁了啊。那ret也是指向sub指向的空间。那是不是就指向一个野指针啊?
ostream& operator<<(ostream& out, const string& s);流提取。
cpp
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
istream& operator >> (istream& in, string& s);流插入,这里我们可以为了提高效率,来定义一个数组,依次放入数据,当放满了在插入到字符串中。这里就是减少了开空间次数。当然这里可以根据个人爱好来决定程序运行的效率。只不过这样效率要高一点。
cpp
istream& operator >> (istream& in, string& s)
{
const int N = 10;
char buff[N];
int i = 0;
//char buff[256] = { 0 };
char ch;
ch=in.get();//一个一个字符的提取
while (ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch= in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
拷贝构造的现代写法:
cpp
void swap(string& s)
{
std::swap(_str, s._str); //这里要用库里面的交换函数,不然他会调用他自己
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)//拷贝构造的现代写法,让编译器自己去构造,然后在交换
{
string tmp(s._str);
swap(tmp);
}
string(const string& str)//拷贝构造的传统写法
{
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
3.string.cpp文件
cpp
#include"string.h"
namespace S
{
void string::reserve(size_t n)//扩容
{
if (n > _capacity)//这里是为了这个函数单独使用
{
char* tmp = new char[n + 1];//多开一个空间
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void string::push_bank(char ch)//插入
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//这里特别要注意
}
string& string ::operator=(const string& s)//赋值
{
if (this!= &s)//这里就是小心自己给自己赋值的情况
{
delete[]_str;//先释放掉原来的空间
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
string& string::operator+= (char ch)//+=一个字符
{
push_bank(ch);
return *this;
}
void string::append(const char* str)//在字符串之后添加一个字符串
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//小于2*_capacity,要多少空间就给多大的空间。如果大于就按2*_capacity来扩容
reserve(len + _size < 2 * _capacity ? len + _size : 2 * _capacity);
}
strcpy(_str + _size, str);
/*for (int i = 0; i < len; i++)//这里的效果跟strcpy是一样的
{
_str[_size+i] = str[i];
}
_str[_size+len] = '\0';
_size += len;*/
}
string& string ::operator+=(const char* _str)//+=一个字符串
{
append(_str);
return *this;
}
void string ::insert(size_t pos, char ch)//在pos点插入一个字符
{
assert(pos < _size);
if (_size = _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t end = _size+1;
while (pos<end)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void string:: insert(size_t pos, const char* str) //在pos插入一个字符串
{
assert(pos < _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size < 2 * _capacity ? len + _size : 2 * _capacity);
}
size_t end = len + _size;
while (pos+len-1 < end)//这里要仔细思考,建议画图了解
{
_str[end]= _str[end - len];
--end;
}
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void string:: erase(size_t pos, size_t len )//删除pos之后的len个字符
{
if (len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)//查找
{
for (int i = pos; i < _size; i++)
{
if (ch == _str[i])
return i;
}
return npos;
}
string string ::substr(size_t pos, size_t len )//拷贝pos到len之间的数据
{
assert(pos <= _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);//提前开好空间,提高效率
for (int i = 0; i <len; i++)
{
sub += _str[i + pos];
}
return sub;
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
istream& operator >> (istream& in, string& s)
{
const int N = 10;
char buff[N];
int i = 0;
//char buff[256] = { 0 };
char ch;
ch=in.get();
while (ch != '\n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch= in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
//in >> ch;
//while (ch!=' '&&ch!='\0')//in在输入和提取任何类型的值时默认空格和换行都是分隔符,
// //所以这里就要用到get().
//{
// s += ch;
// cin >> ch;
//}
return in;
}
void test_string5()
{
string s("hello world");
string ret = s;
cout << ret.c_str() << endl;
/*size_t ret = s.find('w');
string s1=s.subtr(ret);
cout << s1.c_str() << endl;
string copy(s);*/
//cout << copy.c_str() << endl;
}
4.test.cpp文件
cpp
#include"string.h"
int main()
{
S::test_string5();
return 0;
}
三、auto和范围for
这里小编补充一个知识点,就是auto关键字和范围for。
- auto声明的变量必须由编译器在编译时期推导而得
- auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
- .auto不能直接用来声明数组
- 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
cpp
#include<iostream>
using namespace std;
int main()
{
auto a = 10;
auto b= 2.2;
auto d = 'c';
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(d).name() << endl;
auto aa = 1, bb = 2;
// 编译报错:error C3538: 在声明符列表中,"auto"必须始终推导为同一类型
auto cc = 3, dd = 4.0
// 编译报错:error C3318: "auto []": 数组不能具有其中包含"auto"的元素类型
auto array[] = { 4, 5, 6 }
}
auto的用途:
cpp
int main()
{
std::map<std::string, std::string> dict = { { "apple", "苹果" },{ "orange",
"橙子" }, {"pear","梨"} };
// auto的用武之地
//std::map<std::string, std::string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
return 0;
}
之前实现的迭代器代码那里也可以这样写。
//string::iterator it = s1.begin();
auto it=s1.begin();
while (it != s1.end())
{
cout << *it;
++it;
}
cout << endl;
2.范围for
- 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号" :"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
- 范围for可以作用到数组和容器对象上进行遍历
- 范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
cpp
#include<iostream>
#include <string>
#include <map>
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");
for (auto ch : str)//依次把str中的数据给ch
{
cout << ch << " ";
}
cout << endl;
return 0;
}
今天的分享就到这里吧,祝大家天天开心!