本章内容简介
关键字: static
运算符: &, *(一元运算符)
如何创建并且初始化数组
指针(在已经学过的基础上), 指针和数组
编写处理数组的函数
二维数组
10.1数组
亮点
月份是用define定义的,所以直接到声明处修改值就可以实现多处同时修改
/*day_mon1.c* -- 打印每个月的天数*/// 20210320 21:20#include <stdio.h>#define MONTHS 12int 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 and %2d days.\n", index + 1, days[index]);return 0;}
未初始化数组的后果
编译器自动初始化数组,全部为CC,所以内存地址就在一个地方了;
这里显示的是内存中已有的四个数字的内存变量,由于被编译器初始化,所以值完全相等;
/*no_data.c -- 为初始化数组*///20210321 08:18#include <stdio.h>#define SIZE 4int main(void){int no_data[SIZE];int i;printf("%2s%14s\n", "i", "no_data[i]");for (i = 0; i < SIZE; i++)printf("%2d%14d\n", i, no_data[i]);return 0;}

部分初始化数组
编译器会自动给未初始化的数组赋值,赋值为零
/*somedata.c -- 部分初始化数组*///20210321 08:33#include <stdio.h>#define SIZE 4int main(void){int some_data[SIZE] = { 1492, 1066 };int i;printf("%2s%14s\n", "i", "some_data[i]");for (i = 0; i < SIZE; i++)printf("%2d%14d\n", i, some_data[i]);return 0;}

正确声明数组
使用const关键字,锁定数组只读;
方括号数组不填写,数组自动匹配元素个数;
sizeof统计整个数组的大小,除以单个元素即可求得数组元素的个数;
/*day_mon.c -- *///20210321 08:48#include <stdio.h>int main(void){const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int index;for (index = 0; index < sizeof days / sizeof days[0]; index++)printf("Month %2d has %d days.\n", index + 1, days[index]);return 0;}
10.1.2 指定初始化器
用数组元素占位初始化容易造成逻辑混乱,还是按照顺序写比较好,临时添加让后面看到的人很难过,还得自己推算值和索引位置的对应;
//designate.c -- 使用指定初始化器//20210321 09:01#include <stdio.h>#define MONTHS 12int main(void){int days[MONTHS] = { 31, 28,[4] = 31, 30, 31,[1] = 29 };int i;for (i = 0; i < MONTHS; i++)printf("%2d %d\n", i + 1, days[i]);return 0;}

10.1.3 给数组元素赋值
//数组不允许直接赋值的方式初始化数组;int oxen[5];int yank[5];xen = yank;//下标不能越界int oxen[5];int yank[4];oxen[5] = yank[4];//长度与初始化约束值必须一致oxen[5] = {1,2,3,4};
10.1.4数组越界
数组的长度以声明变量为准;
数组越界就是调用对象的标数超过了数组本身的索引值;
//bounds.c -- 数组下标越界//20210321 09:38#include <stdio.h>#define SIZE 4int main(void){int value1 = 44;int arr[SIZE];int value2 = 88;int i;printf("value1 = %d value2 = %d\n", value1, value2);for (i = -1; i <= SIZE; i++)arr[i] = 2 * i + 1;for (i = -1; i < 7; i++) // 数组越界printf("%2d %d\n", i, arr[i]);printf("value1 = %d value2 = %d\n", value1, value2);printf("address of arr[-1]: %p\n", &arr[-1]);printf("address of arr[4]: %p\n", &arr[4]);printf("address of value1: %p\n", &value1);printf("address of value2: %p\n", &value2);return 0;}

10.1.5 指定数组的大小
10.2 多维数组
循环遍历完成内层一维和二维数组的初始化;
多维数组书写,可以不必嵌套花括号,但在元素不够的情况下,又没有使用多层花括号,那么就会自动按照先后顺序填空然后初始化,这样数据就有可能前后移动,造成失真;
所以还是要“不辞辛劳”,用花括号标明层级;
/*rain.c -- 计算每年的总降水量,五年中每个月的平均降水量*/// 20210321 11:20#include <stdio.h>#define MONTHS 12#define YEARS 5int main(void){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++) //控制月份的循环,至多12次subtot += rain[year][month];printf("%5d %15.1f\n", 2010 + year, subtot); //年份加长,公元纪年total += subtot;}printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);printf("MONTHLY AVERAGE:\n\n");printf("Jan Feb Mar Apr May Jun Jul Aug Sep Oct");printf("Nov Dec\n");for (month = 0; month < MONTHS; month++) //外循环控制月数{for (year = 0, subtot = 0; year < YEARS; year++) //内循环控制年数subtot += rain[year][month]; //求累积printf("%4.1f", subtot / YEARS); // 字符宽度最大为四,精度为1}printf("\n");return 0;}

10.3指针和数组
数组名,就是数组首元素的地址;
指针加一是增加一个存储单元,因此加一后移动到下一个元素的地址吗,而不是下一个字节的地址;
这也是为什么,声明指针的时候需要声明指针指向存储对象的数据类型;
/*pnt_add.c -- 指针地址,通过修改指针地址来修改数组数组名(数组首元素地址)*///20210321 20:56#include <stdio.h>#define SIZE 4int 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;}

