内置类string常用接口及模拟实现【C++】【STL】【附题】

简介: 内置类string常用接口及模拟实现【C++】【STL】【附题】

1. 标准库类型string

标准库类型string表示可变长字符序列,也就是我们所说的字符串。string包含在std命名空间中,且必须在使用前包含头文件。

1.2 常用接口

string作为一种新类型,相较于C中的字符数组,加之以C++中访问对象中的各种功能函数,在操作上有了许多方便。

1.2.1 *构造接口

下面使用几个例子了解string类构造对象的各种方法:

#include <iostream>
#include <string>
using namespace std;
int main()
{
    string s1 = "hello";
    string s2 = s1;
    string s3("world");
    string s4(s3);
    string s5(5,'a');
    cout << s1 << endl;
    cout << s2 << endl;
    cout << s3 << endl;
    cout << s4 << endl;
    cout << s5 << endl;
    return 0;
}

结果:

hello

hello

world

world

aaaaa

  • s1和s3是等价的,都是以一个常量字符串初始化对象,创建一个副本,可以认为赋值符号右边的字符串是一个匿名对象;
  • s2和s4是等价的,都是拷贝构造;
  • s5是被初始化为5个连续的a

【注意】

C++为了兼容C,在字符串最后会添加一个\0,即使是空串:

void test2()
{
    string s = "";
}
int main()
{
    test2();
    return 0;
}

通过调试可以看到,s中的内容是\0

1.2.2 功能接口

长度or容量
void test3()
{
    string s = "hello";
    cout << s.size() << endl;
    cout << s.length() << endl;
    cout << s.capacity() << endl;
}
int main()
{
    test3();
    return 0;
}

结果:

5

5

22

  • size&&length:返回字符串有效字符长度。它们没有什么不同,只是名字上的差别,因为size的意思更有“个数”的意思,所以一般使用size;
  • capacity:string类对象的总空间的大小,即容量。在这里,默认容量是22个字节(char);
其他
void test4()
{
    string s1 = "hello";
    string s2;
    cout << "s1 isEmpty?" << s1.empty() << endl;
    cout << "s2 isEmpty?" <<  s2.empty() << endl;
    s1.clear();
    cout << "s1 isEmpty?" <<  s1.empty() << endl;
    cout << "s1 capacity:" << s1.capacity() << endl;
    s1.reserve(25);
    cout << "s1 capacity:" <<  s1.capacity() << endl;
    string s3 = "world";
    s3.resize(8,'a');
    cout << s3 << endl;
}
int main()
{
    test4();
    return 0;
}

结果:

s1 isEmpty?0

s2 isEmpty?1

s1 isEmpty?1

s1 capacity:22

s1 capacity:31

worldaaa

  • empty():是空串则返回true,否则返回false;
  • clear():清空字符串(有效字符);
  • reserve(size_t n):;为字符串预留空间(有效字符);
  • resize(size_t n, char c):将有效字符改成n个,多余的空间用字符c填充。

【注意】

  • clear()不会改变capacity;
  • resize(size_t n)和resize(size_t n, char c),如果n比capacity大,会自动扩容,增大capacity;如果n比capacity小,则不会改变capacity;
  • reserve()同上。

1.2.3 访问及遍历

operator[]

实际使用直接像数组一样使用[]即可。这里仅强调是重载过的。

void test5()
{
    string s1 = "hello";
    for(int i = 0; i < s1.size(); i++)
    {
        cout << s1[i] << " ";
    }
}
int main()
{
    test5();
    return 0;
}

结果:

h e l l o

[i]

作用:返回i位置的字符,const string类对象的调用。

begin()&&end()
void test5()
{
    string s1 = "hello";
    std::basic_string<char>::iterator i = s1.begin();
    for(;i < s1.end(); i++)
    {
        cout << *i << " ";
    }
}

结果:

h e l l o

begin()和end()都是迭代器,暂时把它当做指针,在STL会学习他。

  • begin():获取第一个字符的迭代器;
  • end():获取最后一个字符的下一个位置的迭代器。

1.2.4 修改操作

push_back(char c)、append(const char* s)、operator+=
void test6()
{
    string s1 = "hell";
    s1.push_back('o');
    cout << s1 << endl;
    s1.append(" world");
    cout << s1 << endl;
    s1 += "!!!";
    cout << s1 << endl;
}
int main()
{
    test6();
    return 0;
}

