【C++】string的模拟实现

news2025/1/11 8:10:41


目录

一、std::swap和std::string::swap的区别

二、string的默认构造函数

1、构造函数

2、拷贝构造

3、赋值运算符重载

4、析构函数

三、string中的小接口

四、遍历接口的实现

1、对operator[]进行重载

2、迭代器

五、reserve和resize

六、插入删除查找相关接口

1、push_back、append、+=

2、insert和earse

3、find

七、流插入和流提取

八、模拟实现的string整体代码


一、std::swap和std::string::swap的区别

如果用std::swap交换两个string对象,将会发生1次构造和2次赋值,也就是三次深拷贝;而使用std::string::swap仅交换成员,代价较小。

二、string的默认构造函数

1、构造函数

string(const char* s = "")
{
    _size = strlen(s);//_size和_capacity均不包含'\0'
    _capacity = _size;
    _arr = new char[_size + 1];
    memcpy(_arr, s, _size + 1);
}

构造函数用缺省值,能够满足空串的构造。

这里设计_size和_capacity均不包含'\0'。_arr的空间多new一个,用于储存'\0'。

再将形参的内存拷贝至_arr中,即可完成构造。

2、拷贝构造

写法1:老老实实的根据string对象的私有变量进行拷贝构造。

string(const string& s)
{
    _size = s._size;//_size和_capacity均不包含'\0'
    _capacity = s._capacity;
    _arr = new char[_capacity + 1];
    memcpy(_arr, s._arr, _capacity + 1);
}

写法2:通过构造一个临时对象,将这个临时对象的私有变量全部和*this的私有变量交换。

注意拷贝构造需要先将_arr初始化为nullptr,防止后续tmp拿到随机地址。(tmp销毁将调用析构函数,对一块随机地址的空间进行析构程序将会崩溃)

void swap(string& s)
{
    std::swap(_arr, s._arr);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}
string(const string& s)
    :_arr(nullptr)//防止交换后tmp._arr为随机值,析构出错
{
    string tmp(s.c_str());//构造
    swap(tmp);
}

3、赋值运算符重载

写法1:同样的老实人写法。这种写法要防止自己给自己赋值!

string& operator=(const string& s)
{
    if (this != &s)//防止自己给自己赋值
    {
        _size = s._size;
        _capacity = s._capacity;
        char* tmp = new char[_capacity + 1];
        delete[] _arr;
        _arr = tmp;
        memcpy(_arr, s._arr, _capacity + 1);
    }
    return *this;
}

写法2:通过构造临时变量tmp,完成赋值。这种写法无需担心自己给自己赋值的情况,并且_arr无需初始化为nullptr。 

void swap(string& s)
{
    std::swap(_arr, s._arr);
    std::swap(_size, s._size);
    std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
    string tmp(s.c_str());//构造
    swap(tmp);
    return *this;
}

4、析构函数

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

三、string中的小接口

//string的size()接口
size_t size()const//右const修饰*this,这样const和非const对象均可调用
{
    return _size;
}
//string的c_str()接口
const char* c_str()const
{
    return _arr;
}
//string的capacity()接口
size_t capacity()const
{
    return _capacity;
}
//string的clear()接口
void clear()
{
    _arr[0] = '\0';
    _size = 0;
}
//string的判空
bool empty()const
{
    return _size == 0 ? false : true;
}

如果函数形参不发生改变的,无脑加const修饰。

只有指针和引用会有const权限问题。

四、遍历接口的实现

1、对operator[]进行重载

char& operator[](size_t pos)//普通对象,可读可写
{
    assert(pos < _size);
    return _arr[pos];
}
const char& operator[](size_t pos)const//const对象,仅读
{
    assert(pos < _size);
    return _arr[pos];
}

让字符串进行下标式的访问,需要重载两个operator[]函数,正常对象去调可读可写,const对象调用只读。

2、迭代器

typedef char* iterator;
iterator begin()
{
    return _arr;
}
iterator end()//end指向字符串的'\0'
{
    return _arr + _size;
}

