C++初阶:STL之string类

一.为什么学习string类?

在C语言中没有字符串这一数据类型,都是用字符数组来处理字符串,C++也支持这种C风格的字符串。除此之外,C++还提供了一种自定义数据类型--string,string是C++标准模板库(STL)中的一个字符串类,包含在头文件string中,它能更方便快捷地定义,操作字符串。用C++ string类来定义字符串,不必担心字符串中字符溢出,内存不足等情况,而且string类中重载的运算符和提供的多个函数足以完成我们针对字符串所需要的所有操作。

1.C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

二.标准库中的string类

2.1.string类

1.string是表示字符串的字符串类;
2.该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作;
3.string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
4.不能操作多字节或者变长字符的序列。

注意:

在使用string类时,必须包含#include<string>头文件以及using namespace std

2.2.string类的常用接口说明

string类对象的常见构造

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

案例:

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

int main()
{
	//string()
	string s1;//构造一个空字符串
	cout << s1 << endl;

	//string (const char* s)
	string s2("hello world");//用C-string来构造string类对象
	cout << s2 << endl;

	//string& operator= (const char* s);		
	string s3 = "hello world";//类型转换,将一个字符串赋值给到string对象
	cout << s3 << endl;

	//string(size_t n, char c)
	string s4(5, 'c');//用5个c字符组成的字符串来构造string类对象
	cout << s4 << endl;

	//string(const string& str)
	string s5(s2);//用原有的字符串s2进行拷贝构造
	cout << s5 << endl;

	//string(const string& str, size_t pos, size_t len = npos)
	string s6(s3, 6, 3);//从s3字符串中的pos位置开始拷贝len个字符
	cout << s6 << endl;

	return 0;
}

运行结果:

扩展:

basic_string::npos
static const size_type npos = -1;

npos是一个静态成员常量,类型一般是string::size_type,多用于容器中,用来表示不存在的位置。由于size_t为无符号整型,于是-1就被转换为无符号整数类型,因此npos也就成了该类别的最大无符号值。

string类对象的容量操作

              函数名称                        功能说明
            size(重点)             返回字符串有效字符长度
               length             返回字符串有效字符长度
              capacity                   返回空间总大小
          empty (重点)检测字符串是否为空串,是返回true,否则返回false
           clear (重点)                   清空有效字符
         reserve (重点)                为字符串预留空间
          resize (重点)将有效字符的个数该成n个,多出的空间用字符c填充

详解:

size:获取字符串长度(不包括末尾\0)

length:获取字符串长度(不包括末尾\0)

注意:

size()与length()的方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一
致,一般情况下基本都是用size()。

capacity:获取当前string类对象能够存储的有效字符量

注意:

在VS平台下,capacity的默认大小为15字节(不包括末尾\0),它的容量会随着字符串的变长而发生相应的变化。

案例:

int main()
{
	string s1("hello world");
	//用size()和length()两个函数求得字符串s的结果是一样的,都不包括末尾的'\0'
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << s1.capacity() << endl;
	cout << "--------------------" << endl;

	string s2;
	cout << s2.size() << endl;
	cout << s2.length() << endl;
	cout << s2.capacity() << endl;
	cout << "--------------------" << endl;

	string s3("hello world,we are togethere!");
	cout << s3.size() << endl;
	cout << s3.length() << endl;
	cout << s3.capacity() << endl;
	cout << "--------------------" << endl;
	
	return 0;
}

运行结果:

empty:判断字符串是否为空

clear:清空字符串的内容

注意:

clear()只是将string中有效字符清空,不改变底层空间大小。

案例:

int main()
{
	string s1("hello world");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	cout << s1.empty() << endl;

	s1.clear();
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	cout << s1.empty() << endl;
	
	return 0;
}

运行结果:

resize:将字符串的长度(size\length)变为n

reserve:为字符串预留空间

注意:

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不会改变容量大小。

案例:

int main()
{
	//reserve:为字符串预留空间,知道需要多少空间,提前开辟空间,减少扩容,提高效率
	//扩容
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;
	s1.reserve(50);
	cout << s1.size() << endl;//不会发生改变
	cout << s1.capacity() << endl;
	cout << s1 << endl;
	cout << "------------" << endl;

	//扩容+初始化
	string s2("hello world");
	//s2.resize(50);
	//resize:将有效字符的个数改成n个,多出的空间用字符c填充
	s2.resize(50, 'x');//多出的空间用字符x填充
	cout << s2.size() << endl;//会发生改变
	cout << s2.capacity() << endl;
	cout << s2 << endl;
	cout << "------------" << endl;

	//比size小,删除数据,保留前5个
	s2.resize(5);
	cout << s2.size() << endl;
	cout << s2.capacity() << endl;//不会发生变化
	cout << s2 << endl;

	return 0;
}

运行结果:

扩展:

string在Windows下是如何进行增容的?我们使用下面这段代码进行测试:

int main()
{
	string s;
	//s.reserve(100);
	size_t sz = s.capacity();
	cout << "making s grow:\n";
	cout << "capapcity changed:" << sz << '\n';
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capapcity changed:" << sz << '\n';
		}
	}

	return 0;
}

运行结果:

可以看到在Windows下的VS中,capacity大概是以1.5倍的速度进行增容的;而在Linux下的g++中,capacity大概是以2倍的速度进行增容的。

但是,我们往往会优先考虑使用reserve为字符串预留空间。只要知道需要多少字节的空间,并提前进行开辟空间,就可以减少扩容,提高运行效率。

string类对象的访问及遍历操作

                   函数名称                    功能说明
           operator[] (重点)返回pos位置的字符,const string类对象调用
                 begin+endbegin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
                rbegin+rendrbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器
                     范围for

C++11支持更简洁的范围for的新遍历方式

operator[]:返回pos位置的字符

at:访问指定下标位置处的字符

注意:

operator[]和at的功能是相似的,但是不一样的地方在于对越界的处理:operator[]直接使用断言报错,而at则会抛出异常。

案例一:

int main()
{
	string s("abcdefgh");
	cout << s << endl;

	for (size_t i = 0; i < s.size(); i++)
	{
		//可以对字符进行修改
		s[i]++;
	}
	cout << s << endl;

	for (size_t i = 0; i < s.size(); i++)
	{
		cout << s[i] << " ";
	}
	cout << endl;

	const string s2("hello bite");
	//s2[0] = 'h';//编译失败,因为const类型对象不能修改

	//通过at函数,遍历打印s2的每个字符
	for (size_t i = 0; i < s2.size(); ++i)
	{
		cout << s2.at(i) << " ";
	}
	cout << endl;

	return 0;
}

运行结果: 

案例二:

int main()
{
	string s1("hello world");
	try
	{
		s1.at(100);
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}

	return 0;
}

运行结果:

迭代器是泛化的指针,STL算法利用迭代器对容器对容器中的元素序列进行操作,迭代器提供了访问容器中每个元素的方法。

