1. 为什么学习string类?
- C语言中的字符串
C语言中,字符串是以'\0'
结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str
系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP
的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
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
类时,必须包含#include
头文件以及using namespace std
;
// std::string
//string是针对char类型的一种实例化,类似于下方的代码
typedef basic_string<char> string;
// 动态增长字符数组
template<class T>
class basic_string
{
private:
T* _str;
size_t _size;
size_t _capacity;
};
typedef basic_string<char> string;
2.2 string类的常用接口说明
1.string类对象的常见构造
(constructor):构造函数
default (1) string(); // 重点学习;构造空的string类对象,即空字符串
copy (2) string (const string& str); // 重点学习;拷贝构造函数
substring (3) string (const string& str, size_t pos, size_t len = npos);
from c-string (4) string (const char* s); // 重点学习;用C-string来构造string类对象
from sequence (5) string (const char* s, size_t n);
fill (6) string (size_t n, char c); // 重点学习;string类对象中包含n个字符c
range (7) template <class InputIterator>
string (InputIterator first, InputIterator last);
//用法如下:
(1) empty string constructor (default constructor)
空字符串构造函数(默认构造函数)
Constructs an empty string, with a length of zero characters.
构造一个空字符串,长度为0个字符。
(2) copy constructor
Constructs a copy of str.
(3) substring constructor 子字符串构造函数
Copies the portion of str that begins at the character position pos and spans len characters (or until the end of str, if either str is too short or if len is string::npos).
复制从字符位置pos开始并跨越len字符的部分(或者直到str结束,如果str太短或者len是string::npos)。
// string::npos
// std::string::npos
// static const size_t npos = -1;
(4) from c-string
Copies the null-terminated character sequence (C-string) pointed by s.
复制s指向的以空结束的字符序列(C-string)。
(5) from buffer
Copies the first n characters from the array of characters pointed by s.
从s指向的字符数组中复制前n个字符。
(6) fill constructor
Fills the string with n consecutive copies of character c.
用字符c的n个连续副本填充字符串。
- 演示样例
#include <iostream>
#include <string> // 使用string,必须包含头文件<string>
#include <vector>
#include <list>
using namespace std; // string也在std的命名空间中,因此,使用时需要展开
void test_test1()
{
// 不同的构造方式,则会调用不同的构造函数来构造对象
string s1; // 对应这个构造函数string()来创建对象;
string s2("西安市雁塔区"); // string (const char* s);来创建对象
// 因为没有explicit的限制,所以会进行隐式类型的转换,先生成一个临时变量,再拷贝构造创建s3
string s3 = "西安市雁塔区";
// string (size_t n, char c);
// 用字符c的n个连续副本填充字符串。
string s4(10, '*');
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
// 运算符重载
// string& operator+= (const char* s);
s2 += "瞪羚路";
cout << s2 << endl;
// 拷贝构造;对应string (const string& str);
string s5(s2);
// 拷贝构造
string s6 = s2;
cout << s5 << s6 << endl;
// 对应string (const char* s, size_t n);
// 从s指向的字符数组中复制前n个字符。
string s7("hello world", 5);
cout << s7 << endl;
// 对应string(const string & str, size_t pos, size_t len = npos);
// 复制从字符位置pos开始并跨越len字符的部分(或者直到str结束,如果str太短或者len是string::npos)。
string s8(s7, 2, 3);
cout << s8 << endl;
string s9(s7, 2, 30);
cout << s9 << endl;
//对应(const char* s, size_t n);
string s10(s7, 2);
cout << s10 << endl;
}
/*
// 重载 operator[]的机制
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
*/
void test_string2()
{
string s1("1234");
// 遍历他
// 1、下标 []; 重载了operator[]
for (size_t i = 0; i < s1.size(); ++i)
{
// char& operator[] (size_t pos); 可以在string的手册中查看相关用法
s1[i]++;
}
//s1[10]; // 重载,越界访问是一定会报错的(因为重载函数内的机制)
cout << s1 << endl;
// 2、范围for
for (auto& ch : s1)
{
ch--;
}
cout << s1 << endl;
// 反转一下字符串
size_t begin = 0, end = s1.size() - 1;
while (begin < end)
{
swap(s1[begin++], s1[end--]);
}
cout << s1 << endl;
// reverse(s1.begin(), s1.end());
cout << s1 << endl;
// 3、迭代器 -- 通用的访问形式(string形式的迭代器和vector和list的访问形式是相同的)
// s1.begin()指向字符串中的第一个字符
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
// 迭代之后,it1指向下一个字符
*it1 += 1;
++it1;
}
it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
vector<int> v; // 需要包含头文件<vector>
vector<int>::iterator vit = v.begin();
while (vit != v.end())
{
cout << *vit << " ";
vit++;
}
cout << endl;
list<int> lt; // 需要包含头文件<list>
list<int>::iterator ltit = lt.begin();
while (ltit != lt.end())
{
cout << *ltit << " ";
ltit++;
}
cout << endl;
}
// 正向iterator begin(); 反向reverse_iterator rbegin(); -- 可以遍历读写容器数据
// const正向:const_iterator begin() const;
// const反向:const_reverse_iterator rbegin() const;
// -- const迭代器只能遍历,不能修改容器数据
// 被const修饰的对象,只可以用const迭代器来迭代
void Print(const string& s)
{
// 遍历读,不支持写
string::const_iterator it = s.begin(); //此处的const修饰it
//const string::iterator it = s.begin(); // 此处的const修饰it指向的内容
while (it != s.end())
{
// *it += 1;
cout << *it << " ";
++it;
}
cout << endl;
// 反向迭代器,具体用法查看string手册
// string::const_reverse_iterator rit = s.rbegin();
auto rit = s.rbegin(); // auto自动识别类型
while (rit != s.rend())
{
cout << *rit << " ";
++rit; // ++rit是正方向,越来越靠近s.rbegin
}
cout << endl;
}
void test_string3()
{
// 使用这个构造函数构造string (const char* s);
string s1("1234");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//string::reverse_iterator rit = s1.rbegin();
auto rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
Print(s1);
}
int main()
{
test_test1();
return 0;
}
2.string类对象的容量操作
size(重点)
- 返回字符串有效字符长度
length
- 以字节为单位返回字符串的长度。
capacity
- 返回空间总大小
empty (重点)
- 检测字符串释放为空串,是返回true,否则返回false
clear (重点)
- 清空有效字符
reserve (重点)
- 为字符串预留空间
resize (重点)
- 将有效字符的个数该成n个,多出的空间用字符c填充
#include <iostream>
#include <string>
using namespace std;
void test_string4()
{
string s("hello world");
// 字符串的大小
cout << s.size() << endl;
// 字符串所占字节的大小
cout << s.length() << endl;
// string对象容量的大小
cout << s.capacity() << endl;
// 返回字符串最大可以有多大,根据当前系统
cout << s.max_size() << endl;
string ss;
cout << ss.max_size() << endl;
cout << s << endl;
cout << s.capacity() << endl;
// 清除字符串的内容,使其成为一个空字符串
s.clear();
cout << s << endl;
cout << s.capacity() << endl;
}
// 打印结果
11
11
15
2147483647
2147483647
hello world
15
15
void TestPushBack()
{
string s;
size_t sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
cout << s.size() << endl;
cout << "making s grow:\n";
for (int i = 0; i < 1000; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
// 打印结果
capacity changed: 15
0
making s grow:
capacity changed: 31
capacity changed: 47
capacity changed: 70
capacity changed: 105
capacity changed: 157
capacity changed: 235
capacity changed: 352
capacity changed: 528
capacity changed: 792
capacity changed: 1188
// 由上面的打印结果可以知道,扩容时string容量的变化
// 如果我们知道要插入多少数据,提前用reserve开好空间,避免不断扩容,提高效率
void TestPushBack()
{
string s;
// 请求将字符串容量调整为计划中的大小更改,最大长度为n个字符。
s.reserve(1000);
size_t sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
cout << s.size() << endl;
cout << "making s grow:\n";
for (int i = 0; i < 1000; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
/*
// 打印结果
capacity changed: 1007
0
making s grow:
*/
void test_string5()
{
TestPushBack();
}
void test_string6()
{
// hello world一共是11个字符
string s1("hello world");
// 将字符串的大小调整为5,那么后6个字符就会被删除
s1.resize(5);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl << endl;
// void resize (size_t n); // 没有指定插入的数据,则会默认插入'\0'
// void resize (size_t n, char c); // 指定插入的数据,则插入c
// 11<n<15 插入数据
string s2("hello world");
//s2.resize(15); // 没有指定插入的数据,则会默认插入'\0'
s2.resize(15, 'x');
cout << s2.size() << endl;
cout << s2.capacity() << endl;
cout << s2 << endl << endl;
// n>15 扩容+插入数据
string s3("hello world");
s3.resize(20, 'x');
cout << s3.size() << endl;
cout << s3.capacity() << endl;
cout << s3 << endl << endl;
}
/*
// 打印结果为
5
15
hello
15
15
hello worldxxxx
20
31
hello worldxxxxxxxxx
*/
int main()
{
test_string6();
return 0;
}
3.string类对象的修改操作
push_back
append
operator+=
void test_string7()
{
/*
string s1("hello world");
// push_back就只有这一种用法
// 对应 void push_back (char c);
s1.push_back(' ');
s1.push_back('!');
// append有多种用法,可以自行查看string手册
// 对应 string& append (const char* s);
s1.append("hello world");
cout << s1 << endl;
string s2("!!!!!!!");
// 对应 string& append (const string& str);
s1.append(s2);
cout << s1 << endl;*/
string s1("hello world");
s1 += ' ';
s1 += '!'; // 对应 string& operator+= (char c);
s1 += "hello world"; // 对应 string& operator+= (const char* s);
cout << s1 << endl;
string s2("!!!!!!!");
s1 += s2; // 对应 string& operator+= (const string& str);
cout << s1 << endl;
}
void test_string8()
{
string s("hello world");
s.insert(0, "qwy"); // 对应 string& insert (size_t pos, const char* s);
cout << s << endl;
s.insert(9, "qwy");
cout << s << endl;
s.erase(9, 3); // 对应 string& erase (size_t pos = 0, size_t len = npos);
cout << s << endl;
s.erase(0, 3);
cout << s << endl;
// string& erase (size_t pos = 0, size_t len = npos);
// s.erase(5, 30);
s.erase(5);
cout << s << endl;
}
// 打印结果
qwyhello world
qwyhello qwyworld
qwyhello world
hello world
hello
void test_string9()
{
string s1("hello world hello world");
string s2("hello world hello world");
string s3(s2);
string s4(s3);
// 从s指向的字符数组中复制前n个字符。
// string& assign (const char* s, size_t n);
s1.assign("hello qwy", 5);
cout << s1 << endl;
// replace的效率是比较慢的
// 对应string& replace (size_t pos, size_t len,const char* s);
// pos 就是要替换的第一个字符的位置
// len 要替换字符的长度
// 赋值指向s的字符串到string对象
s2.replace(6, 5, "qwy");
cout << s2 << endl;
// 将' '替换成20%
size_t find (const char* s, size_t pos = 0) const;
size_t pos = s3.find(' ');
while (pos != string::npos)
{
s3.replace(pos, 1, "20%");
pos = s3.find(' ', pos+3);
}
cout << s3 << endl;
// 对case1的优化
string ret;
// ret.reserve(s4.size());
// ret.reserve(s4.size() + space_size*2); // space_size为空格的个数
for (auto ch : s4)
{
if (ch != ' ')
{
ret += ch;
}
else
{
ret += "%20";
}
}
cout << ret << endl;
}
void test_string10()
{
string file("test.cpp");
// 对应const char* c_str() const noexcept;
// 调用成员函数c_str(),将string对象的字符串以c字符串的形式返回
FILE* fout = fopen(file.c_str(), "r");
assert(fout);
char ch = fgetc(fout);
while (ch != EOF)
{
cout << ch;
ch = fgetc(fout);
}
fclose(fout);
}
void test_string11()
{
// "Test.cpp"
// "Test.cpp.tar"
string file;
cin >> file;
// 要求取后缀
size_t pos = file.rfind('.');
// 对应 size_t rfind (const char* s, size_t pos = npos) const;
if (pos != string::npos)
{
// 对应string substr (size_t pos = 0, size_t len = npos) const;
// string suffix = file.substr(pos, file.size() - pos);
// 不指定len,那么就会使用缺省值
string suffix = file.substr(pos);
cout << suffix << endl;
}
}
// bool operator== (const string& lhs, const string& rhs);
// bool operator== (const char* lhs, const string& rhs);
// bool operator== (const string& lhs, const char* rhs);
void test_string12()
{
string s1("111111");
const char* p2 = "22";
s1 == p2;
p2 == s1;
}
int main()
{
test_string12();
return 0;
}
4.习题
仅仅反转字母
// 解题思路
class Solution {
public:
string reverseOnlyLetters(string s)
{
// 字符串的首尾下标分别为begin和end
size_t begin = 0, end = s.size()-1;
// 利用快排的思想,交换左右两边的字母
// int isalpha ( int c ); // 检查c是否为字母
while(begin < end)
{
while(begin < end && !isalpha(s[begin])) // 重载operator[]
++begin;
while(begin < end && !isalpha(s[end]))
--end;
swap(s[begin],s[end]);
++begin;
--end;
}
return s;
}
};
字符串相加
// 解题思路
class Solution
{
public:
string addStrings(string num1, string num2)
{
// 解题思路,从最低位开始相加,如果和大于10,则进一位
int end1 = num1.size()-1, end2 = num2.size()-1;
// carry n.进位
int carry = 0;
// 用来储存求出来的字符串的和
string retstr;
// 当两个字符串都迭代结束时,相加才会结束
while(end1 >=0 || end2 >= 0)
{
// 如果字符串还没有结束,则拿到end处的字符,否则赋值为0
// 字符串中存储的是字符的ASCLL值,因此要减去字符'0',将其变为数字
// 如:0的ASCLL值为48, 9的ASCLL值为57;因此57-48的值为9
// 从尾部的两个数开始相加
int val1 = end1 >= 0 ? num1[end1]-'0' : 0;
int val2 = end2 >= 0 ? num2[end2]-'0' : 0;
// 拿到最低位的两个数之后,我们再相加,还要加上进位
int ret = val1 + val2 + carry;
// 如果大于10,进位,并将其个位的值储存到要返回的string中
carry = ret / 10;
ret %= 10;
// 对应 string& insert (size_t pos, size_t n, char c);
// 字符串中储存的是字符,而不是数字,因此要加上字符'0'
// 进行头插
retstr.insert(0, 1, '0' + ret);
--end1; //处理完最低位后,减1,再处理前一位
--end2;
}
// 最后一个进位,可能会超出字符串个数,因此要单独处理
if(carry == 1)
{
// 进行头插
retstr.insert(0, 1, '1');
}
return retstr;
}
};
// 对上面解题思路的优化
class Solution
{
public:
string addStrings(string num1, string num2)
{
int end1 = num1.size()-1, end2 = num2.size()-1;
int carry = 0;
string retstr;
// 提前开好空间,防止不断扩容,提高效率
// 对应 void reserve (size_t n = 0);
retstr.reserve(max(num1[end1], num2[end2])+1);
while(end1 >=0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1]-'0' : 0;
int val2 = end2 >= 0 ? num2[end2]-'0' : 0;
int ret = val1 + val2 + carry;
carry = ret / 10;
ret %= 10;
//使用尾插,效率更高,最后只需要逆置字符串即可
retstr += ('0'+ret);
--end1;
--end2;
}
if(carry == 1)
{
retstr += '1';
}
// 逆置字符串
// 对应 void reverse (BidirectionalIterator first, BidirectionalIterator last);
reverse(retstr.begin(), retstr.end());
return retstr;
}
};
字符串中的第一个唯一字符
class Solution {
public:
int firstUniqChar(string s)
{
//创建一个数组来记录每个字母出现的次数
// 一共有26个字母
int count[26] = {0};
// 遍历一遍string,记录字母的出现的个数
for(char ch : s) //使用范围for来进行遍历
{
//字符使用ASCLL值进行存储的,因此减去'a',就可以得到相应的下标
count[ch - 'a']++;
}
// 再找出第一个不重复的字符
for(size_t i = 0; i < s.size(); i++)
{
if(count[s[i] - 'a'] == 1)
return i;
}
return -1;
}
};
字符串最后一个单词的长度
#include <iostream>
using namespace std;
int main()
{
string str;
// cin >> str; // cin输入时,遇到换行,或者空格,系统默认为多项输入间隔
// istream& getline (istream& is, string& str); 默认换行结束提取
getline(cin,str);
// 反向查找
size_t pos = str.rfind(' ');
cout << str.size() - 1 - pos << endl;
return 0;
}
3. string类的模拟实现
string的私有成员变量
char* _str; // _str指向存放字符串的指针
size_t _size; // 字符串的大小
size_t _capacity; // 存放字符串空间的容量
string的迭代器
typedef char* iterator;
// 开始迭代器,返回一个迭代器指针,这个指针指向字符串的第一个字符
iterator begin()
{
return _str;
}
// 结束迭代器,返回一个迭代器指针,这个指针指向字符串的最后一个字符
iterator end()
{
return _str + _size;
}
string的构造函数
// 不需要传参的构造函数
/*
string()
{
_str = new char[1];
_str[0] = '\0';
_capacity = _size = 0;
}
*/
// 构造函数
// '\0' // 字符\0, \0的ASCLL值为0,就相当于是空指针
// "\0" // 字符串存储了一个字符'\0',且以\0结束
// " " // 没有存储任何字符,以\0结束
// 也不可以使用nullptr来作为缺省参数,因为strlen读取空指针时会报错
// 使用全缺省,就不需要单独构建无参的构造函数
string(const char* str = "" )
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // 多的一个空间是给\0的
strcpy(_str, str);
}
string的插入函数
// 在指定位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
// 在插入一个新的字符前,需要判断当前空间的容量够不够,如果不够,那么就需要进行扩容
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
// 挪动数据(错误演示)
/*
int end = _size; //当pos为0时,就会 --end就为负数
// pos的类型为size_t,如果end为-1,则end会发生隐式类型转换,小范围往大范围提升
// 因此需要将pos强转为int类型
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
*/
// 挪动数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
// 在pos位置进行插入
_str[pos] = ch;
++_size;
// 返回string对象
return *this;
}
// 在指定位置插入字符串
string& insert(size_t pos, const char* str)
{
// 插入字符串之前,先要判断当前存放字符串的空间,容量是否足够
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
// 需要挪动的字符的个数为 _size-pos+1 (加1是因为也要挪动\0)
// 又因为_size = end - len; 所以需要挪动的字符的个数为 end-pos-len+1
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 拷贝len个字符,防止str的\0被拷贝,len是str的长度,但不包括\0
// 如果拷贝了\0,则读取时到\0结束,这样就读取不到后续的字符
// 从str拷贝len个字符到_str + pos
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string删除指定位置的字符串
// 删除pos位置后指定长度的字符
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
// 删除字符的数量 len+pos >= _size 也就是将剩余的字符全部删除
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
// 将_str + pos + len后的字符,拷贝到str + pos
// 这样就会将 _str+pos 到 _str+pos+len 之间要删除的字符串给覆盖掉
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
// 没有拷贝构造的情况下,一定要传引用返回,不然传值返回会调用系统默认的拷贝构造
// 那样的拷贝就是浅拷贝,这样的话,空间不会重新申请,两个对象指向同一块空间
// 会对同一块空间,调用两次析构函数,则编译器会报错
return *this;
}
string的查找函数
// 寻找字符,并返回字符的位置
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
string调整字符串大小的函数
// 调整储存数据个数的大小
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
string调整存储容量
// 申请存储的空间
void reserve(size_t n)
{
// 多一个空间来存储\0
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str; // 释放原空间
_str = tmp;
_capacity = n;
}
string尾插字符
// 尾插数据
void push_back(char ch)
{
// 当空间不够时,进行扩容
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string尾插字符串
// 尾插字符串
void append(const char* str)
{
// 如果容量不够,先进行扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 将str按字节拷贝到_str + _size指向的空间中
strcpy(_str + _size, str);
_size += len;
}
string运算符重载
// 重载operator+= ;(字符)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
// 重载operator+= ;(字符串)
string& operator+=(const char* str)
{
append(str);
return *this;
}
string的拷贝构造函数
// 自己写的交换
void swap(string& s)
{
std::swap(_str, s._str); // 调用库里面的交换
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 现代写法(拷贝构造函数)
// s2(s1)
// 必须对s2进行初始化,
// 如果没有初始化,s2._str就是野指针(里面存放的是一个随机值)
// 因此tmp与s2交换之后,tmp无法析构,编译器就会报错
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
// 临时创建一个string对象tmp,并使用s._str指针对这个对象进行构造
// 注:string(const char* str = "" ) 这是我们自己实现的构造函数
// 注:s._str就是一个指向字符串的字符指针,类型为char*
string tmp(s._str);
//this->swap(tmp);
swap(tmp);
}
// s2(s1)
// 拷贝构造传统写法
/*
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
*/
// s1 = s3;(对象赋值传统写法)
/*
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;
}
*/
// s1 = s3;(对象赋值的现代写法)
string& operator=(const string& s)
{
if (this != &s)
{
// 使用我们自己写的构造函数(构造函数,并传s._str初始化)
// string tmp(s._str);
// s就是s3的引用
// 拷贝构造一个临时对象,再将s1和tmp进行交换
// 注:使用的拷贝函数,是上述我们自己实现的拷贝函数string(const string& s)
string tmp(s);
swap(tmp);
}
return *this;
}
// s1 = s3;(对象赋值的现代写法的进一步优化)
// 传值传参s就是s3的拷贝构造,因此只需要交换s1和s
string& operator=(string s)
{
swap(s);
return *this;
}
string的析构函数
// 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
string的其他函数
// c形式的字符串
const char* c_str() const
{
return _str;
}
// 存储数据的大小
size_t size() const
{
return _size;
}
// 容量大小
size_t capacity() const
{
return _capacity;
}
// 普通对象:可读可写;重载operator[]
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// const对象:只读 重载operator[]
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
string的完整实现以及测试用例
// string.h
namespace qwy
{
class string
{
public:
// 迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// 不需要传参的构造函数
/* string()
{
_str = new char[1];
_str[0] = '\0';
_capacity = _size = 0;
}
*/
// 构造函数
// '\0' // 字符\0, \0的ASCLL值为0,就相当于是空指针
// "\0" // 字符串存储了一个字符'\0',且以\0结束
// "" // 没有存储任何字符,以\0结束
// 也不可以使用nullptr来作为缺省参数,因为strlen读取空指针时会报错
string(const char* str = "" ) // 使用全缺省,就不需要单独构建无参的构造函数
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1]; // 多的一个空间是开给\0的
strcpy(_str, str);
}
// 在指定位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
// 挪动数据
/*int end = _size; //当pos为0时,就会--end就为负数
//pos的类型为size_t,如果end为-1,则end会发生隐式类型转换,小范围往大范围提升
// 因此需要将pos强转为int类型
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)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
/* int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}*/
size_t end = _size + len;
// 需要挪动的字符的个数为 end-pos-len+1(加1是因为也要挪动\0)
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
// 拷贝len的字符,防止str\0被拷贝,
// 如果拷贝了\0,则读取时到\0结束,这样就读取不到后续的字符
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
// 删除pos位置后指定长度的字符
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
// 删除字符的数量 len+pos >= _size 也就是将剩余的字符全部删除
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
// 将_str + pos + len后的字符,拷贝到str + pos
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
// 没有拷贝构造的情况下,一定要传引用返回,不然传值返回会调用系统默认的拷贝构造
// 那样的拷贝就是浅拷贝,这样的话,空间不会重新申请,两个对象指向同一块空间
// 会对同一块空间,调用两次析构函数,则编译器会报错
return *this;
}
// 寻找字符,并返回字符的位置
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
// 找字符串的字串,并返回寻找的字符串的个数
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
// 调整储存数据个数的大小
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
// 清除数据
void clear()
{
_size = 0;
_str[0] = '\0';
}
// c形式的字符串
const char* c_str() const
{
return _str;
}
// 存储数据的大小
size_t size() const
{
return _size;
}
// 容量大小
size_t capacity() const
{
return _capacity;
}
// 普通对象:可读可写;重载operator[]
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
// const对象:只读 重载operator[]
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
// 申请存储的空间
void reserve(size_t n)
{
char* tmp = new char[n + 1]; // 多一个空间来存储\0
strcpy(tmp, _str);
delete[] _str; // 释放原空间
_str = tmp;
_capacity = n;
}
// 尾插数据
void push_back(char ch)
{
// 当空间不够时,进行扩容
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
// 尾插字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
// 重载operator+= ;(字符)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
// 重载operator+= ;(字符串)
string& operator+=(const char* str)
{
append(str);
return *this;
}
// 自己写的交换
void swap(string& s)
{
std::swap(_str, s._str); // 调用库里面的交换
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 现代写法(拷贝构造函数)
// s2(s1)
// 必须对s2进行初始化,
// 如果没有初始化,s2._str就是野指针(里面存放的是一个随机值)
// 因此tmp与s2交换之后,tmp无法析构,编译器就会报错
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str); // 构造函数
//this->swap(tmp);
swap(tmp);
}
// s2(s1)
// 拷贝构造传统写法
/*string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}*/
// s1 = s3;(对象赋值传统写法)
/*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;
}*/
// s1 = s3;(对象赋值的现代写法)
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// //string tmp(s._str); // 构造函数(构造函数,并传s._str初始化)
// string tmp(s); // 拷贝构造
// swap(tmp);
// }
// return *this;
//}
// s1 = s3;(对象赋值的现代写法的进一步优化)
// 传值传参s就是s3的拷贝构造,因此只需要交换s1和s
string& operator=(string s)
{
swap(s);
return *this;
}
// 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
//特例:const修饰的整数,可以直接给默认值来初始化(静态)
const static size_t npos = -1;
/*
const static size_t N = 10;
int a[N];
*/
//const static double x; // 其他类型的数,只能在类外面初始化(静态)
};
// 流插入
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
// 流提取
// 空格默认是多项输入的分隔符,因此对于cin,scanf是提取不到空格的
// 如果想要拿到空格,就需要使用get()函数
// std::istream::get
istream& operator>>(istream& in, string& s)
{
s.clear(); // 防止对象中本来就有数据,因此需要先清除
// 这种写法,如果字符串特别长,就需要不断扩容,会影响运行效率
//char ch = in.get();
//while (ch != ' ' && ch != '\n') // 当遇到空格或者换行时结束
//{
// s += ch;
// ch = in.get();
//}
// 对上面的算法进行了优化
char buff[128] = { '\0' };
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127) // 因为buff数组要留一个空间来放置\0
{
// 满了
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
/*
void func(const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i] << " ";
}
cout << s.c_str() << endl;
string::iterator it1 = s.begin();
while (it1 != s.end())
{
(*it1)--;
++it1;
}
cout << s.c_str() << endl;
for (auto ch : s) // 范围for的底层实现其实就是迭代器的底层实现
{
cout << ch << " ";
}
cout << endl;
}
*/
// 测试用例
void test_string1()
{
string s1("hello world");
cout << s1.c_str() << endl;
for (size_t i = 0; i < s1.size(); ++i)
{
s1[i]++;
}
cout << s1.c_str() << endl;
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
(*it1)--;
++it1;
}
cout << s1.c_str() << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
void test_string2()
{
string s1("hello");
s1 += ' ';
s1 += '!';
s1 += '!';
s1 += "world hello world";
cout << s1.c_str() << endl;
string s2;
s2 += 'x';
cout << s2.c_str() << endl;
}
void test_string3()
{
string s1("helloworld");
cout << s1.c_str() << endl;
s1.insert(5, ' ');
cout << s1.c_str() << endl;
s1.insert(0, 'x');
cout << s1.c_str() << endl;
string s2("helloworld");
cout << s2.c_str() << endl;
s2.insert(5, " + ");
cout << s2.c_str() << endl;
s2.insert(0, "hello ");
cout << s2.c_str() << endl;
s2.insert(0, "x");
cout << s2.c_str() << endl;
string s3;
s3.insert(0, "");
cout << s3.c_str() << endl;
}
void test_string4()
{
string s1("hello hello world");
s1.erase(0, 6);
cout << s1.c_str() << endl;
s1.erase(5);
cout << s1.c_str() << endl;
}
void test_string6()
{
string s1("hello world");
s1.resize(5);
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1.c_str() << endl << endl;
string s2("hello world");
//s2.resize(15);
s2.resize(15, 'x');
cout << s2.size() << endl;
cout << s2.capacity() << endl;
cout << s2.c_str() << endl << endl;
string s3("hello world");
s3.resize(20, 'x');
cout << s3.size() << endl;
cout << s3.capacity() << endl;
cout << s3.c_str() << endl << endl;
}
void test_string7()
{
// 流插入(编译器的实现方法)
/*
std::string s1("hello world");
cout << s1 << endl;
cout << s1.c_str() << endl;
s1.insert(5, 1, '\0');
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl;
cout << s1.c_str() << endl;
*/
// 流插入(我们自己实现的)
string s1("hello world");
cout << s1 << endl;
cout << s1.c_str() << endl;
s1.insert(5, '\0');
cout << s1.size() << endl;
cout << s1.capacity() << endl;
cout << s1 << endl; // 对对象打印,是按照数据个数打印
cout << s1.c_str() << endl;// 按照c字符串打印,是到\0结束
//
/* char ch1, ch2;
cin >> ch1 >> ch2;
*/
cin >> s1;
cout << s1 << endl;
string s2;
cin >> s2;
cout << s2 << endl;
string s3;
cin >> s3;
cout << s3 << endl;
}
void test_string8()
{
string s1("hello world");
string s2(s1);
cout << s1 << endl;
cout << s2 << endl;
string s3("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
s1 = s3;
cout << s1 << endl;
cout << s3 << endl;
s1.swap(s2); // 调用自己写的swap
cout << s1 << endl;
cout << s2 << endl;
// 调用库的swap,这个代价太大,影响运行效率;
// 因为模板需要实例化,也就是s1和s2都需要实例化
swap(s1, s2);
cout << s1 << endl;
cout << s2 << endl;
// 有了模板之后,内置类型也有构造和析构(兼容模板)
// int i(10);对内置类型进行构造 --> int i = 10;
// int j = int(); // 创建一个匿名对象int(),再进行赋值,匿名对象初始化为0
}
}
test.cpp
#include "string.h"
int main()
{
qwy::test_string8();
return 0;
}