【C总集篇】第八章 数组和指针


第八章 数组和指针

数组

数组回顾

​   前面介绍过,数组由数据类型相同的一系列元素组成。需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型。编译器根据这些信息正确地创建数组。

简单来说:

  • 数组:类型相同的数据元素的集合,是C语言中的一种构造数据类型。
  • 这些元素会顺序地存储在内存的某段区域。
  • 普通变量可以使用的类型,数组元素也都可以用

例如:

/*一些数组声明*/
int main(void)
{
	float candy[365];	/*内含365个float 类型元素的数组*/
	char code[12];		/*内含 12 个char 类型元素的数组*/
	int states[50];		/*内含50 个int 类型元素的数组*/
}

方括号([])表明candy、code和states 都是数组,方括号中的数字表明数组中的元素个数。

​   要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素。数组元素的编号从0开始,所以 candy[0]表示 candy 数组的第1个元素,candy[364]表示第 365 个元素,也就是最后一个元素

初始化数组

初始化数组介绍
#include <stdio.h>
#define MONTHS 12

int main(void)
{
    int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
    .........
}

​   如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格

​ 根据上面的初始化,把31赋给数组的首元素(days[0]),以此类推.

int a[1] = {1};
int ab[2] = {1,2};
int abc[3] = {1,2,3};

再比如:

#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 %d has %2d days.\n", index +1,days[index]);
    
    return 0;
}

在这里插入图片描述

  这个程序还不够完善,每4年打错一个月份的天数(即,2月份的天数)。该程序用初始化列表初始化days[],列表(用花括号括起来)中用逗号分隔各值。
​   注意该例使用了符号常量 MONTHS表示数组大小,这是我们推荐且常用的做法。例如,如果要采用一年13个月的记法,只需修改#define这行代码即可(初学可以慢慢培养),不用在程序中查找所有使用过数组大小的地方。

初始化失败案例
#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;
}

在这里插入图片描述

使用数组前必须先初始化它与普通变量类似,在使用数组元素之前,必须先给它们赋初值

​   如果不对数组进行初始化,则编译器使用的值是内存相应位置上的现有值,每台机器上运行该程序后的输出会与该示例不同。

部分初始化
#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;
}

在这里插入图片描述

如上所示,当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为0

​   也就是说,如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值.但是,如果部分初始化数组,剩余的元素就会被初始化为0。
​   如果初始化列表的项数多于数组元素个数,编译器可没那么仁慈,它会毫不留情地将其视为错误。

​ 注意,这点性质非常重要,当我们以后如果使用数组的时候,却不知道要给他初始化什么值时,我们可以这样做

// 这样这20个元素初始化的值都被赋予零,可以方便我们后期修改
char aa[20]={0};
自动匹配数组

我们目前使用编译器自动匹配数组大小和初始化列表中的项数

#include <stdio.h>
int main(void)
{
    const int days[] = {31,28,31,30,31,30,31,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;
}

在这里插入图片描述
如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。

​   注意 for 循环中的测试条件。由于人工计算容易出错,所以让计算机来计算数组的大小。sizeof运算符给出它的运算对象的大小(以字节为单位)。所以sizeof days 是整个数组的大小(以字节为单位),sizeof day[0]是数组中一个元素的大小(以字节为单位)。整个数组的大小除以单个元素的大小就是数组元素的个数。

给数组赋值

在这里插入图片描述
注意这段代码中使用循环给数组的元素依次赋值。

  ​ C不允许把数组作为一个单元赋给另一个数组,除初始化以外也不允许使用花括号列表的形式赋值。下面的代码段演示了一些错误的赋值形式:

在这里插入图片描述

数组边界

在使用数组时,要防止数组下标超出边界。也就是说,必须确保下标是有效的值。例如,假设有下面的声明:

int aa=[20];

​   那么在使用该数组时,要确保程序中使用的数组下标在0~19的范围内,因为编译器不会检查出这种错误(但是,一些编译器发出警告,然后继续编译程序)。

我们可以看到以下的示例:

#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;
}

在这里插入图片描述

  编译器不会检查数组下标是否使用得当。在标准中,使用越界下标的结果是未定义的。这意味着程序看上去可以运行,但是运行结果很奇怪,或异常中止。

