C++ 中的字符串

第三章 字符串、向量和数组

C++ 中定义了丰富的抽象乐行数据库,比如string和vector是两种重要的标准库类型,前者支持可变长字符串,后者便是可变长集合。还有一种标准库类型是迭代器,配合string和vector使用。

内置数组是一种更基础的类型,string和vector都是对它的抽象。

3.1 命名空间的using声明

声明有两种

显示声明,使用作用域声明符(::)。作用域声明符的作用:编译器赢从操作符左侧名字的作用域中寻找右侧那个名字。如std::cin 表示从标准输入中读取内容,意味着使用命名空间std中的名字cin

using声明:

使用方式如下

using namespace::name;

一旦声明后面可以直接使用名字访问

实例

#include <iostream>
using std::cin;
 
int main()
{
    int i;
    cin >> i;    // cin已经使用using方式声明,可以编译通过
    cout << i;   // 执行错误,没有声明
    std::cout << i;  //采用方式一声明,编译可以通过
    return 0;
}    

3.2 标准库类型

标准库类型string表示可变长的字符序列,使用string类型必须包含string头文件。

#include <string>
using std::string;

3.2.1 定义和初始化string对象

常见的初始化

    string s1 ;             // 默认初始化,s1是一个空字符串
    string s2  = s1;        // s2是s1的副本
    string s2(s1);          // s2是s1的副本
	string s3("hello");      // s3是‘‘value’’的副本,除了字面值最后那个空字符外
    string s3  = "hello";   // s3是该字符串字面值的副本,该字面值除了最后一个空字符外都会被拷贝到新创建的string对象中去
    string s4(10,'c');      //s4的内容是cccccccccc,给定字符重复若干次后得到的序列赋值给string对象

字符串初始化分为2种

  • 拷贝初始化:使用(=)号,编译器等号右侧的初始化值拷贝到新创建的对象中去。
  • 直接初始化:不使用(=)号,则这行的是直接初始化。

多个值初始化需要用直接初始化,其他的可以相互代替,如

	string s3("hello"); 	// 直接初始化
    string s3  = "hello";   // 拷贝初始化
    string s4(10,'c');      // 直接初始化,s4的内容是cccccccccc 等价于 string s4 = string(10,'c') 先创建了一个临时对象用于拷贝 等价于如下两步 1. string tmp = string(10,'c'); 2. string s4 = tmp; 

3.2.2 常用的string操作

常用的语法,while中内容如果不是空白字符或回车,则读取到的内容有效

while(cin >> s){
  .... 
}

读取字符串一整行

getline(输入流,string对象)
程序结果将保存在string对象中,每次读取一整行遇到回车换行符返回    
    

注意:string.size返回的len类型的是string::size_type,编译器允许使用auto或者decltype来推断变量类型,如

atuo len = line.size(); // len的类型是string::size_type,是一个无符号整形,需要注意的是如果len和一个有符号的int比较大小,肯定是小于负数的

3.2.3 处理string对象中的字符

可以使用cctype头文件中定义的标准库函数处理这部分工作

使用基于范围的for语句遍历和处理每一个字符

语法形式

for(declaration : expression)
    statement

其中expression是一个对象,表示一个字符序列。declaration遍历后的一个字符变量,每次遍历都会被赋值。

程序举例

#include <string>
#include <iostream>
using std::string;
using std::cout;
using std::cin;
using std::endl;
 
int main(int argc, char const *argv[])
{
 
    string str("Hello word!!!!");
    //推断str size 类型防止溢出
    decltype(str.size()) punct_cnt=0;
    for(char c:str)
    {
        if(ispunct(c)){
            ++punct_cnt;
        }
        cout << c <<endl; 
    }
    cout<<punct_cnt<<" punct_cnt in "<<str<<endl;
    
 
    return 0;
}
 
输出
    4 punct_cnt in Hello word!!!!

再看一下个字符创转换大写的例子,注意例子中需要使用字符引用,以此改变字符的值

#include <string>
#include <iostream>
using std::string;
using std::cout;
using std::cin;
using std::endl;
 
int main(int argc, char const *argv[])
{
    string s2 ("Hello word !");
    for(auto &c :s2)  //需要使用字符引用,而不是将字符赋值给c
    {
        c = toupper(c);
    }
    cout << s2 << endl;
    return 0;
}
 
 
输出
HELLO WORD !

使用索引访问字符串

字符创下标类型是 string::size_type,其值是**[0,s[s.size()-1]]**传入整数值可以自动完成转换,但是记得不要对一个空字符使用索引,避免未知的异常。

