前言
接下来的讲解部分是指针的进阶,包含多种指针以及对应的数组,这部分章节对我们来说很重要,也是c语言中的重点模块儿,重要性不言而喻
我们直接进入正题,开始我们今天重要的学习旅程吧😎😎😎
一、指针部分
1.字符指针:
在字符指针使用时,我们通常有两种使用的方式,前者是一般使用方式,后者是我们今天重点所讲部分
int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
定义一个字符变量存放字符,然后在取出字符的地址,存到字符指针当中去,我们后续可以通过解引用操作符对字符进行操作
int main() { const char* pstr = "hello bit."; printf("%s\n", pstr); return 0; }
下面这样的定义类型,其实只是将常量字符串的首字符地址放到了字符指针pstr中,而我们打印字符串使用%s时,其实也只需要将首字符地址传给printf函数,它会自动打印字符串直到\0结束
1.1相关的练习题
#include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char *str3 = "hello bit."; const char *str4 = "hello bit."; if(str1 ==str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 ==str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0; }
问题详解:
1.由答案我们可以看出str1和str2是不同的,这是为什么呢,其实答案很好理解的,因为我们创建了两个不同的字符数组(他们连名字都不相同,那这两个数组怎么可能相同啊???),那么他们在内存中的空间位置肯定是不同的,而数组名代表首元素地址,两个不同的数组的首元素地址肯定也是不相同啊,那么自然str1肯定和str2是不相同的啦
2.首先常量字符串要在内存中开辟空间存储它本身,那么我们有必要在内存中储存两个一模一样的东西吗?(c/c++会把常量字符串储存到单独的一个内存区域中) 当这两个指针指向同一个常量字符串时,实际上就是指向同一块儿地址**(指针就是地址,地址就是指针)**
2.数组指针
2.1数组指针的定义
1.数组指针嘛,那其实非常简单,就是指向数组的指针,数组有哪些组成部分呢?(有数组名,元素个数,元素类型),那我们写一个指针,让他指向数组就可以了
例如: int arr[10]={0}; int (*ps)[10]=&arr; //注意ps和[]的结合优先级较高,如果没有括号ps会先和[]结合,那样就不是指针了,变成数组了
2.2&数组名和数组名的对比
我们下面看一段代码,比较这两种操作的不同
#include <stdio.h> int main() { int arr[10] = {0}; printf("%p\n", arr); printf("%p\n", &arr); return 0; }
此时我们可以看出,&arr+1的地址相比于&arr是要大40个字节,而arr+1的地址是要比arr的地址大4个字节。
那么由此便可以说明问题,&arr实际上取出的是整个数组的地址
2.3数组指针的使用
我们可以通过打印数组内容的方式,来练习使用一下数组指针
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; int(*pa)[10] = &arr; int i = 0; for (i = 0; i < 10; i++) { printf("%d ", (*pa)[i]); } for (i = 0; i < 10; i++) { printf("%d ",*(*pa+i));//*pa==arr,pa放的是&arr,解引用操作后相当于把&符号抵消了 } return 0; }
代码解释:
首先我们将一维数组arr的地址放到一个数组指针*pa里面。
使用方法1:我们之前就知道,如果我们想要访问一个数组的元素内容,我们可以通过下标访问的形式。那这里第一个for循环就可以很好解释,(*pa)==arr,(*pa)[i]==arr[i]
使用方法2:*pa得到首元素地址,对首元素地址进行解引用操作,即可拿到首元素,那么我们每次解引用时让首元素地址向后挪动整数i,即可跳过元素的单位字节大小的地址。这里补充一个小知识点,指针的类型可以决定,指针±整数一次性跳过多少个字节 ,之后再进行解引用操作,就可以拿到相应的元素了
不足之处: 这里有很多人,感觉这样访问数组元素的方法比较智障🤣🤣🤣,我也感觉很智障,哈哈哈😆,但指针使用数组指针的场景其实不是这样的,这里只不过想用代码的样子给大家呈现一下,这个数组指针的使用方法大概是什么样子的
下面的代码再较高级呈现一下,数组指针的使用形式(坚持读下去,相信你自己😋)
void print1(int arr[3][5], 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]); } printf("\n"); } } void print2(int(*p)[5], int x, int y) { int i = 0; for (i = 0; i < x; i++) { int j = 0; for (j = 0; j < y; j++) { printf("%d ", *(*(p + i) + j)); //*(p+i)其实得到的是这一行的首元素的地址 //+i表示的是跳过整整一行的元素 printf("%d ", (*(p + i))[j]); //*(p+i)拿到这一行的数组名,p放的就是&arr,*p则为arr printf("%d ", p[i][j]); //二维数组比较特殊*(p+i)其实相当于p[i],*(p[i]+j)相当于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} }; print1(arr, 3, 5); print2(arr, 3, 5); return 0; }
代码解析:
1.知识点 我们知道数组名代表首元素地址,当二维数组的数组名被当作参数传递时,我们可以把二维数组想象成多个一维数组的集合,则每一个一维数组相当于这个二维数组的每一个元素,这样来看二维数组名其实就是第一行一维数组的地址,所以我们就可以用数组指针来接受这个参数
2.知识点 * (*(p+i)+j)这个代码有很多人是比较难理解的,我们重点来讲解一下这个代码,由知识点1可以知道p代表第一行数组的地址,那么如果我们对这个地址进行±整数的话,那它是不是就跳过一行数组的地址了,(因为指针的类型决定指针±整数后,跳过字节的个数嘛) ,所以我们可以这样的先对这个p进行解引用这样就拿到了第一行的数组名了,我们再对这个数组名解引用就拿到二维数组的第一行数组的第一个元素了,之后我们再让这个数组名±整数,那他就可以在第一行数组里面跳来跳去了,我们再次对他解引用,就可以完全拿到第一行数组的每一个元素了。
当我们拿到第一行所有的元素之后,想要拿第二行的数组的元素个数时,我们只要让这个接收二维数组的数组名的指针,也就是p+1,不就好了么。所以我们再第一次解引用的括号中让i从0慢慢变大,这样就能保证每一行的数组的数组名都可以拿得到,最后每一行的数组名再加减整数,再解引用,完全就可以访问到二维数组的所有元素内容了
3.(补充内容)来看几个代码,加深对指针的理解
int arr[5]; int* parr1[10]; int(*parr2)[10]; int(*parr3[10])[5];
解释1: arr是数组名,这个数组类型就是去掉数组名剩下的部分,比如这个数组的大小是5个int,数组元素类型是int
解释2: parr1是数组名,去掉数组名,剩下的就是数组的类型,比如这个数组的大小是10个int*,数组元素类型是整形指针
解释3: parr2是一个指针,指针所指向的是一个数组,这个数组大小是10个int,每个数组元素类型是int
解释4: parr3是一个数组名,这个数组的大小是10,数组的每个元素类型是一个指针数组,每个指针数组可以存放5个int型的指针
最后总结:
*(p+i)==p[i];//切记,我们这里拿到的是每一行的数组名 *(*(p+i)+j)==p[i][j]; *(p[i]+j)==p[i][j]
其实由上面的代码,我们也可以感觉到,指针和数组似乎冥冥之中有一种联系,他们似乎总是可以相互表示,代码真是神奇哈🧐
3.函数指针
3.1概念的引入
#include <stdio.h> void test() { printf("hehe\n"); } int main() { printf("%p\n", test); printf("%p\n", &test); return 0; }
其实从这里,我们可以看出一些端倪,就是如果当我们想要传一个函数的地址,调用这个函数时,我们可以不用去传&函数名,直接传函数名就OK了
3.2函数指针的写法和有关代码阅读
我们接下来,先写一个函数指针,存放一下函数的地址,之后再阅读一个较难的代码
void put() { printf("hello world!"); } int main() { put(); void(*ps)() = put; return 0; }
代码1: 我们知道指针去掉名字,剩下的就是指针类型,道理相同,void(*)()其实就是一个函数指针的类型,具体指向的函数是返回类型是空,无参数类型,那么在0之前的括号里面放一个函数指针的类型,那其实就是强制类型转换,将0这个整型转换为函数指针类型,我们是知道指针类型加上一个名字之后,那么这个名字其实就能代表这个函数的地址。这其实与我们的代码不谋而合,0现在其实就是函数的地址,也是这个函数的函数名。或许这里很多人会感觉很不舒服,0怎么就是函数的地址了呢?其实你可以这么想,当0的类型是函数指针类型时,我们可以想一下,什么东西可以拥有类型呢?(例如:指针去掉指针名剩下的就是指针类型,数组去掉数组名剩下的就是数组类型,由此我们可以看出去掉什么东西,剩下的就是类型呢?答案显而易见,也就是去掉名字)所以名字才有类型,0都有函数指针类型了,那么0其实就是函数名 然后,对函数名进行解引用操作,拿到这个函数,对这个函数进行调用,又因为这个函数是无参数的,所以调用这个函数时,是不对这个函数进行传参的,那么最右边的括号里面是什么东西都没有的
代码2: 我们知道一个函数共有3个组成部分,分别是,函数名,函数参数,函数返回类型,当我们看到signal后面有个括号时,我们其实就可以猜到,这里其实就是调用了一个名叫signal的函数,这个函数的参数是int和函数指针类型,而且这个函数的返回类型也是一个函数指针类型
3.3最后的一小部分的补充练习
int (*p)(int a, int b); int* p(int a, int b);
代码1: p是一个指针变量这个指针所指向的是一个函数类型,这个函数的返回类型是int参数分别也是两个int
代码2: p这里是一个函数名,这个函数的返回类型是int型的指针,函数的参数是两个int
4.指向函数指针数组的指针
4.1概念解释:
这里的这个指针其实也就是个地址而已** 永远记住指针就是地址,地址就是指针** ,那这个地址究竟是什么呢?它其实就是个数组的地址,这个数组里面的每个元素都是函数指针,概念就是这么简单,我们只要将他层层剥离即可完美得到概念
//下面我们来写一段相应的代码,以便加深我们对于概念的理解和掌握
4.2上代码
void test(const char* str) { printf("%s\n", str); } int main() { 1.void (*pfun)(const char*) = test; 2.void (*pfunArr[5])(const char* str); 3.pfunArr[0] = test; 4.void (*(*ppfunArr)[5])(const char*) = &pfunArr; return 0; }
代码1 首先*pfun是一个指针,这个指针的类型是一个函数指针,指向的函数是一个返回类型为void,参数类型为const char 的一个函数,这个指针中存放了test函数的地址
代码2 首先pfunArr是一个数组,这个数组的大小是5,数组的每个元素是函数指针,每个指针所指向的函数类型为返回类型是void,参数类型是const char
代码3 将test函数的地址放到函数指针数组的第一个元素里面,使这第一个元素指向的函数是test函数
代码4 将函数指针数组的地址放到指向函数指针数组的指针当中,**这里的指针是比较难写的,如果直接写不好写的话,我们可以像下面这样,将函数指针数组的数组名替换为(指针)即可 ,这样来写,就不怕我们把这个指针给写错了
void(*pfunArr[5])(const char* str); void (*(*ppfunArr)[5])(const char*) = &pfunArr
二、传数组和指针时,函数的参数设计
1.牢记以下重要的东西
很重要的知识要记住:我们要牢记,当传数组或指针到函数里面时,实际上传过去的是地址!地址!地址!一定要记住了😡😡😡
也是很重要的知识:我们再设计函数参数来接收地址时,有两个选择,你觉得哪个方便就用哪个,一个选择是用指针接收地址,另一个选择是,就用它本身去接收他自己(这个非常重要,因为我怕你在平常阅读代码的时候,由于这个知识点的缺失,从而看不懂一些代码)
2.一维数组传参
void test(int arr[])正确,用它本身去接收,大小可以不用写 {} void test(int arr[10]) 完全正确,用它本身去接收 {} void test(int*arr) 正确,传过来int数据的地址,我用一级指针接收 {} void test2(int*arr[20]) 正确,完全用它本身去接收 {} void test2(int**arr) 正确,用二级指针接收传过来的一级指针 {} int main() { int arr[10] = { 0 }; int* arr2[20] = { 0 }; test(arr); test2(arr2); return 0; }
3.二维数组传参
void test(int arr[3][5]) 完全正确,用它自己接收自己 {} void test(int arr[][])不正确,行可以省略,但列是不可以省略的 {} void test(int arr[][5])正确,没有省略列,用它自己接收自己 {} void test(int *arr)二维数组数组名,是首行数组的地址,应该数组指针来接收而不是整形指针 {} void test(int*arr[5])这是拿了个整型指针数组接收地址来了,地址必须用指针接收,完全错误 {} void test(int(*arr)[5]) 这个完全正确,用指针数组来接收二维数组的首行数组的地址 {} void test(int**arr) 二级指针接收是没必要,这里又不是传一级指针过来,人家就传个地址而已 {} int main() { int arr[3][5] = { 0 }; test(arr); return 0; }
4.一级指针传参
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,10 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); print(p, sz); //一级指针p(里边放着首元素地址),传给函数print return 0; }
这里面就用到了我们的“牢记一下重要东西的内容了”,我们用它本身去接收他自己
所以当函数参数是一级指针时,它能接收什么东西呢???😢
1.可以接受它本身,(也就是一个存放某个元素地址的一级指针)
2.可以接收某个变量的地址
5.二级指针传参
void test(int** ptr) { printf("num=%d\n", **ptr); } int main() { int n = 10; int* p = &n; int** pp = &p; test(pp); test(&p); return 0; }
这里面还是用到,我们所牢记的内容了
所以当函数参数是二级指针时,它能接收什么东西呢???😢
1.可以接收他本身,(也就是一个存放一级指针地址的二级指针)
2.可以接收某个一级指针变量的地址
3.也可以接收指针数组的数组名
三、数组部分
1.指针数组
这个是真没什么可说的了,简直太简单了,我们随便起个数组名加上个数组大小,再加个数组元素类型就好了嘛,这么简单,就不说了
int* arr1[10]; 这个是整形指针数组 char* arr2[4]; 这个是字符指针数组 char** arr3[5]; 这个是二级字符指针数组
2.1概念的引入
在我们熟练掌握数组的三元素组成后,如果我们想要写一个函数指针数组的话,其实非常简单,我们只需要将数组元素类型写成函数指针类型就可以
例如:int(*parr[5])(char*)
像上面的数组的元素类型就是,int( * )( char * )那这其实就是一个函数指针类型,指向的函数的返回类型是int,参数是char*
2.2函数指针数组的具体使用场景
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(*parr[4])(int, int) = { Add,Sub,Mul,Div}; int i = 0; for (i = 0; i < 4; i++) { printf("%d\n", parr[i](2, 3)); } return 0; }
功能所在:上面代码其实可以初步向我们展示函数指针数组的功能,他其实就是方便在当我们调用多个函数时,省去我们重复写调用函数的代码,如果有函数指针数组的话,我们直接访问函数指针数组的元素内容就可以了,然后向我们得到的函数名传相应的参数就可以了
下面给大家做一个功能较完整的函数指针数组的使用吧,上面的代码只能对这个数组的使用简单介绍一下,下面我们来完整的实现一下这个数组吧!!!
void menu() { printf("*******************************\n"); printf("****1.Add 2.Sub*******\n"); printf("****3.Mul 4.Div********\n"); printf("******** 0.Exit ***********\n"); printf("*******************************\n"); } 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() { menu(); int input = 0; int x = 0; int y = 0; int (*(parr)[4])(int, int) = { Add,Sub,Mul,Div }; do { printf("请输入您要选择的菜单的序号:>"); scanf("%d", &input); if (input == 0) { printf("退出程序"); } else if (input <= 1 && input <= 4) { printf("请输入两个操作数:>"); scanf("%d %d", &x, &y); printf("%d\n", parr[input - 1](x, y)); printf("请继续选择输入您要的序号:>"); } else { printf("输入错误,请重新输入"); } } while (input); return 0; }