​   注意,该编译器似乎把 value2储存在数组的前一个位置,把value1储存在数组的后一个位置(其他编译器在内存中储存数据的顺序可能不同)。

​   在上面的输出中,arr[-1]与value2对应的内存地址相同,arr[4]和 value1对应的内存地址相同。因此,使用越界的数组下标会导致程序改变其他变量的值。不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止。

指定数组大小

在这里插入图片描述

多维数组

多维数组是指二维及以上的数组。

  • 一维数组:一列长表或一个向量
  • 二维数组:一个表格或一个平面矩阵
  • 三维数组:一本书或三维空间的一个方阵
  • 多维数组: 多维空间的一个数据列阵

在这里插入图片描述
我们这里主要以二维数组进行展开

二维数组的定义

二维数组的声明与一维数组相同,一般形式如下:

类型说明符 数组名 [常量表达式 1] [常量表达式 2]
其中 "常量表达式1" 被称之为行下标,"常量表达式2" 被称之为是列下标。二维数组下标的取值范围,如下所示:

 行下标的取值范围是:0~n-1。
 列下标的取值范围是:0~m-1。
 二维数组最大元素下标识:array[n-1][m-1]

例如:定义一个33列的整型数组

int array[3][3]

上述代码声明了一个 33 列的二维数组,其 数组名 是 array,其下标变量的类型为整形。

在这里插入图片描述
注意:

  • 在 C语言中,二维数组是按行排列的,即按行顺序存放,先存放 array[0] 行,再存放 arrar[1] 行,接着存放 array[2] 行。每行有 3 个元素,也是其依次存放的。
  • 注意:不管是 行下标 还是 列下标,其中的索引值都是从 0 开始的。
  • 注意:和一维数组是一样的,我们都需要注意下标越界的问题。

例如:

​ 一个学习小组有 5 个人,每个人有三门课的考试成绩。求全组分科的平均成绩和 各科总平均成绩···

#include <stdio.h>
int main()
{
	int i, j;     //二维数组下标
	int sum = 0;  //当前科目的总成绩
	int average;  //总平均分
	int v[3];     //各科平均分
	int a[3][5];  //用来保存每个同学各科成绩的二维数组,三门学科,五个同学。
	printf("请输入各科学生的成绩:\n");
	for (i = 0; i < 3; i++)
	{
		printf("\n一门学科数入5次成绩\n");
		if (i == 0)
			printf("数学学科:");
		if (i == 1)
			printf("语文学科:");
		if (i == 2)
			printf("英语学科:");
		for (j = 0; j < 5; j++)
		{
			scanf("%d", &a[i][j]);  //输入每个同学的各科成绩
			sum += a[i][j];			//计算当前科目的总成绩(sum)
		}
		v[i] = sum / 5;  // 当前科目的平均分,用总的成绩除以5
		sum = 0;	     // 把当前科目总成绩清0
	}
	average = (v[0] + v[1] + v[2]) / 3;
	printf("\n数学: %d\n语文: %d\n英语: %d\n", v[0], v[1], v[2]);
	printf("平均分: %d\n", average);
	return 0;
}

在这里插入图片描述

二维数组的初始化

​   二维数组初始化也是在类型说明时给各下标变量赋以初值。二维数组可按行分段赋值, 也可按行连续赋值。下面是一个带有 3 行 4 列的数组:

int a[3][4] = {  
 {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
 {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
 {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
};

也可以是这样的:

int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

获取每行每列数组元素的值:

#include<stdio.h>
int main(void)
{
	int i, j;
	int arr[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("arr[%d][%d] = [%d] [%p]\n", i, j, arr[i][j], &arr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

可以留意一下这边的地址:

在这里插入图片描述

arr[0][0] 与 arr[0][1] 的地址相差了 4 个字节,刚好是一个int的位置
arr[0][0] 与 arr[1][0] 的地址相差了 12 个字节,刚好是三个int的位置    

二维数组初始化的性质大部分与一位数组相同

例如:

可以只对部分元素赋值,未赋值的元素自动取"0"值。例如:

int arr[3][3] = {{1,2},{2,3},{3,4}};
是对每一行的第一列元素赋值,未赋值的元素的值为 0。赋值后各元素的值为:
1  2  0
2  3  0
3  4  0

但如果是这样的话:

int arr[3][3] = {1,2,2,3,3,4};
则初始化的结果为,未赋值的元素的值为 01 2 2
3 3 4
0 0 0

如果对全部元素赋值,那么第一维的长度可以不给出。例如:

int arr[3][3] = {{1,2,3},{2,3,4},{3,4,5}};

等价于

int arr[][3] = {{1,2,3},{2,3,4},{3,4,5}};

注意:行 是可以进行省略的,但是 列 不能进行省略。

指针与数组

​   指针提供一种以符号形式使用地址的方法。因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。因此,使用指针的程序更有效率。尤其是,指针能有效地处理数组。我们很快就会学到,数组表示法其实是在变相地使用指针。

下面的语句成立:
  我们举一个变相使用指针的例子:数组名是数组首元素的地址。也就是说,如果flizny是一个数组,

flizny == &flizny[0];//数组名是该数组首元素的地址

​   flizny 和&flizny[0]都表示数组首元素的内存地址(&是地址运算符)两者都是常量,在程序的运行过程中,不会改变。但是,可以把它们赋值给指针变量,然后可以修改指针变量的值。

#include <stdio.h>
#define SIZE 4
int main(void)
{
    short dates [SIZE];
    short * pti;
    short index;
    double bills[SIZE];
    double * ptf;
    
    pti = dates;    // assign address of array to pointer
    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;
}

![image-

我们的系统中,地址按字节编址,short类型占用2字节,double类型占用8字节。

​   在C中,指针加1指的是增加一个存储单元对数组而言,这意味着把加1后的地址是下一个元素的地址,而不是下个字节的地址。这是为什么必须声明指针所指向对象类型的原因之一。只知道地址不够,因为计算机要知道储存对象需要多少字节(即使指针指向的是标量变量,也要知道变量的类型,否则*pt 就无法正确地取回地址上的值)。

在这里插入图片描述
在这里插入图片描述

dates +2 == &date[2]			//相同的地址
*(dates +2) == dates[2]		//相同的值

​   以上关系表明了数组和指针的关系十分密切,可以使用指针标识数组的元素和获得元素的值。从本质上看,同一个对象有两种表示法。实际上,C语言标准在描述数组表示法时确实借助了指针。也就是说,定义 ar[n]的意思是* (ar +n)。可以认为*(ar +n)的意思是“到内存的 ar 位置,然后移动n个单元检索储存在那里的值”。

  顺带一提,不要混淆 * (dates+2)和 * dates+2。间接运算符( * )的优先级高于+,所以*dates+2相当于( * dates)+2:

* (dates+2) //dates第3个元素的值
* dates+2	//dates第1个元素的值加2
#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));   // same as days[index]   
    return 0;
}

可以发现,这个其实是和调用数组是一样的

在这里插入图片描述
days 是数组首元素的地址

​   days+index 是元素

​   days[index]的地址,

​   而*(days+index)则是该元素的值,相当于 days[index]。

​   for循环依次引用数组中的每个元素,并打印各元素的内容。这样编写程序是否有优势?不一定。编译器编译这两种写法生成的代码相同。要注意的是,指针表示法和数组表示法是两种等效的方法。该例演示了可以用指针表示数组,反过来,也可以用数组表示指针。在使用以数组为参数的函数时要注意这点。

函数 数组 与指针

函数 数组 与指针 初了解

​   假设要编写一个处理数组的函数,该函数返回数组中所有元素之和,待处理的是名为marbles的int类型数组。应该如何调用该函数?也许是下面这样:

int marbles[10];

​   那么,该函数的原型是什么?记住,数组名是该数组首元素的地址,所以实际参数marbles 是一个储存 int 类型值的地址,应把它赋给一个指针形式参数,即该形参是一个指向int 的指针:

int sum(int*ar);//对应的函数原型

sum()从该参数获得了什么信息?

  • 它获得了该数组首元素的地址,知道要在该位置上找出一个整数。
    在这里插入图片描述
    我们还可以用 等价与int sum(int*ar);
int sum(int arl],int n);

​   同时,关于函数的形参,只有在函数原型或函数定义头中,才可以用int ar[]代替int*ar:

  ​ int *ar 形式和 int ar[]形式都表示 ar 是一个指向 int 的指针。

​   但是,int ar[ ]只能用于声明形式参数。第2种形式(int ar[ ])提醒读者指针 ar 指向的不仅仅一个 int 类型值,还是一个 int 类型数组的元素。

在这里插入图片描述

#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
    int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
    long answer;
    
    answer = sum(marbles, SIZE);
    printf("The total number of marbles is %ld.\n", answer);
    printf("The size of marbles is %zd bytes.\n",
           sizeof marbles);
    
    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;
}

在这里插入图片描述
​   注意,marbles的大小是40字节。这没问题,因为marbles内含10个int 类型的值,每个值占4字节,所以整个marbles的大小是 40字节。

​   但是,ar才8字节。这是因为 ar 并不是数组本身,它是一个指向 marbles数组首元素的指针。我们的系统中用8字节储存地址,所以指针变量的大小是8字节(其他系统中地址的大小可能不是8字节)。

​   简而言之,marbles是一个数组,ar 是一个指向 marbles数组首元素的指针,利用C中数组和指针的特殊关系,可以用数组表示法来表示指针 ar。

使用指针形参

​   函数要处理数组必须知道何时开始、何时结束。sum()函数使用一个指针形参标识数组的开始,用一个整数形参表明待处理数组的元素个数(指针形参也表明了数组中的数据类型)。但是这并不是给函数传递必备信息的唯一方法。

  ​ 还有一种方法是传递两个指针,第1个指针指明数组的开始处(与前面用法相同),第2个指针指明数组的结束处。

#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
    int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
    long answer;
    
    answer = sump(marbles, marbles + SIZE);
    printf("The total number of marbles is %ld.\n", answer);
    
    return 0;
}


int sump(int * start, int * end)
{
    int total = 0;
    
    while (start < end)
    {
        total += *start;
        start++;         
    }
    
    return total;
}

在这里插入图片描述
​   *指针 start 开始指向 marbles 数组的首元素,所以赋值表达式total += start 把首元素(20)加给 tota1

​   然后,表达式 start++递增指针变量 start,使其指向数组的下一个元素因为 start 是指向 int 的指针,start 递增1相当于其值递增int 类型的大小。

​   因为 while 循环的测试条件是一个不相等的关系,所以循环最后处理的一个元素是end 所指向位置的前一个元素。这意味着 end 指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时指向数组后面第一个位置的指针仍是有效的指针。这使得while循环的测试条件是有效的,因为start在循环中最后的值是end。注意,使用这种“越界”指针的函数调用更为简洁:

answer = sump(marbles, marbles + SIZE);

​   因为下标从0开始,所以marbles+SIE指向数组末尾的下一个位置。如果while的条件是start <= end,则必须使用下面的代码:

answer = sump(marbles, marbles + SIZE - 1);

​   这种写法既不简洁也不好记,很容易导致编程错误。顺带一提,虽然C保证了marbles+SIZE有效,但是对marbles[SIZE] (即储存在该位置上的值)未作任何保证,所以程序不能访问该位置。

total += *start;
start++;     

同时这段函数可以简化成

total += *start++;

注意:

在这里插入图片描述
  一元运算符 * 和++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是start。也就是说,指针 start 先递增后指向。使用后缀形式(即start++而不是++start)意味着先把指针指向位置上的值加到 tota1上,然后再递增指针。如果使用 * ++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使用( * start)++,则先使用start 指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然 * start++的写法比较常用,但是(start++)这样写更清楚。

以上这段话简单理解就是:

total += *start++;
/*
因为结合律是从右往左, 所以先执行start++
但是start++ 是可以理解为先提供start 之后再将 start + 1
所以由于start++ 先提供了 start,可以转化成以下步骤
变成*start(解引start中的值)了
之后在start++
*/


total += *++start;
/*
这种就先递增指针,再使用指针指向位置上的值
可以等价于:
        start++;
        total += *start; 
*/



(* start)++
/*
由于()的优先级是最高的,所以(* start)是直接解引出来了
之后再 ++ 
*/
    
    
*(start++)
/*
 与*start++等价;
*/

注意:这代码一定要能自己看懂,看懂的话以上的基本没啥大问题了

#include <stdio.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;
}

在这里插入图片描述
解析:

1:

printf("  *p1 = %d,   *p2 = %d,     *p3 = %d\n",*p1     ,   *p2     ,     *p3);
p1 = p2 = data; // 首先根据这一段, 将data的首地址赋给 P1,P2 

所以 *p1 相当于 data[0],所以是100
P2 同理

    
p3 = moredata;	// 首先根据这一段, 将moredata的首地址赋给 P3 
所以 *p3 相当于 moredata[0],所以是300

2:

printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",*p1++   , *++p2     ,     (*p3)++);
*p1++
等价于:
    *P1;		//data[0]
	 P1++;		
所以,此时printf 的值还是为当时P1的解引值,打印完之后P1才指向下一个
    
    
*p2++
等价于:
    P2++;
    *P2;	//data[1]
所以,此时printf 的打印出来的值是直接为指向下一个的解引值
    
    
(*p3)++
   等价于:
    *P3;
    P3++;	//data[1] 
所以,此时printf 的值还是为当时P3的解引值,打印完之后P3的解引值才会增加

3:

printf("  *p1 = %d,   *p2 = %d,     *p3 = %d\n",*p1     ,   *p2     ,     *p3);
此时为步骤2打印后的值

注意:

​   至于C语言,ar[i]和 * (ar+1)这两个表达式都是等价的。无论 ar 是数组名还是指针变量,这两个表达式都没问题。但是,只有当ar是指针变量时,才能使用ar++这样的表达式。

指针操作

八种基本指针操作方式

#include <stdio.h>
int main(void)
{
    int urn[5] = {100,200,300,400,500};
    int * ptr1, * ptr2, *ptr3;
    
    ptr1 = urn;       
    ptr2 = &urn[2];     

    printf("pointer value, dereferenced pointer, pointer address:\n");
    printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
           ptr1, *ptr1, &ptr1);
    

    ptr3 = ptr1 + 4;
    printf("\nadding an int to a pointer:\n");
    printf("ptr1 + 4 = %p, *(ptr4 + 3) = %d\n",
           ptr1 + 4, *(ptr1 + 3));
    ptr1++;           
    printf("\nvalues after ptr1++:\n");
    printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n",
           ptr1, *ptr1, &ptr1);
    ptr2--;            
    printf("\nvalues after --ptr2:\n");
    printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p\n",
           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("\nsubtracting an int from a pointer:\n");
    printf("ptr3 = %p, ptr3 - 2 = %p\n",
           ptr3,  ptr3 - 2);
    
    return 0;
}

