本章内容简介

关键字: static

运算符: &, *(一元运算符)

如何创建并且初始化数组

指针(在已经学过的基础上), 指针和数组

编写处理数组的函数

二维数组

10.1数组

亮点

月份是用define定义的,所以直接到声明处修改值就可以实现多处同时修改

  1. /*day_mon1.c* -- 打印每个月的天数*/
  2. // 20210320 21:20
  3. #include <stdio.h>
  4. #define MONTHS 12
  5. int main(void)
  6. {
  7. int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  8. int index;
  9. for (index = 0; index < MONTHS; index++)
  10. printf("Month %2d and %2d days.\n", index + 1, days[index]);
  11. return 0;
  12. }

image.png

未初始化数组的后果

编译器自动初始化数组,全部为CC,所以内存地址就在一个地方了;

这里显示的是内存中已有的四个数字的内存变量,由于被编译器初始化,所以值完全相等;

  1. /*no_data.c -- 为初始化数组*/
  2. //20210321 08:18
  3. #include <stdio.h>
  4. #define SIZE 4
  5. int main(void)
  6. {
  7. int no_data[SIZE];
  8. int i;
  9. printf("%2s%14s\n", "i", "no_data[i]");
  10. for (i = 0; i < SIZE; i++)
  11. printf("%2d%14d\n", i, no_data[i]);
  12. return 0;
  13. }

image.png
部分初始化数组

编译器会自动给未初始化的数组赋值,赋值为零

  1. /*somedata.c -- 部分初始化数组*/
  2. //20210321 08:33
  3. #include <stdio.h>
  4. #define SIZE 4
  5. int main(void)
  6. {
  7. int some_data[SIZE] = { 1492, 1066 };
  8. int i;
  9. printf("%2s%14s\n", "i", "some_data[i]");
  10. for (i = 0; i < SIZE; i++)
  11. printf("%2d%14d\n", i, some_data[i]);
  12. return 0;
  13. }

image.png

正确声明数组

使用const关键字,锁定数组只读;

方括号数组不填写,数组自动匹配元素个数;

sizeof统计整个数组的大小,除以单个元素即可求得数组元素的个数;

  1. /*day_mon.c -- */
  2. //20210321 08:48
  3. #include <stdio.h>
  4. int main(void)
  5. {
  6. const int days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  7. int index;
  8. for (index = 0; index < sizeof days / sizeof days[0]; index++)
  9. printf("Month %2d has %d days.\n", index + 1, days[index]);
  10. return 0;
  11. }

image.png

10.1.2 指定初始化器

用数组元素占位初始化容易造成逻辑混乱,还是按照顺序写比较好,临时添加让后面看到的人很难过,还得自己推算值和索引位置的对应;

  1. //designate.c -- 使用指定初始化器
  2. //20210321 09:01
  3. #include <stdio.h>
  4. #define MONTHS 12
  5. int main(void)
  6. {
  7. int days[MONTHS] = { 31, 28,[4] = 31, 30, 31,[1] = 29 };
  8. int i;
  9. for (i = 0; i < MONTHS; i++)
  10. printf("%2d %d\n", i + 1, days[i]);
  11. return 0;
  12. }

image.png

10.1.3 给数组元素赋值

  1. //数组不允许直接赋值的方式初始化数组;
  2. int oxen[5];
  3. int yank[5];
  4. xen = yank;
  5. //下标不能越界
  6. int oxen[5];
  7. int yank[4];
  8. oxen[5] = yank[4];
  9. //长度与初始化约束值必须一致
  10. oxen[5] = {1,2,3,4};

10.1.4数组越界

数组的长度以声明变量为准;

数组越界就是调用对象的标数超过了数组本身的索引值;

  1. //bounds.c -- 数组下标越界
  2. //20210321 09:38
  3. #include <stdio.h>
  4. #define SIZE 4
  5. int main(void)
  6. {
  7. int value1 = 44;
  8. int arr[SIZE];
  9. int value2 = 88;
  10. int i;
  11. printf("value1 = %d value2 = %d\n", value1, value2);
  12. for (i = -1; i <= SIZE; i++)
  13. arr[i] = 2 * i + 1;
  14. for (i = -1; i < 7; i++) // 数组越界
  15. printf("%2d %d\n", i, arr[i]);
  16. printf("value1 = %d value2 = %d\n", value1, value2);
  17. printf("address of arr[-1]: %p\n", &arr[-1]);
  18. printf("address of arr[4]: %p\n", &arr[4]);
  19. printf("address of value1: %p\n", &value1);
  20. printf("address of value2: %p\n", &value2);
  21. return 0;
  22. }

