【C++】STL之string类模拟-3

简介: 【C++】STL之string类模拟

6、String Operations —— 字符串操作

然后再来讲讲有关字符串的一些操作

c_str

  • 首先的话就是这个【c_str】,可以看到上面我在测试完一个结果后都会去cout << s << endl;打印一下,如果你就使用了上面这些代码的话,一定是会报错的,因为流插入运算符<<和 string类对象并没有对应的重载函数,这一块我后面在讲流插入的时候会提到,报错的同学可以先用下面这种
const char* c_str()
{
  return _str;
}
  • 这个【c_str】就是string类的对象转换成为字符串,那么我们知道对于字符串而言与流插入<<是有重载的,所以才可以起到一个很好地匹配
cout << s1.c_str() << endl;

image.png

从pos位置开始找指定的字符

  • 这个很简单,就是去遍历一下当前对象中的_str,若是在遍历的过程中发现了字符ch的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回npos这个最大的无符号数
size_t find(char ch, size_t pos) const
{
  assert(pos <= _size);
  for (size_t i = 0; i < _size; i++)
  {
    if (_str[i] == ch)
    {
      return i;
    }
  }
  return npos;
}

从pos位置开始找指定的字符串(找子串)

  • 上面是在找单独的一个字符,现在我们来找找一个字符串,那么string类的对象本身就是一个字符串,这也就演化成了在一个字符串中寻找一个子串,还记得我们在 数据结构 | 串的模式匹配问题 中所讲到的如何在一个主串中寻找子串,那时我们有谈到【暴搜】和【KMP】两种写法
  • 不过在这里呢,我直接使用的是C语言中的库函数 strstr,这个的话我们在 字符串函数与内存函数解读 的时候也有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针。那我们如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可
size_t find(const char* s, size_t pos)  const
{
  assert(pos < _size);
  char* tmp = strstr(_str, s);
  if (tmp)
  {
    // 指针相减即为距离
    return tmp - _str;
  }
  return npos;
}

那我们立马来测试一下

  • 首先是去找一个字符a,我们从第0个位置开始找
size_t pos = s1.find('a', 0);

image.png

  • 再来试试去找一个字符串

image.png

从pos位置开始取len个有效字符(取子串)

上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?

string substr(size_t pos, size_t len = npos)


  • 首先要考虑到的是,如果我们从pos位置开始所要取的子串长度大于剩余的串长,那最多能取到的有效范围也就是从pos位置开始的到末尾的_size结束这段距离,所以当这个所取长度过长的话,我们就要考虑去更新一下取子串长度的有效范围

image.png

  • 可以看到,我以这个n作为可取的子串长度,一开始得让其等于传入进来的len长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len
  • 但是如果呢这个长度超出了有效范围后,我们便要去更新这个n = _size - pos
size_t n = len;
if (len == npos || pos + len > _size)
{
  // 就算要取再大的长度,也只能取到pos - _size的位置
  n = _size - pos;
}
  • 那接下去的话我们就可以去取这个子串了,使用循环的方式从pos位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
  tmp += _str[i];
}
return tmp;
  • 测试一下可以看到,我们从第5个位置开始取,取5个有效字符,最后拿到【world】

image.png

  • 再来试一下这个len很大的情况,此时可以看到我们取到的还是这个【world】

image.png

  • 我们可以通过调试来看看这个n是怎么发生变化的

  • 当然,你也可以不传递,此时这个len取的就是默认的【npos】

7、Non-member function overloads —— 非成员函数重载

最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多

relational operators

这里有很多的关系运算符我们来模拟实现一下

① 小于

bool operator<(const string& s)
  • 首先读者要清楚的是我们在比较两个 string对象 的时候,所使用的规则并不是去比较它们的长度,而是去比较它们的ASCLL码值,这里我首先要介绍的第一种方法就是采取 ==双指针== 的形式去进行一一比较,有点类似于我们所讲过的 数据结构 | 归并排序 的遍历思维
  • 在遍历的过程中,只有当 前一个对象中的字符 出现小于 后一个对象中的字符 时,才返回true;出现大于的情况就返回false;如果是相等情况的话则双指针继续向后进行遍历,直接有一个遍历结束位置跳出循环
