C++标准模板库STL之容器--string

STL简介

        STL(standard template libaray - 标准模板库)是C++标准库的重要组成部分,不仅是一个可复用的组件库,还是一个包罗了数据结构与算法的软件框架。

STL的六大组件及相关函数
仿函数greater、less……
算法find、swap、reverse、sort、merge……
迭代器iterator、const_iterator、reverse_iterator、const_reverse_iterator
空间配置器allocator
容器string、vector、list、deque、map、set、multimap、multiset
配接器stack、queue、priority_queue

        对于STL的学习,第一步是要熟练使用STL,第二步是了解泛型技术的内涵和STL的原理并进行实操,第三步是对STL进行扩展。所以接下来的一段时间,我会将STL的各种容器和重要函数进行讲解,首先从string开始。

string类

        在C语言中,字符串是以' \0 '为结尾的字符集合,为了方便操作,C语言标准库中提供了一些  str系列的库函数,但是这些库函数与字符串是分离开的,不符合面向对象的思想,而且底层空间需要用户自己管理,稍不留神可能就会越界访问。在C++中针对这些问题做了一系列工作,即在C++标准库中定义了string类。

标准库中的string类

        string类的文档介绍:string - C++ Reference (cplusplus.com)

        需要注意的是,使用string类时,必须包含#include头文件以及using namespace std;

auto 和 范围for

        在C++中补充了两个关键字,为了方便后续的学习,我们在此处先了解一下。

auto

  • 在早期C/C++中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,后来在C++11中,标准委员会变废为宝赋予了 auto 全新的含义:auto 不再是一个存储类型指示符,而是作为一种全新的类型指示符来指示指示编译器,auto 声明的变量必须有编译器在编译时期推导而得。
  • 用auto声明指针类型时,使用 auto 和 auto* 没有任何区别,但用auto声明引用类型时须加&。
  • 当在同一行声明多个变量时,这些变量必须是相同类型的,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
  • auto 不能作为函数的参数,可以做返回值,但是建议谨慎使用,
  • auto 不能直接用来声明数组。
  • 换取了遍历,但是牺牲了可读性。

代码讲解:

#include<iostream>
using namespace std;
int func1()
{
	return 10;
} 
// 不能做参数
void func2(auto a)	//X
{}
// 可以做返回值,但是建议谨慎使用
auto func3()
{
	return 3;
} 
int main()
{
	int a = 10;
	auto b = a;
	auto c = 'a';
	auto d = func1();
	// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项
	auto e;  //X

    //输出变量的类型名
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;

	int x = 10;
    //使用 auto 和 auto* 没有任何区别
	auto y = &x;
	auto* z = &x;
    //auto声明引用类型时须加&
	auto& m = x;

	cout << typeid(x).name() << endl;
	cout << typeid(y).name() << endl;
	cout << typeid(z).name() << endl;

	auto aa = 1, bb = 2;
	// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型
	auto cc = 3, dd = 4.0;  //X
	// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型
	auto array[] = { 4, 5, 6 };   //X
	return 0;
}

        很多人在接触 auto 的时候不明白它的意义,在我们未来学习的知识越来越复杂时,类型名也会越来越长,为了简化类型名的编写,auto 就有了意义。代码如下(无需看懂,了解即可):

#include<iostream>
#include <string>
#include <map>
using namespace std;
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;
}

范围for

        在C语言中,我们想要循环遍历连续的空间,需要先确定空间的范围,但是对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此在C++11中引入了基于范围的 for 循环。范围 for 后跟的括号中由冒号" : "分为两部分,第一部分是范围内用于迭代的变量,第二部分是被迭代的空间,范围 for 自动迭代,自动取数据,自动判断结束。一般我们使用范围 for 可以作用到数组和容器对象上进行遍历,且范围 for 的底层就是迭代器,因此所有容器都支持范围for。