结果:

hello

hello world

hello world!!!

  • push_back(char c):在字符串后尾插字符c;
  • append(const char* s):在字符串后追加一个新的字符串s;
  • operator+=:在字符串后追加字符串或字符。

【注意】

  • 追加字符时,三者都可,值得注意的是append(),需要写成:append(1,c);为了方便,常使用+=。
  • 为了合理利用空间,可以使用reserve提前为字符串预留空间。
c_str
void test7()
{
    string s = "hello";
    const char* c = s.c_str();
    cout << c << endl;
}
int main()
{
    test7();
    return 0;
}

结果:

hello

c_str(),返回一个指向数组的指针,该数组包含以空字符结尾的字符序列(即 C 字符串),表示字符串对象的当前值。此数组包含构成字符串对象值的相同字符序列,以及末尾的附加终止空字符 ( \0)。

【注意】

  • 此函数只能获取对应的字符串;
  • 返回的字符串是被const修饰的,原因是c_str()本身被const修饰。
npos、find、rfind
void test8()
{
    string s1 = "hello";
    cout << s1.find('o') << endl;
    cout << s1.find('o', 5) << endl;
    cout << s1.find('p') << endl;
    cout << string::npos << endl;
}
int main()
{
    test8();
    return 0;
}

结果:

4

18446744073709551615

18446744073709551615

18446744073709551615

  • find(char c, size_t pos):从字符串下标pos位置往后开始,往后查找c字符,找到则返回下标,否则返回npos;rfind则从下标pos位置往前开始;
  • npos:这是一个定义在string类内部的公有静态成员变量,它的值是-1,类型是size_t,也就是正数;按无符号整型打印-1,这是一个很大的数。

size_t:即unsigned int或unsigned long。在底层实现中,所有下标都是size_t类型,因为下标不可能为负数。为了统一,接收下标使用的临时变量尽量也使用size_t定义。

substr
void test9()
{
    string s1 = "hello";
    cout << s1.substr(1,7) << endl;
}
int main()
{
    test9();
    return 0;
}

结果:

ello

  • substr(size_t pos, size_t n):在字符串中从下标pos位置开始,往后截取含有n个字符的子串并返回。

1.2.5 string类非成员函数

operator+
void test10()
{
    string s1 = "hello";
    string s2 = s1 + " world";
    cout << s2 << endl;
}
int main()
{
    test10();
    return 0;
}
  • operator+能实现简单的字符串拼接,包括字符、字符串、string对象。

【注意】

operator+的底层实现是传值返回,效率较低,在模拟实现string类时会了解。

operator>>、operator<<
void test11()
{
    string s1;
    cin >> s1;
    cout << s1 << endl;
}
int main()
{
    test11();
    return 0;
}

输入:

hello

输出:

hello

  • 其实前面一直在使用重载的输入和输出运算符,作用就和它本身一样,将字符串读取或写入内存中。
getline()
void test12()
{
    string s1 = "hello";
    getline(cin, s1);
    cout << s1 << endl;
}
int main()
{
    test12();
    return 0;
}

输入:

world

输出:

world

  • getline(istream& is, string& str):获取一行字符串。从输入流istream is中提取字符并储存在字符串str中,然后返回istream类型的引用,以判断功能是否正常。
比较大小
void test13()
{
    string s1 = "hello";
    string s2 = "world";
    if(s1 == s2)
        cout << "s1 == s2" << endl;
    if(s1 < s2)
        cout << "s1 < s2" << endl;
    if(s1 > s2)
        cout << "s1 > s2" << endl;
    if(s1 >= s2)
        cout << "s1 >= s2" << endl;
    if(s1 <= s2)
        cout << "s1 <= s2" << endl;
    if(s1 != s2)
        cout << "s1 != s2" << endl;
}
int main()
{
    test13();
    return 0;
}

结果:

s1 < s2

s1 <= s2

s1 != s2

  • 这些比较大小的操作符都是比较字符串字符的ascii值,直到不同的就返回结果。类似C语言中的strcmp函数。

2. 练习

2.1 仅仅反转字母

【思路】

和字符串反转思路类似,在限制下标的同时,在字符串的头和尾分别移动,如果遇到字母则停止,然后交换。

