【C++】第五章:STL之模拟实现string

C++ string类详解:构造、拷贝、迭代、容量与访问
本文详细介绍了C++ string类的构造函数、深浅拷贝、迭代器使用、容量管理以及元素访问方法,探讨了const修饰的必要性,以及string大小的不确定性。还涉及了浅拷贝、深拷贝的其他实现和一些常见问题解答。

在这里插入图片描述

成员变量

class string 
{
    private:
    char* _str;
    size_t _size;
    size_t _capacity;
    static const size_t npos;
};

const size_t string::npos = -1;// 初始化为-1

四大成员函数

string由于是带指针的类,所以需要考虑深浅拷贝的问题,如果不懂深浅拷贝的可以[点这里]( (7条消息) 两张图带你理解深浅拷贝_zhybiancheng的博客-优快云博客 )。

1.构造函数和析构函数没有什么难点,就只将成员变量初始化和清理掉。只是要记得要在析构的时候将指针指向的空间释放掉。

2.拷贝构造和赋值运算符重载有两点需要注意,

2.1.拷贝构造和赋值运算符重载都需要考虑深浅拷贝的问题。即字符串中的指针需要指向和目标字符串有同样大小和内容的空间,而不是两个指针指向同一块空间。

2.2.拷贝构造和运算符重载都有两种写法:

写法1:比较中规中矩,

拷贝构造:开辟一块和目标空间一样大小的空间并将其中的内容拷贝到字符串指针指向的刚刚开辟的空间中。

运算符重载:先要销毁原有的空间,后面的步走和拷贝构造类似。

写法2:

拷贝构造:利用构造函数创建出和目标字符串相同的字符串,然后使string的指针指向刚刚创建出的空间即可。

运算符重载:利用上面的拷贝构造直接创建出一个和目标字符串相同的字符串,或者也可以把拷贝构造的过程再实现一遍,即使用构造函数创建出一个和目标字符串相同的字符串,最后是string的指针指向该创建出的空间。

注意:上述的指针指向创建出的空间,是将两个字符串对象交换成员变量,而不是直接指向,因为创建出的字符串对象是临时对象,出了函数之后就会直接销毁掉。

public:
    // 4大成员函数

    // 构造函数
    string(const char* str = ""):// str的缺省值为空字符串
    _size(strlen(str)),
    _capacity(_size)
    {
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

    //// 拷贝构造 写法1
    //string(const string& s):
    //	_str(new char[strlen(s._str) + 1])
    //{
    //	strcpy(_str, s._str);
    //	_size = s._size;
    //	_capacity = s._capacity;
    //}

    //// 运算符重载 写法1
    //string& operator=(const string& s)
    //{
    //	if (this != &s)
    //	{
    //		delete[] _str;// 销毁原来的空间
    //		_str = new char[strlen(s._str) + 1];// 创建和目标空间大小一样的空间
    //		strcpy(_str, s._str);// 拷贝
    //		_size = s._size;
    //		_capacity = s._capacity;
    //	}
    //	return *this;
    //}

    // 用于拷贝构造和赋值运算重载中的交换成员变量
    void swap(string& s)
    {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);
    }


    // 拷贝构造 写法2
    string(const string& s):
    _str(nullptr),// 必须要将_str初始化为nullptr
    _size(0),     // 这样tmp和*this交换之后,tmp指向nullptr
    _capacity(0)
    {
        string tmp(s._str);// 用构造函数做一份字符串s的拷贝

        swap(tmp);// 这的swap(tmp)相当于this->swap(tmp)
    }

    // 赋值运算符重载 写法2
    string& operator=(string s)// 注意这里是string s,而不是const string& s
        // 在函数的形参中就已经进行了一次拷贝构造
    {
        swap(s);
        return *this;
    }


    // 析构函数
    ~string()
    {
        delete[] _str;// 释放_str指向的空间
        _size = _capacity;
        _str = nullptr;
    }

迭代器 Iterators

在string中iterators很像指针。

public:
    // 迭代器
    typedef char* iterator;
    typedef const char* const_iterator;// const对象的迭代器
    //迭代器相关

    // begin()
    iterator begin()
    {
        return _str;
    }

    // const对象的begin()
    const_iterator begin() const
    {
        return _str;
    }

    // end()
    iterator end()
    {
        return _str + _size;
    }

    // const对象的end()
    const_iterator end() const
    {
        return _str;
    }

