10. 数组和指针
一、数组
-
数组由数据类型相同的一系列元素组成。
-
需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。
-
普通变量可以使用的类型,数组元素都可以用。
-
方括号([])表明 candy、code 和 states 都是数组,方括号中的数字表明数组中的元素个数。
如,float candy[365]; // 内含 365 个 float 类型元素的数组
-
要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。
-
数组元素的编号从 0 开始,所以 candy[0] 表示 candy 数组的第 1 个元素,candy[364] 表示第 365 个元素,也就是最后一个元素。
1.1 初始化数组
-
只储存单个值的变量有时也称为标量变量。
-
如,int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};
用以逗号分隔的值列表(用花括号括起来)来初始化数组, 各值之间用逗号分隔。在逗号和值之间可以使用空格。根据上面的初始化, 把 1 赋给数组的首元素(powers[0]),以此类推推
-
当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0。
-
如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;如果部分初始化数组,剩余的元素就会被初始化为 0。
-
可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数。
-
sizeof days 是整个数组的大小,sizeof days[0] 是数组中一个元素的大小(以字节为单位)。
-
整个数组的大小除以单个元素的大小就是数组元素的个数
-
使用 const 声明数组
- 用 const 声明和初始化数组的话,这个数组就是只读数组,程序在运行过程中不能修改数组中的内容。
- 和普通变量一样,应该使用声明来初始化 const 数据。
- 使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值。
- 译器使用的值是内存相应位置上的现有值,系统不同,输出的结果可能不同。
- 初始化列表中的项数应与数组的大小一致。
-
注意 存储类别警告
- 数组和其他变量类似,可以把数组创建成不同的存储类别。
- 不同的存储类别有不同的属性,所以不能把本章的内容推广到其他存储类别。
- 对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为0。
1.2 指定初始化器(C99)
-
C99 增加了一个新特性:指定初始化器,利用该特性可以初始化指定的数组元。
-
C99 规定,可以在初始化列表中使用带方括号的小标指明待初始化的元素。
如, int arr[6] = {[5] = 212}; // 把 arr[5] 初始化为 212
-
对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为 0。
#include <stdio.h> 输出:1 31 #define MONTHS 12 2 29 3 0 int main() 4 0 { 5 31 int days[MONTHS] = {31, 28, [4] = 31, 30, 31, [1] = 29}; 6 30 int i; 7 31 8 0 for (i = 0; i < MONTHS; i++) 9 0 printf("%2d %d\n", i + 1, days[i]); 10 0 11 0 return 0; 12 0 } 13 0 /*如果指定初始化器后面有更多的值,那么后面的这些值将被用于初始化指定元素后面的元素 [4] = 31, 30, 31,在 days[4] 被初始化为 31 后,days[5] 和 days[6] 将分别被初始化为 30 和 31 如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化 -
如果没有指定元素的大小,编译器会把数组的大小设置为足够装得下初始化的值。
int stuff[] = {1, [6] = 23}; int staff[] = {1, [6] = 4, 9, 10}; //stuff数组 有7个元素,编号为0~6;而staff数组有9个元素)
1.3 给数组元素赋值
- 声明数组后,可以借助数组下标(或索引)给数组元素赋值。
- C 不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值。
1.4 数组边界
- 在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。
- 编译器不会检查数组下标是否使用得当。在C标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。
- 如果编译器在运行时添加额外代码检查数组的每个下标值,这会降低程序的运行速度。
- 数组元素的编号从 0 开始,最好是在声明数组时使用符号常量来表示数组的大小。
1.5 指定数组的大小
- 在 C99 标准之前,声明数组时只能在方括号中使用整型常量表达式。
- 整型常量表达式,是由整型常量构成的表达式。
- sizeof 表达式被视为整型常量,但是 const 值不是(与 C++ 不同)。
- 表达式的值必须大于 0。
int n = 5;
int m = 8;
float a1[5]; // 可以
float a2[5*2 + 1]; //可以
float a3[sizeof(int) + 1]; //可以
float a4[-4]; // 不可以,数组大小必须大于0
float a5[0]; // 不可以,数组大小必须大于0
float a6[2.5]; // 不可以,数组大小必须是整数
float a7[(int)2.5]; // 可以,已被强制转换为整型常量
float a8[n]; // C99之前不允许
float a9[m]; // C99之前不允许
- C99 标准允许后两种的声明,这创建了一种新型数组,称为变长数组或简称 VLA(C11 把 VLA 设定为可选,而不是语言必备的特性)。
- 声明 VLA 时不能进行初始化。
二、多维数组
/* rain.c -- 计算每年的总降水量、年平均降水量和5年中每月的平均降水量 */
#include <stdio.h> 660
#define MONTHS 12 // 一年的月份数
#define YEARS 5 // 年数
int main(void) {// 用2010~2014年的降水量数据初始化数组
const float rain[YEARS][MONTHS] = {
{ 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 },};
int year, month;
float subtot, total;
printf(" YEAR RAINFALL (inches)\n");
for (year = 0, total = 0; year < YEARS; year++) { // 每一年,各月的降水量总和
for (month = 0, subtot = 0; month < MONTHS; month++)
subtot += rain[year][month];
printf("%5d %15.1f\n", 2010 + year, subtot);
total += subtot; // 5年的总降水量
}printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
printf("MONTHLY AVERAGES:\n\n");
printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec\n");
for (month = 0; month < MONTHS; month++) { // 每个月,5年的总降水量
for (year = 0, subtot = 0; year < YEARS; year++)
subtot += rain[year][month];
printf("%4.1f ", subtot / YEARS);
}printf("\n");
return 0; }
- 程序使用了两个嵌套for循环。第1个嵌套for循环的内层循环,在year不 变的情况下,遍历month计算某年的总降水量;而外层循环,改变year的 值,重复遍历month,计算5年的总降水量。这种嵌套循环结构常用于处理二 维数组,一个循环处理数组的第1个下标,另一个循环处理数组的第2个下标。
- 第2个嵌套for循环和第1个的结构相同,但是内层循环遍历year,外层循 环遍历month。记住,每执行一次外层循环,就完整遍历一次内层循环。因 此,在改变月份之前,先遍历完年,得到某月 5 年间的平均降水量,以此类推
2.1 初始化二维数组
-
初始化二维数组是建立在初始化一维数组的基础上。
-
初始化二维数组 dates(第6行-第11行)
-
这个初始化使用了 5 个数值列表,每个数值列表都用花括号括起来。
-
如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是并不会影响其他行的初始化。
-
初始化时可以省略内部的花括号,只保留最外面的一堆花括号,要保证初始化的数值个数正确。
-
如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值,后面没有值初始化的元素被统一初始化为 0。
-
2.2 其他多维数组
-
声明一个三维数组:int nums[10][20][30];
nums 内含 10 个元素,每个元素是内含 20 个元素的数组,这 20 个数组元素中的每个元素是内含 30 个元素的数组。
-
处理三维数组要使用 3 重嵌套循环,处理四维数组要使用 4 重嵌套循环。对于其他多维数组,以此类推。
三、指针介绍(原9.7)
-
指针是一个值为内存地址 的变量(或数据对象)。
-
指针变量的值是地址,正如char 类型变量的值是字符,int 类型变量的值是整数。
-
要创建指针变量,先要声明指针变量的类型。
-
假设一个指针变量名是 ptr,
对:ptr = &pooh; //把 pooh 的地址赋给 ptr。
对于这条语句,我们说 ptr “指向” poohptr 和 &pooh 的区别是 ptr 是变量,而 &pooh 是常量。
3.1 间接运算符:*
-
地址运算符:&
-
一般注解:后跟一个变量名时,&给出该变量的地址。
-
示例:&nurse 表示变量 nurse 的地址
-
-
地址运算符:*****
-
一般注解: 后跟一个指针名或地址时,*给出储存在指针指向地址上的值。
-
注意:不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。
-
示例:
int pooh = 24; int * ptr; // 声明一个指针变量 ptr int val; ptr = &pooh; // 把 pooh 的地址赋给 ptr val = * ptr; // 把 ptr 指向的值赋给变量 val printf("%d\n", val); //输出:24 //语句 ptr = &pooh; 和 val = * ptr; 放在一起相当于语句:val = pooh -
3.2 声明指针
-
声明指针必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。
-
程序必须知道储存在指定地址上的数据类型。long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭。
int * pi; // pi 是指向 int 类型变量的指针 char * pc; // pc 是指向 char 类型变量的指针 float * pf, * pg; // pf、pg 都是指向 float 类型变量的指针 -
类型说明符表明了指针所指向对象的类型,星号()表明声明的变量是一个指针。
如:int * pi;,声明的意思是 pi 是一个指针,pi 是 int 类型 -
* 和指针名之间的空格可有可无。通常,在声明时使用空格,在解引用变量时省略空格。
-
指针实际上是一个新类型,不是整数类型。
3.3 使用指针在函数间通信
//程序清单9.15 swap3.c程序
//swap3.c -- 使用指针解决交换函数的问题
#include <stdio.h>
void interchange(int * u, int * v); //该函数传递的不是x和y的值,而是它们的地址
int main(void)
{
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x, y);
interchange(&x, &y); // 把地址发送给函数
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}
void interchange(int * u, int * v)
{
int temp;
temp = *u; // temp获得 u 所指向对象的值。u的值是&x,所以u指向x。
//不要写temp = u;,这条语句赋给temp的是x的地址,不是x的值。函数要交换的是x和y的值,不是地址。
*u = *v; //这条语句相当于: x = y;
*v = temp;
}
-
**void interchange(int , int );
-
一般而言,可以把变量相关的两类信息传递给函数。
-
第1种,function1(x); ,传递的是x的值。
-
第2种,function2(&x);,传递的是x的地址。
-
第1种形式要求函数定义中的形式参数必须是一个与x的类型相同的变量:int function1(int num)
第2种形式要求函数定义中的形式参数必须是一个指向正确类型的指 针:int function2(int * ptr)
-
如果要计算或处理值,那么使用第 1 种形式的函数调用;
如果要在被调函数中改变主调函数的变量,则使用第2种形式的函数调用。
-
-
变量:名称、地址和值
-
变量的名称、地址和变量的值之间关系密切。
-
编写程序时,可以认为变量有两个属性:名称和值。
计算机编译和加载程序后,认为变量也有两个属性:地址和值。
-
在许多语言中,地址都归计算机管,对程序员隐藏。然而在 C 中,可以通过&运算符访问地址,通过*运算符获得地址上的值。
-
简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的 派生量,而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。
-
四、指针和数组
-
数组表示法其实是在变相地使用指针。
-
数组名是数组首元素的地址。即,如果 flizny 是一个数组,下面的语句成立:
flizny == &flizny[0]; // 数组名是该数组首元素的地址
flizny 和 &flizny[0] 都表示数组首元素的内存地址(&是地址运算符)。 两者都是常量,在程序的运行过程中不会改变。但是,可以把它们赋值给指针变量,然后可以修改指针变量的值。
//程序清单10.8 pnt_add.c程序
// pnt_add.c -- 指针地址
#include <stdio.h>
#define SIZE 4
int main(void)
{
short dates[SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;
pti = dates; // 把数组地址赋给指针
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (index = 0; index < SIZE; index++)
printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);
return 0;
}
/* 输出: short double
pointers + 0: 0x7fff5fbff8dc 0x7fff5fbff8a0
pointers + 1: 0x7fff5fbff8de 0x7fff5fbff8a8
pointers + 2: 0x7fff5fbff8e0 0x7fff5fbff8b0
pointers + 3: 0x7fff5fbff8e2 0x7fff5fbff8b8
- 在 C 中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址。
- 指针指向的是标量变量,也要知道变量的类型,否则 *pti 就无法正确地取回地址上的值。
- 指针的值是它所指向对象的地址。地址的表达方式依赖于计算机内部的硬件。许多计算机都是按字节编址,意思是内存中的每个字节都按顺序编号。一个较大对象的地址通常是该对象第一个字节的地址。
- 在指针前面使用 * 运算符可以得到该指针所指向对象的值。
- 指针加 1,指针的值递增它所指向类型的大小(以字节为单位)

- 以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C 语言标准在描述数组表示法时确实借助了指针。
- 定义 ar[n] 的意思是 (ar + n),(ar + n) 的意思是“到内存的 ar 位置,然后移动 n 个单元”
- 间接运算符()的优先级高于 +,所以 days+2 相当于 (days) + 2,(days + 2) 是 days 第 3 个元素的值,*days + 2 是 days 第 1 个元素的值加 2
//程序清单10.9 day_mon3.c程序
// day_mon3.c -- uses pointer notation
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int index;
for (index = 0; index < MONTHS; index++)
printf("Month %2d has %d days.\n", index + 1, *(days + index));
//与 days[index]相同
return 0;
}
/* 输出:Month 1 has 31 days.
Month 2 has 28 days.
Month 3 has 31 days.
......
Month 11 has 30 days.
Month 12 has 31 days.
这里,days是数组首元素的地址,days + index是元素days[index]的地址,而*(days + index)则是该元素的值,相当于days[index]。
for循环依次引用数组中的每个元素,并打印各元素的内容。

浙公网安备 33010602011771号