size_t i1 = 0;
size_t i2 = 0;
while (i1 < _size && i2 < s._size)
{
  if (_str[i1] < s._str[i2])
  {
    return true;
  }
  else if (_str[i1] > s._str[i2])
  {
    return false;
  }
  else
  {
    i1++;
    i2++;
  }
}
  • 当这个循环跳出的时候,我们可以将比较的情况分为以下三种
  • 第一种是两个对象的长度是相等的,此时双指针都遍历结束,那return false
  • 第二种是后一个遍历结束,但是前一个没有结束,那就是前一个来的大,那return false
  • 第三种则是前一个遍历结束,但是后一个没有结束,那就是后一个来的大,是符合的,所以return true

image.png

  • 那我下面给出两种判断的方式,第一种呢则是去比较两个指针的位置
return i1 < _size && i2 == s._size;
  • 第二种呢就方便一些,直接去比较两个对象中数据个数的大小即可
return _size < s._size;

不过呢,上面这种方法虽然易懂一些,但是并不精炼

  • 下面我再介绍一种方法,可读性不是那么强,考察到了对【三目运算符】的理解
bool operator<(const string& s)
{
  int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
  return ret == 0 ? _size < s._size : ret < 0;
}
  • 还记得我们讲过的 memcmp 吗?这是一个内存比较函数,其是以字节的形式去一个个进行比较,那比较的长度我们可以先以二者中小的那个为准,所以后面的三目运算符起到的就是这个作用
  • 接下去呢,在两个对象相同的部分比较完后,再去比较后面的那些部分呢,所以需要这个ret == 0为前提条件,然后比较的便是二者的_size大小;那如果这个ret != 0的话我们只需要返回小于0的那种情况即可

② 等于

  • 然后再来讲讲operator==,这里我们可以使用到的是【逻辑运算符】先去排除掉一部分的情况,因为若是两个对象的_size都不相同的话,那一定是不会相同的
  • 那么在_size相同的情况下,我们再去使用memcpy()根据字节去一一比价两个对象中_str的内容,只有其返回值为0的时候才表示两个对象完全相同
bool operator==(const string& s)
{
  return _size == s._size && memcmp(_str, s._str, s._size) == 0;
}

那有了上面的【小于】和【等于】之后,下面的我们就可以去做一个复用了,这一块我们在 类的六大天选之子 中讲解日期类的关系运算符重载时有提到过

③ 小于等于

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

② 不等于

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

立马来测试一下吧

image.pngimage.pngimage.pngimage.png

最后的话再来补充两个【流插入】和【流提取】,也是非常地重要

operator<< 流插入

  • 那有认真学习过【类和对象】的话,就可以知道为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到【右元】这个东西,不过呢我们不建议使用这个,会破坏类的封装性
// 流插入
ostream& operator<<(ostream& out, const string& s)
{
  for (size_t i = 0; i < s.size(); i++)
  {
    out << s[i];
  }
  return out;
}
  • 还有一点要提醒的是对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个 ==防拷贝== 的效果,即在后面加上一个= delete
ostream operator<<(ostream& out, const string& s)

image.png💬 好,那到这里的话,我们是时候来讲讲这个cout << s.c_str()cout << s 的区别了

  • c的字符数组, 以\0为终止算长度
  • string不看\0, 以size为终止算长度

image.png

operator>> 流提取【⭐】

接下去再来看看这个【>>流提取】

  • 这里首先要注意的一点就是第二个参数的前面不能和【<<流插入】一样加const,因为我们会去修改这个 s
istream& operator>>(istream& in, string& s)

【第一版本】:无法读取 空格换行符

  • 首先可以来看下代码,我们通过cin >> ch来将缓冲区内的字符放到【ch】中,接着以换行作为结束读取的标志来不断读取下一个字符并拼接到对象 s 中去
istream& operator>>(istream& in, string& s)
{
  char ch;
  in >> ch;
  while (ch != '\n')
  {
    s += ch;
    in >> ch;
  }
  return in; 
}
  • 下面我展示一下这个写法的两种BUG,一个是在读取到\n的时候缓冲区会继续等待字符的输入,而不是结束读取

  • 还有一种则是在读取的过程中如果读到空格了,是不会识别到的,而是会继续读取下一个字符

【第二版本】:使用get()读取到流中流中的空格和换行符

  • 在流提取istream中有一个接口叫做【get】,我们使用它就可以读取到空格和换行符了

image.png代码如下,可先参考

// 流提取
istream& operator>>(istream& in, string& s)
{
  char ch = in.get();
  while (ch != '\n')   // 以换行作为分隔符
  {
    s += ch;
    ch = in.get();
  }
  return in;
}
  • 然后我们通过调试来观察一下就可以发现我们使用ch = in.get()读取到了中间的空格,而且在读取到\n换行符的时候也成功退出了循环

