hi~
导语:
上文我们已经谈了指针的定义、创建、常见的错误、二级指针等相关问题。那么这篇文章就让我们深入探讨一下指针的应用。
正文:
字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ,一般使用如下方式定义。
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
就按照我们介绍的,type+*,的方式来定义。但是除了这一种还有一种常用的方式:
int main()
{
char*str="hello world";
printf("%s\n",str);
return 0;
}
请输出打印结果
很多人有疑问,我这里是字符指针,存储的是字符的地址,为什么能把字符串放进去?
其实不管是字符还是字符串,指针始终都只存储首元素的地址,所以首元素不都是字符吗那么我用字符指针来存储就是对的。
这里大家要注意,很多人以为是把字符串 hello world 放到字符指针 str 里了,但是/本质是把字符串 hello world. 首字符的地址放到了str中。
搞懂了这些我们来看看一道题目
#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;
}
请输出打印结果:
这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当 几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。内存相同,地址就相同,那么指针就一样。
但是用相同的常量字符串去初始化 不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
指针数组
先问一个问题:指针数组是指针还是数组?
答:是数组。指针只是它的修饰词,本质上还是数组。
数组有整型数组、字符数组,那指针数组有啥呢?
首先arr3是一个数组 ,这个数组有5个元素,每个元素是整型指针。
那么怎么定义的?其实就是type+*+【】
知道了这些,那就让我们分享几个指针数组吧:
第一个是一个数组,数组有10个元素,每个元素是整型指针型的;
第二个是一个数组,数组有4个元素,每个元素是字符指针类型的;
第三个是一个数组,数组有5 个元素,每个元素是二级字符指针类型的。
数组指针
请问数组指针是数组还是指针?
有了上道题目的教训你现在应该知道了,数组指针是指针!
数组指针与指针数组的区别:
p1是指针数组,p2是数组指针
解释:p先和*结合,说明这是一个指针,那它指向的是一个数组,这个数组有10个元素,每个元素是int 型。欧克,首先它是一个指针,然后它又指向一个数组,那么就是数组指针。
在这里要注意,【】的结合优先级是要比*高的,所以p1先和【】结合再和*结合,这也导致了为什么它是指针数组。
数组名与&数组名
为了方便后续的讲解,这里插入关于数组名的知识点。
对于下面的数组
arr和&arr分别代表着什么?
我们知道数组名代表的是数组首元素的地址,那么&数组名有代表的是什么呢?为了打消疑问,我们通过编译器的打印结果来探究。
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
0000006B91FCFCB8 |
0000006B91FCFCB8 |
欸,结果是一样的,但是这就能说明二者代表的含义是一样的吗?我们再来看看下一个代码
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", arr+1);
printf("%p\n", &arr+1);
return 0;
}
0000000AFD2FF648 |
0000000AFD2FF648 |
0000000AFD2FF64C |
0000000AFD2FF670 |
欸,可以发现arr和&arr分别+1的结果是不一样的。这是为什么呢?
首先arr+1与arr相差4个字节,可以由表格数据得出。由此我们知道既然相差4个字节,那么代表他们之间相差1个整型元素,也就是1个数组元素,所以arr代表的是数组首元素的地址。
&arr与&arr+1相差的是(70-4c)16进制转换,得到是40个字节,这个值刚好是我们定义的数组的大小。那么我们知道&arr代表的是一整个数组。
所以我们发现,arr代表数组的首元素,&arr代表一整个数组,二者的含义不同,不过二者的地址是一样的。
好了引进了这些知识点之后,让我们深入的了解一下数组指针吧
数组指针的应用
数组指针是一个指向数组的指针,那么这个指针里面存放的是数组的地址
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
上述展示的用法虽然没有错,但是我们一般不会这样使用数组指针。参考下面代码:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
数组指针常用于函数的传参中,这里arr是首元素的地址,但是数组是二维数组,二维数组的首元素是二维数组的第一行,所以arr其实是第一行的地址,也就是数组的地址,用数组指针来接收。
好了,让我们做几道题目来检验一下目前的学习成果吧:
int arr[5]; |
int *parr1[10]; |
int (*parr2)[10]; |
int (*parr3[10])[5]; |
第一个是一个整型数组;
第二个是一个指针数组,数组有10个元素,每个元素是int*
第三个是数组指针,指向的是一个int型数组
第四个,首先看到(),【10】先和parr3结合,所以这是一个数组,接着数组的类型是int(*)[5]
这是一个指向数组的指针,所以总的来说这是一个指针数组,数组的每个元素是指向整型数组的指针。
数组参数、指针参数
一维数组传参、二维数组传参
在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?
void test(int arr[]);
void test(int arr[10]);
void test(int* arr);
void test2(int arr[][]);
void test2(int arr[][5]);
void test2(int* arr[5]);
void test2(int**arr);
void test2(int(*arr)[5]);
int main()
{
int arr1[10];
int arr2[3][5];
test(arr1);
test2(arr2);
return 0;
}
现在请以此判断上述代码是否正确:
第一个:传参过去的数组名,代表的是一整个数组,定义一个数组来接收,没有问题。
第二个:要知道形参根本不管你数组的大小,就像是第一个,数组的大小直接省略了。所以这个也是可以的。
这里插入一个问题,要是形参的数组的大小与传过来的数组大小不匹配怎么办?会报错吗?
就像是这个例子,形参的数组大小是5,与传过来的数组大小不匹配,但是调试时,并未报错,运行时程序也通过了。只不过我们只知道这样写不会报错,但是能否正常实现功能就不知道了,还是建议大家不要这样写。
言归正传,第三个:数组名是数组首元素的地址,要用指针来接收,int*符合条件,可以。
第四个:此时传参的是二维数组,二维数组用二维数组来接收,没有问题。但是,二维数组只能省略行的大小,不能省略列的大小,所以不可以。
第五个:由第四个可知,此时未省略列的大小,可以。
第六个:注意,这里是一个指针数组,我们传参时只见过指针来接收,或者数组来接收,从未见过用指针数组来接收,不可以。
第七个:二维数组名是首元素的地址。而二维数组本质上是一维数组的组合,所以二维数组的首元素就是一维数组,那么一维数组的地址是什么呢?并不是二级指针吧,所以不可以。
第八个注意,这是个数组指针。那么吃过上面一个的亏,有的同学就说,这个也不可以!但是,真的不可以吗?我们先想想二维数组是什么?二维数组本质上来说就是一维数组的组合,而一维数组要用地址来接收,那么一维数组的组合就是几个指针的组合,因此要用数组指针来接收。
一级指针传参、二级指针传参
#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;
}
看到上面这个例子,定义了一个整型指针,传参整型指针,那我们就用整型指针来接收。
这很简单对吧,那么现在我有一个问题。倘若函数设计时参数部分为一级指针时,我们反过来能给函数传递什么参数呢?
void test(int*arr)
这个能接收什么样的参数?
首先传一个整型指针是可以的,将一个整型的地址传过去也是可以的,还可以将一个整型数组传过去。由此可知,我们在设计一个函数时,一定要考虑到这个函数可以接受什么样的类型。
#include <stdio.h>
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;
}
我们看到上面一个例子,P是一个整型指针,那么&P就是一个二级指针,所以pp是一个二级指针,要用二级指针来接收。
同样的,反过来思考当函数参数设计成二级指针时,我们能传什么样的参数?
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*的地址,那么就要用二级指针来接收。
函数指针
函数指针就是指向函数的指针。
函数也是又地址的,那么&函数名也是有意义的,它代表的是函数的地址。之前我们学到数组指针的时候,我们知道&arr代表的是数组的地址,arr代表的是数组首元素的地址,他们打印的地址都是一样的,那么类比过来,函数指针是否也有这样的概念呢?
#include<stdio.h>
void test(int arr[5])
{}
int main()
{
int arr1[10];
int arr2[3][5];
printf("%p\n",test);
printf("%p",&test);
//test2(arr2);
return 0;
}
请输出打印结果:
可见这两个概念在函数指针里面也存在,他俩都代表函数的地址,只不过函数名就是函数的地址没有函数首元素的地址之说。
现在我们看看如何定义一个函数指针。
类比于数组指针,我们可以尝试一下
int (*p)(int,int )=&test;
首先要确保他是一个指针,这个指针指向的是一个函数,这个函数的参数与返回值的类型要确定。接着后面的括号是函数的参数类型,前面的int是函数的返回值的类型 。
看到这里你们学废了吗?学废了就让我们看看下面的代码练习一下吧:
//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);
你知道上面的两个代码的含义吗?
这样的代码比较奇葩,我们来逐步分析。
第一个:先看到最里面的括号,(*)说明是指针,去掉(*),发现这是指针的类型,这是一个函数指针,函数指针后面跟着一个0说明要将0强制类型转换为函数指针类型,强制类型转换后用*来解引用,变成了函数指针,后面跟着一个(),说明此时是在调用函数指针。(补:这个题目比较难,看不懂的话不做强求)
第二个:这个也是一个死变态,我们一步步来。先看到最里面的括号void(*)(int)是一个函数指针类型,signal(int,void(*)(int))是一个函数名与函数的参数,它的返回类型是void(*)(int)。所以这个代码就是一个signal的函数声明。这个代码看起来太过复杂,可不可以简化一下呢?
int main()
{
typedef void (*ptr)(int);
ptr signal(int,ptr);
}
重定义一下void(*)(int)为ptr,这样就可以大大简化代码了。
函数指针数组
先问一个问题,函数指针数组是函数还是数组还是指针?
根据前面我们学习的内容,可以推断出函数指针数组是数组,这个数组存放的是函数的地址,
那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];
以上的写法哪一种是正确的呢?
答案是第一个。parr1先和【10】结合说明他是一个数组,去掉parr1【10】后,int(*)()是一个函数指针类型,那么第一种写法就是函数指针数组。
前文讲了许多关于函数指针的内容,但是有人就会问了,我们学习函数指针有什么用呢?那么在下一篇文章我们就来介绍函数指针的作用,以及实现。
总结
经过这一篇文章的学习,我们掌握了各种指针类型的概念定义与使用,对不同指针类型的传参也更加熟练。在区别不同指针类型或者复杂的类型时,我们可以遵循以下步骤:
1将复杂结构分成单独的模块
2确定最底层的结构类型
3依次向外层辨认
希望大家能学有所获。
都看到这了,不给个赞吗?
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。
好了我是happysky,编程之路你我一起探索,我们下期JAN