Capacity

// 字符串的capacity相关
// size()
size_t size() const 
{
    return _size;
}

// capacity()
size_t capacity() const 
{
    return _capacity;
}

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

// resize() 为数组开空间,并初始化
void resize(size_t n, char val = '\0')
{
    // n < _size
    if (n < _size)
    {
        _str[n] = '\0';
        _size = n;
    }
    // _size < n <_capacity
    // n > _capacity
    else
    {
        if (n > _capacity)// 判断是否需要扩容
        {
            reserve(n);
        }

        // 将[_size, n]之间都用val填充
        for (size_t i = _size; i < n; i++)
        {
            _str[i] = val;
        }
        _str[n] = '\0';
        _size = n;
    }

}

// reserve() 为数组开空间
void reserve(size_t n)
{
    if (n > _size)
    {
        char* tmp = new char[n + 1];
        strncpy(tmp, _str, _size);
        tmp[_size] = '\0';
        delete[] _str;

        _str = tmp;
        tmp = nullptr;
        _capacity = n;
    }
}
// clear()
void clear()
{
    //delete[] _str;
    //_str = new char[1];
    _size = 0;
    _str[0] = '\0';
}

Element access(改)

// 访问,改
// s[]访问数组(只读)
const char& operator[](size_t i) const// const对象会调用该函数
{
    assert(i < _size);
    return _str[i];
}

// s[]访问数组(可读可改)
char& operator[](size_t i)// 非const对象会调用该函数
{
    assert(i < _size);
    return _str[i];
}

Modifiers(增,删)

// 增

// push_back()
void push_back(char ch) 
{
    if (_size >= _capacity)
    {
        reserve(_capacity * 2 == 0 ? 1 : _capacity * 2);// 判断是否为空字符串
    }
    _str[_size ++] = ch;
    _str[_size] = '\0';
}

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

// append()加字符串
void append(const char* str)
{
    int len = strlen(str) + _size;// 添加字符串的大小+字符串原有的大小
    if (len >= _capacity)
    {
        reserve(len * 2 == 0 ? 1 : len * 2);// 扩容
    }
    strcpy(_str + _size, str);// 将str接在_str的后面
    _size = len;// 更新字符串的长度
}

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

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

// 在pos位置上插入字符c/字符串s,并返回该字符的位置
string& insert(size_t pos, char c)
{
    assert(pos <= _size);
    if (_size >= _capacity)
    {
        reserve(2 * _capacity == 0 ? 4 : _capacity * 2);
    }
    char* end = _str + _size;// 从最后一个'\0'开始往后移动
    while (end >= _str + pos)
    {
        *(end + 1) = *end;
        end--;
    }
    _str[pos] = c;
    _size++;
    return *this;
}

string& insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    int len = strlen(str);// 求出添加字符串的长度
    if (len + _size >= _capacity)
    {
        reserve(len + _size);
    }
    char* end = _str + _size;
    while (end >= _str + pos)
    {
        *(end + len) = *end;
        end--;
    }
    strncpy(_str + pos, str, len);
    _size += len;
    _str[_size] = '\0';
    return *this;
}

//删

// 删除pos位置上的元素,并返回元素的下一个位置
string& erase(size_t pos = 0, size_t len = npos)
{
    assert(pos <= _size);
    if (len + pos >= _size)
    {
        _str[pos] = '\0';
    }
    else
    {
        strcpy(_str + pos, _str + pos + len);// 将后面的字符串用strcpy接到前面
        _size -= len;
        _str[_size] = '\0';
    }
    return *this;
}

String operations(查)

// 返回字符串的首地址
const char* c_str() const 
{
    return _str;
}
// 查

// 返回字符c/子串s在string中的第一个位置
size_t find(char c, size_t pos = 0) const
{
    while (pos < _size)
    {
        if (_str[pos] == c) return pos;
        pos++;
    }
    return -1;
}

size_t find(const char* s, size_t pos = 0) const
{
    // KMP
    char* p = strstr(_str + pos, s);
    if (p != nullptr)
    {
        return p - _str;
    }
    else
    {
        return npos;
    }
}

Compare

