数组

来源:互联网 发布:3306端口攻击 编辑:程序博客网 时间:2024/06/11 22:24

一、显示初始化数组元素:

数组的声明形如a[d],其中a是数组的名字,d是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;相反,如果指明了维度,那么初始值的总数量不应该超出指定的大小。如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值。

const unsigned sz = 3;  int ia1[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的定义是错误的。尽管字符串字面值”Daniel”看起来只有6个字符,但是数组的大小必须至少是7,其中6个位置存放字面值的内容,另外1个存放结尾处的空字符。

三、不允许拷贝和赋值

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

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

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

四、理解复杂的数组声明

和vector一样,数组能存放大多数类型的对象。例如,可以定义一个存放指针的数组。又因为数组本身就是对象,所以允许定义数组的指针及数组的引用。在这几种情况中,定义存放指针的数组比较简单和直接,但是定义数组的指针或数组的引用就稍微复杂一点了:

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

默认情况下,类型修饰符从右向左依次绑定。对于ptrs来说,从右向左(参见2.3.3节,第58页)理解其含义比较简单:首先知道我们定义的是一个大小为10的数组,它的名字是ptrs,然后知道数组中存放的是指向int的指针。

但是对于Parray来说,从右向左理解就不太合理了。因为数组的维度是紧跟着被声明的名字的,所以就数组而言,由内向外阅读要比从右向左好多了。由内向外的顺序可帮助我们更好地理解Parray的含义:首先是圆括号括起来的部分,*Parray意味着Parray是个指针,接下来观察右边,可知道Parray是个指向大小为10的数组的指针,最后观察左边,知道数组中的元素是int。这样最终的含义就明白无误了,Parray是一个指针,它指向一个int数组,数组中包含10个元素。同理,(&arrRef)表示arrRef是一个引用,它引用的对象是一个大小为10的数组,数组中元素的类型是int。

五、访问数组元素

在使用数组下标的时候,通常将其定义为size_t类型。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef头文件中定义了size_t类型,这个文件是C标准库stddef.h头文件的C++语言版本。
与vector和string一样,当需要遍历数组的所有元素时,最好的办法也是使用范围for语句。例如,下面的程序输出所有的scores:

for (auto i : scores)   // 对于scores中的每个计数值        cout << i << " ";         // 输出当前的计数值  cout << endl; 

六、指针和数组

通常情况下,使用取地址符来获取指向某个对象的指针,取地址符可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能得到指向该元素的指针:

string nums[] = {"one", "two", "three"}; //数组的元素是string对象  string *p = &nums[0];                 // p指向nums的第一个元素 

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

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

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

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

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

尽管ia是由10个整数构成的数组,但当使用ia作为初始值时,编译器实际执行的初始化过程类似于下面的形式:

auto ia2(&ia[0]); //显然ia2的类型是int* 

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

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

标准库函数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;

注:一个指针如果指向了某种内置类型数组的尾元素的”下一位置”,则其具备与vector的end函数返回的与迭代器类似的功能。特别要注意,尾后指针不能执行解引用和递增操作。

指向数组元素的指针可以执行所有迭代器运算。这些运算,包括解引用、递增、比较、与整数相加、两个指针相减等,用在指针和用在迭代器上意义完全一致。两个指针相减的结果的类型是一种名为ptrdiff_t的标准库类型,和size_t一样,ptrdiff_t也是一种定义在cstddef头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t是一种带符号类型。

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

int ia[] = {0,2,4,6,8}; // 含有5个整数的数组

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

int i = ia[2];      // ia转换成指向数组首元素的指针                      // ia[2]得到(ia + 2) 所指的元素  int *p = ia;        // p指向ia的首元素  i = *(p + 2);       // 等价于i = ia[2] 

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

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

虽然标准库类型string和vector也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,而内置的下标运算无此要求,上面的最后一个例子很好地说明了这一点。内置的下标运算符可以处理负值,当然,结果地址必须指向原来的指针所指同一数组中的元素(或是同一数组尾元素的下一位置)。

内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。

0 0
原创粉丝点击