【第三版本】:clear()清理缓冲区内的字符

  • 接下去我们再来看一种现象,就是当我们重复去操作同一个对象的时候,此时可以看到缓冲区内的字符并没有去做一个清除,所以我们后面再去输入的时候就会造成一个追加的现象

  • 还记得我们在上面讲到过的clear()吗,用来清理 string对象 中的数据
void clear()
{
  _str[0] = '\0';
  _size = 0;
}
  • 在加上这个后,可以发现第二次再去输入的时候就不会造成追加的现象了

image.png

  • 我们也可以通过调试来进行观察

【第四版本】:预存数组减少扩容

  • 难道写成上面这样就好了吗,我们知道当这个 string对象 的容量不够的时候可以去做一个扩容,那若是这个对象本身的大小就很大的话随着s += ch就会去产生频繁扩容的现象,这其实是不好的

image.png💬 那有同学说:那我们在读取数据之前就开出一个很大的数组来不就好了,这样肯定能装得下无需扩容了

  • 那我想问:如果这个数据比你开出的数组大小还要再大很多呢?该怎么办?
  • 如果这个对象中只有一个字符,那你开了一个大小为1024B的空间, 剩下的1023B不是造成了很大的浪费吗?

带着上面的这些疑问,我们一起改造一下这个流提取的接口

  • 首先我们确实是要先开出一个数组,数组的大小给128即可
char buf[128];
int i = 0;
  • 然后还是以循环的方式去读取,在循环内部呢把每次读取到的字符放到数组中去,并使用变量i去做一个计数
while (ch != '\n')    // 以换行作为分隔符
{
  buf[i++] = ch;
  // ...
  ch = in.get();
}
  • 在每次将字符存放到数组中后,我们便要去判断一下这个i是否到达了 127,若是的话就不能再继续读取了,而是要把最后的\0给手动加上,那这就算是一个完整的字符串了,追加到 string对象 中的即可,最后的话别忘了把i重置为0,继续下一组数据的读取
if (i == 127)
{
  buf[i] = '\0';
  s += buf;
  i = 0;
}
  • 当跳出循环的时候,我们还要对这个【i】再去判断一下,若是这个i != 0的话,即没有到达127,只能说明这一组数据还无法追加到对象中。那我们还要再去做一个手动追加,防止数据丢失
if (i != 0)
{
  buf[i] = '\0';
  s += buf;
}

整体代码如下:

// 流提取
istream& operator>>(istream& in, string& s)
{
  s.clear();
  char ch = in.get();
  char buf[128];
  int i = 0;
  while (ch != '\n')    // 以换行作为分隔符
  {
    buf[i++] = ch;
    // 不能等到128再去判断,要为最后的\0留空间
    if (i == 127)
    {
      buf[i] = '\0';
      s += buf;
      i = 0;
    }
    ch = in.get();
  }
  // 若是有数据且不到127的话,进行倒入
  if (i != 0)
  {
    buf[i] = '\0';
    s += buf;
  }
  return in;
}

最后我们再来测试一下,发现确实扩容的次数大大减少了

image.png

难道你认为这样就完了吗?不,还有一点我们没考虑到

【第五版本】:清理字符前多余的空格

  • 对比一下我们自己实现的和库里的,就可以发现存在不同之处,库里对于字符串前面的【空格】会去做处理,但是我们在流提取的逻辑中没有考虑到这一点

image.pngimage.png

  • 不仅如何,库里面对于【换行】这一块也会去做处理,但是呢我们实现的一敲下回车Enter就直接结束了

image.pngimage.png

  • 所以我们应该在读取第一个字符的时候先将【空格】或【换行】给清理掉,直接用get函数即可,它可以读取到缓冲区中的所有内容
// 处理前缓冲区前面的空格或者换行
while (ch == ' ' || ch == '\n')
{
  ch = in.get();
}

整体代码如下:

// 流提取
istream& operator>>(istream& in, string& s)
{
  s.clear();
  char ch = in.get();
  // 处理前缓冲区前面的空格或者换行
  while (ch == ' ' || ch == '\n')
  {
    ch = in.get();
  }
  char buf[128];
  int i = 0;
  while (ch != '\n')   // 以换行作为分隔符
  {
    buf[i++] = ch;
    // 不能等到128再去判断,要为最后的\0留空间
    if (i == 127)
    {
      buf[i] = '\0';
      s += buf;
      i = 0;
    }
    ch = in.get();
  }
  // 若是有数据且不到127的话,进行倒入
  if (i != 0)
  {
    buf[i] = '\0';
    s += buf;
  }
  return in;
}

