string类的理解
- 为什么需要学习string类
- 标准库中的string类
- string类简单了解
- string类常见接口
- string模拟实现
- 深浅拷贝问题
- 标准库下的string
- VS环境下
- g++环境下
为什么需要学习string类
在C语言中,字符串和字符串相关的函数是分开的,不太符合面向对象的思想,封装性太低,同时需要用户自己管理底层空间,稍不留神可能就会越界访问;
为此,C++在这些问题的基础上提出了string类;
标准库中的string类
string类简单了解
string类文档介绍;
- string类也是利用模板实例化出来的一个具体类,string本身并不是模板;
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作
单字节字符字符串的设计特性。- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信
息,请参阅basic_string)。- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits
和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个
类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1.string是表示字符串的字符串类
2.该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3.string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
4.不能操作多字节或者变长字符的序列。
string类常见接口
(constructor)
1、string();//无参构造函数,利用空串构造string类对象
2、string(const char*str);//利用C语言字符串构造string类对象;
3、string(const string &s);//拷贝构造函数
4、string(size_t n,char c);//利用n个c字符来构造string类对象
(destructor)
~string();//析构函数
operator=
1、string&operator=(const string&s);//string对象赋值给string对象;
2、string&operator=(const char*str);//C语言字符串赋值给string对象;
3、string&operator=(char ch);//C字符给string赋值;
Capacity:
1、size();//返回字符串长度;
2、length();//返回字符串长度
size与length在底层实现上是一样的,早期的string是不在容器之内的,求字符长度是用的length,多出来一个size主要是为了与后面string加入容器队伍过后名称的统一!
3、capacity();//返回当前的字符数组的容量;
、4、empty();//判断string对象是否为空串;是:true;不是:false;
5、clear();//清除有效字符,size置0,容量不变
6、reserev(size_t n=0);//请求更改容量,若n大于当前已开辟的容量,那么就开辟n个空间(或则更多,vs环境下就是这样,Linux:g++下就是要多少开多少);若n小于等于当前已开辟的容量,编译器不做处理;此操作不会更改size的大小;
7、resize(size_t n,char c=‘\0’)//重新调整字符串有效字符的大小;如果n大于当前size的大小,那么当前对象可能会发生扩容,size=n,新出来的空间利用指定字符c来填充;若n小于当前的大小,那么size=n,将当前对象的有效字符缩短置n个,capacity不变;
iterator:
begin();//返回开头的指针
end();//返回最后一个元素的下一个位置的指针;
我们可以利用迭代器完成字符串遍历的操作;
rbegin();//反向迭代器,返回最后一个元素的指针;
rend();//反向迭代器,返回第一个元素的前一个位置的指针;
反向迭代器也就是反着遍历:
begin/end有重载,rbegin/rend也有重载
主要是重载的const对象;非const对象调用begin/end,rbegin/rebd;const对象调用begin()const/end()const,rbegin()const/rend()const;
当然在C++11中const对象,调用正向迭代器,可以用cbegin/cend;
反向迭代调用crbegin/crend;
按照非const对象与const对象来分:
非const对象调用begin/end,rbegin/rend;
const对象调用cbegin/cend,crbegin。crend;
按照正向迭代器、反向迭代器来分:
begin/end可以被非const对象和const对象调用;
rbegin/rend可以被非const对象和const对象调用;
注意:要注意调用的迭代器类型要匹配;
迭代器总的来说有四种:iterator(非const对象正向迭代器)、const_iterator(const对象正向迭代器);
reverse_iterator(非const对象反向迭代器)、const_reverse_iterator(const对象反向迭代器);
Element access:
operator[](size_t pos);//重载运算符[]//这是利用assert的方式来检查越界
at(size_t pos);//使用at成员函数访问//这是利用抛出异常的方式来检查越界
Modifiers:
operator+=(const string&s);//追加一个string对象
operator+=(const char*s);//追加一个C字符串
operator+=(char ch);//追加一个字符;
append(const string&s);
append(const char*s);
append(size_t n.char c);
push_back(char c);//插入一个字符
pop_back();//删除最后一个字符
insert(size_t pos,const string&s);//pos位置插入一个string对象
insert(size_t pos,const char*s);//pos位置插入C字符串
insert(size_t pos,size_t n ,char c);//pos位置插入n个指定字符;
erase (size_t pos = 0, size_t len = npos)//从pos位置开始删除len个字符;
swap (string& str);//此swap是在string类内部实现的,与模板swap不一样;
我们可思考一下,为什么要在string内部实现实现一个swap呢?模板swap不够我们用吗?
当然不是,两个swap都能用,但是在使用上存在效率问题,string内部的swap效率要高于模板swap;
下面我们通过画图来解释:
String operations:
const char* c_str() const;//将我们的string转换为C字符串
size_t find(const string &str,size_t pos=0);//从pos位置开始寻找str字符串,找到了,返回其所在位置,找不到返回npos;
size_t find(const char *s,size_t pos=0);
size_t find( char ch,size_t pos=0);
Non-member function overloads:
relational operators:
getline();
从istream流获取字符串,以指定字符delim作为分隔符;delim默认是’\n’;
string模拟实现
string.h
#pragma once
#pragma warning(disable:4996)
#include<iostream>
#include<assert.h>
#include<string.h>
namespace MySpace
{
class string//字符串末尾需要存储'\0',但是'\0'不算做_capacity的大小
{
// friend std::ostream& operator<<(std::ostream& cout, const string& str);
friend std::istream& operator>>(std::istream& cin, string& str);
public:
//string()///无参构造//根据STL的表现,string对象会被初始化为一个空串
//:_size(0),_capacity(0)
//{
// _str = new char[1];//这里为什么不用new char?答:虽然语法支持这样做,但是考虑到析构函数的统一写法:delete[],建议用new[]开空间
// _str[0] = '\0';
//}
//对于无参构造函数和有参构造函数,我们可以考虑利用缺省值
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size + 4;//这里可以考虑多开一点,保持capacity不为0;//不一定要多少开开多少
//可以多开4个空间
_str = new char[_capacity + 1];//这里需要将'\0'的空间考虑进来,但是_capacity不记录'\0'的空间
strcpy(_str, str);
}
//拷贝构造//使用编译器默认的会发生浅拷贝问题
string(const string& str)
:_size(str._size), _capacity(str._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
//析构
~string()
{
delete[] _str;
_size = _capacity = 0;
}
//赋值运算符,不能用编译器提供的,编译器提供的是浅拷贝
string& operator=(const string& str)
{
if (this != &str)
{
char* tmp = new char[str._capacity + 1];//开辟失败,抛出异常,不会丢失原数据
//开辟成功
strcpy(tmp, str._str);
_size = str._size;
_capacity = str._capacity;
delete[]_str;
_str = tmp;
}
return *this;
}
string& operator=(const char* str)
{
if (str != _str)
{
resize(0);
*this += str;
}
return *this;
}
string& operator=(char ch)
{
resize(0);
*this += ch;
return *this;
}
//字符串长度
size_t size()const
{
return _size;
}
//清理有效字符
void clear() {
_size = 0;
_str[_size] = '\0';
}
//判断是否是空串
bool empty()const
{
return _size == 0;
}
//获取当前的有效存储容量
size_t capacity()const
{
return _capacity;
}
//[]访问
//1、非const对象
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//2、const对象
const char& operator[](size_t pos)const//返回值是没必要加const的,因为const对象的const是修饰_str的不是修饰*_str的
{
assert(pos < _size);
return _str[pos];
}
//比较运算符
bool operator>(const string& str)const
{
size_t l1 = 0;
size_t l2 = 0;
while (l1 <= _size && l2 <= str._size)
{
if (_str[l1] == str._str[l2])
{
l1++;
l2++;
}
else if (_str[l1] > str._str[l2])
return true;
else
return false;
}
return false;
}
bool operator==(const string& str)const
{
if (_size != str._size)
return false;
else
{
size_t l1 = 0;
size_t l2 = 0;
while (l1 <= _size && l2 <= str._size)
{
if (_str[l1] == str._str[l2])
{
l1++;
l2++;
}
else
return false;
}
return true;
}
}
bool operator<(const string& str)const
{
return !(*this > str || *this == str);
}
bool operator>=(const string& str)const
{
return !(*this < str);
}
bool operator<=(const string& str)const
{
return !(*this > str);
}
bool operator!=(const string& str)const
{
return !(*this == str);
}
//string转换成C字符串
const char* c_str() const
{
return _str;
}
//迭代器
//1、非const对象
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//2、const对象
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
//重设size的大小
void resize(size_t n, char ch = '\0')
{
if (n > _size && n <= _capacity)
{
memset(_str+_size,ch,(n-_size)*sizeof(char));
}
else if(n>_capacity)
{
reserve(n);
memset(_str+_size,ch,(n-_size)*sizeof(char));
}
_str[n] = '\0';
_size = n;
}
//pos位置插入字符
string& insert(size_t pos, char ch)
{
assert(pos<=_size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end=_size+1;
while(end>=pos+1)
{
_str[end]=_str[end-1];
end--;
}
_str[pos]=ch;
_size++;
return *this;
}
//pos位置插入字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);
}
size_t end=_size+len;
while(end>=pos+len)
{
_str[end]=_str[end-len];
end--;
}
strncpy(_str+pos,str,len);
_size+=len;
return *this;
}
//从pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{
assert(empty() != true && pos < _size);
int left = pos;
int right = pos+len-1;
if(len==npos||pos+len>=_size)
right=_size-1;
int end = right + 1;
while (end <= _size)
{
_str[end - (right - left + 1)] = _str[end];
end++;
}
_size -= (right - left + 1);
return *this;
}
//交换
void swap(string& str)
{
std::swap(_str,str._str);
std::swap(_size,str._size);
std::swap(_capacity,str._capacity);
}
//扩容,重新设置有效容量
void reserve(size_t n = 0)
{
//开空间,拷数据
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
//插入字符
void push_back(char ch)
{
//检查容量
if (_size +1> _capacity)//容量已满,扩二倍
{
reserve(_capacity*2);
}
_str[_size] = ch;
_str[_size + 1] = '\0';//'\0'需要我们自己手动插入
_size++;
}
//追加字符串
string& append(const char* str)
{
size_t len = strlen(str);
const char* sour = str;
char* dest = _str;//先记录一下原来的指针
if (_size + len > _capacity)//需要扩容
{
reserve(_size+len);
}
//strcpy(_str+_size,str);//这里不建议用strcat,因为strcat会去找_str的\0,这是O(N)的消耗,没必要
//这里用strcpy的话会造成自己给自己追加不能完成
//可是如果考虑到自己给自己追加的话,strcpy也会不妥,会发生无穷拷贝
if (dest == sour)//dest==str,说明是自己给自己追加,由于已经开好了空间,之前的空间也就被释放了,sour也就是个也
sour = _str;//野指针,我们需要更换sour的指向
memmove(_str+_size,sour,len*sizeof(char));
_size += len;
_str[_size] = '\0';
return *this;
}
//重载+=运算符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(const string& str)
{
append(str._str);
return *this;
}
//从pos位置开始查找字符c第一次出现的位置
size_t find(char c,size_t pos=0)const
{
size_t i=pos;
for(i=pos;i<_size;i++)
{
if(_str[i]==c)
return i;
}
return npos;
}
int *GetNext(const char*p)const
{
int len=strlen(p);
int *next=new int[len];
next[0]=-1;
int j=0;
int k=-1;
while(j<len)
{
if(-1==k||p[k]==p[j])
{
next[j+1]=k+1;
k=next[j+1];
j++;
}
else{
k=next[k];
}
}
return next;
}
//从pos位置开始查找字串第一次出现的位置
size_t find(const char*str,size_t pos=0)const
{
int i=pos;
int j=0;
int len1=_size;
int len2=strlen(str);
if(len2==0)
return 0;
int *next=GetNext(str);//根据字串获的next数组
while(i<len1&&j<len2)
{
while(j==-1||(i<len1&&j<len2&&_str[i]==str[j]))
{
i++;
j++;
}
if(j>=len2)
{
return i-j;
}
else
{
j=next[j];
}
}
return npos;
}
private:
char* _str;
size_t _size;//记录当前已经存储的字符个数
size_t _capacity;//记录当前能存储的字符的空间,不包含最后以'\0'结尾字符串的空间,但是实际我们需要将这个空间开出来;
public:
static const size_t npos;//与STL保持一致
};
size_t const string::npos = -1;
//重载<<运算符
std::ostream& operator<<(std::ostream& cout, const string& str)
{
//cout << str._str << std::endl;
string::const_iterator it=str.begin();
while(it!=str.end())
{
std::cout<<*it;
it++;
}
return cout;
}
std::istream& operator>>(std::istream& cin, string& str)
{
//char tmp[1000];
//std::cin >> tmp;
//str = tmp;
//return cin;
// char ch;
//ch= cin.get();
//while(ch!=' '&&ch!=9&&ch!='\n')
//{
// str+=ch;
// ch=cin.get();
srvar cin>>ch;
//}
char buff[128];
int i=0;
char ch;
ch= cin.get();
while(ch!=' '&&ch!=9&&ch!='\n')
{
buff[i++]=ch;
if(i==127)
{
buff[127]='\0';
str+=buff;
i=0;
}
ch=cin.get();
}
if(i)
{
buff[i]='\0';
str+=buff;
}
return cin;
}
void test7()
{
string str1="Hello World";
size_t index=str1.find("ld",10);
std::cout<<index<<std::endl;
}
void test4()
{
//string str1;
//str1 += '*';
//str1 += "hahaha";
//string str2 = "999";
//str1 += str2;
/* str1.reserve(100);
str1.reserve(10);
str1.push_back('a');
str1.append("hahahaha");*/
string str1 = "Hello World";
std::cin >> str1;
string str2 = "jkjj";
str1.swap(str2);
//str1.erase();
//str1.insert(5,"YY");
//str1.resize(5);
str1.resize(20,'*');
}
//Void test3()
//{
// string str1 = "Hello World!";
// //string::iterators it = str1.begin();
// for (auto it : str1)
// {
// std::cout << it << std::endl;
// }
//}
void test1()
{
string str1;
string str2("Hahaha");
/*std::cout << str1 << std::endl;
std::cout << str2 << std::endl;*/
std::cout << str1.c_str() << std::endl;//cout打印char*时不会及那个char*当作地址来打印;
//而是将char*当作字符串首元素地址来打印,也就是将char*指针当作字符串来打印,遇到\0停止;
std::cout << str2.c_str() << std::endl;
//std::cout << (char*)nullptr << std::endl;
string str3 = str2;
std::cout << str3 << std::endl;
string str4="dfdhbvdawovhewvabkjawbvjewbovnoeiwhvbejw clcKJFHEBWKLVNWEAUOGBUB";
str4 = str3;
std::cout << str4 << std::endl;
std::cout << str4.size() << std::endl;
}
void test2()
{
//const string str1 = "Hello World!";
//for (size_t i = 0; i < str1.size(); i++)
//{
// //str1[i]++;
// std::cout << str1[i] << " ";
//}
//std::cout << std::endl;
string str1 = "abccd";
string str2 = "fgwaghbre ewAR";
std::cout << str1.capacity() << std::endl;
std::cout << str1.size() << std::endl;
/* if (str1 == str2)
{
std::cout << "str1==str2" << std::endl;
}
else
{
std::cout << "str1!=str2" << std::endl;
}*/
}
void test5()
{
string str1;
string str2 = "Hello World!";
string str3 = str2;
std::cout << (str1+=str2+=str2 )<< std::endl;
std::cout << "--------------" << std::endl;
std::cout << str2.capacity() << std::endl;
std::cout << "--------------" << std::endl;
std::cout << str3.capacity() << std::endl;
std::cout << "--------------" << std::endl;
}
void test6()
{
// string str1="xxxxxxxxx";
// str1.erase(1,4);
// std:: cout<<str1<<std::endl;
// std::cout<<str1;
string str="";
// str+='\0';
// str+='\0';
//srvtr+='\0';
// str+='\0';
//tmpstr+="yyyyyyyyy";
std::cin>>str;
std::cout<<str.size()<<std::endl;
std::cout<<str<<std::endl;
}
}
深浅拷贝问题
请移步至博客:C++深浅拷贝
标准库下的string
VS环境下
为了演示方便,以下演示都是在64位平台下验证:
VS平台下,string总共占28个字节,内部结构稍微复杂 ,先是有一个联合体,联合体用来定义string中字符串的存储空间:
当字符串长度小于等于15时,使用内部固定的字符数组来存放
当字符串长度大于15时,从堆上开辟空间;
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个字节。
g++环境下
g++环境下string的总共占4/8字节,g++环境下的string是通过指针来实现的,该指针指向一块空间:
lenth:字符串长度;
capacity:字符串长度;
refcount:引用计数;
像这样实现string,在拷贝和赋值方面效率会很高,为什么这么说呢?
那么g++为什么要这么做?
因为如果每次都老老实实的完成拷贝和赋值的话(也就是向VS环境下的string)是需要代价的,我们需要完成从另一个空间将数据拷贝到其他空间,这是一个不小的时间消耗,但是g++向这样设计的话,就不需要进行数据的拷贝,只需要完成string内部指针的赋值即可,更加高效!
那么如果是多个对象指向同一块空间的话,万一我想对某个对象进行修改或删除操作呢?
那么这时候我们就不能在str1与str2共享的空间上直接进行修改,因为如果我们对其直接进行修改,str1对象上的数据也会跟着被修改!这不是我们所希望的,遇到这种情况,g++提供了“写时拷贝的技术” 就是当我们需要对str2对象进行修改数据的操作时,OS会在内存重新开辟一块空间给str2,然后让str2在新空间上进行修改操作,这样就避免了修改str2的数据时误修改了str1的数据!
g++的这种作法其实就是一种赌徒思想,它在赌用户不会进行修改数据的操作,赌对了它就赚了,赌错了,就老老实实开辟空间,然后拷贝数据!