当前位置 : 主页 > 编程语言 > c语言 >

string类详解和模拟实现 #C++

来源:互联网 收集:自由互联 发布时间:2023-08-29
定义和结构 对字符串的使用和操作往往非常频繁,在C++中,为了便于对字符串进行管理,引入了string类。string类是basic_string类模板的一个实例:有 typedef basic_stringchar, char_traits, allocat

定义和结构

对字符串的使用和操作往往非常频繁,在C++中,为了便于对字符串进行管理,引入了string类。string类是basic_string类模板的一个实例:有typedef basic_string<char, char_traits, allocator> string。string类的存储结构其实是一个顺序表,与C语言传统字符串的使用相比,用string类对字符串进行管理,往往更加简单、方便、快捷和安全。因为字符串使用频繁,string的出现比STL更早,并在后续发展中对其进行了重写,本文通过模拟实现string类及其部分常用的接口以深入体会和理解string类的使用。

string类详解和模拟实现 #C++_string

class string
{
  private:
  size_t _size;
  size_t _capacity;
  char* _str;
 
	/*…………*/

同时要在类域中声明一个静态成员变量npos作为某些操作的操作范围标识:

static const size_t npos;
/*…………*/
const size_t string::npos = -1;

迭代器

在STL中,容器和算法是分别设计的,而迭代器是二者之间的胶合剂,便于两者之间更好地配合。迭代器提供了一种统一的方式拿到容器的数据,而无需暴露容器的底层实现。从使用上看,任何容器都支持迭代器,并且用法相似。

string维护的是一个连续线性空间,普通指针可以作为string的迭代器而满足所有的必要条件,例如对迭代器的加减操作(operator++operator--)和解引用操作(operator*)等,这些操作普通指针可以天然满足,string支持随机存取,普通指针同样可以做到。

public:
  typedef char* iterator;
  typedef const char* const_iterator;

  ////迭代器
  iterator begin() {
    return _str;
  }

  iterator end() {
    return _str + _size;
  }

  const_iterator cbegin() const {
    return _str;
  }

