序言
重点说一下指针数组和数组指针以及数组与指针的关系,内容较多,要是有缺漏,后面后补加上来
指针数组
前面我们分析过.本质是一个数组,和字符数组、整型数组等一样,里面存储的不过是指针罢了
#include<stdio.h>
#include<windows.h>
int main()
{
int a = 10;
int b = 10;
int c = 10;
int*parr[3] = {&a,&b,&c};//parr就是一个指针数组,3可以省略
for(int i = 0;i<3;i++)
{
printf("*arr[%d] == %d\n",i,*parr[i]);
}
system("pause");
return 0;
}
总结一下,挖掘重点
- parr去掉,int*[ ],' [ ] '的优先级高所以先结合,是一个数组
- int*表示存储的是指针,指针的类型是整型
数组指针
首先我们先给出结论 数组指针是一个指针,就像好孩子一样,本质是孩子,修饰语是“ 好 ”
- 数组指针是一个指针
- 数组指针指向一个数组,就像整型指针指向一个整型一样
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[10] = {0};
int(*p) [10] = &arr; //p就是一个数组指针
system("pause");
return 0;
}
我们可以看出,p先和*结合,表明是一个指针.p和[]结合,表明该指针指向向一个数组,10表示里面存储10个元素,每一个元素都是int类型.
- (* )这里表明p是一个指针
- int[10]表明这个指针指向的是一个数组,数组是一个整形数组,里面有10个元素
注意:这里的10一定不可以省略,不然会报错
指针与数组
我们都对数组很是了解,对一些指针的的基础知识也不陌生,今天我们就仔细的看看他们之间有什么关系.这里也回答一下指针的另一个左右简化代码.
一维数组
先从简单的开始吧,我们从现象中寻找结论.我们先看一下数组名和首元素地址的关系
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[10] = {0};
printf("%p\n",arr);
printf("%p\n",&arr);
printf("%p\n",&arr[0]);
system("pause");
return 0;
}
所以说,一维数组名的地址就是整个数组的地址,我们也可p和arr是一样的
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p = arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
system("pause");
return 0;
}
&数组名和数组名
这里我们需要重点谈了,上面我们发现&数组名和数组名再数值上是一样的,那么它们的含义一样吗?
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
system("pause");
return 0;
}
我们发现再数值上是一样的.我们对数组取地址,那么我们接受的指针应该是一个数组指针,它+1跳过是整个数组的长度.
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
printf("==============\n");
printf("%p\n", arr + 1);
printf("%p\n", &arr + 1);
system("pause");
return 0;
}
这里给大家总结一下,&数组名表示是数组的地址,数组名表示是首元素的地址,它们只是恰巧在数值上相等罢了.
那么这里我们就有点疑惑,我们如何接受&数组名,这里就用到我们前面的数组指针.
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
return 0;
}
那么这里有一个问题,我们如何通过数组的地址拿到数组的元素呢,下面的就可以.*p表示我们得到数组首元素的地址,+i表示得到数组底i的地址,后面再解引用一下.
int main()
{
int arr[10] = { 0 };
int(*p)[10] = &arr;
for (int i = 0; i < 10; i++)
{
printf("%d ", *((*p) + i));
}
return 0;
}
这里我们需要观察sizeof(数组名)的值,后面的专题部分会专门谈到.
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
printf("%d\n", sizeof(p));
printf("%d\n", sizeof(arr));
system("pause");
return 0;
}
- 首元素地址和数组名、数组的地址相同
- sizeof(数组名)求得是整个数组所开辟的空间,sizeof(p)求的是一个元素的大小,这里后面的面试题重点谈
数组传参
我们把数组作为参数在实际中也是很常见的,我们都知道形参是实参的一份临时拷贝,试想一下,如果我们传入的是数组,编译器把这个数组的元素都给拷贝的一份,这样的话对空间的要求不是很大吗,尤其那些我们只是传过去看看数组里面的元素,不做其他的动作,这样全部拷贝的做法实在是太傻了.所以在实际中,我们数组传参,会退化.数组会退化成一个指针,也就是我们拷贝的是一份指针,也就是4/8个字节,这样大大提高了效率.
#include<stdio.h>
#include<windows.h>
void Print1(int arr[],int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
void Print2(int* parr, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", parr[i]);
}
}
int main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sz = sizeof(arr) / sizeof(arr[0]);
Print1(arr,sz);
printf("\n");
Print2(arr,sz);
system("pause");
return 0;
}
二维数组
二维数组相比较于一位有点困难.首先我们都明白,二维数组在逻辑上可以说是一个一位数组,只不过一维数组的元素也是一个一维数组而已.
当然,这个是我们逻辑上这么认为的,在在实际物理内存中是这样的,这就是我们二维数组列的大小不能省略的原因,它标识这二维数组的一行我们可以4几个元素,后面有大用.
&数组名和数组名
首先我们确定一点就是,数组的地址在数值上就是就是首元素的地址,这句话是对的,不过有一点的考究,二维数组的首元素是什么
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[4][4] = { 0 };
printf("%p\n", arr[0]);
printf("%p\n", arr);
printf("%p\n", &arr[0][0]);
system("pause");
return 0;
}
这里和大家说一下,和二维数组数组一样,数组名就是首元素的地址,&数组名是真正数组的地址,只不过在数值上相等罢了.
int main()
{
int arr[4][4] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", &arr[0][0]);
printf("===============\n");
printf("%p\n", arr+1);
printf("%p\n", &arr + 1);
printf("%p\n", &arr[0][0] + 1);
system("pause");
return 0;
}
我先来看一下&数组名应该如何做.
int main()
{
int arr[4][4] = { 0 };
int(*p)[4][4] = &arr;
system("pause");
return 0;
}
在来谈一下首元素的地址,我们知道,数组名是首元素的的地址,按照逻辑上上而言,二维数组的首元素是一个指针,该指针指向一个数组,也就是首元素是一个数组指针.
int main()
{
int arr[4][4] = { 0 };
int(*p)[4] = arr;
system("pause");
return 0;
}
既然是一个函数指针,那么数组名+1应该跳过一个一维数组的大小
#include<stdio.h>
#include<windows.h>
int main()
{
int arr[4][4] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr[1]);
printf("%p\n", arr+1);
system("pause");
return 0;
}
二维数组传参
这个就比较简单的了,我们知道了二维数组的数组名是一个数组指针,这里我们可以用方法二接受二维数组
#include<stdio.h>
#include<windows.h>
// 方法一
void Print1(int arr[4][4])
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
}
}
// 方法二
void Print2(int(*p)[4])
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d ", *((*(p + i)) + j));
}
}
}
int main()
{
int arr[4][4] = { { 1, 2, 3, 4 } };
Print1(arr);
printf("%\n");
Print2(arr);
system("pause");
return 0;
}
至于*(*(p + i)) + j),我们也分析一下,p+i表明我们在二维数组的第i行,*(p+i)表明我们得到第i行的第一个元素的地址,加上j表示得到第i行第j列的元素的地址,我们最后解引用一下.这里可以令i = j = 0.这就是((*(p + 0)) + 0) 与 arr[0][0]关系
- p+0 就是p 指向arr的 首元素的
- *(p+0) 就是 arr[0]
- *(p + 0)) + 0 指向arr[0]+0
- 那么((*(p + 0)) + 0)就是arr[0][0]
函数指针
在写这个之前,我们先做一下铺垫
函数有地址吗
我们这里有点疑惑,函数也存在地址吗,如果存在,它们的引用是什么呢?
void test()
{
printf("hehe");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
看来函数是有地址的,而且和数组类似,&函数名 和函数名的地址一样,这里我们把这两个方法看作是一样的
如何定义函数地址
这个很难用文字只管描述出来,我们是例子来辅助
int Add(int x, int y)
{
return x + y;
}
int main()
{
int(*pf1)(int, int) = Add;
int(*pf2)(int, int) = &Add;
printf("%d\n", pf1(1, 2));
printf("%d\n", (*pf1)(1, 2));
printf("%d\n", (*pf2)(1, 2));
return 0;
}
这里我们得出一些个结论
- *pf1 和 pf1是一样的
- &函数名和函数名类型是一样的
我来解释一下int(*pf1)(int, int) = Add;
- pf1先和*结合,表明pf1是一个指针
- 其次和后面的(int,int)中的()结合,表明pf1指向的是一个函数
- (int,int)表名函数的参数有两个,都是int型
- 最前面的int表示返回值是int型
这里我们说一下啊函数指针的应用,要是前面的用法就有点挫了.我们实现一个简单的计算器.
#include<stdio.h>
#include<windows.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("************************\n");
printf("*** 0. exit ***\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4 Div ***\n");
printf("************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
do
{
menu();
printf("请选择: > ");
scanf("%d", &input);
switch (input)
{
case 0:
printf("已退出\n");
break;
case 1:
printf("请输入两个数: ");
scanf("%d %d",&x, &y);
printf("%d\n", Add(x, y));
break;
case 2:
printf("请输入两个数: ");
scanf("%d %d", &x, &y);
printf("%d\n", Sub(x, y));
break;
case 3:
printf("请输入两个数: ");
scanf("%d %d", &x, &y);
printf("%d\n", Mul(x, y));
break;
case 4:
printf("请输入两个数: ");
scanf("%d %d", &x, &y);
printf("%d\n", Div(x, y));
break;
default:
printf("请重新选择\n");
break;
}
} while (input);
return 0;
}
上面的代码你不会觉得很冗余吗,尤其是在case里面的那部分,我们是不是可以把它封成一个函数,传入函数的地址就可以了.这样的话可以大大简化代码.
#include<stdio.h>
#include<windows.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("************************\n");
printf("*** 0. exit ***\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4 Div ***\n");
printf("************************\n");
}
void calculator(int(*pf)(int,int))
{
int x = 0;
int y = 0;
printf("请输入两个数: ");
scanf("%d %d", &x, &y);
printf("%d\n", (*pf)(x, y));
}
int main()
{
int input = 0;
do
{
menu();
printf("请选择: > ");
scanf("%d", &input);
switch (input)
{
case 0:
printf("已退出\n");
break;
case 1:
calculator(Add);
break;
case 2:
calculator(Sub);
break;
case 3:
calculator(Mul);
break;
case 4:
calculator(Div);
break;
default:
printf("请重新选择\n");
break;
}
} while (input);
return 0;
}
我们来阅读下面的两个代码,你将会看到函数指针的"魅力".
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
(*(void (*)())0)();()0表示强制类型转换,转换的类型是void (*)().这是一个函数指针,没有参数没有返回值.也就是说这个代码是这样理解的,我们把地址为0 的地方转换成函数,这个函数没有参数和返回值
void (*signal(int , void(*)(int)))(int);这个就比较难理解了,signal是一个函数,它存在两个参数,一个是int,另一个是一个函数指针void(*)(int).signal函数的返回值还是一个函数指针void(*)(int)).
你会发现函数指针实在是太难了,这里我们不要求精通,只需要了解,可以分辨出是什么样的函数指针就可以了.
函数指针数组
我们都知道数组是相同类型元素的集合,既然函数指针也是一种指针类型,那么它也可以构成数组,不过要求他们参数和返回类型一摸一样
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int(*pf[4])(int, int) = { Add,Sub,Mul,Div };
return 0;
}
这里我也解释一下,int(*pf[4])(int, int) = { Add,Sub,Mul,Div };
- pf先和[4]结合,表明是一个数组,数组里面有4 的元素
- 去掉pf[4],得到int(* )(int, int) ,这是一个函数指针,所以说4个元素每个都是是一个函数指针
这里我还是以计算器为例子.这个就比较简单了.
#include<stdio.h>
#include<windows.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void menu()
{
printf("************************\n");
printf("*** 0. exit ***\n");
printf("*** 1.Add 2.Sub ***\n");
printf("*** 3.Mul 4 Div ***\n");
printf("************************\n");
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int (*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };//pfArr是一个函数指针的数组,也叫转移表
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
break;
}
else if (input >= 1 && input <= 4)
{
printf("输入2个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("ret = %d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
指向函数指针数组的指针
首先说一下,我也不想套娃,不过知识点在这,我也没办法,这是最后一个了.
int main()
{
int(*pf[4])(int, int) = { Add,Sub,Mul,Div };
int(*(*ppf)[4])(int, int) = &pf;
return 0;
}
解释吧 int(*(*ppf)[4])(int, int) = &pf;
- ppf和*结合,标明是指针
- 去掉ppf,得到的 int(*[4])(int, int)表示是一个数组
回调函数
回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数.回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应.这里我们先不谈了,等到后面我有时间了补上.
指针例题
我们先来看一下指针常见的.
sizeof(数组名) & sizeof(p)
我们知道sizeof(数组名)是计算该数组占据的所有字节,这里我们就有下面的问题了.我们在32为平台下测试
这里我们简单的下一个结论,只要数组名显式的和数字结合(显示主要是为了分辨0),这个里就要考虑指针的大小.要是只有数组名就是整个数组的大小,&数组名就是指针的大小
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(a+0));
printf("%d\n",sizeof(*a));
printf("%d\n",sizeof(a+1));
printf("%d\n",sizeof(a[1]));
printf("%d\n",sizeof(&a));
printf("%d\n",sizeof(*&a));
printf("%d\n",sizeof(&a+1));
printf("%d\n",sizeof(&a[0]));
printf("%d\n",sizeof(&a[0]+1));
这里还有测试一下字符数组
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", sizeof(arr)); // 6
printf("%d\n", sizeof(arr + 0)); // 4
printf("%d\n", sizeof(*arr)); // 1
printf("%d\n", sizeof(arr[1])); // 1
printf("%d\n", sizeof(&arr)); // 4
printf("%d\n", sizeof(&arr + 1)); // 4
printf("%d\n", sizeof(&arr[0] + 1)); // 4
return 0;
}
这里还有的,涉及到字符数组的sizeof,我们就好把它strlen进行比较.注意,strlen函数不管你是&数组名还是数组名,他就把参数当作一个指针,按照指针去找'\0'.有人可能会疑惑,如果我们传入&数组名,这个不就是数组指针吗?到时候+1可是跳整个数组,这里大家想一想strlen是记录什么的,它是计算字符串的大小,也就是函数的参数char*,这里会发生类型转换,所有前面的顾虑没必要.
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%d\n", strlen(arr));
printf("%d\n", strlen(arr + 0));
printf("%d\n", strlen(&arr));
printf("%d\n", strlen(&arr + 1));
printf("%d\n", strlen(&arr[0] + 1));
printf("%d\n", strlen(*arr)); // 这是一个 bug 野指针
printf("%d\n", strlen(arr[1]));
return 0;
}
二维数组相关的计算
注意,整个部分是有点苦困难的,我们需要把上面的结论给跟新一下,我们先来看现象,后面总结结论.
#include <stdio.h>
#include <string.h>
int main()
{
//二维数组
int a[3][4] = { 0 };
printf("%d\n", sizeof(a)); // 3*4 *4
printf("%d\n", sizeof(a[0][0])); // 4
printf("%d\n", sizeof(a[0])); // 4*4
printf("%d\n", sizeof(a[0] + 1)); // 4
printf("%d\n", sizeof(*(a[0] + 1))); // 4
printf("%d\n", sizeof(a + 1)); // 4 * 4
printf("%d\n", sizeof(*(a + 1))); // 4*4
printf("%d\n", sizeof(&a[0] + 1)); //
printf("%d\n", sizeof(*(&a[0] + 1))); // 4*4
printf("%d\n", sizeof(*a)); // 4
printf("%d\n", sizeof(a[3])); // 4*4
return 0;
}
我们需要解释一下,我们把a看作一个一位数组,sizeof(a)就是所有的空间只要a显示的加上一个数,就是指针的大小.
int main()
{
//二维数组
int a[3][4] = { 0 };
printf("%d\n", sizeof(a+0));
return 0;
}
但是一旦我们*(a+n)或者a[n]就可以看作一个一维数组的数组名,得到该行的大小.这两个规则和一位数组是一样的.
int main()
{
//二维数组
int a[3][4] = { 0 };
printf("%d\n", sizeof(*(a + 0)));
return 0;
}
类型转换
由于不同类型的指针+1跳过的字节数是不同的,这里就会出现一些强制类型转换的例题.
例题一: 这道题倒是挺简单的,跳过几个字节,我们看指针解引用占据几个字节
#include <stdio.h>
#include <string.h>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1); // &a 是一个数组指针,解引用是一个数组 +1跳过整个数组 后面又强制类型转换了
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
例题二:下面这道题,我们可以知道p解引用是20的字节的变量,所以+1跳过20个字节,
#include <stdio.h>
#include <string.h>
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
//假设p 的值为0x100000。 如下表表达式的值分别为多少?
//已知结构体类型的变量test的大小有20个字节
int main()
{
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1); // 强制类型转换 就变成了 3+1 = 4
printf("%p\n", (unsigned int*)p + 0x1); // 强制类型转换成 unsigned int*,解引用是4个字节,所以+1跳过4个字节
return 0;
}
例题三,这个又有点困难了.这里强制类型转换的更加厉害.
int main()
{
int a[4] = { 1, 2, 3, 4 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
return 0;
}
这道题涉及到三个知识知识点,我们分别讲述.首先就是大小端的问题,数据在内存中是如何存储的,我的电脑是小段机,存储数据按照"小小小"来存储.在说ptr2,我们首先明白指针指向第一个字节,强制类型转换后,跳过一个字节,又因为我们强制类型转换成int*,所以解引用访问4个字节,这就是我们得到下面结果的理由.
例题四 注意这是我们再一次涉及到二维数组,我们需要先分析一下.
p[0]和*(p+0)等价,这里面又等价于*(p).我们知道p是一个数组指针,数组指针解引用得到该数组首元素的地址,这里的地址是int.第二个要注意的是这里我们用的是逗号表达式,这里我们需要细心点.
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}
例题五:这个题很难,这里我在图片上解释了,这里就不浪费时间了
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
例题六 这个就比较简单了
int main()
{
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
例题七这个我要解释一下,不是太难,主要是一个思路的问题,我们需要*先和谁结合.char* pa = a;首先pa先和左面的临近它的*结合,表明它是一个指针,指向的是char*.我们加1跳过4个字节.
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
例题八,这个是我们这篇博客最后一个例题,来个看起比较麻烦的.等下我整体做一个总结.
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- *++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}
总结
指针是C语言里面的一个难点,我们需要细心的琢磨.遇到题不要慌,多画画图,一点点分析.这里要注意指针和数组的关系,一般的题就是按照这个思路出的.