#include<iostream>
#include <string>
using namespace std;
int main()
{
	int array[] = { 1, 2, 3, 4, 5 };

	// C++98的遍历:for循环
	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,自动赋值,自动迭代,自动判断结束。
	// 引用遍历数组:会修改array中的数据。
	for (auto& e : array)
		e *= 2;

	// 拷贝遍历数组:不会修改array中的数据。
	for (auto e : array)
		cout << e << " " << endl;

	// string类的遍历
	string str("hello world");
	for (auto ch : str)
	{
		cout << ch << " ";
	} 
	cout << endl;
	return 0;
}

string类中的常用接口

1、string类对象中的常见构造函数

函数名称功能说明
string();构造空的string类对象,即空字符串
string(const char* s);用C格式的字符串来构造string类对象
string(size_t n, char c);string类对象中包含n个字符l类型的c
string(const string& s);拷贝构造函数

 2、string类对象中的容量操作函数

函数名称功能说明
size返回字符串有效字符的长度
length返回字符串有效字符长度
capacity返回空间总大小
empty检测字符串释放为空串,是则返回true,不是则返回false
reserve为字符串预留(规定)空间
clear清空string类对象中的有效字符
resize将有效字符的个数改为n个,多出的空间用字符c填充

注意事项:

  1. size()与length()函数的方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,并且一般情况下基本都是size()函数。
  2. clear()函数只是将string中的有效字符清空,并没有改变底层空间的大小。
  3. resize(size_t n)与resize(sizr_t n, char c)都是将字符串中的有效字符个数修改为n个,不同点在于当字符个数增多时,resize(n)是用 0 来填充多出的元素空间,而resize(n,'c')是用字符c来填充多出来的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
  4. reserve(size_t res_arg = 0); 为string预留空间,reserve函数不会改变有效元素的个数,但是当reserve传入的参数小于string的底层空间总大小时,在vs编译器环境下,reserve不会改变容量大小,在g++编译器环境下,reserve可能会缩容。因此官方给的回答是发起一个不具约束力的请求,所以只是可能会缩容,真正决定看编译器。

 3、string类对象的访问及遍历操作函数

函数名称功能说明
operator[pos]运算符重载,返回pos位置的字符,const string类对象调用
begin获取string类对象第一个字符的迭代器
end获取string类对象最后一个字符下一个位置的迭代器
rebgin获取string类对象最后一个字符的迭代器
rend获取string类对象第一个字符前一个位置的迭代器
范围forC++11支持的更为简洁的遍历方式
#include<iostream>
using namespace std;

void Teststring4()
{
	string s("hello zy");
	// 3种遍历方式:
	// 需要注意的以下三种方式除了遍历string对象,还可以遍历修改string中的字符,
	// 另外以下三种方式对于string而言,第一种使用最多,因为可以修改数组。
	// 1. for+operator[]
	for (size_t i = 0; i < s.size(); ++i)
		cout << s[i] << endl;

	// 2.迭代器:在string中使用相对较少
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << endl;
		++it;
	}
	// 反向迭代器
	string::reverse_iterator rit = s.rbegin();
	while (rit != s.rend())
	{
		cout << *rit << endl;
		rit++;
	}
	const string s1("hello zy");
	// const迭代器
	string::const_iterator cit = s1.begin();
	while (cit != s1.end())
	{
		//*cit += 2;     cit不能赋值给常量,因为const。
		cout << *cit << endl;
		cit++;
	}
	// const反向迭代器
	string::const_reverse_iterator rcit = s.rbegin();
	while (rcit != s.rend())
	{
		//*rcit += 2;     rcit不能赋值给常量,因为const。
		cout << *rcit << endl;
		rcit++;
	}

	// string::reverse_iterator rit = s.rbegin();
	// C++11之后,直接使用auto定义迭代器,让编译器自动推导迭代器的类型
	//auto rit = s.rbegin();
	//while (rit != s.rend())
	//	cout << *rit << endl;

	// 3.范围for:遍历使用较多。
	//for (auto ch : s)
	//	cout << ch << endl;
}

4、string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插一个字符c
append在字符串后追加字符串
operator+=在字符串后追加字符串str
c_str返回c格式的字符串
find从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从第pos位置起截取n个字符,然后将其返回
erase删除str中的所有字符,不改变容量大小
insert插入字符串,位置由参数决定
replace替换pos位置的字符,可以一换多
find_first_of返回所给定字符串中任意一个字符的第一次出现的下标
find_last_of返回所给定字符串中任意一个字符的最后一次出现的下标

