【深入理解指针】指针的进阶

目录

  1. 一些常见指针的用法和意义

  2. 指针数组

  3. 数组指针

  4. 函数指针

  5. 函数指针数组


1.一些常见指针的用法和意义

常见的指针有以下几种

char*,int*,float*,double*

指针的初阶用法就是认识并能熟练使用像char*,int*等一些用法,

比如说实现一个交换两个整数的函数,你要知道实参和形参的区别,也就是为什么要传指针。

这些就是一些初阶用法。

而指针的意义是什么呢?

其实指针类型的意义就是:决定了每一次访问内存的的大小

我们经常在定义指针的时候是不是都是按照它的类型进行定义的,比如是int类型,就定义一个int*,char类型就定义一个char*,其实你可以想象一下,地址全都是占4或8个字节的空间,哪它为什么要存在指针类型呢?

我把int*类型的指针放在char*行不行?你是否有这样的疑问?*

​ 来我给你举个例子

//下面输出什么
#include
int main()
{
	int a = 0x00001234;
	int b = 0x00001234;
	int* pa = &a;
	*pa = 1;

	char* pb = (char*)&b;
	*pb = 1;

	printf("a=%x,b=%x\n", a, b);
	return 0;
}

看懂这个答案的前提你得了解数据在内存中的存放方式,不然可以会有点懵懵懂懂的感觉,我在上一个文章就有详细的介绍过!

【深入理解指针】指针的进阶_第1张图片

OK,你再回想一下,地址全都是占4或8个字节的空间,哪它为什么要存在指针类型呢?当你这样想之后,再回想一下上面的例子,就会更近一步想到,哦!原来是每次访问内存的大小!

那可能会有人问我把上面int类型的b的指针放在char*之后,能不能把它也改成1?

能呀,就一个一个字节的改就行了呀,只不过你觉不觉得这样有点多此一举呢,我完全可以用一个int*,直接进行4个字节的改动,要char*的话还有一个一个字节的去改动。所以这就是存在指针类型的意义!

【深入理解指针】指针的进阶_第2张图片


接下来给大家提升一点难度:字符char*指针的另一种用法!

【深入理解指针】指针的进阶_第3张图片

上面的字符串是存在p里面吗??

首先我们知道char*类型是指针吧,那指针的大小是4或8个字节吧,所以上面的字符串是肯定放不下的,那可能有人学过或了解过数据截断的讲法,但是这里是不可能截断的,因为在后面已经完完整整的把字符串的内存打印出来了。

我给你举个和上面这个类似的例子

#include
int main()
{
	int arr[10] = { 1,2,3,4,5,7,8,9,10 };
	int* pa = arr;
	return 0;
}

上面这个代码pa是存放整个数组的元素吗? 不是了吧,大家都知道是存放数组首元素地址。

所以我们上面的字符串是存在p里面吗??

显然不是,其实它也是存放首字符的地址,然后通过首字符的地址找到后面的字符!大家可以跟数组这一块类别一下就明白了!

而且呢!它指向的内容是无法被修改的,(不是因为我在char*p前加了const才不被修改的,是它本身就无法被修改,如果你修改了,程序会崩溃,加const就是为防止自己去修改,这样如果不小心修改了,编译器会报错提醒我们)

这样的字符也称“常量字符串”

下面有一个这样的题目

//下面输出什么?
#include 
int main()
{
    char arr1[] = "hello,My friend!";
    char arr2[] = "hello,My friend!";
    const char* arr3 = "hello,My friend!";
    const char* arr4 = "hello,My friend!";
    if (arr1 == arr2)
        printf("arr1 =arr2\n");
    else
        printf("arr1 != arr2 \n");

    if (arr3 == arr4)
        printf("arr3 = arr4 \n");
    else
        printf("arr3 != arr4 \n");

    return 0;
}

输出结果如下:

【深入理解指针】指针的进阶_第4张图片

为什么会是这样的结果呢,其实呢!数组arr1和arr2它们在开辟空间的时候是分别开不一样的空间,所以它们两的地址是不可能相同的,而arr3和arr4,就是我们上面讲的,它们是不可被修改的,也就是说它们在开辟空间的时候,没必要开两个不一样的空间了,反正它们的内容也不会被修改,那为什么还要浪费空间开新的呢,这时候编译器就很聪明了,它就把两个指针都指向同一块内存空间。因此arr3=arr4!

2.指针数组

指针数组和数组指针其实很多人都容易混淆,怎么区分呢?