begin:获取第一个字符的迭代器

end:获取最后一个字符下一个位置的迭代器

注意:

string提供了begin()与end()函数,这一对函数返回的是头尾元素的迭代器(可以理解为指针)。begin()返回第一个元素的迭代器,end()返回最后一个元素的迭代器。

rbegin:获取最后一个字符的迭代器

rend:获取第一个字符前一个位置的迭代器

注意:

string提供了rbegin()与rend()这样一对函数,用于返回一个逆向迭代器,rbegin()返回的是逆向遍历的第一个元素,即倒数第一个元素,rend()返回的是逆向遍历的末尾元素的后一个位置,即第一个元素的上一个位置,其实这个位置已经不在容器中了。

案例:

void Func(const string& s)
{
	//迭代器iterator
	//string::iterator it = s.begin();//err,要用const迭代器,且不允许写,只能遍历和读容器的数据
	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

//正向迭代器,const正向迭代器,反向迭代器,const反向迭代器
int main()
{
	string s1("hello world");

	//迭代器iterator:可以遍历和读取容器的数据
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	Func(s1);

	//反向迭代器
	//string::reverse_iterator rit = s1.rbegin();
	auto rit = s1.rbegin();//auto可以自动推导类型
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;

	return 0;
}

运行结果:

范围for:对一组序列中的每个对象进行操作

如果想对一组序列中的每个对象进行操作,可以使用C++中提供的基于范围的for语句。该语句遍历序列中每个元素并对序列中的每个值进行某种操作。

案例:

int main()
{
	string s1("hello world");
	string::iterator it = s1.begin();//begin()指向起始位置,end()指向最后一个字符的下一个位置,左闭右开
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	//范围for
	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	//修改
	for (auto& ch : s1)
	{
		ch++;
	}

	for (auto ch : s1)
	{
		cout << ch << " ";
	}
	cout << endl;

	return 0;
}

运行结果:

注意:

auto ch : s1 只能读,不能写;auto& ch : s1 可读可写。范围for的底层实现原理还是迭代器,所以一个类如果不支持迭代器就不支持范围for。

string类对象的修改操作

                函数名称                        功能说明
              push_back               在字符串后尾插字符c
                append           在字符串后追加一个字符串
          operator+= (重点)             在字符串后追加字符串str
               c_str(重点)                   返回C格式字符串
          find + npos(重点)从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
                   rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
                  substr在str中从pos位置开始,截取n个字符,然后将其返回

push_back: 在字符串后尾插字符c

append:在字符串后追加一个字符串

operator+=:在字符串后追加字符串str

注意:

在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。与 push_back和append相比,更加推荐使用operator+=,因为它可以追加一个字符,也可以追加一个字符串,甚至可以追加一个对象。

案例:

int main()
{
	string s1("hello");

	//插入单个字符
	s1.push_back(' ');
	s1.push_back('!');
	cout << s1 << endl;

	//插入字符串
	s1.append(" world");
	cout << s1 << endl;

	//+=:在字符串后面追加字符串str
	s1 += ' ';
	s1 += '!';
	s1 += " world";
	cout << s1 << endl;

	return 0;
}

运行结果:

c_str:返回C格式字符串

案例:

int main()
{
	string s1("hello world");

	cout << s1 << endl;//流插入是按照size去打印的
	cout << s1.c_str() << endl;//返回C形式的字符串,遇到'\0'则终止
	cout << (void*)s1.c_str() << endl;//按指针打印
	cout << "--------------------" << endl;

	cout << s1 << endl;
	cout << s1.c_str() << endl;
	s1 += '\0';
	s1 += '\0';
	s1 += "xxxxx";
	cout << s1 << endl;
	cout << s1.c_str() << endl;
	cout << "--------------------" << endl;

	string filename("test.cpp");

	//string类给我们提供了一个接口函数c_str,帮助我们将string对象转换为C字符串
	//否则编译会报错:不存在从"std:string"到"const char*"的适当转换函数
	//FILE * fopen ( const char * filename, const char * mode );
	FILE* fout = fopen(filename.c_str(), "r");//加c_str()是为了兼容c
	if (fout == nullptr)
		perror("fopen fail");

	char ch = fgetc(fout);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fout);
	}

	fclose(fout);

	return 0;
}

运行结果:

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

npos:

rfind:从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置

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

案例:

int main()
{
	string file("string.cpp.tar.zip");

	size_t pos = file.find('.');
	if (pos != string::npos)
	{
		//substr:在str中从pos位置开始,截取n个字符,然后将其返回
		//string suffix = file.substr(pos, file.size() - pos);
		string suffix = file.substr(pos);//给缺省值,则有多少取多少
		cout << suffix << endl;
	}

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

	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;

	return 0;
}

运行结果:

扩展:

find_first_of:从起始位置开始找出所有相关字符
find_last_of:从最后位置开始找出所有相关字符

案例:

int main()
{
	string str("Please,replace the vowels in this sentence by asterisks.");
	size_t found1 = str.find_first_of("aeiou");
	while (found1 != string::npos)
	{
		str[found1] = '*';
		found1 = str.find_first_of("aeiou", found1 + 1);
	}

	cout << str << endl;

	size_t found2 = str.find_last_of("aeiou");
	while (found2 != string::npos)
	{
		str[found2] = '*';
		found2 = str.find_first_of("aeiou", found2 + 1);
	}

	cout << str << endl;

	return 0;
}

运行结果:

string类非成员函数

                    函数名称                   功能说明
                   operator+尽量少用,因为传值返回,导致深拷贝效率低
              operator>> (重点)              输入运算符重载
              operator<< (重点)              输出运算符重载
                   getline (重点)              获取一行字符串
       relational operators (重点)                   大小比较

operator+:尽量少用,因为传值返回,导致深拷贝效率低

案例:

int main()
{
	string s1("hello ");
	string s2("world");

	string s3 = s1 + s2;
	cout << s1 << endl;
	cout << s3 << endl;

	s1 += s2;
	cout << s1 << endl;

	return 0;
}

运行结果:

注意:

operator+ 和 operator+=:它们的作用都是对字符串进行尾插,但是 operator+= 会对当前字符串进行修改,而 operator+ 则不会对当前字符串进行修改。

operator>>: 输入运算符重载

operator<<:输出运算符重载

getline:获取一行字符串

案例:

int main()
{
	string str;
	getline(cin, str);

	cout << str << endl;

	return 0;
}

运行结果:

vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证的,32位平台下指针占4个字节。

vs下string的结构

string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字
符串的存储空间:

  1. 当字符串长度小于16时,使用内部固定的字符数组来存放;
  2. 当字符串长度大于等于16时,从堆上开辟空间。