输出字符串的首字母

    string str = "hello word!!";
    if (!str.empty()) // 如果s为空那么,str[0]将会出现未定义
        cout << str[0]<<endl;
 
// 输出 h

修改程序的首字母为大写,然后打印

 string str = "hello word!!";
    if(!str.empty())
        str[0] = toupper(str[0]);
 
    cout<<str<<endl;     

修改字符串第一个单词转换为大写

    string str = "hello word!!";
    if(str.empty())
        return 0;
    for(decltype(str.size()) index=0 ; index != str.size() && !isspace(str[index]);++index)
       str[index] = toupper(str[index]);
    
    cout << str <<endl;
    
输出
HELLO word!!

注意检查下标

使用下标是必须确保其在合理范围内么也就是说,下标必须大于等于0,小于size值。这里建议下标类型统一使用string::size_type,因为此类型是无符号整数,可以确保下标不会小于0,因此只需要保证下标小于size() 值即可。

3.3 标准库类型vector

需要包含头文件

#include <vector>
using std::vector;

C++ 中没有java泛型的概念

3.3.1 定义和初始化vector对象

vector<T> v1        		v1是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1)    		v2是包含有v1所有元素的副本
vector<T> v2 = v1   		等价于v2(v1),v2是包含v1所有元素的副本
vector<T> v3(n,val) 		v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n) 			v4包含了n个重复的执行了初始化的对象
vector<T> v5{a,b,c,d....}   v5包含了初始化个数的元素,每个元素被赋予相应的初始化值
vector<T> v5={a,b,c,d...}   等价于v5{a,b,c...}

举例

vector<string> svec; //默认初始化,svec不含任何元素
vector<int> ivec2(svec);   //把svec的元素拷贝给ivec2
vector<int> ivec3 = svec;  //把svec的元素拷贝给ivec3
 
vector<string> ivec4(ivec2); //错误:svec4的元素是string对象,不是int,无法直接赋值

列表出初始化vector对象

//三种初始化
// 1. 拷贝初始化(即使用=)特点:只能初始化一个值
vetor<string> articles = {"a","an","the"};
// 2.提供类内初始化,只能使用开呗初始化或使用花括号初始化
 
// 3. 使用花括号初始化,如果提过的是是初始化元素值的列表,则只能使用这种形式初始化
vector<string> v1{"a","an","the"}
 

创建指定数量的元素

vector<int> ivec(10,-1);    //10个int类型元素,每个都被初始化为-1
vector<string> svec(10,"hi!") //10个string类型的元素,每个都被初始化为"hi!"
 
    

值初始化

一般情况下vector可以创建不用刻意指定初始化值,此时库会根据类型创建初始化值,并把这个值赋值给容器中的所有元素。

  • 如果对象元素类型是内置类型(常量值):比如int,则元素初始化自动设置为0
  • 如果元素是其他类型,比如string,则元由类默认初始化。
vector<int> ivec(10);       //10个int类型元素,每个都被初始化为0
vector<string> ivec(10);    //10个string类型元素,每个都被初始化为空string对象

注意

  1. 有些类型不支持默认初始化,需要提供初始化值

注意列表初始值是元素数量还是值

如果只传递一个数字,并且vector的类型是int那就很容易误解,这个数字既可以是数量也可以是值,如vector

vector<int> v1(10);     	//v1有10个元素,每个值是0
vector<int> v2{10};     	//v1有1个元素,值是10
 
vector<int> v3(10,1);     	//v1有10个元素,每个值是10
vector<int> v4{10,1};     	//v1有2个元素,10和1

如果使用的是圆括号,可以说提供的值是用来构造(construct)vector对象的,例如,v1的初始值说明了vector对象的容量,如果传入两个值,一个说明容量一个说明值

如果是花括号,则表示想用列表初始化vector对象。

上面的定义时初始化赋值适合三种情况

  1. 初始化值已知且数量少
  2. 初始化值是另一个vector对象的副本
  3. 所有元素的初始值都一样

3.3.2 向vector对象中添加元素

性能方面,定义并初始化性能最好,不用扩容

vector支持的操作

v.empty()			//如果v不包含任何元素,返回真,否则返回假
v.size()			//返回v中元素的数量
v.push_back(t)		//向v的尾端添加一个值为t的元素
v[n]				//返回v中的第n个位置上的元素的引用
v1=v2				//用v2中的元素的拷贝替换v1中的元素
v1={a,b,c}			//用列表重点额元素的拷贝提花v1中的元素
v1==v2				//v1和v2相等当且仅当他们的元素数量相同且对应位置的元素都相同
v1!=v2				
<,<=,>,>=			//以字典顺序进行比较
#include <iostream>
#include <vector>
using namespace std;
 
