【C++】详解string类

简介: 【C++】详解string类

简介

因为世界上有很多种语言,所以在STL库里有一个管理字符的模板类:basic_string模板类,而本篇要讲的string类是basic_string模板类的一个实例化,类型为char

string类的接口设计是比较冗余的,是因为string类有一些历史包袱。在STL出现之前,就已经有string类了。在STL被惠普实验室开源之后,string类既要符合STL的标准又要向前兼容,所以string类的接口比较冗余。

string类的底层是顺序表,它在堆上开辟连续的空间,并将代码段中的常量字符串进行拷贝。通过顺序表实现对字符串的增删查改。可参考C++的动态内存管理一文:

小编会介绍一些常见的接口,解释它们的功能,参数。然后模拟实现一下。为了方便,小编会调用C语言库中的一些字符串操作函数。头文件string.h

模拟实现不是造一个更好的轮子,而是为了帮助我们更好的理解string类。模拟实现的代码是禁不住在各种场景下测试的,一定会出现各种bug。小编会指出一些能想的到的bug。庖丁解牛,恢恢乎游刃有余

C++是一门面向对象的语言,大家不必对每一个接口都有细致的了解。小编写本篇的目的是为了让大家能对string类的主体框架有一定的了解。让大家在查文档时游刃有余。


框架

为了让类名和接口的写法和库里的保持一致,需要封一层命名空间,名字为MyString

数据设计:

顺序表的有效数据:size_t _size

顺序表的空间大小:size_t _capacity

指向顺序表的指针:char* _str

static const size_t npos = -1;

nposstring类中的一个静态成员变量,它表示一个无效的位置或者未找到的位置。在string类中,当查找某个子字符串或字符时,如果未找到,则返回npos

namespace MyString //命名空间
{
 
class string  //string类
{
 
public:
 
 
//接口(方法)
 
 
private:
 
  size_t _size; //数据有效个数
  size_t _capacity;  //空间大小
  char* _str;  //指向的空间 
  static const size_t npos = -1; 
 
};
 
}

构造


功能:构造一个string类对象,并将其初始化

在C++11的标准中,有非常多的构造函数,如下图

模拟实现:

小编先问大家一个问题,当我们用"x\0xxx"字符串构造对象时,有效数据_size应该给成多少呢,1还是5?

可以参考一下标准库里的实现

标准库的逻辑是:在初始化对象时,以第一个"\0'为准。我们就可以调用库中的strlen(),帮我们计算一下要初始化_size的大小。strlen()计算长度时也是以第一个"\0"为准。

后面会有"x\0xxx",但_size是5的情况。因为标准库中的string类的有效数据是以_size为准,而不是以"\0"为准。这是C++的string类和C语言中字符串函数不一样的地方。

缺省构造函数

代码如下

//全缺省构造
string(const char* str = "")  
  :_size(strlen(str))                  
  ,_capacity(_size + 1)          
  ,_str(new char[_capacity])  
{
  strcpy(_str, str);
}

传对象构造函数

代码如下

  
           string(const string& str)
      :_size(str._size)
      , _capacity(str._capacity)    
      , _str(new char[str._capacity])                       
    {
      memcpy(_str, str._str, str._capacity); //这里用strcpy()拷贝会有很坑的bug
    }

为什么小编会说用strcpy()拷贝数据会有一个很坑的bug呢?

因为string()是以"\0"为基准作为结束条件的。而string类的顺序表可能会存放"x\0xxx"这样的字符串,数据可能会拷贝不全。

拷贝构造


代码如下

  //拷贝构造
  string(const string& str) 
  {
    _size = str._size;  //有效数据个数
    _capacity = str._capacity;  //空间
     _str = new char[str._capacity]; //开空间,深拷贝    
    memcpy(_str, str._str, str._capacity);  //这里建议用memcpy()防止中间有"\0"的问题
 
  }

现在式写法,代码如下

void swap(string& s) //交换数据函数
{
  std::swap(_str, s._str);
  std::swap(_size, s._size);
  std::swap(_capacity, s._capacity);
}
string(const string& s) 
  : _str(nullptr) //初始化数据
  ,_size(0)
  ,_capacity(0)
{
  string Tmp(s._str);  //调用构造函数构造一个与s对象一样的临时对象
 
  swap(Tmp);     //调用交换函数交换两个对象
}

