目录
1 STL简介
2 string类
2.1 构造 析构 拷贝
2.2 size length
2.3 [ ]的使用
2.4 迭代器
2.5 Push_Back和append
3 sort的使用
1 STL简介
STL是一个标准库,是C++标准库的一个重要部分,那么什么是STL?STL是一个模板库,包含了算法框架和数据结构。
STL有不同的版本,比如原始版本,P.J 版本,R.W 版本,SGI版本,不同版本有不同的特点,比如SGI版本的可移植性好,比如P.J版本的可读性较差。
STL这个库里面有六大部分,分别是算法,容器,迭代器,仿函数,空间配置器,配接器。我们即将介绍的,是容器部分的string,可以形象的把容器理解为数据结构,里面还有链表list,树set,顺序表vector等。
这里简单说明,就进入string的正式部分了。
2 string类
string首先是一个类,是委员会发明类之前的一个“前车”,所以成员函数部分可能有点冗余,比如函数有120多个。
string类是字符数组,可以进行增删查改,但是这里的字符不一定是一个字节,通过后面的学习就知道了,比如w_char就是4字节,我们先不做了解。
2.1 构造 析构 拷贝
constructor就是构造的意思,所以这里面进入,就是构造函数的真面目了:
构造函数就有7个,这也是造成冗余的原因之一.
int main()
{
string s;
return 0;
}
第一个构造函数使用如上,即什么也不干,那么s里面就是空的,什么也没有,也可以通过调试观察里面有什么,当然,里面有其他的,我们先不做深究。
int main()
{
string s1("abcdefg");
cout << s1 << endl;
return 0;
}
第二个构造使用如上,很简单,但是为什么支持直接打印呢?因为流重载重载了string类,所以可以打印,那么这也是个验证的好方法。const string& str就是常量字符串的意思,所以我们给上常量字符串就行了。
int main()
{
string s1("abcdefg");
string s4(s1);
return 0;
}
第四个构造使用如上,也就是给一个字符串的指针就可以了,使用很常见,也很实用。
以上3个构造函数的最常用到的,后面三个可以作为了解,毕竟有点鸡肋的。
int main()
{
string s1("abcdefg");
string s2(s1, 0, 3);
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
这个构造重载的参数有3个,分别是常量字符串,pos,npos,我们看文档的时候不如连蒙带猜,平时的pos使用是位置的意思,比如之前链表,顺序表的时候pos是位置的索引,即从常量字符串的pos位置开始,那么什么时候结束呢?
到npos位置的时候结束:
查看文档发现,npos的值是-1?但是是size_t类型的,并且下面写到Maximum value for size_t,结合之前介绍的char的轮盘,可以知道npos是42亿多。
那么函数的意思就是从pos位置拷贝一直到npos?什么字符串要占4个G的大小?
所以结合文档(string太短了或者len的值是npos,就会直接拷贝完)
int main()
{
string s1("abcdefg");
string s2(s1, 0);
string s3(s1, 0,30);
return 0;
}
第一个我们只给了两个参数,那么函数就使用缺省值,即npos,第二个给了三个,但是30明显超出了s1的大小,所以这俩个字符串都是拷贝完s1,实际上使用的时候不会有第二种的写法。
int main()
{
string s5("Hello world",5);
cout << s5 << endl;
return 0;
}
第五个函数的意思就是从一个常量字符串里面拷贝多少个字节进去,我们从Hello World里面拷贝5个字节进去,那么打印出来就是Hello。
int main()
{
string s7(10, 'x');
cout << s7 << endl;
return 0;
}
第六个函数的意思就是拷贝n个c字符到string里面去。
以上3个作为了解,实际用处不太大的。
第7个涉及到了迭代器,暂时不介绍。
destructor即析构函数,析构没什么特殊的,出了作用域,string就自己销毁了,不需要自己去销毁。
拷贝有个很舒服的地方在于可以直接使用=:
当然,重载也重载了三个拷贝函数,
int main()
{
string s1 = "abcdefg";
string s2 = s1;
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
使用起来也是很方便,底层是怎么操作的我们就不用深究了,但是肯定是发生了隐式类型准换的,s1想要引用,就加const即可。
第三个函数就不用深究了,作用不大。
2.2 size length
在C语言中,我们计算数组的大小常常是size/size,在string中,我们直接调用size就行了:
int main()
{
string s1 = "abdeef";
cout << s1.size() << endl;
cout << s1.length() << endl;
return 0;
}
但是通过文档,我们发现size和length没有区别,都是返回string的长度,那么出现两个相同功能的函数的原因是因为string发明太早了,我们对于一个字符串可以说大小也可以说长度,顺序表也是,但是对于后面的树等结构,长度不太说的过去,所以对于string来说,length最初是专门为它服务的,大小是都能用的,length只有string可以用,所以最好后面统一使用size。
2.3 [ ]的使用
以前访问数组我们通常使用下标 + [ ]进行访问,这点在string里面都是可以使用的,但是这里实际上和数组访问有区别,这里不是指针偏移,这是调用的函数,[]重载。
int main()
{
string s1 = "abcdefg";
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
return 0;
}
可是如果到这里你觉得[]就介绍完了你就大错特错辣。
class string
{
public:
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
string里面的[]重载函数如上,第一个重点是引用返回,因为是引用返回,所以可以减少拷贝,第二个重点是,assert,因为使用了暴力检查,所以越界了就会直接报错:
既然是引用返回,所以我们可以修改字符串里面的内容,但是[]重载有两个版本,一个是普通版本没有const,一个是const版本,当我们不希望string被修改的时候就可以:
int main()
{
const string s2("123456");
s2[1] = 'a';
return 0;
}
2.4 迭代器
迭代器iterator,有如上几个函数,实际上我们了解前4个就可以了,后面以c开头的其实就是const,表示迭代的元素不能被修改而已。
先看使用:
int main()
{
string s1("Hello world");
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
it1++;
}
return 0;
}
因为iterator是公有函数,所以使用的时候必须加上类名+类域访问符,使it1 = s1.begin(),就是相当于it1从字符H开始,end同理,end就是字符串的最后一个标志->'\0',那么这里看起来十分像指针,但是并不是,不如我们来看看类型。
int main()
{
cout << typeid(string::iterator).name() << endl;
return 0;
}
吓人吧?但是目前来说我们可以把它当作指针使用,但是我们只发挥了它的一层功力。
我们现在讨论一个问题,遍历一个字符数组有多少种方式?
遍历方式1:下标 + []
int main()
{
string s1("Hello world");
for (int i = 0; i < s1.size(); i++)
{
cout << s1[i];
}
return 0;
}
遍历方式2:迭代器
int main()
{
string s1("Hello world");
string::iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1 << " ";
it1++;
}
return 0;
}
遍历方式3:范围for
int main()
{
string s1("Hello world");
for (auto e : s1)
{
cout << e;
}
return 0;
}
第一种方式没有什么好说的,第二种是迭代,第三种实际上底层也是调用的迭代器,来看看:
已经出现了刚才那个一长串的类型了,就不用多说了吧?
所以范围for循环底层也是通过迭代器实现的。
当我们进入到begin的文档就会大仙有两个版本,const和普通版本:
int main()
{
const string s1("Hello world");
string::const_iterator it1 = s1.begin();
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
return 0;
}
const的作用不用多说,这里要注意的是为什么const_iterator,而不是const iterator?
我们类比指针,如果是const int* p ,那么修饰的是*p,指向不能变,如果是int* const p,就是修饰的p本身,那么如果是const iterator,修饰的就是迭代器本身,本身不能改变,还谈何遍历呢?
所以C++采用的方式是const_iterator。
接着就是rbegin的使用,如果说begin是正方向遍历,rbegin就是逆方向遍历,r也不难猜出来,Reverse,逆置。
使用如下:
int main()
{
string s1("Hello world");
string::reverse_iterator it1 = s1.rbegin();
while (it1 != s1.rend())
{
cout << *it1;
it1++;
}
return 0;
}
加个reverse_,同const一样的,使用了之后begin都要变成rbegin。
可能有疑问了,这里++?为什么不是--,实际上++是重载之后的--,不难想象++就是倒着回去遍历的。
2.5 Push_Back和append及+=
数据离不开插入数据:
Push_back即尾插,在字符串末尾插入一个数据,插入之后,对应的字符串长度也会增加。
int main()
{
string s1("Hello world");
s1.push_back('x');
cout << s1 << endl;
return 0;
}
push_back是插入一个字符,append是插入一个字符串,append插入的字符串可以是一个字符吗?那也是可以的,一个字符也可以是字符串:
int main()
{
string s1("Hello world");
s1.append("x");
s1.append("abcd");
cout << s1 << endl;
return 0;
}
当然,这里也是有许多重载的,我们也是可以连蒙带猜的去猜使用方法的,但是实际上使用最多的是第三个,后面的其实,用处不太大;
int main()
{
string s1("Hello world");
string s2("123456");
s1.append(s2,4,5);
s1.append(s2,4);
cout << s1 << endl;
return 0;
}
这里就不介绍了看看文档咯。
当然类似的,有插入就有删除,比如尾删pop_back等,就不介绍了:
但是呢,还是略显麻烦了,不就是加个字符吗?string有个堪称神力的重载:
不管是push_back还是append在这个重载面前都黯然失色了,因为这个太方便:
int main()
{
string s("aaa");
s += "bbb";
s += "c";
cout << s << endl;
return 0;
}
要加什么直接加上去就行了,很方便。
3 sort的使用
讲了这么多string内部的函数,这里就介绍一点实际应用,string是一种容器(数据结构),那么容器是存储数据的,算法是修改数据的,他们之间的联系靠迭代器完成,为什么说迭代器不是冗余的设计,因为迭代器是两者之间的桥梁,使用如下:
int main()
{
string s1("Hello world");
sort(s1.begin(), s1.end());
cout << s1 << endl;
return 0;
}
这里按照字典序排列,即ASCII码值排列字符串,sort所在的头文件是algorithm,中文意思就是算法的意思,sort的使用要注意左闭右开
左闭右开有个优点就是好计算总共排序多少元素,因为左减右就直接算出来了,这里是对整个string进行排序,如果想要进行部分排序,只需要:
int main()
{
string s1("Hello world");
sort(s1.begin(), s1.end() + 5);
cout << s1 << endl;
return 0;
}
这里是对前5个元素进行排序,可以看到离不开迭代器。
感谢阅读!