string的迭代器是字符指针,写完迭代器就可以用迭代器实现访问、修改了。

范围for的底层也是一个迭代器,但是范围for底层只认begin()和end(),如果和自己实现的迭代器接口名称对不上,那么范围for将无法使用。

五、reserve和resize

//sring的reserve接口, 如果预开空间小于现有空间,将不会改变容量。
void reserve(size_t n = 0)
{
    if (n + 1 > _capacity)
    {
        char* tmp = new char[n + 1];
        memset(tmp, '\0', n + 1);
        memcpy(tmp, _arr, _size);
        delete[] _arr;
        _arr = tmp;
        _capacity = n;
    }
}
//sring的resize接口
void resize(size_t n, char c)
{
    //判断n的大小
    if (n > _capacity)
    {
        reserve(n);
        memset(_arr + _size, c, n - _size);
        _size = n;
    }
    else
    {
        _arr[n] = '\0';
        _size = n;
    }
}

reserve是扩容,可以用于预开空间,防止频繁的空间申请。申请一块n+1大小的空间,将该空间全部初始化'\0',再将_arr中的数据拷贝至tmp中,释放_arr,_arr指向tmp。

在resize中需要考虑_size扩容和缩容的问题。

六、插入删除查找相关接口

1、push_back、append、+=

string& push_back(const char c)
{
    //判断容量
    if (_size == _capacity)
    {
        size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;//防止出现空串的情况
        reserve(newCapacity);
    }
    _arr[_size++] = c;
    return *this;
}
string& append(const char* s)
{
    //判断容量
    size_t len = strlen(s);
    if (_size + len > _capacity)
    {
        reserve(_size + len);
    }
    strcpy(_arr + _size, s);
    _size += len;
    return *this;
}
string& operator+=(const char c)
{
    push_back(c);
    return *this;
}
string& operator+=(const char* s)
{
    append(s);
    return *this;
}

写push_back要考虑到原对象为空串的情况(即_capacity为0)。

+=可以复用push_back和append。

2、insert和earse

string& insert(size_t pos, char c)
{
    assert(pos < _size);
    //判断容量
    if (_size == _capacity)
    {
        reserve(_capacity + 1);
    }
    //挪动数据
    for (size_t i = _size; i > pos; --i)
    {
        _arr[i] = _arr[i - 1];
    }
    _arr[pos] = c;
    ++_size;
    return *this;
}
string& insert(size_t pos, const char* s)
{
    size_t len = strlen(s);
    //判断容量
    if (len + _size > _capacity)
    {
        reserve(len + _size);
    }
    //挪动数据
    for (size_t i = _size + len; i > pos + len - 1; --i)
    {
        _arr[i] = _arr[i - len];
    }
    memcpy(_arr + pos, s, len);
    _size += len;
    return *this;
}
string& earse(size_t pos, size_t len = npos)
{
    assert(pos < _size);
    //先判断删到底的情况
    if (len == npos || pos + len >= _size)
    {
        _arr[pos] = '\0';
        _size = pos;
    }
    else
    {
        memcpy(_arr + pos, _arr + pos + len, _size - pos - len);
        _size -= len;
    }
    return *this;
}

insert接口在挪动数据时,从最后一个元素的后一个(后len个)位置开始覆盖,可以保证不出现size_t 类型越界的情况。

earse接口,需要分类讨论字符串是否删到底。

注意,这个pos是const static成员,C++语法中,只有指针和整型的const static成员是可以在类中进行初始化的。

3、find

size_t find(const char c, size_t pos = 0)const
{
    assert(pos < _size);
    for (size_t i = pos; i < _size; ++i)
    {
        if (_arr[i] == c)
        {
            return i;
        }
    }
    return npos;
}
size_t find(const char* s, size_t pos = 0)const
{
    assert(pos < _size);
    const char* p = strstr(_arr, s);
    if (p != nullptr)
    {
        return _arr - p;
    }
    return npos;
}

从指定位置找字符或字符串,找到了,返回第一个匹配字符/子串的下标。

七、流插入和流提取