代码演示:

// 测试string:
// 1. 插入(拼接)方式:push_back  append  operator+= 
// 2. 正向和反向查找:find() + rfind()
// 3. 截取子串:substr()
// 4. 删除:erase
void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'b';           // 在str后追加一个字符'b'   
	str += "it";          // 在str后追加一个字符串"it"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串

	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;

	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;

	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");
	if (start == string::npos)
	{
		cout << "invalid url" << endl;
		return;
	}
	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;

	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}

注意:

  1. 在string尾部追加字符串时,s.push_back(c) 与 s.append(1,c) 和 s += 'c' 三种实现方式差不多,一般情况下string类的+=重载操作符使用较多,因为+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以通过reserve把空间预留好。

 5、string类的重载全局函数

函数名称功能说明
operator+

尽量少用,因为传值返回会导致深拷贝,降低效率

operator>>输入运算符重载
operator<<输出运算符重载
getline获取一行字符串
relational operator大小比较

注意:上面的几个接口大家需要了解,后面的OJ题目中会体现他们的用法。sting类中还有一些其他的操作,这里不一 一列举,大家在需要时自行查看文档即可。

6、vs和g++中string结构的说明

注意:下述结构实在32为平台下进行的验证,该平台下的指针变量占四个字节的空间。

  • vs下string的结构:string总共占28个字节,内部结构稍微复杂一些,显示有一个联合体,联合体用来定义string中字符串的存储空间:(1)当字符串的长度小于16时,使用内部固定的字符数组来存放。(2)当字符串的长度大于等于16时,则从堆上开辟空间存放字符串,不再使用数组。
union _Bxty
{ 
    // 用于小缓冲区的存储器或指向大缓冲区的指针
    value_type _Buf[_BUF_SIZE];    //_BUF_SIZE = 16
    pointer _Ptr;
    char _Alias[_BUF_SIZE]; // 允许混叠
} _Bx;

        这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那 string 对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

        其次,还有一个 size_t 字段保存字符串的长度,一个 size_t 保存从堆上开辟空间的总容量。即 size和 capacity。最后还有一个指针用于做其他事情,这里不作了解。因此在vs下的 string 总共占16+4+4+4=28个字节。

  • G++下string是通过写时拷贝来实现的,string对象总共占4个字节,内部只包含一个指针,该指针将来会指向一块堆空间,内部包含了如下字段:1.空间总大小。2.字符串有效长度。3.引用计数。
struct _Rep_base
{
    size_type _M_length;
    size_type _M_capacity;
    _Atomic_word _M_refcount;
};

string 练习

1、仅仅翻转字母

        给你一个字符串 s ,根据下述规则反转字符串:所有非英文字母保留在原有位置,所有英文字母(小写或大写)位置反转,返回反转后的 s 。

提示:

  • 1 <= s.length <= 100
  • s 仅由 ASCII 值在范围 [33, 122] 的字符组成
  • s 不含 '\"' 或 '\\'

 解析:

        这道题比较简单,思路是定义对象的起始点和终止点,通过两端向中间汇聚,当遍历的字符不是字母时继续遍历,为字母时停下,两端进行交换,直到起始变量不再小于终止变量,遍历结束。代码如下:

class Solution {
public:
    bool isLetter(char ch)
    {
        if(ch>='a'&&ch<='z')
            return true;
        if(ch>='A'&&ch<='Z')
            return true;
        
        return false;
    }
    string reverseOnlyLetters(string s) {
        int begin = 0, end = s.size() - 1;
        while (begin < end)
        {
            while (begin < end && !isLetter(s[begin]))
            {
                begin++;
            }
            while (begin < end && !isLetter(s[end]))
            {
                end--;
            }
            swap(s[begin++],s[end--]);
        }
        return s;
    }
};

2、字符串中的第一个唯一字符

        给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。

提示:

  • 1 <= s.length <= 10^5
  • s 只包含小写字母