【注意】

  • 接收下标的临时变量最好使用size_t类型;
  • 在循环内进行查找操作时,用if只能移动一次。用循环必须在循环内再限制下标,否则可能会出现死循环;
  • 使用了标准库内置的swap()函数和isalpha()函数。

【代码】

class Solution {
public:
    string reverseOnlyLetters(string s) {
        if(s.empty())
            return s;
        size_t begin = 0, end = s.size() - 1;
        while(begin < end)
        {
            while(!isalpha(s[begin]) && begin < end)
                begin++;
            while(!isalpha(s[end]) && begin < end)
                end--;
            swap(s[begin], s[end]);
            begin++;
            end--;
        }
        return s;
    }
};

2.2 字符串中的第一个唯一字符

【思路】

类似计数排序的思路:时间换空间。新开一个数组,遍历字符串,为每个小写字母计数;然后再遍历一次字符串,如果字母出现的次数等于1,则返回下标,否则返回-1;

【注意】

  • 保持一致,依然使用size_t处理下标;
  • 以0-25为下标给26个小写“挖坑”的简便方法:用字母减去字母表中第一个字母a,即得到当前字母在字母表中的序号-1。
class Solution {
public:
    int firstUniqChar(string s) {
        int a[26] = { 0 };
        for(size_t i = 0; i < s.size(); i++)
        {
            a[s[i] - 'a']++;
        }
        for(size_t i = 0; i < s.size(); i++)
        {
            if(a[s[i] - 'a'] == 1)
                return i;
        }
        return -1;
    }
};

2.3 字符串最后一个单词的长度

【思路】

非常简单,得到字符串长度,从后计数,直到遍历完或者遇到空格停止。

【注意】

C++的标准输入流对象cin不支持输入字符串,因为空格或回车会被认为是分隔符。我们使用getline()函数代替cin>>s

【代码】

#include <iostream>
#include <string>
using namespace std;
int main() {
  string s;
  getline(cin, s);
  size_t end = s.size() - 1;
  int count = 1;
  while (end--)
  {
    if (s[end] != ' ')
    {
      count++;
    }
    else
      break;
  }
  cout << count << endl;
}

2.4 验证回文串

【思路】

根据题意,大写转小写,移除所有数字字符,言外之意这就是稍后要比较的前提。

思路简单,头尾同时遍历,分别遇到非字母字符就跳过,然后比较。相同则继续走,不同则返回false。

【注意】

  • 大写转小写:由于ASCII表中小写字母都比对应的大写字母多32,所以只需遍历+=32即可。
  • 同第一题,非字母字符迭代同样要用while,且要限制begin<end,避免出现死循环。如果最后错误有溢出,很有可能是出现了死循环。

【代码】

class Solution {
public:
    void toSmallAlpha(string& s)
    {
        for (int i = 0; i < s.size(); i++)
        {
            if (s[i] >= 'A' && s[i] <= 'Z')
                s[i] += 32;
        }
    }
    bool isNumberOrAlpha(char ch)
    {
        return isalpha(ch) || isdigit(ch);
    }
    bool isPalindrome(string s) {
        if (s.empty())
            return false;
        toSmallAlpha(s);
        int begin = 0, end = s.size() - 1;
        while (begin < end)
        {
            while (begin < end && !isNumberOrAlpha(s[begin]))
                begin++;
            while (begin < end && !isNumberOrAlpha(s[end]))
                end--;
            if (s[begin] == s[end])
            {
                begin++;
                end--;
            }
            else
                return false;
        }
        return true;
    }
};

2.5 字符串相加

【思路】

逆序遍历两个字符串,然后分别相加当前字符对应的数字,增加一个保存进位的临时变量tmp,用作下一次相加的对象之一。经过进位处理,得到当前数字对于的字符,并将它尾插到新字符串尾部,直到两个数字字符串都被遍历完毕。最后得到的字符串是倒序的,需要反转后再返回。

【注意】

  • 迭代的条件:至少还有一个没有遍历完,也就是所有数字字符都要遍历到;
  • 数字和它对应的字符相互转换的方法同字母;
  • 关于对进位的处理:搭配%和/使用,逻辑较简洁,但是这样效率可能不是很高;因为两个个位数相加最大值为18,所以最多进1,tmp为0和1两种状态。但是这样做需要考虑极端情况:999+1,最后一个进位无法尾插,所以在最后一次进位时尾插tmp。

