C++ string类详解及模拟实现

news2024/11/20 10:34:25

目录

【本节目标】

 1. 为什么学习string类?

1.1 C语言中的字符串

 1.2 面试题(暂不做讲解)

 2. 标准库中的string类

2.1 string类(了解)

 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

3. string类的模拟实现

3.1string类常用成员函数的模拟实现

3.2 浅拷贝和深拷贝 

 4.完整代码                                                            


【本节目标】

1. 为什么要学习string类
2. 标准库中的string类
3. string类的模拟实现
4. 扩展阅读


 1. 为什么学习string类?


1.1 C语言中的字符串

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


 1.2 面试题(暂不做讲解)

415. 字符串相加 - 力扣(LeetCode)

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


 2. 标准库中的string类


2.1 string类(了解)

string类的文档介绍:sscplusplus.com/reference/string/string/?kw=string

1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
4. 不能操作多字节或者变长字符的序列
在使用string类时,必须包含#include头文件以及using namespace std;


 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

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) (重点)拷贝构造函数

演示:

void Teststring()
{
    string s1; // 构造空的string类对象s1
    string s2("hello world"); // 用C格式字符串构造string类对象s2
    string s3(s2); // 拷贝构造s3
}

2.string类非成员函数

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

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

void Teststring()
{
	string s1; // 构造空的string类对象s1
	string s2("hello world"); // 用C格式字符串构造string类对象s2
	string s3(s2); // 拷贝构造s3
	cin >> s1;
	cout << s1 << endl;
	cout << s2 << endl;
}

3. string类对象的容量操作

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

演示:

void test1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, world!!!");
	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;
}

void test2()
{
	string s;
	// 将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 test3()
{

	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;

}

注意:
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不会改变容量大小。

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

函数名称功能说明
operator[] (重
点)
返回pos位置的字符,const string类对象调用
begin+ endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭
代器
rbegin + rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭
代器

范围for

C++11支持更简洁的范围for的新遍历方式

 string的遍历
迭代器 begin()+end()   for+[]  范围for
 注意:string遍历时使用最多的还是for+下标 或者 范围for(c++11后才支持)
 begin()+end()大多数使用在需要使用stl提供的算法操作string时,比如:采用reverse逆置string

演示:

void test4()
{
	string s1("hello world");
	const string s2("Hello world");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;

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

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

void test5()
{
	string s("hello world");
	// 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(auto的底层还是迭代器)
	for (auto ch : s)
		cout << ch << endl;
}

C++中的迭代器是一种用于遍历容器(如数组、向量、链表等)中元素的对象。迭代器提供了一种统一的访问容器元素的方式,使得我们可以通过迭代器来访问容器中的元素,而不用关心容器的具体实现细节。

在C++标准库中,迭代器被设计为类似指针的对象,可以通过递增(++)和递减(--)操作符来访问容器中的元素。迭代器可以分为多种类型,包括输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器,每种类型的迭代器支持不同的操作。

通过使用迭代器,我们可以在容器中进行元素的遍历、查找、修改等操作,这使得C++中的容器类成为非常强大和灵活的工具。在编写C++程序时,熟练掌握迭代器的使用可以帮助我们更高效地处理容器中的数据。

注意:普通迭代器和const迭代器的区别就是可不可以修改,const 对象就要去调用const的迭代器。

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

这是反向的迭代器,用法和正向的一样,只是反着遍历。

当然,如果你觉得反向迭代器太长,不方便,你可以直接用auto定义迭代器。

5.string类对象的修改操作

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

 测试string:
 1. 插入(拼接)方式:push_back  append  operator+= 
 2. 正向和反向查找:find() + rfind()
 3. 截取子串:substr()
 4. 删除:erase

演示:

void test6()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'w';           // 在str后追加一个字符'w'   
	str += "orld";          // 在str后追加一个字符串"orld"
	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;
}


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

string增删查改函数及其它常用函数总结:

1.增加数据

void test1()
{
	string s("hello world");
	//任意位置做插入
	s.insert(5, "xxxxx");
	cout << s << endl;
	//头插(注意头插是以单个字符为单位)
	s.insert(0, 2, 'x');
	cout << s << endl;
}

2.删除数据

void test2()
{
	//从第5个位置开始删除4个字符
	string s("hello world");
	s.erase(5, 4);
	cout << s << endl;
	//第二个数据不给,从起始位置开始删除全部数据
	s.erase(0);
	cout << s << endl;
}

3.查找

void test3()
{
	string s("hello world");
	//查找空格位置
	size_t pos = s.find(' ');
	cout << pos << endl;
}

4.修改

修改有很多函数,但我们最常用的就是第一个。

eg:将所有空格位置都修改成 '%'

void test4()
{
	string s("h e l l o w o r l d");
	size_t pos = s.find(' ');
	while (pos!=string::npos)
	{
		//从pos位置开始的一个字符修改成'%'
		s.replace(pos, 1, "%");
		pos = s.find(' ');
	}
	cout << s << endl;
}

5.c_str 函数

eg:用c的方式将string创建的字符串读取到文件中,直接读取肯定是不行的,c语言中fopen函数要求是const char* 类型,而string是自定义类型,这时候类型不一样不能进行直接操作,为了兼容c语言string引入了c_str函数,返回c语言形式下字符串的指针类型。

void test5()
{
	string filename("FileName.cpp");
	FILE* f = fopen(filename.c_str(), "r");
	char ch = fgetc(f);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(f);
	}
}


