从C语言到C++_13(string的模拟实现)深浅拷贝+传统/现代写法(上)

简介: 从C语言到C++_13(string的模拟实现)深浅拷贝+传统/现代写法

前两篇博客已经对string类进行了简单的介绍和应用,大家只要能够正常使用即可。

在面试中,面试官总喜欢让学生自己来模拟实现string类,

最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。

为了更深入学习STL,下面我们就自己来模拟实现一下string的常用接口函数:


1.  string默认成员函数

1.1 构造和析构

我们先试着来实现 string 的构造和析构:

整体框架:

string.h

#pragma once
 
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  class string
  {
  public:
    string(const char* s)
    {
 
    }
 
    ~string() 
    {
 
    }
  private:
    char* _str;
  };
 
  void test_string1()
  {
    string s1("hello world");
  }
}

Test.c:

#include "string.h"
 
int main()
{
  try
  {
    rtx::test_string1();
  }
  catch (const exception& e)
  {
    cout << e.what() << endl;
  }
 
  return 0;
}

这里为了和原有的 string 进行区分,我们搞一个命名空间给它们括起来。

我们的测试就放在简单提到的try catch上,然后该序号就能测试了。

构造函数是这样写吗?这样写的话拷贝构造能直接用默认生成的吗

    string(const char* s)
      : _str(new char[strlen(s) + 1])// 开strlen大小的空间(多开一个放\0)
    {
        strcpy(_str, str);
    }

然后我们先实现析构,用 new[] 对应的 delete[] 来析构:

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

放到上面的框架:编译通过

此时我们改一下测试用例 test_string1,如果我们要用 s1 拷贝构造一下 s2:

详细解析:

说明:上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用 s1 构 造 s2 时,编译器会调用默认的拷贝构造。最终导致的问题是, s1 、 s2 共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃 , 这种拷贝方式,称为浅拷贝

1.2 深浅拷贝介绍

如何解决这样的问题呢?

我们 s2 拷贝构造你 s1,本意并不是想跟你指向一块空间!

我们的本意是想让 s2 有一块自己的空间,并且能使其内容是 s1 里的 hello world

这就是深拷贝。


所以这里就涉及到了深浅拷贝的问题,我们下面就来探讨一下深浅拷贝的问题。

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

深拷贝(开一块一样大的空间,再把数据拷贝下来,指向我自己开的空间) 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

1.3 拷贝构造的实现

我们之前实现日期类的时候,用自动生成的拷贝构造(浅拷贝)是可以的,

所以当时我们不用自己实现拷贝构造,让它默认生成就足够了。

但是像 string 这样的类,它的拷贝构造我们不得不亲自写:

    string(const string& s)
      :_str(new char[s._capacity + 1])
    {
      strcpy(_str, s._str);
    }

这就实现了深拷贝。

1.4 赋值的实现

现在有一个 s3,如果我们想把 s3 赋值给 s1:

  void test_string1()
  {
    string s1("hello world");
    string s2(s1);
 
    string s3("!!!");
    s1 = s3;
  }

如果你不自己实现赋值,就和之前一样,会是浅拷贝,也会造成崩溃。

所以,我们仍然需要自己实现一个 operator= ,首先思路如下:

    string& operator=(const string& s)
    {
      if (this != &s)
      {
        delete[] _str;// 释放原有空间
        _str = new char[s._capacity + 1];// 开辟新的空间
        strcpy(_str, s._str);// 赋值
        _size = s._size;
        _capacity = s._capacity;
      }
      return *this;
    }

根据我们的实现思路,首先释放原有空间,然后开辟新的空间,

最后把 s3 的值赋值给 s1。为了防止自己给自己赋值,我们可以判断一下。

这时我们还要考虑一个难以发现的问题,如果 new 失败了怎么办?

抛异常!失败了没问题,也不会走到 strcpy,但问题是我们已经把原有的空间释放掉了,

走到析构那里二次释放可能会崩,所以我们得解决这个问题。