【代码】

class Solution {
public:
    string addStrings(string num1, string num2) {
        int end1 = num1.size() - 1;
        int end2 = num2.size() - 1;
        string str;
        int tmp = 0, Num = 0, i = 0, Num1 = 0, Num2 = 0;
        while (end1 >= 0 || end2 >= 0)
        {
            if(end1 >= 0)
                 Num1 = num1[end1--] - '0';
            if(end2 >= 0)
                 Num2 = num2[end2--] - '0';
            tmp = tmp + Num1 + Num2;
            if (tmp < 10)
            {
                str.push_back(tmp + '0');
                tmp = 0;
            }
            else
            {
                str.push_back((tmp - 10) + '0');
                tmp = 1;
                if (end1 == -1 && end2 == -1)
                    str.push_back(tmp + '0');
            }
            Num1 = Num2 = 0;
        }
        reverse(str.begin(), str.end());
        return str;
    }
};

2.6 反转字符串 II

【思路】

从题目翻译过来的意思:每隔k个反转一次,如果剩余不够k个,就全部反转。

其实就是一个定长的区间内反转,然后移动固定距离。控制步长和步数,也就控制了控制了反转的区间。

【注意】

  • 如何让一个定长区间移动固定距离:让左右端点同时移动(步数*步长)个单位长度;
  • 除例子中的情况是反转长度为k的区间外,剩下的都是直接全部反转;由于第二种情况是最后一次反转,步长的累加放在第一种情况就好。

【代码】

class Solution {
public:
  //反转函数
    void reverseSubstr(string& s, int begin, int end)
    {
        while (begin < end)
        {
            swap(s[begin++], s[end--]);
        }
    }
    string reverseStr(string s, int k) {
        int num = s.size(), count = 0;
        int begin = 0, end = k - 1;//begin和end限制了步长
        while (num >= 0)
        {
            if (num < k)
            {
                reverseSubstr(s, begin + (2 * k) * count, s.size() - 1);
            }
            else
            {
                reverseSubstr(s, begin + (2 * k) * count, end + (2 * k) * count);
                count++;//统计步数
            }
            //num是剩余长度,每次用掉2k个
            num -= (2 * k);
        }
        return s;
    }
};

2.7 反转字符串中的单词 III

【思路】

正向反向遍历都可以,主要是确定反转的单词区间和控制下标不要越界。

反向遍历:用begin和end下标限制反转区间,首先将end置为最后一个元素下标,然后用循环找空格(假设有空格)的下标,空格后一个位置就是begin,以此类推。

【注意】

  • 需要考虑只有一个单词的字符串,即没有空格。在找空格的下标的循环中,要限制下标不越界,既避免了死循环,也能兼顾找不到空格的情况。

【代码】

class Solution {
public:
    void reverseSubstr(string& s, int begin, int end)
    {
        while (begin < end)
        {
            swap(s[begin++], s[end--]);
        }
    }
    string reverseWords(string s) {
        int cur = s.size() - 1;
        int begin = 0, end = 0;
        int pos = 0;
        while(cur >= 0)
        {
            end = cur;
            while(cur >= 0 && s[cur] != ' ')
            {
                cur--;
            }
            begin = cur + 1;
            reverseSubstr(s, begin, end);
            cur--;
        }
        return s;
    }
};

2.8 字符串相乘

【思路】

根据题意,是要返回一个新字符串,

【代码】

class Solution {
public:
    string multiply(string num1, string num2) {
        int len1 = num1.size(), len2 = num2.size();
        int len = len1 + len2;
        //一定要初始化新串的值,否则后面会出现随机值
        string ret(len, '0');
        for(int i = len1 - 1; i >= 0; i--)
        {
            for(int j = len2 - 1; j >= 0; j--)
            {
                //tmp是当前位之和
                int tmp = (num1[i] - '0') * (num2[j] - '0') + (ret[i + j + 1] - '0');
                //当前的最后一位,得
                ret[i + j + 1] = tmp % 10 + '0';
                ret[i + j] += tmp / 10;
            }
        }
        for(int i = 0; i < len; i++)
        {
            if(ret[i] != '0')
                return ret.substr(i);
        }
        return "0";
    }
};