// 比较函数
// <
bool operator<(const string& s) const
{
    int ans = strcmp(_str, s._str);
    return ans < 0;
}
// > 
bool operator>(const string& s) const
{
    return !(*this < s) && !(*this == s);
}
// <=
bool operator<=(const string& s) const
{
    return !(*this > s);
}
// >=
bool operator>=(const string& s) const
{
    return !(*this < s);
}
// ==
bool operator==(const string& s) const
{
    int ans = strcmp(_str, s._str);
    return ans == 0;
}
// !=
bool operator!=(const string& s) const
{
    return !(*this == s);
}

IO

// 输入输出可以放在类外实现而不用友元函数在类内实现
// 因为可以使用string的public接口就不用访问string的成员变量了

// 输出
ostream& operator<< (ostream& output, string& s)
{
    for (size_t i = 0; i < s.size(); i++)
    {
        output << s[i];
    }
    return output;
}

// 输入
istream& operator>> (istream& input, string& s)
{
    // 输入之前要将原字符串中的内容清除
    s.clear();

    // 这里不能用cin,因为cin流提取会忽略回车和空格
    // 所以这里使用get()函数,就可以将所有的字符都读入了
    char ch;
    input.get(ch);

    while (ch != ' ' && ch != '\n')// 遇到空格或者回车就停止
    {
        // 这里没有直接操作字符串s,而是使用+=这个运算符,这样就可以不用使用友元函数了
        s += ch;// 复用+=
        input.get(ch);
    }

    return input;
}

// 输入一行
istream& getline(istream& input, string& s)
{
    s.clear();
    char ch;
    input.get(ch);
    while (ch != '\n')// 遇到回车才停止
    {
        s += ch;// 复用+=
        input.get(ch);
    }
    return input;
}

扩展问题

1. 什么样的成员函数需要const修饰?

在谈const修饰什么样的成员函数之前需要搞清楚const修饰成员函数的功能和目的。

const修饰成员函数其实是在修饰成员函数中隐含的*this,这样做是为了保护对象不被修改外界修改对象中的成员变量。

既然const的目的是保护对象,那么就可以知道如果一个成员函数中不需要修改对象的成员变量,就可以使用const修饰来到达保护对象的目的,根据是否需要修改成员变量,可以将成员函数分成以下三大类:

1.所有的对象在成员函数中都不需要修改成员变量,即对象只读,这样的对象需要加上const修饰。如find(), size() ...。所有的对象都可以调用这些函数,并且不会改变对象的成员变量值。

2.所有的对象在成员函数中都需要修改成员变量,即对象可读可写,这样的对象不能加上const修饰。如insert(),erase()...。只有非const对象才可以调用这些函数,因为在函数中会改变对象的成员变量的值。

3.有些对象在函数中需要改变成员变量,有一些对象在函数中不需要改变成员变量,所以就需要写两个版本的函数,一个加上const修饰,一个不加const修饰。如operator[], begin(), end()。const对象和非const对象都可以调用这些函数,但是实现的功能又不同,所以需要写两个版本。

2.string类的大小到底是多少?

C标准委员会只规定了string类的实现接口,而没有规定如何实现,所以每一个公司或者每一个人实现的string类都可能是不同的,所以string类的大小是不确定的,因为类中的成员变量是未知的。

可以验证一下上面这个版本,vs版本(PJ版)和Linux版本(SGI版)中string类的大小
在这里插入图片描述

在这里插入图片描述

3.string类的深拷贝是否有其他的实现方法?

其实还有另一种实现深拷贝的方法叫做:引用计数浅拷贝+写时深拷贝

这是普通的深拷贝:
在这里插入图片描述
这是引用计数浅拷贝+写时深拷贝
在这里插入图片描述

在这里插入图片描述

有的时候创建完string对象后不一定会全部用完,所以暂时不用的对象就先指向原来的空间,当需要对其中一个对象进行改动的时候,为了不影响其他的对象,就需要深拷贝出一块单独的空间,然后修改其中的值。

当创建出的对象没有被用完的情况下,这样的方法效率会高一点,但是如果所以的对象全部都是用完了,那么效率想和原来的直接深拷贝没有区别。

4.单参数的隐式类型转换

通常使用string的时候都是string s = "hello world";但是这并没有调用构造函数,真正的构造函数的写法应该是string s("hello world");。这是因为其中出现了隐式类型的转换

当构造函数中只有一个参数的时候

// 构造函数
string(const char* str = ""):// str的缺省值为空字符串
_size(strlen(str)),
_capacity(_size)
{
    _str = new char[_capacity + 1];
    strcpy(_str, str);
}

