C++初阶:STL之string类

目录

一.为什么学习string类?

1.1.C语言中的字符串

二.标准库中的string类

2.1.string类

2.2.string类的常用接口说明

string类对象的常见构造

string类对象的容量操作

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

string类对象的修改操作

string类非成员函数

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

vs下string的结构

g++下string的结构 

三.牛刀小试

题一:仅仅反转字母

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

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

题四:验证回文串

题五:字符串相加

四.string类的模拟实现

4.1.基本成员函数

构造函数

拷贝构造函数

赋值运算符重载

析构函数

4.2.容器操作函数

size

capacity

clear

empty

reserve

resize

4.3.访问及遍历操作函数

operator[ ]

begin

end

4.4.修改操作函数

push_back

append

operator+=

c_str

find

insert

erase

swap

4.5.非成员函数

relational operators

operator>

operator==

operator>=

operator<

operator<=

operator!=

operator<<

operator>>

4.6.string类的完整实现

string.h

test.cpp


一.为什么学习string类?

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

1.1.C语言中的字符串

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

二.标准库中的string类

2.1.string类

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

注意:

在使用string类时,必须包含#include头文件以及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
#include
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;
}

运行结果:

C++初阶:STL之string类_第1张图片

扩展:

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)

C++初阶:STL之string类_第2张图片

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

C++初阶:STL之string类_第3张图片

注意:

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

运行结果:

C++初阶:STL之string类_第4张图片

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

运行结果:

C++初阶:STL之string类_第5张图片

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

C++初阶:STL之string类_第6张图片

reserve:为字符串预留空间

C++初阶:STL之string类_第7张图片

注意:

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

运行结果:

C++初阶:STL之string类_第8张图片

扩展:

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

运行结果:

C++初阶:STL之string类_第9张图片

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

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

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

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

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

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

C++初阶:STL之string类_第10张图片

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

C++初阶:STL之string类_第11张图片

注意:

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:获取最后一个字符的迭代器

C++初阶:STL之string类_第12张图片

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

C++初阶:STL之string类_第13张图片

注意:

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

C++初阶:STL之string类_第14张图片

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

C++初阶:STL之string类_第15张图片

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

C++初阶:STL之string类_第16张图片

注意:

在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格式字符串

C++初阶:STL之string类_第17张图片

案例:

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

运行结果:

C++初阶:STL之string类_第18张图片

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

C++初阶:STL之string类_第19张图片

npos:

C++初阶:STL之string类_第20张图片

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

C++初阶:STL之string类_第21张图片

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

C++初阶:STL之string类_第22张图片

案例:

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+:尽量少用,因为传值返回,导致深拷贝效率低

C++初阶:STL之string类_第23张图片

案例:

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:获取一行字符串

C++初阶:STL之string类_第24张图片

案例:

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个字节。 

C++初阶:STL之string类_第25张图片

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

扩展:

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

C++初阶:STL之string类_第26张图片

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

C++初阶:STL之string类_第27张图片

赋值运算符重载

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

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

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

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

		//赋值
		//深拷贝
		//s1s2
		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_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<>(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
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;
}

你可能感兴趣的:(c++)