3. string类的模拟实现


3.1string类常用成员函数的模拟实现

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

成员变量,指向字符串的指针,字符个数,容量大小:

class my_string
{
public:

private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

接下来我们实现类的构造,拷贝构造,析构函数,赋值重载:

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

			strcpy(_str, str);
		}
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		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;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

        我们在写构造的时候并没有去使用初始化列表,我们发现初始化这三个变量的时候都是要调用strlen函数去计算字符的长度,为了提高效率,避免重复使用就用了赋值的操作,而这种赋值的操作在初始化列表中像这样写是不行的,因为初始化列表初始的先后是根据变量在声明时候的先后来确定的,所以这里是不能使用初始化列表的。我们发现给了构造函数缺省参数, 这是为了满足在使用srting实例化对象的时候可以不进行初始化,如果没有提供实际的字符串作为参数,就会使用空字符串作为默认值,确保对象可以被正确初始化,库中的string在这里是默认给一个字符的空间,来存放'\0‘。在构造函数中初始化_str的时候为什么不能直接将str给_str,因为str为const类型权限不够,且_str必须是一个可以修改的值,所以只能先给_str开足够的空间,再将str的内容拷贝给_str。

        这里的赋值重载也是十分简单的,使用一个临时的空间交换数据,返回结果就可以了,需要注意的是返回的引用类型。(它返回引用类型的原因是为了支持连续赋值操作,比如str1 =str2=str3。通过返回引用类型,可以实现将多个赋值操作连在一起,确保每次赋值操作后返回的是被赋值的对象本身,而不是一个临时副本。)


 模拟实现string类中的迭代器:

        为什么说是string中的迭代器而不是迭代器,string中的空间是连续的,我们简单的去调用指针就可以去到达迭代器的效果(代器的类型是与容器类型相关联的,不同容器类型对应不同的迭代器类型。每种容器类型都有其特定的迭代器类型)

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

这样就实现了const和非const两种string的迭代器了。

效果:

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

	}


其它成员函数

返回整个字符串,返回size,下标访问

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

		size_t size() const
		{
			return _size;
		}

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

			return _str[pos];
		}

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

			return _str[pos];
		}

在这段代码中,成员函数 c_str() 、 size() 、 operator[] 分别被声明为 const 成员函数和非 const 成员函数。这种设计的目的是为了实现常量对象和非常量对象的区分,以提供更灵活的使用方式。

1. const char* c_str() const : 这个成员函数返回字符串的 const char* 指针,因为它不修改对象的成员变量,所以被声明为 const 成员函数。这样可以确保在常量对象上也可以调用这个函数。

2. size_t size() const : 这个成员函数返回字符串的大小,同样不修改对象的成员变量,因此被声明为 const 成员函数。这样可以在常量对象上安全地调用。

3. const char& operator[](size_t pos) const 和 char& operator[](size_t pos) : 这两个 operator[] 函数分别用于常量对象和非常量对象,以实现对字符串的访问。 const 版本的 operator[] 用于常量对象,返回一个 const char& ,表示只能读取字符而不能修改;非 const 版本的 operator[] 用于非常量对象,返回一个 char& ,允许修改字符。这种设计使得可以根据对象的常量性选择合适的操作符重载版本。

