本章内容简介
关键字: static
运算符: &, *(一元运算符)
如何创建并且初始化数组
指针(在已经学过的基础上), 指针和数组
编写处理数组的函数
二维数组
10.1数组
亮点
月份是用define定义的,所以直接到声明处修改值就可以实现多处同时修改
/*day_mon1.c* -- 打印每个月的天数*/
// 20210320 21:20
#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 and %2d days.\n", index + 1, days[index]);
return 0;
}
未初始化数组的后果
编译器自动初始化数组,全部为CC,所以内存地址就在一个地方了;
这里显示的是内存中已有的四个数字的内存变量,由于被编译器初始化,所以值完全相等;
/*no_data.c -- 为初始化数组*/
//20210321 08:18
#include <stdio.h>
#define SIZE 4
int 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 4
int 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 12
int 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 4
int 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 5
int 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 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;
}
指针与数组等效
指针表示法和数组表示法等效;
days = days[0]; (days + index) = days[0 + index];
/*day_mon3.c -- use pointer notation*/
//20210321 21:16
#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;
}
;
10.4函数,数组和指针
数组数据类型的函数声明,和之前一样,但多了一个类型声明,其次就是参数个数声明,在类型声明中,必须指明数组,可以不写数组名字,但是方括号一定要有;
还可以使用解引用运算符加数组名字声明数据类型;
参数的个数也可以不明确指定,但不能缺少类型声明;
int ar[]只能用于函数原型声明;
//sum_arr1.c -- 数组元素之和
//20210322 07:28
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n); //也可以用指针声明变量, int * ar
int 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 10
int 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修饰,编译器在运行过程中将其视为常量;