C++初阶-string的使用及模拟

2022-11-30 12:49:19 浏览数 (1)

C string的使用及模拟

  • 零、前言
  • 一、什么是string类
  • 二、string类常用接口说明
    • 1、string类对象常见构造
    • 2、string类对象容量操作
    • 3、string类对象访问及遍历操作
    • 4、string类对象修改操作
    • 5、string类非成员函数
  • 三、模拟实现string类
    • 1、实现string类接口展示
    • 2、深浅拷贝问题
    • 3、string类深拷贝写法
    • 4、string类其他常用接口模拟

零、前言

本章主要讲解C string类的相关知识以及使用,还会模拟实现一下string类

一、什么是string类

  • 引入:

C语言中,字符串是以’’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问,由此C 做出改进引入了string类

  • 概念:

  1. string是表示字符串的字符串类
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作
  3. string在底层实际是:basic_string模板类的别名
代码语言:javascript复制
typedef basic_string<char, char_traits, allocator>string;

  1. 不能操作多字节或者变长字符的序列

注:在使用string类时,必须包含

代码语言:javascript复制
#include<string>
using namespace std;  

二、string类常用接口说明

注:下面讲解最常用的接口

1、string类对象常见构造

函数名称

功能说明

string() (重点)

构造空的string类对象,即空字符串

string(const char* s) (重点)

用C-string来构造string类对象

string(size_t n, char c)

string类对象中包含n个字符c

string(const string&s) (重点)

拷贝构造函数

string (const string& str, size_t pos, size_t len = npos)

从str对象中由pos位置开始截取len个长度的字符,len > str长度就结束

string (const char* s, size_t n)

从s指向的字符数组中复制前n个字符

  • 使用示例:
代码语言:javascript复制
void Teststring1()
{
	// 构造空的string类对象,等同于string s1("");
	string s1; 
	// 用C格式字符串构造string类对象s2
	string s2("hello cole"); 
	// 拷贝构造s3
	string s3(s2);
	// 截取string类部分拷贝
	string s4(s3, 6);//从第6个开始拷取到最后
	// 截取字符数组部分拷贝
	char str[] = "hello cole";
	string s5(str, 5);//拷取前5个字符
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
}
// 注意:string类对象支持直接用cin和cout进行输入和输出
  • 结果:

2、string类对象容量操作

函数名称

功能说明

size(重点)

返回字符串有效字符长度

length

返回字符串有效字符长度

capacity

返回空间总大小

empty (重点)

检测字符串释放为空串,是返回true,否则返回false

clear (重点)

清空有效字符

reserve (重点)

为字符串预留空间**

resize (重点)

将有效字符的个数该成n个,多出的空间用字符c填充

  • 使用示例1:
代码语言:javascript复制
// size/clear/resize
void Teststring2()
{
	string s("hello cole!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 将s中有效字符个数增加到15个,多出位置用缺省值''进行填充
	// "aaaaaaaaaa"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}
  • 结果1:
  • 使用示例2:
代码语言:javascript复制
//reserve
void Teststring4()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}
  • 结果2:
  • 使用示例3:
代码语言:javascript复制
// 利用reserve提高插入数据的效率,避免增容带来的开销
void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "making s grow:n";
	for (int i = 0; i < 100;   i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << 'n';
		}
	}
}
void TestPushBackReserve()
{
	string s;
	s.reserve(100);
	size_t sz = s.capacity();
	cout << "capacity: " << sz << 'n';
	cout << "making s grow:n";
	for (int i = 0; i < 100;   i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed: " << sz << 'n';
		}
	}
}
  • 结果3:
  • 注意:

  1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()
  2. clear()只是将string中有效字符清空,不改变底层空间大小
  3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间
  • 注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变
  1. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小

3、string类对象访问及遍历操作

函数名称

功能说明

operator[] (重点)

返回pos位置的字符,普通对象和const对象有相应的版本

begin end

egin获取第一个字符的迭代器 end获取最后一个字符下一个位置的迭代器

rbegin rend

begin获取第一个字符的迭代器 end获取最后一个字符下一个位置的迭代器

范围for

C 11支持,最终替换成迭代器

  • 使用示例:
代码语言:javascript复制
void Teststring5()
{
	string s1("hello cole");
	//使用operator[] size()遍历字符串
	for (size_t i = 0; i < s1.size(); i  )
	{
		//等同与s1.operator[](i)
		cout << s1[i] << " ";
	}
	cout << endl;
	//使用正向迭代器遍历字符串
	string::iterator it1 = s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1 << " ";
		it1  ;
	}
	cout << endl;
	//注意:iterator为普通迭代器,可读可写,const_iterator为const迭代器,只能读取字
	string::const_iterator it3 = s1.begin();
	while (it3 != s1.end())
	{
		//*it3 =1; 会报错
		cout << *it3 << " ";
		it3  ;
	}
	cout << endl;
	//使用反向迭代器遍历字符串
	string::reverse_iterator it2 = s1.rbegin();
	while (it2 != s1.rend())
	{
		cout << *it2 << " ";
		it2  ;
	}
	cout << endl;
	//同样的反向迭代器的const迭代器为const_reverse_iterator
	//范围for遍历字符串
	for (auto e : s1)
	{
		cout << e << " ";
	}
}
  • 结果:

4、string类对象修改操作

函数名称

功能说明

push_back

在字符串后尾插字符c

append

在字符串后追加一个字符串

operator = (重点)

在字符串后追加字符串str

c_str(重点)

返回C格式字符串

find npos(重点)

从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置

rfind

从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置

substr

在str中从pos位置开始,截取n个字符,然后将其返回

  • 使用示例1:
代码语言:javascript复制
void Teststring6()
{
	string str;
	cout << str << endl;
	str.push_back('c'); // 在str后插入字符
	cout << str << endl;
	str.append("hello"); // 在str后追加一个字符"hello"
	cout << str << endl;
	str  = ''; // 在str后追加结束符
	str  = "cole"; // 在str后追加一个字符串"cole"
	cout << str << endl;//即使遇到‘’也不会停止,只有把字符串给完全遍历完了才会停止遍历
	cout << str.c_str() << endl; // 以C语言的方式打印字符串(遇到就停止)
}
  • 结果1:
  • 使用示例2:
代码语言:javascript复制
void Teststring7()
{
	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');//从后往前找字符. 
	string suffix(file.substr(pos, file.size() - pos));//拷贝pos位置之后的字符串
	cout << suffix << endl;
    
	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");//从前往后找字符串 
	if (start == string::npos)//没找到返回npos
	{
		cout << "invalid url" << endl;
		return;
	}
	start  = 3;
	size_t finish = url.find('/', start);//再往后找结束位置
	string address = url.substr(start, finish - start);//拷贝子串
	cout << address << endl;
    
	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos   3);
	cout << url << endl;
}
  • 结果2:

注:npos是string里面的一个静态成员变量

代码语言:javascript复制
static const size_t npos = -1;
  • 注意:

  1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s = ‘c’ 三种的实现方式差不多,一般情况下string类的 =操作用的比较多, =操作不仅可以连接单个字符,还可以连接字符串
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好(提高效率)

5、string类非成员函数

函数

功能说明

operator

尽量少用,因为传值返回,导致深拷贝效率低

operator>> (重点)

输入运算符重载

operator<< (重点)

输出运算符重载

getline (重点)

获取一行字符串

relational operators (重点)

大小比较

注:对上述函数只是了解一下,对于其他的操作函数可以查阅文档了解

三、模拟实现string类

注:这里我们只是模拟实现string的一些常用接口,并非要完全复刻,学习下string类的底层,让对string类的理解更深一点就行了

1、实现string类接口展示

注:模拟时为了避免与C 本身提供的string类造成命名冲突,我们选择在命名空间里进行实现

代码语言:javascript复制
namespace cole
{
    class string
    {
        friend ostream& operator<<(ostream& _cout, const cole::string& s);
        friend istream& operator>>(istream& _cin, cole::string& s);
        friend istream& getline(istream& _cin, string& s);

    public:
        typedef char* iterator;
        typedef const char* const_iterator;

        string(const char* str = "");
        string(const string& s);
        string& operator=(const string& s);
        ~string();
        void swap(string& s);
        // iterator
        iterator begin();
        iterator end();
        const_iterator begin()const;
        const_iterator end()const;

        // modify
        void push_back(char c);
        string& operator =(char c);
        void append(const char* str);
        string& operator =(const char* str);
        void clear();
        const char* c_str()const;

        // capacity
        size_t size()const;
        size_t capacity()const;
        bool empty()const;
        void resize(size_t n, char c = '');
        void reserve(size_t n);

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

        //relational operator
        bool operator<(const string& s);
        bool operator<=(const string& s);
        bool operator>(const string& s);
        bool operator>=(const string& s);
        bool operator==(const string& s);
        bool operator!=(const string& s);