union _Bxty
{ 
	//storage for small buffer or pointer to larger one
	value_type _Buf[_BUF_SIZE];
	pointer _Ptr;
	char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量。
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。 

g++下string的结构 

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指
针将来指向一块堆空间,内部包含了如下字段:

  1. 空间总大小;
  2. 字符串有效长度;
  3. 引用计数。
struct _Rep_base
{
	size_type _M_length;
	size_type _M_capacity;
	_Atomic_word _M_refcount;
};

 注意:指向堆空间的指针,用来存储字符串。

三.牛刀小试

题一:仅仅反转字母

题目描述:

给你一个字符串 s ,根据下述规则反转字符串:

  • 所有非英文字母保留在原有位置。
  • 所有英文字母(小写或大写)位置反转。

返回反转后的 s

示例 1:

输入:s = "ab-cd"
输出:"dc-ba"

示例 2:

输入:s = "a-bC-dEf-ghIj"
输出:"j-Ih-gfE-dCba"

示例 3:

输入:s = "Test1ng-Leet=code-Q!"
输出:"Qedo1ct-eeLg=ntse-T!"

实现:

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)
	{
		size_t 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;
	}
};

题二:字符串中的第一个唯一字符

题目描述:

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

示例 1:

输入: s = "leetcode"
输出: 0

示例 2:

输入: s = "loveleetcode"
输出: 2

示例 3:

输入: s = "aabb"
输出: -1

实现:

class Solution
{
public:
	int firstUniqChar(string s)
	{
		int countA[26] = { 0 };

		//统计每个字符出现的次数
		for (auto ch : s)
		{
			countA[ch - 'a']++;
		}

		// 按照字符次序从前往后找只出现一次的字符
		for (int i = 0; i < s.size(); ++i)
		{
			if (countA[s[i] - 'a'] == 1)
				return i;
		}
		return -1;
	}
};

题三:字符串最后一个单词的长度

题目描述:

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

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

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

示例:

  输入:hello nowcoder
  输出:8
  说明:最后一个单词为nowcoder,长度为8  

实现:

int main()
{
	string str;
	//cin >> str;//err,cin遇到空格或换行就会停止读取
	getline(cin, str);//获取一行字符串

	size_t pos = str.rfind(' ');

	if (pos != string::npos)
	{
		cout << str.size() - pos - 1 << endl;//-1是减去空格
	}
	else//没有空格,只包含一个单词
	{
		cout << str.size() << endl;
	}

	return 0;
}

题四:验证回文串

题目描述:

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

字母和数字都属于字母数字字符。

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

示例 1:

输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。

示例 2:

输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。

示例 3:

输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s是一个空字符串""。由于空字符串正着反着读都一样,所以是回文串。

实现:

class Solution 
{
public:
	bool isLetterOrNumber(char ch)
	{
		return (ch >= '0' && ch <= '9')
			|| (ch >= 'a' && ch <= 'z')
			|| (ch >= 'A' && ch <= 'Z');
	}

	bool isPalindrome(string s) {
		// 先小写字母转换成大写,再进行判断
		for (auto& ch : s)
		{
			if (ch >= 'a' && ch <= 'z')
				ch -= 32;
		}
		int begin = 0, end = s.size() - 1;
		while (begin < end)
		{
			while (begin < end && !isLetterOrNumber(s[begin]))
				++begin;
			while (begin < end && !isLetterOrNumber(s[end]))
				--end;
			if (s[begin] != s[end])
			{
				return false;
			}
			else
			{
				++begin;
				--end;
			}
		}
		return true;
	}
}

题五:字符串相加

题目描述:

给定两个字符串形式的非负整数 num1num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

示例 1:

输入:num1 = "11", num2 = "123"
输出:"134"

示例 2:

输入:num1 = "456", num2 = "77"
输出:"533"

示例 3:

输入:num1 = "0", num2 = "0"
输出:"0"

实现:

class Solution
{
public :
	string addStrings(string num1, string num2)
	{
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		int next = 0;//进位

		//为字符串预留空间,避免开辟空间,提高效率
		string strRet;
		strRet.reserve(num1.size() > num2.size() ? num1.size() + 1 : num2.size() + 1);

		while (end1 >= 0 || end2 >= 0)
		{
			/*int val1 = 0;
			if (end1 >= 0)
			{
				val1 = num1[end1] - '0';
			}

			int val2 = 0;
			if (end2 >= 0)
			{
				val2 = num1[end2] - '0';
			}*/

			int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2] - '0' : 0;

			int ret = val1 + val2 + next;

			//if (ret > 9)
			//{
			//	ret -= 10;
			//	next = 1;//进位置为1
			//}
			//else
			//{
			//	next = 0;
			//}

			next = ret / 10;
			ret = ret % 10;

			//将运算的结果进行头插
			//strRet.insert(0, 1, '0' + ret);

			//将运算结果进行尾插
			strRet += ('0' + ret);

			--end1;
			--end2;
		}

		//防止最后的进位没有处理赶紧,比如9+1
		if (next == 1)
		{
			strRet.insert(0, 1, '1');
		}

		reverse(strRet.begin(), strRet.end());

		return strRet;
	}
};

四.string类的模拟实现

上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。

前提:为了避免和string类发生冲突,我们采用命名空间的方式来隔离冲突域。如下所示:

namespace bit
{
	class string 
    {
	public:
		//...
	private:
		size_t _size;
		size_t _capacity;
		char* _str;
	};
}

4.1.基本成员函数

构造函数

版本一:无参构造函数

采用初始化列表的方式,首先将_size_capacity的初始值设为0,然后给字符数组_str开辟一个字节大小的空间,并将其初始化为\0,同时也是为了同带参构造函数一样,在析构时,统一进行delete[]。

string()
	//不要给空指针,解引用可能会导致程序崩溃
	:_str(new char[1])//加上[1]是为了同带参构造函数一样,在析构时,统一delelte[]
	, _size(0)
	, _capacity(0)
{
	_str[0] = '\0';
}

版本二:带参构造函数

与无参构造函数不同,我们这里并不是直接将_size和_capacity初始化为0。首先调用strlen函数统计字符串str中有效字符个数(不包含\0),并采用初始化列表的方式对_size进行初始化,同时在函数体内将_capacity的初始值置为与_size相同大小。然后为_str开辟_capacity+1大小的空间。最后再调用strcpy函数,将字符串str拷贝到字符数组_str中。