如上函数参数只有const char* str的时候,就可以进行隐式类型的转换。当先进行到string s = "hello world";的时候,hello world先是构造一个临时对象tmp("hello world"),然后将这个临时对象在拷贝构造给对象s

在这里插入图片描述

值得一说的是:其实编译器的优化行为,其实在编译器上运行是时候,实际上hello world字符串使直接变成了对象s的。这是因为编译器会优化在同一行上构造函数和拷贝构造函数。不过也可以理解这种优化的行为,因为运行在同一行上,所以肯定是要将字符串转化成string对象的,所以编译器就偷了一个懒,干脆就省去了中间临时对象的创建,这样效率还可以提升不少呢!

但是如果是

string s;
s = "hello world";

还是会创建临时对象的,因为这是s已经是定义好的一个对象了,所以只能将字符串转换成一个string对象,然后进行拷贝构造了。

完整代码

#pragma once
#include <iostream>
#include <cstring>
#include <algorithm>
#include <assert.h>

using std::cout;
using std::endl;
using std::cin;
using std::ostream;
using std::istream;

namespace zhy 
{
	class string 
	{
	public:
		// 迭代器
		typedef char* iterator;
		typedef const char* const_iterator;// const对象的迭代器

	public:
		// 4大成员函数

		// 构造函数
		string(const char* str = ""):// str的缺省值为空字符串
			_size(strlen(str)),
			_capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		//// 拷贝构造 写法1
		//string(const string& s):
		//	_str(new char[strlen(s._str) + 1])
		//{
		//	strcpy(_str, s._str);
		//	_size = s._size;
		//	_capacity = s._capacity;
		//}

		//// 运算符重载 写法1
		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		delete[] _str;// 销毁原来的空间
		//		_str = new char[strlen(s._str) + 1];// 创建和目标空间大小一样的空间
		//		strcpy(_str, s._str);// 拷贝
		//		_size = s._size;
		//		_capacity = s._capacity;
		//	}
		//	return *this;
		//}

		// 用于拷贝构造和赋值运算重载中的交换成员变量
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}


		// 拷贝构造 写法2
		string(const string& s):
			_str(nullptr),// 必须要将_str初始化为nullptr
			_size(0),     // 这样tmp和*this交换之后,tmp指向nullptr
			_capacity(0)
		{
			string tmp(s._str);// 用构造函数做一份字符串s的拷贝

			swap(tmp);// 这的swap(tmp)相当于this->swap(tmp)
		}

		// 赋值运算符重载 写法2
		string& operator=(string s)// 注意这里是string s,而不是const string& s
								   // 在函数的形参中就已经进行了一次拷贝构造
		{
			swap(s);
			return *this;
		}


		// 析构函数
		~string()
		{
			delete[] _str;// 释放_str指向的空间
			_str = nullptr;
		}

		//迭代器相关

		// begin()
		iterator begin()
		{
			return _str;
		}

		// const对象的begin()
		const_iterator begin() const
		{
			return _str;
		}

		// end()
		iterator end()
		{
			return _str + _size;
		}

		// const对象的end()
		const_iterator end() const
		{
			return _str;
		}

		// 访问
		// s[]访问数组(只读)
		const char& operator[](size_t i) const// const对象会调用该函数
		{
			assert(i < _size);
			return _str[i];
		}

		// s[]访问数组(可读可改)
		char& operator[](size_t i)// 非const对象会调用该函数
		{
			assert(i < _size);
			return _str[i];
		}

		// 字符串的capacity相关
		// size()
		size_t size() const 
		{
			return _size;
		}

		// capacity()
		size_t capacity() const 
		{
			return _capacity;
		}

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

		// resize() 为数组开空间,并初始化
		void resize(size_t n, char val = '\0')
		{
			// n < _size
			if (n < _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			// _size < n <_capacity
			// n > _capacity
			else
			{
				if (n > _capacity)// 判断是否需要扩容
				{
					reserve(n);
				}

				// 将[_size, n]之间都用val填充
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = val;
				}
				_str[n] = '\0';
				_size = n;
			}

		}

		// reserve() 为数组开空间
		void reserve(size_t n)
		{
			if (n > _size)
			{
				char* tmp = new char[n + 1];
				strncpy(tmp, _str, _size);
				tmp[_size] = '\0';
				delete[] _str;

				_str = tmp;
				tmp = nullptr;
				_capacity = n;
			}
		}

