【C++】STL--string类--拆析解剖string类的实现以及string类的底层详解(2)

上节内容我们使用了它的函数接口,但是没有实现这次我们对string类进行手写实现

一.vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

vs下string的结构

string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义

string中字符串的存储空间:当字符串长度小于16时,使用内部固定的字符数组来存放;当字符串长度大于等于16时,从堆上开辟空间

cpp 复制代码
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个字节,内部只包含了一个

指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数
  • 指向堆空间的指针,用来存储字符串。
cpp 复制代码
struct _Rep_base
{
size_type               _M_length;
size_type               _M_capacity;
_Atomic_word            _M_refcount;
};

二. string类的模拟实现

string 实际上就是一个管理 字符数组 的顺序表

上节已经对string类进行了简单的介绍,大家只要能够正常使用即可,主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数,在接下来进行手写实践

分为三个文件:

string.h

string.h 定义了一个完整的 bit::string 类,包含迭代器、构造/析构、容量操作、元素访问、修改操作、查找操作、全局比较和流运算符。短小函数直接在 .h 中定义(inline),复杂函数在 .h 中声明,需要在 .cpp 中实现。

完整代码:

cpp 复制代码
#include<assert.h>

using namespace std;

namespace bit
{
	class string {
	public:
		typedef char* iterator;
		typedef const char* const_iterator;
		//返回迭代器
		// 1. 非const对象调用,能修改元素
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}
		// 2. const对象专属,只读,不会修改成员
		const_iterator begin() const
		{
			return _str;
		}
		const_iterator end() const
		{
			return _str + _size;
		}
		//短小频繁调用的函数,可以直接定义在类里面。默认是inline
		string(const char* str = "")
		{
			_size = strlen(str);
			//_capacity不包括\0
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);

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

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

		// s2(s1)
		// 现代写法
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
		}



		// s2 = s1
		// s1 = s1
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				delete[] _str;

				_str = new char[s._capacity + 1];
				strcpy(_str, s._str);
				_size = s._size;
				_capacity = s._capacity;
			}

			return *this;
		}*/

		// s1 = s3;
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		//string tmp(s._str);
		//		string tmp(s);

		//		swap(tmp);
		//	}

		//	return *this;
		//}

		// s1 = s3;
		string& operator=(string tmp)
		{
			swap(tmp);

			return *this;
		}
		////////////////////////////////////////////////////////////////////////
		////赋值重载  深拷贝(开新空间 + 复制内容)
		////s2=s1
		////s1=s1
		//string& operator=(const string& s)  // 返回当前对象的引用
		//{
		//	if (this != &s)                  // 1. 防止自己给自己赋值
		//	{
		//		delete[] _str;               // 2. 释放当前对象原来的内存

		//		_str = new char[s._capacity + 1];  // 3. 开新空间(大小和 s 一样)
		//		strcpy(_str, s._str);        // 4. 拷贝内容

		//		_size = s._size;             // 5. 拷贝大小
		//		_capacity = s._capacity;     // 6. 拷贝容量
		//	}
		//	return *this;                    // 7. 返回当前对象
		//}
		///////////////////////////////////////////////////////
		//析构函数
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;

		}
		//c_str() 返回内部指针(只读)
		const char* c_str()const
		{
			return _str;
		}
		//清空字符串
		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}
		//返回字符串长度
		size_t size() const
		{
			return _size;
		}
		//返回容量
		size_t capacity() const
		{
			return _capacity;

		}
		//非 const 返回 char&(可读可写),const 返回 const char&(只读)
		//下标访问运算符重载,让 string 对象可以像数组一样用 [ ] 访问字符。
		// 普通对象调用(可读可写)
		char& operator[](size_t pos)
		{
			assert(pos < _size);    // 断言:pos 必须小于字符串长度
			return _str[pos];       // 返回字符的引用
		}

		// const 对象调用(只读)
		const char& operator[](size_t pos) const
		{
			assert(pos < _size);    // 断言:pos 必须小于字符串长度
			return _str[pos];       // 返回字符的常量引用
		}


		//这些在类里面声明,因为不频繁复=调用
		void reserve(size_t n);//预留空间,避免频繁扩容
		void push_back(char ch);//在字符串末尾追加一个字符
		void append(const char* str);//在末尾追加一个 C 字符串
		//追加字符或字符串(更简洁的写法)
		string& operator+=(char ch);
		string& operator+=(const char* str);
		void insert(size_t pos, char ch);//在 pos 位置插入一个字符
		void insert(size_t pos, const char* str);//在 pos 位置插入一个 C 字符串
		void erase(size_t pos, size_t len = npos);//从 pos 开始删除 len 个字符

		size_t find(char ch, size_t pos = 0);//从 pos 开始查找字符 ch,返回位置,找不到返回 npos
		size_t find(const char* str, size_t pos = 0);//从 pos 开始查找子串 str
		string substr(size_t pos = 0, size_t len = npos);//从 pos 开始截取 len 个字符,返回新字符串
	private:
		//char _buff[16];
		char* _str;
		size_t _size;
		size_t _capacity;

		//static const size_t npos = -1;
		static const size_t npos = -1;
	};
	//全局函数
	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);
	//流插入	operator<<	从程序 -->输出流	读取 s 的内容	const string&(只读)
	//流提取	operator>>	从输入流 -->程序	写入 s	string& (可修改)


	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);


}

