C++学习笔记(十一)——String类的模拟实现(一)

简介: C++学习笔记(十一)——String类的模拟实现

String类各函数接口总览


默认成员函数


构造函数


构造函数设置为缺省函数,若不传入函数,则默认构造为空字符串。字符串的初始大小和容量均设为传入C字符串的长度。(不包含'\0');

string s1("hello world");//构造函数

//构造函数
string(const char* str = "")
{
  _size = strlen(str); //初始时,字符串大小设置为字符串长度
  _capacity = _size; //初始时,字符串容量设置为字符串长度
  _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
  strcpy(_str, str); //将C字符串拷贝到已开好的空间
}

拷贝构造函数


模拟实现拷贝构造之前,我们需要先了解深浅拷贝:

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规.

string s1("hello world"):string   s2(s1); //拷贝构造

下面来看一下浅拷贝这个例子:

string s1("hello world");
string s2(s1);

运行结果:

image.png

报错了?为什么会这样?我们根据上面给的定义简单分析下:

image.png

通过调试也会发现,这里他们共同用一块空间

image.png

因此,为了避免两次释放同一块空间,我们需要进行深拷贝,深拷贝就是重新开出来一块空间.

下面提供两种深拷贝的两种写法:

写法一:传统的写法

传统写法的思想比较简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是相互独立的。

//拷贝构造函
string(const string& str)
    :_str(new char(strlen(str._str)+1))
  ,_size(0)
  ,_capacity(0)
{
  strcpy(_str, str._str);//将str._str拷贝一份到_str
  _size = str._size;//_size赋值
  _capacity = str._capacity;//_capacity赋值
}       

image.png

会发现,深拷贝会重新开辟一个空间出来,这样就不会出现一个空间被释放两次的错误

写法二:现代的写法

image.png

现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。

//现代写法
void swap(string& s)
{
//调用库里的swap
  ::swap(_str, s._str);//交换两个对象的字符串
  ::swap(_size, s._size);//交换两个对象的大小
  ::swap(_capacity, s._capacity);//交换两个对象的容量
}
string(const string& s)
  :_str(nullptr)
  ,_size(0)
  ,_capacity(0)
{
  string tmp(s._str);//调用构造函数
  swap(tmp);//交换这两个对象
}

赋值运算符重载函数


与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。下面也提供深拷贝的两种写法:

string d1;

string d2(2022,2,20);

d1=d2;

写法一:传统写法

赋值运算符重载函数的传统写法与拷贝构造函数的传统写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。

//传统写法
string& operator=(const string& s)
{
  if (this != &s) //防止自己给自己赋值
  {
    delete[] _str; //将原来_str指向的空间释放
    _str = new char[strlen(s._str) + 1]; //重新申请一块刚好可以容纳s._str的空间
    strcpy(_str, s._str);    //将s._str拷贝一份到_str
    _size = s._size;         //_size赋值
    _capacity = s._capacity; //_capacity赋值
  }
  return *this; //返回左值(支持连续赋值)
}

写法二:现代写法

赋值运算符重载函数的现代写法与拷贝构造函数的现代写法也是非常类似,但拷贝构造函数的现代写法是通过代码语句调用构造函数构造出一个对象,然后将该对象与拷贝对象交换;而赋值运算符重载函数的现代写法是通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。

//现代写法
void swap(string& s)
{
//调用库里的swap
  ::swap(_str, s._str);//交换两个对象的字符串
  ::swap(_size, s._size);//交换两个对象的大小
  ::swap(_capacity, s._capacity);//交换两个对象的容量
}
string& operator=(const string& s)
{
  if (this != &s) //防止自己给自己赋值
  {
    string tmp(s); //用s拷贝构造出对象tmp
    swap(tmp); //交换这两个对象
  }
  return *this; //返回左值(支持连续赋值)
}

析构函数


string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。

//析构函数
~string()
{
  delete[] _str;  //释放_str指向的空间
  _str = nullptr; //及时置空,防止非法访问
  _size = 0;      //大小置0
  _capacity = 0;  //容量置0
}

容量和大小相关函数


size和capacity


size函数用于获取字符串当前的有效长度(不包括’\0’)。

//大小
size_t size()const
{
  return _size; //返回字符串当前的有效长度
}

capacity函数用于获取字符串当前的容量。

//容量
size_t capacity()const
{
  return _capacity; //返回字符串当前的容量
}

reserver和resize


reserve和resize这两个函数的执行规则一定要区分清楚。

reserve规则:

 1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。

 2、当n小于对象当前的capacity时,什么也不做。

//改变容量,大小不变
void reserve(size_t n)
{
  if (n > _capacity) //当n大于对象当前容量时才需执行操作
  {
    char* tmp = new char[n + 1]; //多开一个空间用于存放'\0'
    strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')
    delete[] _str; //释放对象原本的空间
    _str = tmp; //将新开辟的空间交给_str
    _capacity = n; //容量跟着改变
  }
}

注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。

image.png

 resize规则:

 1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。

 2、当n小于当前的size时,将size缩小到n。

//改变大小
void resize(size_t n, char ch = '\0')
{
  if (n <= _size) //n小于当前size
  {
    _size = n; //将size调整为n
    _str[_size] = '\0'; //在size个字符后放上'\0'
  }
  else //n大于当前的size
  {
    if (n > _capacity) //判断是否需要扩容
    {
      reserve(n); //扩容
    }
    for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch
    {
      _str[i] = ch;
    }
    _size = n; //size更新
    _str[_size] = '\0'; //字符串后面放上'\0'
  }
}

empty


empty是string的判空函数,我们可以调用strcmp函数来实现,strcmp函数是用于比较两个字符串大小的函数,当两个字符串相等时返回0。

//判空
bool empty()
{
  return strcmp(_str, "") == 0;
}
相关文章
|
9天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
30天前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
24 2
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
2月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
39 0
java基础(13)String类
|
1月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
55 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
2月前
|
安全 Java
String类-知识回顾①
这篇文章回顾了Java中String类的相关知识点,包括`==`操作符和`equals()`方法的区别、String类对象的不可变性及其好处、String常量池的概念,以及String对象的加法操作。文章通过代码示例详细解释了这些概念,并探讨了使用String常量池时的一些行为。
String类-知识回顾①
|
1月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
51 4