深入探索 C++ 中的 string 类:从基础到实践

在 C++ 编程的世界里,string类是处理字符串的得力助手。它为开发者提供了便捷、高效的字符串操作方式,相较于 C 语言中以'\0'结尾的字符数组和str系列库函数,string类更符合面向对象编程(OOP)的思想,让字符串处理变得轻松许多。

一、为什么选择 string 类

1.1 C 语言字符串的不足​

在 C 语言的世界里,字符串以特殊的'\0'字符作为结尾标识,这种设计虽然简洁,但也带来了诸多隐患。开发者在操作字符串时,必须手动管理底层内存空间,稍有疏忽就可能引发越界访问问题,导致程序崩溃或出现难以调试的错误。例如,使用strcpy函数进行字符串拷贝时,若目标字符数组的空间不足以容纳源字符串,就会发生内存溢出,覆盖相邻内存区域的数据。而且,C 标准库中的str系列函数(如strlen、strcat等)与字符串数据本身相互分离,这不仅不利于代码的模块化组织,也不符合面向对象编程的封装思想,使得代码的维护和扩展变得困难重重。​

1.2 实际应用需求​

在实际的软件开发场景中,无论是在线评测(OJ)平台上的算法题目,还是企业级项目的日常开发工作,string类的使用频率都极高。它的优势在于简单易用、操作便捷,能够显著提升开发效率。例如,在处理用户输入的文本信息、解析文件中的字符串数据,或者进行字符串的拼接、查找等操作时,string类都能轻松应对,极大地减少了开发者编写复杂底层代码的工作量。

二、标准库中的 string 类​

2.1 基本使用​

在 C++ 程序中使用string类,首先需要包含<string>头文件,这是引入string类定义的关键步骤。同时,为了避免每次使用string类时都显式指定命名空间,通常会引入using namespace std;语句。不过,在大型项目中,为防止命名冲突,也可以采用std::string的形式来明确使用标准库中的string类。​

string类提供了丰富多样的构造函数,以满足不同的创建需求。例如,通过string s1;可以创建一个空字符串,此时该字符串不包含任何有效字符;使用string s2("hello bit");能够基于 C 风格字符串构造一个string对象,将"hello bit"的内容复制到新创建的string对象中;而string s3(s2);则是通过拷贝构造函数,基于已有的string对象s2创建一个与之内容完全相同的新对象s3。​

cpp 复制代码
#include <iostream>​

#include <string>​

using namespace std;​

​

int main() {​

    string s1;​
    
    string s2("Hello, World!");​

    string s3(s2);​

    cout << "s1: " << s1 << endl;​

    cout << "s2: " << s2 << endl;​

    cout << "s3: " << s3 << endl;​

    return 0;​

}​

2.2 辅助语法:auto 和范围 for​

  • auto 关键字:在 C++11 标准推出后,auto关键字被赋予了全新的功能和含义。它能够让编译器在编译阶段自动推导变量的类型,极大地简化了变量声明的过程。在声明指针类型变量时,auto和auto*的效果是等同的,都能让编译器正确推导指针类型;而在声明引用类型变量时,必须显式加上&符号,以明确表示这是一个引用变量。
cpp 复制代码
int num = 10;​

auto a = num; // a为int类型​

auto* p = &num; // p为int*类型​

auto& r = num; // r为int&类型​

然而,auto关键字也存在一些使用限制。它不能作为函数的参数类型,因为函数参数需要明确的类型定义,以便在函数调用时进行参数传递和类型检查;虽然auto可以作为函数的返回值类型,但在实际编程中并不建议轻易使用,因为这可能会导致代码的可读性和可维护性下降;此外,auto也无法直接用于声明数组,因为数组的大小和元素类型需要在声明时明确指定。​

在遍历map容器时,auto关键字的优势得以充分体现。通过auto it = dict.begin();这样简洁的语句,就能声明一个合适类型的迭代器,方便地遍历map中的键值对。​

cpp 复制代码
#include <iostream>​

#include <map>​

#include <string>​

using namespace std;​

​

