【C++】————string基础用法及部分函数底层实现

 9efbcbc3d25747719da38c01b3fa9b4f.gif

                                                      作者主页:     作者主页

                                                      本篇博客专栏:C++

                                                      创作时间 :2024年6月30日

9efbcbc3d25747719da38c01b3fa9b4f.gif

前言:

 本文主要介绍STL容器之一  ----  string,在学习C++的过程中,我们要将C++视为一个语言联邦(摘录于Effective C++ 条款一)。如何理解这句话呢,我们学习C++,可将其分为四个板块;分别为C、Object-Oriented C++(面向对象的C++)、Template C++(模板)、STL。本文就介绍STL中的string;

一、string是什么?

  string是STL文档的容器之一,是一个自定义类型,是一个类,由类模板basic_string实例化出来的一个类;

我们看一下cplusplus上是咋介绍的?

我们简单看一下即可,下面我来为大家做介绍。

二、string的使用:

  由于string出现的时间实际是早于STL的,是后来划分进STL库的,所以string开始的设计比较冗余,有许多没有必要的接口(一共106个接口函数);这也是被广大C++程序员吐槽的一个槽点,我们无需将每一个接口都记住,我们需要将核心接口记住并熟练使用,遇见一些默认的接口查看文档即可;

2.1构造函数

        在C++98中,string的构造函数一种有如下7种;

int main()
{// 1、无参默认构造// string();string s1;// 2、拷贝构造   // string (const string& str);string s2(s1);// 4、通过字符串常量初始化   // string (const char* s);string s4("hello world");// 3、通过字符串子串初始化   // string (const string& str, size_t pos, size_t len = npos);string s3(s4, 5, 5);// 5、通过字符串前n个字符初始化   // string (const char* s, size_t n);string s5("hello wrold", 6);// 6、用n个字符c初始化字符串  //  string (size_t n, char c);string s6(10, 'x');// 7、迭代器区间初始化(暂不介绍)return 0;
}

 其中提一下第三种,pos为子串的位置,len子串的长度,若len大于从子串pos位置开始后面字符总数,则表示初始化到子串结尾即可,比如我们要用 “hello world” 初始化字符串,若pos为6,len为20,则用world初始化字符串s1;len还有一个缺省值npos,其数值为无符号整型的-1,也就是无符号的最大值(无符号无负数);

2.2赋值重载:

赋值重载使string能够用=对string对象重新赋值,string的赋值重载一共有有如下三种;

int main()
{string tmp("hello world");string s1;string s2;string s3;// 1、string类进行赋值重载s1 = tmp;// 2、使用字符串常量赋值重载s2 = "hello world";// 3、使用字符赋值重载s3 = 'A';return 0;
}

2.3容量相关接口

 首先介绍如下六个简单一些的接口;

int main()
{string s1("hello world");// string中储存的字符个数(不包括\0)cout << s1.length() << endl;// 与length功能相同cout << s1.size() << endl;// 可以最多储存多少个字符(理论值,实际上并没有那么多)cout << s1.max_size() << endl;// string的当前容量cout << s1.capacity() << endl;// 当前string对象是否为空cout << s1.empty() << endl;// 清空s1中所有字符s1.clear();return 0;
}

 其中的length与size两种并无相异,由于string出现的较早,当时没有STL其他容器,先出现了length,后来为了统一接口,于其他容器接口保持一致,因此出现了size;

int main()
{// reserve 提前开空间(可能会大于指定的大小,因此开空间规则不同)string s1;s1.reserve(100);cout << s1.size() << endl;cout << s1.capacity() << endl;// resize 提前开空间并初始化 缺省值为0string s2;s2.resize(100);cout << s2.size() << endl;cout << s2.capacity() << endl;s2 += "hhhhhh";return 0;
}

  我们发现reserve仅仅只是修改capacity,而resize不仅会修改capacity,还会修改size,然后用第二个参数取去初始化新增的区间;当指定大小小于原来空间时,reserve什么都不会做,而resize则会则断大于指定大小后面的区域(在后面补零);

总结:reserve仅改变capacity,resize既改变capacity又改变size;当指定大小小于字符串的size时,resize还可以截断(在后面补 \0 )

2.4迭代器

    迭代器是STL库中的一个特殊的存在,我们可以通过迭代器对string类中的字符进行增删查改; 在string类中,我们可将其视为指针;string类中的迭代器接口有如下几种;

 begin函数返回的是字符串中第一个字符的位置的迭代器,而end函数返回的字符串中最后一个字符的下一个位置的迭代器; 因此遍历一个string类,有一下三种方法;