解析:

        因为英文字母只有26个,因此我们开辟一个26字符的数组用来计算每个字符的个数,使用范围for遍历每一个string对象中的字符,在对应位置的数组上++,然后按照string中的字符顺序进行二次遍历,为的是找到数组中为1的字符,并返回下标位置。代码如下:

class Solution {
public:
    int firstUniqChar(string s) {
        int arr[26] = {0};
        for(auto ch : s)
        {
            arr[ch-'a']+=1;
        }
        for(int i = 0;i < s.size(); i++)
        {
            if(1 == arr[s[i]-'a'])
            {
                return i;
            }
        }
        return -1;
    }
};

3、字符串最后一个单词的长度

        计算字符串最后一个单词的长度,单词以空格隔开,字符串长度小于5000。(注:字符串末尾不以空格为结尾)。

输入描述:

        输入一行,代表要计算的字符串,非空,长度小于5000。

输出描述:

        输出一个整数,表示输入字符串最后一个单词的长度。

解析:

        本道题的难点在于流输入操作符cin不能接收第一个空格之后的字符,因此我们需要使用string的函数getline,该函数可以保留空白字符继续向后读取,直到遇到换行符 ' \n ' 。思路为使用getline读取字符串后,找到最后一个空格出现的位置pos,使用s.size() - pos - 1就得到答案了。这里要注意的是size()求出来的大小是字符个数,下标需要-1。代码如下:

#include <iostream>
#include <string>
using namespace std;

int main()
{
    string s;
    while(getline(cin, s))
    {
        size_t pos = s.rfind(' ');
        cout<< s.size() - pos - 1 <<endl;
    }
    return 0;
}

4、验证回文串

        如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 ,字母和数字都属于字母数字字符。

        给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 

提示:

  • 1 <= s.length <= 2 * 10^5
  • s 仅由可打印的 ASCII 字符组成

 解析:

        本道题的思路是将所有字母数字字符全部+=到一个新字符串,对新字符串进行逆置拷贝,并进行比较,若相等则为true,反之为false。代码如下:

class Solution {
public:
    bool isPalindrome(string s) {
       string s1;
       for(auto ch : s)
       {
            // 将小写字母转换成大写字母
            if(ch >= 'a' && ch <= 'z')
            {
                ch -= 32;
            }
            // 尾插字符
            if((ch >= '0' && ch <= '9')
            || (ch >= 'A' && ch <= 'Z'))
            {
                s1 += ch;
            }
       }
        // 拷贝构造
       string s2(s1);
        //逆置字符串
       reverse(s2.begin(),s2.end());
        // 判断是否回文
       return s1 == s2;
    }
};

5、字符串相加

        给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

提示:

  • 1 <= num1.length, num2.length <= 10^4
  • num1 和num2 都只包含数字 0-9
  • num1 和num2 都不包含任何前导零

 解析:

        这道题思考起来比较麻烦,因为字符串中存的数字位数可以超过整形最大值,因此stoi函数就不能用了,我们需要对两个字符串中的相同位置的字符相加后得到一个小于10的数,并要判断是否进位,因此我们使用size()函数算出两个字符串的最后一个有效字符的位置(个位),通过遍历的方法将相同位置的字符数字与进位变量 next 以整形的方式相加后+=到新的字符串中,直到两个位置变量都为小于0后结束遍历,翻转字符串就得到了相加后的字符数字。需要注意里面对字符+/- '0' 是为了转换为相应的数据类型。代码如下:

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size()-1, end2 = num2.size()-1;
        string str;
        int next = 0;
        while(end1 >= 0 || end2 >= 0)
        {
            int val1 = end1>=0 ? num1[end1]-'0' : 0;
            int val2 = end2>=0 ? num2[end2]-'0' : 0;
            end1--;
            end2--;
            int ret = val1 + val2 + next;
            next = ret / 10;
            ret %= 10;
            str += (ret+'0'); 
        }
        if(next == 1)
            str += '1';
        reverse(str.begin(),str.end());
        return str;
    }
};

string类的模拟实现

        在上面我们已经对string类进行了简单的讲解,各位只需要会正常使用即可。在面试中,面试官总是喜欢让面试者自己来模拟实现string类,最主要的是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。下面我会对以上的函数和一些常用函数进行模拟实现,并对一些常见问题进行说明与补充。

        浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

