10.1 数组
数组(array)由一系列类型相同的元素构成。数组声明(array declaration)中包括数组元素的数目和元素的类型。如:
int month[12]; /* 12个整数的数组 */
/* int 是数组中元素的类型;month 是数组名,也是数组首元素的地址;
方括号 [] 表示 month 是一个数组;方括号中的数字 12 指明了数组大小(所包含的元素数目) */
10.1.1 初始化
可以使用花括号括起来的一系列数值来初始化数组,数值之间用逗号隔开。
int month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
如果需要使用只读数组,建议在声明并初始化数组时,使用关键字 const。如:
const int month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
如果不初始化数组,数组元素存储的是无用的数值;如果部分初始化数组,未初始化的元素则被设置为零;如果初始化列表中项目个数大于数组大小,编译器将报错。
10.1.2 指定初始化项目(C99)
C99新增了=一种新特定:指定初始化项目(designated initializer)。该特性允许选择对某些元素进行初始化。如:
int month[12] = {31, 28, [4] = 31, 30, [1] = 29};/* 在初始化列表中使用带有方括号的元素下标可以指定某个特定的元素 */
该语句将31赋给 month[0],将28赋给 month[1],将31赋给 month[4],将30赋给 month[5],再将29赋给 month[1]。其他未初始化的元素被设置为0。若多次对一个元素进行初始化,则最后一次的有效,因次 month[1] 的数值最终为29。
10.1.3 为数组赋值
声明完数组后,可以借助数组的索引对数组元素进行赋值。注意:C 不支持把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表形式进行赋值(初始化的时候除外)。
/* 为数组赋值 */
#include <stdio.h>
#define SIZE 5 /* 可采用标识符常量代替数组大小 */
int main(void)
{
int toes[SIZE] = {1, 2, 3, 4, 5}; /* 这里是可以的 */
int year[SIZE];
year[0] = 5; /* 允许 */
year = toes; /* 不允许 */
year = {1,3,5,7,9}; /* 不起作用 */
}
10.4.1 数组边界
数组索引不能超出数组边界。数组计数是从0开始。数组大小为20的数组,其索引为0~19。
10.1.5 指定数组大小
C99标准引入了一种新数组:变长数组(variable-length array),简称 VLA。
10.2 多维数组
数组的数组,称之为二维数组。如:
float rain[5][12]; /* 5个由12个浮点数组成的数组的数组 */
/* rain 是一个包含5个元素的数组,并且每个元素都是包含12个 float 数值的数组 */
10.2.1 初始化二维数组
可以用逗号分隔的5个数值列表来初始化像 rain 这样的二维数组。初始化的时候也可以省略内部的花括号,只保留外部花括号。
float rain[5][12] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};
10.2.2 更多维数的数组
三维数组:
int box[10][20][30]; /* 数组box是由10个二维数组(每个二维数组都是20行,30列)堆放起来构成的立方体*/
一维数组是排成一排的数据;二维数组是放在一个平面的数据;三维数组把平面数据一层一层地垒起来。处理数组通常需要使用循环:处理一维数组使用一重循环;处理二维数组使用二重嵌套循环;处理三维数组使用三重嵌套循环……
10.3 指针和数组
- 数组名同时是数组首元素的地址。
- 指针的数值就是它所指的对象的地址。
- 在指针前运用取值运算符 * 就可以得到该指针所指的对象的数值。
- 对指针加1,等价于对指针的值加上它所指的对象的字节大小。
month == &month[0]; /* 相同的地址 */
*month == month[0]; /* 相同的值 */
month + 2 == &month[2]; /* 相同的地址 */
*(month + 2) == month[2]; /* 相同的值 */
从本质上说,对同一个对象有两种不同的符号表示方法。
10.4 函数、数组和指针
指针最基本的功能在于同函数交换信息,另一基本功能是用在处理数组的函数中。当函数的实际参数是一个数组名时,形式参量必须是与之相匹配的指针。在且仅在这种场合中, C 对 int ar[ ] 和 int *ar 做出同样解释,即 ar 是指向 int 的指针。因此,以下四种函数原型等价:
int sum(int *ar);
int sum(int *); /* 原型允许省略变量名*/
int sum(int ar[]);
int sum(int []); /* 原型允许省略变量名*/
以下两种函数定义等价:
int sum(int *ar)
{
//statement
}
int sum(int ar[])
{
//statement
}
处理数组的函数实际上是使用指针作为参数的,但在编写和处理数组的函数时,数组符号和指针符号都是可以选用的。C 保证在为数组分配存储空间的时候,指向数组之后的第一个位置的指针是合法的(month+12合法),但不能该指针进行取值运算。
10.5 指针操作
int *ptr;
int urn[5] = {1, 2, 3, 4, 5};
ptr1 = urn; /* 把数组 urn 首地址赋给指针 ptr1 */
ptr2 = &urn[3]; /* 把 urn[3] 的地址赋给指针 ptr2 */
- 赋值(assignment):可以把一个地址赋值给指针。通常使用数组名或地址运算符&来进行地址赋值。
- 取值(dereferencing):取值运算符 * 可以取出指针所指向地址中存储的数值。*ptr1 的结果为1.
- 取指针地址:使用运算符 & 可以得到存储指针本身的地址。
- 将一个整数加给指针:使用运算符 + 可以把一个整数加给一个指针,或把一个指针加给一个整数。这个整数会和指针所指类型的字节数相乘,然后将所得结果加到初始地址上。ptr1+4 的结果等同于&urn[4],*ptr1 的结果为5。
- 增加指针的值:可通过一般的加法或增量运算符来增加一个指针的值。ptr1++ 的结果等同于&urn[1],*ptr1 的结果为2。
- 从指针中减去一个整数:使用运算符 - 可以从指针中减去一个整数。指针必须是第一个操作数,或是一个指向整数的指针。这个整数会和指针所指类型的字节数相乘,然后将所得结果从初始地址中减去。ptr2-3 的结果等同于&urn[0],*ptr2 的结果为1。
- 减小指针的值:可通过一般的减法或减量运算符来减小一个指针的值。ptr2-- 的结果等同于&urn[2],*ptr2 的结果为3。
- 求差值(differencing):可以求出两个指针的差值。差值的单位是相应类型的大小。ptr2-ptr1的结果为3,表示指针所指向对象之间的距离是3个 int 数值的大小。有效指针差值运算的前提是参加运算的两个指针是指向同一数组(或其中之一是指向数组后的第一个地址)。
- 比较:可以使用关系运算符来比较两个指针的值,前提是两个指针具有相同的类型。
注意:严禁对未初始化的指针取值。
10.6 保护数组内容
10.6.1 对形式参量使用 const
若函数想修改数组,那么在声明数组参量时就不要使用 const;若函数不需要修改数组,那么在声明数组参量时最好使用 const。
int sum(const int ar[], int n); /* 原型 */
int sum(const int ar[], int n) /* 定义 */
{
int i;
int tatal = 0;
for(i = 0; i<n ; i++)
{
total += ar[i];
}
}
10.6.2 有关 const 的其他内容
- const 可以用来创建符号常量:
const int SIZE = 5;
- 在函数声明参量时使用 const,不仅可以保护数组(10.6.1程序),而且使函数可以使用声明为 const 的数组。
- 可以用 const 创建数组常量:
const int month[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* 如果随后的代码试图改变数组,编译器将报错 */
- 可以用 const 声明指向常量的指针:
int years[5] = {2015, 2016, 2017, 2018, 2019};
const int * pc = years; //pc声明为指向常量(const int)的指针
*pc = 2000; //不合法,指向常量的指针不能用于修改数据
pd++; //合法,可以让pd指向其他地址
rates[0] = 10; //合法,因为rates不是常量
/* 可以将常量或非常量数据的地址赋给指向常量的指针 */
int years[5] = {2015, 2016, 2017, 2018, 2019};
const int score[3] = {100, 85, 90};
const int * pc = years; //合法
pc = score; //合法
pc = &years[3]; //合法
/* 只有非常量数据的地址才可以赋给普通的指针 */
int years[5] = {2015, 2016, 2017, 2018, 2019};
const int score[3] = {100, 85, 90};
int * pnc = years; //合法
pnc = score; //不合法
pnc = &years[3]; //合法
- 可以用 const 声明常量指针:
int years[5] = {2015, 2016, 2017, 2018, 2019};
int * const pc = years; //pc指向数组首地址
pc = &years[2] //不合法,不能更改指针指向的地址
*pc = 2000; //合法,可以更改指针所指向地址的数据
- 可以用两个 const 来创建指向常量的常量指针,这个指针既不能更改所指向的地址,也不能修改所指向地址的数据:
int years[5] = {2015, 2016, 2017, 2018, 2019};
const int * const pc = years; //pc指向数组首地址
pc = &years[2] //不合法,不能更改所指向的地址
*pc = 2000; //不合法,不能更改所指向地址的数据
10.7 指针和多维数组
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} }; /* 二维数组有四个元素,每个元素是包含两个int的数组*/
zippo == &zippo[0]; /* 数组名同时是数组首元素的地址 */
*zippo == zippo[0]; /* 对zippo取值得到二维数组的首元素,该元素是一个一维数组 */
//
//
//
zippo[0] == &zippo[0][0]; /* zippo[0]本身是一维数组的数组名,同时也是其首元素zippo[0][0]的地址 */
*zippo[0] == zippo[0][0]; /* 对zippo[0]取值得到其首元素的数值 */
**zippo == zippo[0][0]; /* 对zippo进行两次取值即可得到通常的数值zippo[0][0] */
zippo++ == &zippo[1]; /* 指向该二维数组的下一个元素(一个包含两个int的数组) */
zippo[0]++ == &zippo[0][1];/* 指向该一维数组的下一个元素(一个int数值) */
10.7.1 指向多维数组的指针
int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} }; /* 二维数组有四个元素,每个元素是包含两个int的数组*/
int (* pz) [2]; /* pz指向一个包含两个int值的数组,即pz指向int[2]*/
pz = zippo; /* 将zippo首地址赋给指针pz */
zippo[m][n] == *(*(zippo + m) + n);
pz[m][n] == *(*(pz + m) + n); //合法
10.7.2 指针兼容性
- 指针之间的赋值规则比数值类型的赋值更严格。指针赋值的必要前提是:左值和右值都指向同一类型。
int *pt; // pt是指向int的指针
int (*pa) [3]; // pa是指向int[3]的指针
int **pz; // pz是指向int的指针的指针
int ar1[2][3];
int ar2[3][2];
pt = &ar1[0][0]; // 合法,都指向int
pt = ar1[0]; // 合法,都指向int
pa = ar1; // 合法,都指向int[3]
pa = ar2; // 不合法,ar2指向int[2]
pz = ar2; // 不合法,pz指向int的指针
pz = &pt; // 合法,都指向int的指针
*pz = pt; // 合法,都指向int
*pz = ar2[0]; // 合法,都指向int
- 把 const 指针赋给非 const 指针是错误的。把非 const 指针赋给 const 指针是允许的(前提是只进行一层间接运算)。
int * p1; // 非const指针
const int * p2; // const指针
const int ** pp2; // const指针
p1 = p2; // 不合法,把const指针赋给非const指针
p2 = p1; // 合法,把非const指针赋给const指针
pp2 = &p1; // 不合法,把非const指针赋给const指针,但进行了多次间接运算
10.7.3 函数和多维数组
如果要编写处理二维数组的函数,可以使用如下的函数声明:
int somefunction(int (*ar) [4]);
int somefunction(int ar[][4]); //当且仅当ar是函数的形式参量时合法
声明N维数组的指针时,可以参看如下的函数声明:
int somefunction(int (*ar) [4][5][6]); //ar是一个指针
int somefunction(int ar[][4][5][6]);
函数声明中,最左边的方括号表示这是一个指针,可以留空;而其他方括号描述的是所指对象的数据类型,不允许留空。
10.8 变长数组(VLA)
C99引入了变长数组,它允许使用变量定义数组各维。如:
int rows = 5;
int cols = 6;
double sales[rows][cols]; //一个变长数组
变长数组必须是自动存储类的,它们必须在函数内部或作为函数参量声明,且声明时不可以进行初始化。
如何声明一个带有二维变长数组参数的函数:
int sum(int rows, int cols, int ar[rows][cols]); //ar是一个变长数组,维数的声明必须先于数组
int sum(int, int, int ar[*][*]); //可以省略变量名,但需要用星号*来代替省略的维数
10.9 复合文字
C99新增了复合文字(compound literal)。文字是非符号常量。对于数组来说,复合文字看起来像是在数组的初始化列表前面加上用圆括号括起来的类型名。如:
int diva[2] = {10, 20};
(int [2]) {10, 20}; //一个复合文字,类型名就是 int[2]
初始化一个复合文字时也可以省略数组大小,如:
(int []) {100, 200, 300}; //有3个元素的复合文字
复合文字必须在创建的同时以某种方式使用它们,如使用指针保存其位置:
int * pt; // 一个指向int的指针
pt = (int [2]) {10, 20}; // 这个文字常量被标识为一个int数组,同时也代表首元素的地址,因此可以赋给一个指向int的指针
// 随后就可以使用这个指针。*pt值为10,pt[1]值为20
复合文字也可以作为实际参数被传递给带有类型与之匹配的形式参量的函数,如:
int sun(int ar[], int n); //函数声明
...
int total;
total = sum((int []) {1, 2, 3, 4, 5}, int 5); //第一个参数是包含5个元素的int数组,同时也是首元素地址