用来打印的函数

打印不能修改,要加const,因此需要有const的成员函数可以被调用,在这里就需要成员函数const的版本了。

    void print_str(const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			//s[i]++;
			cout << s[i] << " ";
		}
		cout << endl;

		string::const_iterator it = s.begin();
		while (it != s.end())
		{
			// *it = 'x';

			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

用来增加(尾插)数据的函数及扩容的函数

下面是这些成员函数的逻辑解释:

		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)
			{
				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;
		}
		string& operator+=(char ch)
		{
			push_back(ch);

			return *this;
		}

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

			return *this;
		}

1. void reserve(size_t n) :
- 如果传入的大小 n 大于当前容量 _capacity ,则进行扩容。
- 创建一个新的大小为 n + 1 的临时字符数组 tmp 。
- 将原字符串 _str 的内容复制到临时数组 tmp 中。
- 删除原字符串 _str 的内存空间。
- 将临时数组 tmp 赋值给字符串成员 _str ,更新容量 _capacity 为 n 。

2. void push_back(char ch) :
- 如果当前字符串大小 _size 等于容量 _capacity ,则需要扩容。
- 计算新的容量 newCapacity ,如果当前容量为0,则设置为4,否则为原容量的两倍。
- 调用 reserve(newCapacity) 进行扩容。
- 将字符 ch 添加到字符串末尾,更新字符串大小 _size ,并在末尾添加字符串结束符 \0 。

3. void append(const char* str) :
- 获取要追加的字符串 str 的长度 len 。
- 如果追加后的总长度(当前大小 _size 加上 str 的长度 len )超过容量 _capacity ,则进行扩容。
- 将字符串 str 复制到当前字符串的末尾,更新字符串大小 _size 。

4.对于+=只需要重新去调用实现好的函数就好了

试试效果:

	void test()
	{
		string s1("hello ");
		
		s1 += "world";
		cout<< s1.c_str() << endl;
	}

头插

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

			if (_size == _capacity)
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newCapacity);
			}

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}

			_str[pos] = ch;
			_size++;
		}

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			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;
			}

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

1. void insert(size_t pos, char ch) :
- 首先,该函数接受要插入的位置 pos 和要插入的字符 ch 。
- 确保插入位置在有效范围内(小于等于当前字符串长度 _size )。
- 如果当前字符串的大小达到容量上限 _capacity ,则扩展容量。
- 从插入位置开始,依次将原字符串中的字符向后移动一个位置,为新字符 ch 腾出空间。
- 在插入位置处设置新字符 ch ,并增加字符串的大小 _size 。

2. void insert(size_t pos, const char* str) :
- 该函数接受要插入的位置 pos 和要插入的字符串 str 。
- 确保插入位置在有效范围内。
- 计算要插入的字符串 str 的长度 len 。
- 如果插入字符串后的总长度将超出容量 _capacity ,则扩展容量。
- 从插入位置开始,将原字符串中的字符逐个向后移动 len 个位置,为插入字符串 str 腾出空间。
- 使用 strncpy 函数将字符串 str 插入到指定位置处,并更新字符串的大小 _size 。

这两个函数实现了在字符串的指定位置插入字符或字符串的功能,并且在需要时会动态调整字符串的容量。如果你有任何疑问或需要进一步解释,请随时告诉我!

对任意位置进行删除

class string
{
public:
	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;
		}
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	const static size_t npos = -1;
};

1. 首先,函数接受两个参数: pos 表示擦除的起始位置, len 表示要擦除的字符数,缺省值为 npos 。
2. 函数首先检查起始位置 pos 是否有效,即小于当前字符串的大小 _size 。
3. 如果 len 等于 npos (默认值)或者起始位置 pos 加上要擦除的长度 len 大于等于当前字符串的大小 _size ,则说明要擦除的范围包括了字符串末尾或超出字符串末尾:
- 在这种情况下,将起始位置 pos 处的字符设置为结束符 '\0' ,表示字符串的结束。
- 更新字符串的大小 _size 为 pos ,即擦除到指定位置 pos 处。
4. 如果擦除范围在字符串中间:
- 使用 strcpy 函数将从起始位置 pos + len 处开始的剩余字符串复制到起始位置 pos 处,覆盖要擦除的字符。
- 减少字符串的大小 _size 以反映已擦除的字符数 len 。
5. 函数完成擦除操作后,字符串中从位置 pos 开始的字符将被擦除,而其余字符将向前移动以填补擦除的空间。

