string类

目录

string类

1. 为什么要学习string类

1.1 C语言中的字符串

1.2 练习题

2. 标准库中的string类

2.1 string类(了解)

2.2 string类的常用接口说明

2.2.1. string类对象的常见构造

2.2.2. string类对象的容量操作 

2.2.3. string类对象的访问及遍历操作

2.2.4. string类对象的修改操作

2.2.5. string类非成员函数

2.2.6. vs和g++下string结构的说明

2.2.7. 练习

3. string类的模拟实现

3.1 经典的string类问题

3.2 浅拷贝

3.3 深拷贝

3.3.1 传统版写法的String类

3.3.2 现代版写法的String类

3.4 写时拷贝

3.5 string类的模拟实现

string类

1. 为什么要学习string类

1.1 C语言中的字符串

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

1.2 练习题

已上述OJ题为例,在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。

2. 标准库中的string类

2.1 string类(了解)

string类的文档介绍

1. 字符串是表示字符序列的类

2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。

5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1. string是表示字符串的字符串类

2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。

3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;

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

使用string类时,必须包含#include头文件以及using namespace std;

2.2 string类的常用接口说明

2.2.1. string类对象的常见构造
(constructor)函数名称功能说明
string() (重点)构造空的string类对象,即空字符串
string(const char* s) (重点)用C-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s) (重点)拷贝构造函数
void Teststring()
{
	string s1;
	// 构造空的string类对象s1
	string s2("hello bit");
	// 用C格式字符串构造string类对象s2
	string s3(s2);
	// 拷贝构造s3
}
int main()
{
	Teststring();
	return 0;
}
2.2.2. string类对象的容量操作 
函数名称功能说明
size(重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串释放为空串,是返回true,否则返回false
clear (重点)清空有效字符
reserve (重点)为字符串预留空间**
resize (重点)将有效字符的个数改成n个,多出的空间用字符c填充

string容量相关方法使用代码演示

注意:
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在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大
小,如果是将元素个数减少,底层空间总大小不变。

4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。

2.2.3. string类对象的访问及遍历操作
函数名称功能说明
operator[] (重点)返回pos位置的字符,const string类对象调用
begin+ endbegin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin + rendbegin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
范围forC++11支持更简洁的范围for的新遍历方式

string中元素访问及遍历代码演示:

// 测试string容量相关的接口
// size/clear/resize
void Teststring1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, bit!!!");
	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个,多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时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;
}

2.2.4. string类对象的修改操作
函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串
operator+= (重点)在字符串后追加字符串str
c_str(重点)返回C格式字符串
find + npos(重点)从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回

string中插入和查找等使用代码演示:

void Teststring5()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'b';           // 在str后追加一个字符'b'   
	str += "it";          // 在str后追加一个字符串"it"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串

	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;

	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;

	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");
	if (start == string::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;
}

注意:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。

2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

2.2.5. string类非成员函数
函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>>(重点)输入运算符重载(遇到" "或"\n"停止输入)
operator<< (重点)输出运算符重载
getline(重点)获取一行字符串
relational operators(重点)大小比较

 代码演示:

void Teststring()
{
	string s1("Hello World!");
	cout << "s1:" << s1 << endl;

	string s2;
	cin >> s2;
	cout << "s2:" << s2 << endl;

	string s3;
	getline(cin, s3);
	cout << "s3:" << s3 << endl;

	string s4;
	s4 = s3 + "hello";
	cout << "s4:" << s4 << endl;

	cout << (s3 < s4) << endl;
}

string类中还有一些其他的操作,这里不一一列举,大家在需要用到时查文档即可。

2.2.6. vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节。

1.vs下string的结构

 string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:

  • 当字符串长度小于16时,使用内部固定的字符数组来存放
  • 当字符串长度大于等于16时,从堆上开辟空间
union _Bxty
{
	// storage for small buffer or pointer to larger one
	value_type _Buf[_BUF_SIZE];
	pointer _Ptr;
	char _Alias[_BUF_SIZE]; // to permit aliasing
} _Bx;

这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。
其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量
最后:还有一个指针做一些其他事情。
故总共占16+4+4+4=28个字节。

2.g++下string的结构

G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

  • 空间总大小
  • 字符串有效长度
  • 引用计数
struct _Rep_base
{
	size_type
		_M_length;
	size_type
		_M_capacity;
	_Atomic_word
		_M_refcount;
};
  • 指向堆空间的指针,用来存储字符串 
2.2.7. 练习

1.仅仅反转字母

class Solution
{
public:
    string reverseOnlyLetters(string s)
    {
        int left = 0;
        int right = s.size() - 1;
        while (left < right)
        {
            while (left < right && !((s[left] >= 'a' && s[left] <= 'z') || (s[left] >= 'A' && s[left] <= 'Z')))
                left++;
            while (left < right && !((s[right] >= 'a' && s[right] <= 'z') || (s[right] >= 'A' && s[right] <= 'Z')))
                right--;
            swap(s[left++], s[right--]);
        }
        return s;
    }
};

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

class Solution 
{
public:
    int firstUniqChar(string s) 
    {
        char max = s[0];
        char min = s[0];
        for(size_t i = 0;i < s.size();i++)
        {
            if(max<s[i])
                max = s[i];
            if(min>s[i])
                min = s[i];
        }
        int* arr = new int[max-min+1]{0};
        for(size_t i = 0;i < s.size();i++)
        {
            arr[s[i]-min]++;
        }
        for(size_t i = 0;i < s.size();i++)
        {
            if(arr[s[i]-min] == 1)
                return i;
        }
        return -1;
    }
};

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