具体分块进行说明:

1.迭代器(支持范围 for)

cpp 复制代码
iterator begin();              // 返回指向第一个字符的迭代器
iterator end();                // 返回指向末尾的迭代器
const_iterator begin() const;  // const 版本
const_iterator end() const;    // const 版本

2.构造 / 析构 / 赋值

cpp 复制代码
string(const char* str = "");  // 构造(支持默认空串)
string(const string& s);       // 拷贝构造(深拷贝)
string& operator=(string tmp); // 赋值(现代写法:传值+交换)
~string();                     // 析构(释放内存)

3.容量操作

cpp 复制代码
size_t size() const;           // 返回字符串长度
size_t capacity() const;       // 返回当前容量
void clear();                  // 清空字符串
void reserve(size_t n);        // 预留空间(避免扩容)

4.元素访问

cpp 复制代码
char& operator[](size_t pos);             // 可读可写(普通对象)
const char& operator[](size_t pos) const; // 只读(const 对象)
const char* c_str() const;               // 返回 C 风格字符串

5.修改操作

cpp 复制代码
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);      // 删除
void swap(string& s);                    // 交换两个字符串

6.查找操作

cpp 复制代码
size_t find(char ch, size_t pos = 0);        // 查找字符
size_t find(const char* str, size_t pos = 0);// 查找子串

7.截取操作

cpp 复制代码
string substr(size_t pos = 0, size_t len = npos);  // 截取子串

8.全局运算符(比较 / 输入 / 输出)

cpp 复制代码
// 比较运算符
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);

// 流插入 / 流提取
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);

问1:为什么有的函数写在类里面,有的只写在类外面?

:因为短小频繁的函数适合放类内 (自动变成内联,效率高),复杂冗长的函数适合放类外(减少头文件膨胀,编译快)

问2:为什么 operator== 这些比较函数放类外?

答:为了支持左右操作数都可以隐式转换。

cpp 复制代码
// 放类外:两边都能转换
"hello" == s;   //  左边 const char* 可以转换
s == "hello";   //  右边 const char* 可以转换

// 如果放类内(成员函数),左边必须是 string 对象
// s.operator==("hello") 
// "hello".operator==(s) 报错! 左边不是 string 对象

问3.:为什么 cin >> scout << s 放类外?

答:因为左操作数是流对象(cin/cout),不是 string 对象。

cpp 复制代码
// 左操作数是 istream,不是 string
istream& operator>>(istream& in, string& s);

// 左操作数是 ostream,不是 string
ostream& operator<<(ostream& out, const string& s);
如果放在类内作为成员函数,调用方式会变成:

s >> cin;   // 书写不符合习惯
s << cout;  //  同上

问4:你的代码里 swap 为什么放类内?

swap 很简短,只有3行,放类内自动变成内联,效率高。

cpp 复制代码
void swap(string& s) {
    std::swap(_str, s._str);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}

代码中:1行小函数(beginsizec_str)放类内;复杂函数(reserveinsertfind)只声明;比较和流运算符放类外(因为左操作数不是 string)。

好处:短小函数放类内(自动内联,调用快),复杂函数放类外(减少头文件依赖,编译快),全局运算符放类外(支持左右操作数隐式转换,且符合 cin >> s 的使用习惯)


string.cpp

.cpp 文件实现了 bit::string 所有成员函数和全局运算符,包括扩容、尾插、插入、删除、查找、截取、比较和输入输出。 namespace bit 里是为了和头文件的类匹配。