传统写法:需要自己开空间,自己拷贝数据

现在写法:把事情交给别的函数或方法去做,具有面向对象思维

析构函数

//析构函数
~string()
{
  delete[] _str;
  _size = _capacity = 0;
}

容量

size()

功能:

返回字符串有效长度

模拟实现:

size_t size() const
{
  return _size;    
}

capacity()

功能:

返回string类的顺序表的空间

模拟实现:

size_t capacity() const
{
  return _capacity;
}

empty()

功能:

判断字符串是否为空

模拟实现:

bool empty() const
{
  if (_size == 0)
    return true;
  else
    return false;
}

clear()

功能:

清空有效字符

模拟实现:

void clear()
{
  _str[0] = '\0';
  _size = 0;
}

_str[0] = '\0';这句代码是为了兼容C语言的一些接口

reserve()

功能:

将容量调整至n
此函数不会影响到字符串
n大于_capacity,编译器会将容量调整n(可能更大,不同版本的STL具体实现不同)
n小于或等于_capacity,编译器可能会缩容,也可能不会,不具有强制性

参数:

无符号整型n,代表要扩容的大小

模拟实现:

void reserve(size_t n = 0)
{
  if (n <= _capacity)
    return;
 
  char* ptr = new char[n + 1]; //开辟新空间, 多开一个是为了存放‘\0’                       
  memcpy(ptr, _str, _size + 1); //把旧空间的值拷贝给新空间  
  delete[] _str; //释放旧空间   
  _str = ptr; //_str指向新空间          
  _capacity = n + 1;                                         
  ptr = nullptr;      
}

resize()

功能:

将字符串长度调整为n个字符的长度
n < _size 时,删除多余字符(删掉_size - n个字符)
n > _size 时,会先扩容,如果指定了c,会将其初始化为字符c,没有指定,会将其初始化为"\0"

参数:

无符号整型n,代表要调整字符的长度
字符型c,代表要初始化的字符

模拟实现:库中给了我们两个接口,但我们只需要实现一个全缺省即可

思路也很简单,只需要处理n的不同情况即可。代码如下,配有详细的注释

void resize(size_t n, char c = '\0')//c为全缺省参数,不指定c时会将新开辟的空间初始化为"\0"
{
  if ( n < 0) //n小于为非法值,直接返回
    return;
 
  if (n > _size) //n大于有效字符时,会扩容
  {  
    reserve(n); //扩容,复用reserve,后面凡是扩容都会复用reserve
 
    for (size_t i = _size; i < _capacity - 1; i++) //对开辟的空间初始化
    {
      _str[i] = c;
    }
 
    _str[_capacity - 1] = '\0';  //给最后一块空间赋值为\0
 
    _size = n; //不要忘了调整有效数据
 
    return;
  }
 
  _str[n] = '\0'; //当n小于或等于0时删数据
  _size = n;
}

遍历

检引用符号"[ ]"的重载

功能:

检索第pos位置的下标

参数:

无符号整型pos,代表下标

返回值:

char类型,返回string类中顺序表的的第pos位置的字符

模拟实现:库中给了两个,一个非成员函数,一个成员函数。代码如下

char& operator[] (size_t pos)
{
  assert(pos >= 0);
  assert(pos <= _size);  //检查下标的合法性
 
  return _str[pos];  //返回顺序表中该下标的值
 
}
const char& operator[] (size_t pos) const
{
  assert(pos >= 0); 
  assert(pos <= _size); 
 
  return _str[pos]; 
}

迭代器

需要把返回值的类型取别名为迭代器的通用类型,代码如下

typedef char* iterator; //正向迭代器,可修改
 
typedef char* reverse_iterator; //反向迭代器,可修改
 
typedef const char* const_iterator; //正向迭代器,不可修改
 
typedef const char* const_reverse_iterator; //反向迭代器,不可修改

因为string类型的底层是基于顺序表实现的,所以string类型的迭代器是指针

begin()

获取有效字符的第一个位置
iterator begin() //获取第一个字符的位置
{
  return _str;
}
const_iterator begin() const
{
  return _str;
  }

end()

获取最后一个有效字符的下一个位置
iterator end() //最后一个字符的下一个位置
{
  return _str + _size; 
}
const_iterator end() const
{
  return _str + _size;
 
  }

rbegin()