int main() {​

    map<string, int> dict = {{"apple", 1}, {"banana", 2}};​

    for (auto it = dict.begin(); it != dict.end(); ++it)//等价于for(auto e:dict)
    {​

        cout << it->first << ": " << it->second << endl;​

    }​

    return 0;​

}​
  • 范围 for:C++11 引入的范围 for 循环,为数组和容器的遍历操作带来了革命性的改变。它无需开发者手动编写复杂的迭代器初始化、条件判断和迭代器递增代码,而是自动完成迭代、数据提取和结束条件判断等操作,使代码更加简洁明了,同时也降低了因手动编写迭代逻辑而导致的错误风险。
cpp 复制代码
int main(){
    int array[] = {1, 2, 3, 4, 5};​

    for (auto& e : array) {​

        e *= 2;​

    }​

    for (int num : array) {​

        cout << num << " ";​

    }​
    return 0;
}

上述代码中,第一个范围 for 循环通过引用的方式遍历数组array,对每个元素进行乘以 2 的操作;第二个范围 for 循环则以值传递的方式遍历数组,并输出每个元素的值。​

2.3 常用接口说明​

  • size()和length()这两个成员函数的功能都是返回字符串中有效字符的数量,二者在本质上没有区别,size()的设计是为了与其他标准容器的接口保持一致性,方便开发者统一使用。
cpp 复制代码
size_t size()const {
    return _size;
}

int mian(){ 
    string s;
    s.size();//返回类型为size_t
    return 0;
}
  • empty()函数用于判断字符串是否为空,即有效字符个数是否为 0,它返回一个布尔值,若为空则返回true,否则返回false。
cpp 复制代码
bool empty()const {
    if (_size==0)
    {
        return true;
    }
    return false;
}
int main(){
    string s;
    s.empty();//直接检查size
    return0;
}
  • clear()函数可以清空字符串中的有效字符,但并不会释放底层的内存空间,只是将字符串的长度置为 0,后续向字符串中添加字符时,若空间足够,无需重新分配内存。
cpp 复制代码
void clear() {
    _size = 0;
    _str[0] = '\0';
}
int main(){
    string s;
    s.clear();
    return 0;
}
  • reserve()函数用于预留指定大小的内存空间,它能够提前为字符串可能的增长做好准备,避免在频繁添加字符时因内存重新分配而带来的性能开销。
cpp 复制代码
void reserve(size_t n) {
    if (n > _capacity)
    {
        char* tmp = new char[n + 1];
        if (_str != nullptr)
        {
            memcpy(tmp, _str, _size + 1);
            delete[] _str;
        }
        _str = tmp;
        _capacity = n;
    }
}
  • resize()函数则用于改变字符串的有效字符个数,当指定的新长度大于当前长度时,会在字符串末尾按需填充指定字符(默认为'\0'),若新长度小于当前长度,则会截断字符串。
cpp 复制代码
void resize(size_t n, char c='\0') {
    if (n != _size)
    {
        if (n < _size)
        {
            _str[n + 1] = '\0';
            _size = n;
        }
        else
        {
            for (size_t i = _size; i < n; i++)
            {
                push_back(c);
            }
        }
    }
}

cpp 复制代码
string str = "hello";​

cout << "Size: " << str.size() << endl;​

cout << "Length: " << str.length() << endl;​

cout << "Is empty? " << (str.empty()? "Yes" : "No") << endl;​

str.reserve(20);​

str.resize(10, 'a');​

cout << "Resized str: " << str << endl;​
  • 访问及遍历操作:operator[]操作符为访问字符串中指定位置的字符提供了便捷方式,它与访问数组元素的方式类似,通过索引即可获取对应位置的字符。begin()和end()函数分别返回指向字符串首字符和尾后字符的迭代器,利用这两个迭代器,可以使用传统的for循环对字符串进行遍历;rbegin()和rend()函数则返回反向迭代器,用于从字符串末尾开始向前遍历;而 C++11 的范围 for 循环,更是以一种简洁优雅的方式实现了字符串的遍历。
cpp 复制代码
string str = "world";​

for (size_t i = 0; i < str.size(); ++i) {​//下标[]访问
    cout << str[i];​
}​
cout << endl;​

for (string::iterator it = str.begin(); it != str.end(); ++it) {​//迭代器
    cout << *it;​
}​
cout << endl;​