完整代码如下:

cpp 复制代码
//手写函数不是用库里面的
#include"string.h"
//如果 .cpp 不包 namespace bit,你写 void string::reserve 
// 编译器会认为这是全局域的 string 类,和头文件里 
// bit::string 是两个完全无关的类
// ,直接报未定义、匹配不上。套上同一个 namespace bit 后,
// string::reserve 等价于 bit::string::reserve,
// 和头文件声明的类匹配上。
namespace bit
{
	//const size_t string::npos = -1;
	//预留空间函数
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);//把原内容复制到新空间
			delete[] _str;//删除原来的空间
			_str = tmp;//tmp代替_str最初的位置
			_capacity = n;//容量变为n
		}
	}

	//尾插一个字符串
	void string::push_back(char ch)
	{
		//空间满的话
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

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

	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;//this	指向当前对象的指针
		//*this	当前对象本身(解引用)
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)//超出容量空间
		{
			// 大于2倍,需要多少开多少,小于2倍按2倍扩
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}
		strcpy(_str + _size, str);
		_size += len;
	}




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


	//insert 先检查位置和空间,然后从后往前把 [pos, _size] 的字符(包括 \0)往后挪一位,腾出位置插入新字符,最后更新 _size。

	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size); // 检查pos合法性,不能超过字符串当前长度
		// 扩容:如果容量用完,按规则扩容(空串扩为4,否则翻倍)
		if (_size == _capacity)
		{
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

		// 1. 挪动数据:从末尾开始,把字符向后移1位,腾出pos位置
		size_t end = _size + 1; // 扩容后长度+1,包括'\0'
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}

		// 2. 插入字符,更新长度
		_str[pos] = ch;
		++_size;
	}


	void string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);
		size_t len = strlen(s); // 计算插入字符串的长度
		// 扩容:如果总长度超过容量,按规则扩容(不够就扩到刚好,否则翻倍)
		if (_size + len > _capacity)
		{
			reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
		}

		// 1. 挪动数据:从末尾开始,把字符向后移len位
		size_t end = _size + len;
		while (end > pos + len - 1)
		{
			_str[end] = _str[end - len];
			--end;
		}

		// 2. 插入字符串,更新长度
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}
		_size += len;
	}




	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		// 情况1:删除长度超过剩余字符,直接截断到pos位置
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		// 情况2:正常删除,挪动数据覆盖被删除的部分
		else
		{
			for (size_t i = pos + len; i <= _size; i++)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}


	// 从pos位置开始,向后找字符ch
	size_t string::find(char ch, size_t pos)
	{
		// 检查:起始位置必须是有效字符位,不能是\0
		assert(pos < _size);

		// 从下标pos开始,逐个遍历有效字符
		for (size_t i = pos; i < _size; ++i)
		{
			if (_str[i] == ch)
			{
				// 找到,返回当前下标
				return i;
			}
		}
		// 遍历完都没找到,返回npos(表示不存在)
		return npos;
	}



	// 从pos位置开始,向后找子串s
	size_t string::find(const char* s, size_t pos)
	{
		assert(pos < _size);

		// _str + pos:指针跳转到查找起始位置
		const char* ptr = strstr(_str + pos, s);

		if (ptr == nullptr)
		{
			// 没找到
			return npos;
		}
		else
		{
			// 指针相减 = 子串起始下标
			return ptr - _str;
		}
	}

	string string::substr(size_t pos, size_t len)
	{
		// 只能从有效字符位置开始截取,不能取 \0
		assert(pos < _size);

		// 容错:剩余字符不够 len 个,就截到字符串末尾为止
		if (len >= _size - pos)
		{
			len = _size - pos;
		}

		string sub;        // 新建空字符串,用来存截取结果
		sub.reserve(len);  // 提前开空间,避免频繁扩容

		// 逐个拷贝字符
		for (size_t i = 0; i < len; i++)
		{
			sub += _str[pos + i];
		}
		return sub;
	}

	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}

	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}

	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}

	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}

	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}


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

		return out;
	}
	//流提取
	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		const int N = 256;
		char buff[N];
		int i = 0;
		char ch;

		//in >> ch;
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i++] = '\0';
				s += buff;
				i = 0;
			}

			//in>>ch;
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;


	}

}

分块描述:

1.文件头 + 命名空间

cpp 复制代码
#include "string.h"

namespace bit
{
}

包含头文件,所有实现包在 namespace bit 里,和头文件匹配。


