漫步STL-string in [Cpp] v.s. String in [Java]

BingWallpaper5

string in [Cpp] v.s. String in [Java]

image-20220305204026633

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 对象可以比较大小】

image-20220215124231803

🍁 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指向。最终指向的是堆中的空间地址。

image-20220215131237321

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

image-20220215143049198

1.1.4 String的特性

🌿 String是一个final类,代表不可变的字符序列

🌿 字符串是不可变的。一个字符串对象一旦被分配,其内容是不可变的

🌰

下面这段代码其实就是创建了2个对象,不是改变字符串的内容

String s1 = "hello";
s1="haha"; 

image-20220215143559920

🌰🌰

下面的代码创建了几个对象?

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

三个对象

分析一下发生的过程

  1. 先 创建一个 StringBuilder sb = StringBuilder()
  2. 执行 sb.append(“hello”);
  3. sb.append(“abc”);
  4. String c= sb.toString()
  5. 最后其实是 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)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

小结:

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

⚠️ 使用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;

image-20220221220122865

2.4 遍历字符串的几种方式

函数名称功能说明
operator[ ] 返回pos位置的字符,const string类对象调用
begin+ endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
范围forC++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*)

image-20220223141806560

也就是c_str中间有'\0'就停止了,后面再+=什么东西是输不出来的,虽然输出没有!!!,但是size仍旧计算了进去

🌿 find + npos

先来看看什么是npos,npos就是整型的最大值

find的几种构造,可以传字符也可以传字符串

image-20220223202131793

find的返回值如果没有找到的话就会返回npos,所以用了find找就要判断一下

image-20220223194011036

// 假设要求取出文件名的后缀
	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共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

image-20220228134515835

3.2 浅拷贝与深拷贝

3.2.1 浅拷贝

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

3.2.2 深拷贝

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

image-20220228134734848

深拷贝- 拷贝构造函数

	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

  • 21
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值