打印示例:

在这里插入图片描述

  • 赋值:

    • 可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。在该例中,把urn数组的首地址赋给了ptr1,该地址的编号恰好是0x7fff5fbff8d0。变量ptr2获得数组urn的第3个元素(urn[2])的地址。注意,地址应该和指针类型兼容。也就是说,不能把 double 类型的地址赋给指向int 的指针,至少要避免不明智的类型转换。C99/C11已经强制不允许这样做。
  • 解引用:

      • 运算符给出指针指向地址上储存的值。因此,*ptr1的初值是100,该值储存在编号为0x7fff5fbff8d0的地址上。
  • 取址:

    • 和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。本例中,ptr1储存在内存编号为0x7fff5fbff8c8的地址上,该存储单元储存的内容是0x7fff5fbff8d0,即urn的地址。因此&ptr1是指向ptr1的指针,而ptr1是指向utn[0]的指针。
  • 指针与整数相加:

    • 可以使用+运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。因此ptr1+4与&urn[4]等价。如果相加的结果超出了初始指针指向的数组范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
  • 递增指针:

    • 递增指向数组元素的指针可以让该指针移动至数组的下一个元素。因此,ptr1++相当于把ptr1的值加上4(我们的系统中int为4字节),ptr1指向urn[1](见图 10.4,该图中使用了简化的地址)。现在ptr1的值是0x7fff5fbff8d4(数组的下一个元素的地址),*ptr 的值为200(即 urn[1]的值)。注意,ptr1本身的地址仍是 0x7fff5fbff8c8。毕竟,变量不会因为值发生变化就移动位置。
      在这里插入图片描述
  • 指针减去一个整数:

    • 可以使用-运算符从一个指针中减去一个整数。指针必须是第1个运算对象,整数是第2个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。所以ptr3-2与&urn[2]等价,因为ptr3指向的是&arn[4]。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。
  • 递减指针:

    • 当然,除了递增指针还可以递减指针。在本例中,递减ptr3使其指向数组的第2个元素而不是第3个元素。前缀或后缀的递增和递减运算符都可以使用。注意,在重置ptr1和ptr2前,它们都指向相同的元素urn[1]。
  • 指针求差:

    • 可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。例如,程序清单10.13的输出中,ptr2-ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误。
  • 比较

    • 使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象