int main(int argc, char const *argv[])
{
    vector<int> svec;
 
    for (size_t i = 0; i < 10; i++)
    {
        svec.push_back(i);
    }
 
    for (auto num : svec)
    {
         num *= num;
    }
 
    for (auto num : svec)
    {
        cout << num;
    }
    cout << endl;
 
    //程序中只有定位为引用类型才能改变值
    for (auto &num : svec)
    {
        num = num * num;
    }
    for (auto num : svec)
    {
        cout << num << " ";
    }
    cout << endl;
 
    return 0;
}
 
//输出
0123456789
0 1 4 9 16 25 36 49 64 81 
 

vector的size是size_type类型的

要使用size_type,首先指定他是有那种类型定义的。vector对象的类型总是包含着元素的类型

vector::size_type //正确

vectore::size_type //错误

程序实现了0-100之间成绩区间统计,每10个一个统计,100分单独统计为满分

#include <iostream>
#include <vector>
using namespace std;
 
int main(int argc, char const *argv[])
{
    //程序实现了0-100之间成绩区间统计,每10个一个统计,100分单独统计为满分
    vector<unsigned> sorces(11, 0);
    unsigned grade;
    cout << "请输入学士成绩[0,100]" << endl;
    while (cin >> grade)
    {
        if (grade < 0 && grade > 100)
        {
            cout << "成绩不合法,程序将退出" << endl;
            break;
        }
 
        int index = grade/10;
        sorces[index] +=1; 
        cout << "请继续输入学士成绩[0,100]" << endl;
    }
 
    for (auto sorce: sorces)
    {
        cout<< sorce << " ";    
    }
    
    cout << endl;
 
    return 0;
}
 
 
 
请输入学士成绩[0,100]
0
请继续输入学士成绩[0,100]
1
请继续输入学士成绩[0,100]
2
请继续输入学士成绩[0,100]
12
请继续输入学士成绩[0,100]
10
请继续输入学士成绩[0,100]
20
请继续输入学士成绩[0,100]
30
请继续输入学士成绩[0,100]
40
请继续输入学士成绩[0,100]
50
请继续输入学士成绩[0,100]
60
请继续输入学士成绩[0,100]
50
请继续输入学士成绩[0,100]
aa
3 2 1 1 1 2 1 0 0 0 0 
 

不能用下表形式添加元素

有一个很大的误区是通过下标方式来添加元素。但是c++中无法通过下表访问一个索引不存在的元素

vector<int> ivec;   //空vector对象
 
for decltype(ivec.size() ix = 0; ix != 10; i++)
	ivec[ix] = ix;    //严重错误:ivec不包含任何元素

上面这个错误很典型,ivec是一个空vector,不包含任何元素,当然也就不能通过小标方位任何元素!正确的操作应该使用ivec.push_back(t)添加元素

vector<int> ivec;   //空vector对象
 
for decltype(ivec.size() ix = 0; ix != 10; i++)
	ivec.push_back(ix);   

vector对象(以及String对象)的下标运算符可以用于访问已存在的元素,但是不能用于添加元素。

记住:只能对确一致存在的元素执行下标操作!(一个很好的操作是,使用前检查其size)

vector ivec; //空vector对象

cout << ivec[0]; //错误:ivec不包含任何元素

vector ivec2(10); //含有10个元素的vector的对象

cout << vector[10]; //错误:ivec2元素的合法索引是从0-9

试图访问一个不存在的元素将引发错误,但是编译器编译期间不会被发现,只有程序运行时产生一个不可预知的值

通过下标访问一个不存在的元素行为非常常见,并且会产生非常严重的后果。比如缓冲器溢出就是其中一种,这是导致PC以及其他设备上应用程序出现安全问题的一个中烟原因

建议使用for循保证范围

#include <iostream>
#include <vector>
using namespace std;
 
int main(int argc, char const *argv[])
{
   vector<string> words;
   string word;
   cout << "请单词,请使用回车结束输入" << endl;
     while (cin >> word)
    {
        if (word=="\n")
        {
           break;
        }
        words.push_back(word);
        
    }
 
    //数量统计
    cout << "单词总数"<< words.size()<<":";
 
    //转大写
    for(auto &str:words)
    {
        for(auto &c:str)
    {
        c=toupper(c);
    }
    }
 
    //打印
    for(auto str:words)
    {
        cout << str << " ";
    }
    cout << endl;
 
    return 0;
}
 