很简单,捉住主语!

比如:好孩子,那他的主语是不是“孩子”,”好“这个词是修饰孩子的。

同样:指针数组,它是个数组,指针是修饰它的类型

我们常用的有整型数组,字符数组等,整型数组就是存放整型的数组,而字符数组就是存放字符的数组,

所以指针数组,就是存放指针的数组!!

那它是如何在代码中定义的呢?

【深入理解指针】指针的进阶_第5张图片

3.数组指针

数组指针 主语:是指针!

我们知道int*是指向int型的指针,char*是指向char型的指针

所以数组指针是指向数组的指针!

p1, p2分别是什么?

int *p1[10];

int (*p2)[10];

p1上面我们讲过是指针数组

而p2就是我们的数组指针了!

为什么呢?

int (*p)[10]; //解释:p先和*结合,说明p是一个指针变量,然后指向的是一个大小为10个整型的数组。所以p是一个 指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

写出下面的数组指针

【深入理解指针】指针的进阶_第6张图片

我们知道定义一个指针是要根据它的类型,然后在类型后面+*就可以定义一个指针了,比如int *a,所以说我们在定义数组指针的时候也要知道数组的类型,那么数组的类型是什么呢?

举个例子,int a=0;去掉变量名后就是它的类型,比如a的类型就是int,所以数组也是同样的方式,int a[5],去掉变量名后

就是int [5],这个就是它的类型吗?

是的,别怀疑,这个就是它是类型,然后呢,我们就像普通类型一样在加个*和指针名就可以

只不过它的指针名跟int那些不一样,数组指针的名字是放在int 和[]之间的,而且必须要用括号括起来,不然就变成这样了

int *pa [5],它是一个指针数组,因为[]优先级高与*,所以呢数组a的指针最终的结果就是int (*pa)[5],b的也是同样的分析方法。

那数组指针怎么用呢?它有什么作用呢?

我给大家写一个数组指针错误的使用方式!

#include 
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
    int(*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p

    //但是我们一般很少这样写代码 一个数组指针的比较笨的使用方法
    int i = 0;
    for (i; i < 10; i++)
    {
        printf("%d ", *((*p) + i)  );
    }
    printf("\n");
    //上面的写法我们完全可以用一个整型指针来完成
    int* parr = arr;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", *(parr + i));
    }
    return 0;
}

上面这个列子就是数组指针错误的使用方式,为什么说它错误呢!因为它是的用一个数组指针来实现数组打印,而这个功能完完全全可以用整型指针来实现,可它偏偏用了数组指针,这就将它复杂化,简直就是脱裤子放屁,多此一举!所以我们学数组指针可不是用来实现这样的功能的。

相对正确的使用方式是用于二维数组传参的时候使用的

举个例子

#include 
//以二维数组的形式接受地址
void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    int j = 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 j = 0;
    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;
}

学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:

int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
//思考一下上面的代码是表示数组呢,还是数组指针呢?还是其他什么呢?

【深入理解指针】指针的进阶_第7张图片

int arr[5];//一个整型数组,数组有5个元素,每个元素的类型是整型
int *parr1[10];//一个指针数组,数组里有10个元素,每个元素的类型是一个整型指针(int*)
int (*parr2)[10];//数组指针,指向的对象是一个拥有10个元素的数组,每个元素的类型是整型(int)
int (*parr3[10])[5];//一个数组指针数组,数组有10个元素,每个元素的类型是数组指针(int(*)[5])

第四个再解释一下,因为[]的优先级比*要高,所以parr3先和[]结合,所以它是个数组,又因为我们把parr3[]去掉之后,就剩下了int(*)[5],这不就是数组指针类型吗,其实跟第三个对比一下,parr2去掉之后,就是int(*)[10],这就是数组指针。所以第四个的类型是数组指针,而它是一个数组,所以最后其实就是一个数组指针数组


4.函数指针

“函数指针”我想大家首先得知道它是一个指针吧!

而且是一个指向函数的指针

可能有人会问—>什么鬼?函数也有地址吗?

【深入理解指针】指针的进阶_第8张图片

有没有呢我给你测试一下就知道了,看下面一段代码

#include 
void test()
{
	printf("hehe\n");
}
int main()
{
	printf("%p\n", test);
	printf("%p\n", &test);
	return 0;
}

【深入理解指针】指针的进阶_第9张图片