2.容量管理

cpp 复制代码
void string::reserve(size_t n)
{
    if (n > _capacity)                      // 只有 n 大于当前容量才扩容
    {
        char* tmp = new char[n + 1];        // 开新空间
        strcpy(tmp, _str);                  // 拷贝原内容
        delete[] _str;                      // 释放旧空间
        _str = tmp;                         // 指向新空间
        _capacity = n;                      // 更新容量
    }
}

手动扩容,提前开好空间,避免多次扩容。


3.尾部插入

cpp 复制代码
void string::push_back(char ch)
{
    if (_size == _capacity)                 // 空间满了
    {
        reserve(_capacity == 0 ? 4 : _capacity * 2);  // 扩容
    }
    _str[_size] = ch;                       // 放字符
    ++_size;                                // 长度+1
    _str[_size] = '\0';                     // 末尾加结束符
}

string& string::operator+=(char ch)
{
    push_back(ch);                          // 复用 push_back
    return *this;                           // 返回自身,支持链式调用
}

void string::append(const char* str)
{
    size_t len = strlen(str);               // 要追加的字符串长度
    if (_size + len > _capacity)            // 空间不够
    {
        reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
    }
    strcpy(_str + _size, str);              // 拷贝到末尾
    _size += len;                           // 更新长度
}

string& string::operator+=(const char* str)
{
    append(str);                            // 复用 append
    return *this;
}

尾插字符/字符串,空间不够就扩容,+= 是简洁写法。


4.中间插入

