c++STL-string的模拟实现

news2025/5/13 20:42:25

c++STL-string的模拟实现

  • string的模拟实现
    • string的模拟线性表的实现
      • 构造函数
      • 析构函数
      • 获取长度(size)和获取容量(capacity)
      • 访问 [] 和c_str
      • 迭代器(iterator)
      • 交换swap
      • 拷贝构造函数
      • 赋值重载(=)
      • 扩容(reserve和resize)
      • 插入(insert)
      • 尾插(push_back、+=、append)
      • 比较运算符重载
      • 删除(erase)
      • 清空(clear)
      • 流插入(operator<<)和流提取(operator>>)
      • 查找(find)
      • 取字符串(substr)
    • string模拟参考程序

string的模拟实现

建议先看c++STL-string的使用-CSDN博客

这里的模拟实现是帮助自己更好地理解string。不排除以后觉得库里的string不好用,自己亲自写一个更好的。

并且c++的标准并没有规定怎么实现,而是给定了指标,程序员达到这个标准即可。

这里贴一个功能文档,后期自己想复习了,就照着这个文档搓一遍string

string的模拟线性表的实现

这里参考顺序表的概念和实现-CSDN博客:

class string{
public:
    //const static size_t npos=-1;//特例,不建议用特例
    const static size_t npos;
private:
    char* _str;
    size_t _size;
    size_t _capacity;
}
const size_t string::npos=-1;

_size表示\0的位置。

_capacity表示数组的大小。数组需要预留1个空间给\0,因此_capacity会比实际情况小1。

const静态整型变量可以给缺省值。类的成员变量给的缺省值是给初始化列表使用,但静态成员变量不会通过构造函数的初始化列表,所以按照标准应该是不可以的。

猜测写c++语法的人或写编译器的人为了个人方便,为const静态整型变量开了一个特例,甚至只能是整型,别的类型比如double不允许。

因为string的成员函数很多而且功能大都是重复的,所以只选几个重要的功能模拟实现。

也不做basic_string的类模板,就只做basic_string<char>

不确定的功能也可以用库中的作对比。

构造函数

对比string的构造函数接口:

请添加图片描述

这里选择形参为char*的构造函数来模拟实现。

#include<iostream>
#include<string>
using namespace std;

int main() {
    string st;
    cout << st;//调用默认构造函数也不会越界
    string st2("abc");
    cout << st2;
    return 0;
}

所以构造函数满足不给初始值也不会越界(即至少有1个\0)。

目前能想到的方案是构造函数的形参给全缺省,用全缺省的形参去初始化成员变量,用全缺省的形参也能省去无参构造函数的定义。

几种常见的不合理的构造函数:

//1
//成员指针_str可修改,str经过const修饰不可通过str修改内存
//_str也不能加const修饰。
string(const char* str)
    :_str(str)//
    ,_size(strlen(str))
{
    //拷贝字符串
}

//2
//初始化列表按照成员变量在类中声明的顺序。
//所以_capacity
string(const char* str)
    :_size(strlen(str))
    ,_capacity(strlen(str))
    ,_str(new char[_capacity+1])
{
    //拷贝字符串
}

析构函数

析构函数如果不在意效率,可以按照顺序表的方式去释放空间。如果在意效率,后文会介绍引用计数和写时拷贝。

获取长度(size)和获取容量(capacity)

size_t size() const;
size_t capacity() const;

获取长度:返回_size,并保证不会修改*this

获取容量:返回_capacity,并保证不会修改*this

这样可以方便查看对象的情况。

访问 [] 和c_str

请添加图片描述

请添加图片描述

重载operator[],返回类型char&。需要注意判断下标是否合理。

要提供2个[],一个不加const,另一个返回值和this指针都加const

c_str返回_str的地址。

迭代器(iterator)

      iterator begin();
const_iterator begin() const;
      iterator end();
const_iterator end() const;

迭代器的功能和指针类似,但不一定是指针。在模拟实现阶段用指针代替。

请添加图片描述

请添加图片描述

其中范围for要求类至少有普通版本和const版本的begin()end()(迭代器但凡更换一个名字都不支持,例如Begin,范围for就不支持),并且这些迭代器能正常访问,以及解引用(*)、前置递增(++)以及相等比较(==)和不等比较(!=)。