for (auto c : str) {​//范围for
    cout << c;​
}​

cout << endl;​
  • 修改操作:push_back、append和operator+=都可以实现向字符串末尾追加内容的功能。push_back函数用于添加单个字符,append函数可以追加字符串常量、string对象等多种形式的内容,而operator+=操作符使用起来最为灵活和广泛,它可以直接与字符串常量、string对象进行拼接。c_str()函数返回一个以'\0'结尾的 C 风格字符串,常用于将string对象传递给需要 C 风格字符串参数的函数。
cpp 复制代码
void string::push_back(char c) {
    if (_capacity == _size)
    {
       size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
        reserve(newcapacity);
    }
    _str[_size++] = c;
    _str[_size] = '\0';
}

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

void append(const char* str) {
    size_t len = strlen(str);
    if (_capacity < _size + len)
    {
        size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
        reserve(newcapacity);
    }
    memcpy(_str+_size, str, len + 1);
    _size += len;
}

string& operator+=(const char* str) {
    append(str);
    return *this;
}
  • find和rfind函数用于在字符串中查找指定字符或子串的位置,find是从字符串开头开始查找,rfind则是从字符串末尾开始查找,若查找成功,返回找到的位置索引,否则返回string::npos(一个表示不存在的特殊值);substr函数用于截取字符串中的子串,它可以指定起始位置和截取长度,返回截取得到的新字符串。
cpp 复制代码
    size_t string::find(const char* s, size_t pos ) const {
        assert(pos < _size);
        const char* p = strstr(_str,s);
        if (p == nullptr)
        {
            return npos;
        }
        return p - _str;
    }
cpp 复制代码
string str = "hello world";​

str.push_back('!');​

str.append(" How are you?");​

str += " I'm fine.";​

cout << "Modified str: " << str << endl;​

​

size_t pos = str.find("world");​

if (pos != string::npos) {​

cout << "Found 'world' at position: " << pos << endl;​

}​

​

string sub = str.substr(6, 5);​

cout << "Substring: " << sub << endl;​
  • 非成员函数:operator>>和operator<<是针对string类进行的输入和输出运算符重载,通过它们可以方便地使用cin进行字符串输入,使用cout进行字符串输出;getline函数能够读取一整行字符串,包括其中的空格字符,它以换行符作为输入结束的标志,常用于获取用户输入的多行文本;关系运算符(如==、!=、<、>等)用于比较两个字符串的大小,比较规则是按照字典序逐个字符进行比较。

cpp 复制代码
std::ostream& operator<<(std::ostream& _cout, const string& s) {
    for (size_t i = 0; i < s._size; i++)
    {
        _cout << s[i];
    }
    return _cout;
}

std::istream& operator>>(std::istream& _cin, string& s) {
    char buf[256];
    char ch;
    size_t i = 0;
    ch=_cin.get();
    while (ch != ' ' && ch != '\n')
    {
        buf[i++] = ch;
        ch= _cin.get();
        if (i == 255)
        {
            buf[i] = '\0';
            s.append(buf);
            i = 0;
        }
    }
    if (i != 0)
    {
        buf[i] = '\0';
        s.append(buf);
    }
    return _cin;
}
cpp 复制代码
string input;​

cout << "Please enter a sentence: ";​

getline(cin, input);​

cout << "You entered: " << input << endl;​

​

string str1 = "apple";​

string str2 = "banana";​

if (str1 < str2) {​

    cout << str1 << " is less than " << str2 << endl;​

}​

2.4 vs 和 g++ 下 string 结构差异​

  • vs 下:在 Visual Studio(vs)环境下,32 位平台中string对象占用 28 字节的内存空间。其内部采用联合体(union)的设计方式,当字符串长度小于 16 时,会使用内部固定的字符数组来存储字符串内容,这种方式避免了频繁的堆内存分配,提高了存储效率;当字符串长度大于等于 16 时,则会从堆上动态开辟空间来存储字符串。此外,string对象中还包含用于记录字符串长度、堆上空间容量的字段,以及一个指向堆空间的指针,以便在需要时访问堆上的数据。
  • g++ 下:在 GNU C++ 编译器(g++)中,string对象仅占 4 字节,它通过写时拷贝(Copy-On-Write,简称 COW)技术来实现高效的内存管理。内部只有一个指向堆空间的指针,堆空间中除了存储字符串内容外,还包含空间总大小、字符串有效长度和引用计数等重要字段。引用计数用于记录当前有多少个string对象共享同一块堆内存,在对象创建和销毁时,通过增减引用计数来管理内存,只有当引用计数为 1 时,才真正释放堆内存,从而避免了不必要的内存复制操作,提高了程序的性能。