获取最后一个有效字符的位置
reverse_iterator rbegin() //获取最后一个字符的位置
{
  return _str + _size - 1;
 
}
const_reverse_iterator rbegin() const
{
  return _str + _size - 1;
 
  }

rend()

获取第一个有效字符的前一个位置
reverse_iterator rend()//获取第一个字符位置的前一个位置
{
  return _str - 1;
}
const_reverse_iterator rend() const
{
  return _str - 1;
 
}

修改

push_back()

功能:

将字符c尾插至string类的字符串后面

参数:

字符型c,代表要尾插的字符

模拟实现:

void push_back(char c)
{
  if ((_size + 1) == _capacity || _size == _capacity) //判断是否要扩容
  {
    reserve(2 * _capacity);
  }
 
  _str[_size] = c;  //尾插
  _str[_size + 1] = '\0'; //插入\0
  ++_size;  //更改有效数组的值
 
}

"+="运算符重载

功能:

尾插一个类的字符串
尾插一个字符串
尾插一个字符

返回值:

string类的引用

这三类运算符重载互相构成函数重载

实现代码如下

string& operator+= (const string& str)         
{
  size_t len = str._size + 1;  
  if ((_size + len) >= _capacity)  //扩容
  {
    reserve(2 * (_capacity + _size + len));
  }
 
  strcpy(_str + _size, str._str);   //拷贝数据
 
  return *this; 
  }

上述代码中拷贝数据用了strcpy(),在一般场景下是没问题的。但禁不住测试,大家能调试出来吗?这个bug前文已经提过很多次了。

string& operator+= (const char* s)
{
  while (*s)
  {
    push_back(*s);
    s++;
 
  }
  return *this;
  }
string& operator+= (char c)
{
  push_back(c);
 
  return *this;
  }

小编这里复用了push_back(),小编懒得再造轮子了(害羞)

c_str

功能:

返回C形式的字符串

模拟实现:

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

find()

功能:

查找字符串序列在string类的顺序表中第一次出现的位置

参数:

string类类型的引用,表示要查找的字符串序列为string类型的顺序表
无符号整型pos,表示从哪一个下标开始找
字符型c,表示要查找的字符
字符型指针s,表示要查找的字符串

返回值:

找到:返回该位置的下标
没找到:返回npos

模拟实现:小编只实现其中的一种

size_t find(const string& str, size_t pos = 0) const
{
  if (pos < 0 || pos > _size) //检查坐标合法性
    return npos;
 
char* ptr = strstr(_str + pos, str._str); //strstr()是一种暴力匹配,
if (ptr == NULL)
{
  return npos;
}
else
{
  return ptr - _str;  
}
}

我们不是为了造更好的轮子,用strtstr()即可。


其他

赋值运算符重载

现在式写法

string& operator=(string tmp)
{
  swap(tmp);
 
    return *this;
}

比较运算符重载

只需实现"<"和"="即可,其他可以复用

"<"运算符重载

bool operator<(const string& s) 
{
  size_t i = 0;
  size_t j = 0;
 
  while (i < _size && j < s._size)//按长度小的字符串比较
  {
    if (_str[i] < s._str[j])
    {
      return true;
    }
    else if (_str[i] > s._str[j])
    {
      return false;
    }
    else
    {
      ++i;
      ++j;
    }
  }
 
  return _size < s._size;//如果相等长度下相等,长度小的字符串就小
}

"="运算符重载

bool operator==(const string& s)
{
  size_t i = 0;
  size_t j = 0;
 
  while (i < _size && j < s._size)
  {
    if (_str[i] < s._str[j] || _str[i] > s._str[j])
    {
      return false;
    }
    else
    {
      ++i;
      ++j;
    }
  }
 
  return _size == s._size ? true : false;
}

复用

  bool operator<=(const string& s)
  {
    return *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); 
  }

本文到这里就结束啦

相关文章
|
2月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
315 5
|
10月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
10月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
141 3
|
6月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
189 0
|
6月前
|
存储 编译器 C语言
关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论
你真的了解string的'\0'么?你知道创建一个string a("abcddddddddddddddddddddddddd", 16);这样的string对象要创建多少个对象么?你知道string与vector进行扩容时进行了怎么的操作么?你知道怎么求Vector 最大 最小值 索引 位置么?
175 0
|
6月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
281 0
|
8月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
324 12
|
9月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
189 16
|
9月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
267 11