C++基础知识要点--字符串、向量和数组 (Primer C++ 第五版 · 阅读笔记)

曾经终败给现在 2024-04-01 18:10 194阅读 0赞

目录

    • C++基础知识要点—字符串、向量和数组
      • 1、命名空间的using声明
      • 2、标准库类型string
        • string对象上的操作
        • 处理string对象中的字符
      • 3、标准库类型vector(类模板)
        • 定义和初始化vector
        • 向vector对象中添加元素
        • 其他vector操作
      • 4、迭代器的介绍 ( ==类似于指针类型== )
        • 使用迭代器
        • 迭代器运算符
        • 迭代器的运算
      • 5、数组
        • 定义和初始化内置数组
        • 访问数组元素
        • 指针和数组
        • C风格字符串
        • 与旧代码的接口
      • 6、多维数组

C++基础知识要点–字符串、向量和数组

1、命名空间的using声明

有了using声明就无须专门的前缀(形如命名空间 :: )也能使用所需的名字了。using声明具有如下的形式:

  1. using namespace::name ;

一旦声明了上述语句,就可以直接访问命名空间中的名字:

  1. #include <iostream>
  2. //using声明,当我们使用名字cin时,从命名空间std中获取它
  3. using std::cin;
  4. int main (){
  5. int i;
  6. cin >> i; //正确:cin和std: :cin含义相同
  7. cout << i; //错误:没有对应的using声明,必须使用完整的名字
  8. std::cout << i; //正确:显式地从std中使用cout
  9. return 0;
  10. }

注:头文件不应包含using声明

2、标准库类型string

  1. #include <string>
  2. using std::string;

直接初始化和拷贝初始化

  • 如果使用等号(=)初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。
  • 与之相反,如果不使用等号,则执行的是直接初始化
  • 当初始值只有一个时,使用直接初始化或拷贝初始化都行。如果像上面的s7那样初始化要用到的值有多个,一般来说只能使用直接初始化的方式:

    string s5 = “hiya” ; //拷贝初始化
    string s6 ( “hiya” ) ; //直接初始化
    string s7(10, ‘c’) ; //直接初始化,s7的内容是cccccccccc

string对象上的操作

在这里插入图片描述
读写string对象

  1. int main ()
  2. string s; //空字符串
  3. cin >> s; //将string对象读入s,遇到空白(即空格符、换行符、制表符等)停止
  4. cout << s << endl; //输出s
  5. string line;
  6. //每次读入一整行,直至到达文件末尾
  7. while (getline (cin, line)) //getline函数直到遇到换行符为止
  8. cout << line << endl;
  9. return 0;

string::size_type类型
由于size函数返回的是一个无符号整型数,因此切记,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果

  • 例如,假设n是一个具有负值的int,则表达式s.size ( )<n的判断结果几乎肯定是true。这是因为负值n会自动地转换成一个比较大的无符号值。
  • 如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。

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

  1. string s4 = s1 +","; //正确:把一个string对象和一个字面值相加
  2. string s5 = "hello"+ ","; //错误:两个运算对象都不是string
  3. //正确:每个加法运算符都有一个运算对象是string
  4. string s6 = s1 + "," + "wor1d" ; //表达式s1+ ","的结果是一个string对象
  5. string s7 = "hello" + "," + s2; //错误:不能把字面值直接相加

注:因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。

处理string对象中的字符

在cctype头文件中定义了一组标准库函数处理这部分工作,表3.3列出了主要的函数名及其含义。
在这里插入图片描述

使用C++版本的C标准库头文件
cctype头文件(从C语言继承)和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间 std,而定义在名为.h的头文件中的则不然。

使用范围for语句改变字符串中的字符
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型,所谓引用只是给定对象的一个别名,因此当使用引用作为循环控制变量时,这个变量实际上被依次绑定到了序列的每个元素上。使用这个引用,我们就能改变它绑定的字符。

  1. string s("Hello world!!!" );
  2. //转换成大写形式。
  3. for (auto &c : s) //对于s中的每个字符(注意:c是引用)
  4. c= toupper(c) ; //c是一个引用,因此赋值语句将改变s中字符的值
  5. cout << s << endl;