2.9 找出字符串中第一个只出现一次的字符

【思路】

遍历字符串,使用内置类string的接口:find_first_of(char ch)和find_last_of(char ch),前者是找到ch第一次出现的位置,后者是找到ch最后一次出现的位置,找不到则返回npos(-1)。如果只出现一次,那么这两个接口的返回值都是相同的。另一种情况就是不存在只出现一次的字符,则在遍历完毕前最后一次打印-1。

【代码】

#include<iostream>
#include<string>
using namespace std;
int main(){
    string s;
    getline(cin, s);
    for(int i = 0; i < s.size(); i++)
    {
        if(s.find_first_of(s[i]) == s.find_last_of(s[i]))
        {
            cout << s[i] << endl;
            break;
        }
        if(i == s.size() - 1)
            cout << "-1" << endl;
    }
    return 0;
}

3. 模拟实现内置类string

了解接口的底层实现,能熟练且深刻地掌握string类各种常用接口的用法。而模拟实现某个类或接口,也是面试时常见的问题。

当然,内置类string有一百多个接口,在这里只实现最常用的,大部分接口都是被重载的。

为了区分内置类,取名为Mystring,除此之外,也可以在自定义命名空间内使用string作为自定义字符串类,以区分内置string类。

3.1 成员变量

string底层是一个能自动扩容的字符数组,所以需要有一个变量记录容量和字符的个数。除此之外,还有最重要的字符数组,它是被new出来的。

  • 容量:size_t _capacity;
  • 数量:size_t _size;
  • 字符数组:char* str;
  • npos:static const size_t npos = -1;

许多容器都包含npos,它表示“找不到”的情况的返回值。既然如此,npos是公有静态成员变量。

private:
    size_t _size;
    size_t _capacity;
    char* _str;
public:
      const static size_t npos = -1;

3.2 *构造函数&&析构函数

  • 构造函数:由于底层是一个动态数组所以使用new动态分配内存,之所以不使用malloc,是为了方便;
  • 析构函数:相对地,使用delete释放new分配的内存,并将容量和数量计数器置零,指针置空。

浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。

//构造函数
Mystring(const char* str = "")
{
    _size = strlen(str);
    _capacity = _size;
    _str = new(char[_capacity + 1]);
    strcpy(_str, str);
}
//析构函数
~Mystring()
{
    delete[] _str;
    _size = 0;
    _str = nullptr;
    _capacity = 0;
}

【注意】

这里的构造函数给str以缺省值"",是为了符合内置string默认空串为\0的做法。除此之外,申请内存的大小比_capacity多1,就是为了保存这个\0

值得注意的是,_size即个数,不包含末尾的\0

构造函数中的初始化列表不是必要的,而且它可能会出现初始化顺序的问题。

【另辟蹊径】

向系统申请内存,然后将字符拷贝到内存中,这样做是符合常理且传统的,有的人提出了新的方法:直接使用传入的字符串,交换它们成员变量的值,以达到构造Mystring对象的目的。

由于稍后还要使用这里的思路,所以将交换功能封装为一个函数。

void swap(Mystring& tmp)
{
    std::swap(tmp._capacity, _capacity);
    std::swap(tmp._size, _size);
    std::swap(tmp._str, _str);
}
Mystring(const Mystring& str)
{
  string tmp(str._str);
  swap(tmp);
}

3.3 operator[]

  • operator[]:重载[]操作符,是为了让使用Mystring能像使用数组一样自然。

【注意】

  • 接收下标的类型最好为size_t,下面将不再赘述;
  • 返回值为引用类型的原因:a. 为了像数组一样读和写;b. 减少传值拷贝在性能上的消耗;
  • 需要判断下标是否越界,由于pos是size_t类型,只需要判断其小于_size即可;
  • 运算符重载,还需要有被const修饰的版本,目的是配合用户需要,返回值只能读不能写,下面将不再赘述。
//返回引用是为了读写
char& operator[](size_t pos)
{
    assert(pos < _size);
    return _str[pos];
}
//加const是只读
const char& operator[](size_t pos) const
{
    assert(pos < _size);
    return _str[pos];
}

3.4 迭代器

迭代器在初学STL会学习到,在这里只需要把它当做指针即可。