string(const char* str)
	:_size(strlen(str))
{
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

版本三:全缺省构造函数

我们将上述的无参与带参构造函数合二为一,归并为全缺省的构造函数。同时对_capacity的初始值做一些改进,避免后续问题的出现。

//string(const char* str = nullptr)//err,会导致空指针解引用
//string(const char* str = '\0')//err,\0的ascii码是0
string(const char* str = "")//或者const char* str = "\0"
	:_size(strlen(str))
{
	_capacity = _size == 0 ? 3 : _size;//要确保_capacity不能为0,否则调用push_back()插入字符时会失败,导致程序崩溃
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

拷贝构造函数

在一个类中若没有显示定义拷贝构造函数,对于内置类型不作处理,而对于自定义类型则会调用类中提供的默认拷贝构造函数,但此时则会造成浅拷贝的问题。为了解决这个问题,我们必须显示地实现一个拷贝构造函数。

​string(const string& s)
	:_size(s._size)
	,_capacity(s._capacity)
{
	_str = new char[s._capacity + 1];
	strcpy(_str, s._str);
}

扩展:

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

赋值运算符重载

赋值运算符重载与拷贝构造相类似,都存在浅拷贝问题。为了避免调用默认赋值运算符重载函数而导致的内存泄漏,我们需要手动实现以达到深拷贝。

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;*/

		char* tmp = new char[s._capacity + 1];
		strcpy(tmp, s._str);
		delete[] _str;
		_str = tmp;

		_size = s._size;
		_capacity = s._capacity;
	}
			
	return *this;
}

析构函数

析构函数所要完成的工作:释放掉string类对象所申请的内存空间,并将_size和_capacity置为0。

~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

4.2.容器操作函数

size

size()主要用于获取当前字符串的有效长度。注意:要加const加以修饰,否则会造成权限放大。

size_t size() const
{
	return _size;
}

capacity

capacity()主要用于获取空间总大小。注意:要加const加以修饰,否则会造成权限放大。

size_t capacity() const
{
	return _capacity;
}

clear

clear()主要用于清空有效字符。直接在_str[0]的位置存放上一个\0即可,并将_size置为0即可。

void clear()
{
	_str[0] = '\0';
	_size = 0;
}

empty

empty()主要用于判断字符串是否为空,直接判断0==_size是否成立。

bool empty() const
{
	return 0 == _size;
}

reserve

当所需容量n大于旧容量_capacity时,我们就会选择进行扩容。首先要对n>_capacity是否成立进行判断,目的是防止缩容,导致程序异常,然后开辟一块n+1字节大小的空间,接着将旧空间的内容拷贝到新空间并释放旧空间的内容,最后让_str指向新空间并更新_capacity的大小。

void reserve(size_t n)
{
	if (n > _capacity)//防止缩容,导致程序异常
	{
		char* tmp = new char[n + 1];//开辟新空间,包含'\0'
		strcpy(tmp, _str);//将旧空间的内容拷贝到新空间
		delete[] _str;//释放旧空间
		_str = tmp;
		_capacity = n;//包含有效字节的空间,不包含'\0'
	}
}

resize

resize(size_t n, char ch = '\0'):主要是将有效字符个数变为n,若有空间多出则用字符ch填充。此时分三种情况进行讨论:

  1. 情况一:若n<_size,则将多余的数据直接进行删除,并将下标为n位置处的元素改为\0;
  2. 情况二:若_size<n<_capacity,则把多出的空间用字符ch填充,并将下标为n位置处的元素改为\0;
  3. 情况三:若n>_capacity,则先调用reserve函数进行扩容,再把多出的空间用字符ch填充,最后再将下标为n位置处的元素改为\0。 
void resize(size_t n, char ch = '\0')
{
	if (n < _size)//=时不作处理
	{
		//删除数据,保留前n个
		_size = n;
		_str[_size] = '\0';
	}
	else if (n > _size)
	{
		if (n > _capacity)
		{
			reserve(n);
		}

		size_t i = _size;
		while (i < n)
		{
			_str[i] = ch;
			++i;
		}

		_size = n;
		_str[_size] = '\0';
	}
}

4.3.访问及遍历操作函数

operator[ ]

operator[ ]主要用于返回pos位置的字符,它有两种实现形式:一个是用const string类对象进行调用但可读不可写,一个是用string类对象进行调用但可读可写。

版本一:给const对象调用,不允许修改

//给const对象调用,不允许修改
const char& operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

版本二:给非const对象调用,允许修改

//给非const对象调用,允许修改
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

begin

begin()主要用于获取第一个字符的迭代器,它有两种实现形式:非const迭代器和const迭代器。

版本一:非const迭代器

//非const迭代器
typedef char* iterator;

iterator begin()
{
	return _str;
}

版本二:const迭代器

//const迭代器
typedef const char* const_iterator;//可以修改,但指向的内容不能修改

const_iterator begin() const
{
	return _str;
}

end

end()主要用于获取最后一个字符的迭代器,它有两种实现形式:非const迭代器和const迭代器。它往往会和begin()配套使用。

版本一:非const迭代器

//非const迭代器
typedef char* iterator;

iterator end()
{
	return _str + _size;
}

版本二:const迭代器

//const迭代器
typedef const char* const_iterator;//可以修改,但指向的内容不能修改

const_iterator end() const
{
	return _str + _size;
}

4.4.修改操作函数

push_back

void push_back(char ch)主要用于在字符串后尾插字符ch。在插入字符之前,首先要判断当前容量是否充足,即_size+1是否大于_capapcity,若大于则先调用reserve函数进行扩容,再进行字符的插入,否则直接进行字符的插入。因为_size指向\0的位置,所以可以把字符直接放在这个位置,同时将_size进行自增并加上\0,否则会发生乱码。

void push_back(char ch)
{
	if (_size + 1 > _capacity)
	{
		reserve(_capacity * 2);
	}

	_str[_size] = ch;
	++_size;

	//要加'\0',否则会发生乱码
	_str[_size] = '\0';

	//insert(_size, ch);
}

append

void append(const char* str)主要用于在字符串后追加一个字符串。首先要计算所追加字符串的长度len,然后要判断当前容量是否充足,即_size+len是否大于_capapcity,若大于则先调用reserve函数进行扩容,再进行字符串的插入,否则直接进行字符串的插入。

void append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	strcpy(_str + _size, str);
	//strcat(_str, str);

	_size += len;

	//insert(_size, str);
}

operator+=

operator+=主要用于在字符串后追加字符串或字符,有两种形式:operator+=(char ch)用于追加单个字符,operator+=(const char* s)用于追加字符串。

版本一:operator+=(char ch)

这里主要通过调用push_back()对单个字符进行追加。又因为要改变自身,所以我们要返回*this。同时采用引用返回,可以有效减少拷贝。

string& operator+=(char ch)
{
	push_back(ch);

	return *this;
}

版本二:operator+=(const char* s)

这里主要通过调用append()对单个字符进行追加。又因为要改变自身,所以我们要返回*this。同时采用引用返回,可以有效减少拷贝。

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

	return *this;
}

c_str

c_str()主要用于将string类对象转换成C格式字符串。

const char* c_str()
{
	return _str;
}

find

find主要用于从字符串pos位置开始往后找字符或字符串,有两种形式:size_t find(char ch, size_t pos = 0)用于从pos位置开始查找字符ch,size_t find(const char* str, size_t pos = 0)用于从pos位置开始查找字符串str。