下标运算符

  • 使用下标时必须确保其在合理范围之内,也就是说,下标必须大于等于0而小于字符串的 size()的值。
  • 一种简便易行的方法是,总是设下标的类型为string::size_type,因为此类型是无符号数,可以确保下标不会小于0。

    string::size_type num; //用于保存从输入流读取的数

  • 此时,代码只需保证下标小于size()的值就可以了。

    //依次处理s中的字符直至我们处理完全部字符或者遇到一个空白
    for (decltype(s.size()) index = 0; index != s.size() && !isspace (s[index]); ++index)

    1. s[index] = toupper(s[index] );//将当前字符改成大写形式

程序的输出结果将是:

  1. SOME string

3、标准库类型vector(类模板)

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

    include //要想使用vector,必须包含适当的头文件

    using std::vector;

模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化( instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。
对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。

  1. vector<int> ivec; // ivec保存int类型的对象
  2. vector<Sales_item> Sales_vec; //保存 sales_item类型的对象
  3. vector<vector<string>> file; //该向量的元素是vector对象

vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector

定义和初始化vector

定义vector对象的常用方法:
在这里插入图片描述
列表初始化vector对象

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

    vector articles = {

    1. "a", "an", "the" }; //拷贝初始化(即使用=时),只能提供一个初始值

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

  1. vector<string> v1{
  2. "a", "an", "the" }; //列表初始化
  3. vector<string> v2("a", "an", "the") ; //错误,(提供的初始元素值的列表,只能使用花括号,不能使用圆括号)

还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:

  1. vector<int> ivec (10,-1); //10个int类型的元素,每个都被初始化为-1
  2. vector<string> svec (10,"hi!"); // 10个string类型的元素,每个都被初始化为"hi!"
  3. vector<string> v6("hi"); //错误:不能使用字符串字面值构建vector对象

初始化一定数量的相同元素

  1. vector<int> ivec(10) ; //10个元素,每个都初始化为0
  2. vector<string> svec(10); //10个元素,每个都是空string对象
  3. //注意是初始化时用的圆括号还是花括号
  4. vector<int> v1(10); // v1有10个元素,每个的值都是0
  5. vector<int> v2{
  6. 10}; // v2有1个元素,该元素的值是10
  7. vector<int> v3(101); //v3有10个元素,每个的值都是1
  8. vector<int> v4{
  9. 101}; // v4有2个元素,值分别是10和1
  • 如果用的是圆括号,可以说提供的值是用来构造(construct) vector对象的,
  • 如果用的是花括号,可以表述成我们想列表初始化(list initialize)该vector对象。

如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了:

  1. vector<string> v5 {
  2. "hi"}; //列表初始化:v5有一个元素
  3. vector<string> v5 {
  4. "hi"}; //列表初始化:v5有一个元素
  5. vector<string> v6 ("hi"); //错误:不能使用字符串字面值构建vector对象
  6. vector<string> v7 {
  7. 10}; //v7有10个默认初始化的元素
  8. vector<string> v8 {
  9. 10,"hi"}; //v8有10个值为"hi"的元素
向vector对象中添加元素

元素数量无法确定时,利用vector 的成员函数push _back向其中添加元素。push_back负责把一个值当成vector对象的尾元素 “压到 (push)” vector对象的“尾端( back)”。例如:

  1. vector<int> v2; //空 vector对象
  2. for (int i = 0; i != 100; ++i)
  3. v2.push_back(i); //依次把整数值放到v2尾端
  4. //循环结束后v2有100个元素,值从0到99
  5. //从标准输入中读取单词,将其作为vector对象的元素存储string word;
  6. vector<string> text; //空vector对象
  7. while (cin >> word){
  8. text.push_back ( word); //把 word添加到text后面
  9. }

vector使用的蕴含要求:

  • 如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
  • 范围for语句体内不应改变其所遍历序列的大小。
其他vector操作

大多数都和string的相关操作类似
在这里插入图片描述
size返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型。要使用size_type,需首先指定它是由哪种类型定义的、vector对象的类型总是包含着元素的类型:

  1. vector<int>::size type //正确
  2. vector::size type //错误

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

4、迭代器的介绍 ( 类似于指针类型 )

  • 我们已经知道可以使用下标运算符来访问string对象的字符或vector对象的元素,还有另外一种更通用的机制也可以实现同样的目的,这就是迭代器(iterator)。
  • 除了vector之外,标准库还定义了其他几种容器。所有标准库容器 都可以使用迭代器,但是其中只有少数几种才同时支持下标运算符。
  • 严格来说,string对象不属于容器类型,但是string支持很多与容器类型类似的操作。vector支持下标运算符,这点和string一样;string支持迭代器,这也和 vector是一样的。

类似于指针类型

  • 迭代器也提供了对对象的间接访问。就迭代器而言,其对象是容器中的元素或者string对象中的字符。使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另外一个元素。
  • 迭代器有有效和无效之分,这一点和指针差不多。有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置;其也所有情况都属于无效。
使用迭代器

和指针不一样的是,获取迭代器不是使用取地址符,有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都拥有名为beginend 的成员

  • 其中 begin成员负责返回指向第一个元素(或第一个字符)的迭代器。
  • end 成员则负责返回指向容器(或string对象)“尾元素的下一位置(one past the end)”的迭代器,也就是说,该迭代器指示的是容器的一个本不存在的“尾后(off the end)”元素,end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器
  • 如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。

    //b表示v的第一个元素,e表示v尾元素的下一位置
    auto b = v.begin(), e = v.end (); //b 和e的类型相同

迭代器运算符

使用 == 和 != 来比较两个合法的迭代器是否相等,如果两个迭代器指向的元素相同或者都是同一个容器的尾后迭代器,则它们相等;否则就说这两个迭代器不相等。
在这里插入图片描述

  • 和指针类似,也能通过解引用迭代器来获取它所指示的元素

    //利用下标运算符把string对象的第一个字母改为大写
    string s(“some string”);
    if (s.begin () != s.end()) {

    1. //确保s非空
    2. auto it = s.begin(); // it表示s的第一个字符
    3. *it = toupper(*it); //将当前字符改成大写形式

    }

    //依次处理s的字符直至我们处理完全部字符或者遇到空白
    for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)

    1. *it = toupper(*it) ; //将当前字符改成大写形式

