C++STL——string类的模拟实现

string类的实现

默认成员函数

构造函数

构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括’\0’)。

public:
      string(const char* str="") 
      {
         _size=strlen(str);
         _capacity=_size;//这里不是_size+1因为'\0'不是有效字符
         _str=new char[_capacity+1];
         strcpy(_str,str);
      }
private:
	char* _str;
	size_t _size;//已经有多少个有效字符
	size_t _capacity;//能存多少个有效字符 [!!!'\0'不是有效字符]
	static size_t npos=-1;
}

拷贝构造函数

在拷贝构造中,存在类的默认拷贝构造函数是浅拷贝的问题。

浅拷贝是按字节序拷贝,因为对于指针类型变量进行拷贝会导致指向同一块空间,会对同一块区域析构两次造成内存问题。

而拷贝构造函数有两种写法。

传统写法

写法一:传统写法

//s2(s1)---->深拷贝--传统写法
string(const string& s)
    :_str(new char[strlen(s._str)+1])
    ,_size(0)
    ,_capacity(0);
    {
        strcpy(_str,s._str);
    	_size = s._size;
        _capacity = s._capacity;
    }
现代写法

写法二:现代写法

可以理解为空手套白狼。

//交换两个对象的数据
void swap(string& s)
{
	//调用全局的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}
//s2(s1)---->深拷贝--现代写法
string(const string& s)
    :_str(nullptr)
    {
        string tmp(s._str);//string tmp对象是在栈上,里面的_str对象是在堆上
        swap(tmp);
    }

赋值运算符重载

与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。

传统写法

写法一:传统写法:

string& operator&=(const string& s)
{
    if(this != &s)
    {
        char* ptr=new char[strlen(s._str)+1])
        if(ptr == nullptr) perror("error");
        delete[] _str;
        _str=ptr;
        strcpy(_str,s._str);
    	_size = s._size;
        _capacity = s._capacity;
    }
    return *this;
}
现代写法

写法二:现代写法1:

//交换两个对象的数据
void swap(string& s)
{
	//调用全局的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}
string& opeartor=(string s)
{
   	swap(s);//交换两个对象
    return *this;
}

但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法:

现代写法2

//交换两个对象的数据
void swap(string& s)
{
	//调用全局的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}
string& operator=(const string& s)
{
    if(this != s){
        string tmp(s);
        swap(s);
    }
    return *this;
}

但实际中很少出现自己给自己赋值的情况,所以采用“现代写法1”就行了。

析构函数

当对象销毁时堆区对应的空间并不会自动销毁,防止内存泄漏,我们需要使用delete手动释放堆区的空间。

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

迭代器相关函数

string类中,迭代器只是字符指针,而到了list中,迭代器就是封装好的iterator类。

begin和end

typedef char* iterator;
typedef const char* const_iterator;        
iterator begin()
{
    return _str;
}

const_iterator begin()const
{
    return _str;
}
typedef char* iterator;
typedef const char* const_iterator;    
iterator end()
{
    return _str+_size;
}
const_iterator end() const
{
    return _str+_size;
}

看到了string类中迭代器的底层实现,再看迭代器遍历string的代码,其实是字符串的指针遍历。

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

而我们常用的范围for实际上就是auto替换成迭代器

string s = "hello";
for( auto&e : s){
    cout<< e <<" ";
}
cout<<endl;

容量和大小相关函数

size和capacity

size用于获得当前字符串的有效长度

size_t size() const
{
    return _size;
}

capacity函数用于获取字符串当前容量

size_t capacity() const
{
    return _capacity;
}

reserve和resize

reserve规则:
 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
 2、当n小于对象当前的capacity时,什么也不做。

//reserve
void reserve(size_t n){
    if (n < _capacity) return;	 //空间足够时不开辟
    char* tmp = new char[n + 1]; //多的1是给'\0'
    if (!tmp) perror("reserve");
    strncpy(tmp,_str,_size+1);
    delete[] _str;
    _str = tmp;
    _capacity = n;
}

Tips:这里的要用strncpy,因为如果字符串中途有\0会导致拷贝提前停止,是个小细节


resize(size_t n)与resize(size_t n,char c)都是将字符串中有效字符个数改变到n个,不同的是当字符增多时

  1. resize(size_t n)是用0来填充多出的元素空间,另一个使用char c
  2. resize 在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小。如果将元素个数减少,底层空间的总大小不变

resize规则:
 1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
 2、当n小于当前的size时,将size缩小到n。

resize的测试代码

void test_string5()
{
	string s;
	//s.reserve(100);//提前开空间
	s.resize(100);//放100个字符
	size_t sz = s.capacity();
	cout << "making s grow:" << endl;
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << "\n";
		}
	}
}
void test_string6()
{
	string s("hello world");
	s.resize(5);//hello
	s.resize(20, 'x');//helloxxxxxxxxxxx
}
// 开空间+初始化,扩展capacity 并且初始化空间。size也要动
void resize(size_t n, char val = '\0')
{
    if (n < _size)
    {
        _size = n;
        _str[_size] = '\0';
    }
    else
    {
        if (n > _capacity)
        {
            reserve(n);
        }

        for (size_t i = _size; i < n; ++i)
        {
            _str[i] = val;
        }
        _str[n] = '\0';
        _size = n;
    }
}