image.png
10.1.5 指定数组的大小
image.png

10.2 多维数组

循环遍历完成内层一维和二维数组的初始化;

多维数组书写,可以不必嵌套花括号,但在元素不够的情况下,又没有使用多层花括号,那么就会自动按照先后顺序填空然后初始化,这样数据就有可能前后移动,造成失真;

所以还是要“不辞辛劳”,用花括号标明层级;

  1. /*rain.c -- 计算每年的总降水量,五年中每个月的平均降水量*/
  2. // 20210321 11:20
  3. #include <stdio.h>
  4. #define MONTHS 12
  5. #define YEARS 5
  6. int main(void)
  7. {
  8. const float rain[YEARS][MONTHS] = {
  9. {4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
  10. {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
  11. {9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
  12. {7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
  13. {7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
  14. };
  15. int year, month;
  16. float subtot, total;
  17. printf("YEAR RAINFALL (inches)\n");
  18. for (year = 0, total = 0; year < YEARS; year++) // 控制年数的循环,至多五次
  19. {
  20. for (month = 0, subtot = 0; month < MONTHS; month++) //控制月份的循环,至多12次
  21. subtot += rain[year][month];
  22. printf("%5d %15.1f\n", 2010 + year, subtot); //年份加长,公元纪年
  23. total += subtot;
  24. }
  25. printf("\nThe yearly average is %.1f inches.\n\n", total / YEARS);
  26. printf("MONTHLY AVERAGE:\n\n");
  27. printf("Jan Feb Mar Apr May Jun Jul Aug Sep Oct");
  28. printf("Nov Dec\n");
  29. for (month = 0; month < MONTHS; month++) //外循环控制月数
  30. {
  31. for (year = 0, subtot = 0; year < YEARS; year++) //内循环控制年数
  32. subtot += rain[year][month]; //求累积
  33. printf("%4.1f", subtot / YEARS); // 字符宽度最大为四,精度为1
  34. }
  35. printf("\n");
  36. return 0;
  37. }

image.png

10.3指针和数组

数组名,就是数组首元素的地址;

指针加一是增加一个存储单元,因此加一后移动到下一个元素的地址吗,而不是下一个字节的地址;

这也是为什么,声明指针的时候需要声明指针指向存储对象的数据类型;

  1. /*pnt_add.c -- 指针地址,通过修改指针地址来修改数组数组名(数组首元素地址)*/
  2. //20210321 20:56
  3. #include <stdio.h>
  4. #define SIZE 4
  5. int main(void)
  6. {
  7. short dates[SIZE];
  8. short* pti;
  9. short index;
  10. double bills[SIZE];
  11. double* ptf;
  12. pti = dates;
  13. ptf = bills;
  14. printf("%23s %15s\n", "short", "double");
  15. for (index = 0; index < SIZE; index++)
  16. printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);
  17. return 0;
  18. }

image.png

指针与数组等效

指针表示法和数组表示法等效;

days = days[0]; (days + index) = days[0 + index];

  1. /*day_mon3.c -- use pointer notation*/
  2. //20210321 21:16
  3. #include <stdio.h>
  4. #define MONTHS 12
  5. int main(void)
  6. {
  7. int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  8. int index;
  9. for (index = 0; index < MONTHS; index++)
  10. printf("Month %2d has %d days.\n", index + 1, *(days + index)); // 与days[index]相同
  11. return 0;
  12. }

;image.png

10.4函数,数组和指针

数组数据类型的函数声明,和之前一样,但多了一个类型声明,其次就是参数个数声明,在类型声明中,必须指明数组,可以不写数组名字,但是方括号一定要有;

还可以使用解引用运算符加数组名字声明数据类型;

参数的个数也可以不明确指定,但不能缺少类型声明;

int ar[]只能用于函数原型声明;

  1. //sum_arr1.c -- 数组元素之和
  2. //20210322 07:28
  3. #include <stdio.h>
  4. #define SIZE 10
  5. int sum(int ar[], int n); //也可以用指针声明变量, int * ar
  6. int main(void)
  7. {
  8. int marables[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };
  9. long answer;
  10. answer = sum(marables, SIZE);
  11. printf("The total number of marables is %ld.\n", answer);
  12. printf("The size of marables is %zd bytes.\n", sizeof marables);
  13. return 0;
  14. }
  15. int sum(int ar[], int n)
  16. {
  17. int i;
  18. int total = 0;
  19. for (i = 0; i < n; i++)
  20. total += ar[i];
  21. printf("The size of ar is %zd bytes.\n", sizeof ar);
  22. return total;
  23. }

image.png

10.4.1 使用指针形参

第一个程序

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

image.png

  1. /*order.c--指针运算中的优先级*/
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #define SIZE 10
  5. int sump(int *start, int *end); // 函数原型ANSI : 指针标记数组的开始和结束
  6. int main(void)
  7. {
  8. int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26,31,20 }; // 声明一个名为marbles的数组,数组长度为10,元素的数据类型为整型;
  9. long answer; // 声明一个长型变量
  10. answer = sump(marbles, marbles + SIZE); // 调用子函数,给函数传递两个指针参数,第一个参数是数组,第二个参数是数组的长度加上一个数字
  11. printf("The total numbles of marbles is %ld.\n", answer);
  12. return 0;
  13. }
  14. /*使用指针算法*/
  15. int sump(int* start, int* end)
  16. {
  17. int total = 0; // 初始化变量
  18. while (start < end) // 指针默认可以移动到数组最后一个元素的下一个位置,但是不保证该位置上元素的值,且不能被访问,可以看作是一个形式上的变量
  19. {
  20. total += *start; // 解引用地址运算符,指向该变量地址所存储的值
  21. start++; // 对指针索引值进行增减,也就是对指向元素的指针向后移动
  22. // 此处的代码也可以写成 totoal += *(start++);先对指针进行运算,再解引用地址;同是一元运算符,运算顺序从右到左,遵循结合律
  23. }
  24. return total;
  25. }

第二个程序

主要考察自增减运算符;运算符的优先级;解引用地址运算符;
逻辑解释附在代码里;

  1. /*order.c -- 指针运算中的优先级*/
  2. /*20210428 -- 18:17*/
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. int data[2] = { 100, 200 };
  6. int moredata[2] = { 300, 400 };
  7. int main(void)
  8. {
  9. int* p1, * p2, * p3;
  10. p1 = p2 = data;
  11. p3 = moredata;
  12. printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
  13. printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);
  14. printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
  15. return 0;
  16. }
  17. /*p1和p3类似,都是运算符在指针变量的右边,所以他们仅仅是赋值,解引用地址是在索引值对象为数组首元素开始的,其中p1指针就是当前元素的值:100,但是运算符对原数据进行修改,也就是数组指针指向下一个元素,再解引用,原数组的值最后就是200;p3指针的自增减运算是300,因为先是原地解引用,然后对原位置的元素增加1;p2是指针向下一个元素移动一个位置,也是原地赋值,最后再解引用,所以值是200*/
  18. /*指针将会打印两遍,用于检查值是否被修改;自增减运算符在左边,可以理解左倾冒进,先运算后赋值;自增减运算符在右边,可以理解为右倾保守,先赋值,再运算,赋值是将值传递给一个新变量,运算是针对原变量,所以值与运算分离*/

image.png

10.5 指针操作

  1. /*ptr_ops.c -- 用于展示指针操作*/
  2. /*20210429 1419*/
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. int urn[5] = { 100, 200, 300, 400, 500 }; // 全局变量
  6. int test(void);
  7. int test(void)
  8. {
  9. int a = 100;
  10. printf("100的地址是%p, 变量a的地址是%d.\n",&urn[0], &a);
  11. return 100;
  12. }
  13. int main(void)
  14. {
  15. int* ptr1, * ptr2, * ptr3;
  16. ptr1 = urn; /*把数组赋值给一个指针*/
  17. ptr2 = &urn[2]; /*将一个数组元素赋值给一个指针*/
  18. printf("pointer value, dereferenced pointer, address:\n");
  19. printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p", ptr1, *ptr1, &ptr1); /*指针加法*/
  20. ptr3 = ptr1 + 4;
  21. printf("\nAdding an int to a pointer:\n");
  22. printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
  23. printf("%p\n", urn[4]);
  24. ptr1++;
  25. printf("\nvalues after ptr1++:\n");
  26. printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p", ptr1, *ptr1, &ptr1); /*递增指针*/
  27. ptr2--; /*递减指针*/
  28. printf("\nvalues after --ptr2:\n");
  29. printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p", ptr2, *ptr2, &ptr2);
  30. --ptr1; /*恢复为初始值*/
  31. ++ptr2; /*恢复为初始值*/
  32. printf("\nPointers reset to original values:\n"); /*用一个指针减去另外一个指针*/
  33. printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
  34. printf("\nsubtracting one pointer from another:\n");
  35. printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
  36. printf("n\subtracting an int from a pointer:\n");
  37. printf("ptr3 = %p, ptr3 -2 = %p\n", ptr3, ptr3 - 2);
  38. test();
  39. return 0;
  40. }

10.5 指针操作

完整代码

  1. /*ptr_ops.c -- 指针操作*/
  2. /*20210501 1250*/
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. int main(void)
  6. {
  7. int urn[5] = {100, 200, 300, 400, 500};
  8. int *ptr1, *ptr2, *ptr3;
  9. ptr1 = urn;
  10. ptr2 = &urn[2];
  11. printf("\n pointer value, dereferenced pointer, pointer address:\n");
  12. printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
  13. /*指针加法*/
  14. ptr3 = ptr1 + 4;
  15. printf("\n Adding an int to a pointer:\n");
  16. printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));
  17. ptr1++;
  18. printf("\n values after ptr1++:\n");
  19. printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);
  20. ptr2--;
  21. printf("\n values after --ptr2:\n");
  22. printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n", ptr2, *ptr2, &ptr2);
  23. --ptr1;
  24. ++ptr2; /*恢复为初始值*/
  25. printf("\n Pointers reset to original values:\n");
  26. printf("ptr1 = %p, ptr2 = %p\n", ptr1, ptr2);
  27. /*一个指针减去另外一个指针*/
  28. printf("\n subtracting one pointer from another:\n");
  29. printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %td\n", ptr2, ptr1, ptr2 - ptr1);
  30. /*一个指针减去一个整数*/
  31. printf("\n subtracting an int from a pointer:\n");
  32. printf("ptr3 = %p,ptr3 - 2 = %p\n", ptr3, ptr3 - 2);
  33. return 0;
  34. }

