C++ Primer 第三章 字符串,向量和数组(万字合集)

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

        第三章介绍两种标准库类型:string和vector。

        string表示可变长的字符序列;

        字面意思就是“字符串”。

        vector存放的时某种给定类型对象的可变长序列;

3.1 命名空间的using声明

        入门时期,我们用到的库函数大多属于命名空间std,例如

std::cin//表示从输入中读取内容

        作用域操作符(::)的含义:编译器从左侧名字所示的作用域中找到右侧的名字。

        从而我们可以得知,std::cin的意思就是 使用命名空间std中的名字cin。 

        若一直重复输入就比较繁琐,此时我们可以用到using声明。

using namespace::name;

        using:using声明。

        namespace:命名空间,例如std。

        name:你所需要的名字,例如cin,cout。

#include
using std::cin;
int main()
{
        int i;
        cin>>i;
        cout<

        ^s  保存 ^x 返回(^的意思是ctrl,所以这里是ctrl+s,ctrl+x)

        编译运行后我们可以看到,cout报错了,并且提示我们需要改成std::cout,因为我们没有using std::cout; 

        而下面一行的std::cout<

        每个名字都需要独立的using声明。

using std::cin;using std::cout;
using std::endl;

        直接使用using namespace std;不太好,因为我们大多使用cin,cout,endl之类的,在std中占很小的一部分。而using namespace std;则是把std中的所有内容都拿过来用了,把很多用不到的内容也一起使用。

        std中有很多名字,如果全部拿过来使用,很有可能会和自己编写的代码发生冲突。

        例如:你原本编写的“英雄联盟”中有伊泽瑞尔了,还直接把“云顶之弈”的代码全部using放到“英雄联盟”的代码中,而“云顶之弈”也有伊泽瑞尔,程序就会出现冲突,有两个重名的函数。

        用到什么就using一下,就可以很大程度的避免冲突。

        头文件中不应包含using声明

        因为头文件的内容会拷贝到所有引用它的文件中,如果头文件里有某个using声明,那么每个使用了该头文件的文件都会出现这个声明。就也有可能发生刚刚说的“伊泽瑞尔”的情况。

3.2 标准库类型string

        标准库类型string 表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。

        即

#include
using std::string;

        3.2.1定义和初始化string对象 

        如何初始化类的对象是由类本身决定的。

        例

string s1;                    //默认初始化,s1是一个空字符串

string s2=s1;              //s2是s1的副本

string s3="hiya";        //s3是该字符串字面值得副本

string s4(10,'c');        //s4的内容是cccccccccc

        拷贝初始化和直接初始化 

        等号(=)右边等于左边称之为拷贝初始化;反之则成为直接初始化;

string s5="hiya";         //拷贝初始化

string s6("hiya");        //直接初始化

string s7(10,'c');         //直接初始化,s7=cccccccccc;

        拷贝初始化和直接初始化区别

        拷贝初始化 :  =     比直接初始化多了个步骤,效率相对较低

        直接初始化:()

类似于a和c之间利用b进行交换

b=a;

a=c;

c=b;

string s8 =string(10,'c');

 string(10,'c');先提出一个临时string对象 然后在把临时srting对象赋值给s8。↕上下两个函数等价。

string temp(10,'c');
string s8=temp;

3.2.2 string对象上的操作

#include
#include
using std::cout;
using std::cin;
using std::endl;
using std::string;
int main()
{
    string s;
    cin>>s;
    cout<

    C++ Primer 第三章 字符串,向量和数组(万字合集)_第1张图片    

        在读取操作时,string对象会自动忽略开头的空白(即空格符,换行符,制表符等),并从第一个真正的字符开始读起,直到遇见下一处空白为止。

        读取未知数量的string对象
#include
using namespace std;
int main()
{
        string word;
        while(cin>>word)             //使用while循环,反复读取,直到文件末尾
                cout<

C++ Primer 第三章 字符串,向量和数组(万字合集)_第2张图片

        一直反复读取,直到文件末尾。输入空格,回车都没有用,因为在它的思维里面 空格 和 回车 也是一个字符,所以不会停止。

        输入^D(等效于  ENF),则可以停止。

        因为ENF就是文件末尾。

        使用getline读取一整行

        有时我们需要得到我们在字符串中保留输入时的空白字符,此时我们可以使用 geline函数  来代替   >>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流读入内容,直到遇到换行符为止(Tips:最后的换行符也被读进来了),然后把所读得内容存入到string对象中去(Tips:此时不会存入换行符)。

        getline函数只要遇到换行符就会结束读取操作并进行存储,如果没有内容,直接输入回车键,此时的string对象就是一个空的string。

        和输入运算符一样,getline也会返回它的流参数。因此输入运算符能作为判断条件,getline函数的结果同样能作为条件。

        例:可以通过改写之前的程序,我们能实现一次输出一整行,而不再是每次输出一行一个词了。

        我们拷贝一下刚刚的primer0324.cpp

cp priemr0324.cpp priemr0325.cpp

         这样我们就得到了和primer0324.cpp一样的文件primer0325.cpp,再通过nano就可以直接打开了。

       

include
using namespace std;
int main()
{
        string line;
        while(getline(cin,line))
                cout<

        Tips:此时getline函数返回的那个换行符实际上已经被丢弃了,因为string对象不会保存换行符,所以我们在结尾加了个endl;

C++ Primer 第三章 字符串,向量和数组(万字合集)_第3张图片

string的empty和size操作

        empty函数:根据string对象是否为空返回一个对应的bool值。

        empty是string的一个成员函数。调用该函数只要使用一下点函数指明哪个对象需要执行empty函数就可以了。

        例:

while(getline(cin,line))
{
    if(!line.empty())
    {
        cout<

        line就是string对象,所以使用line.empty()

        意思是:每次读取一整行,遇到空行直接跳过输出。

        其中line非空时,line.empty返回false值(0),反之返回true值(1)

        使用if(!line.empty())的意思就是

line非空时line.empty返回0,!a为真,if条件成立,输出line。

line为空时line.empty返回1,!a为假,if条件不成立,不输出line。

        size函数:返回string对象的长度(即string对象中字符的个数)。

例:

#include
using namespace std;
int mian()
{
    while(getline(cin,line))
    {
        if(line.size()>100)
                cout<

 每次输出一整行,但是只输出超过100个字符的行。

string::size_type类型

        对于size函数来说,返回的是一个string::size_type类型的值。string类及其它大多数标准库类型都定义了几种配套的类型。这些配套类型体现了标准库类型与机器无关的特性。即使我们不知道srting::size_type类型的细节,但是我们知道:它是一个无符号类型的值,并且能够存放下任何一个string对象的大小。其中所有用于存放string类型的返回变量都是string::size_type类型的。

        在C++11新标准中,允许编译器通过auto或者decltype来推断变量的类型:

auto len = line.size();

         len的类型是string::size_type

        由于size函数返回的是一个无符号整形数,所以在表达式中混用带符号数和无符号数可能会产生意想不到的结果。

        例:

        设n是一个具有负值的int,这表达式s.size()

因为负值n会自动转换成一个比较大的无符号值。

        比较string对象

        string对象中的字符,对大小写敏感,所以在比较同一个字母的大写和小写是不同的。

        相等运算符(==和!=)分别检验两个string对象相等或不相等,string对象相等意味着它们的长度相同并且包含的字符也全部相同。关系运算符<,<=,>,>=分别检验一个string对象是否小于,小于等于,大于,大于等于另一个string对象。并且这些关系运算符都遵守字典顺序:

        1.如果两个string对象的长度不同,并且短的string对象的每个字符都与较长的string对象对应位置上的字符相同,也就是说较短的string对象小于较长的string对象。

        2.如果两个string对象在某些对应的位置上不一致,则string对象比较的结果其实是string对象中第一个对应相异的字符的比较的结果。

        例

        string str = "Hello";

        string phrase = "Hello World";

        string slang = "Hiya";

根据 1 可以判断,对象str小于对象phrase;

根据 2 可以判断,对象slang即大于str也phrase。

        为string对象赋值

        同前拷贝初始化和直接初始化相同

string st1(10,'c'),st2;

//st1的内容:ccccccccccc;st2是一个空字符串。

st1=st2;

//赋值:用st2的副本替换st1的内容

//最后得到st1和st2都是控制符串。

        两个string对象相加

#include
#include
using namespace std;
int main()

{
        string s1="hello",s2="world";
        string s3=s1+s2;
        cout<

        s1+=s2;等效 s1=s1+s2;

C++ Primer 第三章 字符串,向量和数组(万字合集)_第4张图片

         字面值和string对象相加
#include
#include
using namespace std;
int main()

{
        string s1="hello",s2="world";
        string s3=s1+","+s2+'\n';
        cout<

 C++ Primer 第三章 字符串,向量和数组(万字合集)_第5张图片

当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string;

        string s4 = s1 + “,”;                  //正确:把一个string对象和一个字面值相加;

        string s5 =  "hello" + "," ;         //正确:两个运算对象都不是string;

        string s6 = s1 +“,”+“world”;//正确:每个加法运算符都有一个运算对象是string;

        string s7 =  “hello” + “,”+s2;//错误:不能把字面值直接相加;

 “字符串” 其实代表的是字符串的地址

#include
using namespace std;
int main()
{
        string s="Hello world";
        cout<<"Hello world"<

 C++ Primer 第三章 字符串,向量和数组(万字合集)_第6张图片

 “*”为寻址运算符,说明后面那串一定是地址,提取了该字符串的首地址,也就是“H”。

这不仅证明了"字符串"是字符串的地址,也证明了为什么加法运算符两侧至少要有一个string。

 3.2.3处理string对象中的字符

        如何的单独处理string对象中的字符,比如检查一个string是否包含空白,string对象中的字母改小写,某个字符是否出现等。

        处理string的关键问题是如何获取字符本身。

        第一种方法就是去了解cctype头文件,其中的函数可以让我们来判断某个字符。

        第二种方法使用基于范围的for语句(范围for语句)

for(declaration:expression)
    statement

        expression:一个对象,用于表示一个序列。

        declaration:部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration部分的变量都会被初始化为expression部分的下一个元素值。

        statement:说明,你要做什么,让电脑执行什么。比如下面这个示例,就是执行输出abab。

        范围for语句示例:

string str("some string");
for(auto abab : str)
    cout << abab <

         定义了一个str内容是some string,使用范围for语句,其中  : 右侧 str 字符序列。auto 取出str的类型,取出了什么类型abab就是什么类型,而str的类型都是char型,所以abab就是char型。

#include
#include
using namespace std;
int main()
{ 
        string s("Hello world!!!");
        decltype(s.size()) punct_cnt=0;
        for (auto c:s)
                if(ispunct(c))
                        ++punct_cnt;
        cout<

C++ Primer 第三章 字符串,向量和数组(万字合集)_第7张图片

         先创建一个名为s内容为Hello worlad!!!的string。

        然后确定punct_cnt的类型并让punct_cnt等于0(在上文我们提到过,在C++11新标准中,允许编译器通过auto或者decltype来推断变量的类型)所以这里punct_cnt的类型和s.size的返回类型一样。punct_cnt是用来统计s中的标点符号的数量。

        for(auto c:s)用来面向s中的每个字符

        if(ispunct(c))是c标准库中的ispunct(),用于检查所传的字符是否是标点符号字符。

        然后if(ispunct(c))正确则进行++punct_cntl;将标点符号的计数值加1。

        使用范围for语句改变字符串中的字符

        如果想改变string对象中字符的值,必须把循环变量定义成引用类型。示例,把整个string对象转换成大写。

#include
using namespace std;
int main()
{
        string s("Hello world!!!");
        for (auto &c:s)
                c=toupper(c);
        cout<

        C++ Primer 第三章 字符串,向量和数组(万字合集)_第8张图片

           c就是每次遍历对于这个字符的引用。

           toupper()把小写字母转换为大写字母。

           每次迭代时,变量c引用string对象s的下一个字符,实际上就是改变了当前s的一个字符,当整个循环结束后,s就全部为大写了。

        只处理一部分字符

        一种方式使用下标运算符[],另一种是使用迭代器。

        string的下标同数组一样 从s[0]开始一直到s[s.size()]-1。在访问制定字符之前,必须检测字符是否为空,检查过后再用下标,可以减少错误。

3.3 标准库类型vector

        标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector“容纳着”其他对象,所以它也常被称作为 容器 。

        使用vector,必须包含适当的头文件:

#include
using std::vector;

        在C++中既有类模板,也有函数模板,而vector就是一个类模板。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为 实例化 ,当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

        举个例子,一个布料,做成裙子,它就变成了裙子;做成衬衫,它就变成了衬衫;一个瓶子,放了酒,它就变成了酒瓶;放了调料,它就变成相应的调料瓶。这个过程就是实例化。

vector  ivec;                                         //ivec保存int类型的对象

vector  Sales_vec;                //保存Sales_item类型的对象

vector>  file;                       //该向量的元素是vector对象

        在使用vector模板的时候,我们把它分为了int类型,Sales_item类型,vector类型。在说明的时候我们就不能说它是vector模板了,应该称之为vector类型,vector类型,vector>类型。就像刚刚所说布料制作成了相对于的衣物,衬衫就是衬衫,裙子就是裙子,不能把它们全部叫做布料了。

        而vector>类型就像一个大包的果仁,里面用小包装拆开封装了很多杏仁,核桃等。为什么这里形容了“很多”,因为内层的vector中放的也不再是个int之类的单独的一个东西,而是一个string容器,里面放着更多的东西。

        vector能容纳大多数类型的对象作为其元素,但是因为 引用 不是对象,所以不存在包含 引用的vector。

        某些老式编译器需要在外层vector对象的右尖括号和其元素类型之间添加一个空格,如应该写成vector >而非vector>

3.3.1 定义和初始化vector对象

C++ Primer 第三章 字符串,向量和数组(万字合集)_第9张图片

        默认初始化vector对象,创建一个制定类型的空vector;

vcctor svec;          //默认初始化,svec不含任何元素;

        空vector可以在程序运行时很高效地往vector对象中添加元素。实际上,最常见的方式就是先定义一个空的vector,然后当运行时获取元素的值后再逐一添加。

vector ivec;                           //正确 直接初始化,初始状态为空。

//添加值给ivec。

vector ivec2(ivec);                //正确 ivec拷贝初始化给ivec。

vector ivec3 = ivec;              //正确 第二行与第三行等价。

vector svec (ivec2);         //错误 svec的对象是string,不是int。

列表初始化vector对象

用花括号括起来的0个或多个初始元素值被赋给vector对象:

vector articles = {"a","an","the"};

        上述vector对象包括三个元素:第一个字符串“a”,第二个字符串“an”,最后一个是字符串“the”。

        C++语言提供了几种不同的初始化方式。部分初始化方式可以等价地使用,有些不行。

        例:

        1.使用拷贝初始化时(就是用=的时候),只能提供一个初始值。

        2.如果提供的是一个类内初始化时,则只能使用拷贝初始化或使用花括号的形式初始化。

        3.初始元素值的列表,则只能把初始化都放在 花括号 中进行列表初始化。

创建制定数量的元素

        vector对象容纳的元素数量和所有元素的统一初始化vector对象:

vector ive(10,-1);                        //10个int类型的元素,每个都被初始化为-1;

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

        此时初始化一定数量相同值的元素,优先使用圆括号。

值初始化

        通常情况下,可以只提供vector对象内容的元素而略去初始值。此时库会生成一个值初始化的元素初始值,并把它赋值给容器中所有的元素,这个初始值会由vector对象中元素的类型决定。

        若vector对象的元素是内置类型,例如是int类型,则默认全为0;

        若vector对象的元素是某种类的类型,例如是string类型,则元素由类默认初始化。

vector ivec (10);            //10个元素,全部都是0。

vector svec(10);     //10个元素,全部都是空string对象。

 列表初始值还是元素数量?

        在某些情况下,确实初始化的真实含义依赖于传递初始值的是花括号还是圆括号。例如,用一个整数来初始化vector时,整数的含义可能是vector对象的容量也可能是元素的值。类似的,用两个整数来初始化vector时,这两个整数可能一个是vector对象的容量,另一个是元素的初始值,也可能时容量为2的vector对象中两个元素的初始值。

vector v1 (10);                   //v1有10个元素,每个的值都是0;

vector v2 {10};                   //v2有1个元素,该元素的值是10;

vector v3 (10,1);                //v3有10个元素,每个的值都是1;

+vector v4 {10,1};                //v4有2个元素,值分别是10和1;

        圆括号():提供的值用来构造vector对象。

v1的初始值说明了vector对象的容量;v3的两个初始值则分别说明了vector对象的容量和元素的初始值。

        花括号{}:用来表示列表初始化vector对象。

初始化的过程会尽量把花括号的值当成元素初始值的列表来处理,只有在无法执行列表初始化是才考虑其他方式。

        另一方面,有时初始化使用了花括号的形式当时提供的值又不能用来列表初始化,就要考虑用该值进行构造vector对象。

vector v5{"hi"};                //列表初始化:v5有一个元素;

vector v6("hi");                //错误:不能使用字符串字面值构建vector对象;

vector v7{10};                  //v7有10个默认初始化的元素;

vector v8{10,"hi"};            //v8有10个值为“hi”的元素;

        3.3.2 向vector对象中添加元素

        对vector对象来说,直接初始化的方式适用于三种情况。

1.初始值已知且数量较少;

2.初始值是另一个vector对象的副本;

3.所有元素的初始值都一样;

        但是更常见的是:创建一个vector对象时并不清楚实际所需要的元素个数,元素的值也没法确定。还有的时候:元素初始值知道,但是值的重量很大而且还各不相同,那么对这种vector对象执行初始化操作也会显得过于繁琐。

        例如,创建一个0到9的vector对象,使用列表初始化就非常简单了。但是如果是0到9999.....怎么办,一个个写出来也太逆天了。这时候就有一个好的方法:先创建一个空的vector,然后在运行时在你有vector成员函数push_back向其中添加元素。

        push_back:把一个值当成vector对象的尾元素“压到(push)”vector的“尾端(back)”。

vector=v2;
for(int i = 0;i!=100;++i)
    v2.push_back(i);

        上述是每次迭代时按顺序的把整数作为v2的新元素给它。同样也可以用刚刚输入的值赋值给vector。

string word;
vector text;
while(cin>>word)
    text.push_back(word);
向vector对象添加元素蕴含的编译假定

        高效便捷地向vector对象中添加元素,很多编程工作被大大简化了。此时要保证所写的循环无误,特别是可能改变vector对象容量的时候。

        Tips:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。范围for语句体内不应该改变所里边序列的大小。

3.3.3 其他vector操作

C++ Primer 第三章 字符串,向量和数组(万字合集)_第10张图片

 访问vector对象中元素的方式和访问string对象中字符的方式差不多。

#include
#include
using namespace std;
int main()
{
        vector v={1,2,3,4,5,6,7,8,9};
        for(auto &i:v)
        {
                i*=i;
        }
        for(auto i:v)
        {
                cout<

C++ Primer 第三章 字符串,向量和数组(万字合集)_第11张图片

         第一个循环把控制变量i定义成引用类型,这样才能通过i给v的元素赋值,其中i的类型有auto制定。众所周知,i+=i等价于i=i+i,类似,i*=i等价于i=i*i。最后一个循环输出所有元素;

        vector的empty和size两个成员与string的同名成员功能完全一致:

empty检查vector对象是否包含元素然后返回布尔值;

size则返回vector对象中的元素个数,返回值的类型是由vector定义的size_type类型。

vector::size_type;      //正确

vector::size_type;                //错误

::前面要接类型,而vector不是类型

        各个相等性运算符和关系运算符也与 string 的相应运算符功能一致。

        两个vector 对象相等当且仅当它们所含的元素个数相同,而且对应位置的元素值也相同。关系运算符依照字典顺序进行比较:如果两个vector 对象的容量不同,但是在相同位置上的元素值都一样,则元素较少的 vector 对象小于元素较多的 vector 对象;

        若元素的值有区别,则vector对象的大小关系由第一对相异的元素值的大小关系决定。只有当元素的值可比较时,vector 对象才能被比较。一些类,如 string 等,确实定义了自己的相等性运算符和关系运算符;另外一些,如 Sales_item 类支持的运算,显然并不支持相等性判断和关系运算等操作。因此,不能比较两个 vector对象。

计算vector内对象的索引

        使用下标运算符能获取到指定的元素。和 string 一样,vector对象的下标也是从0开始计起,下标的类型是相应的size_type类型。只要vector对象不是一个常量,就能向下标运算符返回的元素赋值。此外也能通过计算得到vector内对象的索引,然后直接获取索引位置上的元素。

不能用下标形式添加元素
vector ivec;
for(decltype(ivec.size())ix=0;ix!=10;++ix)
    ivec[ix]=ix;

        这段代码是错误的:ivec是一个空vector,根本不包含任何元素,当然也就不能通过下标去访问任何元素!  等于号(=)是让左边的被赋值成右边的,而在ivec[ix]中左边根本就没有这个东西,怎么去赋值。就像你想用你的点券去换英雄联盟的皮肤,结果老马发现你根本没有点券,它怎么可能平白无故地给你。 

        正确方法是使用push_back;

vector ivec;
for(decltype(ivec.size())ix=0;ix!=10;++ix)
    ivec.push_back(ix);

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

3.4 迭代器介绍

        迭代器 功能:访问容器对象的元素。

所有标准库容器都可以使用迭代器,其中只有少数几种才同时支持下标运算符。严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。

迭代器分为有效和无效,类似于指针。

  • 有效的迭代器:指向某个元素,或者指向容器中尾元素的下一个位置;
  • 无效的迭代器:除上述的其他所有情况。

3.4.1 使用迭代器

        和指针不同,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。例如,这些类型都有 begin 和 end 的成员,

        begin 成员:负责返回指向第一个元素(或第一个字符)的迭代器。

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

         end 成员:负责返回指向容器(或string对象)“尾元素的下一个位置”的迭代器,该迭代器指向的容器是一个本不存在的“尾后”元素。

        所以end成员返回的迭代器常被称作为 尾后迭代器 或 尾迭代器。特殊情况当容器是空时,begin成员和end成员返回的是同一个迭代器,都是尾后迭代器。

迭代器运算符

        与指针相同,迭代器也能通过解引用迭代器来获取它所指示的元素,执行解应用的迭代器必须合法并且指示着某个元素。尝试解引用一个非法迭代器或者尾后迭代器都是未被定义的行为。

 下列程序,利用下标运算符进行大小写转换。

string s("some string");
if (s.begin() != s.end())
{
    auto it = s.begin();
    *it = toupper(*it);
}

         s.begin()!=s.end() 检查s是否为空,根据上面我们提到,当容器为空时begin成员和end成员返回的是同一个迭代器。所以当返回结果一样,说明s为空;如果返回结果不一样,说明s不为空,此时s中至少包含一个字符。

        在if内部声明了一个迭代器变量it并且把begin的返回值赋值给它,此时就得到了指示s中第一个字符的迭代器。最后用toupper转换成大写。

输出结果:Some string

将迭代器从一个元素移动到另一个元素

        迭代器使用递增运算符(++)来从一个元素移动到下一个元素。和整数的递增类似,在整数递增上的"+1",在迭代器的递增中就意味着“向前移动一个位置”。

        因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。因为我们上文提到过(end 成员:负责返回指向容器(或string对象)“尾元素的下一个位置”的迭代器,该迭代器指向的容器是一个本不存在的“尾后”元素)。

        现在我们使用迭代器把第一个单词全部大写。

#include
#include
using namespace std;
int main()
{
        string s{"some string"};
        for(auto it=s.begin();it!=s.end()&&!isspace(*it);++it)
                *it = toupper(*it);
        printf("%s \n",s.c_str());
        return 0;
}

第一步:创建一个string类型的s。

第二步:创建迭代器it作为s的开始,条件it到结尾就停止,it不能为空,执行完成++it,it指向下一个字符。

第三步:字符大写。

泛式编程

        之前我们说到过,只有string和vector等少数标准库类型有下标运算符。并且大部分标准库类型没有定义<运算符,但是所有标准库容器的迭代器都定义了==和!=。所以我们只要养成使用迭代器和!=的习惯,就不用在意使用的是哪种容器类型。

迭代器类型

        我们不需要知道string和vector的size_type成员到底是什么类型。我们把拥有迭代器的标准库类型,使用iterator和const_iterator来表示迭代器类型;

        例如:每个容器类定义了一个名为iterator的类型,而iterator类型指出迭代器概念所规定的一套操作。这就叫迭代器类型。

vector::iterator it;                    // it 能读写vector的元素

string::iterator it2;                           // it2 能读写string对象中的元素

vector::const_iterator it3;       //  it3只能读元素,不能写元素

string::const_iterator it4;                //  it4只能读字符,不能写字符

begin和end运算符

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

vector v;

const vector cv;

auto it1 = v.begin();        // it1的类型是vector::iterator

auto it2 = cv.begin();      // it2的类型是vector::const_iterator

         有时候这些默认操作不是我们所想要的,我们只需要对象只读不进行操作,最好是使用常量类型(比如const_iterator),为了方便操作在C++11 中引入了两个新的函数cbegin()和cend();

        auto it3 = v.cbegin();//it3的类型是vector::const_iterator

        类似于begin和end,上述两个新函数也分别返回第一个元素和最后一个元素的下一位置的迭代器。不同的是,无论是否是常量,返回的都是const_iterator。

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

        解引用迭代器可获得迭代器所指对象,如果该对象的类型恰好是类,就有可能进一步访问它的成员。例如一个vector对象,想要检查其元素是否为空,令it是该vector对象的迭代器,只需要检查it所指字符串是否为空就可以了。

(*it).empty();

         *it的外的()是必须写的,如果不写就会先执行点运算符,再进行解引用。正确的是先对it解引用,然后用解引用的结果再执行点运算符。

(*it).empty();        //解引用it,然后调用结果对象的empty成员

*it.empty();           //错误,试图访问it的名为empty的成员,当时it是个迭代器没有empty成员

        为了简化上述表达式,C++语言定义了 箭头运算符(->)。箭头运算符把解引用和成员访问两个操作结合在一起。也就是说 it->men 和 (*it).mem是等价的。

        例如,用一个名为text的字符创向量存放文本文件中的数据,其中的元素或者一句话或一个用于表示段落分隔的空字符串。如果要输出text中第一段的内容,可以利用迭代器写一个循环令其遍历text,直到遇到空字符串的元素为止;

for(auto it = text.cbegin();it != text.cend()&& !it->empty();++it)
    cout<< *it <
某些对vector对象的操作会使迭代器失效

        虽然vector对象可以动态地增长,但有一些副作用。

已知的限制是

        1.不能再范围for循环中向vector对象添加元素。

        2.人呢刚和一种可能改变vector对象容量的操作,例如push_back,都会使该vector对象的迭代器失效。

Tips:但凡使用了迭代器循环体,都不要向迭代器所属的容器添加元素。

3.4.2 迭代器运算

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

迭代器的算数运算

        可以令迭代器和一个整数值相加或相减,其返回值是向前或向后移动了若干个位置的迭代器。执行这样的操作时,迭代器可能会发生两种情况:

        1.迭代器指示原vector对象(或string对象)内的一个函数;

        2.迭代器指示原vector对象(或stirng对象)尾元素的下一位置。

#include
using namespace std;
const int N=100010;
int n;
int q[N];
void quick(int q[],int l,int r)
{
	if(l>=r) return;
	int i=l-1,j=r+1,x=q[l+r>>1];
	while(ix);
		if(i

        这里使用快排来解释迭代器的算数运算符。 

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

        在快排的代码中,l和r分别类似vi.begin()和vi.size(),快排中的x和mid是同一个作用。

(l+r)>>2简单理解为(l+r)/2; 建议把圆括号都写上,防止出错,也方便查看。

        x是q中最接近q中间数组的一个元素。

        mid是vi中最接近vi中间元素的一个迭代器。

如果q有20个数组,那么x就是q[10];mid在vi中也是一个道理;

        对于string或者vector的迭代器来说,除了判断是否相等,还能使用关系运算符(<,<=,>,>=)对其进行比较,参与比较的迭代器必须合法并且指向同一个容器的元素(或尾元素的下一个位置)。

例如,假设it和mid是同一个vector对象的两个迭代器,可以用下面的代码来比较他们的位置。

if(it

        相应快排的代码中,do...while语句的条件,比较q[i]和x,对i进行调整;比较q[j]和x,对j进行对比。

if(l>=r) return; 就类比于x.begin() != x.end();

        当头尾相碰时,循环结束。

        只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为 difference_type 的带符号整型数。string和vector都定义了 difference_type, 因为这个距离可正可负,所以difference_type 是带符号类型的。

使用迭代器运算

        使用迭代器运算的一个经典算法是二分搜索(上述的快排就是一个典型的二分搜索)。二分搜索从有序序列中寻找某个给定的值。二分搜索从序列中间的位置开始搜索,如果中间位置的元素正好就是要找的元素,搜索完成;如果不是,假如该元素小于要找的元素,则在序列的后半部分继续搜素;假如该元素大于要找的元素,则在序列的前半部分继续搜索。在缩小的范围中计算一个新的中间元素并重复之前的过程,直至最终找到目标或者没有元素可供继续搜索。

//text 必须有序,beg和end表示搜索范围
auto beg = text.begin(),end = test.end();
auto mid = text.begin()+(end - beg)/2;            //初始状态的中间点
//还有元素没有被检测并且没有找到sought时,进行循环搜索
while(mid!= end && *mid != sought)
{
    if(sought<*mid)         //寻找所需要的函数
        end = mid;          //如果在前半部分就省略后半部分
    else                    //若我们要找的元素在后半部分
        beg = mid + 1;      //在mid之后寻找
    mid = beg + (end-beg)/2;//新的中间点
}

程序一开始就定义了三个迭代器:

        1.beg指向搜索范围内的第一个元素;

        2.end指向尾元素的下一个位置;

        3.mid指向中间的那个元素。

搜索范围是名为text的vector的全部范围。

        循环部分先检查搜索范围是否为空。

若mid==end,则已经找遍所有元素,条件不满足,循环终止。

当搜索范围不为空时,可知mid指向某个元素,检查该元素是否满足要求,如果是,则终止循环。

        当进入循环体内部后,程序通过规则去移动beg或者end来缩小搜索范围。

        如果mid所指元素>目标元素sought,则推测出若text含有sought,必出现在mid所指元素的前面。此时,可以忽略mid后面的元素比出现在mid所指元素的后面,并且把mid赋值给end就可以了。

        另一种情况,如果*mid

        循环结束时,mid或者等于end或者指向目标元素sought。如果mid等于end,说明text中没有我们要找的元素。

3.5 数组

        数组是一种类似标准库类型vector的数据结构,但是在性能和灵活性的权衡上又与vector有所不同。

        类似vector的方面:存放相同类型的对象的容器,通过对象的位置进行访问。

        不同vector的方面:数组大小确定不变,不能随意增加元素。

Tips:不清楚元素的确切个数,请使用vector。

3.5.1 定义和初始化内置数组

        数组是一种复合类型。声明形式:a[d],其中a是数组的名字,d是数组的维度。维度说的就是数组中元素的个数,必须大于0。

        数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。所以维度必须是一个常量表达式。

unsigned cnt = 42;                             //不是常量表达式。

constexpr unsigned sz =42;              //常量表达式,关于constexper。

int arr[10];                                          //含有10个整数的数组。

int *parr[sz];                                       //含有42个整型指针的数组。

string bad[cnt];                                   //错误:cnt不是常量表达式。

string strs[get_size()];                        //当get_size是constexper时正确。

        默认情况下,数组的元素被默认初始化。和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

        定义数组的时候必须指定数组的类型,不允许使用auto关键字由初始化的列表推断类型。数组的元素为对象,因此不存在引用的数组。

显式初始化数组元素

const unsigned sz = 3;

int ial[sz] = {0,1,2};                               //含有3个元素的数组,元素值分别是0,1,2

int a2[] = {0,1,2};                                  //维度是3的数组。

int a3[5] = {0,1,2};                                //等价于 a3[] = {0,1,2,0,0};

string a4[3] = {"hi","bye"};                    //等价于 a4[] = {"hi","bye"," "};

int a5[2] = {0,1,2};                                //错误:初始值过多。

字符数组的特殊值

        字符数组由一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。当使用这种方式时,字符串字面值的结尾还有一个空字符。

char a1[]={'C','+','+'};                         //列表初始化,没有空字符

char a2[]={'C','+','+','\0'};                    //列表初始化,含有显式的空字符。

char a3[]="C++";                               //自动添加表示字符串结束的空字符。

const char a4[6]="Daniel";                //错误,没有空间可以存放空字符。

        a1的维度是3,a2,a3的维度是4,a4的定义是错误的。数组的大小必须至少是7,其中前6个位置存放字面值的内容,最后1个存放结尾处的控制符。

不允许拷贝和赋值

        不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值:

int a[]={0,1,2};                //含有3个整形的数组。

int a2[]=a;                      //错误:不允许使用一个数组初始化另一个数组。

a2 =a;                            //错误:不能把一个数组直接赋值给另一个数组。

        若想把一个数组赋值给另一个数组,只能用for循环进行遍历,除此之外没有其他的办法。 

理解复杂的数组声明 
int *ptrs[10];

ptrs是含有10个整型指针的数组。

大小为10的数组,名字为prts,存放int的指针。

int &refs[10]=/*?*/;

错误,不存在引用的数组。

int (*Parray)[10]=&arr;

Parray指向一个含有10个整数的数组。

Parray是一个指针,指向大小为10的数组的指针,其中数组元素是int。

int (&arrRef)[10]=arr;

arrRef引用一个含有10个整数的数组。 

arrRef是一个引用,引用对象是一个大小为10的数组,数组中元素的类型是int。

Tips:想要理解数组声明的含义,最好的方法是从数组的名字开始按照由内向外的顺序阅读。

int arr[3]={0,1,2};

其中arr==&arr[0],指向数组中的“0”,&arr代表的是该数组的起始地址。

int *(&array)[10]=ptrs;

array是整形指针数组的引用。

3.5.2 访问数组元素

        数组除了大小固定以外,用法与vector基本相似。 与标准库类型vector和string一样,数组的元素也能使用范围for语句或下标运算符来访问。数组的索引从0开始。例如一个包含是个元素的数组,它的索引就是从0到9。

检查下标的值

        与vector和string一样,数组的下标是否在合理范围之内由程序员负责检查。所以下标应该大于等于0并且小于数组的大小。

3.5.3 指针和数组

        在使用数组的时候编译器一般会把它转换成指针。通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此对数组的元素使用取地址符就能得到指向该元素的指针;

string nums[]={"one","two","three"};        //数组的元素是string对象

string *p = &nums[0];                              //p指向nums的第一个元素

         数组还有一个特性,在大多数使用数组的时候,编译器会自动地将其代替为一个指向数组为首元素的指针;

string *p2 = nums; //等价于p2=&nums[0]

         Tips:在大多数表达式中,使用数组;类型的对象其实是使用一个指向该数组首元素的指针。

所以我们可以得知。在一些情况下对数组的实际操作是指针的操作,这一结论有很多隐含的意思。其中一层意思是当使用数组为一个auto变量的初始值时,推断得到的类型是指针而非数组;

int ia[]={0,1,2,3,4,5,6,7,8,9};

 ia是一个含有10个整数的数组

auto ia2(ia);

ia2是一个整形指针,指向ia的第一个元素。虽然ia是由10个整数构成的数组,但是使用ia作为初始值时,编译器实际执行的初始化过程类似于下面的形式;

auto ia2(&ia[0]);     

显然ia2的类型是int*

当使用decltype关键字时不会发生上述转换,decltype(ia)返回的类型是由10个整数构成的数组; 

ia2=42;

错误;ia2是一个指针,不能用int值给指针赋值

decltype(ia) ia3 ={0,1,2,3,4,5,6,7,8,9};

ia3=p;                           //错误:不能用整型指针给数组赋值

ia3[4]=i;                        //正确:把i的值赋值给ia3的一个元素

 指针也是迭代器

        vector和string的迭代器支持的运算,数组的指针全都支持。

例如:使用递增运算符将指向数组元素的指针向前移动到下一个位置上:

int arr[] = {0,1,2,3,4,5,6,7,8,9};
int *p = arr;        //p指向arr的第一个元素
++p;                 //p指向arr[1]

         就像使用迭代器遍历vector对象中的元素一样,使用指针也能遍历数组中的元素。      但是,这样做的前提是先得获取到指向数组第一个元素的指针和指向数组尾元素的下一位的指针。

        之前已经介绍过,通过数组名字或者数组中首元素的地址都只能得到指向首元素的指针;不过获取尾后指针就要用到数组的另外一个特殊性质了。我们可以设法获取数组尾元素之后的那个并不存在的元素的地址:

int *e = &arr[10]; //指向arr尾元素的下一位置的指针

        arr数组只有10个元素,所以位置的索引是从0到9,可知这里的arr[10]明显指向arr尾元素的下一位置的指针。接下来这个不存在的元素唯一的用处就是提供其地址用于初始化e。就像尾后迭代器一样,尾后指针也不指向具体的元素。因此,不能对尾后指针执行解引用或递增的操作。

        利用上面的道德指针能重写之前的循环,令其输出arr的全部元素:

for (int *b = arr; b != e;++b)
{
    cout<< *b <

 练习3.35:编写一段程序,利用指针将数组中的元素置为0。

#include 
using namespace std;
int main()
{
    const int size = 5;
    int arr[size];
    int *pt = arr;
    for(int i = 0; i < size; i++)
        *(pt + i)=0;
    for(int i = 0; i < size; i++)
        cout<<*(pt+i)<<"";
    cout<

 

 3.36:编写一段程序,比较两个数组是否相等。再写一段程序,比较两个vector对象是否相等。

#include
using namespace std;
int main()
{
    const int size = 5;
    int arr1[size]={1,2,3,4,5};
    int arr2[size]={1,2,5,7,9};
    for(int i= 0;i

#iclude
#iclude
using namespace std;
int main()
{
    vector int_vector1 = {1,2,3,4,5};
    vector int_vector2 = {1,2,7,9,14};
    vector int_vector3 = {1,2,3,4,5};
    auto it1 = int_vector1.begin();
    auto it2 = int_vector2.begin();
    auto it3 = int_vector3.begin();
    while(it1!= int_vector1.end()&&it2!=int_vector2.end()&&it3!=int_vector3.end())
    {
        if(*it1 != *it2&&*it1==*it3)
            {
                cout<<"int_vector1!=int_vector2"<

C++ Primer 第三章 字符串,向量和数组(万字合集)_第12张图片

标准库函数begin和end

        虽然能通过计算得到尾后指针,但是这种用法极易出错。为了让指针使用的更简单,更安全C++11新标准引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能相似,不过因为数组毕竟不是类类型,所以这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

int ia[] = {0,1,2,3,4,5,6,7,8,9};    
//ia是一个含有10个整数的数组
int *beg = begin(ia);
//指向ia首元素的指针
int *last = end(ia);
//指向arr尾元书的下一位置的指针

begin函数返回指向ia首元素的指针;

end函数返回指向ia尾元素下一位置的指针;

这两个函数定义在iterator头文件中。

        使用begin和end可以很容易地写出一个循环并处理数组中的元素。例如:假设arr是一个整形数组,下面的程序负责找到arr中的第一个负数:

//pbeg指向arr的首元素,pend指向arr尾元素的下一位置
int *pbeg = begin(arr),*pend = end(arr);
//寻找第一个负值元素,如果已经检测完 全部元素则结束循换
while (pbeg != pend && *pbeg >=0)
    ++pbeg;

 一,定义了两个名为pbeg和pend的整形指针,其中pbeg指向arr的第一个元素,pend指向arr尾元素的下一位置。

二,while语句的条件部分通过比较pdeg和pend来确保可以安全地pbeg解引用。

        如果pbeg确实指向了一个元素,来接引用并检查元素值是否为负值。如果是,条件失效,退出循环;如果不是,将指针向前移动一位继续考察下一个元素。

        Tips:尾后指针不能执行解引用和递增操作。

指针运算

C++ Primer 第三章 字符串,向量和数组(万字合集)_第13张图片

C++ Primer 第三章 字符串,向量和数组(万字合集)_第14张图片

        指向数组元素的指针可以执行表3.6 表3.7 列出的所有迭代器运算。这些运算,包括解引用,递增,比较,与整数相加,两个指针相减等,用在指针和用在迭代器上意义完全一致。

        给一个指针加上某整数值(从一个指针减去某整数值),结果仍是指针。新指针指向的元素与原来的指针相比前进(后退)该整数值个位置:

constexper size_t sz = 5;
int arr[sz] = {1,2,3,4,5};
int *ip1 = arr;
//等价于int *ip = &arr[0]
int *ip2 = ip + 4;
//ip2指向arr的尾元素arr[4]

        给指针加上一个整数,得到的新指针仍需指向同一数组的其他元素,或者指向同一数组的尾元素的下一位置:

int *p = arr + sz;
//使用警告:不要解引用!

         当给arr加上sz时,编译器自动地将arr转换成指向数组arr中首元素的指针。执行加法后,指针从首元素开始向前移动了sz个元素,指向新位置的元素。如果计算所得的指针超出了上述范围就会产生错误,而且这种错误编译器一般发现不了。

        和迭代器一样,两个指针相减的结果是它们之间的距离。参与运算的两个指针必须指向同一个数组当中的元素:

auto n = end(arr) - begin(arr);
//n的值是5,也就是arr中元素的数量。

        两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,prtdiff_t也是一种定义在cstddef头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t时一种带符号类型。

        只要两个指针指向同一个数组的元素,或者指向该数组的尾元素的下一位置,就能利用关系运算符对其进行比较。例如:可以按照以下方法遍历数组中的元素:

int *b = arr,*e = arr + sz;
while(b

        如果两个指针分别指向不相关的对象,则不能比较它们。 上述指针运算同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一个对象或者对象的下一位。如果p是空指针,允许给p加上或减去一个值为0的整形常量表达式。两个空指针也允许彼此相减,结果当然是0。

解引用和指针运算的交互

        指针加上一个整数所得的结果是一个指针。假设结果指针指向了一个元素,则允许解引用该结果指针:

int ia[] = {0,2,4,6,8};
//含有5个整数的数组
int last = *(ia + 4);
//正确:last初始化成8,也就是ia[4]的值

        表达式*(ia+4)计算ia前进4个元素后的新地址,解引用该结构指针的效果等价于表达式ia[4]。

         类似解引用运算符和点运算符,指针运算时最好在必要的地方加上圆括号。例如刚刚的实例如果写成这样:

last = *ia +4;
//正确:last = 4等价于ia[0]+4

        此时含义就和之前完全不同了,此时解引用ia,然后给解引用的结果再加上4。 

下标和指针

        就像之前所说,在很多情况下使用数组的名字其实用的是一个指向数组首元素的指针。一个典型的例子就是当对数组使用下标运算符时,编译器会自动执行上述转换操作。

int ia[] = {0,2,4,6,8};
int i = ia[2];
int *p = ia;
i = *(p + 2);

        此时,ia[0]是一个使用了数组名称的表达式,对数组执行下标运算其实是对指向数组元素的指针进行下标运算。 

int *p = &ia[2];
int j = p[1];
int k = p[-2];

         只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算。

        与标准库类型string和vector不同的是,标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求可以处理负值,所用的索引值不是无符号类型。 

3.5.4 C风格字符串

        目前我们学到的处理字符串的方式有两种:C风格字符串,C++风格字符串。而我们知道C++是C的超集,所以C风格字符串在C++中是可以使用的。

        尽管C++支持C风格字符串,但在C++程序尽量不要使用C风格字符串。因为C风格字符串 不仅使用不方便,而且易引发程序漏洞。

        字符串字面值是一种通用结构的实例,这种结构是C++由C继承来的C风格字符串。C风格字符串不是一种类型,而是为了表示和使用字符串而形成的一种约定俗成的写法。按这种习惯写的字符串存放在字符数组中并且以空字符结束(空字符结束:在字符串最后一个字符后面跟着一个空字符串('\0')),一般用指针来操作字符串。

C标准库String函数

        表3.8列举了C语言标准库提供的一组函数,cstirng是C语言头文件,string.h的C++版本。

C++ Primer 第三章 字符串,向量和数组(万字合集)_第15张图片

传入此类函数的指针必须指向以空字符作为结束的数组:

char ca[] = {'C','+','+'};
//不以空字符结束
cout<

strlen函数将可能沿着ca在内存中的位置不断向前寻找,直到遇到空字符才停下来。

比较字符串

        比较标准库string对象的时候,用的是普通的关系运算符和相等性运算符:

string s1 = "A string example";
string s2 = "A different string";
if(s1 < s2) //false:s2小于s1

        如果把这些运算符用在两个C风格字符串上,实际比较的将是指针而非字符串本身:

const char ca1[] = "A string example";
const char ca2[] = "A different string";
if(ca1 < ca2) //未定义的:试图比较两个无关地址

         想要比较两个C风格字符串需要调用strcmp函数,此时比较的就不再是指针了。

如果两个字符串相等,strcmp 返回0;

如果前面的字符串比较大,strcmp 返回正值;

如果后面的字符串比较大,strcmp 返回负值;

if(strcmp(ca1,ca2)<0) 
//和两个string对象的比较s1
目标字符串的大小有调用者指定     

        连接或拷贝C风格字符串与标准库string对象的同类操作差别很大。例如,把刚刚定义的那两个string对象s1和s2连接起来:

string largeStr = s1 + " " + s2;
//将largeStr初始化成s1,一个空格和s2连接

        如果同样操作放好ca1和ca2两个数组身上就会产生错误了。表达式ca1 + ca2试图将两个指针相加,显然这样的操作没有什么意义,也绝对是非法的。

        正确的方法是使用stract函数和strcpy函数。不过想要使用者两个函数,还必须提供一个用于存放结果字符串的数组,该数组必须足够大能容纳下结果字符串以及末尾的空字符。 

        如果我们计算错了largeStr的大小将引发严重错误,一个潜在的问题是,我们在估算largeStr所需的空间时不容易估准,而且一旦内容发生改变,就必须重新检测其空间是否足够。这样的代码风险很高并且经常导致严重的安全泄露。所以对大多数应用来说,使用标准库srting要比C风格字符串更安全,更高效。

3.5.5 与旧代码的接口

        很多C++程序在标准库出现之前就已经写好了,它们肯定没用到string和vector类型。而且,有一些C++程序实际上是与C语音或其它语音的接口程序,当然也无法使用C++标准库。因此,想带的C++程序只能与string对象,vector对象,数组或C风格字符串的代码衔接和相互转换,为了让这一工作变简单一点,C++专门提供了一组功能。

混用string对象和C风格字符串

允许使用字符串字面值来初始化string对象:

String s("Hello World");

        更常见的是,任何出现字符串字面值的地方都可以用以空字符结束的字符数组来代替:

1. 允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值。

2.在string对象的加法运算中允许使用以空字符结束的字符数组为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。

        但是上述性质反过来就不成立了:如果程序的某处需要一个C风格字符串,无法直接用string对象来代替它。例如:不能用srting对象直接初始化指向字符的指针。为了完成该功能,string专门提供了一个名为c_str的成员函数:

char *str = s;
//错误:不能用string对象初始化char*
const char *str = s.c_str();
//正确

        顾名思义,c_str函数的返回值是一个C风格的字符串。所以该函数返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*,从而确保我们不会改变字符数组的内容。

        无法保证c_str函数返回的数组一直有效,如果改变了s的值就又可能让之前返回的数组失去效用。

        Tips:如果执行完c_str()函数后程序向一直都能使用其返回的数组,最好将该数组重新拷贝一份。

使用数组初始化vector对象

        不允许使用一个数组为另一个内置类型的数组赋初值,不允许使用vector对象初始化数组。相反,允许使用数组来初始化vector对象。实现这一目的,只需要指明要拷贝区域的首元素地址和尾后地址就可以了:

int int_arr[] = {0,1,2,3,4,5};
//ivec有6个元素,分别是int_arr中对应元素的副本
vector ivec(begin(int_arr),end(int_arr));

        上述代码中,用于创建ivec的两个指针实际上指明了用来初始化的值在int_arr中的位置,其中第二个指针应指向待拷贝区域尾元素的下一个位置。例如,用使用标准库函数begin和end来分别计算·int_arr的首指针和尾后指针。最终结果,ivec将包含6个元素,它们的次序和值都与数组int_arr完全一致。 

        用于初始化vector对象的值可能也只是数组中的一部分:

//拷贝三个元素:int_arr[1],int_arr[2],int_arr[3]
vector subVec(int_arr + 1,int_arr +4);

        该条代码初始化语句用3个元素创建了对象subVec,3个元素的值分别来自int_arr[1],int_arr[2],int_arr[3]。

3.6 多维数组

        其实C++中没有什么多维数组,所说的多维数组其实就是数组的数组。

        当一个数组的元素依旧是数组时,通常使用两个维度来定义它:一个维度表示数组本身大小,另外一个维度表示其元素(数组的数组)大写:

int ia[3][4];

        大小为3的数组,每个元素是含有4个整数的数组。

int arr[10][20][30] = {0};

        大小为10的数组,它的每个元素都是大小为20的数组,这些数组的元素又包含30个整数的数组,最后将所有元素初始化为0。

        由内到外的顺序阅读此类定义有助于更好地理解其真实含义。在第一条语句中,定义名称为ia,显然ia是一个含有3个元素的数组。再往右边写着ia的元素的维度,所以ia的元素本省又是含有4个元素的数组。在观察最左边,就能知道真正储存的元素(ia的元素的数组)是整数。因此最后可以明确第一条语句的含义:定义了一个大小为3,名为ia的数组,该数组的每个元素都是含有4个整数的数组。

        再用同样的方式理解第二条arr的定义,首先arr是一个大小为10的数组,它的每个元素都是大小为10的数组,而这些数组的元素又都是含有30个整数的数组。并且,定义数组时对下标运算符的数量并没有限制,因此只要愿意就可以定义一个数组,它的元素是数组,数组的元素又是数组(禁止套娃!)

        对于二维数组来说,常吧第一个维度成为行,第二个维度称为列。

多维数组的初始化

        允许使用花括号括起来的一组值初始化多维数组,这点和普通的数组一样。下面初始化形式中,多维数组的每一行分别用花括号括了起来:

int ia[3][4]=    //三个元素,每个元素都是大小为4的数组
{
    {0,1,2,3},  //第1行的初始值
    {4,5,6,7},  //第2行的初始值
    {8,9,10,11}  //第3行的初始值
};

        其中内层嵌套的花括号并非必需的,例如下面的初始化语句,形式上更为简洁,完成的功能和上面的代码完全一致:

int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
//没有标识每行的花括号,与之前的初始化语句是等价的

        类似一维数组,在初始化多维数组时也并非所有元素的值都必须包含在初始化表之内。如果仅仅想初始化每一行的第一个元素,通过如下的语句:

int ia[3][4] = {{0},{4},{8}};
//显式地初始化每行的首元素

        其他没有列出的元素执行默认初始化,这个过程和一位数组一样。此时如果去掉内层花括号,结果就不同了。

int ix[3][4] = {0,3,6,9};
//显式地初始化第1行,其他元素执行值的初始化。

        此时的含义是,它初始化了第一行的4个元素,其他元素被初始化0。 

多维数组的下标引用

        可以使用下标运算符来访问多维数组的元素,此时数组的每个维度都对应一个下标运算符。

        如果表达式含有的下标运算符数量和数组的维度一样多,该表达式的结果将是给定类型的元素;举个例子:arr[0][0][0]的下标运算符数量为3,并且arr是3维的,此时下标运算符数量和数组的维度就是一样多了。

        反之,如果表达式含有的下标运算符数量比数组的维度小,则表达式的结果将是定索引的一个内层数组:

ia[2][3] = arr[0][0][0];
//用arr的首元素为ia最后一行的最后一个元素赋值
int (&row)[4] = ia[1];
//把row绑定到ia的第二个4元素数组上

          在第一个例子中,对于用到的两个数组来说,表达式提供的下标运算符数量和它们各自的维度相同。在等号左侧,ia[2]得到数组ia的最后一行,此时返回的是表达ia最后一行的那个一维数组而并非任何实际元素;对于这个一维数组再去取下标,得到编号为[3]的元素,也就是这行的最后一个元素。

           经常使用for循环来处理多维数组的元素,二维数组大多数就两层嵌套的for循环去处理,外层for循环处理行,内层for循环处理列;三维数组中,最外层循环表示面,中间层表示行,最内层表示列。以此遍历数组:

constexpr size_t rowCnt = 3 , colCnt = 4;
int ia[rowCnt][colCnt];
for(size_t i = 0;i != rowCnt;++i)
{
    for(size_t j = 0;j != colCnt;++j)
        {
            ia[i][j] = i * colCnt + j;
        }
}
使用范围for语句处理多维数组

        由于在C++11新标准中新增了范围for语句,所以前一个程序可以简化为如下形式:

size_t cnt = 0;
for (auto &row : ia)
{
    for(auto &col : row)
        {
            col = cnt;
            ++cnt;
        }
}

        ia是一个由数组构成的数组,每次遍历,相当于一行一行的遍历了ia。所以每次遍历row相当于取出一行ia。相当于:对于外层数组的每一个元素。

        而col又相当于每一行的row的引用。相当于:对于内层数组的每一个元素。

        这个循环赋值给ia元素的值和之前的那个循环是完全相同的,区别在于通过使用范围for语句把管理数组索引的任务交给了系统。因为要改变元素的值,所以得把控制变量row和col声明成引用类型。        第一个for循环遍历ia的所有元素,这些元素是大小为4的数组,因此row的类型就应该是含有4个整数的数组的引用。        第二个for循环遍历那些4个元素数组中的某一个,因此col的类型是整数的引用。每次迭代把cnt的值赋给ia的当前元素,然后将cnt加1。

        在上面的例子中,因为要改变数组元素的值,所以我们选用引用类型作为循环控制变量,但是其实还有一个深层次的原因促使我们这么做。

for(const auto &row:ia)
    for(auto col:row)
        cout<

        这个循环中并没有任何写操作,但是我们还是将外层循环的控制变量声明成了引用类型,这是为了 避免数组ia被自动转换成指针 。

for(auto row:ia)
    for(auto col:row)

        此时ia被自动转化成指针,row也变成指针了,auto col:row就变成col遍历row的每一个地址。但是我们又不要遍历地址。所以外层的引用方式必须要加的。

        程序无法通过编译。这是因为,想之前一样第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素(和其他类型的数组一样)转换成指向该数组内首元素的指针。这样会得到的row的类型就是int*,显然内层的循环就不合法了,编译器将试图在一个int*内遍历,这显然和程序的初衷不一样。

        Tips:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。

指针和多维数组

        当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。

        因为多维数组实际上是数组的数组,所以由多维数组名转换而来的指针实际上是指向第一个内层数组的指针:

int ia[3][4];
//大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia;
//p指向含有4个整数的数组
p = &ia[2];
//p指向ia的尾元素。

        (*p)意味着p是一个指针。接着看右侧,指针p指向的是一个维度为4的数组;再看左侧可知,数组中的元素是整数。所以,p是指向含有4个整数的数组的指针。

//在上述声明中,圆括号必不可少:
int *ip[4];
//整形指针的数组
int (*ip)[4];
//指向含有4个整数的数组

        随着C++11新标准提出,通过使用auto或者decltype就能尽可能地避免在数组前面加上一个指针类型了:

//输出ia中每个元素的值,每个内层数组各占一行
//p指向含有4个整数的数组
for(auto p = ia;p != ia + 3; ++p)
{
    //q指向4个整数数组的首元素,也就是说,q指向一个整数
    for(auto q = *p; q != *p + 4;++q)
        cout << *q <<' ';
    cout<

        外层的for循环首先声明了一个指针并且令其指向ia的第一个内层数组,然后依次迭代直到ia的全部3行都处理完为止。其中递增运算++p负责将指针p移动到ia的下一行。

        内层的for循环负责输出内层数组所包含的值。它首先令指针q指向p当前所在行的第一个元素。*p是一个含有4个数组的数组,像往常一样,数组名被自动地转换成指向该数组首元素的指针。内层for循环不断迭代直到我们处理完了当前内层数组的所有元素为止。为了获取内层for循环的终止条件,再一次解引用p得到指向内层数组首元素的指针,给它加上4就得到了终止条件。

        当然,使用标准库函数begin和end也能实现同样的功能,而且看起来更简洁一些:

//p指向ia的第一个数组
for(auto p = begin(ia); p != end(ia); ++p)
{
    //q指向内层数组的首元素
    for(auto q = begin(*p); q != end(*p); ++q)
        cout<< *q << ' ' ;
        //输出q指向的整数
    cout <

在此代码中,循环终止条件由end函数负责判断。虽然我们能够判断出p和q的类型,但是直接使用auto关键字我们就不必再操心这些类型是什么了。

类型别名简化多维数组的指针

        读,写和理解一个指向多维数组的指针是让人不胜其烦的工作,使用类型别名能让工作变得简单一点。

using int_array = int[4];
//新标准下类型别名的声明
typedef int int_array[4];
//等价typedef声明
//输出ia中每个元素的值,每个内层数组各占一行
for(int_array *p = ia; p != ia + 3;++q)
{
    for (int *q = *p;q != *p + 4; ++q)
        cout<< *q <<' ';
    cout<

        程序将类型“4个整数组成的数组” 命名为int_array,用类型名int_array定义外层循环的控制变量让程序看着更简洁;

你可能感兴趣的:(C++,Primer,c++,算法,数据结构,开发语言,c语言,linux,ubuntu)