可以试着把释放原有空间的步骤放到后面:

    string& operator=(const string& s)
    {
      if (this != &s)
      {
        char* tmp = new char[s._capacity + 1];// 开辟新的空间
        strcpy(tmp, s._str);// 赋值到tmp
        delete[] _str;// 释放原有空间
 
        _str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
        _size = s._size;
        _capacity = s._capacity;
      }
      return *this;
    }

这样一来,就算是动态内存开辟失败了,我们也不用担心出问题了。

这是更标准的实现方式,我们先去开辟空间,放到临时变量 tmp 中,tmp 翻车就不会执行下面的代码,tmp 没有翻车,再去释放原有的空间,最后再把 tmp 的值交付给 s1,

这是非常保险的,有效避免了空间没开成还把 s1 空间释放掉的 "偷鸡不成蚀把米" 的事发生。

1.5 写时拷贝(了解)

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1 ,每增加一个对象使用该资源,就给计数增加1 ,当某个对象被销毁时,

先给该计数减 1 ,然后再检查是否需要释放资源,如果计数为 1 ,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其它对象在使用该资源。

写时拷贝技术实际上是运用了一个 “引用计数” 的概念来实现的。在开辟的空间中多维护四个字节来存储引用计数。

有两种方法:

①:多开辟四个字节(pCount)的空间,用来记录有多少个指针指向这片空间。

②:在开辟空间的头部预留四个字节的空间来记录有多少个指针指向这片空间。

 当我们多开辟一份空间时,让引用计数+1,如果有释放空间,那就让计数-1,但是此时不是真正的释放,是假释放,等到引用计数变为 0 时,才会真正的释放空间。如果有修改或写的操作,那么也让原空间的引用计数-1,并且真正开辟新的空间。


写时拷贝涉及多线程等不好的问题,所以了解一下就行。

2. string 的部分函数实现

刚才我们为了方便讲解深浅拷贝的问题,有些地方所以没有写全。

我们知道string有这几个接口函数:

我们实现只是实现常用的,且length和size是一样的,我们现在增加一些成员:

private:
  char*  _str;
  size_t _size;
  size_t _capacity;   // 有效字符的空间数,不算\0

2.1 完整默认成员函数代码:

#pragma once
 
#include<iostream>
#include<string>
#include<assert.h>
using namespace std;
 
namespace rtx
{
  class string
  {
  public:
    string(const char* s)
    {
      _size =strlen(s);// 因为要算多次strlen 效率低 且放在初始化列表关联到声明顺序 所以不用初始化列表
      _capacity = _size;
      _str = new char[_size + 1];// 开_size+1大小的空间(多开一个放\0)
      strcpy(_str, s);
    }
 
    string(const string& s)
      :_str(new char[s._capacity + 1])
      , _size(s._size)
      , _capacity(s._capacity)
    {
      strcpy(_str, s._str);
    }
 
    string& operator=(const string& s)
    {
      if (this != &s)
      {
        char* tmp = new char[s._capacity + 1];// 开辟新的空间
        strcpy(tmp, s._str);// 赋值到tmp
        delete[] _str;// 释放原有空间
 
        _str = tmp;// tmp赋值到想要的地方,出去tmp就销毁了
        _size = s.size();
        _capacity = s._capacity;
      }
      return *this;
    }
 
    ~string() 
    {
      delete[] _str;
      _str = nullptr;
    }
 
  private:
    char* _str;
    size_t _size;
    size_t _capacity;
  };
 
  void test_string1()
  {
    string s1("hello world");
    string s2(s1);
 
    string s3("!!!");
    s1 = s3;
  }
}

2.2 c_str() 的实现

c_str() 返回的是C语言字符串的指针常量,是可读不写的:

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

返回const char*,因为是可读不可写的,所以我们需要用 const 修饰。

c_str 返回的是当前字符串的首字符地址,这里我们直接 return _str 即可实现。