请单词,请使用回车结束输入
hello
word
! !
jece
.
单词总数6:HELLO WORD ! ! JECE . 

迭代器

迭代器类似于指针类型,提供了对对象的间接访问,其对象是容器中的元素或者string中的字符。使用迭代器访问元素,迭代器也能从一个元素移动到另一个元素。

有效迭代器:有效迭代器或指向某个元素或指向容器中尾元素的下一位置。

无效迭代器:其他所有情况是无效迭代器

迭代器使用

有迭代器的类内部都会有迭代器成员,并且有begin\end成员,begin成员负责返回指向第一个元素(或第一个字符)的迭代器;

auto b = v.begin(),e=v.end;

end成员负责返回指向容器(或string对象)“尾元素的下一位置”的迭代器,改迭代器知识的是容器中一个本不存在的”尾后”元素。本无意义,仅最为标识,表明所有元素处理完毕(或者是空容器判断);比如一个容器为空则 v.begin() == v.end()

如果容器为空,则begin和end放回的是同一个迭代器,都是尾后迭代器。

迭代器运算

*iter 					//返回迭代器iter所指元素的引用
iter->name				//解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem
++iter					//令iter指示容器的下一个元素
--iter					//令iter指示容器的上一个元素
iter1 == iter2			//判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾迭代器,则相等反之,不相等
iter1 != iter2			

和指针类似,也能通过解引用迭代器来获取它所指示的元素,指向解引用的迭代器必须合法并确实指示某个元素。试图解引用已给非迭代器或者尾后迭代器都是未被定义的行为。

#include <iostream>
#include <vector>
 
using namespace std;
 
 
 
 int main(int argc, char const *argv[])
 {
    string str("hello word !!");
    //使用迭代器的方式判断字符串是否为空
    if(str.begin() !=str.end())
    {
        auto s = str.begin();   //取第一个字符
        *s = toupper(*s);		//当前字符的引用
    }    
    cout<<str<<endl;
 
    return 0;
 }
 输出
     Hello word !!

使用迭代器移动,所有单词变大写

#include <iostream>
#include <vector>
 
using namespace std;
 
 
 
 int main(int argc, char const *argv[])
 {
    string str("hello word !!");
    //使用迭代器的方式判断字符串是否为空
    auto s = str.begin();
    while(s !=str.end())
    {
        *s = toupper(*s);
        s++;
    }    
    cout<<str<<endl;
 
    return 0;
 }
 
 输出
 	HELLO WORD !

迭代器类型

一般我们不知道也无需知道迭代器的精确类型,一般拥有迭代器的标准库使用iterator和const_iterator来表示迭代器的类型

vector<int>::iterator it;       //it能读写vector<int> 的元素
string::iterator it2;			//it2能读写string对象中的字符
 
vector<int>::const_iterator it3;//it3只能读元素,不能写元素
string::const_iterator it4;		//it4只能读字符,不能写字符

const_iterator和常量指针差不多,能读取但不能修改它指向的元素值。相反,iterator的对象可以读写。如果vector对象或string对象是个常量,只能使用constant_iterator;如果vector对象或string对象不是常量,那么既可用iterator也可使用const_iterator。

迭代器和迭代器类型

迭代器这个名称有三种含义:可能是迭代器概念本身,也可能是指容器定义的迭代器类型,还有可能是这某个迭代器对象。

begin和end运算符

begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator:

vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); 		//it1的类型是vector<int>::iterator
auto it2 = cv.begin();		//it2的类型是vector<int>::const_iterator

有时默认操作并不满足我们的需要,比如我们对外的封装中只是希望用户读操作而无需写操作,那我们可以强制使用常量类型。可以使用cbegin和cend强制得到const_iterator.

 string str("hello word !!");
 auto s = str.cbegin();     // s就是一个string::const_iterator类型
 
 vector<int> v(10,1);
 auto iv = v.cbegin();		// iv是一个vector<int>::const_iterator对象

类似于begin和end,上述两个分别返回容器第一个元素和最后元素下一位置的迭代器。区别是无论vector对象本身是否是常量,返回值都是const_iterator。

结合解引用和成员访问操作

解引用迭代器可以获得迭代器所指的对象,如果对象类型是类,有希望访问成员。例如字符串组成的vector的对象来说,想要检查元素是否为空,令it是该Vector对象的迭代器,只需检查it所指字符串是否为空就可以了,代码如下:

(*it).empty();

注意:(*it).empty()重点额圆括号必不可少,该表达式的含义是先对it解引用,然后对解引用的结果再执行点运算。如果不加圆括号,点运算符将由it来执行,而非it解引用的结果:

(*it).empty(); 	//解引用it,然后调用结果对象的empty成员
*it.empty();	//错误:试图访问it的名为empty的成员,但it是个迭代器,没有empty成员

上面的第二个表达式的含义是从名为it的对象中寻找其empty成员,显然it是一个迭代器,没有empty成员,所以报错如下

某些对vector对象的操作将会使迭代器失效

不能再for循环中向vector对象添加元素,会导致循环异常

任何改变vector对象容量的操作,比如push_back,都使得vector对象的迭代器失效。

谨记,凡是使用迭代器的循环体,不要向迭代器容器添加元素。

3.4.2 迭代器运算

迭代器的递增原酸令迭代器每次移动一个元素,所有的白准哭容器都有支持递增运算的迭代器。类似的,也能用==和!=对任意标准库类型的两个有效迭代器进行比较。

string和vector的迭代器提供了更多额外的运算符,一方面可以使迭代器的每次移动跨过多个元素,另外也支持迭代器进行关系运算。所有这些运算被称作迭代器运算。

iter + n 			//迭代器加上一个整数值仍得到一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。此时迭代器要么指示容器内的一个元素,要么指示容器微元素的下一个位置。
iter - n 			//迭代器加上一个整数值仍得到一个迭代器,迭代器知识的新位置与原来相比向后移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者指示容器尾元素的下一个位置
iter1 += n          //迭代器假发的复合赋值语句,将iter1加n的结果赋给iter1   
iter1 -= n          //迭代器减法的复合赋值语句,将iter1减n的结果赋给iter1
iter1 - iter2       //两个迭代器相加的结果是他们之间的距离,也就是说,将运算赋右侧的迭代器向前移动差值个元素后将得到左侧的迭代器。参与原酸的两个迭代器必须指向的是同一个容器中的元素或尾元素的下一个位置
>>=<<=       //迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说前者小于后者。参与运算的两个迭代器必须指向的是同一个容器中的元素或者尾元素的下一位置
 

迭代器算数运算

可以领迭代器和一个整数值相加(或相减),其返回值是向前(或向后)移动若干个位置的迭代器。指向这样的操作时,结果迭代器或指示原vector对象(或string对象)的一个元素,或者指向原vector对象(或string对象)尾元素的下一个位置。

例如:如下迭代器,将指向摸个vector对象中间位置的元素

// 计算的带最接近vi中间元素的一个迭代器
auto mid = vi.begin() + vi.size() / 2

如果元素vi有20个元素,vi.size()/2得10,令mid=vi.begin() + 10。下标从0开始,则迭代器所指的元素是vi[10],也就是从首元素开始向前相隔10个位置的那个元素。

vector或string的迭代器,如果需要判断相等,可以使用关系运算符(>、>=、<、、)进行比较。参与比较的两个迭代器必须合法且指向的是同一个容器中的元素或最后一个元素的下一个位置。

比如下面代码

if it < mid:
    //处理vi前半部分的元素

只要两个迭代器指向同一容器中的元素或尾元素的下一位置。就能将其相减,所得差值就是两个迭代器距离,所谓的距离就是右侧迭代器向左移动多少位置就能追上左侧迭代器。距离类型:difference_type的带符号的整型数。

使用迭代器运算

使用迭代器运算的一个景点所发是二分搜索。二分搜索从有序序列中寻找某个给定值。

//给定一个有序字符序列
auto beg = text.begin(),end = text.end();
auto mid = beg+(end - beg)/2;   //初始状态下的中点
// 当还有元素尚未检查并且我们还没有找到sought时执行
while( beg != end && *mid !=sought ){
   if(sought < *mid){           // 我们要找的元素在前面么?
   	  end = mid;				// 是-> 往前找
   }else if(sought > *mid){     // 不是-> 往后找
   	  beg = mid+1;
   }
   mid = beg + (end - beg);  //重新制定中点
}

使用iter完成程序实现了0-100之间成绩区间统计,每10个一个统计,100分单独统计为满分



 int main(int argc, char const *argv[])
 {
    string hit = "请输入成绩(0-100间整数)";
    cout << hit << endl;
    vector<unsigned> sorces(10,0); 
    auto begin  = sorces.begin();
    unsigned grade;
  
    while (cin >> grade )
    {
        *(begin+grade/10) +=1;
    }
    
    cout << "[";
    for( auto grade  = sorces.begin();grade!=sorces.end();grade +=1)
    {
        cout << *grade << " ";
    }
    cout << "]"<<endl;


    return 0;
 }