然后再去测试一下上面的两个场景,就发现什么问题了

image.png

四、写时拷贝(了解)

最后我们再来介绍一个东西叫做【写时拷贝】

1、概念理解

  • 前面我们有谈到过什么是 ==深拷贝==,而 ==浅拷贝== 又会引发怎样的问题,这边再来回顾一下
  • 浅拷贝会导致一块空间被析构两次
  • 浅拷贝会导致一个对象修改也引发另一个对象一并修改
  • 此时我们只有使用 深拷贝 才能解决问题,但是你是否有想过深拷贝所带来的代价呢?我们每去创建一个对象就进行一个深拷贝,但是呢在后面这个对象去不会去做任何的修改,那么深拷贝的意义其实没有多大,还多浪费了一块内存空间,虽然这对操作系统来说算不得什么,但若是你在长期运行这个代码所跑起来的程序时,则会造成内存枯竭💀

所以呢有人就提出了这么一个东西,叫做【写时拷贝】,全称叫做【引用计数 · 写时拷贝

  • 看到下面的图示, s2 呢是 s1 的一份临时拷贝,并且在这个地方我们使用的就是浅拷贝,二者指向的是同一块空间,此处我们会引入一个变量作为引用计数,每当构建出来一个对象的时候,计数器 + 1,所以在当 s2 拷贝完后这个计数器即为【2】
  • 那么此时在析构的时候其所采用的机制便是:当一个对象去进行析构的时候,会先去看这个计数器的值是否为【1】,如果>= 1的话,说明这块空间的维护者不止它一个,那么其就不可以去释放掉这块空间,而是将计数器--,那么此时这个计数器就变成了【1】;接下去当另一个对象再去调用析构函数的时候,发现这个计数器的值是为【1】,表示现在只有它在维护这块空间,其便会去释放掉这块空间
  • 那对于上面的这种机制你可以认为是 ==最后一个走的关灯==

image.png


当然除了解决析构两次的问题,面对拷贝修改这一块它也做了一些文章

  • 当我们要对一个对象中的空间做修改的时候,此时再去执行一个 深拷贝 的逻辑,重新开出一块空间来,把原本的数据拷贝过来,让其指向这块新的空间,然后就在这个新的空间中做修改。最后在将这个计数器--
  • 可以看到这个机制就很好地防止了同时修改的问题

image.png💬 那有的同学说:那反正这最后不还是要去做一个深拷贝的,直接深拷贝不就完了,有什么意义呢?

  • 其实你可以认为这是编译器是在做一个【博弈】,因为在不修改的情况下我们所执行的都是 浅拷贝,那么即可能很多对象都在维护同一块空间,此时如果这几个对象都不会去做写操作的话,那其实我们就是赚的,大家都展示同一块空间的内容即可,共同维护同一块空间,无需再多的开销
  • 而只有当我们对这个对象去进行写操作的时候,才去开辟出一块新的空间进行修改,随开随用,此时也不算太晚。==所以只要你浅拷贝了但是不去修改我就是赚的==

💬 其实读者可以这么来理解

  • 如果有读者像博主一样喜欢健身的话,就可以知道一般去健身房都是需要办卡的,只有当办卡的人数到达一定量的时候,老板才是赚的,为什么呢?原因就在于很多人办了健身卡后一般很少会来,甚至是不来,那么这个时候老板一定是赚的,如果每个会员每天都来的话,这健身器材都要不够了😓
  • 那么老板赌的这个【办了卡不来】和我们上面所聊【拷贝了但是不修改】是一个道理的

image.png

2、双平台对比

清楚了什么叫做【写时拷贝】,我们现在就来测试一下

首先我们现到Linux平台下去看看

  1 #include <iostream>
  2 #include <stdio.h>
  3 #include <string.h>
  4 using namespace std;
  5 
  6 int main(void)
  7 {
  8     string s1("abc");
  9     string s2(s1);
 10 
 11     printf("Show copy\n");
 12     printf("%p\n", s1.c_str());
 13     printf("%p\n", s2.c_str());
 14     cout << "-----------------" << endl;
 15 
 16     s2[0] = 'x';
 17     printf("Show modify\n");
 18     printf("%p\n", s1.c_str());
 19     printf("%p\n", s2.c_str());
 20     cout << "-----------------" << endl;                                  
 21 
 22     return 0;
 23 }
  • 可以看到,一开始在拷贝完之后两块空间中的内容都是一致的,说明这是【浅拷贝】,但是呢我在修改了对象 s2 的空间后,再去打印观察的时候就发现其所维护的空间所在地址发生了变化,也就意味着在修改前它做了一个【深拷贝】
  • 所以在Linux下严格执行的就是我们本模块所讲到的 ==写时拷贝==

