string in [Cpp] v.s. String in [Java]
- 0. Intro
- 1. String in Java
- 2. String in Cpp
- 3. 模拟实现string类
0. Intro
刚刚学习了cpp中STL提供的string类,之前也有浅学Java相关String类的知识点,于是希望整理有关知识,加深记忆,并区分不同的方法与函数,巩固记忆,当然重点还是放在STL中的string类上
1. String in Java
1.1 String类
1.1.1 String 快速入门
🍁 String对象用于保存字符串,也就是一组字符序列
🍁 字符串常量对象是用双引号括起的字符序列。
🍁 字符串的字符使用Unicode字符编码,一个字符(不区分字母还是汉字)占两个字节。
🍁 String类较常用构造器(其它看jdk)
String s1 = new String();
String s2 = new String(String original);//很像拷贝构造
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex,int count);
🍁 String 类实现了接口Serializable【String 可以串行化:可以在网络传输】,接口Comparable 【String 对象可以比较大小】
🍁 String 是 final 类,不能被其他的类继承
🍁 String 有属性private final char value[ ]; 用于存放字符串内容
🍁 value 是一个final类型,不可以修改:即value 不能指向新的地址,但是单个字符内容是可以变化
1.1.2 创建String的两种方式
方式一:直接赋值String s= "Allen";
先从常量池查看是否有"Allen"数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s最终指向的是常量池的空间地址
方式二:调用构造器String s2 = new String("Allen");
先在堆中创建空间,里面维护了value属性,指向常量池的Allen空间,如果常量池没有"Allen",重新创建,如果有,直接通过value指向。最终指向的是堆中的空间地址。
1.1.3 牛刀小试
对于==,比较的是值是否相等
如果作用于基本数据类型的变量,则直接比较其存储的值是否相等,
如果作用于引用类型的变量,则比较的是所指向的对象的地址是否相等。
其实==比较的不管是基本数据类型,还是引用数据类型的变量,比较的都是值,只是引用类型变量存的值是对象的地址
对于equals方法,比较的是是否是同一个对象
String类equals()方法源码:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
🌰
String a = "abc";
String b ="abc";
System.out.println(a.equals(b));//T
System.out.println(a==b); //T
🌰🌰
String a = new String("abc");
String b = new String("abc");
System.out.println(a.equals(b));//T
System.out.println(a==b); //F
🌰🌰🌰
String a = "abc"; //a 指向 常量池的 “abc”
String b =new String("abc");//b 指向堆中对象
System.out.println(a.equals(b)); //T
System.out.println(a==b); //F
//b.intern()
System.out.println(a==b.intern()); //T
System.out.println(b==b.intern()); //F
知识点:
当调用intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(Object)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用
这里b.intern()方法最终返回的是常量池的地址(对象).
🌰🌰🌰🌰
Person p1 = new Person();
p1.name = "Allen";
Person p2 = new Person();
p2.name = "Allen";
System.out.println(p1.name.equals(p2.name));//比较内容: True
System.out.println(p1.name == p2.name); //T
System.out.println(p1.name == "Allen"); //T
String s1 = new String("bcde");
String s2 = new String("bcde");
System.out.println(s1==s2); //False
1.1.4 String的特性
🌿 String是一个final类,代表不可变的字符序列
🌿 字符串是不可变的。一个字符串对象一旦被分配,其内容是不可变的
🌰
下面这段代码其实就是创建了2个对象,不是改变字符串的内容
String s1 = "hello";
s1="haha";
🌰🌰
下面的代码创建了几个对象?
String a = "hello"+"abc";
只有1个对象
String a = "hello"+"abc"; //==>优化等价 String a = "helloabc";
编译器做一个优化,判断创建的常量池对象,是否有引用指向
相当于直接创造了一个”helloabc"
🌰🌰🌰
下面前三行创建了几个对象?
String a = "hello"; //创建 a对象
String b = "abc";//创建 b对象
String c = a + b;//创建c对象
String d = "helloabc";
System.out.println(c == d);//false
String e = "hello" + "abc";//直接看池, e指向常量池
System.out.println(d == e);//是true
三个对象
分析一下发生的过程
- 先 创建一个 StringBuilder sb = StringBuilder()
- 执行 sb.append(“hello”);
- sb.append(“abc”);
- String c= sb.toString()
- 最后其实是 c 指向堆中的对象(String) value[] -> 池中 “helloabc”
1.2 StringBuffer类
1.2.1 StringBuffer快速入门
🍁 StringBuffer 的直接父类是AbstractStringBuilder
🍁 StringBuffer 实现了Serializable, 即StringBuffer 的对象可以串行化
🍁 在父类中AbstractStringBuilder 有属性char[] value,不是final,该value数组存放字符串内容,存放在堆中
🍁 StringBuffer 是一个final 类,不能被继承
🍁 因为StringBuffer 字符内容是存在char[] value, 所有在变化(增加/删除),不用每次都更换地址(即不是每次创建新对象), 所以效率高于String
1.2.2 StringBuffer和String的转换
//String——>StringBuffer
String str = "hello";
//方式1 使用构造器
//注意: 返回的才是StringBuffer 对象,对str 本身没有影响
StringBuffer stringBuffer = new StringBuffer(str);
//方式2 使用的是append 方法
StringBuffer stringBuffer1 = new StringBuffer();
stringBuffer1 = stringBuffer1.append(str);
//StringBuffer ->String
StringBuffer stringBuffer2 = new StringBuffer("阿巴阿巴");
//方式1 使用StringBuffer 提供的toString 方法
String s = stringBuffer2.toString();
//方式2: 使用构造器来搞定
String s1 = new String(stringBuffer2);
🌰
下面的代码中,想要实现转换第二个构造器会出现问题不能传一个null的字符串
String str = null;// ok
StringBuffer sb = new StringBuffer(); //ok
sb.append(str);//需要看源码 , 底层调用的是 AbstractStringBuilder 的 appendNull
System.out.println(sb.length());//4
System.out.println(sb);//null
//下面的构造器,会抛出NullpointerException
StringBuffer sb1 = new StringBuffer(str);//err
//看底层源码 super(str.length() + 16);
System.out.println(sb1);
1.3 StringBuilder类
StringBuilder是一个可变的字符序列。此类提供一个与StringBuffer兼容的API,但不保证同步(StringBuilder不是线程安全)。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候。该类因为在大多数实现中,它比StringBuffer要快
在StringBuilder上的主要操作是append 和insert方法,可重载这些方法,以接受任意类型的数据。
1.3.1 StringBuilder快速入门
🍁 StringBuilder 继承 AbstractStringBuilder 类
🍁 实现了 Serializable ,说明StringBuilder对象是可以串行化(对象可以网络传输,可以保存到文件)
🍁 StringBuilder 是final类, 不能被继承
🍁 StringBuilder 对象字符序列仍然是存放在其父类 AbstractStringBuilder的 char[] value;因此,字符序列是堆中
🍁 StringBuilder 的方法,没有做互斥的处理,即没有synchronized 关键字,因此在单线程的情况下使用StringBuilder
1.4 String、StringBuffer、StringBuilder的比较和选择
1.4.1 比较
🍁 StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且方法也一样
🍁 String:不可变字符序列,效率低,但是复用率高。
🍁 StringBuffer:可变字符序列、效率较高(增删)、线程安全,看源码
🍁 StringBuilder:可变字符序列、效率最高、线程不安全
🍁 String使用注意:
String s="a"; //创建了一个字符串
s+= "b";
//实际上原来的"a"字符串对象已经丢弃了,现在又产生了一个字符串s+"b"(也就是"ab")。如果多次执行这
//些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会
//极大影响程序的性能=>结论:如果我们对String做大量修改,不要使用String
1.4.2 选择
🌿 如果字符串存在大量的修改操作,一般使用StringBuffer 或StringBuilder
🌿 如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder
🌿 如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer
🌿 如果我们字符串很少修改,被多个对象引用,使用String,比如配置信息等
2. String in Cpp
2.1 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;
2.2 常用构造函数接口
(constructor)函数名称 | 功能 |
---|---|
string() (重要) | 构造空的string类对象,即空字符串 |
string(const char* s) (重要) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) (重要) | 拷贝构造函数 |
2.3 容量操作Capacity
函数名称 | 功能 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符 |
reserve | 为字符串预留空间 |
resize | 将有效字符的个数该成n个,多出的空间用字符c填充 |
🍁 size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
🍁 clear()只是将string中有效字符清空,不改变底层空间大小。
🍁 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不会改变容量大小。
演示size/clear/resize/capacity
void Test1()
{
// 注意:string类对象支持直接用cin和cout进行输入和输出
string s("helloworld");
cout << s.size() << endl;//10
cout << s.length() << endl;//10
cout << s.capacity() << endl;//15
cout << s << endl;
// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
s.clear();
cout << s.size() << endl;//0
cout << s.capacity() << endl;//15
// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
// “aaaaaaaaaa”
s.resize(10, 'a');
cout << s.size() << endl;//10
cout << s.capacity() << endl;//15
// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
// "aaaaaaaaaa\0\0\0\0\0"
// 注意此时s中有效字符个数已经增加到15个
s.resize(15);
cout << s.size() << endl;//15
cout << s.capacity() << endl;//15
cout << s << endl;//aaaaaaaaaa
// 将s中有效字符个数缩小到5个
s.resize(5);
cout << s.size() << endl;//5
cout << s.capacity() << endl;//15
cout << s << endl;//aaaaa
}
接下来看看String中用的很少但是Vector用得很多的地方:resize和reserve,来探讨一下它的扩容问题
🌿resize
string s1;
s1.resize(20, 'x');
cout << "size:" << s1.size() << endl;
cout << "capacity:" << s1.capacity() << endl;
cout << s1 << endl;
vector<int> countV;
countV.resize(100, 0);
capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象
size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象
resize在对象中插入n个空间,默认设置为'\0'
,如果空间需要减少,就应该是把空间保留到该数字的大小个空间,而且最 后一个空间肯定是放成'\0'
,capacity是不一定改变的
resize函数一般不会按照你给的数字进行扩容,而是会对齐扩容到4/8的倍数,不同的STL版本实现的效果是不同的
🌿reserve
reserve和resize区别:
resize既分配了空间,也创建了对象,可以通过下标访问。修改capacity大小,也修改size大小。
reserve是容器预留空间,但并不真正创建元素对象,只修改capacity大小,不修改size大小,
另外要知道
reserve的扩容在不同编译器下是不同的
g++和gcc下不同的增容结果,不同的编译器都是实现STL,但是底层如何增容和插入数据根据实现的不同而不同
string s4;
//s4.reserve(127);
int oldCp = s4.capacity();
for (char ch = 0; ch < 127; ++ch)
{
s4 += ch;
if (oldCp != s4.capacity())
{
cout << "增容:" << oldCp << "->" << s4.capacity() << endl;
oldCp = s4.capacity();
}
}
cout << s4 << endl;
2.4 遍历字符串的几种方式
函数名称 | 功能说明 |
---|---|
operator[ ] | 返回pos位置的字符,const string类对象调用 |
begin+ end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
2.4.1 下标+[ ]
这种遍历方式本质是采用了operator[](i)
这种方式来实现,这个遍历方式可以实现读写
string s1;
string s2("hello");
// 1、下标+[]
for (size_t i = 0; i < s2.size(); ++i)
{
// s2.operator[](i)
s2[i] = 'x';
}
cout << endl;
for (size_t i = 0; i < s2.size(); ++i)
{
// s2.operator[](i)
cout << s2[i] << " ";
}
cout << endl;
当然访问数组的时候其实也是可以用at函数
其中at函数和[ ]
的区别是一个越界报错,另一个是抛异常
string s1;
string s2("hello");
//s2[10]; // 越界断言报错
try
{
s2.at(10); // 越界抛异常
}
catch (exception& e)
{
cout << e.what() << endl;
}
2.4.2 迭代器
迭代器由于类似指针所以使用的时候要做解引用操作
// 2、迭代器
// [begin(), end() ) end()返回的不是最后一个数据位置的迭代器,返回是最后一个位置下一个位置
// 注意,C++中凡是给迭代器一般都是给的[)左闭右开的区间
// 迭代器类似指针
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
迭代器的意义是什么?
迭代器意义:像string、vector支持[ ]遍历,但是list、map等等容器不支持[ ]
我们就要用迭代器遍历,迭代器是一种统一使用的方式
2.4.3 反向迭代器
倒着遍历,reverse_iterator对应着rbegin和rend
// 反向迭代器
string s3("123456");
string::reverse_iterator rit = s3.rbegin();
while (rit != s3.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
小结: 传参数的时候,会经常出现const的对象,倘若传进来的是const对象,就应该使用const的迭代器,且不能修改
string::const_iterato r it = s.begin();//begin也可以cbegin也可以,可读性与规范性反正是
2.4.4 范围for循环
C++11 提供范围for,之前,也就是用到auto关键字的地方,特点是,写起来很简洁
依次取容器中的数据,赋值给e,自动判断结束
支持迭代器的就可以使用范围for
2.5 修饰符Modifiers
函数名称 | 功能说明 |
---|---|
push_back(使用不多) | 在字符串后尾插字符c |
append(使用不多) | 在字符串后追加一个字符串 |
operator+= | 在字符串后追加字符串str |
insert(使用不多) | 在字符前插入字符串str |
pop_back | 删除字符串尾字符 |
erase | 在字符串中清除字符 |
实际当中push_back和append用的不多,用的多的主要是operator+=,因为还是来的快
🌿insert
insert可以根据位置插入,但是少用,因为效率低,底层来说是头插的话需要往后移动所有数据,,可以说insert的时间复杂度是O(N2),有时候可以尾插然后选择逆置
string s1;
s1.push_back('h');
s1.push_back('e');
s1.push_back('l');
s1.push_back('l');
s1.push_back('o');
s1.append("world");
s1.append(s2);
s1.append(s2.begin(), s2.end());
s1 += ' ';
s1.insert(0, "x");
cout << s1 << endl;
s1.erase();
🌿 erase
erase一般是从某个位置开始删除某个长度的字符,pop_back用的还是没有erase来的多,而且string用删除操作也用的少一点
erase一般使用要谨慎,要一分为二的去看,越靠头上去上删,会挪动的字符数量会越多
2.6 字符串操作String operations
函数名称 | 功能说明 |
---|---|
c_str | 返回C格式字符串 |
find + npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
🌿c_str
虽然下面的两行结果是一样的,但是实现不同
前者调用string类提供重载,后者调用的内置类型的重载
string s1("hello world");
cout << s1 << endl; // 调用 operator<<(cout, s1)
cout << s1.c_str() << endl; // 调用 operator<<(cout, const char*)
好像还是没差啊?显示上区别在于,再看下面的
s1.resize(20);
s1 += "!!!";
cout << s1 << endl; // 调用 operator<<(cout, s1)
cout << s1.c_str() << endl; // 调用 operator<<(cout, const char*)
也就是c_str中间有
'\0'
就停止了,后面再+=什么东西是输不出来的,虽然输出没有!!!,但是size仍旧计算了进去
🌿 find + npos
先来看看什么是npos,npos就是整型的最大值
find的几种构造,可以传字符也可以传字符串
find的返回值如果没有找到的话就会返回npos,所以用了find找就要判断一下
// 假设要求取出文件名的后缀
string filename = "test.txt";
size_t pos = filename.find('.');//找到"."位置的pos
if (pos != string::npos)
{//两种构造的方式
//法一
string suff(filename, pos, filename.size() - pos);
//法二
string suff(filename, pos);
cout << suff << endl;
}
不过上面这种方法如果对于下面的字符串就有问题了
string filename = "test.txt.zip";
怎么办,倒着找就可以了,用rfind
string filename = "test.txt.zip";
size_t pos = filename.rfind('.');
if (pos != string::npos)
{
//法一
string suff(filename, pos, filename.size() - pos);
//法二
string suff(filename, pos);
cout << suff << endl;
}
🌿 substr
截取子串,之前的要求截取子串也可以
string filename = "test.txt.zip";
size_t pos = filename.rfind('.');
if (pos != string::npos)
{
//法一
string suff = filename.substr(pos, filename.size() - pos);
//法二
string suff = filename.substr(pos);
cout << suff << endl;
}
🌰
写一个函数分别取出一个string中的域名和协议名·
string url1 = "http://www.cplusplus.com/reference/string/string/rfind/";
string url2 = "https://blog.csdn.net/Allen9012?spm=1010.2135.3001.5343";
cout << GetDomain(url1) << endl;
cout << GetProtocol(url1) << endl;
cout << GetDomain(url2) << endl;
cout << GetProtocol(url2) << endl;
取出域名
string GetDomain(const string& url)
{
size_t pos = url.find("://");
if (pos != string::npos)
{//找头找尾
size_t start = pos + 3;
size_t end = url.find('/', start);
if (end != string::npos)
{
return url.substr(start, end - start);
}
else
{
return string();
}
}
else
{
return string();
}
}
取出协议名
string GetProtocol(const string& url)
{
size_t pos = url.find("://");
if (pos != string::npos)
{
return url.substr(0, pos - 0);
}
else
{//找不到返回空字符串
return string();
}
}
🌿 运算符重载Non-member function overloads
函数 | 功能说明 |
---|---|
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operators | 大小比较 |
🌿 getline
istream& getline (istream& is, string& str, char delim);//delims
istream& getline (istream& is, string& str);
Extracts characters from is and stores them into str until the delimitation character delim is found (or the newline character, '\n'
, for (2)).
读取有空格或多行时很方便
3. 模拟实现string类
3.1 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;
};
⚡️ 首先构造函数这里默认的缺省值不可以是空指针,因为这样是非法的,在函数体内部strlen是要对空指针解引用就报错了,可以这样改最好
string (const char* str = "")
{
if(nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
⚡️ 其次上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。
3.2 浅拷贝与深拷贝
3.2.1 浅拷贝
浅拷贝:也称位拷贝,**编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规。**要解决浅拷贝问题,C++中引入了深拷贝。
3.2.2 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情
况都是按照深拷贝方式提供。
深拷贝- 拷贝构造函数
string(const string& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
3.3 现代写法 V.S. 传统写法
为什么要区分现代写法和传统写法呢?先看一下代码
3.3.1 传统写法
要吃饭自己去做
拷贝构造
//拷贝构造函数 // s2(s1)
//1.传统写法
string(const string& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
赋值运算符重载
//1.传统,可能内存不够的时候,导致报异常,这将把原来的_str销毁
string& operator=(const string& s)
{//防止自己给自己赋值
if (this != &s)
{
delete[]_str;//不能先赋值,要先delete销毁
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
3.3.2 现代写法
拷贝构造
相当于我要吃饭,但是我自己不去做,反而是拿东西去换来吃
//2.现代写法
string(const string& s)
:_str(nullptr)
//不用strlen可以赋值nullptr,但是一定要有这句,因为_str是随机值的话交还给tmp
//之后调用析构,不能析构随机值
{
string tmp(s._str);//传入s对象的话就死循环了
swap(_str, tmp._str);
}
赋值运算符重载
相当于点外卖,外卖小哥送达了之后和你交换东西,你给了他不用的垃圾,让骑手帮我扔,然后把外卖给我
推荐用这个写法 ,一般不会自己给自己赋值就写这个
string& operator=(string s)
{
swap(_str, s._str);
return *this;
}
如果担心自己给自己赋值
string& operator=(const string& s)
{
if (this!=&s)
{
string tmp(s);
swap(_str, tmp._str);
return *this;
}
}
3.4 补全string类
之前只是简洁版的string类,接下来开始模拟实现string类的其他接口,在此之前先得补全string的几个成员变量
private:
char* _str;
size_t _size;
size_t _capacity;
那么因此赋值运算符重载和拷贝构造中的swap函数也要相应自己写一个
//交换函数有点重复,自己写一个
void swap(string& s)
{//域作用限定符,为了区分,左边没有域名,指全局
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
//拷贝构造函数 // s2(s1)
string(const string& s)
:_str(nullptr)
, _size(0)
,_capacity(0)
//不用strlen可以赋值nullptr,但是一定要有这句,因为_str是随机值的话交还给tmp
//之后调用析构,不能析构随机值
{
string tmp(s._str);
swap(tmp);
}
//赋值运算符重载 s1 = s3
string& operator=(string s)
{
swap(s);
return *this;
}
🍒 c_str和size
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
🍒 遍历功能函数
下标访问
//只读
const char& operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
// 可读,可写
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + size();
}
//使用
string s1("hello world");
string::iterator it = s1.begin();
while (it !=s1.end())
{
cout << *it << " ";
++it;
}
范围for循环
本质上其实是按照语法写,然后编译器自动调用迭代器来完成任务
//本质上就是替换成迭代器
string s1("hello world");
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
🍒 空间管理
reserve
注意这个reserve,偶然发现问题,出现吞’\0’
//开空间,扩展capacity
void reserve(size_t n)
{// 如果新容量大于旧容量,则开辟空间
if (n > _capacity)
{
char* tmp = new char[n + 1];
//strcpy(tmp, _str);//问题在于把'\0'吃了
strncpy(tmp, _str, _size + 1);//这里+1就是把\0拷进去
// 释放原来旧空间,然后使用新空间
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize
//开空间+初始化,扩展capacity,并且初始化空间,size也要动
void resize(size_t n,char val='\0')
{
if (n > _size)
{
// 如果n大于底层空间大小,则需要重新开辟空间
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, val, n - _size);//在原来之后,直到n之前,换为val
}
_size = n;
_str[n] = '\0';
}
🍒 几个增加字符串函数
以下的几种其实都可以用insert来实现
push_back
void push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_str[_size + 1] = '\0';
++_size;
//insert(_size, ch);
}
append
void append(const char* str)
{
size_t len = _size + strlen(str);
if (len > _capacity)
{
reserve(len);
}
strcpy(_str + _size, str);//用的很巧_str+size,指针直接指向\0
_size = len;
//insert(_size, str);
}
+=
+=其实可以直接用append和push_back
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
insert
//插入字符
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;
}*/
// end如果非负传入pos为0就死循环,只能改成这样写
/*size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end]-1;
--end;
}*/
//这个最好,指针倒是不存在什么问题
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + 1) = *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);
}
// 挪动数据
char* end = _str + _size;
while (end >= _str + pos)//到指定位置
{
*(end + len) = *end;
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
🍒 删除字符串的函数
erase
//也可以返回void
string& erase(size_t pos=0, size_t len=npos)
{
assert(pos < _size);
size_t leftLen = _size - pos;//剩下的长度,不是左边的长度
if (len >= leftLen)//pos后删除光
{
_str[pos] = '\0';
_size = pos;
}
else //删除中间部分
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
🍒 查找字符或者字符串的函数
size_t find(char ch, size_t pos = 0) const
{
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//从哪里开始去找
size_t find(char *str, size_t pos = 0) const
{
assert(pos <_size);
const char* ret = strstr(_str+pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
🍒 运算符重载
判断运算符
注意这里是传入两个字符串,不放到类里面
inline bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
inline bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
inline bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
inline bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
inline bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
inline bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
输出运算符
不能写在类里面,防止类指针抢占第一个参数,影响可读性
输入的特殊形式会把之前的原来的string清空
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
补充输出输入必须的迭代器和clear()
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
void clear()
{
_size = 0;
_str[0] = '\0';
}
getline
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
那么完整的string模拟实现由于太长,不方便写入博客展示,如果有兴趣的话,换一个访问我的GitHub,网速慢可尝试用魔法
https://github.com/Allen9012/cpp/tree/main/c%2B%2B%E5%88%9D%E9%98%B6/string%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B05