【注意】

区间是左闭右开。

//迭代器(begin、end)
//可读可写
iterator begin()
{
    return _str;
}
iterator end()
{
    return _str + _size;
}
//只读
const_iterator begin() const
{
    return _str;
}
const_iterator end() const
{
    return _str + _size;
}

3.5 size()、capacity()

  • 获取元素个数和容量。

【注意】

个数和容量均不包括\0

size_t size()
{
    return _size;
}
size_t capacity()
{
    return _capacity;
}

3.6 =、+=、+

  • +=、+:相当于尾插;
  • =:赋值,相当于拷贝构造。

【注意】

  • 返回值都是引用类型;
  • 深浅拷贝的解释在下面。
//默认=,浅拷贝
// Mystring& operator=(const Mystring& s)
// {
//     if (this != &s)
//     {
//         char* pStr = new char[strlen(s._str) + 1];
//         strcpy(pStr, s._str);
//         delete[] _str;
//         _str = pStr;
//     }
//     return *this;
// }
//新=,深拷贝
Mystring& operator=(Mystring& s)
{
    swap(s);
    return *this;
}
Mystring& operator+=(const char* s)
{
    append(s);
    return *this;
}
Mystring& operator+=(char* s)
{
    append(s);
    return *this;
}
Mystring& operator+(const char* s)
{
    append(s);
    return *this;
}
Mystring& operator+(char* s)
{
    append(s);
    return *this;
}

3.7 *拷贝构造【深浅拷贝】

  • 对于内置类型:深浅拷贝都是直接拷贝;
  • 对于自定义类型:浅拷贝只拷贝它们的地址,深拷贝则完全复制一份,地址不同。

浅拷贝出现的问题就是它们指向同一个空间,所以需要在构造函数中开一个新空间(可以使用初始化列表)。

使用默认的=,实现赋值也是粗暴的浅拷贝,首先会出现内存泄漏,原来的地址被覆盖了,其次还是刚才的问题。如果想直接拷贝值,这样做看起来简单,实际上反而麻烦,因为两个空间的大小会影响太小或太大。

  • 传统写法:先释放原空间,然后让它再指向新空间,记得\0,然后拷贝,最后更新容量。要允许自身赋值,通过判断地址。
  • 新写法:要是不想改变原有数据,创建新的临时对象,然后使用swap构造新对象。

3.8 比较运算符重载

  • 各种比较运算符重载,只需要实现其中的两个,剩下的复用即可。但在这里我是直接使用strcmp()。
  • 其中比较的是第一个不相同的字符的ASCII值。
bool operator>=(const Mystring& s) const
{
    return !(strcmp((*this)._str, s._str) < 0);
}
bool operator<=(const Mystring& s) const
{
    return !(strcmp((*this)._str, s._str) > 0);
}
bool operator==(const Mystring& s) const
{
    return strcmp((*this)._str, s._str) == 0;
}
bool operator>(const Mystring& s) const
{
    return strcmp((*this)._str, s._str) > 0;
}
bool operator<(const Mystring& s) const
{
    return strcmp((*this)._str, s._str) < 0;
}

3.9 reserve()

这个接口在上面已经提到过。它的功能就是管理容量,传入的参数就是要保存的容量。

  • 增容:如果要保存的容量比本身的容量还大,那就是扩容,思路同构造函数;
  • 减容:由于内置string类判断字符串终止的标志是\0,只需将它放在最后一个位置,然后更新_size。