        // 返回c在string中第一次出现的位置
        size_t find(char c, size_t pos = 0) const;
        // 返回子串s在string中第一次出现的位置
        size_t find(const char* s, size_t pos = 0) const;
        // 在pos位置上插入字符c/字符串str,并返回该字符
        string& insert(size_t pos, char c);
        string& insert(size_t pos, const char* str);
        // 删除pos位置上的长度个字符
        string& erase(size_t pos, size_t len=npos);

    private:
        char* _str;
        size_t _capacity;
        size_t _size;
        static const size_t npos;
    };
    //初始化静态私有成员变量
    const size_t string::npos = -1;
}

2、深浅拷贝问题

一般来说对于只有内置类型的成员变量,我们可以选择编译器提供的拷贝构造函数和赋值重载函数(自己不写),编译器提供的这两个函数能够完成浅拷贝(拷贝值),但是对于存在指针成员变量的类,浅拷贝是不行的

  • 示例:
代码语言:javascript复制
namespace cole
{

	class string
	{
	public:
		string(const char* str = "")
		{
			// 断言不为空指针
			assert(str);
			_str = new char[strlen(str)   1];
			strcpy(_str, str);
		}
		~string()
		{
			if (_str)
			{
				delete[] _str;
				_str = nullptr;
			}
		}
	private:
		char* _str;
	};
	// 测试
	void Teststring()
	{
		string s1("hello cole");
		string s2(s1);
	}
}
int main()
{
	cole::Teststring();
	return 0;
}
  • 结果:
  • 解释:

即值拷贝(浅拷贝)让s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,由此对于涉及资源的管理,我们需要自己以深拷贝的方式写拷贝构造函数和赋值重载函数

3、string类深拷贝写法

  • 深拷贝概念:

给每个对象独立分配资源,保证多个对象之间不会因为共享资源而造成多种错误以及程序释放崩溃的问题

  • 传统式:比较常规,易于理解
代码语言:javascript复制
class string
{
public:
    //构造函数
    string(const char* str = "")
    {
        // 构造string类对象时,如果传递nullptr指针,认为程序非法,此处断言下
        assert(str);
        _str = new char[strlen(str)   1];
        strcpy(_str, str);
    }
    //拷贝构造函数
    string(const string& s)
        : _str(new char[strlen(s._str)   1])
    {
        strcpy(_str, s._str);
    }
    //赋值
    string& operator=(const string& s)
    {
        if (this != &s)//避免自己赋值自己(没有意义)
        {
            delete[]_str;
            _str = new char[strlen(s._str)   1];
            strcpy(_str, s._str);
        }
        return *this;
    }
    //析构函数
    ~string()
    {
        delete[]_str;
        _str = nullptr;
    }
private:
    char* _str;
};
  • 现代式:简洁,效果一致
代码语言:javascript复制
class string
{
public:
	//构造函数
	string(const char* str = "")
	{
		if (nullptr == str)//处理空指针
			str = "";
		_str = new char[strlen(str)   1];
		strcpy(_str, str);
	}
	//拷贝构造
	string(const string& s)
		: _str(nullptr)//将自己赋值为空
	{
		string strTmp(s._str);//调用构造函数,生成一个具有相同内容的string
		swap(_str, strTmp._str);//将string对象的各成员变量相互交换
	}//拷贝构造结束strTmp会先析构再销毁

	//赋值重载
	string& operator=(string s)//传值参数,会调用拷贝构造,生成一个具有相同内容的string
	{
		swap(_str, s._str);//将string对象的各成员变量相互交换
		return *this;
	}//赋值重载结束束s会先析构再销毁

	//析构函数
	~string()
	{
		delete[] _str;
		_str = nullptr;
	}
private:
	char* _str;
};

4、string类其他常用接口模拟

  • 实现代码:
代码语言:javascript复制
ostream& operator<<(ostream& _cout, const string& s)
{
    for (auto ch : s)//会替换成string的迭代器
    {
        _cout << ch;
    }
    return _cout;
}
istream& operator>>(istream& _cin, string& s)
{
    s.clear();//清理
    char ch;
    ch = _cin.get();//能够接收空格和结束符
    while (ch != ' ' && ch != 'n')
    {
        s  = ch;
        ch = _cin.get();
    }
    return _cin;
}