三、string 类的应用实践​

通过解决一系列实际问题,能够更加深入地掌握string类的使用技巧和特性。下面以几个经典问题为例进行详细分析:​

  • 仅仅反转字母:给定一个字符串,要求将其中的字母部分进行反转,而其他非字母字符保持原位不变。可以采用双指针法来解决该问题,定义两个指针,一个指向字符串的开头,一个指向字符串的末尾,然后向中间移动,当两个指针都指向字母时,交换它们所指向的字符,直到两个指针相遇,即可完成字母的反转。
cpp 复制代码
#include <iostream>​
#include <string>​
using namespace std;​

string reverseOnlyLetters(string s) {​
    int left = 0, right = s.size() - 1;​
    while (left < right) {​
        while (left < right &&!isalpha(s[left]))
            ++left;​
        while (left < right &&!isalpha(s[right])) 
            --right;​
        swap(s[left], s[right]);​
        ++left;​
        --right;​
    }​
    return s;​
}​

​

int main() {​
    string str = "ab-cd";​
    cout << reverseOnlyLetters(str) << endl;​
    return 0;​
}​
  • 找字符串中第一个只出现一次的字符:对于给定的字符串,需要找出其中第一个只出现一次的字符。可以利用一个数组来统计每个字符出现的次数,遍历字符串,将每个字符对应的数组元素计数加 1。然后再次遍历字符串,找到第一个计数为 1 的字符,即为所求。
cpp 复制代码
#include <iostream>​
#include <string>​
using namespace std;​
​
char firstUniqChar(string s) {​
    int count[256] = {0};​
    for (char c : s) {​
        ++count[c];​
    }​
    for (char c : s) {​
        if (count[c] == 1) {​
            return c;​
        }​
    }​
    return'';​
}​

​

int main() {​
    string str = "leetcode";​
    cout << firstUniqChar(str) << endl;​
    return 0;​
}​
  • 字符串里面最后一个单词的长度:给定一个字符串,其中包含多个单词,单词之间由空格分隔,要求计算最后一个单词的长度。可以使用rfind函数找到字符串中最后一个空格的位置,然后计算从该位置到字符串末尾的字符个数,即为最后一个单词的长度。若字符串中不存在空格,即只有一个单词,则整个字符串的长度就是最后一个单词的长度。
cpp 复制代码
#include <iostream>​
#include <string>​
using namespace std;​


int lengthOfLastWord(string s) {​
    size_t pos = s.rfind(' ');​
    if (pos == string::npos) {​
        return s.size();​
    }​
    return s.size() - pos - 1;​
}​

​

int main() {​
    string str = "Hello World";​
    cout << lengthOfLastWord(str) << endl;​
    return 0;​
}​
  • 验证一个字符串是否是回文:判断一个字符串是否为回文,即正序和倒序读取该字符串时内容完全相同。首先将字符串中的字母统一转换为大写或小写,消除大小写差异对判断的影响。然后使用双指针法,一个指针从字符串开头,一个指针从字符串末尾,向中间移动并比较对应位置的字符,若在移动过程中发现字符不相等,则该字符串不是回文,否则是回文。
cpp 复制代码
#include <iostream>​
#include <string>​
#include <cctype>​
using namespace std;​

bool isPalindrome(string s) {​
    int left = 0, right = s.size() - 1;​
    while (left < right) {​
        while (left < right &&!isalnum(s[left])) 
            ++left;​
        while (left < right &&!isalnum(s[right])) 
            --right;​
        if (tolower(s[left])!= tolower(s[right])) {​
            return false;​
        }    ​
        ++left;​
        --right;​
    }​
    return true;​
}​

​

