STL库容器之一:string(深浅拷贝以及VS和G++的扩容机制)

STL(标准模板库)中的 string 容器是C++中用于处理字符串的类,提供了丰富的功能来替代C风格的字符数组,具有更高的安全性和便捷性。以下是对 string 容器的核心介绍:

基本特性

  • 头文件:需包含 <string>

  • 本质:是 basic_string<char> 的别名,管理动态字符数组,自动处理内存(无需手动分配/释放)。

  • 优势:支持动态扩容、丰富的成员函数、操作符重载,避免缓冲区溢出。

接下来是string的一些接口函数,我们挑几个函数进行实现,达到让我们自己写的string可以使用一些基本功能即可。

string类的常用接口说明

  • string类对象的成员变量
	private:
		char* _str;
		size_t _size;
		size_t _capacity;

_str用来存放字符串,_size记录字符串的长度,_capacity记录为该字符串开辟的空间。

  • string类对象的构造函数

		//string()
		//	:_str(new char[1])
		//	,_size(0)
		//	,_capacity(0)
		//{
		//	_str[_size] = '\0';
		//}

		//string(const char* s)	
		//	:_size(strlen(s))
		//	,_capacity(_size)
		//{
		//	_str = new char[_capacity + 1];
		//	strcpy(_str, s);
		//}

		string(const char* str = "")
		{
			size_t len = strlen(str);
			_capacity = len == 0 ? 3 : len;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
			_size = len;
		}

我在第一次写string构造函数的时候,就是上面注释内容一样,构造了两个构造函数进行初始化,但是不断学习发现,可以通过缺省参数实现两个函数的归整,这样就简化了代码。还有一点就是,我们在new(堆)申请空间时,会预先开辟的空间(_capacity)多开辟一个空间,这样做的目的是为了存放'\0'。

深浅拷贝问题

可以看到非常完美,两个变量都申请到了自己的空间,现在,我们可以回忆一下类和对象的时候关于拷贝构造的问题了,记得当时一看编译器自动生成的默认构造貌似很完美,不需要我们人为操作就可以实现,现在我们尝试用s1来初始化s2,看看会有什么情况?

奇怪了,程序怎么突然崩溃了呢?明明两个结果都打印出来了,为什么最后还是崩溃了呢?其实,报错的地方在析构函数。

这就是经典的深浅拷贝问题了,涉及内存申请的时候就会有这个问题,就比如成员变量中的_str就涉及内存申请的问题。因此如果我们不手动写一个拷贝构造,系统默认的就是值拷贝,这就导致s1和s2两个内容中的成员变量都是同一个_str,当s1进行析构并成功释放空间并将_str改为nullptr时,当s2进行析构时_str为nullptr,这时候编译器是无法释放一个地址为nullptr的内存,所以就会导致崩溃!

举个例子就是,当你每天看到你舍友谈了一个很好的对象,善解人意,体贴入微,你就想也找这么好的对象,于是,你就按照这样的标准开始找对象,恰巧不巧的,你找到了你舍友对象,刚开始还相安无事,各做各的(相当于执行c_str函数),突然有一天,你舍友知道你也谈了一个,于是你们两准备把各自的女朋友带过来,这时候本来4个人的见面,只见了三个,这就有点尴尬了(类似析构的时候),这个时候你就和你的舍友开始拳脚问候了(崩溃了)。所以,值拷贝就是你们两个找到了相同的对象,因此,为了维护和谐的舍友关系,这时候我们就需要进行深拷贝,就是各找各的,找的对象都是类型差不多,厉害的话,即使找到了一对双胞胎,也是一人一个,没有冲突,这就是深浅拷贝问题。

所以这个时候我们就需要手动写一个拷贝构造,不然就会造成浅拷贝的问题。

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

这次就可以明显看到,_str指向了不同的空间,这样就不会在析构的时候出问题,这样我们就可以各自找到各自的对象了,不会因为找到同一个大打出手了。

  • string类对象的容量操作

  1. size()和length()底层实现原理完全相同,引入size()是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
  2. resize是将字符串的有效个数改为n个,如果元素的个数增多,会改变底层容量的大小,但如果将元素的个数减少,底层空间大小不变。
  3. reserve是为string预留空间,不改变有效元素的个数,当reserve的参数小于string的底层总大小时,reserve不会改变容量大小。
		size_t capacity() const
		{
			return _capacity;
		}
		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}

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

		void resize(size_t n, char c = '\0')
		{
			if (n < size())
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}

				while (_size < n)
				{
					_str[_size] = c;
					_size++;
				}

				_str[_size] = '\0';
			}
		}

注意,加上是std::这个域作用限定符就是库中的string。 

看到这里就有人奇怪了,你说的没错,确实resize和reserve当小于容量大小时,不会改变_capacity,但是为什么这里这两个值不同呢?这其实是因为扩容机制有所不同导致的,现在看看g++和VS下的扩容机制是什么样的?分别在vs和gcc下跑下面这段代码

g++和vs下的扩容机制是什么样的?

int main()
{
	std::string s;
	size_t old = s.capacity();
	cout << "初始化:" << old << endl;

	for (int i = 0; i < 100; i++)
	{
		s.push_back('x');

		if (old != s.capacity())
		{
			old = s.capacity();
			cout << "扩容:" << old << endl;
		}
	}
	return 0;
}

