【C++】STL——string(两万字详解)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1. 为什么要学习string类

1.1 C语言中的字符串

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


1.2 两个面试题暂不做讲解

字符串转整形数字

字符串相加

在OJ中有关字符串的题目基本以string类的形式出现而且在常规工作中为了简单、方便、快捷基本都使用string类很少有人去使用C库中的字符串操作函数。


2. 标准库中的string类

2.1 string类(了解)

string类的文档介绍

  • 字符串是表示字符序列的类。
  • 标准的字符串类提供了对此类对象的支持其接口类似于标准字符容器的接口但添加了专门用于操作单字节字符字符串的设计特性。
  • string 类是使用 char(即作为它的字符类型使用它的默认 char_traits 和分配器类型 (关于模板的更多信息请参阅 basic_string)。
  • string 类是 basic_string 模板类的一个实例它使用 char 来实例化 basic_string 模板类并用 char_traits 和 allocator 作为 basic_string 的默认参数(根于更多的模板信息请参考 basic_string)。
  • 注意这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如 UTF-8) 的序列这个类的所有成员(如长度或大小)以及它的迭代器将仍然按照字节(而不是实际编码的字符)来操作。

总结

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

在使用 string 类时必须包含 string 头文件以及 using namespace std;

在这里插入图片描述

#include<string>
#include<iostream>
using namespace std;
int main()
{
	cout << sizeof(char) << endl;
	cout << sizeof(wchar_t) << endl;

	char arr1[] = "hello bit";
	char arr2[] = "byih";
	return 0;
}


2.1 string类的常用接口说明只讲解最常用的接口

2.1.1 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<string>
#include<iostream>
using namespace std;   
//大概原理
template<class T>
class basic_string
{
	T* _arr;
	int _size;
	int _capacity;
};
//typedef basic_string<char> string;
int main()
{
	string s1;
	string s2("hello");  
	string s3("byih");
	string s4(10, 'a');
	string s5(s2);
	//对于string它重载了流提取和流插入等这里就可以直接用
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	
	//赋值 ———— 深拷贝
	s1 = s5;
	cout << s1 << endl;
}

在这里插入图片描述

s1虽然是无参的但是他的第一个位置会有一个\0


2.1.2 string类对象的容量操作造

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

请添加图片描述

#include<string>
#include<iostream>
using namespace std;
void test_string1()
{
	//1、size | length
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << "----------cut----------" << endl;
	//2、max_size
	string s2;
	cout << s1.max_size() << endl;
	cout << s2.max_size() << endl;	
	cout << "----------cut----------" << endl;
	//3、capacity
	cout << s1.capacity() << endl;
	cout << "----------cut----------" << endl;
	//4、resize
	string s3("hello world");
	cout << s3.size() << endl;
	cout << s3 << endl;
	//s3.resize(20);//n大于当前的字符串的长度且没有指定c所以hello world\0\0\0\0...   
	//s3.resize(5);//n小于当前的字符串的长度 它会删除掉从n开始的这些字符
	s3.resize(20, 'x');//n大于当前的字符串的长度且指定c所以hello worldxxxx...
	cout << s3.size() << endl;
	cout << s3 << endl;
	cout << "----------cut----------" << endl;
	//5、reserve
	string s4("hello world");
	s4.reserve(20);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	s4.reserve(10);
	cout << s4 << endl;
	cout << s4.size() << endl;
	cout << s4.capacity() << endl;
	cout << "----------cut----------" << endl;
	//6、clear | empty
	string s5("hello world");
	cout << s5 << endl;
	cout << s5.empty() << endl;;
	s5.clear();
	cout << s5 << endl;
	cout << s5.empty() << endl;
	cout << "----------cut----------" << endl;
	//7、shrink_to_fit 暂且不演示
}   
void test_string2()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\n" << sz << endl;
	for(int i = 0; i < 500; ++i)
	{
		s.push_back('c');
		if(sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed:" << sz << '\n';
		}
	}
	cout << "----------cut----------" << endl;
}
int main()
{
	test_string1();
	test_string2();

	return 0;
}