解引用未初始化的指针

说到注意事项,一定要牢记一点:千万不要解引用未初始化的指针。例如,考虑下面的例子:

在这里插入图片描述

​   为何不行?第2行的意思是把5储存在 pt 指向的位置。

​   但是 pt 未被初始化,其值是一个随机值,所以不知道5将储存在何处

​   这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃。

​   切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。因此,在使用指针之前,必须先用已分配的地址初始化它。例如,可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况)。无论如何,使用指针时一定要注意,不要解引用未初始化的指针!

const 与指针

const 引入

​   编写一个处理基本类型(如,int)的函数时,要选择是传递int 类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针。对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。

​   传递地址会导致一些问题。C通常都按值传递数据,因为这样做可以保证数据的完整性。如果函数使用的是原始数据的副本,就不会意外修改原始数据。但是,处理数组的函数通常都需要使用原始数据,因此这样的函数可以修改原数组。有时,这正是我们需要的。例如,下面的函数给数组的每个元素都加上个相同的值:
在这里插入图片描述
​   所以避免类似错误的唯一方法是提高警惕。ANSIC提供了一种预防手段。如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const。

const 定义以及优缺点

​   关键字const用来定义常量,如果一个变量被const修饰,那么它的值就不能再被改变,我想一定有人有这样的疑问,C语言中不是有#define吗,干嘛还要用const呢,我想事物的存在一定有它自己的道理,所以说const的存在一定有它的合理性,与预编译指令相比,const修饰符有以下的优点:

1、预编译指令只是对值进行简单的替换,不能进行类型检查

2、可以保护被修饰的东西,防止意外修改,增强程序的健壮性

3、编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。

看到如下的代码:

在这里插入图片描述
​   以上代码中的 const 告诉编译器,该函数不能修改 ar 指向的数组中的内容。如果在函数中不小心使用类似 ar[i]++的表达式,编译器会捕获这个错误,并生成一条错误信息。
​   **这里一定要理解,这样使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改。**这样使用const可以保护数组的数据不被修改,就像按值传递可以保护基本数据类型的原始值不被改变一样。

​   一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用const。

const 其他内容

const 回顾

我们在前面使用 const 创建过变量:

const double Pl=3.14159;

修饰局部变量:

const int n=5;
int const n=5;

​   这两种写法是一样的,都是表示变量n的值不能被改变了,需要注意的是,用const修饰变量时,一定要给变脸初始化,否则之后就不能再进行赋值了。

​   虽然用#define 指令可以创建类似功能的符号常量,但是const 的用法更加灵活。可以创建 const数组、const指针和指向 const的指针。

const int days[12]={31,28,31,30,31,30,31,31,30,31,30,31};

如果程序稍后尝试改变数组元素的值,编译器将生成一个编译期错误消息:

days[9]= 44;	/*编译错误 */

这分为常量指针和指针常量

最主要看const的位置

// 常量指针
const int * n;

// 指针常量
int const * n;
常量指针

1、常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的引用来改变变量的值的。

int a[5]={5,2,3,5};
const int* n=&a;

那什么叫不能通过这个指针改变变量的值呢?

// n 指向的 int 类型的值声明为const,这表明不能使用 n 来更改它所指向的值:
*n = 29; // 这样是违法操作
n[2] = 29; // 这样是违法操作   

那什么叫可以通过其他的引用来改变变量的值呢?

a[0] = 20; // 允许,因为a 并未被const限定

在举个其他例子:

在这里插入图片描述
2、常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

int a=5;
int b=6;
const int* n=&a;
n=&b;

指向const的指针通常用于函数形参中,表明该函数不会使用指针改变数据。

void show array(const double *ar,int n);

​   关于指针赋值和 const 需要注意一些规则。首先,把const 数据或非const 数据的地址初始化为指向const的指针或为其赋值是合法的:

在这里插入图片描述
然而,只能把非const数据的地址赋给普通指针:
在这里插入图片描述
这个规则非常合理。否则,通过指针就能改变const数组中的数据

指针常量

  指针常量是指指针本身是个常量,不能在指向其他的地址

  需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向改地址的指针来修改。

int a=5;
int *p=&a;
int* const n=&a;
*p=8;
理解
/*
可以将指针拆分成 如:
*p       p      &p
结引值	   值     地址
这三个部分

常量指针, 指的是整个地址(3部分是个常量),但是他的值还是可以改变的,所以说他的空间是不能变的

指针常量   指的是整个地址(2部分是个常量),但是他的接引值还是可以改变的

在这里插入图片描述

如果实在不懂可以再看看这个:

【指针】const和指针_哔哩哔哩_bilibili

在这里插入图片描述

指针和多维数组

指针和多维数组原理

int zippo[4][2];/*内含int数组的数组 */

​   然后数组名 zippo是该数组首元素的地址。在本例中,zippo的首元素是一个内含两个 int值的数组,所以 zippo 是这个内含两个 int 值的数组的地址。下面,我们从指针的属性进一步分析。

在这里插入图片描述

int zippo[2][3] = { {2,4,1}, {6,8,1} };

例子

#include <stdio.h>
int main(void)
{
    int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} };
    
    printf("   zippo = %p,    zippo + 1 = %p\n",
           zippo,         zippo + 1);
    printf("zippo[0] = %p, zippo[0] + 1 = %p\n",
           zippo[0],      zippo[0] + 1);
    printf("  *zippo = %p,   *zippo + 1 = %p\n",
           *zippo,        *zippo + 1);
    printf("zippo[0][0] = %d\n", zippo[0][0]);
    printf("  *zippo[0] = %d\n", *zippo[0]);
    printf("    **zippo = %d\n", **zippo);
    printf("      zippo[2][1] = %d\n", zippo[2][1]);
    printf("*(*(zippo+2) + 1) = %d\n", *(*(zippo+2) + 1));
    
    return 0;
}

在这里插入图片描述
在这里插入图片描述
图形化:
在这里插入图片描述

指向多维数组的指针

#include <stdio.h>
int main(void)
{
    int zippo[4][2] = { {2,4}, {6,8}, {1,3}, {5, 7} };
    int (*pz)[2];
    pz = zippo;
    
    printf("   pz = %p,    pz + 1 = %p\n",
           pz,         pz + 1);
    printf("pz[0] = %p, pz[0] + 1 = %p\n",
           pz[0],      pz[0] + 1);
    printf("  *pz = %p,   *pz + 1 = %p\n",
           *pz,        *pz + 1);
    printf("pz[0][0] = %d\n", pz[0][0]);
    printf("  *pz[0] = %d\n", *pz[0]);
    printf("    **pz = %d\n", **pz);
    printf("      pz[2][1] = %d\n", pz[2][1]);
    printf("*(*(pz+2) + 1) = %d\n", *(*(pz+2) + 1));
    
    return 0;
}

在这里插入图片描述

​   系统不同,输出的地址可能不同,但是地址之间的关系相同。如前所述,虽然pz是一个指针,不是数组名,但是也可以使用 pz[2][1]这样的写法。可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:

zippo[m][n]==*(*(zippo +m)+ n)
pz[m][n]== *(*(pz +m)+ n)

指针的兼容性

指针之间的赋值比数值类型之间的赋值要严格。

​ 例如,不用类型转换就可以把int类型的值赋给double类型的变量,但是两个类型的指针不能这样做。

在这里插入图片描述
在这里插入图片描述

函数和多维数组

​   如果要编写处理二维数组的函数,首先要能正确地理解指针才能写出声明函数的形参。在函数体中,通常使用数组表示法进行相关操作。

void somefunction(int(*pt)[4]);

或者

void somefunction(int pt[][4]);

例子:

#include <stdio.h>
#define ROWS 3
#define COLS 4
void sum_rows(int ar[][COLS], int rows);
void sum_cols(int [][COLS], int );    
int sum2d(int (*ar)[COLS], int rows); 
int main(void)
{
    int junk[ROWS][COLS] = {
        {2,4,6,8},
        {3,5,7,9},
        {12,10,8,6}
    };
    
    sum_rows(junk, ROWS);
    sum_cols(junk, ROWS);
    printf("Sum of all elements = %d\n", sum2d(junk, ROWS));
    
    return 0;
}