VS2022:

LINUX:

可以看到VS下string的扩容机制将近是1.5倍扩容,而g++下string的扩容机制是2倍扩容。

一般情况下,如果我们知道要求的字符个数有多少,可以提前开好空间,这样就可以减少扩容的代价,提高代码执行时间。

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

char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

const char& operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

 范围for的底层就是迭代器,所以实现了迭代器,范围for也就可以使用了。

begin就是_str的首元素地址,end则是'\0'的位置,因为iterator是char*,所以指针++,就是按照char的字节数进行++,所以这样迭代器就跑起来了。

这里分别实现了一个const迭代器和普通迭代器,功能就是能不能修改的问题。

当修改const迭代器,就会发生如下情况:

  • string类对象的修改操作

		void push_back(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);
			}
			_str[_size] = ch;
			_size += 1;
			_str[_size] = '\0';
		}

		void append(const char* s)
		{
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, s);
			_size += len;
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}

		string& insert(size_t pos, char c)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);
			}

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[end] = c;
			_size++;
			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;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}
		string& erase(size_t pos, size_t len = npos)
		{
			if (len == npos ||pos + len > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

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

		size_t find(char c, size_t pos = 0) const
		{
			assert(pos < _size);

			for (int i = pos; i < _size; i++)
			{
				if (_str[i] == c)
				{
					return i;
				}
			}
			return npos;
		}
		size_t find(const char* s, size_t pos = 0) const
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (strncmp(_str + pos, s,strlen(s)) == 0)
				{
					return pos;
				}
				pos++;
			}
			return npos;
		}

 在实现删除时需要一个引入静态的成员变量。

因此,在类的成员变量处增加了一个静态的成员变量

这时候就有点奇怪了,为什么npos不在构造函数处初始化呢,为什么你要在类外定义呢?这就要为我们看看这个在类内特殊的成员变量了。

静态成员变量

在C++中,静态成员变量是类的特殊成员:

1. 基本概念

  • 类级别共享:静态成员变量属于类本身,而非类的对象。所有对象实例共享同一份静态成员变量,生命周期与程序一致。

  • 唯一存储:无论创建多少对象,静态成员变量仅存在一个副本。

2. 声明与定义

  • 类内声明:使用 static 关键字在类内声明。

    class string {
    private:
        static size_t npos;  // 类内声明
    };
  • 类外定义:必须在类外单独定义(分配存储空间),且无需重复 static 关键字。

    size_t string::npos = -1; // 类外定义并初始化

string模拟实现的完整代码 

#include<iostream>
#include<cstring>
#include<cassert>

using namespace std;
namespace buluo
{
	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;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);
			return _str[pos];
		}

		bool operator!=(const string& s)
		{
			return strcmp(_str, s._str) != 0;
		}

		//string()
		//	:_str(new char[1])
		//	,_size(0)
		//	,_capacity(0)
		//{
		//	_str[_size] = '\0';
		//}

		//string(const char* s)	
		//	:_size(strlen(s))
		//	,_capacity(_size)
		//{
		//	_str = new char[_capacity + 1];
		//	strcpy(_str, s);
		//}

		string(const char* str = "")
		{
			size_t len = strlen(str);
			_capacity = len == 0 ? 3 : len;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
			_size = len;

		}

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

		string& operator=(const string& s)
		{
			char* tmp = new char[s.capacity() + 1];
			if (tmp == nullptr)
			{
				assert(tmp);
			}
			strcpy(tmp, s._str);
			delete[] _str;
			_str = tmp;
			_size = s.size();
			_capacity = s.capacity();

			return *this;
		}

		size_t capacity() const
		{
			return _capacity;
		}
		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _size == 0;
		}

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

		void resize(size_t n, char c = '\0')
		{
			if (n < size())
			{
				_size = n;
				_str[_size] = '\0';
			}
			else
			{
				if (n > capacity())
				{
					reserve(n);
				}

				while (_size < n)
				{
					_str[_size] = c;
					_size++;
				}

				_str[_size] = '\0';
			}
		}

		void push_back(char ch)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);
			}
			_str[_size] = ch;
			_size += 1;
			_str[_size] = '\0';
		}

		void append(const char* s)
		{
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, s);
			_size += len;
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}

		string& insert(size_t pos, char c)
		{
			if (_size + 1 > _capacity)
			{
				reserve(_capacity * 2);
			}

			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				end--;
			}
			_str[end] = c;
			_size++;
			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;
			while (end > pos + len - 1)
			{
				_str[end] = _str[end - len];
				end--;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}
		string& erase(size_t pos, size_t len = npos)
		{
			if (len == npos ||pos + len > _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}

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

		size_t find(char c, size_t pos = 0) const
		{
			assert(pos < _size);

			for (int i = pos; i < _size; i++)
			{
				if (_str[i] == c)
				{
					return i;
				}
			}
			return npos;
		}
		size_t find(const char* s, size_t pos = 0) const
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (strncmp(_str + pos, s,strlen(s)) == 0)
				{
					return pos;
				}
				pos++;
			}
			return npos;
		}

		~string()
		{
			delete[] _str;
			_capacity = _size = 0;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		static size_t npos;
	};
	size_t string::npos = -1;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值