速通C语言第八站 一篇博客带你掌握指针进阶

系列文章目录

 速通C语言系列

 速通C语言第一站 一篇博客带你初识C语言        http://t.csdn.cn/N57xl

 速通C语言第二站 一篇博客带你搞定分支循环   http://t.csdn.cn/Uwn7W

 速通C语言第三站  一篇博客带你搞定函数        http://t.csdn.cn/bfrUM

速通C语言第四站  一篇博客带你学会数组          http://t.csdn.cn/Ol3lz

 速通C语言第五站 一篇博客带你详解操作符      http://t.csdn.cn/OOUBr

速通C语言第六站 一篇博客带你掌握指针初阶   http://t.csdn.cn/7ykR0

速通C语言第七站 一篇博客带你掌握数据的存储 http://t.csdn.cn/qkerU



感谢佬们支持!

文章目录

  • 系列文章目录
  • 前言
  • 一、字符指针
  • 二、数组指针
  • 三、指针数组
  • 四、数组传参和指针传参
  •            一维数组传参
  •            二维数组传参
  •            一级指针传参
  •            二级指针传参
  • 五、函数指针
  •           如何调用
  •           两段有趣的代码
  •           总结
  • 六、函数指针数组
  •          怎么用?
  • 七、指向函数指针数组的指针
  • 八、回调函数
  •          例:qsort快速排序(库函数)
  • 总结

只看目录,大家就能知道这篇博客有多高能了

 


 前言

 上一篇博客中我们了解了数据的存储,这篇博客是指针进阶,就是指针的天花板,很有难度,大家系好安全带准备上车啦~


一、字符指针

原来我们使用时为

char ch='q';
char*pc=ch;  //单个字符

现在我们

char*ps="hello YiGang";

他可以把hello YiGang的首字符地址h存了,这波就像数组的访问形式一样

我们给到一个题:


	char str1[] = "hello YiGang";
	char str2[] = "hello YiGang";

	if (str1 == str2)
	{
		printf("same\n");
	}
	else
	{
		printf("nope\n");
	}

	char*str3= "hello YiGang";
	char* str4 = "hello YiGang";

	if (str3 == str4)
	{
		printf("same\n");
	}
	else
	{
		printf("nope\n");
	}

这个程序将输出什么呢?

 为什么呢?

刚才我们讲过,字符指针可以存字符串首字符的地址,由于hello YiGang 是常量字符串,所以其首字符的地址是固定的,所以str3==str4.

而str1和str2分别为两个数组,所以字符串为各自存的元素,存的地址不同,所以其首字符的地址也不同。

 


二、指针数组

之前我们学的数组指针为

//每个元素为int*
	int* arr[3];

还有:

int a = 10; int b = 20; int c = 30;	
int* arr[3] = { &a,&b,&c };

显然,上面的写法很挫

我们可以将数组的地址存入数组

int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7 };

int* arr[3] = { a,b,c };

由于数组名是首元素的地址,所以我们直接将a b c存进去

由此,我们就模拟出了二维数组一样的东西,但是相较于二维数组,这个东西不连续

我们打印一下

for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 5; ++j)
		{
			printf("%d ", *(arr[i] + j));//  ->  printf("%d ",arr[i][j]);
		}
		printf("\n");
	}

成功打印


三、数组指针

是一种指针,类比整形指针是指向整形的指针,数组指针是指向数组的指针

例:

int arr[10] = { 1,2,3,4,5 };
	int(*parr)[10] = &arr;

此处,parr就是一个数组指针,他存的是arr的地址,即arr[0]的地址

同理

double* d[5];//每个元素为double*
double*( (*pd) [5]) = &d;

那我们如何通过数组指针访问数组的每个元素?

例:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int(*parr)[10] = &arr;

	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *( (*parr) + i));
	}

即(*parr)拿到arr,然后通过arr+i 来访问每个元素。

 (成功打印)

但是,相比直接打印,这种方法太恶心了。


数组指针放在二维数组中会好用很多

例:

	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };

我们写一个打印函数来打印他

//将首行的地址、行数、列数传为参数
	Print(arr, 3, 5);
void Print(int arr[3][5], int row,int col)
{
	for (int i = 0; i < row; ++i)
	{
		for (int j = 0; j < col; ++j)
		{
			printf("%d ", *((*arr+i)+j));
		}
		printf("\n");
	}
}

arr为首行的地址,解引用拿到首行地址 后可以通过+i 来拿到 第二行、第三行的地址

拿到每行的地址后再通过 加 j来拿到每个元素。

画一波图


当然,我们也可以这么打印

printf("%d ",arr[i][j]);

四、数组传参和指针传参

写代码时难免要把数组/指针传给函数,那函数参数应该如何设计?

一维数组传参

假设我们要传一个一维数组

int arr[10] = { 0 };
	test1(arr);

那么该用什么参数接收呢?

1

void test1(int arr[]) //彳亍?

彳亍,由于传过去的仅为首元素地址,我们可以不写 [ ] 中的10.

2

void test1(int arr[10]) //彳亍?

彳亍,写上10也可以

3

void test1(int *arr) //彳亍?

