C++ String类(带你一篇文章搞定C++中的string类)

LaNzikinh篮子 2024-09-12 16:35:01 阅读 58

感谢大佬的光临各位,希望和大家一起进步,望得到你的三连,互三支持,一起进步

数据结构习题_LaNzikinh篮子的博客-CSDN博客

初阶数据结构_LaNzikinh篮子的博客-CSDN博客

收入专栏:C++_LaNzikinh篮子的博客-CSDN博客

其他专栏:c语言基础_LaNzikinh篮子的博客-CSDN博客

个人主页:LaNzikinh-CSDN博客

文章目录

前言一.auto和范围for二.sting类的常用接口三.string类的模拟实现总结


前言

我们前面对C++的泛型编程做了了解,然后我们继续来说,C++标准库里string类


一.auto和范围for

1.1迭代器

如果我们要便利一串数字,我们在C语言中遍历东西,我们都会想到利用这个循环来遍历,而在C++中,我们有一个全新的语法来遍历东西,那就是迭代器。每种数据结构都有一种对应的迭代器,迭代器一般实现为容器的嵌套类型,在容器内部提供具体的实现。但是容器不同,底层元素遍历的方式也不同,那么为什么说迭代器遍历所有容器的方式是一样的呢?那是因为迭代器提供了常用的operator!=,operator++,operator*等运算符的重载函数,把迭代容器的细节全部隐藏在这些通用的运算符重载函数里面,因此用户侧表现出来的就是,迭代器遍历所有容器的方式都是一样的,其实底层都是不一样的。这里不做过多的解释。我们来看下面的例子

<code>string s1;

string s2("hello world");

//s2[0] = 'x';

//cout << s2 << endl;

//下标遍历

for (size_t i = 0; i < s2.size(); i++)

{

cout << s2[i];

}

cout << endl;

//迭代器遍历,it看作一个指针

string::iterator it = s2.begin();

while (it != s2.end())

{

cout << *it;

it++;

}

第一个,我们就是正常以前C语言当中的下标遍历循环遍历,下面那一种就是我们的这个迭代器遍历。

迭代器除了正向迭代器,还有反向迭代器,反向迭代器字面意思就是从后往前遍历,迭代器一共有四种,正向iterator,反向reverse_iterator,常量迭代器const_iterator,反向常量迭代器reverse_const_iterator,这四种。

string::rever_iterator rit=s1.begin();

while(rit!=si.end())

{

rit++;

}

1.2auto和范围for

为什么要讲这么多迭代器的东西呢?就是因为auto和范围for的底层就是迭代器.

//auto,范围for,字符赋值,自动迭代,自动判断结束

// 底层就是迭代器

for (auto& ch : s2)

{

cout << ch;

}

auto会自动推到类型,s2自动赋值给ch。

二.sting类的常用接口

2.1size()和capacity()

这两个就是来看一下你这个里面的容量是多少空间是多少,size():返回字符串有效字符长度,capacity():返回空间总大小,补充:length(),也可以返回字符串有效字符长度

string s1("hello world");

cout << s1.size()<<" "<< s1.capacity();

max_size()就是容量的最大值

2.2reserve()和resize(),注意:reverse()

这两个函数是经常容易搞混的两个函数,一个是开辟空间,你给一个值,我直接给你开通那么大,还有一个就是逆置函数,可以将一串字符给逆置

reserve():为字符串预留空间

<code>void test_string2()

{

string s1("hello world");

cout << s1.size() << " " << s1.capacity();

cout << endl;

s1.reserve(100);

cout << s1.size() << " " << s1.capacity();

}

由实验可知,在string中,reserve()是不会缩容的,开了多少就是多少

注意在string中没有逆置

resize()调整字符串大小,将有效字符的个数该成n个,多出的空间用字符c填充

2.3插入删除字符函数

s.inset(插入),s.erase(删除),s.append(尾插字符串),s.push_back(尾插一个字符),operator+=(尾插入)

<code>string s("hello world");

s += "io";

cout << s << endl;

s.append("yyy");

cout << s << endl;

//下标控制

s.insert(0, "end");

cout << s << endl;

//下标控制

s.erase(6, 10);

cout << s << endl;

2.4replace(),clear(),empty()

replace()是替换字符的意思可以将字符串中的字符替换

string s2("hello world");

s2.replace(5,1,"**");

cout << s2 << endl;

通过下标控制的

empty():检测字符串释放为空串,是返回true,否则返回false,clear():清空有效字符

2.5substr(),find(),operator+

substr():在str中从pos位置开始,截取n个字符,然后将其返回

find():从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

<code>string s("hello world");

int a = s.find("h");

cout << a << endl;

注意非成员函数operator+:尽量少用,因为传值返回,导致深拷贝效率低

2.6输入输出流

他们都属于string类非成员函数

