[C++]string类的使用和模拟实现

目录

[一. 总述:string类函数风格介绍](#一. 总述:string类函数风格介绍)

[1.1 参数风格](#1.1 参数风格)

[1.2 迭代器里const函数和非const函数](#1.2 迭代器里const函数和非const函数)

[1.3 string类里的隐式类型转换](#1.3 string类里的隐式类型转换)

[二. string类介绍和实现](#二. string类介绍和实现)

[2.1 (默认/拷贝)构造函数和析构函数](#2.1 (默认/拷贝)构造函数和析构函数)

[2.2 一些简单的直接内联的函数](#2.2 一些简单的直接内联的函数)

[2.2.1 迭代器](#2.2.1 迭代器)

[2.2.2 建议内联的函数](#2.2.2 建议内联的函数)

[2.2.3 输入输出函数](#2.2.3 输入输出函数)

[2.2.4 就像原来实现顺序表一样,增删查改起手了](#2.2.4 就像原来实现顺序表一样,增删查改起手了)

1.扩容

[2.增 (operator+=/ append/ push_back)](#2.增 (operator+=/ append/ push_back))

3.删 (erase / pop_back)

[4. 查 (find / rfind / find_first_of / find_first_not_of)](#4. 查 (find / rfind / find_first_of / find_first_not_of))

[5.插 (insert)](#5.插 (insert))


string类作为c++学习STL入门很必要的一课,突如其来多而杂的知识,有时会把人弄昏,本文来带大家理清一下string类的使用

string类文档https://cplusplus.com/reference/string/由于编码原因,c++提供了很多的string类兼容各种编码方式,宽字符,窄字符,16位的,32位的都有,本文以最基本的string类举例。

一. 总述:string类函数风格介绍

本文先做一个大体的介绍,再逐个介绍里面的接口

1.1 参数风格

string类提供的接口是很多的,差不多100多个,除了要记住常用函数的的用法,也要记住参数有多少个,顺序是什么,这显然是一个很大的学习成本。但好在string类的风格相对固定,基本看见函数,就知道参数怎么写了。

string类操纵的是字符串,一般提供四种方式

cpp 复制代码
// 传的参数,越往后,就越是附加,越可以不传(只要能满足完成这个函数的最低要求)
// 比如第一种,只传第一个参数,看文档,有专门的构造函数了,传第一个和第二个,第三个是缺省
// 参数的相对顺序基本不变,掌握一个函数的参数顺序,其他的参数顺序,基本就能推出来了

// 第一种,传 string 类型
string (const string& str, size_t pos, size_t len = npos);

// 第二种,传 char* 类型
string (const char* s, size_t n);

// 第三种,传 char 类型
string (size_t n, char c);

// 第四种, 传 迭代器
template <class InputIterator>  string  (InputIterator first, InputIterator last);

1.2 迭代器里const函数和非const函数

随便点开一个迭代器,基本都会发现有两个版本,一个const版本,一个非const版本,这是为什么呢?为什么只有迭代器或者 operator[] 才有这种设定?

迭代器类似指针能够访问字符串,我们不想他被修改时用const,可以接受被修改时,不用const,那这跟提供两种接口,有什么关系呢?假如我们传一个const变量来,那显然是无法用非const接收的,你写了,编译器也不会让你过。那我们只留const版本呢?那也不行,有时,你确实有修改变量的需求,所以,一定要非const版本。当然,你模拟实现只要const,也行。

分享一个实际写成员函数时的问题:函数是const函数,但就是定义不了迭代器,就是因为没有用const的迭代器。(一定是要写成员函数才有),下图简化了场景,可见,就是过不去。

1.3 string类里的隐式类型转换

当你写完构造函数时,其他函数只保留 string 类接口就行了,因为你传char*过去,会进行隐式类型转换,转成string类,功能不变。写了也行,因为这样可以减少拷贝构造,提高一点性能。

二. string类介绍和实现

先实现短小的,放在类里,因为放在类里可以内联,对于频繁调用的可以提高一定的性能

2.1 (默认/拷贝)构造函数和析构函数

默认成员函数都是有返回值的,这里有一个好的点就是测试时,用匿名对象,可以简化很多代码。

建议默认构造和带参构造一起写了,也就是用全缺省的构造函数。

cpp 复制代码
	// 默认构造 + 
	String(const char* s = "")
	{
		_size = strlen(s);
		_capacity = _size <= 15 ? 15 : _size;
		_str = new char[_capacity + 1];
		strcpy(_str, s);
	}

关于构造函数,这里有一些小坑。第一,忘记给'\0'开空间,capacity是有效字符的容量,而不是要开的空间数量;第二,默认构造函数时,没有开空间,直接初始化为nullptr,这样会导致一个问题,打印时,解析空指针的问题。实际上,应该为'\0'开一个空间。总之,不管是传参或者不传参的情况,都不要忘记'\0'.

因为自己申请了空间,所以一定要写析构函数,有析构函数,就一定要写拷贝构造进行深拷贝,否则会面对同一空间释放两次的问题。传值就要调用拷贝构造,因为拷贝构造还没写,所以必须传引用

cpp 复制代码
	~String()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
	// 深拷贝
	String(const String& str)
	{
		_size = str._size;
		_capacity = str._capacity;
		_str = new char[_capacity + 1];
		strcpy(_str, str._str);
	}

其实深拷贝还有一种更现代化的写法,更通用一些,上面那个写法只能用于string类,不通用,关键在strcpy上,你换其他的就不能这样了。在拷贝函数内部,怎么初始化str,就怎么弄个新的,说白了就是复用, 能通过前面写的函数造个新的

cpp 复制代码
String(const String& s)
: _str(nullptr)
{
String strTmp(s._str);
swap(_str, strTmp._str);
}

// 赋值运算符也是一样的
	String& String::operator=(String str)
	{
		swap(str);
		return *this;
	}

2.2 一些简单的直接内联的函数

2.2.1 迭代器

cpp 复制代码
	using iterator = char*;
	using const_iterator = const char*;

迭代器要先typedef一下,由于数组的特殊性,可以用原生指针进行模拟,写了以后就可以用auto for了。

2.2.2 建议内联的函数

cpp 复制代码
// 迭代器
iterator begin();const_iterator begin() const;
iterator end();const_iterator end() const;

// 跟容量有关的
size_t size() const;
size_t length() const;
size_t max_size() const;
void clear();
bool empty() const;

// 成员访问
char& operator[] (size_t pos);const char& operator[] (size_t pos) const;
char& at (size_t pos);const char& at (size_t pos) const;
char& back();const char& back() const;
char& front();const char& front() const;

// 字符串操作
const char* c_str() const;

这些函数就是几乎一行代码的事,直接内联。

2.2.3 输入输出函数

cpp 复制代码
istream& operator>> (istream& is, string& str);
ostream& operator<< (ostream& os, const string& str);

输入输出函数必须重载为全局函数

<< / >>是运算符

(ostream 对象) << (任意类型对象)

成员函数,第一个参数都是this指针,成员函数,就是

(任意类型对象) << (ostream 对象)

s.operator<<(cout); // 完全不符合直觉

s.operator>>(cin);

关于输出,直接拿到用c_str()打印即可,对于输入,有一个问题,假如我要输入一个带空格的字符串,由于空格和'\n'默认是分割符,输入"1234 5",按理说应该只要"1234",但空格是分隔符,实际上"12345"都会读,因此,我们要一个一个读和写

cpp 复制代码
	istream& operator>>(istream& is, String& str)
	{
		str.clear();
		char ch;
		while (is.get(ch) && ch != ' ' && ch != '\n')
		{
			str += ch;
		}
		return is;
	}

2.2.4 就像原来实现顺序表一样,增删查改起手了

1.扩容

要进行增删查改,首先就要先写扩容和缩容的函数。也就是reserve,这个传进去n的时候,现在的空间比n小,就扩容,比n小,就不变(实现简单一些),注意扩容时,我们用的是new/delete,没有用realloc,要自己再复制移动一遍数据。拷贝赋值后,释放旧空间。

cpp 复制代码
	void String::reserve(size_t n)
	{
		if (n <= _capacity)
		{
			return;
		}
		else
		{
			if (n > _capacity)
			{
				_capacity = 2 * _capacity < n ? n : 2 * _capacity;
			}
			char* tmp = new char[_capacity + 1] {'\0'};

			strcpy(tmp, _str);

			delete[] _str;
			_str = tmp;
		}
	}
2.增 (operator+=/ append/ push_back)

尾插没什么难度,注意最后要补个'\0'

3.删 (erase / pop_back)

尾删也没啥难度,重点是erase,这就涉及到挪动数据的问题了,如果用memmove就会好很多。

4. 查 (find / rfind / find_first_of / find_first_not_of)

遍历一遍,相等了就找到了,返回他的下标,没找到,就返回npos

5.插 (insert)

首先,关于at和[]以及迭代器能访问的地方是不一样的。由图1,迭代器在'\0'处访问就会崩,由图2和3,[]访问'\0'不会,但访问'\0'之外就会,由图4,at访问'\0'也会崩

我们插入字符,就是在pos位置,将包括pos后len个字符,向右挪动,然后再从pos依次插入,这里要注意,size_t逆序遍历的问题,也就是头插的问题,如果头插循环条件>=pos,pos走到0,--后并不是-1,因为是size_t,会变成npos,导致无限循环,用迭代器或者控制好变量的赋值都好说。

cpp 复制代码
	String& String::insert(size_t pos, const String& str)
	{
		size_t len = str._size;
		while (_capacity < _size + len)
		{
			_capacity = 2 * _capacity + 1;
		}
		reserve(_capacity);
		// 循环变量的看待
		size_t end = _size + len - 1;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];  
			end--;
		}

		for (size_t i = pos; i < len + pos; i++)
		{
			_str[i] = str._str[i - pos];
		}
		_size += len;
		_str[_size] = '\0';
		cout << _str << endl;
		return *this;
	}

最后就到这里,string类学起来主要是一下子接触太多陌生的东西,但跟背单词一样,每个函数其实并不复杂。

相关推荐
6***83051 小时前
VMware虚拟机配置桥接网络
开发语言·网络·php
LaoZhangGong1231 小时前
“do{}while(0)”的作用
c++·mfc
代码游侠1 小时前
数据结构——单向链表
linux·开发语言·数据结构·学习·算法·链表
csbysj20201 小时前
C 标准库 - <time.h>
开发语言
h***59331 小时前
JAVA进阶 Thread学习06 synchronized关键字
java·开发语言·学习
j***48541 小时前
【JSqlParser】Java使用JSqlParser解析SQL语句总结
java·开发语言·sql
2301_795167201 小时前
Python 高手编程系列一十五:使用 __new __()方法覆写实例创建过程
开发语言·网络·python
如意.7591 小时前
【C++】——异常
java·开发语言
Elnaij1 小时前
从C++开始的编程生活(14)——容器适配器——stack和queue
开发语言·c++