泛型编程
原来使用C或 Java的程序员在转而使用C++语言之后,会对for循环中使用!=而非<进行判断有点儿奇怪。C++程序员习惯性地使用 != ,其原因和他们更愿意使用 迭代器 而非 下标 的原因一样:因为这种编程风格在标准库提供的所有容器上都有效

之前已经说过,只有string和 vector等一些标准库类型有下标运算符,而并非全都如此。与之类似,所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符。因此,只要我们养成使用迭代器和!=的习惯,就不用太在意用的到底是哪种容器类型。

迭代器类型
那些拥有迭代器的标准库类型使用 iterator 和 const_iterator 来表示迭代器的类型:

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

begin和end返回的具体类型由对象是否是常量决定

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

为了便于专门得到const_iterator类型的返回值,C++11新标准引入了两个新函数,分别是 cbegincend:

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

结合解引用和成员访问操作
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员。

  1. //前面的圆括号必不可少
  2. (*it).empty() //解引用it,然后调用结果对象的empty成员
  3. *it.empty() //错误:试图访问it的名为empty的成员,但it是个迭代器
  4. //没有empty成员

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

  1. //依次输出text的每一行直至遇到第一个空白行为止
  2. for (auto it = text.cbegin(); it != text.cend() && !it->empty(); ++it)
  3. cout<<*it<<endl;

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

  • 已知的一个限制是不能在范围for循环中向vector对象添加元素。
  • 另外一个限制是任何一种可能改变vector对象容量的操作,比如 push_back,都会使该vector对象的迭代器失效。