int main() {​
    string str = "A man, a plan, a canal: Panama";​
    cout << (isPalindrome(str)? "Yes" : "No") << endl;​
    return 0;​
}​
  • 字符串相加:给定两个字符串形式的非负整数,要求将它们相加并以字符串形式返回结果。由于字符串中的数字是按位存储的,所以可以从后往前逐位相加,同时处理进位问题。在相加过程中,若某一位的和大于等于 10,则将进位记录下来,加到下一位的计算中。最后将计算得到的结果反转,得到正确的相加结果。
cpp 复制代码
string addStrings(string num1, string num2) {
    string sub="";
    sub.reserve(max(num1.size(),num2.size())+1);
    string::reverse_iterator it1=num1.rbegin();
    string::reverse_iterator it2=num2.rbegin();
    int next=0;
    while(it1!=num1.rend()||it2!=num2.rend())
    {
        char a='0';
        char b='0';
        if(it1!=num1.rend())
            a=*it1;
        if(it2!=num2.rend())
            b=*it2;
        int tmp=a-'0'+b-'0'+next;
        sub+=tmp%10+'0';
        next=tmp/10;
        if(it1!=num1.rend())
            it1++;
        if(it2!=num2.rend())
            it2++;
        }
        if(next==1)
        {
            sub+='1';
        }
        reverse(sub.begin(), sub.end());
        return sub;
    }

四、string 类的模拟实现

4.1 经典问题:浅拷贝与深拷贝

在 C++ 中,当一个类涉及资源管理(如string类管理字符数组资源)时,如果没有显式定义拷贝构造函数和赋值运算符重载,编译器会合成默认版本。这些默认版本采用浅拷贝方式,即简单地将对象中的值逐位拷贝过来。这就导致多个对象会共享同一份资源,比如两个string对象指向同一块字符数组内存。当其中一个对象销毁时,它会释放这块资源,而其他对象并不知道资源已被释放,此时若继续对该资源进行操作,就会引发访问违规错误,程序可能崩溃。

例如,假设有如下简单的String类(为与标准库区分,这里使用String):

cpp 复制代码
class String
{
public:
    String(const char* str = "")
    {
        if (nullptr == str)
        {
            assert(false);
            return;
        }
        _str = new char[strlen(str) + 1];
        strcpy(_str, str);
    }
    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

当进行拷贝构造时,如String s2(s1);,若未显式定义拷贝构造函数,编译器合成的默认拷贝构造函数会使s1s2共用同一块内存空间。当TestString函数结束,s2先被销毁,释放了共享的内存,此时s1_str就成了野指针,再销毁s1时就会出错。

为解决浅拷贝问题,需要采用深拷贝方式。深拷贝的核心思想是为每个对象独立分配资源,使它们之间不会因共享资源而产生多次释放导致的错误。就像每个孩子都有自己独立的玩具,不会因为争抢同一个玩具而出现问题。

4.2 传统版与现代版实现

传统版

传统版的string类实现中,构造函数负责为字符串分配足够的内存空间并复制内容。如果传入的指针为空,通过assert(false)来断言程序出错(因为空指针在这种情况下不符合预期),然后返回。拷贝构造函数同样为新对象分配内存,并将源对象的字符串内容复制过去,保证每个对象都有自己独立的字符串副本。

赋值运算符重载函数首先判断是否是自我赋值(this != &s),如果不是,则先为新内容分配内存,复制源对象的字符串,再释放旧的内存,最后返回当前对象的引用,以支持链式赋值操作。析构函数则负责释放对象所占用的内存资源,确保内存不泄漏。

cpp 复制代码
class String
{
public:
    String(const char* str = "")
        :_size(strlen(str))
    {
        _str = new char[_size + 1];
        memcpy(_str, str, _size + 1);
        _capacity = _size;
    }
    String(const String& s)
        : _str(new char[s._size + 1])
    {
        memcpy(_str, s._str,s._size+1);
    }
    String& operator=(const String& s)
    {
        if (this != &s)
        {
            char* pStr = new char[s._size + 1];
            memcpy(pStr, s._str,s._size+1);
            delete[] _str;
            _str = pStr;
        }
        return *this;
    }
    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

现代版

现代版的构造函数与传统版类似,确保在传入空指针时进行合理处理,并正确分配和初始化字符串内存。拷贝构造函数先将_str初始化为nullptr,然后创建一个临时的String对象strTmp,用源对象的字符串初始化它。接着通过swap函数交换当前对象和临时对象的_str指针,这样当前对象就拥有了源对象字符串的独立副本,而临时对象在离开作用域时会自动销毁其原来指向的内存,避免了内存泄漏。

赋值运算符重载函数采用了一种巧妙的方式,它直接将参数对象作为值传递进来(这会触发参数对象的拷贝构造函数,创建一个副本),然后通过swap函数交换当前对象和副本对象的_str指针。这样,原来的内容会被副本对象带走,当副本对象离开作用域时会自动释放,而当前对象则拥有了新的字符串内容。这种方式不仅实现了深拷贝,还简化了代码,提高了效率。最后同样通过析构函数释放对象占用的内存。

cpp 复制代码
class String
{
public:
    String(const char* str = "")
        :_size(strlen(str))
    {
        _str = new char[_size + 1];
        memcpy(_str, str, _size + 1);
        _capacity = _size;
    }