注意

  1. size()与length()方法底层实现原理完全相同引入size()的原因是为了与其他容器的接口保持一致一般情况下基本都是用size()。
  2. clear()只是将string中有效字符清空不改变底层空间大小。
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个不同的是当字符个数增多时resize(n)用0来填充多出的元素空间resize(size_t n, char c)用字符c来填充多出的元素空间。注意resize在改变元素个数时如果是将元素个数增多可能会改变底层容量的大小如果是将元素个数减少底层空间总大小不变。
  4. reserve(size_t res_arg=0)为string预留空间不改变有效元素个数当reserve的参数小于string的底层空间总大小时reserver不会改变容量大小。

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

函数名称功能说明
operator[] 重点返回pos位置的字符const string类对象调用
begin+ endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
范围forC++11支持更简洁的范围for的新遍历方式
#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
using namespace std;

#include <string>


// 测试string容量相关的接口
// size/clear/resize
void Teststring1()
{
	// 注意string类对象支持直接用cin和cout进行输入和输出
	string s("hello, bit!!!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中的字符串清空注意清空时只是将size清0不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到10个多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到15个多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}

//====================================================================================
void Teststring2()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 测试reserve参数小于string的底层空间大小时是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

// 利用reserve提高插入数据的效率避免增容带来的开销
//====================================================================================
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

// 构建vector时如果提前已经知道string中大概要放多少个元素可以提前将string中空间设置好
void TestPushBackReserve()
{
	string s;
	s.reserve(100);
	size_t sz = s.capacity();

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



// string的遍历
// begin()+end()   for+[]  范围for
// 注意string遍历时使用最多的还是for+下标 或者 范围for(C++11后才支持)
// begin()+end()大多数使用在需要使用STL提供的算法操作string时比如采用reverse逆置string
void Teststring3()
{
	string s1("hello Bit");
	const string s2("Hello Bit");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;

	s1[0] = 'H';
	cout << s1 << endl;

	// s2[0] = 'h';   代码编译失败因为const类型对象不能修改
}

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

	// 2.迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << endl;
		++it;
	}

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


//
// 测试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;
}

int main()
{
	return 0;
}

2.1.4 string类对象的修改操作

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

#include <iostream>
using namespace std;

#include <string>


// 测试string容量相关的接口
// size/clear/resize
void Teststring1()
{
	// 注意string类对象支持直接用cin和cout进行输入和输出
	string s("hello, bit!!!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中的字符串清空注意清空时只是将size清0不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到10个多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 将s中有效字符个数增加到15个多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}

//====================================================================================
void Teststring2()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 测试reserve参数小于string的底层空间大小时是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

// 利用reserve提高插入数据的效率避免增容带来的开销
//====================================================================================
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << '\n';
		}
	}
}

// 构建vector时如果提前已经知道string中大概要放多少个元素可以提前将string中空间设置好
void TestPushBackReserve()
{
	string s;
	s.reserve(100);
	size_t sz = s.capacity();

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



// string的遍历
// begin()+end()   for+[]  范围for
// 注意string遍历时使用最多的还是for+下标 或者 范围for(C++11后才支持)
// begin()+end()大多数使用在需要使用STL提供的算法操作string时比如采用reverse逆置string
void Teststring3()
{
	string s1("hello Bit");
	const string s2("Hello Bit");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;

	s1[0] = 'H';
	cout << s1 << endl;

	// s2[0] = 'h';   代码编译失败因为const类型对象不能修改
}

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

	// 2.迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << endl;
		++it;
	}

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


//
// 测试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;
}

int main()
{
	return 0;
}

注意

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

2.1.5 string类非成员函数

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