istream& getline(istream& _cin, string& s)
{
    s.clear();
    char ch;
    ch = _cin.get();
    while (ch != 'n')
    {
        s  = ch;
        ch = _cin.get();
    }
    return _cin;
}
//构造函数
string::string(const char* str="")
{
    if(str==nullptr)
        str="";
    _str = new char[strlen(str)   1];
    _size = strlen(str);
    _capacity = _size;
    strcpy(_str, str);
}
//交换string
void string::swap(string& s)
{
    ::swap(_str, s._str);
    ::swap(_size, s._size);
    ::swap(_capacity, s._capacity);
}
//拷贝构造
string::string(const string& s)
    :_str(nullptr)
    , _size(0)
    , _capacity(0)
{
    string tmp(s._str);
    swap(tmp);
}
//赋值重载
string& string::operator=(const string& s)//传引用
{
    //避免自己给自己赋值
    if (this != &s)
    {
        string tmp(s._str);
        swap(tmp);
    }
    return *this;
}
//析构函数
string::~string()
{
    delete[] _str;
    _str = nullptr;
    _size = 0;
    _capacity = 0;
}

// iterator(普通对象)
string::iterator string::begin()
{
    return _str;
}
string::iterator string::end()
{
    return _str   _size;
}
// iterator(const对象)
string::const_iterator string::begin()const
{
    return _str;
}
string::const_iterator string::end()const
{
    return _str   _size;
}

// modify
//尾插字符
void string::push_back(char c)
{
    满空间就扩容
    //if (_size == _capacity)
    //{
    //    reserve(_capacity == 0 ? 4 : _capacity * 2);
    //}
    //_str[_size] = c;
    //_size  ;

    insert(_size, c);
}
string& string::operator =(char c)
{
    push_back(c);
    return *this;
}
//追加字符串
void string::append(const char* str)
{
    /*size_t len = _size   strlen(str);
    if (len > _capacity)
    {
        reserve(len);
    }
    strcpy(_str   _size, str);
    _size = len;*/

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

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

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
{
    return _size == 0;
}
//开空间 初始
void string::resize(size_t n, char c)
{
    if (n < _size)
    {
        _str[n] = '';
        _size = n;
    }
    else
    {
        if (n > _capacity)
        {
            reserve(n);
        }
        int pos = _size;
        while (pos < n)
            _str[pos  ] = c;
        _str[n] = '';
        _size = n;
    }
}
//开空间
void string::reserve(size_t n)
{
    //需要的空间大于容量就开空间
    if (n > _capacity)
    { 
        char* tmp = new char[n   1];
        strncpy(tmp, _str, _size   1);//strcpy不能将后的内容也拷贝
        delete[] _str;
        _str = tmp;
        _capacity = n;
    }
}

// access
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];
}

//relational operator
bool string::operator<(const string& s)
{
    return strcmp(_str, s._str) < 0;
}
bool string::operator<=(const string& s)
{
    return *this < s || *this == s;
}
bool string::operator>(const string& s)
{
    return !(*this <= s);
}
bool string::operator>=(const string& s)
{
    return !(*this < s);
}
bool string::operator==(const string& s)
{
    return strcmp(_str, s._str);
}
bool string::operator!=(const string& s)
{
    return !(*this == s);
}

// 返回c在string中第一次出现的位置
size_t string::find(char c, size_t pos) const
{
    //pos的合理性
    assert(pos < _size);
    for (size_t i = pos; i < _size; i  )
    {
        if (_str[i] == c)
            return i;
    }
    //没找到
    return npos;
}
// 返回子串s在string中第一次出现的位置
size_t string::find(const char* s, size_t pos) const
{
    //pos的合理性
    assert(pos < _size);
    const char* ret = strstr(_str   pos, s);
    if (ret)
        return ret - _str;
    //没找到
    return npos;
}
// 在pos位置上插入字符c/字符串str
string& string::insert(size_t pos, char c)
{
    assert(pos <= _size);
    if (_size == _capacity)
    {
        reserve(_capacity == 0 ? 4 : _capacity * 2);
    }
    char* end = _str   _size;
    while (end >= _str   pos)
    {
        *(end   1) = *end;
        end--;
    }
    _str[pos] = c;
    _size  ;
    return *this;
}
string& string::insert(size_t pos, const char* str)
{
    assert(pos <= _size);
    size_t len1 = strlen(str);
    size_t len2 = len1   _size;
    if (len2 > _capacity)
    {
        reserve(len2);
    }
    char* end = _str   _size;
    while (end >= _str   pos)
    {
        *(end   len1) = *end;
        end--;
    }
    strncpy(_str   pos, str, len1);
    _size  = len1;

    return *this;
}
// 删除pos位置上的长度个字符
string& string::erase(size_t pos, size_t len)
{
    assert(pos < _size);
    size_t leftlen = _size - pos - 1;
    if (len >= leftlen)
    {
        _str[pos] = '';
        _size = pos;
    }
    else
    {
        strcpy(_str   pos, _str   pos   len);
        _size -= len;
    }
    return *this;
}

0 人点赞