//更改容量
void reserve(size_t n)
{
    if (n > _capacity)//扩容
    {
        char* tmp = new(char[n + 1]);
        strcpy(tmp, _str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
        //元素个数未变
    }
    else//减容,相当于删除
    {
        _str[n] = '\0';
        _size = n;
    }
}

3.10 insert()、push_back()、append()

它们分别是:插入符号或字符串,尾插符号,尾接字符串。(实际上通过查阅官方文档可得知:它们的功能远不如此,为了实现方便,所以只实现简单的几个)。

insert():

  • 尾插字符:
  • 首先要判断容量,不够要扩容,这里使用了常见的三目运算符;
  • 其次就是挪动数据,由于size_t的特殊性,-1是一个很大的正数,所以在循环中不能让下标变成-1,否则会越界;
  • 然后再插入,最后返回。
  • 尾插字符串:
  • 思路同上。

**push_back():**复用insert()尾插。

**append():**同上。

【注意】

  • 插入后需要更新_size。
  • 为什么不使用strcat(),因为效率低。
Mystring& insert(size_t pos, const char ch)
{
    assert(pos <= _size);
    //扩容
    if (_capacity == _size)
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    //要挪动的数据个数,再加上\0
    /*int num = _size - pos + 1;
    int tail = _size;
    while (num--)
    {
        _str[tail + 1] = _str[tail];
        tail--;
    }*/
    size_t tail = _size + 1;
    while (tail > pos)
    {
        _str[tail] = _str[tail - 1];
        tail--;
    }
    _str[pos] = ch;
    _size++;
    return *this;
}
Mystring& insert(size_t pos, const char* s)
{
    assert(pos <= _size);
    size_t len = strlen(s);
    if (_size + len > _capacity)
    {
        reserve(_size + len);
    }
    // 挪动数据
    size_t end = _size + len;
    while (end >= pos + len)
    {
        _str[end] = _str[end - len];
        --end;
    }
    strncpy(_str + pos, s, len);
    _size += len;
    return *this;
}
void push_back(char ch)
{
    insert(_size, ch);
}
void append(const char* s)
{
    insert(_size, s);
}

3.11 clear()、erase()

  • clear():清空Mystring对象,思路同reserve();
  • erase():清空pos位置往后的内容,思路同上。
void clear()
{
    _str[0] = '\0';
    _size = 0;
    _capacity = 0;
}
void erase(size_t pos)
{
    assert(pos >= 0);
    _str[pos] = '\0';
    _size = pos;
}

3.12 operator<<、operator>>

在类和对象中,我们学习了流输入和流输出运算符,简单地说,<<就是将流中的数据提取到显示器(可以是其他设备);>>就是将输入的数据插入到流中,在这里可以认为“流”是内存的一部分,它由各种数据组成。

人们经常使用cin和cout搭配流插入和流提取运算符输入,之所以能支持众多类型,是因为它被重载了。

  • operator<<:流提取。遍历字符串,将每个元素重载到ostream对象中,然后一个一个地返回;
  • operator>>:流插入。不同于流提取,需要将一定量的数据存入“缓冲区”(buffer),然后将缓冲区的数据插入到流中。通常情况下,在重载中定义一个自定义大小的数组作为缓冲区。注意回车和\0是作为字符串结束的标志。

缓冲区:CPU的速度非常快,寄存器更甚,而磁盘的速度相去甚远,数据流是“穿过”它们的,但是由于速度不匹配,这样会造成输入和输出中的一个很快,一个很慢,这样就会出现不必要的等待时间,缓冲区因此而生。

【注意】

  • 一般情况重载运算符都在类的内部,或作为友元函数(友元的目的是访问类的成员变量)。但作为友元函数,使用它们就没那么自然,需要对象名.<<这样的格式,为了符合日常的习惯,将其写在类的外部,只要能访问类的成员变量即可。
  • 可能出现的问题:链接失败。类的声明的代码都写在.h头文件中,可以尝试将>>和<<的重载的实现写在.cpp文件中。原因是它被编译了两次,使用#program once或上面的方法也许可以解决。
ostream& operator<<(ostream& os, Mystring& s)
{
    for (size_t i = 0; i < s.size(); i++)
    {
        os << s[i];
    }
    return os;
}
istream& operator>>(istream& is, Mystring& s)
{
    s.clear();
    const int N = 32;
    char ch = is.get();
    //自定义缓冲区
    char buffer[N];
    size_t i = 0;
    while (ch != ' ' && ch != '\n')
    {
        //达到缓冲区容量,最后加上\0
        if (i == N - 1)
        {
            buffer[i] = '\0';
            //连接
            s += buffer;
            i = 0;
        }
        //迭代
        buffer[i++] = ch;
        ch = is.get();
    }
    //连接
    buffer[i] = '\0';
    s += buffer;
    return is;
}
string还有许多其他内置类型转换,例如to_string......
目录
相关文章
|
9天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
24 7
|
26天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
51 4
|
28天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
65 5
|
28天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
49 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
101 5
|
1月前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
51 0
|
12天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
22 0
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
85 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
100 4
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
54 2