对于内置类型的数组来说,指针本身就可以作为迭代器使用,因为指针支持这些操作。

其中迭代器使用基础的beginend即可,反向迭代器需要用到适配器模式,在初学阶段暂时用不上。但可以通过其他迭代器类间接调用已有的beginend,以及相关的操作来实现。

交换swap

两个string对象完全可以只交换成员变量的值,这样可以减少多余的拷贝带来的额外开销。

拷贝构造函数

  • 用另一个对象初始化*this,需要另外开辟空间。

  • 还可以利用形参的_str去调用构造函数来初始化临时对象,之后*this和临时对象交换_str的地址即可。但需要注意的是,新生成的临时对象不会去调用原始的构造函数,因此需要额外初始化
    之后临时对象调用析构函数,和*this没有关系。

毕竟也是构造函数,需要对成员变量进行初始化。

拷贝构造函数还会面临另外的问题:浅拷贝问题。

浅拷贝问题是因为类的默认成员函数会自动运行做一些可能出问题的事(比如两个对象中的指针同时指向堆区的某块内存,连续进行两次释放的行为)。如果能花费较小的代价阻止默认成员函数的部分行为,则可以让浅拷贝成为一种优化方式。详细见引用计数写时拷贝

赋值重载(=)

请添加图片描述

char*可以通过调用构造函数间接实现,因此只需要再实现一个char为形参的即可。

进行赋值重载前先进行容量检测(reserve)。

  • 先进行判断,防止自己给自己赋值做多余的工作。

    后检查容量,容量不够时需要扩容,否则直接拷贝即可。

  • 还可以利用形参的_str去调用构造函数来初始化临时对象,之后*this和临时对象交换_str的地址即可。

  • 甚至还可以用传值传参,系统自己调用拷贝构造函数,再交换数据即可。

赋值重载也会涉及浅拷贝问题。详细见引用计数和写时拷贝

三种选择都可以,更推荐后面两个。

扩容(reserve和resize)

请添加图片描述

string的操作中,增加字符串中的内容需要扩容。因此推荐先实现扩容。

首先实现reserve。检测容量是否足够,不够的话扩容,size不变。同时注意初始capacity

如果是缩容,reserve什么也不做。

c++尽量不用realloc扩容,而是另外new另一个空间,因为要扩容的空间可能是自定义类型,需要调用其他类的构造函数和析构函数。

如果是resize

  • _size小于要扩容的大小,则在reserve的基础上更改_size的值,并用指定的符号去填充即可。
  • _size大于要扩容的大小,则相当于缩容,只需要更改_size的值并赋值\0即可。

根据实际经验,MSVC和mingw64的扩容方式不同(MSVC是1.5倍,mingw64是2倍扩容)。

用哪个都可以。

扩容时统一使用新开空间。虽然可以使用realloc,但很多时候realloc也是开辟新空间,释放旧空间。

插入(insert)

请添加图片描述

插入函数选择形参是(size_t,string&)的实现即可。

首先先检查容量是否支持插入新的字符。

然后下标pos后的字符串整体后移若干个单位,并在pos处插入一个字符(串)。可能涉及扩容。

请添加图片描述

而且需要特别注意头插和尾插的情况。若用于表示下标的整型用的是无符号(size_t),则要注意在无符号数的情况下负数的情况。特别是 -1 反而是最大值的情况。

尾插(push_back、+=、append)

请添加图片描述

因为原版的stringappend的设计和+=的功能重复。因此仅实现append+=其中之一,另一个用代码复用即可。

也可以只实现insert,之后凡是和插入有关的都复用insert即可。

往字符串结尾插入字符,如果容量不够需要扩容。push_back是插入1个,+=可以插入1个,也可以插入整个字符串。

扩容需要留意_capacity是否为0。可以用c语言自带的strcpy,将\0顺带拷贝回去。

而且有必要的话需要提供形参为const的另一个版本的成员函数。

比较运算符重载

请添加图片描述

比较运算符重载有两个操作数,可以做成全局函数,将这些比较运算符设置为友元,或直接通过operator[]访问。