operator>>输入流,operator<<输出流

但是如果输入一个空格,就无法接收到了,该怎么办呢?

getline()获取一行字符串,特别的输入,遇到空格不会结束会继续输入数据

2.7特殊函数c_str()返回C格式字符串

他的本质是返回底层空间的指针,比如想用printf输出string,printf接收的是指针,没办法直接输出string,所以就可以用string.c str()进行输出

三.string类的模拟实现

在这里只会实现在string中重要的成员函数,要了解他们的底层逻辑,其他的会使用的可以了

我们先来看他的头文件

<code>using namespace std;

namespace lanzi

{

class string

{

public:

typedef char* iterator;

typedef const char* const_iterator;

const_iterator begin() const

{

return _str;

}

const_iterator end() const

{

return _str + _size;

}

iterator begin()

{

return _str;

}

iterator end()

{

return _str+_size;

}

string(const char* str = " ")

{

_size = strlen(str);

_capacity = _size;

_str = new char[_capacity + 1];

strcpy(_str, str);

}

//深拷贝

//s1(s2);

string(const string& s)

{

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

// s2 = s1

// s1 = s1

//赋值

string& operator=(const string& s)

{

if (this != &s)

{

// s1 = s1

delete _str;

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

return *this;

}

const char* c_str() const

{

return _str;

}

~string()

{

delete[] _str;

_str = nullptr;

_capacity = _size = 0;

}

void clear()

{

_str[0] = '\0';

_size = 0;

}

size_t size() const

{

return _size;

}

size_t capacity() const

{

return _capacity;

}

char& operator[](size_t pos)

{

assert(pos < _size);

return _str[pos];

}

void reserve(size_t n);

void push_back(char ch);

void append(const char* str);

string& operator+=(char ch);

string& operator+=(const char* str);

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);

size_t find(const char* str, size_t pos = 0);

string substr(size_t pos = 0, size_t len = npos);

private:

//char _buff[16];

char* _str;

size_t _size;

size_t _capacity;

//static const size_t npos = -1;

static const size_t npos;

};

bool operator<(const string& s1, const string& s2);

bool operator<=(const string& s1, const string& s2);

bool operator>(const string& s1, const string& s2);

bool operator>=(const string& s1, const string& s2);

bool operator==(const string& s1, const string& s2);

bool operator!=(const string& s1, const string& s2);

ostream& operator<<(ostream& out, const string& s);

istream& operator>>(istream& in, string& s);

}

要实现的就是这样,现在我们一一来实现

3.1构造与析构

我们先来完成最简单的构造与析构函数,注意:在string中是存在\0的,所以自己模拟实现的时候一定要注意这个细节。

初始化

用new来开辟一个空间,但是要存放\0,然后把size,capacity都赋为0

string() :_str(new char[1]{'\0'}), _size(0), _capacity(0)

{}

构造

其实和初始化是可以进行合并的,但是要注意_capacity不包含\0,所以在开空间的时候要多开一个,用来存放\0,用缺省值来实现,如果你不传东西,我就给你初始化,反之构造

string(const char* str = " ")

{

_size = strlen(str);

// _capacity不包含\0,所以在开空间的时候要多开一个,用来存放\0

_capacity = _size;

_str = new char[_capacity + 1];

strcpy(_str, str);

}

析构

把开辟的空间全部释放即可

~string()

{

delete[] _str;

_str = nullptr;

_capacity = _size = 0;

}

3.2赋值与拷贝构造

当String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认 的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内 存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。所以这里我们需要自己来实现的赋值和拷贝构造的深拷贝

拷贝构造

<code>//深拷贝

//s1(s2);

string(const string& s)

{

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

赋值:存在特殊情况,自己给自己赋值

// s2 = s1

// s1 = s1

//赋值

string& operator=(const string& s)

{

if (this != &s)

{

// s1 = s1

delete _str;

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

return *this;

}

3.3容易的成员函数

不做讲解直接代码

迭代器相关的成员函数

typedef char* iterator;

typedef const char* const_iterator;

const_iterator begin() const

{

return _str;

}

const_iterator end() const

{

return _str + _size;

}

iterator begin()

{

return _str;

}

iterator end()

{

return _str+_size;

}

clear函数,size,capacity函数,

void clear()

{

_str[0] = '\0';

_size = 0;

}

size_t size() const

{

return _size;

}

size_t capacity() const

{

return _capacity;

}

[]引用函数

char& operator[](size_t pos)

{

assert(pos < _size);

return _str[pos];

}

以上这些函数都是作为内联函数,在类里面实现的,因为他们经常反复调用,所以在类里用内联函数去实现,节省效率

3.4reserve()扩容函数

动态开辟一个指定空间,然后利用strcpy去实现复制

void string::reserve(size_t n)

{

if (n > _capacity)

{

char* tmp = new char[n + 1];

strcpy(tmp, _str);

delete[] _str;

_str = tmp;

_capacity = n;

}

}

3.5尾插函数push_back

如果满了,就扩容

void string::push_back(char ch)

{

if (_size == _capacity)

{

reserve(_capacity == 0 ? 4 : _capacity * 2);

}

_str[_size] = ch;

_size++;

_str[_size] = '\0';

}

3.6尾插字符串append()

因为是字符串,所以和字符有区别,要注意实现

void string::append(const char* str)

{

size_t len = strlen(str);

if (_size+len > _capacity)

{

reserve(_size + len>2* _capacity? _size + len:2* _capacity);

}

strcpy(_str + _size, str);

_size += len;

}

3.7+=实现尾插

直接复用

string& string::operator+=(char ch)

{

push_back(ch);

return *this;

}

string& string::operator+=(const char* str)

{

append(str);

return *this;

}

3.8插入函数insert()

插入一个字符

往后移动,效率极差

void string::insert(size_t pos, char ch)

{

if (_size == _capacity)

{

reserve(_capacity == 0 ? 4 : _capacity * 2);

}

//存在\0

int end = _size + 1;

while (end > pos)

{

_str[end] = _str[end - 1];

end--;

}

_str[pos] = ch;

_size++;

}

插入一个字符串

先将原来的字符移动位置,然后在指定位置上插入

void string::insert(size_t pos, const char* s)

{

size_t len = strlen(s);

if (_size + len > _capacity)

{

reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);

}

int end = _size + len;

//注意,画图

while (end > pos+len-1)

{

_str[end] = _str[end - len];

end--;

}

for (size_t i = 0; i < len; i++)

{

_str[pos + i] = s[i];

}

_size += len;

}

3.9删除函数erase()

pos表示起始位置,len为要删除的长度,如果长度高于后面大小就全部删除,如果没有,就删指定大小

void string::erase(size_t pos, size_t len = npos)

{

assert(pos > _size);

if (len > _size - pos)

{

_str[pos] = '\0';

_size = pos;

}

else

{

for (size_t i = pos + len; i <= _size; i++)

{

_str[i - len] = _str[i];

}

_size -= len;

}

}

3.11查找函数find()

查找一个字符

根据所给的位置,一一遍历,找到就返回,没有就返回空

size_t string::find(char ch, size_t pos)

{

assert(pos > _size);

{

for (size_t i = pos; i <= _size; i++)

{

if (_str[i] == ch)

return i;

}

return npos;

}

}

查找一个字符串

直接利用函数strstr去找

size_t string::find(const char* str, size_t pos)

{

assert(pos > _size);

{

char* ptr = strstr(_str + pos, str);

if (ptr == nullptr)

{

return npos;

}

else

{

//因为在string中,都是返回下标所以要减去_str

return ptr - _str;

}

}

}

3.21生成子串函数substr()

 len大于剩余字符长度,更新一下len,然后赋值

string string::substr(size_t pos, size_t len)

{

assert(pos < _size);

// len大于剩余字符长度,更新一下len

if (len > _size - pos)

{

len = _size - pos;

}

string sub;

sub.reserve(len);

for (size_t i = 0; i < len; i++)

{

sub += _str[pos + i];

}

return sub;

}

3.31比较函数

只要实现两个,其他都可以复用

bool operator<(const string& s1, const string& s2)

{

return strcmp(s1.c_str(), s2.c_str()) < 0;

}

bool operator<=(const string& s1, const string& s2)

{

return s1 < s2 || s1 == s2;

}

bool operator>(const string& s1, const string& s2)

{

return !(s1 <= s2);

}

bool operator>=(const string& s1, const string& s2)

{

return !(s1 <s2);

}

bool operator==(const string& s1, const string& s2)

{

return strcmp(s1.c_str(), s2.c_str()) == 0;

}

bool operator!=(const string& s1, const string& s2)

{

return !(s1 == s2);

}

3.41流插入提取函数

流提取,利用auto,for来遍历

ostream& operator<<(ostream& out, const string& s)

{

for (auto& ch : s)

{

out << ch;

}

return out;

}

流插入

注意:流插入遇到空格就停止接受,所以要用get()来接受空格,先创建一个buff数组,提高效率,利用get来接收字符,如果不是空和/0的话,就把它放到数组里,如果数组满了的话,就直接结束,将buff数组的值全部放进string的类中,没有的话就继续获取数据

//很难

istream& operator>>(istream& in, string& s)

{

s.clear();

const int N = 256;

char buff[N];

int i = 0;

char ch;

ch = in.get();

while (ch != ' ' && ch != '\0')

{

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;

}

}


总结

string类是C++标准库里的一个类,为什么学习string类,因为C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户 自己管理,稍不留神可能还会越界访问。其实在标准库还有很多这里内容,下次在一一讲解



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。