void sum_rows(int ar[][COLS], int rows)
{
    int r;
    int c;
    int tot;
    
    for (r = 0; r < rows; r++)
    {
        tot = 0;
        for (c = 0; c < COLS; c++)
            tot += ar[r][c];
        printf("row %d: sum = %d\n", r, tot);
    }
}

void sum_cols(int ar[][COLS], int rows)
{
    int r;
    int c;
    int tot;
    
    for (c = 0; c < COLS; c++)
    {
        tot = 0;
        for (r = 0; r < rows; r++)
            tot += ar[r][c];
        printf("col %d: sum = %d\n", c, tot);
    }
}

int sum2d(int ar[][COLS], int rows)
{
    int r;
    int c;
    int tot = 0;
    
    for (r = 0; r < rows; r++)
        for (c = 0; c < COLS; c++)
            tot += ar[r][c];
    
    return tot;
}

在这里插入图片描述
注意,下面的声明不正确:

int sum2(int ar[][]int rows);//错误的声明

​   前面介绍过,编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1]转换成 ar+1。编译器对 ar+1 求值,要知道 ar所指向的对象大小。下面的声明:

int sum2(int ar[][4],int rows);//有效声明

​   表示 ar 指向一个内含4个 int 类型值的数组(在我们的系统中,ar指向的对象占 16字节),所以ar+1的意思是“该地址加上16字节”。如果第2对方括号是空的,编译器就不知道该怎样处理。

不过初学还是建议直接这有写,比较清晰

int sum2(int ar[3][4]int rows);//有效声明,但是3将被忽略

一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值:

int sum4d(int ar[][12][20][30],int rows);

  因为第1对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。下面的声明与该声明等价:

这里,ar 指向一个12x20x30的int 数组

int sum4d(int(*ar)[12][20][30]int rows);// ar是一个指针

变长数组

  注意 变长数组不能改变大小

​   变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度

​   在学习处理二维数组的函数中可能不太理解,为何只把数组的行数作为函数的形参,而列数却内置在函数体内。例如,函数定义如下:

在这里插入图片描述
​   sum2d()函数之所以能处理这些数组,是因为这些数组的列数固定为4,而行数被传递给形参rows,rows是一个变量。但是如果要计算6x5的数组(即6行5列),就不能使用这个函数,必须重新创建一个CLOS为5的函数。因为C规定,数组的维数必须是常量,不能用变量来代COLS。

  鉴于此,C99新增了变长数组(variable-lengih array,VLA),允许使用变量表示数组的维度。如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

​   前面提到过,变长数组有一些限制。变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用static或extern存储类别说明符。而且,不能在声明中初始化它们。最终,C11把变长数组作为一个可选特性,而不是必须强制实现的特性。

#include <stdio.h>
#define ROWS 3
#define COLS 4
int sum2d(int rows, int cols, int ar[rows][cols]);
int main(void)
{
    int i, j;
    int rs = 3;
    int cs = 10;
    int junk[ROWS][COLS] = {
        {2,4,6,8},
        {3,5,7,9},
        {12,10,8,6}
    };
    
    int morejunk[ROWS-1][COLS+2] = {
        {20,30,40,50,60,70},
        {5,6,7,8,9,10}
    };
    
    int varr[rs][cs];  // VLA
    
    for (i = 0; i < rs; i++)
        for (j = 0; j < cs; j++)
            varr[i][j] = i * j + j;
    
    printf("3x5 array\n");
    printf("Sum of all elements = %d\n",
           sum2d(ROWS, COLS, junk));
    
    printf("2x6 array\n");
    printf("Sum of all elements = %d\n",
           sum2d(ROWS-1, COLS+2, morejunk));
    
    printf("3x10 VLA\n");
    printf("Sum of all elements = %d\n",
           sum2d(rs, cs, varr));
    
    return 0;
}


int sum2d(int rows, int cols, int ar[rows][cols])
{
    int r;
    int c;
    int tot = 0;
    
    for (r = 0; r < rows; r++)
        for (c = 0; c < cols; c++)
            tot += ar[r][c];
    
    return tot;
}

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值