你看到了吗!显然在这里,这个函数test()的地址为004E13BB,而且用不用&符号都能把该函数地址取出来!

那问题来了,整型数,字符,数组等的地址我们都已经学会如何保存在相对应的指针中了。

那我们的函数的地址要想保存起来,怎么保存?

我有个选项想让大家选出正确的函数指针:

看下面代码

void test()
{
 printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();

首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针?

其实我想大家应该都能选出来,答案就是第一个pfun1,因为类似上面的数组指针嘛!*符号一般都要括号括起来的,因为它的优先级比较低

那怎么解释这个(void (*pfun1)())呢?

其实是这样的,pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。!

举个例子

//写出下面函数的函数指针
int add(int x, int y)
{
	return x + y;
}

答案应该是:int (*pf)(int,int)=&add,&符号可加可不加

解释:因为add函数的返回值是int ,参数类型为int x,int y,所以函数指针的类似为int(*)(int x,int y),但是x,y可以省略不写

接下来给大家阅读两段有趣的代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

知道上面的代码是什么意思吗?这可不是我胡编乱造出来的,上面的代码来自于《C陷阱和缺陷》这本书。

解释一下这两行代码的意思

第一行代码其实就是一个函数调用,为什么呢?

别急,首先我们把括号拆分一下,然后把外壳(*)(),先去掉,先留下(void(*)() )0,然后这时候就要擦亮你的眼睛了,void(*)(),不就是函数指针吗?,它是一个类型,然后放在0的前面代表什么呢?不就是强制类型转换吗,比如(char)0,就将0强制类型转换成char类型,同样的(void(*)())0,就是将0强制类型转换成函数指针类型,而这个函数的返回值是void,参数为空。

那么现在0就相当于这样的一个:函数的返回值是void,参数为空,的函数指针,然后再把外壳装回来就是(*0)(),就是将0进行解引用操作,然后就相当于调用函数的操作。比如我把0换成pf,最后的形式就是(*pf)(),不就是类似于test(),这样的函数调用吗!

第二行代码其实就是一个函数声明,为什么呢?

首先还是先把括号拆分开,看红色框中的两个进行类比,下面add我相信大家都知道是一个函数声明吧,它的返回类型是int,参数有两个,一个是int,另一个也为int,add为它的函数名

同样的signal其实也是一个函数名,它的参数也有两个,一个是int,另一个是void(*)(int),不知道大家看到这里了,能不能看得出这是一个函数指针呢,这个函数指针的返回值为void,参数为int,其实就是一个传指针的操作,没有什么高大上的,只不过它的形式有点复杂。

而最后的外壳的void(* )(int),我们跟add类比,add的前面是它的返回类型int,所以呢,signal这个函数的返回类型就是一个函数指针,void(*)(int).

【深入理解指针】指针的进阶_第10张图片

有没有什么函数指针的使用场景呢?

有,比如c语言自带的标准库中有一个可以排序任何类型的排序算法qsort,它就涉及了函数指针的参数和使用!

而像qsort这样通过调用函数指针的函数就被称为回调函数

回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

感兴趣的可以翻一下我之前的文章有模拟实现过qsort函数,和qsort函数的介绍!


5.函数指针数组

为了防止大家学懵了,给大家回忆一下什么是数组!

数组是一个存放相同类型数据的存储空间

int *arr[10];//数组的每个元素是int*
int arr1[5];//数组每个元素是int
char* arr[6];//数组每个元素是char*

那我们已经学习了指针数组,现在学这个函数指针数组我相信是不难理解的!

那要把函数的地址存到一个数组中,那这个数组就叫函数指针数组

那函数指针的数组如何定义呢?

还是给几个选项吧!

int (*parr1[10])();
int *parr2[10]();
int (*)() parr3[10];

答案是:parr1

parr1 先和 [] 结合,说明 parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。*

教一下大家如何正确的写好一个函数指针数组

#include

int add(int x, int y)
{
	return x + y;
}
int sum(int x, int y)
{
	return x - y;
}
int main()
{
	//先写函数指针
	int (*pf)(int, int) = &add;

	//再把函数指针copy过来,在变量名后加个[]
	int (*pf1[5])(int, int) = { &add,&sum };

	printf("pf=%p\npf1[0]=%p\npf1[1]=%p \n", pf, pf1[0], pf1[1]);

	return 0;
}

【深入理解指针】指针的进阶_第11张图片


你可能感兴趣的:(C语言系列,c语言,算法,visual,studio,开发语言)