empty

判空函数

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

字符串修改相关函数

insert

实现了insert接口之后插入的函数都可以复用insert

insert是在pos位置之前插入

string& insert(size_t pos,char ch)
{
    assert(pos<=_size);
    if(_size==capacity){
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    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是最后一个字符的下一个位置
    size_t len=strlen(str);
    if(_size+len>_capacity)
    {
        reverse(_size+len);
    }
    char* end = _str + _size;
    while (end >= _str + pos)
    {
        *(end + len) = *end;
        --end;
    }
    strncpy(_str + pos, str, len);
    _size += len;
    return *this;
}

注意:插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的’\0’也插入到字符串中。

push_back

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

复用之后

void push_back(char ch)
{
  	insert(_size,ch);
}

append

复用之前

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

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

复用之后

void append(const char* str)
{
    insert(_size,str);
}

operator+=

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

erase

insert类似,所以一般来说实现了inserterase之后剩下的增加和删除的接口就可以快速复用了。

string& erase(size_t pos,size_t len=npos)
{
    assert(pos<_size);
    if(len>=_size-pos){
        _str[pos]='\0';
        _size=pos;
    }
    else{
        size_t i=pos+len;
        //_str[size]存的实际是'\0'
        while(i<=_size){
            _str[i-len]=_str[i];
            ++i;
        }
        _size-=len;
    }
}

clear

清空但是并不意味着释放空间

//清空字符串
void clear()
{
	_size = 0; //size置空
	_str[_size] = '\0'; //字符串后面放上'\0'
}

swap

若想让编译器优先在全局范围寻找某函数,则需要在该函数前面加上“::”(作用域限定符)。

//交换两个对象的数据
void swap(string& s)
{
	//调用库里的swap
	::swap(_str, s._str); //交换两个对象的C字符串
	::swap(_size, s._size); //交换两个对象的大小
	::swap(_capacity, s._capacity); //交换两个对象的容量
}

字符串访问相关函数

c_str

c_str函数用于获取对象C类型的字符串。

//返回C类型的字符串
const char* c_str()const
{
	return _str;
}

front

char& front() const {
    return _str[0];
}

back

char& back() const {
    assert(_size > 0);
    return _str[_size - 1];
}

operator[]

[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。

//[]运算符重载(可读可写)
char& operator[](size_t i)
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}
//[]运算符重载(只读)
const char& operator[](size_t i)const
{
	assert(i < _size); //检测下标的合法性
	return _str[i]; //返回对应字符
}

find

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

    const char* ret = strstr(_str + pos, str);
    if (ret)
    {
        return ret - _str;
    }
    else
    {
        return npos;
    }
}

rfind

//反向查找第一个匹配的字符串
size_t rfind(const char* str, size_t pos = npos)
{
	string tmp(*this); //拷贝构造对象tmp
	reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
	size_t len = strlen(str); //待查找的字符串的长度
	char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串)
	strcpy(arr, str); //拷贝str给arr
	size_t left = 0, right = len - 1; //设置左右指针
	//逆置字符串arr
	while (left < right)
	{
		::swap(arr[left], arr[right]);
		left++;
		right--;
	}
	if (pos >= _size) //所给pos大于字符串有效长度
	{
		pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
	}
	pos = _size - 1 - pos; //将pos改为镜像对称后的位置
	size_t ret = tmp.find(arr, pos); //复用find函数
	delete[] arr; //销毁arr指向的空间,避免内存泄漏
	if (ret != npos)
		return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
	else
		return npos; //没找到,返回npos
}

关系运算符重载

bool operator<(const string& s)
{
    int ret=strcmp(_str,s._str);
    return ret<0;
}
bool operator==(const string& s){
    int ret=strcmp(_str,s._str);
    return ret==0;
}
bool operator<=(const string& s){
    return *this<s||*this==s;
}
bool operator>(const string& s){
    return !(*this<=s);
}
bool operator>=(const string& s){
    return !(*this<s);
}
bool operator!=(const string& s){
    return !(*this==s);
}

流运算符重载

输出

重载输出:

ostream& operator<<(ostream& out,const string& s)
{
    for(int i=0;i<s.size();i++){
        cout<<s[i];
    }
    return out;
}

输入

重载输入

///写一个重载输入
istream& operator>>(istream& in,const string&s){
    s.clear();//库的string虽然初始化好了重新输入的时候是新的内容。
    char ch;
    ch = in.get();
    while (ch != ' ' && ch != '\n')
    {
        s += ch;
        ch = in.get();
    }
    return in;
}

getline

//getline
istream& getline(istream& in,string&s){
    clear();
    char ch;
    ch = in.get();
    while (ch != '\n')
    {
        s += ch;
        ch = in.get();
    }
    return in;
}

补充

关于string的一些实现方案讨论

c++标准只规定了string要实现的接口功能,具体如何实现,那是各个库的实现人自己决定的

  1. vs系列的编译器,微软工程师实现
    1. 比如VS把长度<=16的字符串并没有放开出来的堆上而是放在数组里
  2. gcc/g++ gnu工程师
  3. clang
int main()
{
    std::string s1("hello");
    YCB::string s2("hello");
    cout<<sizeof(s1)<<endl;
    cout<<sizeof(s2)<<endl;
    
    std::string s1("hello world");
    std::string s4(s3);
    
    printf("%p\n",s1.c_str());//在VS和linux下的头文件
    printf("%p\n",s4.c_str());//
    
    s1[0]='x';
    printf("%p\n",s1.c_str());
    printf("%p\n",s4.c_str());
    
    string s6="hello";//隐式类型转化--expliect
}
  • 关于string的深浅拷贝的解决方案
    • 深拷贝
    • 引用计数浅拷贝+写时拷贝

image-20210924201609674

  • 一般情况下,推荐深拷贝的方案
  • 引用计数的写时拷贝,设计相对复杂并且在特殊情况下存在缺陷。

const使用的小结

  1. 有些在函数内要修改的就不用const 修饰*this

    1. 本身就是修改数据的接口,如:push_back、insert、erase是不能加const
  2. 有些接口同时也要给const对象用就要用const修饰*this

    1. 不修改成员变量的函数,都应该加const
  3. 有些函数要实现const和非const两个版本

    1. 迭代器

    2. operator[]

      1. string s1("hello");
        s1[1];
        s1[1]='x';
        const string s2("world");
        s2[1];//只读
        
  • 总结
    • 只读接口函数+ 加const
    • 只写接口函数+ 不加const
    • 可读可写接口 +加const版本和不加const版本,也就是具体加不加要看函数接口的功能性

简易版string

如果时间紧迫就不必要写一个增删查改完善的string类,就写一个基本的即可,提前把inserterase两个接口写好然后复用

namespace YCB
{
    class string
    {
      	public:
        	///写一个全缺省的
        	string()
                :_str(new char str[1])
             {	_str[0]='\0'; }
        	string(char* str)
                :_str(new char[strlen(str)+1])
             {
                strcpy(_str,str);        
             }
        	string(char* str="")/// ""是'\0'
                :_str(new char[strlen(str)+1])
                {
                    strcpy(_str,str);
                }
        	//s2(s1)---->深拷贝
        	string(const string& s)
            	:_str(new char[strlen(s._str)+1])
            {
                	strcpy(_str,s._str);
            }
        	string& operator=(const string& s)
            {
                if(this!=&s)
                {
                    char* ptr=new char[strlen(s._str)+1])
                	if(ptr==nullptr)
                    {
                    	perror("operator=");
                    }
 				   delete[] _str;
                	_str=nullptr;	
                	_str=ptr;
                    strcpy(_str,s._str);
                }
                return *this;
            }
        	const char* c_str()
            {
                return _str;
            }
        	size_t size()
            {
			 return strlen(_str);
            }
        	char& operator(size_t i)
            {
                return _str[i];
            }
        	~string()
            {
                delete[] _str;
                _str=nullptr;
            }
        private:
        	char* _str;
    };
    void test_string1()
    {
        string s1("hello");
        string s2;
    	for(size_t i=0;i<s1.size();i++)
        {
            s1[i]+=1;
            cout<<s1[i]<" ";
        }
        for(size_t i=0;i<s2.size();i++)//Runtime error
        {
            s2[i]+=1;
            cout<<s2[i]<<" ";
        }
    }
    void test_string_2()
    {
        string s1("hello");
        string s2(s1);
        
        cout<<s1.c_str()<<endl;
        cout<<s2.c_str()<<endl;/
        string s3("world");
        s1=s3;
        cout<<s1.c_str()<<endl;
        cout<<s3.c_str()<<endl;
    }
}

较完整的string

传送

发觉之前的实现中存在一些细节问题,等一段时间后修正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值