指针与数组等效
指针表示法和数组表示法等效;
days = days[0]; (days + index) = days[0 + index];
/*day_mon3.c -- use pointer notation*///20210321 21:16#include <stdio.h>#define MONTHS 12int 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;}
;
10.4函数,数组和指针
数组数据类型的函数声明,和之前一样,但多了一个类型声明,其次就是参数个数声明,在类型声明中,必须指明数组,可以不写数组名字,但是方括号一定要有;
还可以使用解引用运算符加数组名字声明数据类型;
参数的个数也可以不明确指定,但不能缺少类型声明;
int ar[]只能用于函数原型声明;
//sum_arr1.c -- 数组元素之和//20210322 07:28#include <stdio.h>#define SIZE 10int sum(int ar[], int n); //也可以用指针声明变量, int * arint main(void){int marables[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };long answer;answer = sum(marables, SIZE);printf("The total number of marables is %ld.\n", answer);printf("The size of marables is %zd bytes.\n", sizeof marables);return 0;}int sum(int ar[], int n){int i;int total = 0;for (i = 0; i < n; i++)total += ar[i];printf("The size of ar is %zd bytes.\n", sizeof ar);return total;}

10.4.1 使用指针形参
第一个程序
涉及到三个知识点,首先,数组地址的合法范围是最后一个元素位置的下一个位置,该地址有效但无法被访问(正好可以应对<的情况);
其次一元运算符之间存在优先级关系,但是可以通过括号避免(在计算较为简单的情况下),解引用地址运算符和自增减运算符同属于一个运算顺序,所以从右到左开始运算;
最后,容易出现逻辑错误的一点就是,累加首先是对该索引值指向的元素进行求和,然后移动指针,移动指针通过自增减运算符完成,所以首先是自增减,然后再解引用地址运算;

/*order.c--指针运算中的优先级*/#include <stdio.h>#include <stdlib.h>#define SIZE 10int sump(int *start, int *end); // 函数原型ANSI : 指针标记数组的开始和结束int main(void){int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26,31,20 }; // 声明一个名为marbles的数组,数组长度为10,元素的数据类型为整型;long answer; // 声明一个长型变量answer = sump(marbles, marbles + SIZE); // 调用子函数,给函数传递两个指针参数,第一个参数是数组,第二个参数是数组的长度加上一个数字printf("The total numbles of marbles is %ld.\n", answer);return 0;}/*使用指针算法*/int sump(int* start, int* end){int total = 0; // 初始化变量while (start < end) // 指针默认可以移动到数组最后一个元素的下一个位置,但是不保证该位置上元素的值,且不能被访问,可以看作是一个形式上的变量{total += *start; // 解引用地址运算符,指向该变量地址所存储的值start++; // 对指针索引值进行增减,也就是对指向元素的指针向后移动// 此处的代码也可以写成 totoal += *(start++);先对指针进行运算,再解引用地址;同是一元运算符,运算顺序从右到左,遵循结合律}return total;}
第二个程序
主要考察自增减运算符;运算符的优先级;解引用地址运算符;
逻辑解释附在代码里;
/*order.c -- 指针运算中的优先级*//*20210428 -- 18:17*/#include <stdio.h>#include <stdlib.h>int data[2] = { 100, 200 };int moredata[2] = { 300, 400 };int main(void){int* p1, * p2, * p3;p1 = p2 = data;p3 = moredata;printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);return 0;}/*p1和p3类似,都是运算符在指针变量的右边,所以他们仅仅是赋值,解引用地址是在索引值对象为数组首元素开始的,其中p1指针就是当前元素的值:100,但是运算符对原数据进行修改,也就是数组指针指向下一个元素,再解引用,原数组的值最后就是200;p3指针的自增减运算是300,因为先是原地解引用,然后对原位置的元素增加1;p2是指针向下一个元素移动一个位置,也是原地赋值,最后再解引用,所以值是200*//*指针将会打印两遍,用于检查值是否被修改;自增减运算符在左边,可以理解左倾冒进,先运算后赋值;自增减运算符在右边,可以理解为右倾保守,先赋值,再运算,赋值是将值传递给一个新变量,运算是针对原变量,所以值与运算分离*/