image.png

  • 不过呢在Windows环境下的VS中,就不是这样了。我们可以看到一开始在打印的时候 对象s1对象s2 所维护的空间是不同的,所以在拷贝的时候就直接去做了一个【深拷贝】,而不是【浅拷贝】
  • 而且在进行 ==写操作== 之后,它们的空间并没有发生改变,还是之前所维护的那一块空间

image.png

那可能有同学就会觉得VS还是比较奇怪的,包括我们在前面对各类接口做对比的时候,VS都会去做一些比较反常的事

VS你可以把他当做是一个财大气粗的老板,下面我们再来谈一谈VS对 string对象 这一块的容量设计

  • 请读者思考一下这个 对象s1 有多大
string s1("abc");
cout << sizeof(s1) << endl;

💬 可能有读者认为这个对象中一共就三个成员变量,一个指针两个无符号整数,那大小应该就是 12

size_t _size;
size_t _capacity;
char* _str;
  • 但是当我们运行起来可以发现,它的大小竟然是 28

image.png

  • 对于这一块而言我们就要去了解一下 string对象 的底层封装了,在【监视】窗口中我们可以看到,它是把字符串“abc”存到了一个 Buf 数组中,这个数组可容纳的大小为16个字节,虽然下面我圈起来的是【15】,是因为最后还有一个\0
  • 那么这就可以解释为什么大小为 28 了,一个Buf数组16个字节,三个成员变量12个字节,那即为 28

image.png💬 刚才说到这个 Buf数组 只能存放下16个字节的数据,但是当这个数据量变大的时候怎么办呢?

string s2("abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
  • 也是通过【监视】窗口看出,这么多的数据存放到了_Ptr所指向的堆空间中去,我们知道向堆中去申请的空间都是很大的,完全就能放得过了。那就可以看到VS这个机制还是蛮不错的

image.png

总结一下:

  1. 当字符串的_size < 16的时候,字符串是存放在【Buf数组】中的
  2. 当字符串的_size >= 16的时候,字符串存在【_Ptr】所指向的堆空间中

五、总结与提炼

最后来总结一下本文所学习的内容

  • 本文我们重点讲到的是STL中的string类,首先我们初步认识了这个类,逐个地去了解了它的一些接口函数,包括【默认成员函数】、【常见容量操作】、【访问及遍历操作】、【修改操作】、【其他字符串操作】以及【非成员函数重载】。基本上文档中的每一个接口我们都有去了解过,希望读者可以烂熟于心,常常翻阅使用
  • 但仅仅是了解了这些接口后还不够,接下去我们自己去模拟实现了这个string类,去逐步实现每一个接口的功能,不仅让我们对各个接口的性质更加地了解,而且还让我们对类和对象的一些基础语法知识有了很好的巩固。望读者也能够在阅读完本文后自己试着去模拟实现一下
  • 最后呢我们又拓展了一块知识点叫做【写时拷贝】,面对 ==浅拷贝的危害和深拷贝的资源浪费问题==,编译器呢做出了这一块的优化,通过双平台的观察我们可以了解到Linux下的【gcc / g++】采取的就是这种拷贝机制

以上就是本文要介绍的所有内容,感谢您的阅读:rose::rose::rose:

相关文章
|
6天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
43 18
|
6天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
32 13
|
6天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
23 5
|
6天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
20 5
|
6天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
23 4
|
6天前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
19 3
|
16天前
|
编译器 C语言 C++
【c++丨STL】list模拟实现(附源码)
本文介绍了如何模拟实现C++中的`list`容器。`list`底层采用双向带头循环链表结构,相较于`vector`和`string`更为复杂。文章首先回顾了`list`的基本结构和常用接口,然后详细讲解了节点、迭代器及容器的实现过程。 最终,通过这些步骤,我们成功模拟实现了`list`容器的功能。文章最后提供了完整的代码实现,并简要总结了实现过程中的关键点。 如果你对双向链表或`list`的底层实现感兴趣,建议先掌握相关基础知识后再阅读本文,以便更好地理解内容。
20 1
|
2月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
69 2
|
2月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
123 5
|
2月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
128 4