可以用c语言的库函数strcmp代替。但为了增强代码的独立性,尽量选择尽量不使用c语言自带的库函数。

而且在这个地方函数复用会很多,需要注意形参的权限。比如const修饰的形参不可作为实参上传给非const修饰的形参的函数(权限放大)。

删除(erase)

string& erase(size_t pos = 0, size_t len = npos);

从下标pos开始删除长度为len的字符。

pos>=_size则应阻止删除。

_size<=pos+len(或len==npos),则_str[pos]='\0',更新_size即可。

_size>pos+len,则需要挪动数据并更新size

清空(clear)

将首字符设置为空字符\0并修改_size;为0即可。

可以释放空间,但没必要。

流插入(operator<<)和流提取(operator>>)

这里的流插入不一定要写成友元。string还可以通过访问operator[]来访问数据。

若流插入的形参用const修饰,则流插入调用的一些相关的函数比如迭代器也需要用const修饰this

流提取的形参不能用const修饰,因为流提取需要修改后台的数据。

输入的内容通过流提取尾插到string对象中,需要注意的是系统自带的流提取(cinoperator默认将空格和换行作为不同数据的分界,所以需要将所有的字符包括空格和换行符也读取。

读取所有字符的方法:scanf("%c",&st[i]);istream::get()getchar()

既然选择了c++,所以更推荐用istream::get

在读取之前先将string对象清空clear)。

流提取还可以用申请在函数作用域内的数组优化,将数组通过operator+=尾插给对象。这样做可以减少扩容次数。

查找(find)

请添加图片描述

实现的时候可以用暴力查找,时间复杂度为 O ( n × m ) O(n\times m) O(n×m)。如果觉得找匹配串的效率太低还可以尝试kmp算法(理论效率很高,实际效率看情况)或字符串BM算法。

如果是从某个位置开始找,则用_str+pos即可。

取字符串(substr)

string substr (size_t pos = 0, size_t len = npos)const;

因为要返回临时对象,所以需要先实现拷贝构造函数。

生成临时对象,再从*this中取指定长度尾插给临时对象,最后返回临时对象(不能串引用返回)。

如果要取的长度为npos或大于_size,则一直取到尾即可。

取字符串的时候需要注意浅拷贝问题。

string模拟参考程序

模拟实现时将自己的string放在另一个命名空间mystd里,这样想用库里的string就可以加域名std::,想用自己的string就加域名mystd::

最终参考程序(无反向迭代器)的某 .h 文件:

#pragma once
#include<cassert>
#include<iostream>
using std::ostream;
using std::istream;

namespace mystd {
    template<class T>
    void swap(T& a, T& b) {
        T tmp = a;
        a = b;
        b = tmp;
    }

    class string {
    public:
        const static size_t npos;

        //构造函数
        string(const char* s = "") {
            //求s的长度
            size_t len = 0;
            const char* tmps = s;
            while (*tmps) {
                ++len;
                ++tmps;
            }
            //初始化长度、容量等信息
            _size = len;
            _capacity = len;
            _str = new char[_capacity + 1]{ '\0' };

            //拷贝
            tmps = s;
            char* tmps2 = _str;
            while (*tmps2++ = *tmps++);
        }

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

        //返回c风格字符串的地址
        const char* c_str() const {
            return _str;
        }

        //用[]进行索引访问
        char& operator[](size_t pos) {
            assert(pos < _size);
            return _str[pos];
        }
        const char& operator[](size_t pos) const {
            assert(pos < _size);
            return _str[pos];
        }

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

        //返回容量
        size_t capacity()const {
            return _capacity;
        }

        //迭代器
        typedef char* iterator;
        typedef const char* const_iterator;

        iterator begin() {
            return _str;
        }

        iterator end() {
            return _str + _size;
        }

        const_iterator begin() const {
            return _str;
        }

        const_iterator end() const {
            return _str + _size;
        }

        //交换,独立于namespace std中的swap
        void swap(string& a) {
            //两个对象:*this,a
            mystd::swap(_str, a._str);
            mystd::swap(_size, a._size);
            mystd::swap(_capacity, a._capacity);
        }

