当前位置:首页 » 《休闲阅读》 » 正文

C++ —— string类(上)

2 人参与  2024年11月25日 10:01  分类 : 《休闲阅读》  评论

点击全文阅读


目录

string的介绍

string类功能的使用介绍

constructor —— 构造

介绍使用(1)(2)(4) :构造、拷贝构造、带参构造

 介绍(3):拷贝string类对象的一部分字符,从pos位置开始,拷贝len个字符

 npos

介绍(5):取字符串的前n个字符拷贝

介绍(6) :连续n个C字符初始化

介绍(7):用迭代器区间构造

destructor —— 析构

operator= —— 赋值重载

string类对象的容量操作

size()  length()    max_size()

capacity()

reserve() 和 resize()

clear()

empty()

遍历及访问string所需的一些接口

operator[] —— []运算符重载

iterator —— 迭代器 (正向)

begin()

end()

reverse_iterator —— 反向迭代器

rbegin和rend

const修饰的迭代器

范围for

auto

 typeid().name()


string的介绍

string严格来说是属于C++标准库的,不是属于STL(标准模板库)。

STL:

我们看到里面其实是没有string的,string产生的比STL较早的,在头文件中,会单独看到一个string的头文件。C++文档

所谓的string其实就是一个字符串 ,它要解决的就是要管理字符串。(日常当中字符串是很多的,如:身份证号码,家庭地址,电话号码等等)

打开string头文件可以看到:

 有4种,这个与所谓的编码有关系,这里我们先看string:

由于编码的原因,string是typedef出来的,它的原生类型是:

//原生类型basic_string<char>

 除了char,还有其他字符的类型(编码的原因)

//原生类型basic_string<char16_t>

//原生类型basic_string<char32_t>basic_string<wchar_t>

注意:basic_string其实是一个模板,所以string底层还是一个模板。

 所以,string是一个管理字符串的类。

string的介绍:

总的来说,string是一个对象,是用一个字符的顺序表实现的(也就是字符数组实现的),所以它就是一个字符顺序表(或字符数组),它可以动态的增长。(可以主要兼容UTF-8)

 string底层大致成员变量如下:

class string{public:   /*成员函数(功能接口)*/private:char* str;   //指针指向堆上的空间,这些空间存储对应字符串size_t size;   //有效字符个数size_t capacity;  //容量};

下面将介绍string类的常用接口说明。 

string类功能的使用介绍

string主要包含接口如下:

我们要使用string类要记得包含一个头文件:

#include <string>   //C++//C语言的是#include<string.h>    //注意两者区分

constructor —— 构造

string类对象的常见构造
(constructor)函数名称功能说明
string()构造空的string类对象,即空字符串
string(const char* s) 用C-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s) 拷贝构造函数

但string构造总的来说是有七种构造方式的:

我们来看C++98版本的,string它有七种构造方式 ,下面来介绍一下:

default (1)string();        //无参构造(也是默认构造,构造一个空字符串,长度为0个字符)copy (2)string (const string& str);    //拷贝构造substring (3)string (const string& str, size_t pos, size_t len = npos);   //子串构造函数(复制str中,从字符位置pos开始,到len位置字符的部分)from c-string (4)string (const char* s);    //带参构造(用一个常量字符串构造)from sequence (5)string (const char* s, size_t n);    //拷贝前n个字符初始化fill (6)string (size_t n, char c);    //连续n个C的字符去初始化range (7)template <class InputIterator>string  (InputIterator first, InputIterator last);    //迭代器区间初始化
介绍使用(1)(2)(4) :构造、拷贝构造、带参构造

我们先来介绍使用(1)(2)(4):

(string重载了流插入、流提取)

#include<iostream>#include<string>using namespace std;int main(){string s1;           //默认构造string s2("12345");  //带参构造string s3(s2); //拷贝构造//string也重载了流插入流提取(>>,<<)cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;cin >> s1;cout << s1 << endl;return 0;}

 介绍(3):拷贝string类对象的一部分字符,从pos位置开始,拷贝len个字符