class string
{
public:
	void swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	//构造函数
	/*string()
		:_str(new char[1]{'\0'})
		,_size(0)
		,_capacity()
	{}*/
    //String(const char* str = "\0") 错误示范
    //String(const char* str = nullptr) 错误示范
	// 全缺省的拷贝构造即默认构造
	string(const char* str = "")
	{
		_size = strlen(str);
		_capacity = _size;
		// _capacity不包含\0
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	// 解决深拷贝问题的拷贝构造
	string(const string& s)
	{
		/*_size = s._size;
		_capacity = s._capacity;
		_str = new char[_capacity + 1];
		strcpy(_str, s.c_str());*/
		//现代写法
		string tmp(s._str);
		swap(tmp);
	}

	// s2 = s1
	/*string& operator=(const string& s1)
	{
		if (this != &s1)
		{
			delete[] _str;

			_str = new char[s1._capacity + 1];
			strcpy(_str, s1._str);
			_size = s1._size;
			_capacity = s1._capacity;
		}
		return *this;
	}*/
	//s2 = s1现代写法
	string& operator=(string tmp)
	{
		swap(tmp);

		return *this;
	}
	//析构函数
	~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
}

        在上面的代码中,我分别写了普通构造、拷贝构造的字符串传参和类对象传参两种重载形式、赋值重载和析构函数。在字符串传参的拷贝构造中我们如果给了缺省值,那它就可以代替普通构造(这里在类和对象章节讲过,有需要的同学可以翻一下前面的博客)。对比使用类对象传参的拷贝构造的传统写法和现代写法,传统写法的直接复制数据,容易造成浅拷贝问题,导致内存泄漏或对一块空间释放两次,我们需要进行深拷贝来解决此类问题(对申请空间的位置,重新申请空间并将原空间的数据拷贝到新空间);现代写法使用swap函数,先构造了一个临时对象,进行资源交换,这样不仅简化了代码,还避免了手动申请和释放空间,降低了出错风险。在赋值运算符重载中,传统写法需要先检查参数是否等于*this,再手动释放内存;而现代写法中使用接收值参数的方法通过swap进行资源交换,避免了显式释放空间和条件检查,简化了代码,提高了安全性。

	// c_str
	char* c_str() const
	{
		return _str;
	}
	// size
	size_t size() const
	{
		return _size;
	}
	// capacity
	size_t capacity() const
	{
		return _capacity;
	}
	// clear
	void clear()
	{
		_str[0] = '\0';
		_size = 0;
	}

	// []重载
	char& operator[](size_t pos)
	{
		return _str[pos];
	}
	const char& operator[](size_t pos) const
	{
		return _str[pos];
	}
	// 迭代器
	typedef char* iterator;
	iterator begin()
	{
		return _str;
	}
	iterator end()
	{
		return _str + _size;
	}
	// const迭代器
	typedef const char* const_iterator;
	const_iterator begin() const
	{
		return _str;
	}
	const_iterator end() const
	{
		return _str + _size;
	}

 这部分是对一些常用函数和迭代器的简单实现,并不复杂,各位自己看看模仿一下即可。

// reserve
void string::reserve(size_t n)
{
	if (n > _capacity)
	{			
		char* tmp = new char[n+1];
		strcpy(tmp, _str);
		_capacity = n;
		delete[] _str;
		_str = tmp;
	}
}

// push_back
void string::push_back(char ch)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';
}
// append
void string::append(const char* str)
{
	size_t length = strlen(str);
	if (_size + length > _capacity)
	{
		reserve(_size + length > 2 * _capacity ? _size + length : 2 * _capacity);
	}
	strcpy(_str + _size, str);
	_size += length;
}
// operator+=
string& string::operator+=(char ch)
{
	push_back(ch);
	return *this;
}
string& string::operator+=(const char* str)
{
	append(str);
	return *this;
}

