大集合!!C语言指针知识要点大合集!!小白不要错过喔!!收藏这一篇就足够!!(2)

指针!!是C语言最本质的特征,学好了指针才能算正式入门C语言喔!!如果你是C语言小白,看这篇文章就对啦!!✍

接上篇大集合!!C语言指针知识要点大合集!!小白不要错过喔!!收藏这一篇就足够!!(1)-CSDN博客

函数指针

学习完了数组指针,我们还要掌握函数指针。

函数在定义后,都会在内存中保存,而保存的位置就是内存的代码区

既然函数的定义会在内存中存放,所以必然会有属于它的地址我们把这个地址叫做函数指针

函数指针变量的创建

例如对于函数Add:

int Add(int x, int y)
{
    int z = x + y;
    return z;
}

我们创建一个函数指针来存放函数Add的地址,有:

同样的,去掉标识符pAdd之后,函数指针pAdd的类型为int ( * )(int, int)

这里赋值也可以用&Add,函数名不像数组名,两种赋值的效果是完全一样的,没有区别,函数名和&函数名都代表了同一地址,且意义完全相同(正因如此,函数指针会有一个奇妙的现象,下文有介绍)。我们把Add赋值给了pAdd这个指针,说明①两者在类型上将相同(编译器不会报任何警告),它们之间赋值的是地址,而地址是唯一的,那么②它们连内容也是相同的所以Add和pAdd是完全等价的。

通过函数指针来调用函数

函数指针指向了一个函数,那么我们就可以通过对函数指针的解引用来实现调用函数了。

同为Add函数:

int ret = *pAdd(10, 10);

pAdd指向了函数Add,“ * ”解引用后调用Add函数,并传入了两个参数,其中X = 10,Y = 10,ret接受函数的返回值,ret的值为20。

如果你足够细心就可以发现:

int ret = Add(10,10);

两段代码的效果是完全一致的,这可以告诉我们两点:

i.调用函数也可以使用函数指针来调用。效果是一样的。

ii.代码效果的一致也能说明函数名本身就是一个函数指针。

上文说到&Add、Add和pAdd是完全一样的,这里的调用我们又能发现,*pAdd和Add是一样的,所以我们能得到结论Add <==> &Add <==> *pAdd <==> pAdd

也就是说,这里的取地址&和解引用*操作,不会对函数指针的意义产生影响。所以不管我们进行几次解引用和取地址,结果都是一样的,即

pAdd <==> *pAdd <==> **pAdd <==> &&Add <==>  &*&*&*&&*Add……。

神奇吧!不管怎么取地址和解引用,都能化简为Add和pAdd但是为了代码的可阅读性,即使这么写不影响运行,我们还是不要写这种让人眼花的代码了。

函数指针的强制类型转换

函数指针,同样作为一种数据类型,当然也可以进行强制类型转换了。所有数据类型的强制类型转换,操作都是相同的。

如图,这里把int类型数据0,强制类型转换为了void(*)()类型。 

函数指针数组

顾名思义,就是一个存放函数指针的数组。函数指针数组的创建看起来比较复杂。

int(*pf[4])(int, int)