substring (3)string (const string& str, size_t pos, size_t len = npos);   //拷贝构造的一个变形//也就是拷贝一部分字符,从pos位置开始,拷贝len个字符
//接着上面演示的构造代码int main(){string s1;           //默认构造string s2("1234567");  //带参构造string s3(s2); //拷贝构造//string也重载了流插入流提取(cout,cin)cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;cin >> s1;cout << s1 << endl;string s4(s2, 2, 4);  //这里就是让s4去拷贝s2的下标为2的位置开始,拷贝4个字符cout << s4 << endl;return 0;}

我们观察输出结果,看到从3开始拷贝四个字符到6,3456四个字符就到s4里去了。若此时这里给的字符超过字符串长度,会不会报错呢?

string s2("1234567"); string s4(s2, 2, 15);   //让它从pos=2开始拷贝15个字符

我们观察结果,是没有报错的:

文档中: 

 

也就是说string太短了,不够len个字符,它就直到它结束为止(有多少拷贝多少 );这里还说或者为npos也是拷到它的结束。

第三个构造它的len是有缺省值npos的,如果不传第三个参数len,那么它就用这个缺省值,直到拷贝它的结束为止。

string s2("1234567"); string s4(s2, 2);cout<<s2<<endl;cout<<s4<<endl;

运行结果:

 下面对npos介绍一下

 npos

npos是一个string的const静态的成员变量,它可以直接在类里面用,若在外面用的时候,需要指定类域。

npos的值真实的值其实不是-1,在这里-1存的就是补码(-1补码是全1) ,这里又赋值给size_t类型,size_t是unsigned int(无符号整型),-1就会变成整型最大值。

所以这里npos要表达的是:从pos位置取len个字符,如果len给npos,就是要取整型的最大值,

字符串不可能有这么长(字符串中遇到\0会停止),且又给了整型的最大值了,就是要让它拷贝到字符串的结束。

介绍(5):取字符串的前n个字符拷贝
string (const char* s, size_t n);    //第(4)个(带参构造)的变形//取字符串的前n个字符拷贝

 

也就是取一个字符串的前n个字符初始化。

测试一下: 

int main(){string s6("hello world", 5);cout << s6 << endl;return 0;}

 运行结果:

介绍(6) :连续n个C字符初始化
string (size_t n, char c);    //n个C字符初始化

也就是连续n个C的字符初始化,例如:'X'就是一个C字符。