// insert
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}
	// 避免size_t将负数转为正整数最大值问题
	size_t end = _size+1;
	while (end > pos)
	{
		_str[end] = _str[end-1];
		end--;
	}
	_str[end] = ch;
}
void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t length = strlen(str);
	if (_size + length > _capacity)
	{
		char* tmp = new char[_size + length > 2 * _capacity ? _size + length : 2 * _capacity];
	}
	size_t end = _size + length;
	//while (end > pos + length - 1)
	while (end >= pos + length)
	{
		_str[end] = _str[end - length];
		end--;
	}
	for (size_t i = 0; i < length; i++)
	{
		_str[pos + i] = str[i];
	}
	_size += length;
}
// erase
void string::erase(size_t pos, size_t len)
{
	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;
	}
}

// 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;
}
size_t string::find(const char* str, size_t pos)
{   //从第pos位置开始找第一个匹配的字符串
	assert(pos < _size);
	const char* ptr = strstr(_str + pos, str);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}
//substr
string string::substr(size_t pos, size_t len)
{
	assert(pos < _size);

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

 在上面的代码中:

  • reserve:用于确保字符串有足够的空间存储 n 个字符。如果 n 大于当前容量,就分配新的内存,拷贝现有内容,更新容量,并释放旧内存。

  • push_back:向字符串末尾添加一个字符。如果当前大小等于容量,调用 reserve 扩展容量(初始为 4 或翻倍)。然后将字符添加到末尾并更新大小。

  • append:添加一个 C 风格字符串到末尾,先检查是否需要扩展容量,然后使用 strcpy 将新字符串拷贝到当前字符串末尾,并更新大小。

  • operator+=:重载 += 运算符以支持字符和字符串的追加,调用 push_backappend

  • insert:在指定位置插入一个字符或字符串。如果插入后超出当前容量,则调用 reserve 扩展容量。然后通过移动后续字符来腾出空间。

  • erase:删除指定位置的字符或字符串。根据长度调整大小,并用后续字符覆盖已删除部分。

  • find:查找字符或子字符串,返回首次出现的位置。使用简单循环或 strstr 函数进行查找。

  • substr:返回从指定位置开始的子字符串,创建一个新的 string 对象并分配适当的空间。

// operator
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);
}

// 流插入、提取
ostream& operator<<(ostream& out, const string& s1)
{
	for (auto ch : s1)
	{
		out << ch;
	}
	return out;
}
istream& operator>>(istream& in, string& s1)
{
	s1.clear();

	const int N = 256;
	char buff[N];
	int i = 0;

	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		buff[i] = ch;
		i++;
		if (i == N - 1)
		{
			buff[i] = '\0';
			s1 += buff;
			i = 0;
		}
		ch = in.get();
	}
	if (i > 0)
	{
		buff[i] = '\0';
		s1 += buff;
	}
	return in;
}

 上面的代码是重载的全局函数:

  • 比较运算符

    • operator<operator<= 使用 strcmp 比较两个字符串,判断它们的字典序关系。
    • operator>operator>= 利用 operator<=operator< 进行逻辑反转,简化代码。
    • operator==operator!= 也使用 strcmp,分别判断相等和不相等,提供直观的比较功能。
  • 流插入

    • operator<< 实现了将自定义字符串输出到流中,使用范围基于的 for 循环逐字符输出,方便与标准输出流的集成。
  • 流提取

    • operator>> 从输入流中读取字符,直到遇到空格或换行。使用缓冲区来存储读取的字符,确保字符串的拼接,处理了缓冲区满的情况。

以上就是对string类的模拟实现,下面是完整代码:

string.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once

#include<iostream>
#include<assert.h>
using namespace std;
namespace zy
{
	class string
	{
	public:
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}

		//构造函数
		/*string()
			:_str(new char[1]{'\0'})
			,_size(0)
			,_capacity()
		{}*/
		// 全缺省的拷贝构造即默认构造
		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			// _capacity不包含\0
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 解决深拷贝问题的拷贝构造
		string(const string& s)
		{
			/*_size = s._size;
			_capacity = s._capacity;
			_str = new char[_capacity + 1];
			strcpy(_str, s.c_str());*/
			//现代写法
			string tmp(s._str);
			swap(tmp);
		}