版本一:size_t find(char ch, size_t pos = 0)

首先判断pos位置是否合法,若合法则从pos位置开始依次向后遍历,然后与字符ch逐个进行比较,若相同则返回下标所在位置,若不相同则继续往后比较。若查找失败,则直接返回npos。

size_t find(char ch, size_t pos = 0)
{
	assert(pos < _size);

	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}

	return npos;
}

版本二:size_t find(const char* str, size_t pos = 0)

首先判断pos位置是否合法,若合法则调用函数strstr,将字符串_str与字符串str从pos位置开始进行逐一比较并返回指针p。若p的值为nullptr则表明查找失败,若p的值不为nullptr则直接返回相应字符串的下标。

size_t find(const char* str, size_t pos = 0)
{
	assert(pos < _size);

	//strstr(str1,str2)用于判断字符串str2是否是str1的子串
	//如果是,则返回str1字符串从str2第一次出现的位置开始到str1结尾的字符串;否则,返回NULL
	char* p = strstr(_str + pos, str);
	if (p == nullptr)
	{
		return npos;
	}
	else
	{
		return p - _str;
	}
}

insert

insert()用于在字符串的某个位置插入一个字符或字符串,有两种形式:string& insert(size_t pos, char ch)用于在pos位置插入一个字符ch,string& insert(size_t pos, const char* str)用于在pos位置插入一串字符str。

版本一:string& insert(size_t pos, char ch)

在插入字符之前,首先要判断插入位置是否合法,然后要判断当前容量是否充足,即_size+1是否大于_capapcity,若大于则先调用reserve函数进行扩容。在插入数据之前,先将pos位置之后的数据由后向前依次向后移动一位,再在pos位置插入字符ch,同时将_size的值+1。

string& insert(size_t pos, char ch)
{
	assert(pos <= _size);

	if (_size + 1 > _capacity)
	{
		reserve(2 * _capacity);
	}

	//size_t end = _size;//size_t是无符号数,设pos=0,则当end=0时,--end会变成-1,而-1的无符号数为最大值,所有要改成有符号数,但依旧会报错,因为会发生隐式类型提升(end是有符号数,pos是无符号数),可以将pos改为有符号数
	//while (end >= pos)//end>=(int)pos
	//{
	//	_str[end + 1] = _str[end];
	//	--end;
	//}

	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end + 1] = _str[end];
		--end;
	}

	_str[pos] = ch;
	++_size;

	return *this;
}

版本二:string& insert(size_t pos, const char* str)

在插入字符之前,首先要判断插入位置是否合法,然后要判断当前容量是否充足,即_size+len是否大于_capapcity,若大于则先调用reserve函数进行扩容。在插入数据之前,先将pos位置之后的数据由后向前依次向后移动len位,再调用函数strncpy将字符串str拷贝至_str+pos位置,同时将_size的值+len。

string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);

	//判断是否需要扩容
	if (_size + len > _capacity)
	{
		reserve(_size + len);
	}

	//挪动数据
	//法一
	size_t end = _size + len;
	while (end > pos + len - 1)//当len=1时,相当于插入了一个字符
	{
		_str[end] = _str[end - len];
		--end;
	}

	//法二
	/*size_t end = _size;
	for (size_t i = 0; i < _size + 1; i++)
	{
		_str[end + len] = _str[end];
	}*/


	//拷贝插入
	strncpy(_str + pos, str, len);
	_size += len;

	return *this;
}

erase

string& erase(size_t pos, size_t len = npos)主要用于删除从pos位置开始长为len的字符。首先要判断pos的位置是否合法,然后对len的取值进行判断。若len的值为npos或者pos+len>=_size,则直接将pos位置以后的数据进行删除,这里主要是通过_str[pos] = '\0'进行实现,最后再更新_size;若len的值不为npos且pos+len<_size,则调用函数strcpy将_str + pos + len位置开始的数据拷贝至_str + pos位置,最后再更新_size。

string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);

	//不给npos,则从pos位置开始将后面数据全部删完
	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}

	return *this;
}

swap

void swap(string& s)主要用于实现对象之间的数据交换,为了提高程序的运行效率,这里通过调用std库中的swap函数进行功能实现。

void swap(string& s)
{
	//调用std库中的swap
	std::swap(_str, s._str);
	std::swap(_capacity, s._capacity);
	std::swap(_size, s._size);
}

4.5.非成员函数

relational operators

relational operators是关系运算符重载,主要进行大小比较。常见的关系运算符有:>, ==, >=, <, <=, !=等。我们要明白的一点是:在比较两个string类对象时,并不是拿它们所对应的字符串长度进行比较,而是去比较它们所对应字符的ASCLL码值。

operator>

主要通过调用函数strcmp,对两个字符串自左向右逐个字符进行比较。若比较结果大于0则返回true,否则返回false。

bool operator>(const string& s) const
{
	//strcmp用于比较两个字符串并根据比较结果返回整数
	//两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇'\0'为止
	return strcmp(_str, s._str) > 0;
}
operator==

主要通过调用函数strcmp,对两个字符串自左向右逐个字符进行比较。若比较结果等于0则返回true,否则返回false。

bool operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}
operator>=

复用关系运算符:operator >和operator==

bool operator>=(const string& s) const
{
	//return *this > s || s == *this;//err,因为s是const对象,不能调用非const的成员函数,所以要改写成const成员函数
	return *this > s || *this == s;
}
operator<

复用关系运算符:operator >=,并进行取反操作。

bool operator<(const string& s) const
{
	return !(*this >= s);
}
operator<=

复用关系运算符:operator >,并进行取反操作。

bool operator<=(const string& s) const
{
	return !(*this > s);
}
operator!=

复用关系运算符:operator ==,并进行按位取反操作。

bool operator!=(const string& s) const
{
	return !(*this == s);
}

operator<<

operator<<用于输出运算符重载,但是要将其定义为类外的全局函数,同时进行引用返回,这样可以减少拷贝。

ostream& operator<<(ostream& out, const string& s)
{
	//out<<s.c_str()//err,因为这样遇到'\0'就会截止输出,从而不一定输出s.size()个字符
	//C的字符数组, 以\0为终止算长度,string不看\0, 以size为终止算长度
	for (auto ch : s)
	{
		out << ch;
	}

	return out;
}

operator>>

operator<<用于输入运算符重载,需要将其定义为类外的全局函数,具体过程可以分为两步,一是清理缓存,二是读取数据。清理缓存主要是通过调用clear()函数进行;读取字符主要是通过调用get()函数进行,然后开辟一个char类型的数组用于对读取的字符进行存储,这样可以减少频繁扩容带来的损失。