int main()
{string s1("hello world");// 三种遍历方式// 1、通过[]来访问每一个字符for (int i = 0; i < s1.size(); i++){cout << s1[i] << " ";}cout << endl;// 2、通过迭代来来访问每一个字符string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";it++;}cout << endl;// 3、通过范围for(其实范围for就是编译器替换成了迭代器遍历的方法)for (auto ch : s1){cout << ch << " ";}cout << endl;return 0;
}

     rbegin与rend系列为反向迭代器;rbegin返回的是最后一个字符的位置的迭代器,rend返回的是第一个字符的前一个位置的迭代器;

 我们可以通过反向迭代器,对其逆向遍历;反向迭代器的类型为 string::reverse_iterator;

int main()
{	string s1("hello world");string::reverse_iterator rit = s1.rbegin();while (rit != s1.rend()){cout << *rit << " ";rit++;}}

2.5下标访问:

    关于元素的访问,也有如下四个接口,最常用的还是方括号; 

int main()
{// []重载使string可以像字符数组一样访问string s1("hello world");cout << s1[0] << endl;cout << s1[1] << endl;// at 于[] 功能相同,只不过[]的越界是由assert来限制,而at则是抛异常cout << s1.at(0) << endl;cout << s1.at(1) << endl;// front访问string中第一个字符cout << s1.front() << endl;// back访问string中最后一个字符cout << s1.back() << endl;return 0;
}

方括号的使用如同数组的方括号使用相同;at与方括号用法相同,只是遇见非法访问时是抛异常解决;

2.6修改

string的修改接口设计得十分冗余;其中我们可以用+=替代append与push_back;实际中,也是+=用得比较多,但是我们还是了解一下相关用法; 

  +=我们可以加等一个string类,可以加等一个字符,也可以加等一个字符指针;因此有以下用法;

int main()
{string tmp("xxxx");string s1("hello world");// += 字符s1 += ' ';// += string类s1 += tmp;// += 字符指针s1 += " hello world";cout << s1 << endl;
}

int main()
{string tmp("xxxx");string s1;// 尾加字符// void push_back (char c);s1.push_back('c');// 尾加string类  // string& append (const string& str);s1.append(tmp);// 尾加string从subpos位置开始的sublen个字符   //string& append (const string& str, size_t subpos, size_t sublen);s1.append(tmp, 2, 3);// 用字符指针指向的字符串/字符尾加// string& append (const char* s);s1.append("hello world");// 用字符指针指向的字符串的前n个字符尾加// string& append (const char* s, size_t n);s1.append("hello world", 6);// 尾加n个c字符   // string& append (size_t n, char c);s1.append(5, 'x');// 迭代器区间追加// template <class InputIterator>// string& append(InputIterator first, InputIterator last);s1.append(tmp.begin(), tmp.end());cout << s1 << endl;return 0;
}

 assign为string的赋值函数;是一个扩增版的operator =,用的并不多,主要用法如下;

int main()
{string tmp("hello world");string s1;// 使用string类对其赋值// string& assign (const string& str);s1.assign(tmp);cout << s1 << endl;// 使用string类中从subpos位置开始的sublen个串来赋值// string& assign (const string& str, size_t subpos, size_t sublen);s1.assign(tmp, 2, 5);cout << s1 << endl;// 使用字符指针所指向的字符串对其赋值// string& assign (const char* s);s1.assign("hello naiths");cout << s1 << endl;// 使用字符指针所指向的字符串的前n个对其赋值// string& assign (const char* s, size_t n);s1.assign("hello naiths", 7);cout << s1 << endl;// 使用n个c字符对其赋值// string& assign (size_t n, char c);s1.assign(10, 'x');cout << s1 << endl;// 使用迭代器对其赋值// template <class InputIterator>// string& assign(InputIterator first, InputIterator last);s1.assign(tmp.begin(), tmp.end());cout << s1 << endl;return 0;
}