迭代器的运算

在这里插入图片描述

  1. //计算得到最接近vi中间元素的一个迭代器
  2. auto mid = vi.begin() + vi.size() / 2;
  3. if (it < mid)
  4. //处理vi前半部分的元素

两个指向同一个容器中的元素的人迭代器相减,所得距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为difference_type的带符号整型数。

使用迭代器
使用迭代器运算的一个经典算法是二分搜索。

  1. // text必须是有序的
  2. //beg 和 end表示我们搜索的范围
  3. auto beg = text.begin() , end = text.end();
  4. auto mid = text.begin() + (end - beg) / 2; //初始状态下的中间点
  5. //当还有元素尚未检查并且我们还没有找到sought时执行循环
  6. while (mid != end && *mid != sought) {
  7. if (sought< *mid) //我们要找的元素在前半部分吗?
  8. end = mid; //如果是,调整搜索范围使得忽略掉后半部分
  9. else //我们要找的元素在后半部分
  10. beg = mid + l; //在mid之后寻找
  11. mid = beg + (end - beg) / 2 ; //新的中间点
  12. )

5、数组

数组是一种类似于标准库类型vector 的数据结构, 但是数组的大小确定不变,不能随意向数组中增加元素。因此对某些特殊的应用来说程序的运行时性能较好,但是相应地也损失了一些灵活性。

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

定义和初始化内置数组

数组的声明形如a[d],数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式:

  • 定义数组的时候必须指定数组的类型,不允许用auto关键字由初始值的列表推断类型。
  • 另外和 vector一样,数组的元素应为对象,因此不存在引用的数组。
  • 默认情况下,数组的元素被默认初始化

    unsigned cnt = 42; //不是常量表达式
    constexpr unsigned sz = 42; //常量表达式,关于constexpr;
    int arr[10]; //含有10个整数的数组
    int *parr[sz]; //含有42个整型指针的数组
    string bad[cnt]; //错误: cnt不是常量表达式
    string strs[get_size()]; // 当get_size是constexpr时正确; 否则错误

显式初始化数组元素

  1. const unsigned sz = 3;
  2. int ia1[sz] = {
  3. 0, 1, 2}; //含有3个元素的数组,元素值分别是0,1,2
  4. int a2[]= {
  5. 0, 1, 2}; //维度是3的数组
  6. int a3[5]= {
  7. 0, 1, 2}; //等价于a3 []= {0, 1, 2, 0, 0}
  8. string a4[3]= ("hi", "bye"}; //等价于a4[] = { "hi", "bye", "")
  9. int a5 [2] = {
  10. 0, 1, 2}; //错误:初始值过多