istream& operator>>(istream& in, string& s)
{
	//清理缓存
	s.clear();

	//读取数据
	
	//get()函数是cin输入流对象的成员函数,用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符
	char ch = in.get();
	char buff[128];//提前开辟128字节的空间,减少频繁扩容带来的损失
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 127)
		{
			buff[127] = '\0';

			//每满一次buff,则重新加一次
			s += buff;
			i = 0;
		}
		ch = in.get();
	}

	//若是有数据且未满127字符,则直接进行读入
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}

	return in;
}

4.6.string类的完整实现

string.h

#pragma once
#include<assert.h>

//string类的模拟实现
namespace bit
{
	class string
	{
	public:
		无参构造函数
		//string()
		//	//不要给空指针,解引用可能会导致程序崩溃
		//	:_str(new char[1])//加上[1]是为了同带参构造函数一样,在析构时,统一delete[]
		//	, _size(0)
		//	, _capacity(0)
		//{
		//	_str[0] = '\0';
		//}

		带参构造函数
		//string(const char* str)
		//	:_size(strlen(str))
		//{
		//	_capacity = _size;
		//	_str = new char[_capacity + 1];
		//	strcpy(_str, str);
		//}

		//无参和带参的合二为一:全缺省的构造函数
		//string(const char* str = nullptr)//err,会导致空指针解引用
		//string(const char* str = '\0')//err,\0的ascii码是0
		string(const char* str = "")//或者const char* str = "\0"
			:_size(strlen(str))
		{
			_capacity = _size == 0 ? 3 : _size;//要确保_capacity不能为0,否则调用push_back()插入字符时会失败,导致程序崩溃
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//拷贝构造
		//深拷贝
		string(const string& s)
			:_size(s._size)
			,_capacity(s._capacity)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
		}

		//赋值
		//深拷贝
		//s1<s2 s1=s2 s1>s2
		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;*/

				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				_size = s._size;
				_capacity = s._capacity;
			}
			