    template <class InputIterator>
    string(InputIterator first, InputIterator last) {
        while (first!=last)
        {
            *this += *first;
            first++;
        }
    }

    String(const String& s)
    {
        string tmp(s.begin(), s.end());
        swap(tmp);
    }

    String& operator=(String s)
    {
        if (&s != this)
        {
            string tmp(s.begin(), s.end());
            swap(tmp);
        }
        return *this;
    }

    ~String()
    {
        if (_str)
        {
            delete[] _str;
            _str = nullptr;
        }
    }
private:
    char* _str;
};

对比传统版和现代版的赋值运算符重载,现代版利用了参数的传值拷贝和swap操作,代码更简洁,效率也有所提升。因为传统版需要先分配新内存、复制内容、释放旧内存,而现代版通过一次swap操作就完成了赋值,减少了内存分配和复制的次数。

4.3 写时拷贝(了解)

写时拷贝是一种在浅拷贝基础上优化的策略,它引入了引用计数机制。引用计数用于记录资源被多少个对象使用。在对象构造时,将资源的引用计数初始化为 1,表示当前只有一个对象使用该资源。当有新的对象需要使用同一资源时,引用计数增加 1,此时这些对象共享该资源,采用浅拷贝的方式进行赋值,提高了效率,因为避免了频繁的内存分配和复制操作。

当某个对象被销毁时,它会先将引用计数减 1。然后检查引用计数的值,如果计数变为 0,说明该对象是资源的最后一个使用者,此时才释放该资源;如果计数不为 0,说明还有其他对象在使用该资源,不能释放,从而避免了多次释放同一资源的错误。

不过,写时拷贝在读取时存在一些缺陷。由于多个对象共享同一份资源,当一个对象对资源进行写操作时,其他对象感知不到这种变化。如果在读取过程中,资源被其他对象修改,可能会导致读取到不一致的数据。例如,多个string对象共享一块字符数组,其中一个对象通过operator[]修改了某个字符,其他正在读取该字符数组的对象可能会读到修改后的值,这可能不符合程序的预期逻辑。

在实际应用中,写时拷贝适用于读操作频繁、写操作较少的场景,可以有效提高性能。但在对数据一致性要求较高,写操作较为频繁的情况下,需要谨慎使用,或者结合其他机制来保证数据的正确性。

五、总结

C++ 的string类功能强大,掌握其基本使用、常用接口、底层结构和模拟实现,能让开发者在处理字符串相关问题时更加得心应手。无论是在算法竞赛、日常开发还是面试中,对string类的深入理解都将成为你的有力武器。希望大家通过不断实践,熟练运用string类,编写出更高效、更健壮的代码。

附件:模拟实现string常用函数

cpp 复制代码
//string.h
#pragma once
#include<iostream>
#include<string.h>
#include<assert.h>
namespace _string
{

    class string
    {

        friend std::ostream& operator<<(std::ostream& _cout, const string& s);

        friend std::istream& operator>>(std::istream& _cin, string& s);

    public:

        typedef char* iterator;
        typedef const char* const_iterator;

    public:

        string(const char* str = "");

        template <class InputIterator>
        string(InputIterator first, InputIterator last);

        string(const string& s);

        ~string();

        string& operator=(const string& s);

        iterator begin();

        iterator end();

        const_iterator begin()const;

        const_iterator end()const;

        void push_back(char c);

        string& operator+=(char c);

        void append(const char* str);

        string& operator+=(const char* str);

        void clear();

        void swap(string& s);

        const char* c_str()const;

        size_t size()const;

        size_t capacity()const;

        bool empty()const;

        void resize(size_t n, char c = '\0');

        void reserve(size_t n);

        char& operator[](size_t index);

        const char& operator[](size_t index)const;

        bool operator<(const string& s) const;

        bool operator<=(const string& s)const;

        bool operator>(const string& s)const;

        bool operator>=(const string& s)const;

        bool operator==(const string& s)const;

        bool operator!=(const string& s)const;

        size_t find(char c, size_t pos = 0) const;

        size_t find(const char* s, size_t pos = 0) const;

        string& insert(size_t pos, char c);

        string& insert(size_t pos, const char* str);

        string& erase(size_t pos, size_t len);


    private:

        char* _str;

        size_t _capacity;

        size_t _size;

    public:
        static const size_t npos = -1;

    };
    std::ostream& operator<<(std::ostream& _cout, const string& s);

    std::istream& operator>>(std::istream& _cin, string& s);
};
cpp 复制代码
//string.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"_string.h"
namespace _string

{
    string::string(const char* str ) 
        :_size(strlen(str))
    {
        _str = new char[_size + 1];
        memcpy(_str, str, _size + 1);
        _capacity = _size;
    }

    template <class InputIterator>
    string::string(InputIterator first, InputIterator last) {
        while (first!=last)
        {
            *this += *first;
            first++;
        }
    }

    string::string(const string& s) {
        string tmp(s.begin(), s.end());
        swap(tmp);
    }
    string::~string() {
        if (this != nullptr)
        {
            delete[] _str;
        }
        _size = _capacity = 0;
    }

    string& string::operator=(const string& s) {
        if (&s != this)
        {
            string tmp(s.begin(), s.end());
            swap(tmp);
        }
        return *this;
    }


    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::push_back(char c) {
        if (_capacity == _size)
        {
            size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
            reserve(newcapacity);
        }
        _str[_size++] = c;
        _str[_size] = '\0';
    }

    string& string::operator+=(char c) {
        push_back(c);
        return *this;
    }
    //void string::append(const char* str) {
    //    for (int i = 0; str[i] != '\0';i++)
    //    {
    //        push_back(str[i]);
    //    }
    //}
    void string::append(const char* str) {
        size_t len = strlen(str);
        if (_capacity < _size + len)
        {
            size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
            reserve(newcapacity);
        }
        memcpy(_str+_size, str, len + 1);
        _size += len;
    }

    string& string::operator+=(const char* str) {
        append(str);
        return *this;
    }

    void string::clear() {
        _size = 0;
        _str[0] = '\0';
    }

    void string::swap(string& s) {
        std::swap(_str, s._str);
        std::swap(_size, s._size);
        std::swap(_capacity, s._capacity);

    }

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

    size_t string::size()const {
        return _size;
    }

    size_t string::capacity()const {
        return _capacity;
    }

    bool string::empty()const {
        if (_size==0)
        {
            return true;
        }
        return false;
    }

    void string::resize(size_t n, char c) {
        if (n != _size)
        {
            if (n < _size)
            {
                _str[n + 1] = '\0';
                _size = n;
            }
            else
            {
                for (size_t i = _size; i < n; i++)
                {
                    push_back(c);
                }
            }
        }
    }

    void string::reserve(size_t n) {
        if (n > _capacity)
        {
            char* tmp = new char[n + 1];
            if (_str != nullptr)
            {
                memcpy(tmp, _str, _size + 1);
                delete[] _str;
            }
            _str = tmp;
            _capacity = n;
        }
    }

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

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