		// 增加字符或字符串
		// push_back()
		void push_back(char ch) 
		{
			if (_size >= _capacity)
			{
				reserve(_capacity * 2 == 0 ? 1 : _capacity * 2);// 判断是否为空字符串
			}
			_str[_size ++] = ch;
			_str[_size] = '\0';
		}

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

		// append()加字符串
		void append(const char* str)
		{
			int len = strlen(str) + _size;// 添加字符串的大小+字符串原有的大小
			if (len >= _capacity)
			{
				reserve(len * 2 == 0 ? 1 : len * 2);// 扩容
			}
			strcpy(_str + _size, str);// 将str接在_str的后面
			_size = len;// 更新字符串的长度
		}

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

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

		// 返回字符串的首地址
		const char* c_str() const 
		{
			return _str;
		}

		// clear()
		void clear()
		{
			//delete[] _str;
			//_str = new char[1];
			_size = 0;
			_str[0] = '\0';
		}

		// 比较函数
		// <
		bool operator<(const string& s) const
		{
			int ans = strcmp(_str, s._str);
			return ans < 0;
		}
		// > 
		bool operator>(const string& s) const
		{
			return !(*this < s) && !(*this == s);
		}
		// <=
		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}
		// >=
		bool operator>=(const string& s) const
		{
			return !(*this < s);
		}
		// ==
		bool operator==(const string& s) const
		{
			int ans = strcmp(_str, s._str);
			return ans == 0;
		}
		// !=
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}


		// 常用函数
		// 返回字符c/子串s在string中的第一个位置
		size_t find(char c, size_t pos = 0) const
		{
			while (pos < _size)
			{
				if (_str[pos] == c) return pos;
				pos++;
			}
			return -1;
		}

		size_t find(const char* s, size_t pos = 0) const
		{
			// KMP
			char* p = strstr(_str + pos, s);
			if (p != nullptr)
			{
				return p - _str;
			}
			else
			{
				return npos;
			}
		}

		// 在pos位置上插入字符c/字符串s,并返回该字符的位置
		string& insert(size_t pos, char c)
		{
			assert(pos <= _size);
			if (_size >= _capacity)
			{
				reserve(2 * _capacity == 0 ? 4 : _capacity * 2);
			}
			char* end = _str + _size;// 从最后一个'\0'开始往后移动
			while (end >= _str + pos)
			{
				*(end + 1) = *end;
				end--;
			}
			_str[pos] = c;
			_size++;
			return *this;
		}

		string& insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			int len = strlen(str);// 求出添加字符串的长度
			if (len + _size >= _capacity)
			{
				reserve(len + _size);
			}
			char* end = _str + _size;
			while (end >= _str + pos)
			{
				*(end + len) = *end;
				end--;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			_str[_size] = '\0';
			return *this;
		}


		// 删除pos位置上的元素,并返回元素的下一个位置
		string& erase(size_t pos = 0, size_t len = npos)
		{
			assert(pos <= _size);
			if (len + pos >= _size)
			{
				_str[pos] = '\0';
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);// 将后面的字符串用strcpy接到前面
				_size -= len;
				_str[_size] = '\0';
			}
			return *this;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
		static const size_t npos;
	};

	const size_t string::npos = -1;// 初始化为-1

	// 输入输出可以放在类外实现而不用友元函数在类内实现
	// 因为可以使用string的public接口就不用访问string的成员变量了

	// 输出
	ostream& operator<< (ostream& output, string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			output << s[i];
		}
		return output;
	}

	// 输入
	istream& operator>> (istream& input, string& s)
	{
		// 输入之前要将原字符串中的内容清除
		s.clear();

		// 这里不能用cin,因为cin流提取会忽略回车和空格
		// 所以这里使用get()函数,就可以将所有的字符都读入了
		char ch;
		input.get(ch);

		while (ch != ' ' && ch != '\n')// 遇到空格或者回车就停止
		{
			// 这里没有直接操作字符串s,而是使用+=这个运算符,这样就可以不用使用友元函数了
			s += ch;// 复用+=
			input.get(ch);
		}

		return input;
	}

	// 输入一行
	istream& getline(istream& input, string& s)
	{
		s.clear();
		char ch;
		input.get(ch);
		while (ch != '\n')// 遇到回车才停止
		{
			s += ch;// 复用+=
			input.get(ch);
		}
		return input;
	}
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值