  const_iterator cend() const {
    return _str + _size;
  }

构造&析构和内存管理

C++标准库中提供了多种string的构造方式,本文实现以字符串构造的方式和拷贝构造。

需要注意的是,string中成员变量_capacity和_size只将有效字符考虑在内,而不包括 '\0',但是为了兼容C语言,依然要为 '\0' 分配空间。

////构造&析构
//注意初始化列表的成员变量初始化顺序要与定义顺序一致
string(const char* str = "")
  :_size(strlen(str)),
_capacity(_size),
_str(new char[_size + 1]) //+ 1是为了给'\0'预留空间
{
  //因为str一定是一个以'\0'结尾的字符串,所以使用strcpy即可
  strcpy(_str, str);
}

//深拷贝
string(const string& s)
{
  _str = new char[s._capacity + 1];
  //使用memcpy,将'\0'考虑在内
  memcpy(_str, s._str, s._size + 1); //_size + 1,为'\0'预留空间
  _size = s._size;
  _capacity = s._capacity;
}

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

在C语言中,'\0'是字符串结束的标志,C语言库中对字符串的各种操作函数,例如strlen、strcpy等也是以 '\0' 作为字符串操作终止标志的,但是在string中,序列中的'\0'不一定标识对象的结束。即在string中,有效字符的范围是以_size进行标识的,而与序列中的'\0'无关,'\0'可能成为string中的有效字符。因为这一点,在实现一些接口时,不能简单地复用C库中的str系列函数,而需使用mem系列函数对内存进行操作,否则就会造成一些问题。

reserve()是一个对string容量进行操作的成员函数,在元素操作时,为了保证内存空间充足,会频繁调用这个接口。reserve的实现有三步:配置新空间、数据移动和释放旧空间。

void reserve(size_t n)
{
  if (n > _capacity)
  {
    char* tmp = new char[n + 1]; //为'\0'多开一个字节空间
    //考虑'\0'在内
    memcpy(tmp, _str, n + 1);
    delete[] _str;
    _str = tmp;
    _capacity = n;
  }
}

元素访问和操作

Access

string可以支持下标操作,这主要得益于对下标访问操作符的重载。下标访问操作符根据对象是否为const对象被设计成两个函数的重载。

为了与C语言更好的兼容,设计一个c_str()接口返回序列的字符串数据部分。

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

const char* c_str() const
{
  return _str;
}

Add

void push_back(char ch)
{
  if (_size == _capacity) {
    //push之前有可能是空串,_capacity为0
    reserve(_capacity == 0 ? 4 : _size * 2); 
  }
  _str[_size++] = ch;
  _str[_size] = '\0'; //末尾补'\0'
}

void append(const char* str)
{
  int len = strlen(str);
  //判断_size + len与_capacity的关系
  if (_size + len > _capacity) {
    reserve(_size + len);
  }
  strcpy(_str + _size, str);
  _size += len;
}

string& insert(size_t pos, size_t n, char ch)
{
  assert(pos < _size); //检查pos的合法性
  //检查容量
  if (_size + n > _capacity) {
    reserve(_size + n);
  }
  //挪动数据
  size_t end = _size;
  while (end >= pos && end != npos)
  {
    _str[end + n] = _str[end];
    --end;
  }
	//插入数据
  for (int i = 0; i < n; ++i) {
    _str[pos + i] = ch;
  }
  _size += n; //注意不要忘记调整_size

  return *this;
}

string& insert(size_t pos, const char* str)
{
  assert(pos < _size);
  //检查容量
  int len = strlen(str);
  if (_size + len > _capacity) {
    reserve(_size + len);
  }
	//挪动数据
  size_t end = _size;
  //由于end在与pos比较时会被提升至size_t类型,所以要额外判断end与npos的关系
  while (end >= pos && end != npos)
  {
    _str[end + len] = _str[end];
    --end;
  }
	//插入数据
  for (int i = 0; i < len; ++i) {
    _str[pos + i] = str[i];
  }
	//调整_size
  _size += len;

  return *this;
}

在进行push操作时,往往更加习惯使用+=操作符,故而对+=运算符进行重载以简化push的使用:

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

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

Delete

iterator erase(size_t pos = 0, size_t len = npos)
{
  assert(pos < _size);
  //从pos位置清理至最后
  if (len == npos || pos + len >= _size) {
    _str[pos] = '\0';
  }
  else
  {
    //挪动数据,包括'\0'
    size_t end = pos + len;
    while (end <= _size) {
      _str[pos++] = _str[end++];
    }
  }
  //注意不能直接_size -= len,len 可能是npos
  _size = pos;
  return _str + pos;
}

void clear()
{
  if (_size != 0) {
    erase();
  }
}

Search

size_t find(char ch, size_t pos = 0)
{
  assert(pos < _size);
  //遍历搜索指定字符
  for (int i = pos; i < _size; ++i)
  {
    if (_str[i] == ch) {
      return i;
    }
  }
  return npos;
}

size_t find(const char* str, size_t pos = 0)
{
  assert(pos < _size);
  char* ret = strstr(_str, str);
  if (ret) {
    return ret - _str;
  }
  else {
    return npos;
  }
}

在上述第二个接口寻找子串时,由于使用的是strstr,所以当在_str中遇到'\0'时就停止搜索了,由上文可知,这对于string中含多个'\0'的情况可能会造成一些问题。为了避免问题,可以手动实现一个定制的strstr。关于字符串匹配,请参考BP算法和KMP算法。

截取和判断

string substr(size_t pos = 0, size_t len = npos)
{
  assert(pos < _size);
  string tmp;
  //从pos位置截取到最后
  if (len == npos || len + pos > _size)
  {
    for (int i = pos; i < _size; ++i) {
      tmp += _str[i];
    }
  }
  else
  {
    for (int i = pos; i < pos + len; ++i) {
      tmp += _str[i];
    }
  }
  return tmp;
}

void resize(size_t n, char ch = '\0')
{
  if(n <= _size) {
    _str[_size] = '\0';
  }
  //n > _size,判断并调整容量后添加字符
  else
  {
    reserve(n);
    for (int i = _size; i < n; ++i) {
      _str[i] = ch;
    }
    _str[n] = '\0';
  }
  _size = n;
}

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

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

运算符重载

赋值运算符重载

string的赋值运算符重载有两种写法,一种是传统的深拷贝写法,分为开新空间、移动数据两个步骤;另一种写法将这两个步骤委托给拷贝构造进行,可以简化重载函数的书写。

//传统写法
string& operator=(const string& s)
{
  reserve(s._capacity);
  memcpy(_str, s._str, s._size + 1);
  _size = s._size;
  return *this;
}

//现代写法的参数是一个临时的类对象,该类对象是右值对象调用拷贝构造所得
//将tmp临时对象与this对象交换,可以达到赋值的目的
//tmp的生命周期在本函数域内,会自动调用析构释放空间
string& operator=(string tmp)
{
  swap(tmp);
  return *this;
}

类似的,拷贝构造也有一种现代写法,利用构造函数来简化拷贝构造的书写:

//现代写法
//写初始化列表,防止因this对象的随机内容在swap时产生问题
string(const string& s)
  :_size(s._size),
_capacity(s._capacity),
_str(nullptr)
{
  string tmp(s._str);
  swap(tmp);
}

现代写法的拷贝构造利用构造函数拷贝了已有对象的内容,并将其交还给this对象以达到拷贝构造的目的。这种写法的拷贝构造有一个缺陷,即当已有对象的有效数据部分含'\0'时,tmp只会构造出 '\0' 之前的部分数据。综合来说,不建议用这种方式写拷贝构造函数。

比较和判断

bool operator>(const string& s)
{
  //hello helloxxx  true
  //helloxxx hello  false
  //hello hello  false
  int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
  return ret == 0 ? _size > s._size : ret > 0;
}

bool operator==(const string& s)
{
  return _size == s._size && memcmp(_str, s._str, _size) == 0;
}

bool operator!=(const string& s)
{
  return !(*this == s);
}

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);
}
};

