C++string的实现

文章目录

  • 前言
  • 一、string类的特点
  • 二、string类的常用接口
    • 2.1 4大默认成员函数
    • 2.2 容量操作:
    • 2.3 string类对象的访问及遍历操作
    • 2.4 string类对象的修改操作
    • 2.5 判断的接口
    • 2.6 string类非成员函数
  • 三、string类的实现
    • 3.1 考虑成员变量该有什么
      • 3.1.1 为什么使用size_t
    • 3.2 4大默认成员函数的实现
      • 3.2.1 几个错误&&深浅拷贝
      • 3.2.2 拷贝构造和赋值现代写法
      • 3.2.3 C++11 移动构造和移动赋值
    • 3.3 容量操作接口实现
    • 3.4 string类对象的访问及遍历实现
      • 3.4.1 关于降低重复代码的方法
    • 3.5 string类对象的修改操作接口实现
      • 3.5.1 insert关于 size_t 转型到 int的小问题
    • 3.6 判断的接口实现
    • 3.7 string类非成员函数实现
      • 3.7.1 operator>>关于 std>>的小问题


前言

在C语言中,关于字符串的函数很杂乱无章,与面向对象语言不太相符。
C++为了更好的管理字符数组,专门写了一个string类,把常用函数都封装在了string类里面。我在这篇博客中将string类模拟实现一下。

一、string类的特点

1.string是表示字符串的字符串类。
2.该类的接口与常规容器的接口基本相同,再添加一些专门用来操作的string的常规操作。
3.string在底层实际上是:basic_string模板类的别名,typedef basic_string string;
4.不能操作多个字节或者变长字符的序列。
5.在使用string类时,必须包含#include头文件以及using namespace std;

二、string类的常用接口

几点说明:
1、有些接口与C++11有关,我都在声明处标明,如不熟悉,读者可自行跳过。跳过C++11的部分不会影响整体功能的实现。
2、有些接口实现有好几种写法,我会注释掉其他的,保留一种写法。
3、有些接口我没有实现,因为库里的string类实在复杂,读者可自行实现。
4、string类不是模板,适合用分离编译。但是我为了书写方便,没有使用。
5,我用的string类名字跟库里一样,需要封装在命名空间里。

2.1 4大默认成员函数

string(const char* str = ""); //构造函数
string(const string& s); //拷贝构造
string(const&& s); //C++11 移动构造
string& operator=(string s); //赋值
string& operator=(string&& s); // C++11 移动赋值
~string(); //析构函数

2.2 容量操作:

const size_t size() const;
const size_t capacity()const;
void reserve(size_t n = 0);
void resize(size_t n, char c = '\0');
void clear();

2.3 string类对象的访问及遍历操作

char& operator[](size_t pos);
const char& operator[](size_t pos) const;
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;

2.4 string类对象的修改操作

void append(const char* str);
void push_back(const char c);
const string operator+(const string& s);
string& operator+=(const string& s);
string& operator+=(const char c);
string& operator+=(const char* str);
void insert(size_t pos, const char c);
void insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len = npos);
size_t find(const char c, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0)const;
const char* c_str()const;

2.5 判断的接口

bool operator==(const string& s);
bool operator!=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator<(const string& s);
bool operator<=(const string& s);

2.6 string类非成员函数

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

注意:
  1.在string尾部追加字符时,push_back / append/ += 三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2.对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。(这一点当string用多了就会发现很重要。)

三、string类的实现

3.1 考虑成员变量该有什么

任何一个类我觉得最先摆在我们面前的一个问题就是:**我的成员变量是哪些?**而不得不说,这是一个不好回答的问题——因为成员变量的确认往往是在成员方法的实现中完成,即,缺少哪个,需要哪个就补充哪个。这就好像冬天到了我们会添加衣服一样。而C++string类的实现有3个成员变量。
_str是个字符指针,指向字符串。_size表示已经存储的有效字符个数。_capacity表示允许存储的最多有效字符数。
为什么我要加上有效两个字呢?因为字符串末尾需要’\0’,而这个字符是无效的。

string
{
public:
// 以下实现的函数除了非成员函数都是在这实现
private:
 char* _str;
 size_t _size;
 size_t _capacity;
}

3.1.1 为什么使用size_t

也许你会对size_t产生疑惑,因为你可能一直在使用(或者是只见过别人使用),但你却没有深究why size_t ?实际上,大多数编译器上size_t只是unsigned int的typedef,深究可以参考why size_t matter