        //拷贝构造函数
        string(const string& str)
            :_str(nullptr)//临时对象不经过构造函数会产生随机数
            , _size(0)
            , _capacity(0) {
            string tmp(str._str);//生成第2个临时对象
            mystd::string::swap(tmp);
        }

        //赋值重载:
        string& operator=(string tmp) {
            mystd::string::swap(tmp);
            return *this;
        }
        string& operator=(char c) {
            char ch[2] = { c,'\0' };
            string tmp(ch);
            mystd::string::swap(tmp);
            return *this;
        }


        //扩容
        void reserve(size_t n = 0) {
            if (n > _capacity) {
                char* tmp = new char[n + 1]{ '\0' };
                char* aa = tmp, * bb = _str;
                while (*aa++ = *bb++);
                delete[]_str;
                _str = tmp;

                _capacity = n;
            }
        }

        void resize(size_t n, char ch = '\0') {
            if (n <= _size) {
                _str[n] = '\0';
                _size = n;
                while (n < _capacity)
                    _str[n++] = '\0';
                return;
            }
            reserve(n);
            while (_size < n) {
                _str[_size] = ch;
                ++_size;
            }
        }

        //插入
        string& insert(size_t pos, const string& s) {
            assert(pos <= _size);
            reserve(_size + s._size);

            size_t end = _size - 1;
            while (end >= pos && end != -1) {
                _str[end + s._size] = _str[end];
                --end;
            }

            for (size_t i = 0; i < s._size; i++) {
                _str[pos + i] = s[i];
            }
            _size += s._size;
            return *this;
        }

        //尾插
        string& operator+=(const string& s) {
            insert(_size, s);
            _str[_size] = '\0';
            return *this;
        }

        string& operator+=(char s) {
            push_back(s);
            return *this;
        }

        void push_back(char c) {
            char ch[2] = { c,'\0' };
            insert(_size, ch);
        }

        //比较运算符重载
        bool operator==(const string& s) const {
            if (_size != s._size)
                return 0;
            for (size_t i = 0; i < s._size; i++) {
                if (_str[i] != s._str[i]) {
                    return 0;
                }
            }
            return 1;
        }

        bool operator<(const string& s) const {
            for (size_t i = 0; i < s._size && i < _size; i++) {
                if (_str[i] > s._str[i]) {
                    return 0;
                }
            }
            if (_size > s._size)
                return 0;
            return 1;
        }

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

        //删除
        string& erase(size_t pos = 0, size_t len = npos) {
            assert(pos < _size);
            if (len == npos || pos + len >= _size) {
                _str[pos] = '\0';
                _size = pos;
                return *this;
            }
            while (pos + len < _size) {
                _str[pos] = _str[pos + len];
                ++pos;
            }
            _str[pos] = '\0';
            _size -= len;
            return *this;
        }

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

        //查找
        size_t find(const string& s, size_t pos = 0)const {
            for (size_t i = 0, ti; i <= _size - s._size; i++) {
                ti = i;
                for (size_t j = 0; j < s._size; j++) {
                    if (s[j] == _str[ti])
                        ++ti;
                    else
                        break;
                }
                if (ti == i + s._size)
                    return i;
            }
            return npos;
        }
        size_t find(char c, size_t pos = 0)const {
            for (size_t i = 0; i < _size; i++) {
                if (_str[i] == c)
                    return i;
            }
            return npos;
        }

        //提取片段
        string substr(size_t pos = 0, size_t len = npos)const {
            string tmp;
            if (len == npos || pos + len > _size) {
                while (tmp += _str[pos++], pos < _size);
                return tmp;
            }

            while (tmp += _str[pos++], tmp._size < len);
            return tmp;
        }

    private:
        char* _str;
        size_t _size;
        size_t _capacity;
    };
    const size_t string::npos = -1;
    ostream& operator<<(ostream& out, const string& st) {
        for (const auto& x : st)
            out << x;
        return out;
    }
    istream& operator>>(istream& in, string& s) {
        s.clear();

        char buff[129];
        size_t i = 0;

        char ch;
        //连同空格一起接收
        while (ch = in.get(), ch != ' ' && ch != '\n') {
            buff[i++] = ch;
            if (i == 128) {
                buff[i] = '\0';
                s += buff;
                i = 0;
            }
        }

        if (i != 0) {
            buff[i] = '\0';
            s += buff;
        }

        return in;
    }
}

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

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