彳亍,传首元素地址,当然可以用一级指针接收


给一个指针数组的例子

int *arr2[20]={0};
test2(arr2);

那么这个又该用什么参数接收呢?

1

仿照刚才的操作

void test2(int *arr[20])
void test2(int *arr[])

这样肯定是可以的

2

那么该如何用指针接收呢?

这个指针数组的元素为一级指针,取一个一级指针的地址,我们应该用二级指针来接收,即

void test2(int **arr)

注:在我们学习数据结构的单链表时,将会用到这个操作


二维数组传参

int arr[3][5] = { 0 };
	test3(arr);

 传二维数组,传过去的为第一行的地址,所以我们的参数要匹配的是第一行的地址。

1

显然,我们依然可以无脑直接传

void test3(int arr[3][5])

2

当然,我们也可以省略传,但是这波有一个注意的点,就是行省列不省

什么意思呢?

就是传参的时候第一个 [ ] 的3可以省略不写,但第二个 [ ] 的5必须写。

为什么?

因为不写行只写列,即告诉编译器每列5个元素,他依然可以根据你的元素个数推演出你有几行,反之则不行

所以

void test3(int arr[][5])
//彳亍

3

那么该如何用指针接收呢?

由于我们传过去的为第一行的地址,而第一行可视为一个一维数组,所以我们需要的指针要指向一个一维数组。只想一个数组的指针就是我们上面学到的数组指针。

	void test3(int(*arr)[5])
//彳亍

一级指针传参

一级指针传参,我们要用一级指针接收

例:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	int* p = arr;//p是一级指针
	Print(p, sz);
void Print(int *ptr,int sz)
{
	int i = 0;
	for (i = 0; i < sz; ++i)
	{
		printf("%d ", *ptr + i);
	}
}

  (成功打印)


那么假设函数的参数是一级指针,我们可以传什么过去呢?

1 一级指针

test(p);

2 数组

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	test(arr);

3 传地址

int a = 10;
	test(&a);

二级指针传参

二级指针传参,当然也是二级指针接收

例:

int a = 10;
	int *pa = &a;
	int** ppa = &pa;
	test(ppa);
void test(int** p2)
{
	**p2 = 20;
}

那么假设函数的参数是二级指针,我们可以传什么过去呢?

1 二级指针

test(ppa);

2 一级指针的地址

test(&pa);

3 存一级指针的数组

int* arr[10] = { 0 };
	test(arr);

五、函数指针

指向函数的指针 / 存放函数指针的指针

例:

我们先搞一个函数

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

&Add即为Add函数的地址

printf("%p\n", &Add);
	printf("%p\n", Add);

  结果竟然一样!

这波其实很类似数组

啊但是!

虽然打印结果一样,但我们知道本质上arr和&arr不同

但是函数名是完全等于&函数名的

怎么写?

此时,pf就是一个函数指针


如何调用?

	int ret = (*pf)(3, 5);

由于函数名是完全等于&函数名

我们可以直接

	int ret = pf(3, 5);

两段有趣的代码