			return *this;
		}

		//析构
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

		//将string类对象转化为C格式字符串
		const char* c_str()
		{
			return _str;
		}

		//给const对象调用,不允许修改
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

		//给非const对象调用,允许修改
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		//要加const,否则会造成权限放大
		size_t size() const
		{
			return _size;
		}

		//获取容量大小
		size_t capacity() const
		{
			return _capacity;
		}

		//非const迭代器
		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;
		}

		//>
		//不修改成员变量数据的函数,最好都加上const
		bool operator>(const string& s) const
		{
			//strcmp用于比较两个字符串并根据比较结果返回整数
			//两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇'\0'为止
			return strcmp(_str, s._str) > 0;
		}

		//==
		bool operator==(const string& s) const
		{
			return strcmp(_str, s._str) == 0;
		}

		//>=
		bool operator>=(const string& s) const
		{
			//return *this > s || s == *this;//err,因为s是const对象,不能调用非const的成员函数,所以要改写成const成员函数
			return *this > s || *this == s;
		}

		//<
		bool operator<(const string& s) const
		{
			return !(*this >= s);
		}

		//<=
		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}

		//!=
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}

		//扩容
		void reserve(size_t n)
		{
			if (n > _capacity)//防止缩容,导致程序异常
			{
				char* tmp = new char[n + 1];//开辟新空间,包含'\0'
				strcpy(tmp, _str);//将旧空间的内容拷贝到新空间
				delete[] _str;//释放旧空间
				_str = tmp;
				_capacity = n;//包含有效字节的空间,不包含'\0'
			}
		}

		//尾插一个字符
		void push_back(char ch)
		{
			//if (_size + 1 > _capacity)
			//{
			//	reserve(_capacity * 2);
			//}

			//_str[_size] = ch;
			//++_size;

			要加'\0',否则会发生乱码
			//_str[_size] = '\0';

			insert(_size, ch);
		}

		//尾插一串字符
		void append(const char* str)
		{
			//size_t len = strlen(str);
			//if (_size + len > _capacity)
			//{
			//	reserve(_size + len);
			//}

			//strcpy(_str + _size, str);
			strcat(_str, str);

			//_size += len;

			insert(_size, str);
		}

		//+=
		//追加单个字符
		string& operator+=(char ch)
		{
			push_back(ch);

			return *this;
		}

		//+=
		//追加字符串
		string& operator+=(const char* str)
		{
			append(str);

			return *this;
		}
		
		//分三种情况:
		//n<_size
		//_size<n<_capacity
		//n>_capacity
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)//=时不作处理
			{
				//删除数据,保留前n个
				_size = n;
				_str[_size] = '\0';
			 }
			else if (n > _size)
			{
				if (n > _capacity)
				{
					reserve(n);
				}

				size_t i = _size;
				while (i < n)
				{
					_str[i] = ch;
					++i;
				}

				_size = n;
				_str[_size] = '\0';
			}
		}

		//插入字符
		string& insert(size_t pos, char ch)
		{
			assert(pos <= _size);

			if (_size + 1 > _capacity)
			{
				reserve(2 * _capacity);
			}

			//size_t end = _size;//size_t是无符号数,设pos=0,则当end=0时,--end会变成-1,而-1的无符号数为最大值,所有要改成有符号数,但依旧会报错,因为会发生隐式类型提升(end是有符号数,pos是无符号数),可以将pos改为有符号数
			//while (end >= pos)//end>=(int)pos
			//{
			//	_str[end + 1] = _str[end];
			//	--end;
			//}

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end + 1] = _str[end];
				--end;
			}

			_str[pos] = ch;
			++_size;

			return *this;
		}

		//插入字符串
		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);

			size_t len = strlen(str);

			//判断是否需要扩容
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}

			//挪动数据
			//法一
			size_t end = _size + len;
			while (end > pos + len - 1)//当len=1时,相当于插入了一个字符
			{
				_str[end] = _str[end - len];
				--end;
			}

			//法二
			/*size_t end = _size;
			for (size_t i = 0; i < _size + 1; i++)
			{
				_str[end + len] = _str[end];
			}*/


			//拷贝插入
			strncpy(_str + pos, str, len);
			_size += len;

			return *this;
		}

		//删除字符
		string& erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			//不给npos,则从pos位置开始将后面数据全部删完
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

			return *this;
		}

		//对象之间的数据交换
		void swap(string& s)
		{
			//调用std库中的swap
			std::swap(_str, s._str);
			std::swap(_capacity, s._capacity);
			std::swap(_size, s._size);
		}

		//查找某个字符
		size_t find(char ch, size_t pos = 0)
		{
			assert(pos < _size);

			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

		//查找某一字符串
		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);

			//strstr(str1,str2)用于判断字符串str2是否是str1的子串
			//如果是,则返回str1字符串从str2第一次出现的位置开始到str1结尾的字符串;否则,返回NULL
			char* p = strstr(_str + pos, str);
			if (p == nullptr)
			{
				return npos;
			}
			else
			{
				return p - _str;
			}
		}

		//清空字符串
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		//判断字符串是否为空
		bool empty() const
		{
			return 0 == _size;
		}


	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		//成员变量可以给缺省值,静态成员变量不能给缺省值
		//static size_t npos;//声明一
		static const size_t npos;//声明二
		//static const size_t npos = -1;//声明+定义,只针对整型

		/*static const size_t N = 10;
		int _a[N];*/
	};

	//size_t string::npos = -1;//定义一
	const size_t string::npos = -1;//定义二

	//流插入
	//实现为全局函数
	ostream& operator<<(ostream& out, const string& s)
	{
		//out<<s.c_str()//err,因为这样遇到'\0'就会截止输出,从而不一定输出s.size()个字符
		//C的字符数组, 以\0为终止算长度,string不看\0, 以size为终止算长度
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	//流提取
	//实现为全局函数
	istream& operator>>(istream& in, string& s)
	{
		//清理缓存
		s.clear();

		//读取数据
	
		//get()函数是cin输入流对象的成员函数,用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符
		char ch = in.get();
		char buff[128];//提前开辟128字节的空间,减少频繁扩容带来的损失
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[127] = '\0';

				//每满一次buff,则重新加一次
				s += buff;
				i = 0;
			}
			ch = in.get();
		}

		//若是有数据且未满127字符,则直接进行读入
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

	void test_string1()
	{
		string s1;//加上std可以防止程序崩溃
		string s2("hello world");

		cout << s1.c_str() << endl;//可能会崩溃,cout会自动识别类型const char* ,同时会对字符串进行解引用并打印输出,而s1为空,对空指针进行解引用会导致程序崩溃,所以不要将_str初始化为nullptr
		cout << s2.c_str() << endl;

		s2[0]++;

		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;
	}

	void test_string2()
	{
		string s1;
		string s2("hello world");

		string s3(s2);//一个修改会影响另一个,同时会析构两次

		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;

		s2[0]++;
		cout << s2.c_str() << endl;
		cout << s3.c_str() << endl;

		s1 = s3;
		cout << s1.c_str() << endl;
		cout << s3.c_str() << endl;
	}

	//引用传参
	void Print(const string& s)
	{
		//size()函数不加const修饰,会造成权限的放大,从const的s对象调用非const的size()
		for (size_t i = 0; i < s.size(); ++i)
		{
			cout << s[i] << " ";
		}
		cout << endl;

		string::const_iterator it = s.begin();
		while (it != s.end())
		{
			//*it='x';
			++it;
		}
		cout << endl;

		//不支持范围for的原因:首先const对象不能调用非const函数,也不能调用非const迭代器对const对象进行修改,可以设置一个const迭代器
		for (auto ch : s)
		{
			cout << ch << " ";
		}
		cout << endl;
	}

	void test_string3()
	{
		string s1("hello world");
		for (size_t i = 0; i < s1.size(); ++i)
		{
			s1[i]++;
		}
		cout << endl;

		for (size_t i = 0; i < s1.size(); ++i)
		{
			cout << s1[i] << " ";
		}
		cout << endl;

		Print(s1);

		//迭代器
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			++it;//指针可以修改,内容不可以修改
		}
		cout << endl;

		it = s1.begin();
		while (it != s1.end())//左闭右开
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		//范围for:底层调用的是迭代器
		for (auto ch : s1)
		{
			cout << ch << " ";
		}
		cout << endl;
	}

	void test_string4()
	{
		string s1("hello world");
		string s2("hello world");
		string s3("xx");

		cout << (s1 < s2) << endl;
		cout << (s1 == s2) << endl;
		cout << (s1 >= s3) << endl;
	}

	void test_string5()
	{
		string s1("hello world");
		//s1.push_back(' ');
		//s1.append("xxxxxxxxxx");

		s1 += ' ';
		s1 += "xxxxxxxxxxxxxxx";
		cout << s1.c_str() << endl;

		string s2;//s2起始为空时,扩容会失败,导致无法插入数据
		s2 += 'a';
		//s2 += 'b';
		//s2 += 'c';
		cout << s2.c_str() << endl;

		s1.insert(5, 'x');
		cout << s1.c_str() << endl;

		s1.insert(0, 'x');//导致异常
		cout << s1.c_str() << endl;
	}

	void test_string6()
	{
		string s1("hello world1111111111");
		cout << s1.capacity() << endl;

		s1.reserve(10);//不会缩容
		cout << s1.capacity() << endl;
	}

	void test_string7()
	{
		string s1;

		s1.resize(20, 'x');
		cout << s1.c_str() << endl;

		s1.resize(30, 'y');
		cout << s1.c_str() << endl;

		s1.resize(10);
		cout << s1.c_str() << endl;
	}

	void test_string8()
	{
		string s1("11111111");

		s1.insert(0, 'x');
		cout << s1.c_str() << endl;

		s1.insert(3, 'x');
		cout << s1.c_str() << endl;

		s1.insert(3, "yyy");
		cout << s1.c_str() << endl;

		s1.insert(0, "yyy");
		cout << s1.c_str() << endl;
	}

	void test_string9()
	{
		string s1("0123456789");
		cout << s1.c_str() << endl;

		s1.erase(4, 3);
		cout << s1.c_str() << endl;

		s1.erase(4,30);
		cout << s1.c_str() << endl;

		s1.erase(2);
		cout << s1.c_str() << endl;
	}

	//流插入重载必须实现为友元函数?不是
	void test_string10()
	{
		std::string s1("0123456789");
		s1 += '\0';
		s1 += "xxxxxxxx";

		cout << s1 << endl;//遇到\0不终止
		cout << s1.c_str() << endl;//遇到\0则终止

		string s2;
		cin >> s2;
		cout << s2 << endl;

		cin >> s1;
		cout << s1 << endl;
	}
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;

#include"string.h"