int main()
{string tmp("hello world");string s1;// 在pos位置插入string类字符串// string& insert (size_t pos, const string& str);s1.insert(0, tmp);cout << s1 << endl;// 在pos位置插入str的子串(subpos位置开始的sublen个字符)// string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);s1.insert(7, tmp, 0, 6);cout << s1 << endl;// 在pos位置插入字符指针指向的字符串// string& insert (size_t pos, constchar* s);s1.insert(2, "xxx");cout << s1 << endl;// 在pos位置插入字符指针指向的字符串的前n个字符// string& insert (size_t pos, const char* s, size_t n);s1.insert(7, "hello naiths", 8);cout << s1 << endl;// 在pos位置插入n个c字符// string& insert (size_t pos, size_t n, char c);s1.insert(0, 5, 'y');cout << s1 << endl;// 指定迭代器的位置插入n个字符c// void insert (iterator p, size_t n, char c);string::iterator it = s1.begin() + 10;s1.insert(it, 10, 'z');cout << s1 << endl;// 指定迭代器的位置插入字符c// iterator insert (iterator p, char c);s1.insert(s1.begin(), 'A');cout << s1 << endl;// 指定p位置插入迭代器区间的字符// template <class InputIterator>// void insert(iterator p, InputIterator first, InputIterator last);s1.insert(s1.begin(), tmp.begin() + 3, tmp.begin() + 8);cout << s1 << endl;// 删除pos位置开始的len个字符// string& erase (size_t pos = 0, size_t len = npos);s1.erase(2, 5);cout << s1 << endl;// 删除迭代器位置的那个字符// iterator erase (iterator p);s1.erase(s1.begin());cout << s1 << endl;// 删除迭代器区间的字符// iterator erase (iterator first, iterator last);s1.erase(s1.begin() + 2, s1.begin() + 5);cout << s1 << endl;return 0;
}

三、string底层实现

其实对于上面的这些库里面的函数,我们不需要全去记住,记住一些常用的即可,其他的等到我们要用到的时候去cplusplus网站里面找即可,对于下面我们要自己实现的函数,一定要记住,这些函数用的都是比较多的。