代码运行结果

程序清单10.13ptr_ops程序运行结果.png

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

指针的地址就是数组首元素的地址.png

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

指针与整数加法运算原理的计算.png

指针的自增加运算:实际上是将指针移动一个位置,从数组的一个元素移向下一个元素,一个位置的移动,相当于地址上四个字节的移动,四个字节是一个int类型数值占用的字节空间

指针与整数的加法,指针与整数的减法,原理一致,都需要保证取值的合法性

指针与指针的减法:如果同在一个数组,那么他们的结果是相差多少个元素位置,站在地址的角度,差还需要乘以字节数,该字节数取决于数组的数据类型

  1. 如果不在同一个数组,那么可能出结果,也可能直接出错

运算符中的比较:可以用运算符进行比较运算,但前提是指针均指向同一个数组

编译器特性:指针的越界检查,仅仅局限在索引值的合理取值范围(数组的最大长度)和指向指针最后一个元素的后一个元素的位置,除此之外,没有保证,所以是否越界,绝大多数情况取决于程序员的控制;数组末尾后一个元素的位置,仍然可以被解引用运算

10.6.1 保护数组中的数据

为了主函数的值不被子函数改变,或者说为了程序在不改变原数组元素值的情况下运行,可以在函数原型声明和函数中传入参数增加关键字const