注意:此处的nops变量,可以声明时定义是因为加了const,你可以认为这是一种标示,让编译器做了特殊处理,使之可以进行定义,但这这种特殊处理只能用于整型,不能是其它类型。

查找

		size_t find(char ch, size_t pos = 0)
		{
			for (size_t i = pos; i < _size; i++)
			{
				if (_str[i] == ch)
				{
					return i;
				}
			}

			return npos;
		}

	
		size_t find(const char* str, size_t pos = 0)
		{
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr)
			{
				return npos;
			}
			else
			{
				return ptr - _str;
			}
		}

1. find 函数用于从指定位置 pos 开始在字符串中查找给定的子字符串 str 的第一次出现。
2. 使用 strstr 函数在从位置 pos 开始的字符串 _str 中查找子字符串 str 。
3. 如果 strstr 函数返回 nullptr ,表示从指定位置开始未找到子字符串。此时,函数返回 npos 的值(通常定义为表示“未找到”的大值)。
4. 如果找到子字符串,函数通过将指向子字符串开头的指针 ptr 减去指向原始字符串开头的指针 _str ,计算子字符串在原始字符串中的索引位置。
5. 然后,函数返回找到的子字符串的索引位置,或者如果从指定位置开始未找到子字符串,则返回 npos 。

substr(从pos位置开始取len个字符)

string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			size_t end = pos + len;
			if (len == npos || pos + len >= _size)
			{
				end = _size;
			}

			string str;
			str.reserve(end - pos);
			for (size_t i = pos; i < end; i++)
			{
				str += _str[i];
			}

			return str;
		}

1. substr 函数用于从指定位置 pos 开始截取指定长度 len 的子字符串。
2. 首先,函数检查起始位置 pos 是否在有效范围内(小于字符串的大小 _size )。
3. 然后,计算截取的结束位置 end 为 pos + len 。如果指定的长度 len 为 npos 或者起始位置加上长度超过了字符串的大小,则将结束位置设置为字符串的末尾。
4. 创建一个新的字符串 str 来存储截取后的子字符串。
5. 使用 reserve 函数预留足够的空间以容纳截取后的子字符串,避免不必要的内存重新分配。
6. 遍历从起始位置 pos 到结束位置 end 的字符,并将它们添加到新的字符串 str 中。
7. 最后,返回包含截取后子字符串的新字符串 str 。

我们来测试一下这个函数:

	void test()
	{
		string str("hello ");
		
		string sub1;
		int pos1 = str.find('o');
		sub1 = str.substr(0, pos1-0);
		cout<< sub1.c_str() << endl;
	}

我们发现出错了,这是为什么呢?

这是发生浅拷贝的缘故

传值返回并不能返回当前对象,而是返回当前对象的拷贝。str会调用拷贝构造创建临时对象,将临时对象返回给str,但是我们并没有写拷贝构造,这里的拷贝是浅拷贝,浅拷贝对于内置类型直接拷贝它的值,导致临时对象也指向了,函数中str指向的空间,出作用域,str会调用析构函数,str所指向的空间会被释放,导致临时对象返回野指针。

为了解决浅拷贝问题,我自己写拷贝构造和赋值操作

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

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

此时我们再次运行上面程序就成功了。


模拟实现string流插入和流提取

流提取

	void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128];
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

 函数重载了 >> 运算符,接受一个输入流对象 in 和一个字符串对象 s 作为参数。函数的主要功能是从输入流中读取字符串,遇到空格或换行符时停止读取,并将读取的字符串存储在参数 s 中。

在函数中,使用一个缓冲区 buff 来临时存储读取的字符,然后逐个字符读取输入流,直到遇到空格或换行符为止。读取的字符存储在 buff 中,当 buff 达到一定长度时(127个字符),将其添加到字符串 s 中,并清空 buff ,继续读取。最后,将剩余的字符添加到字符串 s 中,然后返回输入流对象 in 。

