• C++ 数组 详解


     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的迭代器支持的运算,数组的指针全都支持。

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

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

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

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

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

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

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

    1. for (int *b = arr; b != e;++b)
    2. {
    3. cout<< *b <
    4. }

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

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. const int size = 5;
    6. int arr[size];
    7. int *pt = arr;
    8. for(int i = 0; i < size; i++)
    9. *(pt + i)=0;
    10. for(int i = 0; i < size; i++)
    11. cout<<*(pt+i)<<"";
    12. cout<
    13. return 0;
    14. }

     

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

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. const int size = 5;
    6. int arr1[size]={1,2,3,4,5};
    7. int arr2[size]={1,2,5,7,9};
    8. for(int i= 0;i
    9. {
    10. if(arr1[i]!=arr2[i])
    11. {
    12. cout<<"arr1!=arr2"<
    13. return -1;
    14. }
    15. }
    16. cout<<"arr1==arr2"<
    17. return 0;
    18. }

    1. #iclude
    2. #iclude
    3. using namespace std;
    4. int main()
    5. {
    6. vector<int> int_vector1 = {1,2,3,4,5};
    7. vector<int> int_vector2 = {1,2,7,9,14};
    8. vector<int> int_vector3 = {1,2,3,4,5};
    9. auto it1 = int_vector1.begin();
    10. auto it2 = int_vector2.begin();
    11. auto it3 = int_vector3.begin();
    12. while(it1!= int_vector1.end()&&it2!=int_vector2.end()&&it3!=int_vector3.end())
    13. {
    14. if(*it1 != *it2&&*it1==*it3)
    15. {
    16. cout<<"int_vector1!=int_vector2"<
    17. cout<<"int_vector1==int_vector3"<
    18. return -1;
    19. }
    20. it1++;
    21. it2++;
    22. it3++;
    23. }
    24. return 0;
    25. }

    标准库函数begin和end

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

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

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

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

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

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

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

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

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

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

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

    指针运算

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

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

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

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

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

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

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

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

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

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

    1. int *b = arr,*e = arr + sz;
    2. while(b
    3. {
    4. ++b;
    5. }

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

    解引用和指针运算的交互

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

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

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

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

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

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

    下标和指针

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

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

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

    1. int *p = &ia[2];
    2. int j = p[1];
    3. 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++版本。

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

    1. char ca[] = {'C','+','+'};
    2. //不以空字符结束
    3. cout<<strlen(ca)<
    4. //严重错误:ca没有以空字符结束

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

    比较字符串

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

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

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

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

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

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

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

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

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

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

    1. string largeStr = s1 + " " + s2;
    2. //将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的成员函数:

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

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

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

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

    使用数组初始化vector对象

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

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

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

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

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

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

    更详细,更全面的介绍,垂阅此文章(该文章篇幅较长,推荐使用电脑阅读)。

    C++ Primer 第三章字符串,向量和数组_WWester的博客-CSDN博客

  • 相关阅读:
    「尚硅谷与腾讯云官方合作」硅谷课堂项目视频发布
    Vue2 element selection组件设置默认选项
    C++进制转换题
    Python传参拷贝问题
    一文了解riscv软件系列之linux内核编译运行
    vue实现的商品列表网页
    主要开源WebGIS介绍、自由及开源GIS软件、组件产品
    Linux 创建虚拟机和安装CentOS过程中的参数解释
    clone()方法使用时遇到的问题解决方法(JAVA)
    JAVA中简单的for循环竟有这么多坑,你踩过吗
  • 原文地址:https://blog.csdn.net/m0_73671341/article/details/132622893