0.关注博主有更多知识
C++知识合集
目录
1.编码问题
2.string类概述
2.6习题练习
3.string类的模拟实现
3.1成员变量
3.2迭代器部分
3.3类的默认成员部分
3.4容量接口
3.5增删查改接口
3.6通用接口
3.7输入与输出
3.8完整代码
1.编码问题
实际上在我们接触C++之前就已经接触过一种编码,即ASCII编码。ASCII编码的主要目的就是为了建立计算机存储的值与英文字符之间的映射关系,因为计算机根本不认识'a'、'b'、'c'等等类似这样的字符,它只认识0、1这样的二进制序列。那么计算机是美国人发明的,那么他们必然想要计算机能够认识自己的文字,即英文字母和一些英文标点符号、数字等等,所以就诞生出了ASCII编码这样的东西,每一个值都确定一个英文符号:
但是ASCII编码仅限于英文,而当今计算机流行于全世界,ASCII必然是不够用的,所以又诞生出了Unicode编码,即统一码,也称万国码。Unicode编码的主要用途就是建立计算机与非英文之间的映射,当然,Unicode也是可以支持ASCII的。那么对Unicode编码的详细概述由官方介绍将会更好:统一码——百度百科。就拿中文来说,Unicode编码使用两个字节来表示一个中文汉字,所以当前最流行的编码就是UTF-8,它既能够支持中文,也能够兼容ASCII。
那么还有一种编码名为GBK编码,这种GBK编码是专门用来支持中文的编码,貌似是国人造的(我也不确定)。但是GBK编码与UTF-8编码存在差异,所以在很多的是不方便的。例如,Windows操作系统的默认编码为GBK编码,而Linux操作系统的默认编码为UTF-8编码,那么在Windows写的程序和在Linux写的程序进行网络套接字收发数据时,就有可能产生乱码。
2.string类概述
事实上string类严格意义上将它不属于STL,因为它的出现要比STL早,所以它的接口设计与其他STL容器比起来显得"不伦不类",因为这是C++后期对string类的升级,目的是为了向STL靠齐,所以添加了类似于size()、迭代器这样的东西。但是本篇博客着重在于模拟实现而不在于介绍接口,因为接口的介绍实在是太麻烦了(string类的接口有100多个),所以建议读者直接去查阅文档:string类接口。
现在要介绍的是,string类是什么。string类是一个专门管理字符串的类,与其说是管理字符串,不如说是管理字符数组的类。就像数据结构当中的顺序表一样,string类的本质就是一个存放char类型元素的顺序表,需要支持插入、删除、扩容、查找等等操作,非常幸运,string类直接提供了这类接口,并且它能够自动扩容,也就是说我们在使用string类的时候根本不需要关心底层的空间大小够不够。
为了向STL靠齐,string类引入了迭代器的概念。迭代器就是一个像指针一样的东西,但是它不完全是指针,迭代器具有指向底层数据结构当中存储的元素的能力,因此,我们可以使用迭代器完成插入、修改、查找、删除和遍历的操作。但是刚才也说了,string类是专门管理字符数组的类,既然是数组,那么我们更倾向于使用下标的方式来完成增删查改,为什么又要引入迭代器呢?原因在于STL的容器当中,每个容器的底层的数据结构都不同,有顺序表、链表、堆、红黑树、哈希、AVL树等等,但是我们使用迭代器的话,就能用统一的操作实现对不同数据结构的控制。通常使用的迭代有begin()和end()两个,其中begin()接口返回的迭代器指向的是第一个有效元素的位置,end()接口返回的迭代器指向是最后一个有效元素的下一个位置,即末尾:
在string类当中,end()指向的位置为'\0'。
那么string类提供的一个接口当中有多个重载,其中不乏const成员函数和非const成员函数,这是因为这些接口(成员函数)要遵循下面的三个原则:
1.如果提供的接口仅仅是只读接口,例如size()、c_str()这种,不会修改底层数据结构的任何属性,那么只需要提供const版本的即可,因为const版本的成员函数无论是const对象还是非const对象都能调用
2.如果提供的接口仅仅是只写接口,例如resize()、reverse()这种,它们直接修改了底层数据结构的大小和容量,那么只需要提供非const版本的即可,因为const对象修改底层属性是一种不合理操作
3.如果提供的接口是可读可写接口,例如operator[]这种,那么它们必须实现const版本和非const版本的两个重载,const对象调用const版本的接口,非const对象调用非const版本的接口
介绍这些的目的是为了做后面模拟实现string类的铺垫。
有一个细节需要注意,那便是string类不以'\0'作为结束标志,我们可以举一个简单的例子验证我们的说法:
int main()
{
string s = "hello";
s.resize(10);/*多的内容以'\0'填充*/
s += "world";
cout << s;
return 0;
}
在监视窗口观察:
原因在于string类(即basic_string<T>)类有两个成员,一个成员用来表示有效数据的个数,另一个用来表示容量大小。调用operator<<()时,以有效数据的个数输出结果:
但如果我们以C风格的字符串形式输出的话,结果就会不同:
int main()
{
string s = "hello";
s.resize(10);/*多的内容以'\0'填充*/
s += "world";
cout << s.c_str();
return 0;
}
再者也需要注意一点,也是对以前类和对象的知识作补充,即运算符重载不能为静态成员函数。所以我们可以很明显的看到string类当中没有一个静态成员函数,原因具体我也不清楚(反正就是编译器报错了),我的猜测是因为static修饰的函数会修改其链接数据,使得该函数只能在本文件当中使用,而在编译的时候会连同其他文件一起编译,但是我们将函数声明为静态,所以会造成"找不到"函数的现象(但是它编译器不给链接错误的信息,所以这个猜测大家可以忽略):
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day)
{}
static bool operator==(const Date &d1, const Date &d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 5, 16);
Date d2(2023, 5, 16);
Date::operator==(d1, d2);/*这样的写法完全没有问题,但是编译器报错*/
return 0;
}
2.6习题练习
917. 仅仅反转字母
class Solution {
public:
string reverseOnlyLetters(string s) {
int begin = 0,end = s.size()-1;
while(begin < end)
{
/*如果碰到了非字母,就跳过*/
if(!isalpha(s[begin])) ++begin;
if(!isalpha(s[end])) --end;
/*反转的本质就是头尾交换
*但是该题交换有条件,必须都是字母*/
if(begin < end && ((isalpha(s[begin]) && isalpha(s[end]))))
swap(s[begin++],s[end--]);
}
return s;
}
};
415. 字符串相加
class Solution {
public:
string addStrings(string num1, string num2) {
int end1 = num1.size()-1,end2 = num2.size()-1;
int carry = 0;/*进位*/
string resStr;
resStr.reserve(max(num1.size(),num2.size())+1);
while(end1 >= 0 || end2 >= 0)
{/*只要有一个字符串没有遍历完,都要进入循环*/
int operNum1 = end1 < 0? 0 : num1[end1] - '0';
int operNum2 = end2 < 0? 0 : num2[end2] - '0';
/*两个字符串的每一位的和再加上进位,就等于当前的和
*但是每一位的范围在0~9,所以大于9时需要进位*/
int tmpSum = operNum1 + operNum2 + carry;
carry = tmpSum / 10;
tmpSum %= 10;
resStr += tmpSum + '0';
--end1,--end2;
}
if(carry == 1) resStr += '1';/*如果还有进位*/
reverse(resStr.begin(),resStr.end());
return resStr;
}
};
387. 字符串中的第一个唯一字符
class Solution {
public:
int firstUniqChar(string s) {
/*字符串中只有小写字母,并且小写字母只有26个
*所以使用数组用来计数,然后再遍历一遍字符串
*找出只出现一次的字符下标即可*/
int count[26] = {0};
for(auto &e:s)
{
count[e - 'a']++;
}
for(int i=0;i<s.size();i++)
{
if(count[s[i] - 'a'] == 1) return i;
}
return -1;
}
};
HJ1 字符串最后一个单词的长度
#include <iostream>
using namespace std;
int main()
{
string str;
/*一定是使用getline读取一整行字符串
*因为流提取(>>)碰到空格即截止*/
getline(cin,str);
cout << str.size() - (str.rfind(' ') + 1) << endl;
return 0;
}
125. 验证回文串
class Solution {
public:
bool isPalindrome(string s) {
/*大写转小写*/
for(auto &e:s) if(isupper(e)) e = tolower(e);
int begin = 0,end = s.size()-1;/*双指针*/
while(begin < end)
{
if(!isalnum(s[begin]))
{
++begin;
continue;
}
if(!isalnum(s[end]))
{
--end;
continue;
}
/*上面两个if是移除所有非字母、数字字符的*/
if(s[begin] != s[end]) return false;
++begin;
--end;
}
return true;
}
};
541. 反转字符串 II
class Solution {
public:
string reverseStr(string s, int k) {
for(int i=0;i<s.size();i+=2*k)
{
/*如果从当前位置开始,还剩余2k个字符,翻转前k个
*从当前位置开始,还剩余k个或k个以上字符,翻转前k个*/
if(i+2*k <= s.size() || i+k<=s.size())
{
reverse(s.begin()+i,s.begin()+i+k);
}
else
{/*全部翻转*/
reverse(s.begin()+i,s.end());
}
}
return s;
}
};
557. 反转字符串中的单词 III
class Solution {
public:
string reverseWords(string s) {
/*思路就是找到一个空格就逆序
*使用STL的算法find(),不使用string类的find()成员
*因为STL的find()返回的是通用迭代器*/
auto itBegin = s.begin();
string::iterator itEnd;
while((itEnd = find(itBegin,s.end(),' ')) != s.end())
{
reverse(itBegin,itEnd++);
itBegin = itEnd;
}
reverse(itBegin,s.end());
return s;
}
};
43. 字符串相乘
class Solution {
public:
string multiply(string num1, string num2) {
if(num1 == "0" || num2 == "0") return "0";
/*思路就是将乘法拆成加法,例如123*456,就相当于:
*123*456 = 123*6 + 123*50 + 123*400
*按照这个思路分析下面的代码,不会很难*/
int end2 = num2.size()-1;
int zeroCnt = 0;
string res = "0";
while(end2 >= 0)
{
int end1 = num1.size()-1;
int cnt = zeroCnt;
int carry = 0;
string onceProduct;/*乘法拆开后的每一次乘积*/
while(end1 >= 0)
{
int n1 = num1[end1] - '0';
int n2 = num2[end2] - '0';
int product = n1 * n2 + carry;
carry = product / 10;/*两个数的乘积最多为81*/
product %= 10;
onceProduct += (product + '0');
--end1;
}
if(carry > 0) onceProduct += (carry + '0');
reverse(onceProduct.begin(),onceProduct.end());
while(cnt--) onceProduct += '0';
/*到这里onceProduct可能是很多个0组成的字符串
*但是并不影响,因为紧接着调用addStrings()函数*/
res = addStrings(res,onceProduct);
++zeroCnt;
--end2;
}
return res;
}
string addStrings(string num1, string num2) {
int end1 = num1.size()-1,end2 = num2.size()-1;
int carry = 0;/*进位*/
string resStr;
resStr.reserve(max(num1.size(),num2.size())+1);
while(end1 >= 0 || end2 >= 0)
{/*只要有一个字符串没有遍历完,都要进入循环*/
int operNum1 = end1 < 0? 0 : num1[end1] - '0';
int operNum2 = end2 < 0? 0 : num2[end2] - '0';
/*两个字符串的每一位的和再加上进位,就等于当前的和
*但是每一位的范围在0~9,所以大于9时需要进位*/
int tmpSum = operNum1 + operNum2 + carry;
carry = tmpSum / 10;
tmpSum %= 10;
resStr += tmpSum + '0';
--end1,--end2;
}
if(carry == 1) resStr += '1';/*如果还有进位*/
reverse(resStr.begin(),resStr.end());
return resStr;
}
};
3.string类的模拟实现
我们先看string类是如何定义的,以及需要什么成员变量和成员函数声明,然后对每一个接口的实现进行讲解:
class String
{/*毕竟是模拟实现,所以就不用模板模拟basic_string<char>了*/
public:
/*原生指针就可以充当迭代器
*原生指针具有指向、修改、插入...的能力
*只要自定义类型有了迭代器,在外部就可以使用范围for遍历了
*但是一定要注意,使用范围for的类型,其迭代器一定要配套*/
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;}
public:
/*构造、拷贝、赋值、析构*/
String(const char *str = "");
String(const String &str);
//String &operator=(const String &str);
String &operator=(String str);/*现代写法*/
~String();
/*有关容量的接口*/
void reserve(int capacity);
void resize(size_t n, const char &ch = '\0');
/*增、删、查、改*/
void push_back(const char &ch);
String &insert(size_t pos, size_t n, const char &ch);
String &insert(size_t pos, const String &s);
String &append(size_t n, const char &ch);
String &append(const String &s);
String &operator+=(const char &ch);
String &operator+=(const String &s);
String &erase(size_t pos = 0, size_t len = npos);
size_t find(const char &ch, size_t pos = 0) const;
size_t find(const String &str, size_t pos = 0) const;
String operator+(const String &str);
/*通用接口*/
char &operator[](size_t pos);
const char &operator[](size_t pos) const;/*可读可写接口,要重载两个版本*/
size_t size() const;
size_t capacity() const;
void clear();
const char *c_str() const;/*只读接口*/
void swap(String &str);
/*输入与输出*/
friend ostream &operator<<(ostream &out, const String &str);
friend istream &operator>>(istream &in, String &str);
private:
char *_str;
size_t _size;
size_t _capacity;
static const size_t npos = -1;/*常量整数可以在类内直接初始化*/
};
以上就是我们要模拟实现的接口,当然了,我们不可能将所有接口原模原样的复现出来,模拟实现的重点在于复习类和对象的知识,以及更加熟悉STL。
3.1成员变量
我们即使不清楚string类底层到底是怎么实现的,但我们可以大概猜测它就是一个顺序表,所以可以看到上面的成员变量有四个。其中"char *_str"用来指向字符数组;"size_t _size"用来表示有效元素个数,如果将_size作为下标,那么它指向的位置就是'\0';"size_t _capacity"用来表示容量,容量表示当前还能装下多少字符,注意,我模拟实现的_capacity是包括了'\0'了,即当_capacity=1时,表示当前容量为1,即已经存了一个'\0'。
3.2迭代器部分
对于string类来说,它的底层实际上就是一个字符数组,也就是顺序表,既然是顺序表那么就说明元素的指向、遍历、增删查改都可以通过指针去完成,所以string类的迭代器就是一个原生指针。那么有关于迭代器的接口上面已经实现好了:
iterator begin() {return _str;}
iterator end() {return _str + _size;}
const_iterator begin() const{return _str;}
const_iterator end() const{return _str + _size;}
很显然,begin()要返回起始位置,直接将_str返回即可;end()要返回末尾位置,直接将'\0'的位置返回即可。
3.3类的默认成员部分
这里的默认成员指的是构造函数、拷贝构造、析构函数、赋值运算符重载。那么对于它们的实现是这样的:
String::String(const char *str) /*缺省值给到声明处,定义不要再给了*/
{
_capacity = strlen(str) + 1;
_size = _capacity - 1;
_str = new char[_capacity];
strcpy(_str, str);
}
String::String(const String &str)
{
_capacity = str._capacity;
_size = str._size;
_str = new char[_capacity];
memcpy(_str, str._str,str._capacity);
}
/*传统写法*/
//String &String::operator=(const String &str)
//{
// /*统一异地扩容*/
// char *newStr = new char[str._capacity];
// memcpy(newStr, str._str, str._capacity);
// delete[] _str;
// _str = newStr;
// _capacity = str._capacity;
// _size = str._size;
// return *this;
//}
String &String::operator=(String str)
{
swap(str);
return *this;
}
String::~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
对于构造函数来说,当参数为字符串时,那么它就将该字符串拷贝到自己底层的字符数组当中即可;但是当参数为当前对象的引用时(拷贝构造),记得前面说过,string类不以'\0'结尾,所以拷贝过去的不应该是字符串,而是底层的整个字符数组,这就是为什么我会写一个memcpy的原因。
对于赋值运算符重载来说,注释部分就是标准的、教科书式的写法,但是这样写太麻烦了,因为代码行数比较多;非注释部分是一种现代写法,函数的参数为形参,也就是说调用该函数时参数部分会发生一次拷贝构造,然后在函数体内直接调用了swap()函数交换底层的成员变量。最后当该函数退出时,形参会自动调用析构,此时就会将不需要的空间释放掉。我们以逻辑图来理解这种现代写法:
注意我们调用的swap()是自己写swap():
void String::swap(String &str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
不直接调用库当中swap()的原因在于,如果我们直接将对象作为库当中swap()的参数,那么在交换过程中会发生三次深拷贝,这个过程体现在源码当中:
// STL的swap()实现
template <class T> void swap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
那么我们自己写的swap()就是将深拷贝的对象尽量变小,最终变成了内置类型。这里就需要注意了,STL当中swap()函数的函数体内有一"T c(a)"写法,我们知道,如果T是自定义类型,那么这就是在调用拷贝构造,那如果T是内置类型,也能这么写吗?答案是可以这么写,原因就出现在模板上,严格意义上来说,内置类型没有拷贝构造、析构函数这些概念,但是因为有了模板,而模板参数不知道是不是内置类型,所以就允许内置类型采用自定义类型的写法(支持一部分):
int main()
{
int a(10);
//int a = 10;
int b(a);
//int b = a;
return 0;
}
3.4容量接口
容量接口无非就是reserve()和resize(),它们的实现是这样的:
void String::reserve(int capacity)
{
if (capacity > _capacity)/*扩容只有往大了扩*/
{
/*统一异地扩容*/
char *newStr = new char[capacity];
/*这里不能用strcpy,因为strcpy遇到'\0'截止
*我们说过string类不以'\0'结尾*/
memcpy(newStr, _str, _capacity);
delete[] _str;
_str = newStr;
_capacity = capacity;
}
}
void String::resize(size_t n, const char &ch)
{
/*resize有三种情况:小于当前有效个数->删除
*大于当前有效个数&&小于当前容量->扩展
*大于等于当前容量->扩容+扩展*/
if (n <= _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
for (int i = _size; i < n; i++)
{
push_back(ch);/*复用,push_back()自动检查是否需要扩容*/
}
}
}
对于reserve()来说,只有扩容没有缩容,所以扩容之后的容量一定要大于当前容量。
对于resize()来说,分成三种情况,但大体上可以分为两种,也就是说,如果resize()之后的有效个数小于等于当前字符个数,那就与删除无异;其他情况就相当于尾插。我们的尾插直接复用push_back()接口(后面有它的实现),push_back()的内部会自动检测是否需要扩容并且还会自动添加'\0'。
3.5增删查改接口
这一部分接口是核心接口,它们是这样实现的:
void String::push_back(const char &ch)
{
if (_size + 1 == _capacity)
{
/*扩容*/
reserve(_capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
String &String::insert(size_t pos, size_t n, const char &ch)
{
assert(pos < _size);
if (_size + n + 1 >= _capacity)
{
reserve(_size + n + 1);
}
size_t end = _size + n;
while (end >= pos + n)
{
_str[end] = _str[end - n];
--end;
}
while (pos <= end)
{
_str[pos++] = ch;
}
_size += n;
return *this;
}
String &String::insert(size_t pos, const String &s)
{
assert(pos < _size);
if (_size + s._size + 1 >= _capacity)
{
reserve(_size + s._size + 1);
}
size_t end = _size + s._size;
while (end >= pos + s._size)
{
_str[end] = _str[end - s._size];
--end;
}
/*插入字符串时不需要插入'\0'*/
strncpy(_str + pos, s._str, s._size);
_size += s._size;
return *this;
}
String &String::append(size_t n, const char &ch)
{
if (_size + n + 1 >= _capacity)
{
reserve(_size + n + 1);
}
while (n--) push_back(ch);
return *this;
}
String &String::append(const String &s)
{
if (_size + s._size + 1 >= _capacity)
{
reserve(_size + s._size + 1);
}
memcpy(_str + _size, s._str, s._size + 1);
_size += s._size;/*最后不忘修改_size*/
return *this;
}
String &String::operator+=(const char &ch)
{
push_back(ch);
return *this;
}
String &String::operator+=(const String &s)
{
append(s);
return *this;
}
String &String::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size -= len;
return *this;
}
memcpy(_str + pos, _str + pos + len, _size - (pos + len) + 1);
_size -= len;
return *this;
}
它们的实现逻辑都非常简单,勤画图就可以搞定。需要注意的是,表示位置的类型为size_t,即无符号整数,这就意味着它们没有负数。
3.6通用接口
/*可读可写接口,要重载两个版本*/
char &String::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char &String::operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
size_t String::size() const
{
return _size;
}
size_t String::capacity() const
{
return _capacity;
}
void String::clear()
{
_size = 0;
_str[0] = '\0';
}
size_t String::find(const char &ch, size_t pos) const
{
assert(pos < _size);
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch) return i;
}
return npos;
}
size_t String::find(const String &str, size_t pos) const
{
assert(pos < _size);
char *findStr = strstr(_str, str._str);
if (findStr == nullptr) return npos;
return findStr - _str;
}
String String::operator+(const String &str)
{
String tmp(*this);
tmp += str;
return tmp;
}
这里需要注意string类当中的find()和算法库当中的find()的差别,string类当中的find()的返回值是下标,算法库的find()的返回值是迭代器。
3.7输入与输出
ostream &operator<<(ostream &out, const String &str)
{
for (auto &e : str) out << e;
return out;
}
istream &operator>>(istream &in, String &str)
{
str.clear();
char buffer[128];
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
str += buffer;
i = 0;
}
buffer[i++] = ch;
buffer[i] = '\0';
ch = in.get();
}
if (i >= 0)
{
buffer[i] = '\0';
str += buffer;
}
return in;
}
CPP 复制 全屏
我们之前多次强调string类不以'\0'结尾,所以在实现流插入运算符重载时不应该站在字符串的角度去实现;对于流提取运算符重载来说,我们的思路是利用ostream类提供的get()方法一个一个地读取字符,并先将这些字符送入缓冲区buffer当中,待缓冲区满时,再尾插到String对象当中。这样地做法能够减少频繁扩容。
3.8完整代码
模拟实现string类——String.hpp完整代码