上面的几个接口大家了解一下下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作这里不一一列举大家在需要用到时不明白了查文档即可。


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

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

  • vs下string的结构
    string总共占28个字节内部结构稍微复杂一点先是有一个联合体联合体用来定义string中字符串的存储空间
    — 当字符串长度小于16时使用内部固定的字符数组来存放
    — 当字符串长度大于等于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个字节内部只包含了一个指针该指针将来指向一块堆空间内部包含了如下字段
    — 空间总大小
    — 字符串有效长度
    — 引用计数
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!”

提示:

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

题解思路

我们使用 begin 指针从左边开始扫描字符串 send 指针从右边开始扫描字符串 s。如果两个指针都扫描到字母且begin < end那么交换 s[begin]和 s[end]然后继续进行扫描否则表明反转过程结束返回处理后的字符串。

代码实现

class Solution 
{
public:
    bool isLetter(char ch)//判断是否为字母如果是返回treu否则返回false
    {
        if(ch >= 'a' && ch <= 'z')
            return true;
        if(ch >= 'A' && ch <= 'Z')
            return true;
        return false;
    }
    string reverseOnlyLetters(string s) 
    {
        auto begin = s.begin();
        auto end = s.end() - 1;
        while(begin < end)
        {
            while(begin < end && !isLetter(*begin))//非英文字母且保证最后begin和end重合
            {
                ++begin;
            }
            while(begin < end && !isLetter(*end))//非英文字母且保证最后begin和end重合
            {
                --end;
            }
            swap(*begin, *end);//交换
            //交换后还要调整
            ++begin;
            --end;
        }
        return s;
    }
};

第二题

字符串中第一个唯一字符

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

示例1

输入s = “”
输出0

示例2

输入: s = “love”
输出: 2

示例3

输入: s = “aabb”
输出: -1

提示:

  • 1 <= s.length <= 105
  • s 只包含小写字母

解题思路

首先遍历一遍字符串然后把每个字母的出现次数计算出来。

之后再遍历一遍字符串。遍历过程中如果遇到了一个值出现过一次的字母就返回这个字母的下标。

代码实现

class Solution 
{
public:
    int firstUniqChar(string s) 
    {
        int count[26] = {0};
        //统计每个字符出现的次数
        for(auto ch:s)
        {
           count[ch - 'a']++;
        }
        //找出字符串中第一个不重复的字符
        for(int i = 0; i < s.size(); ++i)
        {
            if(count[s[i] - 'a'] == 1)
            {
				return i;
            }
        }
        return -1;
    }
};

第三题

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

请添加图片描述

  • 解题思路

注意这里读区字符串不能直接cin因为cin遇到空格就会终止要用getline

  • 代码实现
#include<string>
#include<iostream>
using namespace std;
int main()
{
    string s;
    //cin >> s;
    getline(cin, s);
    size_t pos = s.rfind(' ');
    if(pos != string::npos)//多个单词
    {
        cout << s.size() - (pos + 1) << endl;
    }
    else//一个单词
    {
        cout << s.size() << endl;
    }
    return 0;
}


第四题

验证回文串

请添加图片描述

  • 解题思路

使用双指针。初始时左右指针分别指向 sgood\textit{sgood}sgood 的两侧随后我们不断地将这两个指针相向移动每次移动一步并判断这两个指针指向的字符是否相同。当这两个指针相遇时就说明 sgood\textit{sgood}sgood 时回文串。

  • 代码演示