#pragma once
#define _CRT_SECURE_NO_WARNINGS#include<iostream>
#include<assert.h>
using namespace std;namespace bit
{class string{public://string();//不传参的情况string(const string& s);string(const char* str = "");//这里可以之际全缺省~string();const char* c_str() const;size_t size() const;char& operator[](size_t pos);const char& operator[](size_t pos) const;string& operator=(const string& s);//实现一个迭代器typedef char* iterator;iterator begin();iterator end();typedef const char* const_iterator;const_iterator begin() const;const_iterator end() const;void reserve(size_t n);void push_back(char ch);void append(const char* str);string& operator+=(char ch);string& operator+=(const char* str);void insert(size_t pos, char ch);void insert(size_t pos, const char* str);void erase(size_t pos, size_t len=npos);size_t find(char ch, size_t pso = 0);size_t find(const char* str, size_t pos = 0);void swap(string& s);string substr(size_t pos,size_t len);void clear();private:char* _str;size_t _size;size_t _capacity;const static size_t npos;};istream& operator>>(istream& is, string& str);ostream& operator<<(ostream& os, const string& str);}

上面这些就是我们要实现的一些函数的底层是什么样的,当然,我只是基于自己的理解去写这些,比起C++库里面的肯定要low一点,所以我们理解基础的一些思路即可。

下面我们来看一下这些代码,大家看一下,想要更好的理解可以自己去实现一遍。

#include"string.h"namespace bit
{//深拷贝string::string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}const size_t string::npos = -1;//不传参的析构//string::string()/*{_str = new char[1] {'\0'};_size = 0;_capacity = 0;}*///构造函数string::string(const char* str)//strlen是运行时计算长度,效率比较低,三个strlen重复计算了//但是如果像下面这样写,还是会出现一些问题,因为初始化列表的初始化是按声明的顺序初始化,这里就会出现问题/*:_str(new char[strlen(str) + 1]),_size(strlen(str)),_capacity(strlen(str))*/: _size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);}//赋值string& string::operator=(const string& s){//避免自己给自己赋值if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);//这样写是为了避免前面的空间大小小于后面的那个delete[] _str;_str = tmp;}return *this;}//析构string::~string(){delete[] _str;_size = 0;_capacity = 0;}//打印const char* string::c_str() const//像这种用于打印的函数,可以在后面加上一个const,//加上const意味着不能修改指向的内容和本事的值,这样即使放生权限的缩小依然不会报错{return _str;}size_t string::size() const{return _size;}char& string::operator[](size_t pos)//可读可写{assert(pos < _size);return _str[pos];}const char& string::operator[](size_t pos) const{assert(pos < _size);return _str[pos];}//迭代器string::iterator string::begin(){return _str;}string::iterator string::end(){return _str + _size;}//无法修改的迭代string::const_iterator string::begin() const{return _str;}string::const_iterator string::end() const{return _str + _size;}void string::reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[]_str;_str = tmp;_capacity = n;}}void string::push_back(char ch){if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;_str[++_size] = '\0';}void string::append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;}string& string::operator+=(char ch){push_back(ch);return *this;}string& string::operator+=(const char* str){append(str);return *this;}void string::insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len >= _capacity){reserve(_size + len);}int end = _size;while (end >= pos){_str[end+len] = _str[end];--end;}memcpy(_str + pos, str, len);_size += len;}void string::insert(size_t pos,char ch){if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}size_t end = _size;while (end >= pos){_str[end + 1] = _str[end];--end;}_str[pos] = ch;}void string::erase(size_t pos, size_t len){assert(pos <= _size);//当len大于前面的个数if (len>=_size-pos){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}}size_t string::find(char ch, size_t pos){while (pos!=_size){if (ch == _str[pos]){return pos;}++pos;}return npos;}size_t string::find(const char* str, size_t pos){char* p = strstr(_str + pos, str);return p - _str;}void string::swap(string& s){std::swap(_str, s._str);std::swap(_capacity, s._capacity);std::swap(_size, s._size);}string string::substr(size_t pos, size_t len){//len大于后面的剩余字符,有多少取多少if (len > _size - pos){string sub(_str - pos);return sub;}else{string sub;sub.reserve(len);for (size_t i = 0; i < len; i++){sub += _str[pos + i];}return sub;}}void string::clear(){_str[0] = '\0';_size = 0;}istream& operator>>(istream& is, string& str){char ch;is >> ch;while (ch != ' ' && ch != '\n'){str += ch;is >> ch;}return is;}ostream& operator<<(ostream& os, const string& str){for (size_t i = 0; i < str.size(); i++){os << str[i];}return os;}}

最后:

十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:

1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。

2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。

3.成年人的世界,只筛选,不教育。

4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。

5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。

最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)

愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/38206.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实验6 形态学图像处理

1. 实验目的 ①掌握数字图像处理中&#xff0c;形态学方法的基本思想&#xff1b; ②掌握膨胀、腐蚀、开运算、闭运算等形态学基本运算方法&#xff1b; ③能够利用形态学基本运算方法&#xff0c;编程实现图像去噪&#xff0c;边界提取等功能。 2. 实验内容 ①调用Matlab /…

C语言 统计字符类型个数

这个程序读取用户输入的一行字符&#xff0c;并统计其中的英文字母、空格、数字和其他字符的个数。 #include <stdio.h> #include <ctype.h>int main() {char ch;int letters 0, spaces 0, digits 0, others 0;printf("输入一行字符: ");// 逐字符读…

PMBOK® 第六版 结束项目或阶段

目录 读后感—PMBOK第六版 目录 不论是阶段的收尾还是项目整体的收尾&#xff0c;都应是令人振奋的事。然而&#xff0c;在实际生活中&#xff0c;收尾工作却相当艰难。会遭遇负责人调离、换任&#xff0c;导致不再需要已购产品&#xff1b;项目收尾时对照招标文件或合同&…

51-61 CVPR 2024 最佳论文 | Rich Human Feedback for Text-to-Image Generation

23年12月&#xff0c;加州大学圣地亚哥、谷歌研究院、南加州大学、剑桥大学联合发布Rich Human Feedback for Text-to-Image Generation论文。 作者受大模型中RLHF技术的启发&#xff0c;用人类反馈来改进Stable Diffusion等文生图模型&#xff0c;提出了先进的RichHF-18K数据…

足球虚拟越位线技术FIFA OT(一)

此系列文章用于记录和回顾开发越位线系统的过程&#xff0c;平时工作较忙&#xff0c;有空时更新。 越位线技术 越位技术已被用于图形化分析足球中潜在的越位情况。 自 2018 年将视频助理裁判 &#xff08;VAR&#xff09; 引入比赛规则以来&#xff0c;人们越来越关注准确确…

完美世界|单机版合集(共22个版本)

前言 我是研究单机的老罗&#xff0c;今天给大家带来的是完美世界的单机版合集&#xff0c;一共22个版本。本人亲自测试了一个版本&#xff0c;运行视频如下&#xff1a; 完美世界|单机版合集 先看所有的版本的文件&#xff0c;文件比较大&#xff0c;准备好空间&#xff0c;差…

C#高级语法之特性

自定义特性和使用 什么是特性 特性&#xff08;attribute&#xff09;是一种允许我们向程序的程序集增加元数据的语言结构&#xff0c;它是用于保存程序结构信息的某种特殊类型的类。 将应用了特性的程序结构叫做目标设计用来获取和使用元数据的程序&#xff08;对象浏览器&a…

Transformer详解encoder

目录 1. Input Embedding 2. Positional Encoding 3. Multi-Head Attention 4. Add & Norm 5. Feedforward Add & Norm 6.代码展示 &#xff08;1&#xff09;layer_norm &#xff08;2&#xff09;encoder_layer1 最近刚好梳理了下transformer&#xff0c;今…

【VScode】常用配置

1.indenticator 增加白色竖条&#xff0c;显示方法范围 2.Git Graph 给git变换分支增添颜色区分 3.Vue 系列 vue 系列&#xff1a;给纯白色代码添加 颜色区分 3.eslint eslint警告&#xff0c;比如{ } 只写了半个会标红提示错误 等错误信息提示 需要配置js等页面 非下…

1.linux操作系统CPU负载

目录 概述CPU平均负载查看平均负载结束 概述 CPU 使用率 和CPU 平均使用率。 CPU平均负载 单位时间内系统处于 [可运行状态] 和 [不可中断状态] 的平均进程数&#xff0c;就是平均活跃进程数&#xff0c;和CPU使用率并没有直接关系 可运行状态 正在使用CPU或者正等待CPU的进…

【Elasticsearch】linux使用supervisor常驻Elasticsearch,centos6.10安装 supervisor

背景&#xff1a; linux服务器&#xff0c;CentOS 6操作系统&#xff0c;默认版本python2.6.6&#xff0c;避免安装过多的依赖不升级python 在网上查的资料python2.6.6兼容supervisor版本 3.1.3 安装supervisor 手动在python官网下载supervisor&#xff0c;并上传到服务器 下…

量化交易心法——如何建立自己的算法交易事业

量化交易,也称算法交易,是严格按照将计算机算法程序给出的买卖决策进行的证券交易。 一、 什么人适合成为量化交易员 做量化交易并不一定需要特别高的学历,只要具备一定的金融学以及统计学知识,有一定的经济基础,不需要用交易的收益来维持日常生活,因为并不是很快就能找…

Linux_动、静态库

目录 一、静态库 1、静态库的概念 2、制作静态库的指令 3、制作静态库 4、链接静态库 二、动态库 1、动态库的概念 2、制作动态库的指令 3、制作动态库 4、链接动态库 5、动态库的加载 三、静态库与动态库的区别 结语 前言&#xff1a; 在Linux下大部分程序进…

第2章 数据存储篇

目录 2.1 MongoDB&#xff1a;面向文档的灵活存储 2.1.1 MongoDB基础与架构 2.1.1.1基本概念 2.1.1.2MongoDB安装与配置 1&#xff09;安装MongoDB-Linux安装示例&#xff08;以Ubuntu为例&#xff09; 2&#xff09;更新包列表并安装MongoDB 3&#xff09;启动MongoDB服…

利用OPT算法解决最短访问次数问题

一、题目 数据库缓存&#xff0c;模拟访问规则如下&#xff1a; 当查询值在缓存中&#xff0c;直接访问缓存&#xff0c;不访问数据库。否则&#xff0c;访问数据库&#xff0c;并将值放入缓存。 若缓存已满&#xff0c;则必须删除一个缓存。 给定缓存大小和训练数据&#xff…

对代理模式和动态代理以及AOP的一些理解

代理模式&#xff1a; 代理模式&#xff0c;也叫做静态代理&#xff0c;是一种结构型设计模式&#xff0c;它为其他对象提供了一种代理&#xff0c;以控制对这个对象的访问。 代理模式可以在不修改原有类的情况下&#xff0c;对其功能进行扩展&#xff0c;编译时就确定了代理…

【JavaEE】多线程代码案例(1)

&#x1f38f;&#x1f38f;&#x1f38f;个人主页&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;JavaEE专栏&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;上一篇文章&#xff1a;多线程&#xff08;2…

leetcode每日一练:顺序表OJ题

第一题&#xff1a;移除元素 题目要求&#xff1a;给一个数组nums和一个值val&#xff0c;你需要 原地 移除所有所有数值等于val的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用0(1)的额外空间并 原地 修改输入数组。 元素的顺序…

【Tools】AIGC:人工智能生成内容的新时代

那年夏天我和你躲在 这一大片宁静的海 直到后来我们都还在 对这个世界充满期待 今年冬天你已经不在 我的心空出了一块 很高兴遇见你 让我终究明白 回忆比真实精彩 &#x1f3b5; 王心凌《那年夏天宁静的海》 随着人工智能&#xff08;AI&#xff09;技术的…

三生随记——午夜咖啡馆

在城市的边缘&#xff0c;隐藏着一间古老的咖啡馆——“午夜咖啡馆”。它的外观不起眼&#xff0c;却总能在夜晚吸引那些寻找安宁或寻求刺激的顾客。据说&#xff0c;咖啡馆的老板是一位年长的绅士&#xff0c;他的脸上总是挂着神秘莫测的微笑。 艾米是一名作家&#xff0c;常常…