相关文章

YashanDB(崖山数据库)V23.4 LTS 正式发布

2024年回顾 2024年11月我们受邀去深圳参与了2024国产数据库创新生态大会。在大会上崖山官方发布了23.3。这个也是和Oracle一样采用的事编年体命名。 那次大会官方希望我们这些在一直从事在一线的KOL帮助产品提一些改进建议。对于这样的想法&#xff0c;我们都是非常乐于合作…

python 写一个工作 简单 番茄钟

1、图 2、需求 番茄钟&#xff08;Pomodoro Technique&#xff09;是一种时间管理方法&#xff0c;由弗朗西斯科西里洛&#xff08;Francesco Cirillo&#xff09;在 20 世纪 80 年代创立。“Pomodoro”在意大利语中意为“番茄”&#xff0c;这个名字来源于西里洛最初使用的一个…

PyCharm 加载不了 conda 虚拟环境,不存在的

#工作记录 前言 在开发过程中&#xff0c;PyCharm 无法加载 Conda 虚拟环境是常见问题。 在不同情况下&#xff0c;“Conda 可执行文件路径”的指定可能会发生变化&#xff0c;不会一尘不变&#xff0c;需要灵活处置。 以下是一系列解决此问题的经验参考。 检查 Conda 安装…

设计模式学习整理

目录 UML类图 设计模式六大原则 1.单一职责原则 2.里氏替换原则 3.依赖倒置原则 4.接口隔离原则 5.迪米特法则(最少知道原则) 6.开(放封)闭原则 设计模式分类 1.创建型模式 2.结构型模式 4.行为型模式 一、工厂模式(factory——简单工厂模式和抽象工厂模式) 1.1、…