class Solution {
public:
    bool IsLetterOrNun(char ch)
    {
        if((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    bool isPalindrome(string s) {
        int begin = 0, end = s.size() - 1;
        while(begin < end)
        {
            while(begin < end && !IsLetterOrNun(s[begin]))//跳过非字母数字
            {
                ++begin;
            }
            while(begin < end && !IsLetterOrNun(s[end]))//跳过非字母数字
            {
                --end;
            }
            if(s[begin] != s[end])
            {
                //有一个是数字就不存在大小写比较问题
                if(s[begin] < 'A' || s[end] < 'A')
                {
                    return false;
                }
                else if(s[begin] < s[end] && s[begin] + 32 == s[end])
                {
                    ++begin;
                    --end;
                }
                else if(s[end] < s[begin] && s[end] + 32 == s[begin])
                {
                    ++begin;
                    --end;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                ++begin;
                --end;
            }
        }
        return true;
    }
};

//改进代码


class Solution {
public:
    bool isPalindrome(string s) {
        string sgood;
        for (char ch: s) {
            if (isalnum(ch)) {
                sgood += tolower(ch);
            }
        }
        int n = sgood.size();
        int left = 0, right = n - 1;
        while (left < right) {
           if (sgood[left] != sgood[right]) {
                return false;
            }
            ++left;
            --right;
        }
        return true;
    }
};



第五题

字符串相加

请添加图片描述

  • 解题思路

本题我们只需要对两个大整数模拟「竖式加法」的过程。竖式加法就是我们平常学习生活中常用的对两个整数相加的方法回想一下我们在纸上对两个整数相加的操作是不是如下图将相同数位对齐从低到高逐位相加如果当前位和超过 101010

  • 代码演示
class Solution {
public:
    string addStrings(string num1, string num2) {
        string retStr;
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        int carry = 0;
        while(end1 >= 0 || end2 >= 0)//只要还有一位那么就继续
        {
            int val1 = 0, val2 = 0;
            if(end1 >= 0)
            {
                val1 = num1[end1] - '0';
                --end1;
            }
            if(end2 >= 0)
            {
                val2 = num2[end2] - '0';
                --end2;
            }
            int ret = val1 + val2 + carry;
            if(ret > 9)//进位
            {
                ret -= 10;
                carry = 1;
            }
            else
            {
                carry = 0;
            }
            //头插
            retStr.insert(0, 1, '0' + ret);
            //retStr.insert(retStr.begin(), '0' + ret);//同上
        } 
        if(carry == 1)//还有一位的情况"1","9"
        {
           retStr.insert(retStr.begin(), '1');
        }
        return retStr;
    }
};


3. string类的模拟实现

3.1 经典的string类问题

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

string.h

// 为了和标准库区分此处使用String
class String
{
public:
	/*String()
	:_str(new char[1])
	{*_str = '\0';}
	*/
	//String(const char* str = "\0") 错误示范
	//String(const char* str = nullptr) 错误示范
	String(const char* str = "")
	{
	// 构造String类对象时如果传递nullptr指针可以认为程序非
		if (nullptr == str)
		{
			assert(false);
		return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

string.cpp

#include"string.h"
int main()
{
	TestString();
	return 0;
}

在这里插入图片描述

说明上述String类没有显式定义其拷贝构造函数与赋值运算符重载此时编译器会合成默认的当用s1构造s2时编译器会调用默认的拷贝构造。最终导致的问题是s1、s2共用同一块内存空间在释放时同一块空间被释放多次而引起程序崩溃这种拷贝方式称为浅拷贝


3.2 浅拷贝

由如上代码我们了解了浅拷贝会带来两个问题

  • 析构两次空间
  • 其中一个去修改会影响另一个

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

就像一个家庭中有两个孩子但父母只买了一份玩具两个孩子愿意一块玩则万事大吉万一不想分享就你争我夺玩具损坏。

可以采用深拷贝解决浅拷贝问题即每个对象都有一份独立的资源不要和其他对象共享。父母给每个孩子都买一份玩具各自玩各自的就不会有问题了。


3.3 深拷贝

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

深拷贝会新开辟一块与原对象一样大的空间再把原对象空间上的值拷贝过来。

在这里插入图片描述


3.3.1 深拷贝的传统版写法的string类

string.h

#pragma once
namespace bit
{
	class string
	{
	public:
		string(char* str)
			:_str(new char[strlen(str) + 1]
		{
			strcpy(_str, str);
		}
		//s2(s1)
		string(const string& s)
			:_str(new char[strlen(s.str) + 1])
		{
			strcpy(_str, s._str);
		}
		//s1 = s3
		string operator=(const string& s)
		{
			if(this != &s)//防止自己赋值
			{
				/*delete[] _str;//this->_str
				_str = new char[strlen(s._str) + 1];*/
				char* tmp = new char[strlen(s._str) + 1];
				delete[] _str;
				_str = tmp;
				strcpy(_str, s._str);
			}
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			return _str[pos];
		}
	private:
		char* _str;
	};
	void f1(string s)
	{}
	void f2(const string& s)
	{}
	template<class T>
	void f3(T x)
	{}
	void f3(const T& x)
	{}
	void test_string1()
	{
		string s1("hello");
		s1[0] = 'x';

		string s2(s1);
		s2[0] = 'y';
		
		string s3("hello bit");
		s1 = s3;
	
		f1(s1);
		f2(s2);
	}
}

string.cpp

#include"string.h"
int main()
{
	try
	{
		bit::test_string1();	
	}
	catch(exception& e)
	{
		cout << e.what() << endl;	
	}
	return 0;
}

在这里插入图片描述


3.3.2 深拷贝的现代版写法的String类

class String
{
public:
	String(const char* str = "")
	{
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(nullptr)
	{
		String strTmp(s._str);
		swap(_str, strTmp._str);
	}
	// 对比下和上面的赋值那个实现比较好
	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}
	/*
	String& operator=(const String& s)
	{
		if(this != &s)
		{
			String strTmp(s);
			swap(_str, strTmp._str);
		}
		return *this;
	}
	*/
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

它们俩的效率是一样的但两者都各有千秋

  • 传统写法可读性高便于理解但操作性较低
  • 现代写法代码更加简洁高效但是逻辑更加复杂

3.4 写时拷贝(了解)

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

引用计数用来记录资源使用者的个数。在构造时将资源的计数给成1每增加一个对象使用该资源就给计数增加1当某个对象被销毁时先给该计数减1然后再检查是否需要释放资源如果计数为1说明该对象时资源的最后一个使用者将该资源释放否则就不能释放因为还有其他对象在使用该资源。

在这里插入图片描述

早期 Linux 选择了写时拷贝的技术而 VS 下选择了直接深拷贝的技术。它们本质都是深拷贝只是说 Linux 下先做浅拷贝如果不写就不做深拷贝写了再去做深拷贝并且是谁写谁做。

写时拷贝

写时拷贝在读取时的缺陷


3.5 string类的模拟实现

#pragma once

namespace bit
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}


		const_iterator begin() const
		{
			return _str;
		}

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

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

		// 不能这么初始化空对象
		/*string()
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
			{}*/

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

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

		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);
		}

		// 传统写法
		// s2(s1)
		//string(const string& s)
		//	:_str(new char[s._capacity+1])
		//	, _size(s._size)
		//	, _capacity(s._capacity)
		//{
		//	strcpy(_str, s._str);
		//}

		 s1 = s3
		 s1 = s1
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		char* tmp = new char[s._capacity + 1];
		//		strcpy(tmp, s._str);

		//		delete[] _str;

		//		_str = tmp;
		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}

		//	return *this;
		//}

		// 现代写法 -- 资本主义/老板思维
		// s2(s1)
		void swap(string& tmp)
		{
			::swap(_str, tmp._str);
			::swap(_size, tmp._size);
			::swap(_capacity, tmp._capacity);
		}

		// s2(s1)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp); //this->swap(tmp);
		}

		// s1 = s3
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		//string tmp(s._str);
		//		string tmp(s);
		//		swap(tmp); // this->swap(tmp);
		//	}

		//	return *this;
		//}

		// s1 = s3
		// s顶替tmp做打工人
		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

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

		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

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

				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			// 满了就扩容
			/*if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';*/
			insert(_size, ch);
		}

		void append(const char* str)
		{
			//size_t len = strlen(str);

			 满了就扩容
			 _size + len  8  18  10  
			//if (_size + len > _capacity)
			//{
			//	reserve(_size+len);
			//}

			//strcpy(_str + _size, str);
			strcat(_str, str); 需要找\0效率低
			//_size += len;
			insert(_size, str);
		}

		/*void append(const string& s)
		{
		append(s._str);
		}

		void append(size_t n, char ch)
		{
		reserve(_size + n);
		for (size_t i = 0; i < n; ++i)
		{
		push_back(ch);
		}
		}*/

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

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

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

			// 满了就扩容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			 挪动数据
			//int end = _size;
			//while (end >= (int)pos)
			//{
			//	_str[end + 1] = _str[end];
			//	--end;
			//}
			size_t end = _size+1;
			while (end > pos)
			{
				_str[end] = _str[end-1];
				--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)
			{
				_str[end] = _str[end - len];
				--end;
			}

			strncpy(_str + pos, str, len);
			_size += len;

			return *this;
		}

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

			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}