#include <iostream>
#include<string>
using namespace std;

int main()
{
    string s;
    getline(cin,s);
    int pos = s.rfind(' ');
    cout<<(s.size()-pos-1)<<endl;
    return 0;
}

4.验证一个字符串是否是回文 

class Solution 
{
public:
    bool isPalindrome(string s) 
    {
        for(size_t i = 0; i < s.size(); i++)
        {
            if(s[i]>='A'&&s[i]<='Z')
            {
                s[i]+=32;
            }
        }
        int left = 0;
        int right = s.size() - 1;
        while(left<right)
        {
            while (left < right && !((s[left] >= 'a' && s[left] <= 'z') || (s[left] >= 'A' && s[left] <= 'A') || (s[left] >= '0' && s[left] <= '9')))
                left++;
            while (left < right && !((s[right] >= 'a' && s[right] <= 'z') || (s[right] >= 'A' && s[right] <= 'A') || (s[right] >= '0' && s[right] <= '9')))
                right--;
            if(s[left++]!=s[right--])
                return false;
        }
        return true;
    }
};

5.字符串相加

class Solution 
{
public:
    string addStrings(string num1, string num2) 
    {
        int ret = num1.size() > num2.size()? num1.size():num2.size();
        int* arr = new int[ret + 1]{0};
        int end1 = num1.size()-1;
        int end2 = num2.size()-1;
        int end = ret;
        while(end1>=0||end2>=0)
        {
            if(end1>=0 && end2>=0)
                arr[end--] = num1[end1--]+num2[end2--]-48*2;
            else if(end1>=0 && end2<0)
                arr[end--] = num1[end1--]-48;
            else if(end1<0 && end2>=0)
                arr[end--] =num2[end2--]-48;
        }
        int flag = 0;
        int tmp = 0;
        for(int i = ret;i>=0;i--)
        {
            tmp = (arr[i]+flag)%10;
            flag = (arr[i]+flag)/10;
            arr[i] = tmp;
        }
        if(arr[0]==0)
        {
            for(int i = 1;i <= ret;i++)
            {
                arr[i-1]=arr[i];
            }
            ret--;
        }
        string s;
        for(int i = 0; i <= ret; i++)
        {
            s+=(arr[i]+48);
        }
        return s;
    }
};

6.反转字符串 II

class Solution {
public:
    string reverseStr(string s, int k) {
        int size = s.size();
        for(int i = 0;i<size;i+=2*k)
        {
            reverse(s.begin()+i,s.begin()+min(i+k,size));
        }
        return s;
    }
};

 7.反转字符串中的单词 Ⅲ

class Solution 
{
public:
    string reverseWords(string s) 
    {
        int begin = 0;
        for(size_t i = 0; i <= s.size(); i++)
        {
            if(s[i] == ' ' || s[i] == 0)
            {
                reverse(s.begin() + begin,s.begin() + i);
                begin = i + 1;
            }
        }
        return s;
    }
};

8.字符串相乘 

class Solution
{
public:
    string multiply(string num1, string num2)
    {
        //48
        if (num1 == "0" || num2 == "0")
            return "0";
        int size1 = num1.size();
        int size2 = num2.size();
        int* arr = new int[size1 + size2] {0};
        for (int i = size1 - 1; i >= 0; i--)
        {
            for (int j = size2 - 1; j >= 0; j--)
            {
                arr[i + j + 1] += (num1[i] - 48) * (num2[j] - 48);
            }
        }
        int flag = 0;
        int tmp = 0;
        for (int i = size1 + size2 - 1; i >= 0; i--)
        {
            tmp = (arr[i] + flag) % 10;
            flag = (arr[i] + flag) / 10;
            arr[i] = tmp;
        }
        int len = size1 + size2;
        if (arr[0] == 0)
        {
            for (int i = 1; i < size1 + size2; i++)
            {
                arr[i - 1] = arr[i];
            }
            len--;
        }
        string s;
        for (int i = 0; i < len; i++)
        {
            s += arr[i] + 48;
        }
        return s;
    }
};

3. string类的模拟实现

3.1 经典的string类问题

上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。接下来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是否有问题?

// 为了和标准库区分,此处使用String
class String
{
public:
	/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非
		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;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。 

3.2 浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。

可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。

3.3 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

3.3.1 传统版写法的String类
class String
{
public:
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_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)
		{
			char* pStr = new char[strlen(s._str) + 1];
			strcpy(pStr, s._str);
			delete[] _str;
			_str = pStr;
		}
		return *this;
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};
3.3.2 现代版写法的String类
class String
{
public:
	String(const char* str = "")
	{
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(nullptr)
	{
		String strTmp(s._str);
		swap(_str, strTmp._str);
	}
	// 对比下和上面的赋值那个实现比较好?
	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}
	/*
	String& operator=(const String& s)
	{
	if(this != &s)
	{
	String strTmp(s);
	swap(_str, strTmp._str);
	}
	return *this;
}
*/
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

3.4 写时拷贝

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。 

3.5 string类的模拟实现

string类的模拟实现

具体参考以上链接

  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值