但需要注意的是,如果是对每一个原数组中的元素进行运算,则该操作不成立,他与可计算左值互相矛盾

10.6.1 对形式参数使用const

代码

  1. /*arf.c -- */
  2. /*20210501 17:06*/
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #define SIZE 5
  6. /*特别注意:用于打印数组元素的子函数,它的参数是视为常量的双精度浮点数*/
  7. void show_array(const double ar[], int n); /*两个形式参数,分别限定了数组的数据类型,以及展示多少个数组元素*/
  8. void mult_array(double ar[], int n, double mult); /*三个形式参数,分别限定数组数据类型,多少个元素参与运算,以及乘数是多少*/
  9. int main(void)
  10. {
  11. double dip[SIZE] = {20.0, 17.66, 8.2, 15.3, 22.22};
  12. printf("The original dip array:\n");
  13. show_array(dip, SIZE);
  14. mult_array(dip, SIZE, 2.5);
  15. printf("The dip arrary after calling mult_array():\n");
  16. show_array(dip, SIZE);
  17. return 0;
  18. }
  19. /*显示数组的内容*/
  20. void show_array(const double ar[], int n)
  21. {
  22. int i;
  23. for (i = 0; i < n; i ++)
  24. {
  25. printf("%8.3f ", ar[i]); /*字符的格式为最大八个字符宽度,保留小数点后三位*/
  26. putchar('\n'); /*用内置方法对输出换行*/
  27. }
  28. }
  29. /*把数组的每一个元素都乘以相同的值*/
  30. void mult_array(double ar[], int n, double mult)
  31. {
  32. int i;
  33. for (i = 0; i < n; i++)
  34. {
  35. ar[i] *= mult;
  36. }
  37. }

原因

  1. 对于展示数组元素的子函数而言,仅仅需要打印元素的值,而不需要对数组元素修改。但为了避免被误修改,这里上了双保险,对数据类型加了关键字const修饰,编译器在运行过程中将其视为常量;