在这段代码中,调用 in.get() (in和scanf一样是不能读取到‘空格’和‘回车’的,因此要调用get函数来读取)函数是为了逐个读取输入流中的字符,以便构建字符串。每次调用 in.get() 可以读取输入流中的下一个字符,并将其存储在变量 ch 中。通过这种方式,可以逐个字符地读取输入流,直到遇到空格或换行符为止,从而构建出完整的字符串。

至于为什么要编写 clear 函数,这是因为在字符串类中,可能需要在不同的情况下清空字符串内容。在这段代码中, clear 函数的作用是将字符串对象的大小设置为0,同时将字符串内容的第一个字符设为 \0 ,以实现清空字符串的功能。这样做可以确保在需要清空字符串内容时,可以方便地调用 clear 函数来实现清空操作,使字符串对象重新变为空字符串,而不需要重复编写清空操作的代码。

流插入

    ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

这段代码是为自定义字符串类重载了输出流运算符 << 。函数接受一个输出流对象 out 和一个常量引用的字符串对象 s 作为参数。它的功能是将字符串 s 中的字符输出到输出流 out 中。

在函数中,使用了一个范围 for 循环来遍历字符串 s 中的每个字符。对于字符串中的每个字符 ch ,使用 << 运算符将其输出到输出流 out 中。这个循环实际上将字符串的每个字符输出到输出流中。

最后,函数返回输出流对象 out 。通过这种方式重载了 << 运算符,可以很方便地使用标准的输出流语法将自定义字符串类输出到输出流中。


3.2 浅拷贝和深拷贝 

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

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

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

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

拷贝构造和赋值操作不一样的写法:

	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}

	string& string::operator=(string s)
	{
		swap(s);

		return *this;
	}

首先是拷贝构造函数  ,它接受一个 string 对象 s 的引用作为参数。在函数内部,首先创建了一个临时的 string 对象 tmp ,并将参数 s 的内部字符串 _str 拷贝到了 tmp 对象中。然后通过调用 swap 函数,将 tmp 对象和当前对象进行交换,达到拷贝构造的目的。

接下来是赋值运算符重载函数 s。它接受一个 string 对象 s 作为参数,并返回一个 string 对象的引用。在函数内部,通过调用 swap 函数,将参数 s 和当前对象进行交换,实现了赋值操作。最后返回当前对象的引用。

这两个函数都使用了 swap 函数来交换对象的内部数据,这是一种常见的优化技巧,可以避免不必要的内存拷贝,提高程序的效率。

在上面的拷贝构造函数中,参数是一个 const string& s ,这意味着传递的是一个 string 对象的引用,并且在函数内部不会修改传入的对象。因为拷贝构造函数的目的是创建一个新的对象并初始化为另一个对象的副本,所以通常会使用引用来避免不必要的复制开销。

而在赋值运算符重载函数中,参数是 string s ,表示传递的是一个 string 对象的副本。因为在赋值操作中,我们通常会对传入的对象进行修改,所以直接传递一个副本是可以接受的。在这种情况下,函数内部对传入的对象进行修改,不会影响原始对象,因此不需要使用引用。

总的来说,拷贝构造函数通常使用引用来避免不必要的复制,而赋值运算符重载函数可以选择传递对象的副本来方便修改。


 4.完整代码                                                            

#include<assert.h>
#include<iostream>

using namespace std;

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

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

		size_t size() const
		{
			return _size;
		}

		string(const char* str = "");
		// ִд
		string(const string& s);
		string& operator=(string s);
		~string();

		const char& operator[](size_t pos) const;
		char& operator[](size_t pos);
		void reserve(size_t n);
		void push_back(char ch);

		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);
		void swap(string& s);
		size_t find(char ch, size_t pos = 0);
		//21:10
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);
		void clear();

	private:
		size_t _capacity = 0;
		size_t _size = 0;
		char* _str = nullptr;

		const static size_t npos = -1;
	};

	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
}
namespace bit
{
	string::string(const char* str)
	{
		_size = strlen(str);
		_capacity = _size;
		_str = new char[_capacity + 1];

		strcpy(_str, str);
	}