		// s2 = s1
		/*string& operator=(const string& s1)
		{
			if (this != &s1)
			{
				delete[] _str;

				_str = new char[s1._capacity + 1];
				strcpy(_str, s1._str);
				_size = s1._size;
				_capacity = s1._capacity;
			}
			return *this;
		}*/
		//s2 = s1现代写法
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		// c_str
		char* c_str() const
		{
			return _str;
		}
		// size
		size_t size() const
		{
			return _size;
		}
		// capacity
		size_t capacity() const
		{
			return _capacity;
		}
		// clear
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		// []重载
		char& operator[](size_t pos)
		{
			return _str[pos];
		}
		const char& operator[](size_t pos) const
		{
			return _str[pos];
		}
		// 迭代器
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		// const迭代器
		typedef const char* const_iterator;
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}
		// reserve
		void reserve(size_t n);
		// push_back
		void push_back(char ch);
		// append
		void append(const char* str);
		// operator+=
		string& operator+=(char ch);
		string& operator+=(const char* str);

		// insert
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		// erase
		void erase(size_t pos, size_t len = npos);

		//find
		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		//substr
		string substr(size_t pos = 0, size_t len = npos);


	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		//声明
		static const size_t npos;
	};
	// operator
	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& s1);
	istream& operator>>(istream& in, string& s1);

	void test_string1();
	void test_string2();
}

 string.cpp

#include"string.h"

namespace zy
{
	
	// reserve
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{			
			char* tmp = new char[n+1];
			strcpy(tmp, _str);
			_capacity = n;
			delete[] _str;
			_str = tmp;
		}
	}

	// push_back
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}
	// append
	void string::append(const char* str)
	{
		size_t length = strlen(str);
		if (_size + length > _capacity)
		{
			reserve(_size + length > 2 * _capacity ? _size + length : 2 * _capacity);
		}
		strcpy(_str + _size, str);
		_size += length;
	}
	// operator+=
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}

	// insert
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : 2 * _capacity);
		}
		// 避免size_t将负数转为正整数最大值问题
		size_t end = _size+1;
		while (end > pos)
		{
			_str[end] = _str[end-1];
			end--;
		}
		_str[end] = ch;
	}
	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t length = strlen(str);
		if (_size + length > _capacity)
		{
			char* tmp = new char[_size + length > 2 * _capacity ? _size + length : 2 * _capacity];
		}
		size_t end = _size + length;
		//while (end > pos + length - 1)
		while (end >= pos + length)
		{
			_str[end] = _str[end - length];
			end--;
		}
		for (size_t i = 0; i < length; i++)
		{
			_str[pos + i] = str[i];
		}
		_size += length;
	}
	// erase
	void string::erase(size_t pos, size_t len)
	{
		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;
		}
	}

	// 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;
	}
	size_t string::find(const char* str, size_t pos)
	{//从第pos位置开始找第一个匹配的字符串
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}
	//substr
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

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

	// operator
	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);
	}

	// 流插入、提取
	ostream& operator<<(ostream& out, const string& s1)
	{
		for (auto ch : s1)
		{
			out << ch;
		}
		return out;
	}
	istream& operator>>(istream& in, string& s1)
	{
		s1.clear();

		const int N = 256;
		char buff[N];
		int i = 0;

		char ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i] = ch;
			i++;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s1 += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s1 += buff;
		}
		return in;
	}

	//定义分离到.cpp
	const size_t string::npos = -1;
}

扩展:写时拷贝

        

        写时拷贝本质上就是一种拖延症,在浅拷贝的基础上增加了引用计数的方式来实现。

         引用计数:用来季景路资源使用者的个数。在构造时,将资源的计数记为1,每增加一个对象使用该资源就给计数加一,当某个对象被销毁时,先给该计数减一,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,该资源释放;否则不能释放,否则会导致释放两次甚至更多次。

更多内容请看下面的文章:

C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell

C++的std::string的“读时也拷贝”技术! | 酷 壳 - CoolShell

C++面试中string类的一种正确写法 | 酷 壳 - CoolShell

STL 的string类怎么啦?_stl to string-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值