二分查找的理解

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h>int binary_search(int arr[], int k, int sz) {int left 0;int right sz - 1;//这个是下标&#xff0c;减一是因为在0开始的&#xff0c;怕越界&#xff08;访问无效&#xff09;while (left < right){int mid…

【Java】线程实例化 线程状态 线程属性

线程实例化 继承 Thread 类 创建类继承自 Thread 类 . class MyThread extends Thread重写 run() 方法 . Overridepublic void run(){// 线程要执行的任务代码}实例化自定义线程类 . 实现 Runnable 接口 创建类实现 Runnable 接口 . class MyRunnable implements Runnable实…

卫宁健康WiNGPT3.0与WiNEX Copilot 2.2:医疗AI创新的双轮驱动分析

引言:医疗AI的双翼时代 在医疗信息化的浪潮中,人工智能技术的深度融入正在重塑整个医疗行业。卫宁健康作为国内医疗健康和卫生领域数字化解决方案的领军企业,持续探索AI技术在医疗场景中的创新应用。2025年5月10日,在第29届中国医院信息网络大会(CHIMA2025)上,卫宁健康…

I2C通讯

3.1. 本章节的代码仓库 1 2 3 4 5 6 #如之前有获取则可跳过 #获取仓库 git clone https://gitee.com/LubanCat/lubancat_rk_code_storage.git#代码所在的位置 lubancat_rk_code_storage/quick_start/i2c3.2. i2c I2C(Inter&#xff0d;Integrated Circuit)是一种通用的总线协…

Excel实现单元格内容拼接

一、应用场景&#xff1a; 场景A&#xff1a;将多个单元格拼接&#xff0c;比如写测试用例时&#xff0c;将多个模块拼接&#xff0c;中间用“-”隔开 场景B&#xff1a;将某单元格内容插入另一单元格固定位置&#xff08;例如在B1中添加A1的内容&#xff09; 二、实际应用&a…

2025前端面试遇到的问题(vue+uniapp+js+css)

Vue相关面试题 vue2和vue3的区别 一、核心架构差异 特性Vue2Vue3响应式系统基于Object.defineProperty基于Proxy&#xff08;支持动态新增/删除属性&#xff09;代码组织方式Options API&#xff08;data/methods分块&#xff09;Composition API&#xff08;逻辑按功能聚合&am…

广东省省考备考(第八天5.11)—言语:逻辑填空(每日一练)

错题 解析 第一空&#xff0c;搭配“期盼”&#xff0c;且根据“生命&#xff0c;是来自上天的馈赠”&#xff0c;可知父母对孩子的出生是非常期盼的。A项“望穿秋水”&#xff0c;形容对远地亲友的殷切盼望&#xff0c;C项“望眼欲穿”&#xff0c;形容盼望殷切&#xff0c;均…

github+ Picgo+typora

github Picgotypora 本文将介绍如何使用Picgo在typora中实现上传服务 创建github仓库以及配置token 创建仓库 注意需要Initialize 添加README 配置为public 配置token github点击头像找到setting 选择Developer setting 配置token generate 选第一个第二个都行(我这里选第…

[网安工具] IP 信息收集工具 —— LBD · 使用手册

&#x1f31f;想了解其它网安工具&#xff1f;看看这个&#xff1a;[网安工具] 网络安全工具管理 —— 工具仓库 管理手册 lbd | Kali Linux ToolsVideolbd Usage ExampleTest to see if the target domain (example.com) is using a load balancer:rootkali:~# lbd example.c…

说说es配置项的动态静态之分和集群配置更新API

这天因为某件工作来到了es官网某个参数配置相关的页面&#xff0c;注意到了下图圆圈里的“Dynamic”&#xff1a; 链接&#xff1a;https://www.elastic.co/guide/en/elasticsearch/reference/8.1/modules-cluster.html#misc-cluster-settings 显然这是对配置项的一个描述&am…

LLMs之Mistral Medium 3:Mistral Medium 3的简介、安装和使用方法、案例应用之详细攻略

LLMs之Mistral Medium 3&#xff1a;Mistral Medium 3的简介、安装和使用方法、案例应用之详细攻略 目录 Mistral Medium 3 简介 1、Mistral Medium 3 特点 Mistral Medium 3 安装和使用方法 2、使用方法 (1)、创建Agent (2)、模型微调 Mistral Medium 3 案例应用 Mistr…

并发设计模式实战系列(17):信号量(Semaphore)

&#x1f31f; 大家好&#xff0c;我是摘星&#xff01; &#x1f31f; 今天为大家带来的是并发设计模式实战系列&#xff0c;第十七章信号量&#xff08;Semaphore&#xff09;&#xff0c;废话不多说直接开始~ 目录 一、核心原理深度拆解 1. 信号量本质模型 2. 并发控制…

RAGMCP基本原理说明和相关问题解惑

一、RAG架构原理和局限性 1.1 概念解释 RAG&#xff08;Retrieval-Augmented Generation&#xff09;&#xff1a;检索增强生成&#xff0c;让大模型接受外部输入后&#xff0c;总结输出 向量数据库&#xff1a;向量数据通常是高维空间中的点&#xff0c;代表复杂的数据结构…

Java学习手册:服务注册与发现

一、服务注册与发现的概念 在微服务架构中&#xff0c;服务注册与发现是核心功能之一。由于微服务架构中服务实例的数量和位置是动态变化的&#xff0c;服务注册与发现机制允许服务实例在启动时自动注册到注册中心&#xff0c;并在停止时自动注销。其他服务可以通过查询注册中…

双向Transformer:BERT(Bidirectional Encoder Representations from Transformers)

基于Transformer架构&#xff0c;通过双向上下文建模训练&#xff0c;提高完成任务的性能。 一 BERT的核心理念 1.1双向上下文建模依赖 之前讲的双向递归是用两个RNN进行&#xff0c;而BERT是通过Transformer的自注意力机制同时捕捉上下文信息。 1.1.1掩码语言模型&#xf…

EdgeOne Pages MCP 入门教程

什么是MCP&#xff1f; MCP (Model Context Protocol) 是一个开放协议&#xff0c;允许 AI 模型安全地与本地和远程资源进行交互。通过在支持 MCP 的客户端&#xff08;如 Cline、Cursor、Claude 等&#xff09;上进行统一配置&#xff0c;可以让 AI 访问更多资源并使用更多工…