这里创建了一个函数指针数组,数组大小为4存放的函数指针类型为int(*)(int, int)。同样的,去掉标识符pf,pf的类型就是int(* [4])(int, 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)
{
    if(y)
    {
        return x/y;
    {
    else
    {
        printf("分母不能为零!\n");
         return 0;
   }
}

用上方函数制作一个简单的整数计算器。

如果我们要进行多次计算的时候,都通过函数名来调用函数,会十分麻烦,代码量会很大。所以我们可以做一个转换表:

​
int(*pf[4])(int,int) = {Add,Sub,Mul,Div};//转换表
while(choice == 1 || choice == 2 || choice == 3 || choice == 0)
{
    int x = 0;
    int y = 0;
    scanf("%d %d", &x, &y);
    int ret = pf[choice](x,y);
    printf("%d", ret);
    scanf("%d", &choice);
{

​

函数指针数组pf存放了四种运算函数,通过choice的值来调用不同的函数即可。

这样一来就可以有效减少代码量了,并且也具有了较好的可阅读性

回调函数

定义:通过传入的函数指针,在函数体内再调用函数的函数。

qsort库函数

qsort即快速排序(quick sort),包含在头文件<stdlib.h>中。可对数组进行快速的自定义的不限制于数据类型的排序。

参数

数组指针base,元素个数num,元素类型大小size,比较函数compar。

其中compar函数需要程序员自己编写,依次从数组里传入两个元素到compar中进行比较,比较的方式取决于程序员。

举个例子,让我们来使用一下这个函数(代码可以不用细看,听我娓娓道来~):

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int int_cmp(const void* x, const void* y)//排整型
{
	if (*(int*)x > *(int*)y)
		return 1;
	else if (*(int*)x == *(int*)y)
		return 0;
	else
		return -1;
}
int double_cmp(const void* x, const void* y)//排浮点型
{
	if (*(double*)x > *(double*)y)
		return 1;
	else if (*(double*)x == *(double*)y)
		return 0;
	else
		return -1;
}
int string_cmp(const void* x, const void* y)//排字符串
{
	int ret = strcmp((char*)x, (char*)y) == 0;
	if (ret > 0)
		return 1;
	else if (!ret)
		return 0;
	else
		return -1;
}
typedef struct Stu
{
	char name[20];
	int score;
	int age;
}Stu;
int stu_cmp(const void* x, const void* y)//排结构体
{
	Stu* p = (Stu*)x;
	Stu* q = (Stu*)y;
	if (p->score == q->score)
	{
		if (strcmp(p->name, q->name) == 0)
		{
			if (p->age > q->age)
			{
				return 1;
			}
			else if (p->age == q->age)
				return 0;
			else
				return -1;
		}
		else if (strcmp(p->name, q->name) > 0)
			return 1;
		else
			return -1;
	}
	else if (p->score > q->score)
		return 1;
	else
		return -1;
}
int main()
{
	int num1[10] = { 3,7,4,8,0,5,2,6,1,9 };
	double num2[10] = { 3.53,6.23,9.23,2.34,4.4,0.55,0.123,2.86,14.4, 8.8};
	Stu stus[5] = { {"Tom",100,14},{"Alex",105,18},{"Lily",120,17 },{"Ben",98,15},{"Kiki",98,16} };
	qsort(num1, 10, sizeof(int), int_cmp);//排列整型数组
	qsort(num2, 10, sizeof(double), double_cmp);//排列双精度浮点型数组
	qsort(stus, 5, sizeof(Stu), stu_cmp);//排列结构体
	//分别输出排列后的结果
    for (int i = 0; i < 10; i++)
	{
		printf("%d ", num1[i]);
	}
	printf("\n");
	for (int i = 0; i < 10; i++)
	{
		printf("%.2lf ", num2[i]);
	}
	printf("\n");
	for (int i = 0; i < 5; i++)
	{
		printf("%d  ", stus[i].age);
		printf("%s  ", stus[i].name);
		printf("%d  ", stus[i].score);
		printf("\n");
	}

运行结果:

以上代码中,我自己定义了4个比较函数,分别用来比较整型,双精度浮点型,字符串和结构体stu

int int_cmp(const void* x, const void* y);
int double_cmp(const void* x, const void* y);
int string_cmp(const void* x, const void* y);
int stu_cmp(const void* x, const void* y);

 它们同为int(*)(const void*, const void*)类型,因此都可以传入qsort作为compar函数指针进行回调。我们拿int_cmp来举例,其他的同理也可以解决。

对compar的void*参数进行强制类型转换

这个qsort排列的方式对于要排列的元素数据没有限制的原因,就是因为compar这个函数的的参数是void*,void*类型虽然不可以对其进行任何操作,但是可以进行强制类型转换后在进行操作。

而我们知道,强制类型转换不会改变内存中存放的二进制数据,而是改变对这些二进制数据的解读方式,这对于任何一种强制类型转换来说都是一样的。

而且,只要是在同一环境下运行所有类型的指针所占用的字节数都是一样的(32位环境4字节,64位环境8字节),所以按照程序员需求直接强制类型转换为需要的数据类型即可。

compare的返回值决定排序方式

如上图,compar的返回值只有三种情况

若等于零,则两个元素不交换位置;

若大于零,传入compar的第一个元素会被放在第二个元素后面

若小于零,传入compar的第一个元素会被放在第二个元素的前面

因此,这里的int_cmp最终的效果是实现升序排列如果要降序,那么就让 x>y 时返回-1,让x<y 时返回1即可,也就是和原代码反过来

总结,qsort就是一个典型的回调函数,通过传入的函数指针compar,回调程序员自己定义的比较函数,最后根据返回值对数组进行排列。

我对qsort函数进行了一次模拟实现,有兴趣的可以自己看看。

实际上qsort函数是如何实现排序的,它的底层代码是什么是要取决于编译器的

我的模拟实现是以冒泡排序为基础算法来完成的。

void Swap(void* element1, void*element2, size_t ele_size)
{
	char* p = (char*)element1;
	char* q = (char*)element2; 
	for (int k = 0; k < ele_size; k++)
	{
		char tmp = *(p + k);
		*(p + k) = *(q + k);
		*(q + k) = tmp;
	}
}

void my_qsort(void* base, size_t ele_count, size_t ele_size, int cmp_fun(const void*, const void*))
{
	for (int i = 0; i < ele_count - 1; i++)
	{
		for (int j = 0; j < ele_count - 1 - i; j++)
		{
			if (cmp_fun((void*)((char*)base + j * ele_size), (void*)((char*)base + (j + 1) * ele_size)) > 0)
			{
				Swap((void*)((char*)base + j * ele_size), (void*)((char*)base + (j + 1) * ele_size), ele_size);
			}
		}
	}
}

(仅供参考)

野指针及其成因

指针内容已经接近尾声,结束之前,我们还要了解一个危险的家伙——野指针。它的出现可能会对程序带来极大的不安全和不合法性喔

什么是野指针?

若某个指针变量指向的内存空间没有数据的或者说是没有初始的,就称这个指针为野指针

野指针怎么产生的?

定义了指针变量但没有初始化我们都知道没有初始化的变量,里面保存的值可是随机的,没初始化的指针变量就会随机地指向里的某一块内存

越界访问。如下代码3:

int num[10] = { 0 };
int* p = &num[9];
p = p + 1;

p已经指向了数组末尾,再+1往后走,就越界了,数组外的空间可是没有使用过的喔

指针变量指向的内存空间不再使用了。如下代码4:

int a = 10;
int* p = &a;
free(&a);//这个函数的作用是将a的所占的空间释放,也就是把a清除掉了。

a已经被清除掉了,但是p还是指向原本a的地址,原来的地址已经没有数据了

接受了已经销毁的局部变量地址。如下代码5:

int i = 0;
int* p = NULL; 
while(i < 10)
{
    int ret = i;
    p = &ret;
}

ret为while里面的局部变量,出了循环之后就会销毁,销毁后p指向的就是没有数据的内存空间。同理,指针变量接受了函数里的局部变量的地址,或者形参的地址时,函数调用结束,局部变量和形参销毁了,也会导致野指针的出现。

如何避免野指针的出现

!!记得使用NULL!!

定义时记得初始化如果没有能指向的变量,也要用NULL进行赋值。

指向的内存销毁之后,要置NULL

NULL是void*类型,指向的空间是0x00000000,因此指向0x00000000的(也就是赋值了NULL的)指针变量,无法进行任何访问和操作,从而保障程序运行时的安全合法

结语

关于指针的内容已经圆满完成!!因为篇幅很长,所以分开了两篇发布。

上一篇传送门🚪——》大集合!!C语言指针知识要点大合集!!小白不要错过喔!!收藏这一篇就足够!!(1)-CSDN博客

有什么疑问和困惑欢迎来评论区留言!!🤩我一定尽力及时解答!!制作不易,求关注!!求点赞!!之后还会有更多有用的干货博客会发出哦!!欢迎做客我的主页!!❤❤Elnaij-CSDN博客❤❤

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值