字符串、vector和数组

129 阅读13分钟
  • 3.1 命名空间的using声明(自行回忆)

    • 一般来说位于头文件的代码不应使用using声明,否则可能会包含不需要的声明,发生始料未及的名字冲突。
  • 3.2 string

    • 直接初始化与拷贝初始化

      • 6种初始化string方式:

        • string s1 //默认初始化空串
        • string s2(s1) 等价于 string s2 = s1 //s2是s1的副本
        • string s3("value") 等价于 string s3 = "value" //s3是字面值"value"的副本
        • string s4(n,'c') //将s4初始化为连续n个字符c组成的串。
      • 使用 = 初始化一个变量,执行的是拷贝初始化;使用()或{}执行的是直接初始化。当初始化需要多个值时,一般只能使用直接初始化。
    • string的操作:>>,<<,getline,.empty(),.size(),[],+,=,==,!=,<,<=,>,>=等

      • cin>>s 与 getline(is,s)

        • cin>>s 会自动忽略开头的空白(即空格符、换行符、制表符等),从第一个真正的字符开始读起,到遇见下一处空白为止。
        • getline(is,s)会在最终得到的字符串中保留输入时的空白符。getline遇换行符就结束读取操作并返回结果(读取但丢弃了换行符)。
      • 比较string

        • 1.两个string对象长度不同,且短的对象的每个字符都与长对象对应位置上字符相同,则短string对象小于长string对象。
        • 2.两个对象没共性,则比较结果取第一对相异字符的比较结果。
      • +:当string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个+两侧的运算对象至少有一个是string。

        • ⭐ C++中字符串字面值与string是不同的类型。
    • cctype中处理string中字符的主要函数:

      • tolower(c) //输出对应的小写字母
      • toupper(c) //输出对应的大写字母
      • islower(c) //c为小写字母时为真
      • isupper(c) //c为大写字母时为真
      • isalpha(c) //c为字母时为真
      • isdigit(c) //c为数字时为真
      • isxdigit(c) //c为十六进制数字时为真
      • isspace(c) //c为空白(空格、横纵制表符、回车、换行符或进纸符)时为真
      • iscntrl(c) //c为控制字符时为真
      • ispunct(c) //c为标点符号(c不是控制字符、数字、字母、可打印空白中的任一种)时为真。默认情况下,标点符号为 ! " # $ % & ' ( ) * + , - . / : ; ? @ [ \ ] ^ _ ` { | } ~
      • isprint(c) //c为可打印字符时为真(即c是空格或c具有可视形式)。可打印字符是指除ASCII码0~31及127(共33个)是控制字符或通信专用字符,剩余的都属于可打印字符。
      • isgraph(c) //c不是空格但可打印时为真
    • 范围for——处理string对象中的每个字符的最佳方法。

      • 语法:循环变量定义成auto &可用于修改各值。
      • ⭐ 范围for体内不应改变所遍历序列的大小。
    • string::size_type类型:无符号类型,能存下任何string对象的大小,是str.size()的返回值类型,string对象下标符[]内接受的参数类型。
    • 下标运算符[],接收的输入参数是string::size_type类型的值,返回值是该位置上字符的引用。
  • 3.3 vector——初识容器

    • vector基础初始化

      • 7种初始化:最常见的方式是先定义一个空vector,之后在运行时获取到元素的值后再逐一添加。

        • vector v1 //空vector,潜在的元素是T类型的,执行默认初始化
        • vector v2(v1) 等价于 vector v2 = v1 //v1所有元素拷贝到v2
        • vector v3(n,val) //v3包含n个值为val的元素
        • vector v4(n) //v4包含n个值初始化的元素

          • 只提供元素数量时,库会创建一个值初始化的元素初值,并将它赋给容器中的所有元素。

            • 有些类必须明确提供初始值。则这种类型的对象只提供元素数量就无法完成初始化。
            • 提供元素数量不设定初始值,只能使用直接初始化。
        • vector v5 {a,b,c,...} 等价于 vector v5 = {a,b,c,...} //v5包含{}内个数的元素,每个元素被赋相应初始值

          • 初始化时用列表初始化的形式,但提供的值又不能用来列表初始化,则考虑用这样的值来构造vector对象,即转换为圆括号的用法:

            • 由上例可见,想要列表初始化vector对象,花括号内的值必须与元素类型相同。
      • 用数组初始化vector——C++11新标,begin(arr)和end(arr)函数,定义在iterator头文件中。
      • 适用于直接初始化vector的三种情况:

        • 1.初始值已知且数量较少;
        • 2.初始值是另一个vector对象的副本;
        • 3.所有元素的初始值一样。
        • 其他情况不是说不能用,但太烦琐了。
      • vector在运行时能高效快速地添加元素,定义时设定大小没必要不说,还可能影响性能。
    • .push_back()——向vector里添加元素

      • push_back负责把一个值当成vector的尾元素push到vector对象的尾端(back):
      • 若循环体中包含向vector添加元素的语句,则不能使用范围for:⭐ 范围for体内不应改变所遍历序列的大小。
    • vec.size()

      • 返回值类型为 vector::size_type ,必须指明由哪种类型T定义。如:vector::size_type ss;
    • vector下标索引的作用:

      • vector对象(及string对象)的下标运算符可用于访问已存在的元素,不能用于添加元素。
      • ⭐ 确保下标合法的一种有效手段即尽可能使用范围for。
  • 3.4 使用迭代器

    • 为何获取容器中元素时应多使用迭代器:所有标准库容器都可以使用迭代器,只有少数几种支持下标运算符。
    • 与指针类似,迭代器也提供了对对象的间接访问。对迭代器而言,其对象是容器中的元素或string对象中的字符,使用迭代器可访问某个元素,也能从一个元素移动到另一个元素。
    • 迭代器的有效与无效:有效的迭代器指向其中某个元素,或指向容器的尾后元素;其他情况均属无效。
    • 获取迭代器不使用取地址符,使用成员函数.begin(),.end();//指向尾后元素,称为尾后迭代器,一般是用作"标兵位"。用.end()返回的迭代器不实指某个元素,∴不要对它进行递增或解引用。
    • 迭代器的运算符与指针类似:*iter,iter->mem,++iter,--iter,iter1==iter2,iter1!=iter2

      • C++的风格——为何C++工程师惯性用"!="作为循环判断条件,习惯用迭代器而非下标?

        • 1.因为这种编程风格在标准库提供的所有容器上都适用,只有少数几种标准库类型有下标运算符。
        • 2.所有标准库容器的迭代器都定义了==和!=,反而大多都没有定义<。
        • ∴要养成使用迭代器和!=的习惯。
    • 迭代器类型——每个容器类(包括string)都定义了iterator类型,支持“迭代器可访问容器元素,从某个元素移动到另一个元素”的操作。

      • iterator:如果vector或string对象不是常量,既能使用iterator又能使用const_iterator
      • const_iterator:如果vector或string对象是常量,只能使用const_iterator。若对象只需读操作,最好使用const_iterator。
      • .begin()和.end()

        • 返回类型由对象是否为常量决定,若是,返回const_iterator;反之返回iterator。
        • C++11新标,为专门得到const_iterator类型的返回值,引入.cbegin()和.cend()。不论对象本身是否为常量,返回值都是const_iterator。
      • 迭代器的解引用+成员访问操作符

        • 解引用符可获得迭代器所指的对象,如果该对象类型恰好是类,就能用成员访问符进一步访问。
        • 箭头运算符 ->:同时完成解引用和成员访问。
        • eg:
      • 使vector迭代器失效的操作:任何一种可能改变vector对象容量的操作,如push_back,都会使该vector对象的迭代器失效。(9.3.6节详解 #填坑 )
      • 迭代器运算:iter+n,iter-n,iter+=n,iter-=n,iter1-iter2,>,>=,<,<=

        • difference_type类型:带符号整数,是vector、string迭代器差值返回的类型。
        • 对关系运算符,要求参与运算的迭代器必须指向同一个容器中的元素或尾后元素。如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则前者小于后者。
  • 3.5 数组

    • 数组与vector的异同

      • 同:

        • 有一段连续的内存,有固定的起始地址,可进行随机访问。
        • 存放类型相同的对象。
        • 需要通过其所在位置访问对象元素。
        • 可使用下标符访问元素。
      • 异:

        • 数组存放在栈中,vector存放在堆中。
        • 数组大小确定不变,定义时已确定长度,不能随意向数组中增加元素。
        • 数组不能用另一个数组初始化,也不能将一个数组赋值给另一个数组。
        • 下标类型,vector为vector::size_type;数组为size_t。
      • 因为数组大小固定,性能好的同时相应会损失灵活性。如果不清楚元素的确切个数,请使用vector。
    • 数组的定义

      • 与内置类型变量相同,定义在函数内部的内置类型的数组,数组元素默认初始化后是未定义的。
      • 定义数组时必须指明数组类型,不允许使用auto。
      • 维度必须是大于0的整数常量表达式。
      • 使用列表初始化时,可忽略数组维度,编译器会根据初始值的数量计算并推测出来。
      • 字符数组可以用字符串字面值初始化,字面值结尾处的空字符也会被拷贝进数组。
      • 复杂的数组声明

        • 对于这类数组,应该从数组名字开始,由内而外,由右向左的顺序理解。

          • 理解示例:
        • 更复杂的:

          • 理解示例:
      • size_t类型:

        • 数组下标的类型
        • 一种机器相关的无符号类型
        • 定义在cstddef头文件中
      • 与vector和string不同,内置的下标运算符所用的索引值不是无符号类型,即可处理负值。(此处我目前的理解:1.指针有指向内存中的对象的功能,且能通过+、-等操作将指向往前往后移动,对指针使用下标运算符p[-1]是合法的,等价于p-1的操作,即向后挪一位。数组会被编译器自动转换成指向首元素的指针,也具备这种特性。2.指针指向数组时,给指针下标输入正、负值其本质是控制指针向前、向后挪位。3.为何上条强调数组的下标size_t是无符号类型?是因为数组名是指向数组首元素的指针且数组是不能赋值的,数组名即可看作是常量指针,其指向初始化后是无法变动的,所以数组下标为负会指向一个不属于该程序的内存地址,这是不合法的但是编译器不会报错;而指针可指向数组中的元素,对指针下标输入正、负值可使指针在同一数组的元素中前后移动,这是合法的。因此认为数组下标是无符号类型只是避免出现不必要的错误。)
    • 指针与数组

      • 编译器一般会把数组转换成指针。在很多用到数组名字的地方,编译器会自动将其替换成一个指向数组首元素的指针。因此当使用数组作为一个auto变量的初始值时,推断得到的类型是指针。如图:

        • 尽管arr是数组,但用其做初始值时,编译器实际指向的初始化过程类似于auto p(&arr[0]);
        • ps:decltype、&数组名、sizeof、typeid不会发生前面auto的类型转换。
      • 数组的指针也是迭代器:vector和string迭代器支持的运算,数组的指针全都支持。
      • C++11新标,begin(arr)和end(arr)函数,定义在iterator头文件中。

        • 用于返回数组的首元素指针和尾后元素指针。
      • 因编译器会把数组转换成指针,因此可对数组名解引用访问对应位置的元素。
      • ptrdiff_t类型:两指针相减的结果类型,定义在cstddef头文件中的机器相关的带符号类型。
    • C风格字符串——以空字符('\0')结束的字符数组

      • 尽管C++支持,但在C++程序中最好还是不要使用。
      • 相关函数:

        • strlen(p) //返回p的长度,'\0'不算在内
        • strcmp(p1,p2) //p1==p2,返回0;p1>p2,返回正值;p1<p2,返回负值
        • strcat(p1,p2) //将p2附加到p1之后,返回p1
        • strcpy(p1,p2) //将p2拷贝给p1,返回p1
    • 与旧代码的兼容

      • 1.已知允许用字符串字面值初始化string对象。又∵任何出现字符串字面值的地方可被“以空字符结束的字符数组”替换,∴允许用“以空字符结束的字符数组”初始化string或为其赋值。
      • 2.在string的加法运算中,允许用“以空字符结束的字符数组”作为其中的一个运算对象。
      • 3.在复合赋值运算中(+=,-=等),允许用“以空字符结束的字符数组”作为右侧的运算对象。
      • 上述性质反过来不成立:程序某处需要C风格字符串可能无法直接用string对象来代替。如,不能用string对象直接初始化指向字符的指针,需用c_str()函数处理string对象,返回指针类型是const char*。(这块返回const修饰的类型,书上说是确保我们不会改变字符数组的内容,暂不知道限制这个有何意义,防止外人改变内容?)

        • 若后续操作改变了str的值,可能会让之前返回的数组失去效用。若想一直使用c_str()返回的数组,最好将该数组重新拷贝一份。
      • 现代C++程序应尽量使用vector和迭代器,而不是内置数组和指针;尽量使用string,而不是C风格字符串。
  • 3.6 多维数组

    • 严格说C++没有多维数组,多维数组实际是数组的数组。
    • 用范围for处理多维数组,除最内层的循环外,其他所有循环的控制变量都应是引用类型。——这是为了避免数组被自动转成指针,不使用引用类型编译器会自动将数组形式的元素转为指向该数组内首元素的指针,指针用于内层循环显然是不合法的。
    • 由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
    • C++11新标提出,使用auto避免在数组前面显式声明指针类型:
    • 类型别名简化多维数组的指针(虽然看起来一点不简化( ̄▽ ̄)"):