    bool string::operator<(const string& s) const {
        size_t l1 = _size;
        size_t l2 = s._size;
        size_t p1 = 0;
        size_t p2 = 0;
        while (p1 < l1 && p2 < l2)
        {
            if (_str[p1] > s._str[p2])
            {
                return false;
            }
            else if (_str[p1] < s._str[p2])
            {
                return true;
            }
            p1++;
            p2++;
        }
        //if (p1 == l1 && p2 < l2)
        //{
        //    return false;
        //}
        //else
        //{
        //    return true;
        //}
        return p1 == l1 && p2 < l2;
    }

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

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

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

    bool string::operator==(const string& s) const {
        size_t l1 = _size;
        size_t l2 = s._size;
        size_t p1 = 0;
        size_t p2 = 0;
        while (p1 < l1 && p2 < l2)
        {
            if (_str[p1] != s._str[p2])
            {
                return false;
            }
            p1++;
            p2++;
        }
        return l1 == p1 && l2 == p2;
    }

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

    size_t string::find(char c, size_t pos) const {
        assert(pos < _size);
        for (size_t i = pos; i < _size; i++)
        {
            if (_str[i] == c)
            {
                return i;
            }
        }
        return npos;
    }

    size_t string::find(const char* s, size_t pos ) const {
        assert(pos < _size);
        const char* p = strstr(_str,s);
        if (p == nullptr)
        {
            return npos;
        }
        return p - _str;
    }

    string& string::insert(size_t pos, char c) {
        size_t end = _size + 1;
        if (_capacity == _size)
        {
            size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
            reserve(newcapacity);
        }
        while (end > pos)
        {
            _str[end] = _str[end - 1];
            end--;
        }
        _str[pos] = c;
        ++_size;
        return *this;
        
    }

    string& string::insert(size_t pos, const char* str) {
        size_t len = strlen(str);
        if (_capacity < _size + len)
        {
            size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;
            reserve(newcapacity);
        }
        size_t end = _size + len + 1;
        while (end > pos + len-1)
        {
            _str[end] = _str[end - len];
            end--;
        }
        memcpy(_str + pos, str, len);
        _size += len;
        return *this;
    }

    string& string::erase(size_t pos, size_t len) {
        assert(pos < _size);
        if (len == npos || len >= _size - pos)
        {
            _str[pos] = '\0';
            _size = pos;
        }
        else
        {
            memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));
            _size -= len;
        }
    }
    std::ostream& operator<<(std::ostream& _cout, const string& s) {
        for (size_t i = 0; i < s._size; i++)
        {
            _cout << s[i];
        }
        return _cout;
    }

    std::istream& operator>>(std::istream& _cin, string& s) {
        char buf[256];
        char ch;
        size_t i = 0;
        ch=_cin.get();
        while (ch != ' ' && ch != '\n')
        {
            buf[i++] = ch;
            ch= _cin.get();
            if (i == 255)
            {
                buf[i] = '\0';
                s.append(buf);
                i = 0;
            }
        }
        if (i != 0)
        {
            buf[i] = '\0';
            s.append(buf);
        }
        return _cin;
    }
};
相关推荐
苏三福6 分钟前
ros2 hunmle bag 数据包转为图片数据 python版
开发语言·python·ros2humble
qqxhb1 小时前
零基础学Java——第十一章:实战项目 - 桌面应用开发(JavaFX入门)
java·开发语言·javafx
大神薯条老师1 小时前
Python零基础入门到高手8.4节: 元组与列表的区别
开发语言·爬虫·python·深度学习·机器学习·数据分析
z人间防沉迷k1 小时前
堆(Heap)
开发语言·数据结构·笔记·python·算法
hy.z_7771 小时前
【数据结构】链表 LinkedList
java·数据结构·链表
不二狗1 小时前
每日算法 -【Swift 算法】Two Sum 问题:从暴力解法到最优解法的演进
开发语言·算法·swift
ROCKY_8171 小时前
数据结构——例题3
数据结构
炯哈哈2 小时前
【上位机——WPF】Window标签常用属性
开发语言·c#·wpf·上位机
ROCKY_8172 小时前
数据结构——例题2
数据结构
小白学大数据2 小时前
Python爬虫如何应对网站的反爬加密策略?
开发语言·爬虫·python