10.5 指针操作
/*ptr_ops.c -- 用于展示指针操作*//*20210429 1419*/#include <stdio.h>#include <stdlib.h>int urn[5] = { 100, 200, 300, 400, 500 }; // 全局变量int test(void);int test(void){int a = 100;printf("100的地址是%p, 变量a的地址是%d.\n",&urn[0], &a);return 100;}int main(void){int* ptr1, * ptr2, * ptr3;ptr1 = urn; /*把数组赋值给一个指针*/ptr2 = &urn[2]; /*将一个数组元素赋值给一个指针*/printf("pointer value, dereferenced pointer, address:\n");printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p", ptr1, *ptr1, &ptr1); /*指针加法*/ptr3 = ptr1 + 4;printf("\nAdding an int to a pointer:\n");printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));printf("%p\n", urn[4]);ptr1++;printf("\nvalues after ptr1++:\n");printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p", ptr1, *ptr1, &ptr1); /*递增指针*/ptr2--; /*递减指针*/printf("\nvalues after --ptr2:\n");printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p", ptr2, *ptr2, &ptr2);--ptr1; /*恢复为初始值*/++ptr2; /*恢复为初始值*/printf("\nPointers reset to original values:\n"); /*用一个指针减去另外一个指针*/printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);printf("\nsubtracting one pointer from another:\n");printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);printf("n\subtracting an int from a pointer:\n");printf("ptr3 = %p, ptr3 -2 = %p\n", ptr3, ptr3 - 2);test();return 0;}
10.5 指针操作
完整代码
/*ptr_ops.c -- 指针操作*//*20210501 1250*/#include <stdio.h>#include <stdlib.h>int main(void){int urn[5] = {100, 200, 300, 400, 500};int *ptr1, *ptr2, *ptr3;ptr1 = urn;ptr2 = &urn[2];printf("\n pointer value, dereferenced pointer, pointer address:\n");printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);/*指针加法*/ptr3 = ptr1 + 4;printf("\n Adding an int to a pointer:\n");printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));ptr1++;printf("\n values after ptr1++:\n");printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);ptr2--;printf("\n values after --ptr2:\n");printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);--ptr1;++ptr2; /*恢复为初始值*/printf("\n Pointers reset to original values:\n");printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);/*一个指针减去另外一个指针*/printf("\n subtracting one pointer from another:\n");printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);/*一个指针减去一个整数*/printf("\n subtracting an int from a pointer:\n");printf("ptr3 = %p,ptr3 - 2 = %p\n", ptr3, ptr3 - 2);return 0;}
代码运行结果

从C代码中可以看出,ptr1指针指向的是数组的首元素,所以ptr1经过取地址运算输出的结果,应该是指针本身的地址,而直接将指针变量以指针的格式打印出来,得到的值应该是指向数组首元素的地址

对指针的加法运算原理: 加法运算中的整数乘以字节数(指针所指向类型),然后将乘积与原地址相加,特别注意:指针运算的取值范围,就是数组的表示范围,因为该指针赋予的值就是数组首元素的值,所以对其进行加法运算,运算的结果应该在数组的表示范围以内,也就是指针所代表首元素的索引值和该整数相加,和应该仍然在数组元素的数字范围之内,否则结果未知

指针的自增加运算:实际上是将指针移动一个位置,从数组的一个元素移向下一个元素,一个位置的移动,相当于地址上四个字节的移动,四个字节是一个int类型数值占用的字节空间
指针与整数的加法,指针与整数的减法,原理一致,都需要保证取值的合法性
指针与指针的减法:如果同在一个数组,那么他们的结果是相差多少个元素位置,站在地址的角度,差还需要乘以字节数,该字节数取决于数组的数据类型
如果不在同一个数组,那么可能出结果,也可能直接出错
运算符中的比较:可以用运算符进行比较运算,但前提是指针均指向同一个数组
编译器特性:指针的越界检查,仅仅局限在索引值的合理取值范围(数组的最大长度)和指向指针最后一个元素的后一个元素的位置,除此之外,没有保证,所以是否越界,绝大多数情况取决于程序员的控制;数组末尾后一个元素的位置,仍然可以被解引用运算
10.6.1 保护数组中的数据
为了主函数的值不被子函数改变,或者说为了程序在不改变原数组元素值的情况下运行,可以在函数原型声明和函数中传入参数增加关键字const
但需要注意的是,如果是对每一个原数组中的元素进行运算,则该操作不成立,他与可计算左值互相矛盾
10.6.1 对形式参数使用const
代码
/*arf.c -- *//*20210501 17:06*/#include <stdio.h>#include <stdlib.h>#define SIZE 5/*特别注意:用于打印数组元素的子函数,它的参数是视为常量的双精度浮点数*/void show_array(const double ar[], int n); /*两个形式参数,分别限定了数组的数据类型,以及展示多少个数组元素*/void mult_array(double ar[], int n, double mult); /*三个形式参数,分别限定数组数据类型,多少个元素参与运算,以及乘数是多少*/int main(void){double dip[SIZE] = {20.0, 17.66, 8.2, 15.3, 22.22};printf("The original dip array:\n");show_array(dip, SIZE);mult_array(dip, SIZE, 2.5);printf("The dip arrary after calling mult_array():\n");show_array(dip, SIZE);return 0;}/*显示数组的内容*/void show_array(const double ar[], int n){int i;for (i = 0; i < n; i ++){printf("%8.3f ", ar[i]); /*字符的格式为最大八个字符宽度,保留小数点后三位*/putchar('\n'); /*用内置方法对输出换行*/}}/*把数组的每一个元素都乘以相同的值*/void mult_array(double ar[], int n, double mult){int i;for (i = 0; i < n; i++){ar[i] *= mult;}}
原因
对于展示数组元素的子函数而言,仅仅需要打印元素的值,而不需要对数组元素修改。但为了避免被误修改,这里上了双保险,对数据类型加了关键字const修饰,编译器在运行过程中将其视为常量;