//流插入和流提取的重载时为了自定义类型的输入输出
inline ostream& operator<<(ostream& out, const string& s)//这里访问的到私有,所以可以不用写成友元函数
{
    for (size_t i = 0; i < s.size(); ++i)//流插入按照_size打印,c_str找到'\0'结束打印
    {                                    //比如我在字符串中间插入一个'\0',打印结果不一样
        out << s[i];
    }
    return out;
}
inline istream& operator>>(istream& in, string& s)
{
    s.clear();//用之前先清空s
    //in >> c;//流提取不会识别空格和换行
    char c = in.get();
    char buff[128] = { '\0' };//防止频繁扩容
    size_t i = 0;
    while (c != ' ' && c != '\n')
    {
        if (i == 127)
        {
            s += buff;
            i = 0;
        }
        buff[i++] = c;
        c = in.get();
    }
    if (i > 0)
    {
        buff[i] = '\0';
        s += buff;
    }
    return in;
}

因为string提供了访问私有的接口,所以流插入和流提取可以不用重载成string类的友元函数。

对于流提取,如果频繁的尾插,会造成频繁扩容。而且C++的扩容和C语言的扩容不一样,C++使用new不能原地扩容,只能异地扩容,异地扩容就会导致新空间的开辟、数据的拷贝、旧空间释放。为了防止频繁扩容,我们可以创建一个可以存储128字节的数组,在这个数组中操作,这个数组满了就尾插至对象s中。

为什么不能用getline,而是要一个字符一个字符尾插呢?因为流提取遇到空格和'\n'会结束提取,剩余数据暂存缓冲区,如果是getline的话,遇到空格是不会停止读取的。