字符数组的特殊性

  1. char a1 [] = {
  2. 'c', '+’,'+’}; //列表初始化,没有空字符
  3. char a2[] - {
  4. 'C', '+', '+' , \0'}; //列表初始化,含有显式的空字符
  5. char a3[] = "C++"; //自动添加表示字符串结束的空字符
  6. const char a4[6]= "Daniel"; //错误:没有空间可存放空字符!

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

  1. int a[] = {
  2. 0, 1, 2}; //含有3个整数的数组
  3. int a2[]= a; //错误:不允许使用一个数组初始化另一个数组
  4. a2 = a; //错误:不能把一个数组直接赋值给另一个数组

理解复杂的数组声明

  • 要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读,然后再从右向左阅读。

    int ptrs[10]; //ptrs是含有10个整型指针的数组
    int &refs[10]=/
    ?/; //错误:不存在引用的数组
    int (
    Parray)[10] = &arr; //Parray指向一个含有10个整数的数组
    int (&arrRef)[10] = arr; //arrRef引用一个含有10个整数的数组
    int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针

访问数组元素
  • 数组除了大小固定这一特点外,其他用法与vector基本类似。
  • 在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在 cstddef头文件中定义了size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。

    //以10分为一个分数段统计成绩的数量:0~9,10~19,….90~99,100
    unsigned scores [11] = {

    1. }; //11个分数段,全部初始化为О

    unsigned grade;
    while (cin >> grade){

    1. if (grade <= 100)
    2. ++scores[grade/10]; //将当前分数段的计数值加1

    }

指针和数组
  • 在C++语言中,指针和数组有非常紧密的联系。使用数组的时候编译器一般会把它转换成指针;使用数组类型的对象其实是使用一个指向该数组首元素的指针。
  • 通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。
  • 因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

    string nums[] = {

    1. "one", "two""three"); //数组的元素是string对象

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

    //在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:
    string *p2 = nums; //等价于 p2 =&nums[0]

在一些情况下数组的操作实际上是指针的操作

  1. int ia[] = {
  2. 0,1,2,3,4,5,6,7,8,9} ; // ia是一个含有10个整数的数组
  3. auto ia2(ia); //ia2是一个整型指针,指向ia的第一个元素, 等价于auto ia2(&ia[0]); 显然ia2的类型是int*
  4. ia2 = 42; //错误:ia2是一个指针,不能用int值给指针赋值

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

  1. // ia3是一个含有10个整数的数组
  2. decltype(ia) ia3 = {
  3. 0,1,2,3,4,5,6,7,8,9} ;
  4. ia3 = p; //错误:不能用整型指针给数组赋值
  5. ia3[4] = i; //正确:把i的值赋给ia3的一个元素

指针也是迭代器
指向数组元素的指针拥有更多功能,vector和string 的迭代器支持的运算,数组的指针全都支持。

  1. int arr[]= {
  2. 0,1,2,3,4,5,6,7,8,9};
  3. int *p = arr; //p指向arr的第一个元素
  4. ++p; //p指向arr[1]
  5. int *e = &arr[10]; //指向arr尾元素的下一位置的指针,尾后迭代器,(但这种方法易出错)
  6. for (int *b = arr; b != e; ++b)
  7. cout << *b << endl; //输出arr的元素

标准库函数 begin 和 end

  • 上述方法尽管能计算得到尾后指针,但这种用法极易出错。为了让指针的使用更简单、更安全,C++11新标准引入了两个名为 begin和 end 的函数。
  • begin函数返回指向ia首元素的指针,end函数返回指向ia尾元素下一位置的指针,这两个函数定义在iterator头文件中。

    int ia[] = {

    1. 0,1,2,3,4,5,6,7,8,9}; //ia是一个含有10个整数的数组

    int beg = begin(ia) ; //指向ia首元素的指针
    int
    last = end(ia); //指向arr尾元素的下一位置的指针

指针运算

  1. constexpr size_t sz = 5;
  2. int arr[sz] = {
  3. 1,2,3,4,5};
  4. int *ip = arr; //等价于int *ip = &arr[0]
  5. int *ip2 = ip + 4; //ip2指向arr的尾元素arr[4]
  6. //减法
  7. auto n = end(arr) - begin(arr); //n的值是5,也就是arr中元素的数量

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

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

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

下标和指针

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

  1. int *p = &ia[2]; //p指向索引为2的元素
  2. int j = p[1]; //p[1]等价于*(p + 1),就是ia[3]表示的那个元素
  3. int k = p [-2]; //p[-2]是ia [ 0]表示的那个元素
  • 虽然标准库类型string和 vector也能执行下标运算,但是数组与它们相比还是有所不同。
  • 标准库类型限定使用的下标必须是无符号类型,而 内置的下标 运算 无此要求,内置的下标运算符可以处理负值,
  • 当然,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。
C风格字符串

尽管C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。操作C风格字符串的函数定义在cstring头文件中。

对大多数应用来说,使用标准库string要比使用C风格字符串更安全、更高效。

与旧代码的接口

现代的C++程序应当尽量使用 vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。

6、多维数组

严格来说,C++语言中没有多维数组,通常所说的多维数组其实是数组的数组。谨记这一点,对今后理解和使用多维数组大有益处。

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

  1. int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
  2. //大小为10的数组,它的每个元素都是大小为20的数组,
  3. //这些数组的元素是含有30个整数的数组
  4. int arr [10][20][30]= {
  5. 0}; //将所有元素初始化为О

对于二维数组来说,常把第一个维度称作行,第二个维度称作列。

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

  1. int ia[3][4] = {
  2. //三个元素,每个元素都是大小为4的数组
  3. {
  4. 0, 12, 3}, //第1行的初始值
  5. {
  6. 4, 567}, //第2行的初始值
  7. {
  8. 891011} //第3行的初始值
  9. };
  10. //其中内层嵌套着的花括号并非必需的
  11. //没有标识每行的花括号,与之前的初始化语句是等价的
  12. int ia[3][4] = {
  13. 0,1,2,3,4,5,6,7,8,9,10,11};
  14. //显式地初始化每行的首元素
  15. int ia[3][4] = {
  16. {
  17. 0 }, {
  18. 4 }, {
  19. 8 }};
  20. //显式地初始化第1行,其他元素执行值初始化
  21. int ix[3][4]= {
  22. 0, 3, 6, 9};

多维数组的下标引用

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

程序中经常会用到两层嵌套的for循环来处理多维数组的元素, 我们将元素的值设为该元素在整个数组中的序号。

  1. constexpr size_t rowCnt = 3, colCnt = 4 ;
  2. int ia[rowCnt][colcnt]; //12个未初始化的元素
  3. //对于每一行
  4. for (size_t i = 0; i != rowCnt; ++i){
  5. //对于行内的每一列
  6. for (size_t j = 0; j != colCnt; ++j){
  7. //将元素的位置索引作为它的值
  8. ia[i][j] = i * colCnt + j;
  9. }
  10. }

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

  1. size_t cnt = 0;
  2. for (auto &row : ia) //对于外层数组的每一个元素
  3. for (auto &col : row){
  4. //对于内层数组的每一个元素
  5. col = cnt; //将下一个值赋给该元素
  6. ++cnt; //将cnt加1
  7. }

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

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

  1. for (const auto &row : ia) //对于外层数组的每一个元素
  2. for (auto col : row) //对于内层数组的每一个元素
  3. cout << col << endl;

假设不用引用类型,则循环如下述形式:

  1. for (auto row: ia)
  2. for (auto col : row)

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

指针和多维数组

  • 当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
  • 定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。

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

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

我们首先明确(*p)意味着p是一个指针。接着观察右边发现,指针p所指的是一个维度为4的数组;再观察左边知道,数组中的元素是整数。因此,p就是指向含有4个整数的数组的指针。
在这里插入图片描述
随着C++11新标准的提出,通过使用auto或者decltype 就能尽可能地避免在数组前面加上一个指针类型了:

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

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

  1. //p指向ia的第一个数组
  2. for (auto p = begin(ia) ; p!= end(ia) ; ++p){
  3. //q指向内层数组的首元素
  4. for (auto q = begin(*p); q != end (*p); ++q)
  5. cout<< *q << ' '; //输出q所指的整数值
  6. cout << endl;
  7. }

虽然我们也能推断出p 的类型是指向含有4个整数的数组的指针,q的类型是指向整数的指针,但是使用auto关键字我们就不必再烦心这些类型到底是什么了。

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

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

程序将类型“4个整数组成的数组”命名为int_array,用类型名int_array定义外层循环的控制变量让程序显得简洁明了。

:仅供学习参考,有什么错误欢迎指正!

发表评论

表情:
评论列表 (有 0 条评论,194人围观)

还没有评论,来说两句吧...

相关阅读