手搓string类(上)

简介: 手搓string类

string的成员

class string()
{
 private:
    char *str;
    size_t _size;
    size_t _capacity;
    const static size_t npos=-1;
};

在类和对象中提到过,静态成员变量不能给缺省值,必须要在类外定义。

但是其实有一个特例,那就是针对整形开了一个后门,静态的整形成员变量可以直接在类中定义。

一.构造,拷贝构造,赋值重载和析构

1.构造函数

在类和对象时提到过,如果要显示定义构造函数最好是给全缺省

string(const char *str="")//注意:\0和空指针一样,'\0'是字符常量(char类型),“”和“\0"一样,
{
  _size = strlen(str);//这是成员函数,有this指针
  _capacity = _size;
  _str = new char [_capacity + 1];//多开一个字节给'\0'
  strcpy(_str, str);//可能会传参过来构造
}

这里的_capacity是给有效字符预留的空间,为了给’\0’留位置在开空间的时候要多开一个。

2.拷贝构造

首先是老实人写法,构造一个新的空间,将s._str的值拷贝到新的空间,再将其他的值拷贝

//拷贝构造
string(const string& s)
{
  //先构造一个新空间给_str
  _str = new char[s._capacity + 1];
  //然后将s的其他值赋值给this
  _size = s._size;
  _capacity = s._capacity;
  strcpy(_str, s._str);
}

在下面的写法中,会经常复用C语言中的字符串函数。(不为别的,就是好用)

不是谁都想当老实人,所以又有了一种现代写法(并不是为了提高效率,而是为了让代码更简洁)。

首先构造一个tmp,然后将tmp和s交换

void swap(string&s)
{
  std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型
  std::swap(_size, s._size);
  std::swap(_capacity, s._capacity);
}
string(const string& s)
    :_str(nullptr) //这里必须要给_str初始化,否则拿到一个非法的地址,析构会报错
    ,_size(0)
    ,_capacity(0)
{
  string tmp(s._str);//这里复用构造函数
  swap(tmp);//有隐藏的this指针,一个参数就够了
}

如果不给_str初始化成空指针话,交换以后临时变量tmp就指向了 _str的空间,这可能是一个野指针,临时变量tmp在出这个函数就要被销毁,调用析构的时候delete一个野指针就会产生错误。

可以delete一个空指针,无论是free还是delete当接收的参数是一个空指针时就不做任何处理。

3.swap问题

标准库中的swap函数是一个模板,在交换自定义类型时有一次构造和拷贝构造,代价比较大,所以我们提供一个成员函数会比较好。在成员函数中交换内置类型时就可以使用标准库中的swap函数,**要指定域,因为编译器默认是现在局部找,局部找不到再去全局找,再找不到就报错。**如果去局部找的话,找到的swap函数参数不匹配。

4.赋值重载

复制重载也分为传统写法:

string& operator=(const string& s)
{
  if (this != &s)//要注意不要自己给自己赋值
  {
    //深拷贝
    char* tmp = new char[s._capacity + 1];
    strcpy(tmp, s._str);//为新空间赋值
    _str = tmp;
    _capacity = s._capacity;
    _size = s._size;
    return *this;
  }
}

现代写法,使用传值传参,然后直接使用临时变量交换,这个写法是我比较推荐的(太简洁了)

void swap(string&s)
{
  std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型
  std::swap(_size, s._size);
  std::swap(_capacity, s._capacity);
}
string& operator=(string s)
{
  swap(s);//传值传参,s是一个拷贝,直接使用这个临时变量,反正临时变量出了这个函数就被销毁了
  return *this;
}

s是一个拷贝的临时变量,在销毁的时候会自动调用析构函数清理,不用我们做额外处理,而且直接使用临时变量调用swap还省去了我们创建临时变量。十分简洁,但是可读性不算太好

5.析构函数

直接使用delete销毁空间,再将_size和 _capacity置0就行

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

二.简单接口

1.c_str

这个主要是返回一个C类型的数组指针

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

对于只读的函数接口建议加上const,这样不但普通对象可以使用,const类型的对象也可以使用

2.size(有效字符长度)

size_t size()const
{
  return _size;
}

3.capacity(有效字符容量)

size_t capacity()const
{
  return _capacity;
}

4.operator[]

char& operator[](size_t pos)
{
  //虽然string是自定义类型,但_str是内置类型
  assert(pos < _size);
  return _str[pos];
}
const char& operator[](size_t pos)const//const对象只读
{
  assert(pos < _size);
  return _str[pos];
}

因为const对象只能读不可写,所以这里要重载两个,重载了[]string就可以像访问数组那样访问了

5.迭代器和范围for

强调一下,迭代器虽然行为像指针,但不一定是指针

typedef char* iterator;//这里为了简单,就实现成指针
iterator begin()
{
  return _str;
}
iterator end()
{
  return _str + _size;
}

有了迭代器,就可以使用范围for了,但是范围for只认识beging和end,所以如果要使用范围for,在手搓迭代器的时候就不要乱取名哦。

三.容量

1.reverse

这个是string类中用于扩容的成员函数

void reverse(size_t n)
{
  //只扩容,所以先检查情况
  if (n > _capacity)
  {
    //_str中有数据,不能直接改_str的空间,要先建立临时空间
    char*tmp = new char[n + 1];
    //将_str中的数据拷贝给tmp,再将_str所指的空间释放
    strcpy(tmp, _str);
    delete[]_str;
    _str = tmp;
    _capacity = n;//更新容量
  }
}