int main()
{
	/*try
	{
		bit::test_string10();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}*/

	bit::string s1;
	bit::string s2;
	cout << sizeof(s1) << endl;//12
	cout << sizeof(s2) << endl;//12

	//字符串是存放在堆上的,不占用对象的空间
	bit::string s3("11111");
	bit::string s4("1111111");
	cout << sizeof(s3) << endl;//12
	cout << sizeof(s4) << endl;//12

	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/158831.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

React项目首页中用canvas实现星空

文章目录 前言代码使用后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端系列文章 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&#xff0c;感谢大家…

2019年全国硕士研究生入学统一考试管理类专业学位联考数学试题——解析版

2019 年 1 月份管综初数真题 一、问题求解&#xff08;本大题共 5 小题&#xff0c;每小题 3 分&#xff0c;共 45 分&#xff09;下列每题给出 5 个选项中&#xff0c;只有一个是符合要求的&#xff0c;请在答题卡上将所选择的字母涂黑。 1、某车间计划 10 天完成一项任务&a…

springboot集成xxl-job详解

文章目录 springboot集成xxl-job详解1、springboot集成xxl-job&#xff1a;&#xff08;1&#xff09;pom文件里引入xxl-job依赖&#xff08;2&#xff09;application.properties配置文件&#xff1a;&#xff08;3&#xff09;在你的项目里新建文件结构如下&#xff1a;XxlJo…

【操作系统】调度算法

周转时间完成时间-到达时间 带权周转时间周转时间/运行时间 等待时间周转时间-运行时间 响应比&#xff08;等待时间要求服务时间&#xff09;/ 要求服务时间 先来先服务&#xff08;FCFS&#xff09; 按到达时间顺序。 非抢占式算法。 优点&#xff1a;公平、算法实现简…

C++ string类(一)

1.C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符 OOP(Object Oriented Programming)的思想&#xff0c;而且…

计算机是如何工作的(简单介绍)

目录 一、冯诺依曼体系 二、CPU基本流程工作 逻辑⻔ 电⼦开关——机械继电器(Mechanical Relay) ⻔电路(Gate Circuit) 算术逻辑单元 ALU&#xff08;Arithmetic & Logic Unit&#xff09; 算术单元(ArithmeticUnit) 逻辑单元(Logic Unit) ALU 符号 寄存器(Regis…

算法通关村——字符串反转问题解析

字符串反转问题 我们知道反转是链表的一个重要考点&#xff0c;反转同样是字符串的重要问题。字符串和链表在处理反转的方式上有相似的地方&#xff0c;一般都是运用双指针&#xff0c;一个指针从前找&#xff0c;一个指针从后找。具体的处理办法结合下面具体的题目来看&#…

深度学习入门(第三天)——卷积神经网络

一、卷积神经网络应用领域 CV领域发展&#xff1a; 比赛中预测错误率的百分比&#xff0c;每年逐步下降。Human是人类肉眼的识别能力&#xff0c;2016年开始已经远高于人类肉眼死别能力&#xff0c;后面就取消了该方向的比赛了。 检测任务&#xff1a; 分类与检索&#xff1a;…

【Linux】重定向|重新理解Linux下一切皆文件

文章目录 一、什么是重定向输出重定向的原理认识一下输出重定向的系统调用输出重定向的另外写法 二、浅谈输入重定向三、重定向和进程替换有冲突吗四、Linux下一切皆文件总结 一、什么是重定向 理解重定向之前&#xff1a;先理解一个叫做文件描述符的具体操作。 文件描述符&a…

信创之路数据库人大金仓篇

概要 信创大势所趋&#xff0c;吾等上下求索 参考文档 Linux&#xff1a;人大金仓数据库-KingBaseES V8与 php7的连接配置 laravel9适配人大金仓&#xff08;kingbase&#xff09;数据库 thinkphp6适配人大金仓&#xff08;Kingbase&#xff09;数据库 数据库选型 目前比较…

C语言--统计一行字符串的单词个数, 单词用非字母分割.例如“ab235adg 456ad“被认为是3个单词.

一.题目描述 统计一行字符串的单词个数, 单词用非字母分割. 例如"ab235adg 456ad"被认为是3个单词. 二.思路分析 本题的主要难点在于如何判断有一个单词呢&#xff0c;当然遍历字符串是必须的。下面给出两种不同的思路&#xff1a; 一.当前是字母&#xff0c;下一个…

openRPA开源项目源码编译

最近接触到了一个新的领域——RPA&#xff0c;RPA全称Robotic Process Automation&#xff0c;中文名为机器人流程自动化。RPA可以视作一个数字机器人&#xff0c;它可以通过程序来模拟人与软件系统的交互过程&#xff0c;代替人工将大量重复、有规则的计算机操作自动化&#x…

Vite -静态资源处理 - SVG格式的图片

特点 Vite 对静态资源是开箱即用的。 无需做特殊的配置。项目案例 项目结构 study-vite| -- src| -- assets| -- bbb.svg # 静态的svg图片资源| -- index.html # 主页面| -- main.js # 引入静态资源| -- package.json # 脚本配置| -- vite.co…

3GPP TS38.201 NR; Physical layer; General description (Release 18)

TS38.201是介绍性的标准&#xff0c;简单介绍了RAN的信道组成和PHY层承担的功能&#xff0c;下图是PHY层相关标准的关系。 文章目录 结构信道类型调制方式PHY层支持的过程物理层测量其他标准TS 38.202: Physical layer services provided by the physical layerTS 38.211: Ph…

【Mac开发环境搭建】Docker安装Redis、Nacos

文章目录 Dokcer安装Redis拉取镜像创建配置文件创建容器连接测试Redis连接工具[Quick Redis]设置Redis自启动 Docker安装Nacos Dokcer安装Redis 拉取镜像 docker pull redis创建配置文件 # bind 127.0.0.1 -::1 bind 0.0.0.0 # 是否启用保护模式 protected-mode no# redis端口…

python+pytest接口自动化测试之接口测试基础

一、接口测试的基本信息 1、常用的两种接口&#xff1a;webservice接口和http api接口   webService接口是走soap协议通过http传输&#xff0c;请求报文和返回报文都是xml格式的&#xff0c;可以用soupui、jmeter等工具进行测试。   http api接口是走http协议&#xff0c;…

数据结构与算法之美学习笔记:20 | 散列表(下):为什么散列表和链表经常会一起使用?

目录 前言LRU 缓存淘汰算法Redis 有序集合Java LinkedHashMap解答开篇 & 内容小结 前言 本节课程思维导图&#xff1a; 今天&#xff0c;我们就来看看&#xff0c;在这几个问题中&#xff0c;散列表和链表都是如何组合起来使用的&#xff0c;以及为什么散列表和链表会经常…

【咖啡品牌分析】Google Maps数据采集咖啡市场数据分析区域分析热度分布分析数据抓取瑞幸星巴克

引言 咖啡作为一种受欢迎的饮品&#xff0c;已经成为我们生活中不可或缺的一部分。随着国内外咖啡品牌的涌入&#xff0c;新加坡咖啡市场愈加多元化和竞争激烈。 本文对新加坡咖啡市场进行了全面的品牌门店数占比分析&#xff0c;聚焦于热门品牌的地理分布、投资价值等。通过…

系列四、GC垃圾回收【四大垃圾算法-引用计数法】

一、概述 Java中&#xff0c;引用和对象是有关联的&#xff0c;如果要操作对象则必须要用引用进行。因此判断一个对象是否可以被回收&#xff0c;很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。简单来讲就是给对象中添加一个引用计数器&#xff0c;每当一…

echarts 实现双y轴折线图示例

该示例有如下几个特点&#xff1a; ①实现tooltip自定义样式&#xff08;echarts 实现tooltip提示框样式自定义-CSDN博客&#xff09; ②legend左右区分展示 ③双y轴折线展示 代码如下&#xff1a; this.options {grid: {left: "3%",right: "3%",top: &…
最新文章