int main(){string s7(10, 'X');    //用10个X字符初始化cout << s7 << endl;return 0;}

运行结果: 

介绍(7):用迭代器区间构造
int main(){string s0("Initial string");string s8(s0.begin(), s0.begin() + 7);  //迭代器区间构造cout <<"s0: "<< s0 << endl;cout <<"s8: " << s8 << endl;}

运行结果:

可以看到s8中用了s0起始位置开始的7个字符初始化了 。

destructor —— 析构

string的析构函数会自动调用,string的底层是一个动态开辟的数组,构造和析构函数是自动调用的(类和对象这里有介绍)。

operator= —— 赋值重载

赋值运算符重载也是默认成员函数(类和对象有介绍)。

string (1)string& operator= (const string& str);  //支持string的赋值c-string (2)string& operator= (const char* s);      //支持char*的赋值character (3)string& operator= (char c);             //支持一个字符的赋值

 测试:

int main(){string s1;           string s6("hello world", 5);s1 = s6;    //本质就是调用了operator=cout << s1 << endl;s1 = "abcde";cout << s1 << endl;s1 = 'x';cout << s1 << endl;return 0;}

运行结果:

string类对象的容量操作

函数名称功能说明
size返回字符串有效字符长度
length返回字符串有效字符长度
max_size获取最大长度(能最大开多长)
capacity返回空间容量的大小
empty检查字符串释放为空串,若是空串返回true,否则返回false
clear清空有效字符
reserve为字符串预留空间
resize将有效字符的个数改成n个,多出的空间用字符c填充

size()  length()    max_size()

size()和 length()它们两个功能上完全一致,只是一般size用的比较多,是为了与其他容器接口保持一致。

这里拿size()来说,string提供了一个size的接口,它是用来获取string的长度(有多少个字符)。

 max_size()是用来获取它最大的长度,就是它最大能开多长

int main(){string s("FFDUST");cout << s.length() << endl;cout << s.size() << endl;cout << s.max_size() << endl;}

capacity()

 capacity()接口也就是返回该string当前空间容量的大小是多少

int main(){string s("FFDUST");cout << "s的空间大小为:"<<s.capacity() << endl;}

 

(注意它和size,length接口的区别)

这里还要注意容量扩容问题(每个编译器处理扩容几倍不一致),观察下面程序:

int main()    {string s;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';}}return 0;}

 VS下结果:

 所以VS下,扩容是先扩2倍,再后面扩1.5倍。

reserve() 和 resize()

 reserve()接口就是先给该对象预留多少空间,不改变有效元素个数,若reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小(这里VS下不会缩容,其他编译器可能会缩容,C++规定没有约束)。

int main(){string s("FFDUST");cout << "s的空间大小为:" << s.capacity() << endl;s.reserve(100);cout << "s的空间大小为:" << s.capacity() << endl;s.reserve(1);cout << "s的空间大小为:" << s.capacity() << endl;return 0;}

VS这里会做整数倍对齐,所以这里capacity可能会比传的参数较大一些。

注意:谈string容量的时候默认不包含\0的,所以开空间实际上会多1位用来放\0,但capacity返回的是不带\0的。

reserve的用法:提前开空间,避免扩容,提高效率。

int main(){string s;s.reserve(100);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';}}return 0;}

这里就是给s预留了足够大的空间,再插入时候不会发生扩容。 

resize()接口(将有效字符的个数改成n个,多出的空间用字符c填充),相当于扩容插入。

这里它是有两个版本的:resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0(也就是\0)来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意的是:resize若比size小的话会删除数据(缩容不确定),比capacity大就会扩容。

int main(){string s("ffdust");cout << s << endl;cout << "s的空间大小为:" << s.capacity() << endl;s.resize(100);      //默认插入\0cout << s << endl;cout << "s的空间大小为:" << s.capacity() << endl;}

int main(){string s("ffdust");cout << s << endl;cout << "s的空间大小为:" << s.capacity() << endl;s.resize(100,'1');        //用字符1填充多出的元素空间,注意这里会扩容cout << s << endl;cout << "s的空间大小为:" << s.capacity() << endl;}

 

 clear()

clear()也就是清空有效字符,一般严格来说不会清理掉容量,只是把数据清理掉了

int main(){string s("ffdust");cout << s << endl;cout << &s << endl;cout << "s的空间大小为:" << s.capacity() << endl;s.clear();cout << s << endl;cout << &s << endl;cout << "s的空间大小为:" << s.capacity() << endl;return 0;}

 运行结果:

 empty()

empty()接口时用来判空的(有没有字符),也就是检测字符串是否为空串,若是返回true,否则返回false。

int main(){string s("ffdust");cout << s << endl;if (s.empty()){cout << s << ",是空串" << endl;}else{cout << s << ",不是空串" << endl;}cout << "s的空间大小为:" << s.capacity() << endl;s.clear();cout << s << endl;if (s.empty()){cout << s << ",是空串" << endl;}else{cout << s << ",不是空串" << endl;}cout << "s的空间大小为:" << s.capacity() << endl;return 0;}

运行结果:

遍历及访问string所需的一些接口

函数功能功能说明
operator[]返回pos位置的字符,const string类对象也以调用
begin()+end()begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
rbegin()+rend()rbegin获取最后一个字符迭代器 + rend获取第一个字符前一个位置的迭代器
范围forC++11支持更简洁的范围for的新遍历方式

遍历string有三种方式:

下标+[]迭代器范围for

operator[] —— []运算符重载

string重载operator[],访问pos位置的字符,返回这个字符的引用。 (可以读和修改)

string重载了[],这样就可以让string当成数组一样使用下标访问。string底层意思大致理解是有一个指针,指针指向一个数组,是动态开辟的,它还要有size和capacity,重载operator[],就可以让它去访问第i个位置的字符:

class string{public:    char& operator[](size_t i)    {return _str[i];           //返回第i字符的引用(这里引用返回,不只是可以减少拷贝,还能让它被修改)    }private:char* str;size_t size;size_t capacity;};

使用如下:

int main(){string s6("hello world");cout << s6 << endl;s6[5] = 'a';     //将下标为5的字符修改成acout << s6 << endl;return 0;}

运行结果:

就相当于用数组一样,可以获取这个位置的字符,也可以修改这个位置的字符,但这里的底层就是函数调用,调用的operator[],注意:这里越界了可以检查出来的。 

char& operator[](size_t i){    assert(i < _size);   //这里断言,只要越界了就直接报错return _str[i];}

我们看到这里15位置越界访问了,就报错了。

所以,我们可以搭配着size接口防止越界访问来一个一个字符的遍历:

int main(){string s6("hello world1111");    //下标+[]for (int i = 0; i < s6.size(); i++){cout << s6[i] << " ";}cout << endl;return 0;}

 运行结果:

iterator —— 迭代器 (正向)

 迭代器遍历:

int main(){string s6("hello world");    //迭代器遍历——正向string::iterator it = s6.begin();while (it != s6.end()){cout << *it << " ";++it;}cout << endl;return 0;}

正向遍历(begin+end) :

注:迭代器是STL六大组件之一,迭代器是用来遍历和访问这些容器的 。(迭代器有可能是原生指针,也有可能不是)。 

迭代器是类似于一个指针一样的东西,通过解引用可以访问对应位置的数据,迭代器还规定,不管它底层是怎么定义的,它是属于对应容器的类域。(上面string的迭代器属于string的类域,所以要写成 string::iterator),所以所有的容器都可以通过迭代器来访问(通用的访问容器的方式)。

 begin()

begin()就是返回开始位置的迭代器。

string这里就是返回的是第一个有效字符的位置的迭代器。

注:这里有两个版本,第一个就是普通对象用来调用的,第二个是const修饰的对象的const迭代器所调用(这里普通对象也能调用,但是只能读,不能写)。

end()

end()返回的是最后一个有效数据的下一个位置的迭代器。

string这里就是返回最后一个有效字符的下一个位置的迭代器。

注:这里有两个版本,第一个就是普通对象用来调用的,第二个是const修饰的对象的const迭代器所调用(这里普通对象也能调用,但是只能读,不能写)。

reverse_iterator —— 反向迭代器

 这里就与iterator完全相反着了

 反向遍历(rbegin+rend): 

int main(){      //反向迭代器遍历    // string::reverse_iterator rit = s6.rbegin();    // C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型    auto rit = s6.rbegin();    while (rit != s6.rend())    {    cout << *rit <<" ";    ++rit;    }    cout << endl;}

 运行结果:

rbegin和rend

这两个接口就相当于begin与end接口相反的。  

也就是rbegin获取最后一个字符迭代器 , rend获取第一个字符前一个位置的迭代器

const修饰的迭代器

 const修饰的对象,要用迭代器遍历时,是需要const修饰的迭代器的,它不能使用普通迭代器,

const迭代器修饰的是迭代器的指向,而不是迭代器的本身(类似于const修饰指针)

如下所示:

int main(){    //const对象    const string cs("const hello world");    //const修饰的(正向)迭代器    string::const_iterator cit = cs.begin();  //这里也可以用cbegin接口,不过begin接口有提供的const修饰的版本,这就已经够用了    while (cit != cs.end())     //cend同上也可以用    {    //错误C3892“cit” : 不能给常量赋值    //*cit += 'x';    cout << *cit << " ";    ++cit;    }    cout << endl;    //const修饰的反向迭代器    string::const_reverse_iterator crit = cs.rbegin();   //这里也可以用crbegin接口,不过rbegin接口有提供的const修饰的版本,这就已经够用了    while (crit != cs.rend())    //crend同上也可以用    {    //错误C3892“crit” : 不能给常量赋值    //*crit += 'x';    cout << *crit << " ";    ++crit;    }    cout << endl;    return 0;}

 运行结果:

const修饰的迭代器,普通对象也是可以使用的(也就是权限可以缩小,但不能放大),不过只能读,不能写。

int main(){    //普通对象string s("hello world");    //const修饰的迭代器string::const_iterator it = s.begin();while (it != s.end()){//错误C3892“it” : 不能给常量赋值//*it += 'x';cout << *it << " ";++it;}cout << endl;//const修饰的反向迭代器string::const_reverse_iterator rit = s.rbegin();    while (rit != s.rend())    {    //错误C3892“rit” : 不能给常量赋值    //*rit += 'x';    cout << *rit << " ";    ++rit;    }    cout << endl;    return 0;}

运行结果:

 (这里也可以用auto自动推导它的类型,方便代码的编写)

范围for

对于一个有范围的集合而言,C++11中引入了基于范围的for循环。for循环后的括号由冒号分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。范围for可以作用到数组和容器对象上进行遍历。
auto
在早期 C/C++ 中 auto 的含义是:使用 auto修饰的变量,是具有自动存储器的局部变量。 C++11 中,赋予了 auto 全新的含义即: auto 不再是一个存储类型 指示符,而是作为一个新的类型指示符来指示编译器, auto 声明的变量必须由编译器在编译时期 推导而得。

范围for和auto就可以配合使用来遍历string:

int main(){string s6("hello world");        //字符赋值,自动迭代,自动判断结束for (auto e : s6)  //auto这里自动推导类型为char{cout << e << " ";}cout << endl;return 0;}

 运行结果:

所以这里范围for想表达的是:

注意:范围for的底层角度是跟迭代器一样的,也就是说范围for这段代码被编译了之后会被替换成迭代器。(所以要支持范围for前提是要支持迭代器遍历)

从汇编层可以看到,这里就是被替换成了迭代器: 

 范围for跟迭代器就相当于没有区别,范围for只是方便编写代码。这里对比迭代器,也展示出auto的价值就是缩短代码(简化代码),不过这样会使代码可读性降低。

//这两个it的类型是一致的string::iterator it = s6.begin();//auto会自动推导成上面的string::iteratorauto it = s6.begin();

迭代器是可以修改的。

int main(){string::iterator it = s6.begin();cout << s6 << endl;while (it != s6.end()){*it += 2;           //让里面的每一个值都加等2cout << *it << " ";++it;}cout << endl;for (auto e : s6){cout << e << " ";}cout << endl;return 0;}

 运行结果对比一下:

可以看到每一个值都被改变了,范围for打印数据也受到影响。范围for里也是可以修改数据的,下面在范围for里面打印原来的值:

int main(){string::iterator it = s6.begin();cout << s6 << endl;while (it != s6.end()){*it += 2;           //让里面的每一个值都加等2cout << *it << " ";++it;}cout << endl;for (auto e : s6){        e -= 2;           //只需要让e减等2即可cout << e << " ";}cout << endl;return 0;}

运行结果:

 注意:这里有坑的是,范围for打印出来的结果看似把s6恢复原状了,这时cout一下s6可以看到s6还是没有变回去的。这个原因是:

这里若想在范围for里修改,只需要加& 符号,也就是引用

for (auto& e : s6)    //这里auto自动推导为char类型,                      //若想是char的引用,就需要主动添加一个&//这时e就是s6里面(*it)的别名,也就是取它每个字符的别名{e -= 2;           //这时修改e,就修改了string里的值cout << e << " ";}cout << endl;

注意:用auto声明指针类型时,用auto和auto*没有任何区别(也就是当是一个指针类型时,可以直接写auto即可),但用auto声明引用类型时则必须加上&(auto &)。 

 typeid().name()

 typeid()可以帮助我们观察类型

typeid(变量名).name()   //可以看到该变量的类型
int func1(){return 10;}int main(){int a = 10;auto b = a;auto c = 'a';auto d = func1();cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;int x = 10;auto y = &x;auto* z = &x;auto& m = x;cout << typeid(x).name() << endl;cout << typeid(y).name() << endl;cout << typeid(z).name() << endl;return 0;}

运行结果: 

 

这里auto使用时还需要注意一些事项: 

// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项auto e;

不能这样定义,它不知道e到底是多大,auto定义的变量类型一定是由右边的变量或者函数调用表达式的返回值推导的。

// 不能做参数void func2(auto a) //这里就算给了缺省值也不支持的{}

 auto不能做参数类型,但auto可以做返回值。

// 可以做返回值,但是建议谨慎使用auto func3(){return 3;}

// 编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型auto cc = 3, dd = 4.0;

注意在一行里面定义,定义时要用同一类型

// 编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型auto array[] = { 4, 5, 6 };

auto不能定义一个数组(C++规定)

在这篇先总结一部分string的接口使用,剩下的部分接口总结使用在后续文章介绍。

制作不易,若有不足之处或出问题的地方,请各位大佬多多指教 ,感谢大家的阅读支持!!!   


点击全文阅读


本文链接:http://zhangshiyu.com/post/191642.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1