(*( (void ( * )()) 0)();

刚看到这行代码,我们的第一反应肯定是把电脑扔了,立即推放弃学C语言

先别急,别的我们不认识,0我们肯定认识

0前面有半个括号,前面我们找见剩下半个括号,中间的东西是

void (*) ()

好像就是我们刚刚才学过的函数指针,返回类型为 void,没有参数

既然括号里是函数指针,说明他想用括号离得东西对0强转,所以0变成了一个函数

再在外面加 * 解引用,就是对这个函数进行调用,由于是void 类型,所以调用时传不了参,所以在最后补一个 ()


总结

1 void(*)() 表示一个 没有参数的函数指针类型

2(void(*)())0   对0 强转,转换为一种函数的地址

3 *((void ( * )()0      再加一颗 *,即为进行解引用操作,也就是找到了这个函数

4 (*((void ( * )()0)();   调用0地址的函数,由于函数没有参数,所以我们最后一个括号没东西。


void (*signal(int,void(*) (int))) (int);

这次我们很明显就能看出,外面为

void (* ~ ) (int);

显然,外面是一个函数指针

接下来我们看看里面是什么,signal 和()先结合,说明signal是函数名,通过里面的那个逗号,我们知道signal函数有两个参数,第一个是int,而第二个为

void (*) (int)

显然是一个函数指针,其返回类型为void,参数为int 。


总结

1  signal 和()先结合,说明signal是函数名

2  signal的第一个参数为int,第二个为一个函数指针,这个函数指针指向一个参数为int,返回类型为void的函数

3 signal的函数返回类型也是一个函数指针,该函数指针指向一个参数为int,返回类型为void的函数

4 由于最后为分号,所以这个东西是函数的声明。


六、函数指针数组

存放函数指针的数组

例:

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

int Sub(int x, int y)
{
	return x - y;
}
//pf1就是一个指向函数的指针
	int(*pf1)(int, int) = Add; 
	
	int(*pf2)(int, int) = Sub;

	int (*pfArr[2])(int, int) = { Add,Sub };

由于Add和Sub返回类型、参数一样,所以同类型,可以存在一个数组里


怎么用?

我们实现一个对整形变量加减乘除的计算器

先把不重要的目录和加减函数的实现搞出来

//加
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("1 Add  2 Sub\n");
	printf("3 Mul  4 Div\n");
	printf("0 exit\n");
}

按我们之前的思路,实现计算器必然是用一个do~while循环,里面再用一个switch~case来分支不同运算。

但是每个运算都写个case实在是太冗余了

所以这个时候我们用一个函数指针来操作

int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };

(为了和目录匹配,我们在第一个元素补一个NULL)

nt main()
{
	int input = 0;
	do
	{
		//目录
		menu();
		//函数指针数组
		int (*pfArr[5])(int, int) = { NULL,Add,Sub,Mul,Div };
		printf("请输入\n");
		//输入
		scanf("%d", &input);
		//操作数
		int x = 0;
		int y = 0;
		scanf("%d %d", &x, &y);
		//结果
		int ret = 0;
		ret = (pfArr[input])(x, y);
		printf("%d", ret);

	} while (input);

	return 0;
}

这样,我们就可以通过下标来访问函数

例:

补充:在《C和指针》这本书中,称这种操作为转移表

函数指针数组的一个最经典的应用,就是C++中的虚函数表,这个等我们到继承多态的时候就能知道辣~


七、指向函数指针数组的指针

函数指针数组是个数组,所以我们可以取出他的地址

例:

我们先给一个函数指针数组

	int(*parrf[4]) (int, int);

我们用p3取出他的地址

那我们该如何书写呢?在上面的基础上操作,由于parrf会结合 [ 4 ]成为数组,所以我们应该加一个括号不让parrf和 [ 4 ]结合,然后再补一个*即可

int (*(*p3)[4]) (int,int)=&parrf;

八、回调函数

  回调函数就是一个通过函数指针调用的函数,如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其指向的函数时,我们就说这是回调函数

通俗来讲,我写了个A函数,但是我不直接调用!我把A函数的地址给了B函数,

然后我通过B函数来调用A函数,那B函数的形参就是函数指针,我们称A函数为回调函数

例:我们不用函数指针数组搞计算器了,继续用switch~case,那如何解决每个case的冗余问题?

写一个calc 函数来封装输入输出的操作,然后将4个运算函数的地址作为函数参数传入,每个case对应不同的运算函数。

比如:


case 1;
//加
ret = calc(Add);
printf("%d",ret);
break;

实现一波calc函数

int calc(int* (pf)(int x, int y))
	{
		int x, y = 0;
		printf("输入两个数\n");
		scanf("%d %d", &x, &y);
		return pf(x, y);
	}

尝试一波

(成功)

 其他运算函数也同理,我们也将他们分装到4个case中,大家可以仿照上面自己敲一敲。


再例:qsort快速排序(库函数)

qsort可以排任何类型,整形、字符串,甚至结构体也可以。

第一个参数代表首元素地址,第二个为元素个数,第三个为一个元素为几个字节

那第四个参数是什么?

由于qsort可以排任何类型的数据,但是库函数的实现者怎么知道我们以什么方式比较?所以我们需要自己写两个元素的函数。

例:我们写给一个数组的例子

		int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//个数
int sz = sizeof(arr) / sizeof(arr[0]);

		qsort(arr, sz, sizeof(arr[0]), cmp_int);

针对 cmp_int 的实现

定义第一个数大于第二个数我们就返回 大于0的数

定义第一个数等于第二个数我们就返回 等于0的数

定义第一个数小于第二个数我们就返回 小于0的数

(升序:前减后    降序:后减前)

所以

int cmp_int(const void*e1,const void *e2)
	{
		return *(int*)e1 - *(int*)e2;
	}

打印一波试试

for (int i = 0; i < 10; ++i)
		{
			printf("%d ", arr[i]);
		}

(成功排序)


 我们再举一个结构体的例子

先创、初始化一个结构体

struct stu
{
	char name[20];
	int age;
};

void test4()
{
	struct stu s[] = { {"WenXiao",69}{"ZhangYiGang",20},
     {"DiaoMao",18},{"ZhaoZhiMing",6} };
}

怎么排序?

我们可以按年龄排

实现一波比较函数

int sort_age(const void* e1, const void* e2)
{
	return ((struct stu*)e1)->age - ((struct stu*)e2)->age;
}

排序:

int sz = sizeof(s) / sizeof(s[0]);
	qsort(s, sz, sizeof(s[0]), sort_age);

运行后


我们还可以按名字首字母排

	qsort(s, sz, sizeof(s[0]), sort_name);

比较字符串时,我们要用strcmp函数

int sort_name(const void* e1, const void* e2)
{
    return strcmp (((struct stu*)e1)->name, ((struct stu*)e2)->name);
}

运行一波


总结

 做总结,今天这篇博客带大家上到了指针的天花板,各种抽象的套娃概念,大家要仔细看过

而指针的恶心题目也是面试题中喜欢出的,下篇博客我将给大家带来 指针练习题。

水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。

每日gitee侠:今天你交gitee了嘛

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值