八、模拟实现的string整体代码

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
using std::cout;
using std::cin;
using std::endl;
using std::ostream;
using std::istream;
namespace jly
{
	class string
	{
	public:
		void swap(string& s)
		{
			std::swap(_arr, s._arr);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		//构造函数
		string(const char* s = "")
		{
			_size = strlen(s);//_size和_capacity均不包含'\0'
			_capacity = _size;
			_arr = new char[_size + 1];
			memcpy(_arr, s, _size + 1);
		}
		//拷贝构造
		//写法1
		//string(const string& s)
		//{
		//	_size = s._size;//_size和_capacity均不包含'\0'
		//	_capacity = s._capacity;
		//	_arr = new char[_capacity + 1];
		//	memcpy(_arr, s._arr, _capacity + 1);
		//}
		//写法2
		string(const string& s)
			:_arr(nullptr)//防止交换后tmp._arr为随机值,析构出错
		{
			string tmp(s.c_str());//构造
			swap(tmp);
		}
		//赋值运算符重载
		//写法1
		//string& operator=(const string& s)
		//{
		//	if (this != &s)//防止自己给自己赋值
		//	{ 
		//		_size = s._size;
		//		_capacity = s._capacity;
		//		char* tmp = new char[_capacity + 1];
		//		delete[] _arr;
		//		_arr = tmp;
		//		memcpy(_arr, s._arr, _capacity + 1);
		//	}
		//	return *this;
		//}
		//写法2
		string& operator=(const string& s)
		{
			string tmp(s.c_str());//构造
			swap(tmp);
			return *this;
		}
		//析构函数
		~string()
		{
			_size = _capacity = 0;
			delete[] _arr;
			_arr = nullptr;
		}
		//string的size()接口
		size_t size()const//右const修饰*this,这样const和非const对象均可调用
		{
			return _size;
		}
		//string的c_str()接口
		const char* c_str()const
		{
			return _arr;
		}
		//string的capacity()接口
		size_t capacity()const
		{
			return _capacity;
		}
		//string的clear()接口
		void clear()
		{
			_arr[0] = '\0';
			_size = 0;
		}
		//string的判空
		bool empty()const
		{
			return _size == 0 ? false : true;
		}
		//对operator[]进行重载
		char& operator[](size_t pos)//普通对象,可读可写
		{
			assert(pos < _size);
			return _arr[pos];
		}
		const char& operator[](size_t pos)const//const对象,仅读
		{
			assert(pos < _size);
			return _arr[pos];
		}
		//迭代器
		typedef char* iterator;
		iterator begin()const
		{
			return _arr;
		}
		iterator end()const//end指向字符串的'\0'
		{
			return _arr + _size ;
		}
		//string的reserve接口,如果预开空间小于现有空间,将不会改变容量。
		void reserve(size_t n=0)
		{
			if (n + 1 > _capacity)
			{
				char* tmp = new char[n + 1];
				memset(tmp, '\0', n + 1);
				memcpy(tmp, _arr, _size);
				delete[] _arr;
				_arr = tmp;
				_capacity = n;
			}
		}
		//string的resize接口
		void resize(size_t n, char c='\0')
		{
			//判断n的大小
			if (n > _capacity)
			{
				reserve(n);
				memset(_arr + _size,c,n-_size);
				_size = n;
			}
			else
			{
				_arr[n] = '\0';
				_size = n;
			}
		}
		//插入删除查找相关接口
		string& push_back(const char c)
		{
			//判断容量
			if (_size == _capacity)
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;//防止出现空串的情况
				reserve(newCapacity);
			}
			_arr[_size++] = c;
			return *this;
		}
		string& append(const char* s)
		{
			//判断容量
			size_t len = strlen(s);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_arr+_size,s);
			_size += len;
			return *this;
		}
		string& operator+=(const char c)
		{
			push_back(c);
			return *this;
		}
		string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}
		string& insert(size_t pos, char c)
		{
			assert(pos < _size);
			//判断容量
			if (_size == _capacity)
			{
				reserve(_capacity + 1);
			}
			//挪动数据
			for (size_t i = _size; i > pos; --i)
			{
				_arr[i] = _arr[i - 1];
			}
			_arr[pos] = c;
			++_size;
			return *this;
		}
		string& insert(size_t pos, const char* s)
		{
			size_t len = strlen(s);
			//判断容量
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}
			//挪动数据
			for (size_t i = _size + len; i > pos + len - 1; --i)
			{
				_arr[i] = _arr[i - len];
			}
			memcpy(_arr + pos, s, len);
			_size += len;
			return *this;
		}
		string& earse(size_t pos, size_t len = npos)
		{
			assert(pos<_size);
			//先判断删到底的情况
			if (len == npos || pos + len >= _size)
			{
				_arr[pos] = '\0';
				_size = pos;
			}
			else
			{
				memcpy(_arr + pos, _arr + pos + len,_size-pos-len);
				_size -= len;
			}
			return *this;
		}
		size_t find(const char c, size_t pos = 0)const
		{
			assert(pos < _size);
			for (size_t i = pos; i < _size; ++i)
			{
				if (_arr[i] == c)
				{
					return i;
				}
			}
			return npos;
		}
		size_t find(const char* s, size_t pos = 0)const
		{
			assert(pos < _size);
			const char* p = strstr(_arr, s);
			if (p != nullptr)
			{
				return _arr - p;
			}
			return npos;
		}
	private:
		char* _arr;
		size_t _size;
		size_t _capacity;
		const static size_t npos = -1;//只有const static整型、指针成员变量可以在类中定义,其他类型不行
	};
	//流插入和流提取的重载时为了自定义类型的输入输出
	inline ostream& operator<<(ostream& out, const string& s)//这里访问得到私有,所以可以不用写成友元函数
	{
		for (size_t i = 0; i < s.size(); ++i)//流插入按照_size打印,c_str找到'\0'结束打印
		{                                    //比如我在字符串中间插入一个'\0',打印结果不一样
			out << s[i];
		}
		return out;
	}
	inline istream& operator>>(istream& in, string& s)
	{
		s.clear();//用之前先清空s
		//in >> c;//流提取不会识别空格和换行
		char c=in.get();
		char buff[128] = { '\0' };//防止频繁扩容
		size_t i = 0;
		while (c != ' ' && c != '\n')
		{
			if (i == 127)
			{
				s += buff;
				i = 0;
			}
			buff[i++] = c;
			c = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
	//测试函数
	void test1()
	{
	
	}
}


《 符合学习规律的超详细linux实战快速入门》

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

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

相关文章

DirtyCow脏牛漏洞复现(CVE-2016-5195)

DirtyCow脏牛漏洞复现 本文以vulnhub靶场中的lampiao为例复现脏牛提权漏洞 扫描c段 nmap -sS -Pn 192.168.1.0/24找到疑似ip 对该ip端口进行扫描&#xff0c;多扫出个1898端口 nmap -A -sV -p- 192.168.1.13访问80端口&#xff0c;没有有用的信息 1898也是个apche的http服务…

立足小餐饮,“新名酒”江小白能走多远?

&#xff08;图片来源于网络&#xff0c;侵删&#xff09; 来源 | 螳螂观察 文 | 叶小安 白酒市场从不缺新故事&#xff0c;但一直缺年轻人喜欢的白酒。 上月底&#xff0c;江小白旗下江记酒庄获重庆市江津区华信集团10亿元战略投资。与此同时&#xff0c;江小白产品理念升…

技术贴 | Rocksdb 中 Memtable 源码解析

一、什么是 Memtable&#xff1f; Memtable 是 Rocksdb 在内存中保存数据的一种数据结构&#xff0c;一个 Memtable 的容量是固定的&#xff0c;在 Memtable 写满后&#xff0c;会转换为 Immutable Memtable&#xff0c;Immutable Memtable 中的数据会 Flush 到 SST File 中。…

编程中老生常谈的【编码规范】你还记得多少?进来回顾一下吧【文末送书】

&#x1f3ac; 博客主页&#xff1a;https://xiaoy.blog.csdn.net &#x1f3a5; 本文由 呆呆敲代码的小Y 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f384; 学习专栏推荐&#xff1a;Unity精品学习专栏 &#x1f332; 游戏制作专栏推荐&#xff1a;游戏制作分享 &…

【genius_platform软件平台开发】第八十一讲:ARM Neon指令集一(ARM NEON Intrinsics, SIMD运算, 优化心得)

1. ARM Neon Intrinsics 编程 1.入门&#xff1a;基本能上手写Intrinsics 1.1 Neon介绍、简明案例与编程惯例 1.2 如何检索Intrinsics 1.3 优化效果案例 1.4 如何在Android应用Neon 2. 进阶&#xff1a;注意细节处理&#xff0c;学习常用算子的实现 2.1 与Neon相关的ARM体系结…

寻 友 软 件

寻友软件项目技术技术功能部署Redis部署RocketMQJWT&#xff08;Json Web Token&#xff09;虹软人脸识别部署MongoDB&#xff08;尽量不用docker部署mongo&#xff09;部署Nginx过滤器及拦截器加缓存编码流程DOC接口文档bug技术 技术 前端&#xff1a; flutterandroid环信S…

分销微信小程序介绍_分销小程序有什么作用呢

不同的微商城系统对于分销功能的支持会有不要的叫法&#xff0c;一般来说主要有两种&#xff0c;一种是基于商品分享的分销方式&#xff0c;通过分享链接识别客户从属关系&#xff0c;订单完成&#xff0c;结算佣金&#xff1b;另一种分销商可以建立并独立运营一个分销店铺&…

【JavaSE】关于多态那些事儿

目录 1. 多态 1.1 多态的概念 1.2 多态实现条件 1.3 向上转型 1.3.1 直接赋值 1.3.2 方法传参 1.3.3 方法返回 1.3.4 向上转型的优缺点 1.4 重写 1.4.1 重写的条件 1.4.2 重写注意事项 1.4.3 重载与重写的区别 1.5 通过父类的引用&#xff0c;调用这个父类和子类重…

CSS篇十六——盒子模型之边框

目录一、CSS盒子模型1.1 盒子模型组成1.2 边框&#xff08;border&#xff09;1.2.1 语法格式1.2.2 边框样式 border-style1.2.3 代码示例1.3 表格的细线边框1.3.1 语法格式、代码示例及结果一、CSS盒子模型 网页布局过程&#xff1a; 1.先准备好相关的网页元素&#xff0c;网…

My sql的深度剖析

一.数据库的创建、删除、使用 数据库的创建&#xff1a;create database 数据库名 数据库的删除&#xff1a;drop database 数据库名&#xff1b; 数据库的使用&#xff1a;use数据名&#xff1b; 所有数据库的查看&#xff1a;show databases; 建立数据时如何指定字符集…

在Java中计算Levenshtein莱文斯坦(相似度)编辑距离

在本教程中&#xff0c;我们将研究 Levenshtein 距离算法&#xff0c;该算法也称为编辑距离算法&#xff0c;用于比较单词的相似性。 什么是列文施泰因距离 Levenshtein距离算法由俄罗斯科学家Vladimir Levenshtein创建。 Levenshtein 距离算法通过计算将一个字符串转换为另…

基于单片机的贪吃蛇设计

1 绪论 1.1 设计目的 在21世纪的今天&#xff0c;人们的生活开始变得更加丰富多彩。在繁忙的工作之余&#xff0c;娱乐成为人们生活不可或缺的一份子&#xff0c;而游戏作为近年来逐渐兴起的一种娱乐方式&#xff0c;已经越来越受到人们的青睐。在工作学习之余&#…

dreamweaver网页设计作业制作 学生NBA篮球网页 WEB静态网页作业模板 大学生校园篮球网页代码 dw个人网页作业成品

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

第五章:双指针与离散化的映射

第五章&#xff1a;双指针、离散化、二进制运算与区间合并一、双指针1、什么是双指针&#xff1f;2、双指针的模板3、双指针例题&#xff08;1&#xff09;思路&#xff1a;&#xff08;2&#xff09;解答&#xff1a;C版&#xff1a;C版&#xff1a;二、离散化1、什么是离散化…

java面试强基(3)

重载和重写的区别? 重载 发生在同一个类中&#xff0c;方法名必须相同&#xff0c;参数类型不同、个数不同、顺序不同&#xff0c;方法返回值和访问修饰符可以不同。 重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 重写 重写发生在运行期&#xff0c;…

go语言基本环境搭建

下载地址 Go官网下载地址&#xff1a;https://studygolang.com/dl 一、下载对应电脑得安装包 二、下载完成点击安装下一步&#xff08;选择目录尽量简单&#xff09; 三、是否安装成功 四、环境变量 GOROOT和GOPATH都是环境变量&#xff0c;其中GOROOT是我们安装go开发包的路…

【计算机毕业设计】Springboot医疗管理系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 医疗服务系统&#xff0c;主要的模块包括查看管理员&#xff1b;首页、个人中心…

tomcat官网下载配置全部详细步骤(包含各种报错解决办法)

前言&#xff1a; 名字由来&#xff1a;翻译是野猫&#xff0c;tomcat的作者初衷是希望这个软件可以自力更生&#xff0c;自给自足。不依赖其他插件&#xff0c;独立达到提供web服务的效果 1.tocat和java的关系&#xff1f; tomcat是用Java语言编写的&#xff0c;需要运行在…

大三,请问现在自学Java还来得及吗?

前言 如果还在为入门Java晚而发愁时间够不够&#xff0c;首先你是准备自学&#xff0c;那么我们可以看看现在网络上一些比较热门的Java全体系的学习需要化多长时间&#xff0c;先拿B站上做的比较好的黑马教程和尚硅谷举例&#xff1a; 2022黑马程序员Java学习路线图​www.bili…

耗时半月,终于把牛客网软件测试面试八股文,整理成了文档资料.....

一、面试基础题 简述测试流程: 1、阅读相关技术文档&#xff08;如产品PRD、UI设计、产品流程图等&#xff09;。 2、参加需求评审会议。 3、根据最终确定的需求文档编写测试计划。 4、编写测试用例&#xff08;等价类划分法、边界值分析法等&#xff09;。 5、用例评审(…