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


    文章目录

    1. 标准库类型string

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

    1.2 常用接口

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

    1.2.1 *构造接口

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

    #include 
    #include 
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    结果:

    hello
    hello
    world
    world
    aaaaa

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

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

    void test2()
    {
        string s = "";
    }
    
    int main()
    {
    
        test2();
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    通过调试可以看到,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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结果:

    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 << " ";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    结果:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • operator+能实现简单的字符串拼接,包括字符、字符串、string对象。

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

    operator>>、operator<<
    void test11()
    {
        string s1;
        cin >> s1;
        cout << s1 << endl;
    }
    int main()
    {
        test11();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输入:

    hello

    输出:

    hello

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

    输入:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    结果:

    s1 < s2
    s1 <= s2
    s1 != s2

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

    2. 练习

    2.1 仅仅反转字母

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

    • 接收下标的临时变量最好使用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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    image.png
    【思路】
    类似计数排序的思路:时间换空间。新开一个数组,遍历字符串,为每个小写字母计数;然后再遍历一次字符串,如果字母出现的次数等于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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    image.png
    【思路】
    非常简单,得到字符串长度,从后计数,直到遍历完或者遇到空格停止。
    【注意】
    C++的标准输入流对象cin不支持输入字符串,因为空格或回车会被认为是分隔符。我们使用getline()函数代替cin>>s
    【代码】

    #include 
    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.4 验证回文串

    image.png
    【思路】
    根据题意,大写转小写,移除所有数字字符,言外之意这就是稍后要比较的前提。
    思路简单,头尾同时遍历,分别遇到非字母字符就跳过,然后比较。相同则继续走,不同则返回false。
    【注意】

    • 大写转小写:由于ASCII表中小写字母都比对应的大写字母多32,所以只需遍历+=32即可。
    • 同第一题,非字母字符迭代同样要用while,且要限制begin

    【代码】

    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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.5 字符串相加

    image.png
    【思路】
    逆序遍历两个字符串,然后分别相加当前字符对应的数字,增加一个保存进位的临时变量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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    2.6 反转字符串 II

    image.png
    【思路】
    从题目翻译过来的意思:每隔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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    2.7 反转字符串中的单词 III

    image.png
    【思路】
    正向反向遍历都可以,主要是确定反转的单词区间和控制下标不要越界。
    反向遍历:用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;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2.8 字符串相乘

    image.png
    【思路】
    根据题意,是要返回一个新字符串,
    【代码】

    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";
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

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

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

    #include
    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    【注意】
    这里的构造函数给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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.5 size()、capacity()

    • 获取元素个数和容量。

    【注意】
    个数和容量均不包括\0

    size_t size()
    {
        return _size;
    }
    size_t capacity()
    {
        return _capacity;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    string还有许多其他内置类型转换,例如to_string......
    
    • 1
  • 相关阅读:
    Matlab中error函数的使用
    第九章 动态规划 part16(编辑距离专题)583. 两个字符串的删除操作 72. 编辑距离 编辑距离总结篇
    音视频开发:MediaCodec录制MP4文件
    Spring Retry方法重试
    python opencv 持续点选开始帧,结束帧,切割视频成几个小段
    数组去重(unique())--numpy
    LeetCode 438. 找到字符串中所有字母异位词__滑动窗口
    基因组科学数据的安全管理与应用
    Electron 串口通信
    Axure原型设计:从零开始到精通中文版
  • 原文地址:https://blog.csdn.net/m0_63312733/article/details/126846157