	// ִд
	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}

	string& string::operator=(string s)
	{
		swap(s);

		return *this;
	}

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

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

		return _str[pos];
	}

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

		return _str[pos];
	}

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

	void string::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 string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}

		strcpy(_str + _size, str);
		_size += len;
	}

	string& string::operator+=(char ch)
	{
		push_back(ch);

		return *this;
	}

	string& string::operator+=(const char* str)
	{
		append(str);

		return *this;
	}

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

		if (_size == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newCapacity);
		}
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		_str[pos] = ch;
		_size++;
	}

	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		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;
		}

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

	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

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

	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}

	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}

		return npos;
	}


	size_t string::find(const char* str, size_t pos)
	{
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		size_t end = pos + len;
		if (len == npos || pos + len >= _size)
		{
			end = _size;
		}

		string str;
		str.reserve(end - pos);
		for (size_t i = pos; i < end; i++)
		{
			str += _str[i];
		}

		return str;
	}

	void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}


	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128];
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1491617.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenAI 3年前的AI音乐生成项目:Jukebox,效果比SunoAI v3还好

原来OpenAI 3年前就开始搞AI音乐生成了 效果甚至比最近发布的sunoAI v3还要好&#xff0c;难道OpenAI 想把这个隐藏大招练成无人能敌的状态才放出来再一次轰动全球&#xff1f; OpenAI在2019年8月份就推出了他们的一音乐生成模型&#xff1a;Jukebox Jukebox能够根据提供的歌…

【快速上手QT】07-对话框QDialog

QDialog 今天讲一个我们这个系列的第一篇就提到的东西&#xff1a;QDialog。 相信经过前几篇的学习&#xff0c;大家应该是能够通过QT助手来对QDialog有个初步的了解。 我们就直接来测试一下。 #include "Zhetu.h"#include <qdebug.h> #include <QPushBu…

C++输入输出(I\O)

我们知道C是由C语言发展而来的&#xff0c;几乎完全兼容C语言&#xff0c;换句话说&#xff0c;你可以在C里面编译C语言代码。如下图: C语言是面向过程的语言&#xff0c;C在C语言之上增加了面向对象以及泛型编程机制&#xff0c;因此C更适合中大型程序的开发&#xff0c;然而C…

RK DVP NVP6158配置 学习

NVP6158简介 NVP6158C是一款4通道通用RX&#xff0c;提供高质量图像的芯片。它接受来自摄像机和其他视频信号的独立4通道通用输入来源。它将4通道通用1M至8M 7.5P视频格式数字化并解码为代表8位ITU-R BT.656/1120 4:2:2格式的数字分量视频&#xff0c;并将单独的BT.601格式与27…

计算机网络——概述

计算机网络——概述 计算机网络的定义互连网&#xff08;internet&#xff09;互联网&#xff08;Internet&#xff09;互联网基础结构发展的三个阶段第一个阶段——APPANET第二阶段——商业化和三级架构第三阶段——全球范围多层次的ISP结构 ISP的作用终端互联网的组成边缘部分…

Nodejs 第四十九章(lua)

lua Lua是一种轻量级、高效、可嵌入的脚本语言&#xff0c;最初由巴西里约热内卢天主教大学&#xff08;Pontifical Catholic University of Rio de Janeiro&#xff09;的一个小团队开发而成。它的名字"Lua"在葡萄牙语中意为"月亮"&#xff0c;寓意着Lua…

数据分析-Pandas数据y轴双坐标设置

数据分析-Pandas数据y轴双坐标设置 数据分析和处理中&#xff0c;难免会遇到各种数据&#xff0c;那么数据呈现怎样的规律呢&#xff1f;不管金融数据&#xff0c;风控数据&#xff0c;营销数据等等&#xff0c;莫不如此。如何通过图示展示数据的规律&#xff1f; 数据表&…

平衡搜索二叉树—AVL树

一、定义&#xff1a; 为了避免搜索二叉树的高度增长过快&#xff0c;降低二叉树的性能&#xff0c;规定在插入和删除二叉树的结点的时候&#xff0c;任何结点左右子树的高度差绝对值不超过1&#xff0c;这样的二叉树被称为平衡二叉树&#xff08;balanced Binary Tree&#xf…

为PDF创建目录(侧边栏目录)

通过可以新建书签的pdf阅读器。 知云翻译&#xff1a;可以新建书签和子书签。 Adobe Acrobat&#xff1a;只能新建书签&#xff0c;不能建立子书签。