流运算符重载

////输入输出
std::ostream& operator<<(std::ostream& cout, const shr::string& s)
{
	for (int i = 0; i < s.size(); ++i) {
		cout << s[i];
	}
	return cout;
}

std::istream& operator>>(std::istream& cin, shr::string& s)
{
	s.clear(); //clear实现数据覆盖

	//get可以读取单个字符
	char ch = cin.get();
	//清空缓冲区中的前置空格和换行
	while (ch == ' ' || ch == '\n') {
		ch = cin.get();
	}

	//先将输入的字符放入buff中,buff满后再放入对象中
	//减少扩容次数
	char buff[128];
	int i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		buff[i] = '\0';
		if (i == 127)
		{
			s += buff;
			i = 0;
		}
		ch = cin.get();
	}
	if (i != 0) {
		s += buff;
		i = 0;
	}
	return cin;
}

string类的引用计数和写时拷贝

当进行拷贝构造时,总是进行深拷贝,适用于需要对对象进行读写操作的情况。因为存在只对某些对象进行读操作的情况,多余的深拷贝便会占据多余的内存空间,解决办法便是进行浅拷贝。

浅拷贝有两个问题:对同一块内存的重复释放和对同一块内存数据的操作冲突。对于第一个问题,可以使用引用计数解决,对于每一块内存空间,用一个整型记录本块内存空间目前被引用的次数,当进行拷贝构造时,进行浅拷贝,被引用次数加一;对象销毁时,不释放空间,被引用次数减一;当被引用次数减至0时,释放内存空间。对于第二个问题,可以进行写时拷贝,拷贝构造时进行浅拷贝,此时对于只读情况完全适用,当对对象进行写操作时,进行深拷贝,以避免对原内存空间数据的修改。

通过引用计数和写时拷贝,可以尽量节省不必要的内存消耗。对于对对象进行只读操作的情况,无需开辟额外空间,而是在进行写操作时才进行深拷贝申请必要的内存空间。

string类详解和模拟实现 #C++_string_02

上一篇:Linux Ubuntu man文档的图文安装教程
下一篇:没有了
网友评论