【C++基础】第十七课:数组

定义和初始化内置数组,访问数组元素,指针和数组

Posted by x-jeff on May 31, 2020

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

vector相似,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。

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

2.定义和初始化内置数组

数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0

⚠️维度必须是一个常量表达式

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

如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。

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

2.1.显式初始化数组元素

可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值:

1
2
3
4
5
6
const unsigned sz=3;
int ial[sz]={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};//错误:初始值过多

2.2.字符数组的特殊性

⚠️字符数组有一种额外的初始化形式,我们可以用字符串字面值对此类数组初始化。

‼️当使用这种方式时,一定要注意字符串字面值的结尾处还有一个空字符,这个空字符也会像字符串的其他字符一样被拷贝到字符数组中去:

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

a1的维度是3,❗️a2和a3的维度是4。

2.3.不允许拷贝和赋值

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

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

一些编译器支持数组的赋值,这就是所谓的编译器扩展。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。

2.4.理解复杂的数组声明

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

默认情况下,类型修饰符从右向左依次绑定。对于ptrs来说,首先我们知道定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针。

但是如果存在括号,可以先解读括号里的内容,然后再从右向左依次绑定。例如Parray,首先它是一个指针,然后指向一个含有10个元素的数组,再继续往左解读,得知数组中的元素为int类型。

一个更加复杂的例子:

1
int* (&arry)[10]=ptrs;//arry是数组的引用,该数组含有10个指针

3.访问数组元素

与标准库类型vectorstring一样,数组的元素也能使用范围for语句或下标运算符来访问,数组的索引从0开始。

⚠️在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号数

4.指针和数组

‼️数组有一个特性:在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针:

1
2
3
4
5
6
7
//example1
string nums[]={"one","two","three"};
string *p=&nums[0];//p指向nums的第一个元素
string *p2=nums;//等价于p2=&nums[0]
//example2
int ia[]={0,1,2,3,4,5,6,7,8,9};//ia是一个含有10个整数的数组
auto ia2(ia);//ia2是一个整型指针,指向ia的第一个元素

⚠️decltype(ia)返回的类型是由10个整数构成的数组:

1
2
//ia3是一个含有10个整数的数组
decltype(ia) ia3={9,8,7,6,5,4,3,2,1,0};

4.1.指针也是迭代器

⚠️vectorstring迭代器支持的运算,数组的指针全都支持。

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

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

int *p=arr;获取了数组第一个元素的指针,那我们怎么获得数组尾元素的下一位置呢?此处我们可以利用数组的另一个特殊性质,设法获取数组尾元素之后的那个并不存在的元素的地址:

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

⚠️不能对尾后指针执行解引用或递增的操作。

利用上述知识输出arr的全部元素:

1
2
for(int *b=arr;b!=e;++b)
	cout<<*b<<endl;

4.2.标准库函数beginend

尽管能计算得到尾后指针,但是这种用法极易出错。为了让指针的使用更简单、更安全,C++11新标准引入了两个名为beginend的函数(类似于迭代器同名成员的功能)。不过数组毕竟不是类类型,因此这两个函数不是成员函数。正确的使用形式是将数组作为它们的参数:

1
2
3
int ia[]={0,1,2,3,4,5,6,7,8,9};
int* beg=begin(ia);//指向ia首元素的指针
int* last=end(ia);//指向ia尾元素的下一位置的指针

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

4.3.指针运算

指向数组元素的指针可以执行所有的迭代器运算。例如:

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

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

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

⚠️两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型。因为差值可能为负值,所以ptrdiff_t是一种带符号类型。

如果两个指针分别指向不相关的对象,则不能比较它们:

1
2
3
4
int i=0,sz=42;
int* p=&i,*e=&sz;
while (p<e) //错误
	......

👉上述指针运算同样适用于空指针和所指对象并非数组的指针。两个空指针彼此相减,结果为0。

4.4.下标和指针

对数组执行下标运算其实是对指向数组元素的指针执行下标运算:

1
int i=ia[2];

编译器内部的转换过程:

  1. ia转换成指向数组首元素的指针。
  2. 得到(ia+2)的指针。
  3. 解引用:*(ia+2),返回给i

同样的:

1
2
3
int *p=&ia[2];
int j=p[1];//p[1]等价于*(p+1),就是ia[3]表示的那个元素
int k=p[-2];//p[-2]是ia[0]表示的那个元素

⚠️标准库类型限定使用的下标必须是无符号类型(例如vectorstring),而内置的下标运算无此要求(例如数组)。