【C++】string类接口及模拟实现

一、string介绍

C语言中,字符串是以 ‘\0’ 结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

C++封装了这些接口和操作,创建出string类:

  1. string 是表示字符串的类
  2. 该类的接口与常规容器的接口基本相同,还添加了一些专门用来操作 string 的常规操作。
  3. 不能操作多字节或者变长字符的序列

为什么要将vector和string分开实现?

  1. 字符串默认最后放 ‘\0’,而vector存储数据时不需要
  2. string 里封装了C语言中字符串的操作接口,这些接口需要 ‘\0’ 作为尾部

二、string常用接口

#include <iostream>
#include <string>
using namespace std;
int main()
{
    
    
	// 构造
	string str;				// 无参
	string str1("qwer");	// qwer
	string str2(5, 'a');	// aaaaa,5个a
	string str3(str1);		// 拷贝构造
	// 容量
	int cap = str.capacity();
	cout << cap << endl;
	for (int i = 0; i < 100; i++)		// 为了提高效率 string 内部管理了一个固定大小的数组,16字节的容量,小于15字节不需要扩容
	{
    
    									
		str.push_back('a');
		if (cap != str.capacity())
		{
    
    
			cap = str.capacity();
			cout << cap << endl;
		}
	}
	cout << endl;
	str.reserve(50);					// 底层容量和有效元素都不变
	cout << str.capacity() << endl;
	str.reserve(15);					// 底层容量和有效元素都不变
	cout << str.capacity() << endl;
	// 有效元素
	str1.resize(50, 'a');				// 有效元素变成50个,容量63
	str1.resize(4);						// 有效元素变成4个,容量不变
	// 迭代器和遍历方式
	for (size_t i = 0; i < str1.size(); i++)
		cout << str1[i];
	cout << endl;
	string::iterator it = str1.begin();
	while (it != str1.end())
	{
    
    
		cout << *it;
		++it;
	}
	cout << endl;
	for (auto ch : str1)
		cout << ch;
	cout << endl;
	// 修改操作
	str1.push_back(' ');
	str1.append("hello");
	str1 += 'a';
	str1 += "it";
	// 查找操作
	size_t pos1 = str1.find("it");
	size_t pos2 = str1.rfind("lo");
	return 0;
}

小结:

  • size() 和 length() 方法底层实现原理完全相同,引入size()是为了和其他容器接口保持一致
  • clear() 只是将有效元素清空,不改变底层容量
  • resize() 进行增多有效元素个数时,可能会增大容量。但减少有效元素个数时,不会改变底层容量
  • reserve() 为 string 预留空间,不改变有效元素的个数,当 reserve() 参数小于底层空间时,不会改变容量大小
  • 扩容时以成 1.5 倍代替固定步长可以保证常数的时间复杂度,而固定步长却只能达到 O(n) 的时间复杂度
  • vs 以1.5 倍扩容,Linux 以 2 倍扩容。如果以两倍以上的方式进行扩容,则新申请的空间会大于之前分配内存的总和,导致原始分配的内存不能被使用。

三、模拟实现string类

  • 标准版写法
    • 构造:参数列表应该缺省,并且要判断是否为空,是则生成空字符串,strcpy() 会将 ‘\0’ 也拷贝进去
    • 析构:判空
    • 拷贝构造:杜绝浅拷贝,在初始化时先开辟空间
    • 赋值重载:判断是否给自己赋值,杜绝浅拷贝,先释放原空间,再开辟新空间大小,进行拷贝
#include <iostream>
namespace traditional
{
    
    
    class string
    {
    
    
    public:
        string(const char* str = "")
        {
    
    
            if (str == nullptr)
                str = "";
            _str = new char[strlen(str) + 1];
            strcpy(_str, str);
        }
        string(const string& s)
            :_str(new char[s.size() + 1])
        {
    
    
            strcpy(_str, s.c_str());
        }
        ~string()
        {
    
    
            if (_str)
            {
    
    
                delete[] _str;
                _str = nullptr;
            }
        }
        string& operator=(const string& s)
        {
    
    
            if (this != &s)
            {
    
    
                // 1.释放旧空间
                delete[] _str;
                // 2.拷贝新空间
                _str = new char[s.size() + 1];
                strcpy(_str, s.c_str());
            }
            return *this;
        }
        int size()const
        {
    
    
            return strlen(_str);
        }
        char* c_str()const
        {
    
    
            return _str;
        }
    private:
        char* _str;
    };
}
  • 现代版写法:注意每个函数里都有开辟、拷贝,代码复用性太低,因此可以用swap()函数进行改进,交换指针域
    • 构造:常规操作
    • 析构:常规操作
    • 拷贝构造:初始化时对 _str 赋空,构造一个临时对象,交换临时对象和 this 的 _str 指针域
    • 赋值重载:传参时拷贝构造一份临时对象,交换临时对象和 this 的 _str 指针域

注意:string 类作为函数返回值时,临时拷贝构造的对象的 _str 不为 nullptr,而是随机值。因此必须在拷贝构造初始化时赋空

namespace modern
{
    
    
    class string
    {
    
    
    public:
        string(const char* str = "")
        {
    
    
            if (str == nullptr)
                str = "";
            _str = new char[strlen(str) + 1];
            strcpy(_str, str);
        }
        string(const string& s)
            :_str(nullptr)
        {
    
    
            string strTemp(s.c_str());
            std::swap(_str, strTemp._str);
        }
        ~string()
        {
    
    
            if (_str)
            {
    
    
                delete[] _str;
                _str = nullptr;
            }
        }
        string& operator=(string s)
        {
    
    
            std::swap(_str, s._str);
            return *this;
        }
        int size()const
        {
    
    
            return strlen(_str);
        }
        char* c_str()const
        {
    
    
            return _str;
        }
    private:
        char* _str;
    };
}

四、深浅拷贝与写时拷贝技术

  • 浅拷贝:多个对象共用一份资源,当一个对象销毁时该资源会被销毁,而另一些对象不知道已经被释放,当继续操作时就会发生越界访问。
    • 拷贝构造、赋值重载没有显示定义出来会造成浅拷贝,并且可能造成内存泄漏或者程序崩溃
    • 将一个对象中的内容原封不动的拷贝到另一个对象中,用了同一块物理内存,在释放时会产生6号信号 SIGABRT 而崩溃,double free
  • 深拷贝:内存地址不同,但内容相同
  • 写时拷贝:多个对象共用同一份内存资源,当一个对象的内容即将发生变化时,给这个对象重新分配内存并拷贝原空间内容。如果不进行修改,则由引用计数进行计算什么时候进行释放资源。
    • 引用计数:用来记录资源使用者的个数,在构造时将资源计数器+1,当一个对象销毁时将资源计数器-1,当计数器值为0时将该资源释放。

猜你喜欢

转载自blog.csdn.net/qq_45691748/article/details/110227835