测试一下:

  void test_string1()
  {
    string s1("hello world");
    string s2(s1);
 
    string s3("!!!");
    s1 = s3;
 
    cout << s1.c_str() << endl;
    cout << s2.c_str() << endl;
    cout << s3.c_str() << endl;
  }

2.3 全缺省构造函数的实现

还要考虑不带参的情况,比如下面的 s4:

  void test_string1()
  {
    string s1("hello world");
    string s2(s1);
 
    string s3("!!!");
    s1 = s3;
 
    cout << s1.c_str() << endl;
    cout << s2.c_str() << endl;
    cout << s3.c_str() << endl;
 
    string s4;
  }

无参构造函数:

    string()
      : _str(new char[1])
      , _size(0)
      , _capacity(0) 
    {
      _str[0] = '\0';
    }

一般的类都是提供全缺省的,值得注意的是,这里缺省值给的是 " "

有人看到指针 char* 可能给缺省值一个空指针 nullptr:

string(const char* str = nullptr)

也就相当于直接对这个字符串进行解引用了,这里的字符串又是空,所以会引发空指针问题。

所以我们这里给的是一个空的字符串 " ",常量字符串默认就带有 \0,这样就不会出问题:

    string(const char* s  = "")
    {
      _size =strlen(s);// 因为要算多次strlen 效率低 且放在初始化列表关联到声明顺序 所以不用初始化列表
      _capacity = _size;
      _str = new char[_size + 1];// 开_size+1大小的空间(多开一个放\0)
      strcpy(_str, s);
    }

这样达到的效果和无参构造函数是一样的,且无参编译器不知道调用哪个,

所以我们就需把无参构造函数删了。

2.4 size() 和 operator[] 的实现

size()的实现:

    size_t size() const
    {
      return _size;
    }

size() 只需要返回成员 _size 即可,考虑到不需要修改,我们加上 const。

operator[] 的实现:

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

直接返回字符串对应下标位置的元素,

因为返回的是一个字符,所以我们这里引用返回 char。

我们来测试一下,遍历整个字符串,这样既可以测试到 size() 也可以测试到 operator[] :

  void test_string2() 
  {
    string s1("hello world");
    string s2;
 
    for (size_t i = 0; i < s1.size(); i++) 
    {
      cout << s1[i] << " ";
    }
    cout << endl;
 
    s1[0] = 'x';
    for (size_t i = 0; i < s1.size(); i++)
    {
      cout << s1[i] << " ";
    }
    cout << endl;
  }

普通对象可以调用,但是 const 对象呢?所以我们还要考虑一下 const 对象。

我们写一个 const 对象的重载版本:

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

因为返回的是 pos 位置字符的 const 引用,所以可读但不可写。

从C语言到C++_13(string的模拟实现)深浅拷贝+传统/现代写法(中):https://developer.aliyun.com/article/1513673

目录
相关文章
|
5月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
202 26
|
10月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
6月前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
135 3
|
10月前
|
C++
模拟实现c++中的string
模拟实现c++中的string
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
511 5
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
438 2
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
134 1
|
算法 编译器 C语言
【C语言】C++ 和 C 的优缺点是什么?
C 和 C++ 是两种强大的编程语言,各有其优缺点。C 语言以其高效性、底层控制和简洁性广泛应用于系统编程和嵌入式系统。C++ 在 C 语言的基础上引入了面向对象编程、模板编程和丰富的标准库,使其适合开发大型、复杂的软件系统。 在选择使用 C 还是 C++ 时,开发者需要根据项目的需求、语言的特性以及团队的技术栈来做出决策。无论是 C 语言还是 C++,了解其优缺点和适用场景能够帮助开发者在实际开发中做出更明智的选择,从而更好地应对挑战,实现项目目标。
445 0
|
2月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
308 5
|
6月前
|
存储 编译器 C语言
关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论
你真的了解string的'\0'么?你知道创建一个string a("abcddddddddddddddddddddddddd", 16);这样的string对象要创建多少个对象么?你知道string与vector进行扩容时进行了怎么的操作么?你知道怎么求Vector 最大 最小值 索引 位置么?
161 0