C语言指针与数组

在上一篇对指针介绍的文章当中,我们初次了解到了指针,并且知道了地址和内存间的关系,懂得了如何取地址和对指针的解引用,算是对指针有了一个初步的了解。而今天让我们对指针进行更深一步的了解吧~

一、指针与数组名

我们知道,指针变量是一个用来存放地址的变量,比如我们定义一个整形变量a,那么我们想要用指针变量来存放它的地址,就需要写成int* pa = &a的形式。而这样的形式仅仅能存放一个变量的地址,那么如果我们想要存放数组的地址呢?有些人可能会说,那就int* arr = &arr呗,那么这样的写法是对的嘛?让我们来看一个例子。欸?为什么不能把arr的地址存到指针变量当中呢,而此时当我们把取地址符&去掉后,我们会发现此时代码竟然不报错了!为什么不报错了呢?让我们反推一下,我们知道int* arr一定是一个存储地址的指针变量,而此时代码正确,就充分说明了我们传入Print函数中的arr就是一个地址。那么arr作为一个被我们定义的数组,arr数组名地址究竟是代表整个数组?还是数组的某个元素?

既然实践出真知,让我们写一段代码分成几种情况分别来看一看数组名到底代表什么吧~

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);
	return 0;
}

我们会发现对数组的第一个元素取地址,与数组名的地址是相同的,那么也就说明数组名所代表的就是数组首元素的地址~......欸?或许有些人就要问了:明明之前用sz求数组元素个数的时候,sizeof(arr)/sizeof(arr[0])中sizeof(arr)表示的是整个数组的大小呀,其实sizeof(arr)是一个例外的情况。在一般情况下数组名代表的就是数组首元素地址,而有两个例外的情况

  • sizeof(数组名):在这种情况下,数组名表示的是整个数组的大小,单位是字节,一般用于计算数组的元素个数。
  • &数组名:这种情况下取地址符取的是整个数组的地址。

对于第一个例外我们已经充分了解过啦,那么让我们来说一下第二个情况&数组名。

依然是不变的实践出真知~让我们敲一个代码运行一下看看。

在这里我们可以看到&arr虽然表示整个数组,但输出地址还是数组首元素的形式,没错~让我们更深一步的探索&arr与arr的差别,再一次实践出真知:

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	printf("arr:       %p\n", arr);
	printf("&arr[0]:   %p\n", &arr[0]);
	printf("&arr:      %p\n", &arr);
	printf("arr+1:     %p\n", arr+1);
	printf("&arr[0]+1: %p\n", &arr[0]+1);
	printf("&arr+1:    %p\n", &arr+1);
	return 0;
}

这时候就能看出&arr和arr的差别了,我们定义的数组有五个元素,数组名+1表示的是跳过一个元素,而&数组名+1跳过了整个数组,这就是arr和&arr最本质的差别啦~

二、指针访问数组与数组传参

前面我们充分了解了数组名和&数组名以及两种例外情况,现在再来让我们学习一下如何使用刚刚的知识用指针来对数组进行访问吧~我们来尝试一下使用指针对数组元素进行输入并且输出

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		scanf("%d", arr + i);
	}
	printf("\n");
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
	return 0;
}

我们又学会了如何用指针访问数组,那么我们来看看数组的传参。

在进行传参的时候,是将传递的实参进行复制,然后将这个复制的值进入函数中变成一个形参。但我们想把一个数组传递进去的时候,如果把每一个元素都一个个的复制,一个个的放进函数,要是数组中元素很多,那么会使传参的工作变得繁琐,所以在进行传参时候,数组会退化为指针

int sz(int* arr)
{
	int num = sizeof(arr) / sizeof(arr[0]);
	return num;
}
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	int sz2 = sz(arr);
	printf("%d\n%d", sz1, sz2);
	return 0;
}

由这段代码我们能够清晰的看出数组退化成指针的过程。在将数组传参到函数里后,再使用sizeof(arr)/sizeof(arr[0])求出的值却是1,这就证明了数组的退化,因为在传参的时候将数组名传递给函数,本质上传递的是数组首元素的地址,而用数组首元素去除以数组首元素,得到的值理所应当就是1。

三、指针数组

指针数组,顾名思义是由多个指针类型元素组成的数组,也就是说指针数组是一个存放指针的数组

我们在定义整型数组时,需要写成的格式是:int 数组名[元素个数]。定义字符数组时需要写成的格式是:char 数组名[元素个数]。前面是int还是char取决于想要创造的数组类型,那么由此我们能猜想出指针数组的写法:如果我们想要定义一个用于存放整型指针变量的数组,那么元素对应的类型就是int*,所以格式是:int* 数组名[元素个数]

让我们试着用指针数组来模拟一维数组:

int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	int ar[3] = { a,b,c };
	int* arr[3] = { &ar[0],&ar[1],&ar[2]};
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("地址为:%p ", *(arr + i));
		printf("所指向内容的值为:%d \n", *(*(arr + i)));
	}
	return 0;
}

在这段代码中我们定义了三个整形变量,再用一个整型数组来存放三个整形变量。然后我们又创造了一个整型指针数组,用来存放整型数组中各个元素的地址。需要注意的是:使用数组存放变量,输出时与arr[i]对应的格式是*(arr+i),而此时指针数组中存放的是地址,使用*(arr+i)的格式得到的是存放进指针数组中的指针变量,想要得到指针变量对应的值需要进一步对指针变量再一次解引用,写成*(*(arr + i))的形式。

我们接着再用指针数组模拟一个二维数组:

int main()
{
	int arr1[] = { 1,2,3 };
	int arr2[] = { 4,5,6 };
	int arr3[] = { 7,8,9 };
	int* parr[3] = { arr1, arr2, arr3 };//数组名是数组元素的地址,所以可存放
	int i = 0;
	int j = 0;
	printf("用数组形式:\n");
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	printf("用指针形式:\n");
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 3; j++)
		{
			printf("%d ", *(*(parr+i)+j));
		}
		printf("\n");
	}
	return 0;
}

注:*(*(parr+i)+j)的形式,代表的是第i行第j个元素。

四、数组指针

我们之前学习指针的时候了解到:整形指针变量用来存放整形变量的地址,并且能够指向整形数据。字符指针变量用来存放字符型变量的地址,并且能够指向字符型数据。那么以此类推我们也应该能够看出,数组指针其实指的是一个指针变量,数组指针变量应该用来存放数组的地址,并且能够指向数组的数据。

数组指针的写法:

 int (*p)[5];

注释:(*p)中,p先和*结合来说明p是一个指针变量,然后这个指针变量指向的是一个大小为5的整型的数组,因p为指针变量,指向数组,故而名为数组指针。

我们知道只要是指针就需要初始化,如果不初始化就会变成“野指针”,那么数组指针应该如何初始化呢?我们知道一个整形指针的初始化是将一个整形变量的地址给它:

int a = 5;
int* p = &a;

那么对应的,数组指针初始化应该就是将一个数组的地址传给数组变量,那有人会想:数组名不就是一个地址嘛,直接让int (*p)[5]=arr应该就可以了吧,不不不,大错特错。我们要清楚的了解arr,&arr,和&arr[]之间的区别。数组名确实是一个指针,但是它指的是数组的首元素地址,而这时候我们要给数组指针的是一个完整的数组,所以我们需要给它&arr:

int arr[5] = {0};
int(*p)[5] = &arr;

我们能看到&arr和p是一模一样的。所以数组指针变量用来存放数组的地址,并且能够指向数组的数据。

五、二维数组传参

其实上面在指针数组那一段中就出现了二维数组传参,这里我们更细致的讲解一下二维数组的传参到底该怎么用,该怎么写。在我们理解了指针数组数组指针后我们能够更好地理解二维数组的传参。比如我们先定义一个三行三列的整型数组,那么就需要定义一个数组arr[3][3],用图片表达出来就像这个样子:

\012
0123
1456
278

9

那除了arr[3][3]这种写法,我们再往回看一下刚刚讲到的指针数组的写法:int* 数组名[元素个数],并且再来看一下我们刚刚写的一段用指针数组模拟的二维数组的代码:

    int arr1[] = { 1,2,3 };
	int arr2[] = { 4,5,6 };
	int arr3[] = { 7,8,9 };
	int* parr[3] = { arr1, arr2, arr3 };

通过这段代码我们不难看出,指针数组模拟二维数组时将二维数组分成了三个一维数组,而每一个一维数组代表每一行元素,接着我们来看这三个一维数组,每一个一维数组的类型都是int [3],而将int [3]以int* 数组名[元素个数]的形式来表示就是这样:int (*)[3]。这代表二维数组传参和指针数组传参一样,都是传递地址,所以用指针作为形参来传参也是可以的。写成代码是这样的:

void Print(int(*arr)[3], int x, int y)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < x; i++)
	{
		for (j = 0; j < y; j++)
		{
			printf("%d ", *(*(arr + i) + j));//表示第i行第j列的元素
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
	Print(arr, 3, 3);
	return 0;
}

六、函数指针变量


通过上面学习的数组指针变量来看,不难猜出:

函数指针变量也是一种指针变量,作用是存放函数的地址,并且通过地址能调用对应的函数。可能有人看到这里,心里会想:函数也有地址吗?听过整形变量有地址,听过字符型变量有地址,甚至听过指针变量也有地址,但也没听过函数的地址呀。这次你就听到了~其实只要你把东西写进了代码中,那计算机就会为其分配新的空间。并且有关函数的地址,在之前有一篇关于函数递归的文章中我们就已经提到过了:我们每次递归一个函数,这个函数再次被调用,计算机就会为这个函数分配新的空间。通过函数递归的知识也变相证明了函数也是由地址的。多说无益,让我们用代码来尝试一下,看看函数的地址到底长什么样。

int Add(int a,int b)
{
	return a + b;
}
int main()
{
	printf("%p\n", &Add);
	printf("%p\n", Add);
	return 0;
}

编译结果是:
 

我们会发现&Add和Add是一样的,这也就证明了函数是有地址的,并且函数名就代表了函数的地址。那么既然要了解函数指针变量,还是先从函数指针变量的创建上开始。我们知道整型数组指针的定义格式为int(*)[ ],函数指针变量也和数组指针变量相似,但是由于函数的调用需要传参,所以整型函数指针变量的定义格式为:int(*函数名)(参数,参数)。让我们举个例子:还是拿加法函数Add来做示范:

int Add(int a,int b)
{
	return a + b;
}
int main()
{
	int a;
	int b;
	scanf("%d %d", &a, &b);
	int (*padd)(int, int) = Add;//也可以写成int a,int b 但也可以省略
	printf("%d", (*padd)(a,b));
	return 0;
}

由这段代码中我们能了解到,关于如何创建函数指针padd,并且如何通过函数指针padd来调用指针对应的函数。

七、函数指针数组

顾名思义,函数指针数组是一个数组,用于存放函数的指针(地址),并且可以通过对数组的访问来使用存在函数指针数组中的函数

对于函数指针数组的定义,其实并不难,也并不需要过多的考虑,我们知道如果定义一个整型变量格式是int a;那么定义整型数组格式就是int arr[]。那我们还知道函数指针变量的定义格式为:int(*函数名)(参数),那么我们把它看成一个元素,定义函数指针数组的格式就是:int(*函数名[元素个数])(参数)。对于函数指针数组的使用,我们可以这样来操作:

int Add(int a, int b)
{
	return a + b;
}
int Sub(int a, int b)
{
	return a - b;
}
int Mul(int a, int b)
{
	return a * b;
}
int Div(int a, int b)
{
	return a / b;
}
int main()
{
	int (*p[4])(int, int) = { Add,Sub,Mul,Div };
	int a;
	int b;
	scanf("%d %d", &a, &b);
	printf("%d", (*p[0])(a,b));
	return 0;
}

这样就成功的将加减乘除四个函数存放到函数指针数组p里面去了,并且通过*p[0],*p[1],*p[2],*p[3]来调用相应的加减乘除这四种算法。通过这段代码确实能够看懂函数指针数组的定义以及用法,但并不能充分的了解函数指针数组的作用,有人可能会觉得调用函数指针数组来使用加减乘除四个函数,还不如直接调用函数来的实在。那我们接着把这个加减乘除函数制作成一个计算机的代码,然后通过计算机代码让大家了解函数指针数组的重要性。

int Add(int a, int b)
{
	return a + b;
}
int Sub(int a, int b)
{
	return a - b;
}
int Mul(int a, int b)
{
	return a * b;
}
int Div(int a, int b)
{
	return a / b;
}
int main()
{
	int a;
	int b;
	int k = 0;
	int num = 0;
	int m = 1;
	do
	{
		printf("***********************\n");
		printf("**** 1.Add   2.Sub ****\n");
		printf("**** 3.Mul   4.Div ****\n");
		printf("******** 0.quit *******\n");
		printf("***********************\n");
		printf("请选择:>");
		scanf("%d", &k);
		switch (k)
		{
		case 1:
			printf("请输入两个运算数:");
			scanf("%d %d", &a, &b);
			num = Add(a, b);
			printf("num = %d\n", num);
			break;
		case 2:
			printf("请输入两个运算数:");
			scanf("%d %d", &a, &b);
			num = Sub(a, b);
			printf("num = %d\n", num);
			break;
		case 3:
			printf("请输入两个运算数:");
			scanf("%d %d", &a, &b);
			num = Mul(a, b);
			printf("num = %d\n", num);
			break;
		case 4:
			printf("请输入两个运算数:");
			scanf("%d %d", &a, &b);
			num = Div(a, b);
			printf("num = %d\n", num);
			break;
		case 0:
			printf("退出游戏");
			break;
		default:
			printf("选择错误,重新选择:\n");
			break;
		}
	} while (k);
	return 0;
}

这样就成功的创造出了一个用四个函数制作成的计算机,可以进行加减乘除四种运算并且可以选择开始和退出。虽然这个程序运行起来没有什么问题,但是我们仔细观看会发现,在switch选择语句中的case1,case2,case3,case4中一共只有四行代码,有三行都是重复的,这样就大大降低代码的效率,并且使代码也过多的冗余,导致代码非常长。那么这个时候就需要我们使用函数指针数组了~我们可以把四个函数都存放在函数指针数组中,通过输入的数字调用对应的函数,这样就能够实现用一段代码来替换这四段代码

int Add(int a, int b)
{
	return a + b;
}
int Sub(int a, int b)
{
	return a - b;
}
int Mul(int a, int b)
{
	return a * b;
}
int Div(int a, int b)
{
	return a / b;
}
int main()
{
	int a;
	int b;
	int k = 1;
	int (*p[5])(int x, int y) = { 0,Add,Sub,Mul,Div };
	do
	{
		printf("***********************\n");
		printf("**** 1.Add   2.Sub ****\n");
		printf("**** 3.Mul   4.Div ****\n");
		printf("******** 0.quit *******\n");
		printf("***********************\n");
		printf("请选择:>");
		scanf("%d", &k);
		if (k >= 1 && k <= 4)
		{
			printf("请输入两个运算数:");
			scanf("%d %d", &a, &b);
			printf("结果为:%d\n", p[k](a, b));
		}
		else if (k == 0)
		{
			printf("退出计算器");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}
	} while (k);
	return 0;
}

这段代码使代码量大大减少,也能够更具像的体现出函数指针数组的重要性和作用。

练习题:

字符串旋转

写一个函数,判断一个字符串是否为另外一个字符串旋转之后的字符串。

例如:给定s1 =AABCD和s2 = BCDAA,返回1,给定s1=abcd和s2=ACBD,返回0。

AABCD左旋一个字符得到ABCDA

AABCD左旋两个字符得到BCDAA

AABCD右旋一个字符得到DAABC

思路:等你读到这里应该已经对指针和数组之间的关系掌握了不少了,利用指针与数组和数组传参的知识解题吧~我们可以知道一个字符串左旋是将第一个字符放到最右边,然后其余字符依次往前移一位,那么我们可以先定义一个字符型变量用来接收第一个字符,然后将所有其余字符向前移动一位,最后再把第一个字符利用接收的字符型变量放在最后一位,而右旋只是改变了转移的字母,两者其实差别不大~都可以用同一个思路。

int Print(char* arr1, char* arr2, int sz)
{
	int i = 0;
	int j = 0;
	for (j = 0; j < sz ; j++)//判定左旋
	{
		char tmp = *arr1;
		for (i = 0; i < sz - 1; i++)
		{
			*(arr1 + i) = *(arr1 + i + 1);
		}
		*(arr1 + i) = tmp;
		if (strcmp(arr1, arr2) == 0)
			return 1;
	}
	for (j = 0; j < sz; j++)//判定右旋
	{
		char tmp = *(arr1 + sz - 1);
		for (i = sz - 1; i > 0; i--)
		{
			*(arr1 + i) = *(arr1 + i - 1);
		}
		*arr1 = tmp;
		if (strcmp(arr1, arr2) == 0)
			return 1;
	}
}
int main()
{
	char arr1[] = "AABCD";
	char arr2[] = "BCDAA";//左旋可得
	char arr3[] = "DAABC";//右旋可得
	char arr4[] = "ABBCD";//得不到
	int sz = strlen(arr1);
	if ((Print(arr1, arr2, sz)) == 1)
	{
		printf("YES\n");
	}
	else
	{
		printf("NO\n");
	}
	if ((Print(arr1, arr3, sz)) == 1)
	{
		printf("YES\n");
	}
	else
	{
		printf("NO\n");
	}
	if ((Print(arr1, arr4, sz)) == 1)
	{
		printf("YES\n");
	}
	else
	{
		printf("NO\n");
	}
	return 0;
}

注:有人可能将右旋的代码写成了以下的格式:

for (j = 0; j < sz; j++)//判定右旋
	{
		char tmp = *(arr1 + sz - 1);
		for (i = 0; i < sz - 1; i++)
		{
			*(arr1 + i + 1) = *(arr1 + i);
		}
		*arr1 = tmp;
		if (strcmp(arr1, arr2) == 0)
			return 1;
	}

看似正确,也同样和左旋是一样的方法,但是!右旋是将所有字母都向后移一位,如果还让每一个字母的下一位等于上一位,比如AABCD,第一次转换变成AABCD,第二次就变成了AAACD,第三次变成AAAAD,所以我们需要转变一下思路,让移动从右往左~(其实还有使用strcpy,strcat的方法,更加的简便轻松,但是这不是我们这次要讲的东西,那么欲知后事如何,请听下回分解啦,我们下次再见~~~)

  • 27
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值