DA14531在三星手机手写笔的应用让我打开眼镜

手写笔的功能 这是一款内置蓝牙功能的魔性笔&#xff0c;它是遥控器、是照相、切换摄像头、是暂停或者打开播放列表。乃至更多操作-通过不同的手势隔空操作&#xff0c;或者按下触控按键便可轻松搞定。 手写笔硬件设计 内部结构 采用2.3V可循环充电电池&#xff0c;放入手…

软件测试零基础新手入门必看

软件测试&#xff1a;使用技术手段验证软件是否满足使用需求 目的&#xff1a;减少缺陷&#xff0c;保证质量 一、测试主流技能&#xff1a; 1.功能测试 测试主要验证程序的功能是否满足需求 2.自动化测试 使用工具或代码代替手工&#xff0c;对项目进行测试 3.接口测试 …

【原理图PCB专题】Allegro模块化移动器件报...has the LOCKED property怎么解锁?

在模块化原理图时,PCB也需要做一个模块.mdd文件。这时需要先画好图纸然后再制作模块化文件。 修改文件时会发现模块化器件报错,无法编辑模块内部器件和走线,器件和走线都被LOCKED,如下所示报错内容: Symbol "U1" Selected Cannot edit Symbol "U1". M…

(Linux学习七)进程介绍

一、进程 进程生命周期&#xff1a;由系统程序。form出来的子程序&#xff0c;具备一定的父的资源&#xff08;权利&#xff0c;内存空间&#xff0c;PID&#xff09;直到运行完毕&#xff0c;退出系统 查看进程 ps aux 查看所有进程参数&#xff1a;aux ps a 显示现行…

剑指offer 二维数组中的查找 C++

目录 前言 一、题目 二、解题思路 1.直接查找 2.二分法 三、输出结果 前言 最近在牛客网刷题&#xff0c;刷到二维数组的查找&#xff0c;在这里记录一下做题过程 一、题目 描述 在一个二维数组中&#xff08;每个一维数组的长度相同&#xff09;&#xff0c;每一行都按照…

00. Nginx总结-错误汇总

/www/wangmingqu/index.html" is forbidden (13: Permission denied) 错误图片 错误日志 2024/01/09 22:26:27 [error] 1737#1737: *1 "/www/wangmingqu/index.html" is forbidden (13: Permission denied), client: 192.169.1.101, server: www.wangmingqu.c…

前端知识点、技巧、webpack、性能优化(持续更新~)

1、 请求太多 页面加载慢 &#xff08;webpack性能优化&#xff09; 可以把 图片转换成 base64 放在src里面 减少服务器请求 但是图片会稍微大一点点 以上的方法不需要一个一个自己转化 可以在webpack 进行 性能优化 &#xff08;官网有详细描述&#xff09;

数据结构与算法:堆排序和TOP-K问题

朋友们大家好&#xff0c;本节内容来到堆的应用&#xff1a;堆排序和topk问题 堆排序 1.堆排序的实现1.1排序 2.TOP-K问题3.向上调整建堆与向下调整建堆3.1对比两种方法的时间复杂度 我们在c语言中已经见到过几种排序&#xff0c;冒泡排序&#xff0c;快速排序&#xff08;qsor…

光伏发电预测

XGB、LGB在datacamp(学习网站) data fountain与国家电投系列赛,光伏发电预测 题目:给一组特征,预测瞬时发电量,训练集9000个点,测试集8000个点,特征包含光伏板的属性和外部环境等。 数据字段:ID、光伏电池板背侧温度、光伏电站现场温度、计算得到的平均转换效率、数…

Javaweb之SpringBootWeb案例之自动配置案例的自定义starter分析的详细解析

3.2.4.1 自定义starter分析 前面我们解析了SpringBoot中自动配置的原理&#xff0c;下面我们就通过一个自定义starter案例来加深大家对于自动配置原理的理解。首先介绍一下自定义starter的业务场景&#xff0c;再来分析一下具体的操作步骤。 所谓starter指的就是SpringBoot当…

数据删除

目录 数据删除 删除员工编号为 7369 的员工信息 删除若干个数据 删除公司中工资最高的员工 Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 数据删除 删除数据就是指删除不再需要的数据 delete from 表名称 [where 删…