reverse是控制容量的成员函数,但是缩容的代价太大了。所以只考虑缩容,其实string类中给的reverse是会缩容的。

2.resize

这个是改变有效字符长度的成员函数

void resize(size_t n,char ch='\0')//改变size可能会改变capacity,默认插入补空间的字符是'\0'
{
  if (n > _capacity)//这里也可以写n>_size
  {
    reverse(n);
    //扩大空间以后,要用字符初始化后续空间
    for (size_t i = _size; i < n; i++)
    {
      _str[i] = ch;
    }
    //这里还要改变size
    _size = n;
    //可能使用者会传其他字符来初始化,前面的循环没有在size位置补'\0'
     _str[_size] = '\0';
  }
  else 
  {
    //如果是缩小的话,就直接在n位置补'\0'
    _str[n] = '\0';
    _size = n;
  }
}

有看到n>_size就扩容的,但在我看来只有大于容量的时候才有必要改变有效容量。

在缩小的时候同样没必要更改容量,直接在n位置插入一个’\0’,就无法访问到n后面的元素了,间接改变了__size

3.clear

这个成员函数是将string变成一个空串,在重载流提取的时候会用到这个函数

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

四.插入

1.push_back

尾插单个字符,插入就要考虑扩容

void push_back(char c)
{
  if (_size == _capacity)//容量不够,要扩容
  {
    //扩容要调用reverse,这里还要检查一下_capacity,第一次可能是0
    int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
    reverse(newCapacity);//这里面更新了_capacity,外面不用再更新
  }
  //扩容完毕以后,就可以开始插入了
  _str[_size] = c;
  _size++;
  _str[_size] = '\0';
}

这样尾插只能插入单个字符,所以string还提供了一个append(追加字符串)

2.append

这是一个在末尾追加字符串的成员函数

string& append(const char* s)
{
  //这里可以使用C标准库中的strcpy,不过还是要考虑扩容的问题,要检查剩下的空间是否足够插入
  int len = strlen(s);
  if (_size + len > _capacity)
  {
    //容量不够插入就要扩容
    reverse(_capacity + len);
  }
  //复用C标准库函数
  strcpy(_str + _size, s);
  _size += len;
  return *this;
}

追加一个字符串开原本空间的两倍可能还不够用,最正确的写法是计算一下字符串的长度用于增加空间

3.operator+=

重载+=是string类一个非常正确的选择,在做oj的时候你将发现,+=比尾插和追加好用太多了。

string& operator+=(char c)
{
  //插入一个字符直接_size位置插入,复用push_back
  push_back(c);
  return *this;
}
//还要重载一个字符串类型,可以复用append
string& operator+=(const char* str)
{
  append(str);
  return *this;
}

先介绍push_back和append不是没有道理的,复用可以减少代码冗余还省事,还不快复用起来

4.insert

在pos位置插入一个字符或者字符串,需要挪动数据。

插入单个字符:

string& insert(size_t pos, char c)
{
  assert(pos <= _size);
    //string没有单独的扩容函数,在每个插入数据的地方都要检查容量
  if (_size == _capacity)
  {
    int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
    reverse(newCapacity);
  }
  //插入字符要挪动字符,这里要小心,在pos位置插入
  size_t end = _size + 1;//指向'\0'的下一个位置
  while (end > pos)
  {
    _str[end] = _str[end - 1];
    --end;
  }
  _str[pos] = c;
  _size += 1;
  return *this;
}

这里有一个问题,要知道end和pos都是size_t类型的数据,如果在写判断条件的时候,写成end>=pos,可能会陷入死循环。

如果pos是0,end永远无法小于零,这就死循环咯。

解决办法有两种,其一是把end写成有符号的int并且在判断的时候强转pos,也就是这样:

int end = _size;
while (end >=(int) pos)
{
  _str[end] = _str[end - 1];
  --end;
}

第二种就是我写的这样,把end放在_size+1的位置,这样在判断的时候可以不用取等号,也就完美避免了所有问题。我比较推荐这种写法,因为end作为下标本身取值范围就应该要大于零。

插入字符串:

string& insert(size_t pos, const char* str)
{
  assert(pos <= _size);
  size_t len = strlen(str);
  if (_size +len > _capacity)
  {
    int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
    reverse(newCapacity);
  }
  //扩容完毕,开始挪动数据,这次要挪动len个位置,要考虑一下长度是否越界
  size_t end = _size + len;
  while (end > pos + len-1)
  {
    _str[end] = _str[end - len];
    end--;
  }
  //位置挪出来以后要插入字符串
  /*for (size_t i = pos; i <= pos + len; i++)
  {
    _str[i] = *str;
    3str++;
  }*/
  //此外,还可以复用C库函数strncpy
  strncpy(_str + pos, str, len);
  _size += len;
  return *this;
}

这里要用strncpy而不能使用strcpy,因为strcpy会将’\0’也拷贝过来,而字符串的结束标志就是以’\0’为准的。

相关文章
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
44 0
java基础(13)String类
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
67 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
52 2
|
3月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
2月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
64 4
|
2月前
|
存储 安全 Java
【一步一步了解Java系列】:认识String类
【一步一步了解Java系列】:认识String类
28 2
|
2月前
|
安全 C语言 C++
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
【C++篇】探寻C++ STL之美:从string类的基础到高级操作的全面解析
46 4
|
2月前
|
存储 编译器 程序员
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
【C++篇】手撕 C++ string 类:从零实现到深入剖析的模拟之路
74 2