3.2 4大默认成员函数的实现

              //4大默认成员函数
             // : _str(str) 不能这样,因为此时_str指向常量区,不能改
		     //我们要拷贝构造一个出来。
		     //strlen会解引用str,所以不能给nullptr,缺省值给空字符串
		string(const char* str = "")
		{
			_size = _capacity = strlen(str);
			_str = new char[_size + 1];
			strcpy(_str, str);
		}
		string(const string& s)
			:_str(new char[strlen(s._str) + 1])
			,_size(s._size)
			,_capacity(s._capacity)
		{
			strcpy(_str, s._str);
		}
		//返回引用因为出了函数作用域对象还在
		string& operator=(const string& s)
		{
			if (this != &s) //避免自己给自己赋值的浪费
			{
				char* newstr = new char[strlen(s._str) + 1];
				strcpy(newstr, s._str);
				delete _str;
				_str = newstr;

				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

3.2.1 几个错误&&深浅拷贝

这里还是有几点值得注意的。
1、我将默认的构造函数和含参的构造函数合并写,但是缺省值不能给nullptr,因为下面的strlen就解引用了_str,那会造成空指针的解引用。
2、拷贝构造和赋值要自己写。编译器生成的是值拷贝,也叫做浅拷贝。
C++string的实现_第1张图片
C++string的实现_第2张图片
3、按照effectiveC++的条款,写赋值函数要防止自己给自己赋值。

3.2.2 拷贝构造和赋值现代写法

传统的写法就是开空间,拷贝数据,释放旧空间等一系列操作。我们也有新的写法。

     // C++98的swap写法
        /* void swap(string& s){
		//加上类限定符,调用std的swap
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		} */
		//拷贝构造现代写法
		string(const string& s)
			:_str(nullptr) //防止tmp析构随机数组
		{
			string tmp(s._str);
			// swap(tmp);
			swap(*this, tmp);  //C++11因为移动语义 直接使用swap
		}

				 //赋值的第二种写法
    	   /*string& operator=(const string& s){
			    if (this != &s)
		     	{
			     	string tmp(s);
			 		swap(tmp);
				}
				return *this;
			}*/
			
			// 赋值的现代写法,pass-by-value
			string& operator=(string s) {
			if(this != &s)
				swap(s);
			return *this;
		}

3.2.3 C++11 移动构造和移动赋值

			//C++11 移动构造和移动赋值
			//noexcept 表示不会抛异常
		string(string&& s) noexcept
			:_str(nullptr)
		{
			swap(s);
		}
		
		string& operator=(string&& s)noexcept
		{
			if(this != &s)
			swap(s);
			return *this;
		}

实际上我们会发现移动赋值和赋值函数的现代写法几乎一模一样,实际上移动赋值函数不需要写。为什么呢?看看赋值函数的传值方式,如果是一个右值调用operator=,编译器会用移动构造而非拷贝构造去构造s,而这个过程是没有开空间的,然后去执行接下来的代码。也就是说,operator=现代写法的另外一个好处就是将移动赋值也合并了。

3.3 容量操作接口实现

		const size_t size() const{
			return _size;
			}
		const size_t capacity()const{
			return _capacity;
			}
			
			//增容 增大capacity到n
		void reserve(size_t n = 0){
			assert(n >= _capacity);
			char* newstr = new char[n + 1]; //开空间
			strcpy(newstr, _str);  //拷数据
			delete _str;   
			_str = newstr;            
			_capacity = n;
		}
		//将size变成n
		void resize(size_t n, char c = '\0'){
			if (n < _size) {
				_size = n;
				//(*this)[_size] = '\0';
				_str[_size] = '\0';
			}
			else{
				if (n > _capacity)
					reserve(n);
				while (_size < n){
					_str[_size] = c;
					++_size;
				}
				_str[_size] = '\0';
			}
		}
		void clear(){
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}

reserve将容量_capacity开到n,如果n小于等于_capacity,什么也不做。resize将空间_size开到n。n < _size,将string长度删除到n;n > _size ,用所给字符初始化后面多余出来的空间。实际上,reserve就是很多成员函数重复出来的新的函数,增加复用性。clear跟析构函数的实现一样,所以可以简化析构函数。

		~string(){
			clear();
			}

3.4 string类对象的访问及遍历实现

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

   			//迭代器实际上是char*
		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;
		}

string类提供了operator[ ],我们可以用数组的方式遍历字符串。string类也支持迭代器,所以string也支持范围for(范围for底层就是迭代器)。
但是我们发现上面的代码有大量重复,因为一个是const版本,一个是非const版本,需要面对不同的对象。那么有什么办法去重呢?

3.4.1 关于降低重复代码的方法

通常我们的办法是将相同的部分封装成一个私有(private)函数,然后让这两个函数去调用那个私有函数,在后面的代码种你将看到我的这种处理方式。但是这里我提供另外一种方式。这种方式来自EffectiveC++。这里我们不采用封装私有函数的原因是即使这样,也有重复的部分。(即调用那个函数的过程)

char& operator[](size_t pos){
	return const_cast<char&>(static_cast<const string&>(*this)[pos]);
		}

两次转型 用非const的operator[]去调用const的operator[],第一次将*this转型成const型的,防止无穷递归调用。是安全转型,用static_cast。第二次将const operator[]的返回值的const去掉,用const_cast转型。
注意:反过来,用const去调用非const往往不是一个好主意。

3.5 string类对象的修改操作接口实现

		void append(const char* str){
			size_t len = strlen(str);
			if (_capacity < _size + len){
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		 }
		
	void push_back(const char c){
		if (_size == _capacity)	{
		size_t newcapacity = _capacity == 0 ? 8 : _capacity * 2;
			reserve(newcapacity);
		}
			_str[_size] = c;
			++_size;
			_str[_size] = '\0';
		}
		const string operator+(const string& s){
			string tmp(*this);
			tmp.append(s._str);
			return tmp;
			}
		string& operator+=(const string& s){
			(*this) += s._str;
			return *this;
		}
		string& operator+=(const char c){
			push_back(c);
			return *this;
		}
		string& operator+=(const char* str){
			append(str);
			return *this;
		}
		
		string& insert(size_t pos, const char c){
			assert(pos <= _size);
			if (_size == _capacity){
		size_t newcapacity = _capacity == 0 ? 8 : _capacity * 2;
			reserve(newcapacity);
			}
			moveBack(pos, 1); //复用,封装私有函数
			_str[pos] = c;
			++_size;
			return *this;
		}
		string& insert(size_t pos, const char* str){
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_capacity < len + _size){
				reserve(len + _size);
			}
			moveBack(pos, len); //复用,封装私有函数
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

		string& erase(size_t pos, size_t len = npos){
			assert(pos < _size);
			if (len == npos || pos + len > _size){
				_str[pos] = '\0';
				_size = pos;
			}
			else{
				while(pos + len <= _size)
				_str[pos++] = _str[pos + len];
				_size -= len;
			}
			return *this;
		}
		size_t find(const char c, size_t pos = 0) const{
			for (size_t i = pos; i < _size; ++i){
				if (_str[i] == c)
					return i;
			 }
			return npos;
		}
		size_t find(const char* str, size_t pos = 0)const{
			char* ptr = strstr(_str + pos, str);
			if (ptr != nullptr)
				return ptr - _str;
			return npos;
		}
		const char* c_str()const{
			return _str;
		}

//此时新增私有成员
private:
		 //从pos开始的字符串向后平移len个长度
		 // moveBack为了让两个insert复用
		void moveBack(size_t pos, size_t len){
			size_t end = _size + len;
			int size = _size;
			while (size >= static_cast<int>(pos)){
				_str[end--] = _str[size--];
			}
		}
static size_t npos; //在类里面声明,需要在类外部初始化

static对象的初始化:

//在类外部初始化  在类外部初始化 在类外部初始化 重要的事情说三遍
size_t string::npos = -1; //这行代码写在类外部

各种复用,未完成的函数复用已完成的函数,增加复用性。

3.5.1 insert关于 size_t 转型到 int的小问题

insert函数种需要一次转型,否则在0的位置插入会有问题。实际上是moveBack函数的转型。

3.6 判断的接口实现

		bool operator==(const string& s)const{
			return strcmp(_str, s._str) == 0;
		}
		bool operator!=(const string& s)const{
			return !((*this) == s);
		}
		bool operator>(const string& s){
			return strcmp(_str, s._str) > 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);
		}

较简单,不作讲解。这也算是先苦后甜,哈哈。

3.7 string类非成员函数实现

		ostream& operator<<(ostream& out, const string& s){
		for (size_t i = 0; i < s.size(); ++i){
			out << s[i];
		}
		return out;
	}
		istream& operator>>(istream& in, string& s){
		char c;
		while (1){
			c = in.get();
			if (c == ' ' || c == '\n')
				break;
			   s += c;
		   }
	     	return in;
	  }
		istream& getline (istream& is, string& s){
			char c;
			while (1){
			c = in.get();
			if (c == '\n')
				break;
		    	s += c;
		  }
		return in;
	}

返回引用是为了支持连续的输入和输出。比如
cout << s1 << ‘a’ << endl; cout << s1返回一个ostream的引用,继续输出字符’a’,然后是endl。输入也一样。>>的运算符重载遇到空格或者换行符就结束,getline只遇到换行符才结束。

3.7.1 operator>>关于 std>>的小问题

注意到,在operator>>种,我用到了in.get()这个函数,这是为了获得一个来自键盘的字符。为什么不直接用std的>>呢?
因为>>不会接收到空格和换行符,所以无法判断operator>>的结束。使用get函数不会有此问题。
(全文完)

你可能感兴趣的:(笔记,c++,数据结构,string,c++11)