		size_t find(char ch, size_t pos = 0) const;
		size_t find(const char* sub, size_t pos = 0) const;
		bool operator>(const string& s) const;
		bool operator==(const string& s) const;
		bool operator>=(const string& s) const;
		bool operator<=(const string& s) const;
		bool operator<(const string& s) const;
		bool operator!=(const string& s) const;
	private:
		size_t _capacity;
		size_t _size;
		char* _str;

		// const static 语法特殊处理
		// 直接可以当成定义初始化
		const static size_t npos = -1;
	};

	ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		// 输入字符串很长不断+=频繁扩容效率很低大家可以想法优化一下 
		char ch;
		//in >> ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}

		return in;
	}

	//size_t string::npos = -1;

	void test_string1()
	{
	    /*std::string s1("hello world");
		std::string s2;*/
		string s1("hello world");
		string s2;

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

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

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

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

	void test_string2()
	{
		string s1("hello world");
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;

		it = s1.begin();
		while (it != s1.end())
		{
			*it += 1;
			++it;
		}
		cout << endl;

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

	void test_string3()
	{
		string s1("hello world");
		string s2(s1);
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;

		s2[0] = 'x';
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;

		string s3("111111111111111111111111111111");
		s1 = s3;
		cout << s1.c_str() << endl;
		cout << s3.c_str() << endl;

		s1 = s1;

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

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

		s1.swap(s2);
		swap(s1, s2);
	}

	void test_string5()
	{
		string s1("hello");
		cout << s1.c_str() << endl;
		s1.push_back('x');
		cout << s1.c_str() << endl;
		cout << s1.capacity() << endl;

		s1 += 'y';
		s1 += 'z';
		s1 += 'z';
		s1 += 'z';
		s1 += 'z';
		s1 += 'z';
		s1 += 'z';
		cout << s1.c_str() << endl;
		cout << s1.capacity() << endl;
	}

	void test_string6()
	{
		string s1("hello");
		cout << s1.c_str() << endl;
		s1 += ' ';
		s1.append("world");
		s1 += "bit hello";
		cout << s1.c_str() << endl;

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

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

	void test_string7()
	{
		string s1("hello");
		cout << s1.c_str() << endl;

		s1.insert(2, "world");
		cout << s1.c_str() << endl;

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

	void test_string8()
	{
		string s1("hello");
		s1.erase(1, 10);
		cout << s1.c_str() << endl;


		string s2("hello");
		s2.erase(1);
		cout << s2.c_str() << endl;

		string s3("hello");
		s3.erase(1, 2);
		cout << s3.c_str() << endl;
	}

	void test_string9()
	{
	/*	string s1;
		cin >> s1;
		cout << s1 << endl;*/

		string s1("hello");
		cout << s1 << endl;
		cout << s1.c_str() << endl;
		s1 += '\0';
		s1 += "world";
		cout << s1 << endl;
		cout << s1.c_str() << endl;

		string s3, s4;
		cin >> s3 >> s4;
		cout << s3<< s4 << endl;
	}
}

4. 拓展阅读

面试中string的一种正确写法

STL中的string类怎么了


阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++