cpp 复制代码
void string::insert(size_t pos, char ch)
{
    assert(pos <= _size);                   // 位置合法检查
    if (_size == _capacity)                 // 空间不够就扩容
    {
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    size_t end = _size + 1;                 // 从末尾(含\0)开始
    while (end > pos)                       // 从后往前挪数据
    {
        _str[end] = _str[end - 1];
        --end;
    }
    _str[pos] = ch;                         // 插入字符
    ++_size;                                // 长度+1
}

void string::insert(size_t pos, const char* s)
{
    assert(pos <= _size);
    size_t len = strlen(s);
    if (_size + len > _capacity)            // 空间不够就扩容
    {
        reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
    }
    size_t end = _size + len;               // 从末尾开始
    while (end > pos + len - 1)             // 从后往前挪 len 位
    {
        _str[end] = _str[end - len];
        --end;
    }
    for (size_t i = 0; i < len; i++)        // 插入字符串
    {
        _str[pos + i] = s[i];
    }
    _size += len;
}

在指定位置插入,从后往前挪数据腾出位置(不会覆盖未处理数据)。


5.删除

cpp 复制代码
void string::erase(size_t pos, size_t len)
{
    assert(pos < _size);
    if (len >= _size - pos)                 // 删除长度超过剩余字符
    {
        _str[pos] = '\0';                   // 直接截断
        _size = pos;
    }
    else                                    // 正常删除
    {
        for (size_t i = pos + len; i <= _size; i++)
        {
            _str[i - len] = _str[i];        // 后面数据往前覆盖
        }
        _size -= len;
    }
}

删除指定位置的字符,两种情况:删到末尾直接截断,删中间则后面数据往前覆盖。

6.查找

cpp 复制代码
size_t string::find(char ch, size_t pos)
{
    assert(pos < _size);
    for (size_t i = pos; i < _size; ++i)    // 遍历查找
    {
        if (_str[i] == ch) return i;
    }
    return npos;                            // 找不到返回 npos
}

size_t string::find(const char* s, size_t pos)
{
    assert(pos < _size);
    const char* ptr = strstr(_str + pos, s); // C 语言查找子串
    if (ptr == nullptr) return npos;
    return ptr - _str;                      // 指针相减得下标
}

查找字符或子串,找到返回下标,找不到返回 npos


7.截取子串

cpp 复制代码
string string::substr(size_t pos, size_t len)
{
    assert(pos < _size);
    if (len >= _size - pos) len = _size - pos;  // 不能超出范围
    string sub;
    sub.reserve(len);                           // 提前开空间
    for (size_t i = 0; i < len; i++)
    {
        sub += _str[pos + i];                   // 逐个拷贝
    }
    return sub;
}

截取子串,提前开空间避免频繁扩容。


8.比较运算符

cpp 复制代码
bool operator<(const string& s1, const string& s2)
{
    return strcmp(s1.c_str(), s2.c_str()) < 0;
}

bool operator==(const string& s1, const string& s2)
{
    return strcmp(s1.c_str(), s2.c_str()) == 0;
}

bool operator<=(const string& s1, const string& s2)
{
    return s1 < s2 || s1 == s2;      // 复用 < 和 ==
}

bool operator>(const string& s1, const string& s2)
{
    return !(s1 <= s2);              // 复用 <=
}

bool operator>=(const string& s1, const string& s2)
{
    return !(s1 < s2);               // 复用 <
}

bool operator!=(const string& s1, const string& s2)
{
    return !(s1 == s2);              // 复用 ==
}

只实现 <==,其他运算符通过逻辑关系复用它们。


9.输入输出

cpp 复制代码
ostream& operator<<(ostream& out, const string& s)
{
    for (auto ch : s)                // 范围 for 遍历
    {
        out << ch;
    }
    return out;
}

istream& operator>>(istream& in, string& s)
{
    s.clear();
    const int N = 256;
    char buff[N];
    int i = 0;
    char ch = in.get();              // 读一个字符
    while (ch != ' ' && ch != '\n')  // 读到空格或换行停止
    {
        buff[i++] = ch;
        if (i == N - 1)              // 缓冲区满了
        {
            buff[i] = '\0';
            s += buff;               // 追加到字符串
            i = 0;
        }
        ch = in.get();
    }
    if (i > 0)                       // 剩余字符
    {
        buff[i] = '\0';
        s += buff;
    }
    return in;
}

输出用范围 for 遍历;输入用缓冲区逐字读取,遇到空格或换行停止,拼成一个单词。


test.cpp

测试函数包括我手写 string 类的功能:构造、迭代器、插入、删除、查找、截取、拷贝、赋值、比较、输入输出、交换。

cpp 复制代码
#include"string.h"

namespace bit
{
	void test_string1()
	{
		string s1;
		string s2("hello world");
		cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;

		for (size_t i = 0; i < s2.size(); i++)
		{
			s2[i] += 2;
		}

		cout << s2.c_str() << endl;

		for (auto e : s2)
		{
			cout << e << " ";
		}
		cout << endl;

		string::iterator it = s2.begin();
		while (it != s2.end())
		{
			//*it += 2;
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_string2()
	{
		string s1("hello world");
		s1 += 'x';
		s1 += '#';
		cout << s1.c_str() << endl;

		s1 += "hello world";
		cout << s1.c_str() << endl;

		
		s1.insert(5, '$');
		cout << s1.c_str()<< endl;

		s1.insert(0, '$');
		cout << s1.c_str() << endl;

		string s2("hello world");
		cout << s2.c_str() << endl;

		s2.insert(5, "$$$");
		cout << s2.c_str() << endl;

		s2.insert(0, "$$$$$$$$$$$$$$$$$$$$$$$$$");
		cout << s2.c_str() << endl;
	}

	void test_string3()
	{
		string s1("helllo world");
		s1.erase(6, 100);
		cout << s1.c_str() << endl;

		string s2("hello world");
		s2.erase(6);
		cout << s2.c_str() << endl;

		string s3("hello world");
		s3.erase(6, 3);
		cout << s3.c_str() << endl;
	}

	//_str() 的作用
	
	//	const char* c_str() const;
	//	特点				说明
	//	返回值		const char* (只读,不能修改)
	//	结尾		保证有 \0
	//	用途		传给 C 函数(printf、strcpy、fopen 等)
	//这个函数测试了:find 查找、substr 截取、
	// 拷贝构造、赋值重载、自赋值。预期输出取决于 find
	// 是正向还是反向查找,用 find('.')
	// 会取到第一个点,用 rfind('.') 才取到最后一个点(后缀)。
	void test_string4()
	{
		string s("test.cpp.zip");
		size_t pos = s.find('.');
		string suffix = s.substr(pos);
		cout << suffix.c_str() << endl;

		string copy(s);
		cout << copy.c_str() << endl;

		s = suffix;
		cout << suffix.c_str() << endl;
		cout << s.c_str() << endl;

		s = s;
		cout << s.c_str() << endl;
	}

	void test_string5()
	{
		string s1("hello world");
		string s2("hello world");

		cout << (s1 < s2) << endl;
		cout << (s1 == s2) << endl;

		cout << ("hello world <s2") << endl;
		cout << (s1 == "hello world") << endl;//隐式转换成 string 对象,然后再比较内容。
		//地址进行比较
		//cout<<("hello world " == "hello world")<<endl;
		cout << s1 << s2 << endl;

		string s0;
		cin >> s0;
		cout << s0 << endl;
	}

	//拷贝构造	string s2 = s1;	string(const string& s)	创建新对象,从已有对象拷贝
	//拷贝赋值	s1 = s3;	string& operator=(const string& s)	对象已存在,把另一个对象赋值给它

	void test_string6()
	{
		string s1("hello world");
		string s2 = s1;
		cout << s1 << endl;
		cout << s2 << endl;

		string s3("$$$$$$$");
		s1 = s3;

		cout << s1 << endl;
		cout << s3 << endl;
	}


	void test_string7()
	{
		string s1("hello world");
		string s2("$$$$$$$$$$$$$");
		std::swap(s1, s2);
		s1.swap(s2);
		cout << s1 << s2 << endl;
	}


}

分块描述:

文件结构

cpp 复制代码
#include "string.h"   // 包含自定义的 string 类

namespace bit         // 和 string.h 同一个命名空间
{
    // 所有测试函数
}

测试手写的 bit::string 类是否正常工作。


2.测试函数模块解析

模块1:test_string1

cpp 复制代码
void test_string1()
{
    string s1;                          // 测试无参构造
    string s2("hello world");           // 测试有参构造
    cout << s1.c_str() << endl;         // 输出空串
    cout << s2.c_str() << endl;         // 输出 hello world
    for (size_t i = 0; i < s2.size(); i++)
    {
        s2[i] += 2;                     // 测试 operator[] 修改
    }
    cout << s2.c_str() << endl;         // 输出 jgnnq yqtnf

    for (auto e : s2)                   // 测试范围 for(需要迭代器)
    {
        cout << e << " ";               // 输出 j g n n q   y q t n f 
    }
    cout << endl;
    string::iterator it = s2.begin();   // 测试迭代器
    while (it != s2.end())
    {
        cout << *it << " ";             // 输出 j g n n q   y q t n f 
        ++it;
    }
    cout << endl;
}

测试内容

构造、c_str()operator[]、范围 for、迭代器


模块2:test_string2 (尾部插入 + 中间插入)

cpp 复制代码
void test_string2()
{
    string s1("hello world");
    s1 += 'x';                          // 测试 operator+=(char)
    s1 += '#';                          // 测试 operator+=(char)
    cout << s1.c_str() << endl;        
    s1 += "hello world";                // 测试 operator+=(const char*)
    cout << s1.c_str() << endl;      
    s1.insert(5, '$');                  // 测试 insert(pos, char)
    cout << s1.c_str() << endl;       

    s1.insert(0, '$');                  // 测试头插
    cout << s1.c_str() << endl;        

    string s2("hello world");
    cout << s2.c_str() << endl;         // hello world

    s2.insert(5, "$$$");                // 测试 insert(pos, str)
    cout << s2.c_str() << endl;         // hello$$$ world

    s2.insert(0, "$$$$$$$$$$$$$$$$$$$$$$$$$");  // 长字符串头插
    cout << s2.c_str() << endl;        

测试内容

operator+=(字符/字符串)

insert(字符/字符串,头插/中间插)


模块3:test_string3 (删除测试)

cpp 复制代码
void test_string3()
{
    string s1("helllo world");
    s1.erase(6, 100);                   // 删除长度超过剩余就截断
    cout << s1.c_str() << endl;         // helllo

    string s2("hello world");
    s2.erase(6);                        // len 默认 npos删到末尾
    cout << s2.c_str() << endl;         // hello 

    string s3("hello world");
    s3.erase(6, 3);                     // 删除中间 3 个字符
    cout << s3.c_str() << endl;         // hello rld
}

测试内容

erase 三种情况:超长度删除、默认长度、中间删除


模块4:test_string4 ( 查找 + 截取 + 拷贝 + 赋值)

cpp 复制代码
void test_string4()
{
    string s("test.cpp.zip");
    size_t pos = s.find('.');           // 正向查找第一个点
    string suffix = s.substr(pos);      // 截取子串
    cout << suffix.c_str() << endl;     // .cpp.zip(第一个点开始)

    string copy(s);                     // 拷贝构造
    cout << copy.c_str() << endl;       // test.cpp.zip

    s = suffix;                         // 赋值
    cout << suffix.c_str() << endl;     // .cpp.zip  suffix 不变
    cout << s.c_str() << endl;          // .cpp.zip  s 变了

    s = s;                              // 自赋值测试
    cout << s.c_str() << endl;          // .cpp.zip
}

测试内容

findsubstr、拷贝构造、赋值、自赋值

注意find('.') 找第一个点,结果是 .cpp.zip;如果想取后缀 .zip,要用 rfind('.')


模块5:test_string5 ( 比较 + 输入输出)

cpp 复制代码
void test_string5()
{
    string s1("hello world");
    string s2("hello world");

    cout << (s1 < s2) << endl;          // 0 (相等不小于)
    cout << (s1 == s2) << endl;         // 1 (相等)

    cout << ("hello world <s2") << endl; // 这是字符串,不是比较
    cout << (s1 == "hello world") << endl; // 隐式转换,1

    cout << s1 << s2 << endl;           // 测试 operator<<

    string s0;
    cin >> s0;                          // 测试 operator>>
    cout << s0 << endl;
}

测试内容

比较运算符 <==

operator<<operator>>

隐式类型转换(const char* 变成 string

注意:cout << ("hello world <s2") 输出的是字符串字面量,不是比较结果


模块6:test_string6 (拷贝构造 和拷贝赋值)

cpp 复制代码
void test_string6()
{
    string s1("hello world");
    string s2 = s1;                     // 拷贝构造(s2 还没创建)
    cout << s1 << endl;                 //hello world
    cout << s2 << endl;                 // hello world

    string s3("$$$$$$$");
    s1 = s3;                            // 拷贝赋值(s1 已存在)
    cout << s1 << endl;                 // $$$$$$$
    cout << s3 << endl;                 // $$$$$$$
}

测试内容

拷贝构造 和 拷贝赋值的区别,深拷贝验证验证修改 s3 不影响 s1。


模块7:test_string7 (交换)

cpp 复制代码
void test_string7()
{
    string s1("hello world");
    string s2("$$$$$$$$$$$$$");
    std::swap(s1, s2);      // 标准库 swap
    s1.swap(s2);            // 成员函数 swap
    cout << s1 << s2 << endl;  //  s1 和 s2 交换
}

测试内容

swap 功能(交换内容,不拷贝)


cpp 复制代码
// 为了和标准库区分,此处使用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;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);

这个 String 类只有构造和析构,没有拷贝构造。当 String s2(s1) 时会浅拷贝,两个对象指向同一块内存,析构时重复释放导致崩溃。需要实现深拷贝拷贝构造来修复。
说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认

的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝

浅拷贝

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

就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

引发的问题:浅拷贝只复制指针,不复制内容,导致两个对象共享同一块内存。结果就是修改一个影响另一个,析构时重复释放导致崩溃。


深拷贝

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

传统版写法的String类

cpp 复制代码
class String
{
public:
    // 构造函数:用 C 字符串构造 String 对象
    String(const char* str = "")
    {
        // 不允许传入空指针
        if (nullptr == str)
        {
            assert(false);  // 触发断言,提示错误
            return;
        }
        // 开空间(长度 + 1 给 '\0')
        _str = new char[strlen(str) + 1];
        // 拷贝内容
        strcpy(_str, str);
    }

    // 拷贝构造:用已有对象构造新对象(深拷贝)
    String(const String& s)
        : _str(new char[strlen(s._str) + 1])  // 初始化列表:直接开空间
    {
        // 拷贝内容
        strcpy(_str, s._str);
    }

    // 赋值重载:将 s 赋值给当前对象(深拷贝)
    String& operator=(const String& s)
    {
        // 防止自己给自己赋值
        if (this != &s)
        {
            // 开新空间(独立于原空间)
            char* pStr = new char[strlen(s._str) + 1];
            // 拷贝内容到新空间
            strcpy(pStr, s._str);
            // 释放当前对象的旧空间
            delete[] _str;
            // 指向新空间
            _str = pStr;
        }
        // 返回自身,支持连续赋值
        return *this;
    }

    // 析构函数:释放堆内存
    ~String()
    {
        if (_str)           // 指针非空才释放
        {
            delete[] _str;  // 释放内存
            _str = nullptr; // 置空,避免野指针
        }
    }

private:
    char* _str;  // 指向堆上的字符串
};

传统赋值采用先开空间再释放的顺序:先 new 出一块新空间并拷贝数据,成功后再 delete 释放旧空间,最后让 _str 指向新空间。这样即使 new 失败抛异常,原对象的数据也不会丢失,保证了强异常安全。


现代版写法的String类

cpp 复制代码
class String
{
public:
    // 构造函数:用 C 字符串构造 String 对象
    String(const char* str = "")
    {
        if (nullptr == str)           // 禁止空指针
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str) + 1];  // 开空间(多1给\0)
        strcpy(_str, str);                 // 拷贝内容
    }
 写法一:
    // 拷贝构造:用已有对象构造新对象
    String(const String& s)
        : _str(nullptr)               // 先置空,避免 swap 时交换随机值
    {
        String strTmp(s._str);        // 用 s 的字符串构造临时对象(深拷贝)
        swap(_str, strTmp._str);      // 交换指针,临时对象负责释放旧空间
    }
 写法二:
    // 赋值(现代写法):传值 + 交换
    String& operator=(String s)       // 传值,传参时已拷贝一份
    {
        swap(_str, s._str);           // 交换指针,s 析构时释放旧空间
        return *this;                 // 返回自身,支持连续赋值
    }
 
    // 析构函数:释放堆内存
    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }

private:
    char* _str;
};

传统写法是自己手动开空间、拷贝内容、释放旧空间,代码冗长且需要手动处理自赋值检查;

现代写法的核心思想是"不自己干活,而是借助临时对象 + swap":拷贝构造时用源对象构造一个临时对象,然后交换指针;赋值时直接传值(传参时自动完成拷贝),再交换指针,让临时对象析构时自动释放旧空间。现代写法更简洁、自动处理自赋值、异常安全。

拷贝构造函数的深拷贝(现代写法):

现代写法拷贝构造的核心是:先将当前对象的 _str 置空(因为正在构造的对象成员变量是随机值),然后拿源对象 s1 的字符串构造一个临时对象 tmp(此时 tmp 拥有深拷贝的数据),接着交换 tmp 和当前对象的 _str 指针,让当前对象拿到拷贝好的数据,而 tmp 指向空。当函数结束,临时对象 tmp 自动析构,由于它指向的是空指针,不会释放任何内存,而 s1 原有的内存依然由 s1 自己管理。整个过程无需手动开空间和拷贝,全部复用构造和析构函数,异常安全且代码简洁

赋值运算符重载函数的深拷贝(现代写法):

现代写法赋值的核心是"传值即拷贝,交换即转移":当执行 s1 = s2 时,参数 String s 不是传引用而是传值,这意味着在传参的同时就已经用 s2 拷贝构造 出了一个独立的对象 s(深拷贝已完成)。接着,交换 s1s_str 指针,让 s1 拿到 s 中刚刚拷贝好的数据,而 s 则指向 s1 原来的旧空间。当函数结束时,局部对象 s 自动析构,顺手帮 s1 释放了那块旧空间。整个过程只需一次交换,无需手动写 new/delete,无需自赋值检查,且天然强异常安全


写时拷贝(了解)

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

**引用计数:**用来记录资源使用者的个数。

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

扩展阅读关于写时拷贝的

C++ STL string的Copy-On-Write技术 | 酷 壳 - CoolShell

C++的std::string的"读时也拷贝"技术! | 酷 壳 - CoolShell


三.扩展阅读

1.C++面试中string类的一种正确写法

https://coolshell.cn/articles/10478.html#google_vignette

2.STL 的string类怎么啦?_stl中的string类程序-CSDN博客

相关推荐
程序员二叉1 小时前
【JUC】AQS底层深度拆解|独占/共享模式|队列原理全详解
java·开发语言·面试·juc
踏着七彩祥云的小丑1 小时前
Go 学习第6天:结构体 + 切片 + range遍历
开发语言·学习·golang·go
读书札记20221 小时前
Qt中windeployqt.exe工具的使用:解决使用CMake创建的项目点击exe文件后系统提示0xc000007b的问题
开发语言·qt
xiaoshuaishuai81 小时前
C# 定制化Markdown编辑器
开发语言·c#·编辑器
DogDaoDao1 小时前
C++核心技术深度剖析:从底层原理到工程实践
开发语言·c++·面试·程序员·指针·虚函数
磊 子2 小时前
C++移动语义和智能指针
java·开发语言·c++
不负岁月无痕2 小时前
C++继承与多态知识点及其高频面试问题
开发语言·c++·面试
June`2 小时前
如何组织一个并行程序
开发语言·cuda
dtq04242 小时前
C语言刷题函数1-判断素数(分支语句,函数两种方法)
c语言·开发语言·学习