目录
一,初级指针回顾
1,首先,我们得认识到,指针的本质还是一个变量,只不过这个变量存放的是地址。在我们的计算机中,内存会划分为很多小的存储单元,而每一个存储单元都会有自己独有的内存编号,这个内存编号就是这块内存单元的地址。所以,内存编号 = 指针 = 地址。
2,指针的大小是固定的,不会取决于它所指向的变量的类型,指针变量的大小只与操作系统的种类有关。在32位操作系统下,指针是4个字节的大小,在64位操作系统下,指针是8个字节的大小,只会有这两种情况。
3,指针是有类型区分的,例如 int *p,这里定义的就是一个整形指针变量。而指针的类型,也就决定了它在做加减整数移位操作时移动的步长,p + 1,这个时候加一,移动的就是4个字节,类型不同,移动的字节长度也就不同。同时在进行解引用时,他的访问权限也是不同,int型的指针也就是只能访问4个字节的空间。
4,指针的运算:指针 +- 整数, 指针 - 指针 ,指针的关系运算。
二,进阶指针
1,字符指针
1.1,基本定义与用法
定义:char* p,这里的p就是一个字符指针,指向的是一个字符。
一般使用:
#include<stdio.h>;
int main() {
char ch = 'w';
char* pstr = &ch;
*pstr = 's';
printf("%c\n", ch);
return 0;
}
定义字符后,将字符的地址取出赋给pstr这个字符型指针,这个时候,这个字符型指针指向的就是ch这个字符变量的存储空间,所以后面*pstr取值进行修改是能够达到修改的目的的。
其他用法:
const char* p = "abcdef";
//*p = 'w';
printf("%c\n", *p);//输出a
printf("%s\n", p);//输出abcdef
1,对于上面的字符型指针,我们可能会疑惑为什么会用来接受一个字符串,但实际不是的,这个时候指针p实际指向的是这个字符串的首字母的地址,也就是a的地址,也就是说你通过指针p来访问的话,来取出p的值就只是a,相当于把a这个字符的地址给了指针。并且换一种理解方式来说,这个char型的指针也就只有四个字节(32位系统),不可能放的下七个字节的字符串。
2,另外,这个字符串也是不能够修改的,这是一个常量字符串,不可修改,并且我们还会再用const来修饰这个指针,也就是一个常量指针,指针指向的内容不能够被修改。(这里常量字符串本身就是不能修改的,要在前面加一个const是因为在新版本的编译器中是必须要加的,不然会报错。)
3,虽然指针指向的是字符串的首字母,但由于这个字符串在常量区是连续存储的,所以我们用%s 进行输出字符串,然后将指针p传过去,也就是首字符的地址,是能够进行字符串的输出的。
这里用字符指针指向字符串,和用int型的指针指向一个整形数组是一个道理,这个整形指针指向的也只是首元素的地址,也就相当于数组的地址。
1.2,使用细节
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char arr1[7] = "abcdef";
char arr2[7] = "abcdef";
if (p1 == p2) {
printf("p1 = p2\n");
}
else {
printf("p1 != p2\n");
}
if (arr1 == arr2) {
printf("arr1 = arr2\n");
}
else {
printf("arr1 != arr2\n");
}
输出结果:p1 = p2,arr1 != arr2
分析:如草图所示,对于字符指针p1 ,p2 来说,他们指向的是相同的常量字符串的首字母,因为这个字符串不可变且唯一,所以他在内存中的地址是唯一的,所以p1与p2是相等的,指向同一个地址也即是字符串首字母a的地址。但是对于数组arr1 与 arr2 而言,他们是两个不同的数组,在定义的时候两个数组开辟的空间地址是不一样的,对于他们来说,只是凑巧里面保存的值是一样的,除此之外,二者没有任何的关联,是两个不同的东西。
2,指针数组
2.1,基本定义及其使用
定义:指针数组:用来存放指针的数组就叫做指针数组。
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
for (int i = 0;i < 3;i++) {
printf("%d ", *(arr[i]));
}
在定义数组时,int* 表示的是数组的类型,也就是里面存放的元素的类型,指针类型。要拿到具体的值,同样是遍历,先拿到地址,然后再解引用就可。
int arr1[3] = { 1,2,3 };
int arr2[3] = { 2,3,4 };
int arr3[3] = { 3,4,5 };
int* parr[3] = { arr1,arr2,arr3 };
for (int i = 0;i < 3; i++) {
for (int j = 0;j < 3;j++) {
printf("%d ",parr[i][j]);
//printf(printf("%d ",*(parr[i] + j)))
}
printf("\n");
}
对于上面的指针数组parr来说,其实和二维数组类似了,因为它里面的元素都是指向数组的指针,代表着三个不同的数组。arr1,arr2,arr
3三个数组的数组名作为指针数组的元素,他们分别代表着各自数组首元素的地址,所以只要先通过指针数组parr拿到各个数组的首元素地址,然后在此基础上加上整数,就可以遍历得到数组元素的具体值。
有一个点需要注意,就是在遍历数组的时候,如何将数组元素进行输出的问题:
parr[i][j] = *(parr[i] + j)
这里可能会有很多人不懂为什么这二者是等价的,其实对于数组来说,其 [ ] 的实际作用就是解引用,parr[i] 我们先是拿到了首元素的地址,然后向后遍历加 j ,就能得到各个元素的地址,再将其解引用就能得到具体的值。从根本上说,两种方法不管你怎么写,其实计算机它最终都会转化到*(parr[i] + j)这种输出的方法上进行计算输出。
3,数组指针
3.1,基本定义
定义:用来指向数组的指针,其本质是一个指针。
注意:此时数组指针指向的是整个数组,不能说是指向数组首元素的地址。
int* arr1[10]
int(*p)[10]
注意区分一下上面的两种定义,第一个是指针数组,第二个是数组指针
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。 //这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
3.2,&数组名与数组名
int a = 10;
int* p = &a;
int arr[10] = { 0 };
//数组名是首元素地址
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
p是指针类型,所以在输出的时候,%p就可以控制输出地址类型的数据
输出结果看出,上面的三种方式求地址好像没有什么区别,我们知道数组名就表示数组首元素地址,所以arr与&arr[0]这二者肯定是没有任何区别的。但是对于&arr来说,在这里你看结果是一样,但其实它所表示的意思是不相同的,它代表的是整个数组的地址,我们在后面加个一就能看出区别来。
printf("%p\n", arr);
printf("%p\n", arr + 1);
printf("%p\n", &arr[0]);
printf("%p\n", &arr[0] + 1);
printf("%p\n", &arr);
printf("%p\n", &arr + 1);
我们上下对比输出可以看出,对于第一二种方法来说,加1就是后移访问数组的下一个元素,所以移动的是一个int的字节。但是第三种我们加1后它移动的是40个字节,也就是一整个数组的大小,所以对于&arr来说,它代表的是一整个数组的地址,在后面加上整数就是以整个数组的大小为单位移动的,而不是首元素。
那么既然&arr指向的是整个数组,就可以解释定义的来源了。int(*p)[10] = &arr,&arr的类型就是数组指针类型,即int(*)[10],其中10是不能省略的,它明确的指出了我们这个指针指向的数组的大小。同时,&arr[0],arr的类型就是int*。
小练习:请问p的类型是什么?
char* arr[5]
p = &arr //完整正确的定义为 char* (*p)[5] = &arr
首先arr是一个指针数组,其中每个元素的类型都是char*,所以&arr将整个数组的地址赋给p,p的类型就是一个数组指针,具体为 char* (*)[5],前面的char*表示的是数组的类型。
总结,数组名的理解情况:
除了上述两种情况之外,所有的数组名表示的都是首元素的地址。
3.3,数组指针的使用
1,用于一维数组(不推荐,小题大做,绕弯)
int arr[3] = { 1,2,3 };
int(*p)[3] = &arr;
for (int i = 0;i < 3;i++) {
printf("%d\n", *(*p + i));
}
分析:这里主要需要理解的就是*(*p + i),p是一个数组指针,它所指向的是整个数组的地址,所以我们对其解引用*p,就相当于拿到了整个数组,拿到整个数组,就相当于拿到了数组名,因为数组名代表这个数组,所以有了数组名就知道了首元素地址,后面的工作也就是顺理成章的事。
2,用于二维数组(重要)
void printf1(int (*p)[5], int c, int r) {
for (int i = 0;i < c;i++) {
for (int j = 0;j < r;j++) {
//printf("%d ",*( * (p + i) + j ));
printf("%d ", p[i][j]);
}
printf("\n");
}
}
int main() {
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
//写一个函数输出这个二维数组
int sz1 = sizeof(arr) / sizeof(arr[0]);
int sz2 = sizeof(arr[0]) / sizeof(arr[0][0]);
printf1(arr,sz1,sz2);
return 0;
}
首先,计算数组长度的方法:整个数组的大小除以单个元素的大小,就能够得到数组的长度。注意,数组的大小和长度是不一样的,不能把两者混为一谈。
首先看上面的代码,可能会有疑惑,数组指针接受的不应该是整个数组的地址吗,为什么这里传入的是arr,也就是这个二维数组的首元素地址呢?其实,本质是一样的,因为在二维数组里面,每一个元素都是一个一维数组,所以这里的首元素地址,也就是一整个一维数组的地址,所以数组指针p接受的也是整个数组的地址(降维了)。当我们拿到二维数组第一个元素的地址后,这个时候指针p加上整数,就是直接跨过的一整个一维数组元素。*( * (p + i) + j ),指针p代表整个一维数组的地址,对其解引用就得到了一维数组的首元素地址,在后面加上j循环遍历,就能够输出二维数组的每一个一维数组的具体元素值。
4,数组传参,指针传参
4.1,一维数组传参
对于一维数组传参而言,在形参接收的时候,接收的是数组名,也就是首元素的地址,所以在本质上我们应该是用指针来接收。这里用数组来接收只是为了初期方便我们的理解,而且有一点很重要就是我们数组做形参接收的话,实际上是不会真正创建一个数组的,所以这个形参数组的大小你可以不写甚至随便写。其实在底层上,编译器最终还是会把它转成一个指针来计算。
4.2,二维数组传参
对于二维数组传参而言,其实和一维数组差不多,也有两种写法,这里有几个要注意的点,首先形参的二维数组的行可以不写,但是列必须要写出,因为二维数组的每行其实是连续存储的,你不写出列数,就相当于你不知道它每一行的结束位置。然后就是传首元素地址要用数组指针来接收。
4.3,一级指针传参
#include <stdio.h>
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
一级指针传传参,比较简单,可以用来接收一个变量的地址,也可以直接接收别的一级指针变量,也可以接收一维数组首元素的地址。一级指针之间互指,其实就是将其中一个指针里面存放的地址给了另外的一级指针,两个指针指向的都是同一个地址。
4.4,二级指针传参
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok? 是可以的
return 0;
}
二级指针传参,可以用来接收一级指针的地址,也可以二级指针之间互指,还可以用来接收指针数组首元素的地址(解释一下,指针数组也就是以指针作为数组元素的数组,比如上面的arr,他的每一个元素都是一个char*的类型,那么你传首元素地址就相当于是传char*的地址,也即是一级指针的地址,所以可以用二级指针来接收)
5,函数指针
定义:函数指针,顾名思义,就是指向函数的指针。
5.1,函数的地址
在这里要注意了,对于函数的地址而言,上面的两种写法是一个意思,没有任何区别,都表示函数的地址。
5.2,通过函数指针调用函数
int test(int x,int y) {
return x + y;
}
int main() {
int a = 1;
int b = 1;
int (*p)(int, int) = test;
//int ret = (*p)(a, b);
int ret = p(a, b);
printf("%d\n", ret);
return 0;
}
一般而言,我们对这个函数指针解引用就可以拿到这个函数,然后传参就可以调用了。但是我们发现既然可以直接把函数名赋给函数指针,那么他们的作用其实就是差不多的,所以编译器简略语法后,加不加那个解引用的* 都是一样的。
6,函数指针数组
6.1,基本定义
定义:用来存放函数指针的数组就是函数指针数组
作用:当作一个跳板,可以由数组元素访问到函数,在某些情况下大大简化了函数调用的过程。(转移表)
6.2,函数指针数组的使用
简易计算器的实现:
#include<stdio.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("====1,加法操作====\n");
printf("====2,减法操作====\n");
printf("====3,乘法操作====\n");
printf("====4,除法操作====\n");
printf("====0,退出操作====\n");
printf("==================\n");
}
int main() {
int (*p[5])(int, int) = { 0,ADD,Sub,Mul,Div };
int sz = sizeof(p) / sizeof(p[0]);
int ret = 0;
do {
int a = 0;
int b = 0;
menu();
printf("请输入您想要进行的计算操作>>>");
scanf("%d", &ret);
if (ret == 0) {
printf("退出成功!");
break;
}
else if (ret > 0 && ret < sz) {
printf("请输入两个操作数>>>");
scanf("%d %d", &a, &b);
printf("计算结果为 %d\n", p[ret](a,b));
}
else {
printf("输入错误!");
break;
}
} while (ret);
return 0;
}
运用函数指针数组之后你会发现主函数部分大大精简了,并且添加功能只需要定义好函数,然后把它的地址放到函数指针数组里面就完事了,相对于switch实现,函数指针数组的效率明显更高。
好了,今天的补充就这么多了,感谢大家的观看,后面还有一点会继续补充,希望大家能继续关注,谢谢!有错误也希望大家帮忙指出。