【C语言进阶】指针进阶

讲前声明

在我前面的博客中也讲了指针初阶,那一篇博客讲的东西都比较浅显,还没那么深,但是足以让小白们应付学校的考试。

那么这一篇的博客就会讲的比较深一点。如果觉得这篇内容还可以,点个赞再走呗😊。

首先我们可以从指针的表面看到一些东西。
比如说整形指针,这里我们可以知道该指针指向的是整数类型的量,再比如说数组指针,那么这个指针指向的就是数组,好了打住,在多说点就有点空谈了。

1. 字符指针

很简单,就是char* ,但是可以出一点很考验基本功的题。
先看一下基本的用法。

int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'w';
    return 0;
}

就这样,很基础的东西。
但是我们还可以把char*指向字符串。这就要考考基础了。
先看代码

	char* p1 = "hello world";//常量字符串
	
	char arr[] = "hello world";//数组
	char* p2 = arr;
	

请问,p1 和 p2 的区别是什么?

这就要好好讲讲了

对于p1

p1指向的是一个常量字符串,可不是将"hello world"直接放到p1内的,只是p1指向了"hello world",也就是将这个字符串首元素的地址给了p1,而且这个字符串在内存中是独一份的,无法被修改,如果修改程序则会直接崩掉。就像这样:在这里插入图片描述
那么如何避免这种情况呢?
可以在*前加上const
在这里插入图片描述
这样编译器会直接报错,就无法更改啦。

对于p2
这就是将数组首元素放到了p2中,这个p2的值是可以修改的,这个比较简单,就不讲了。

那么我们来看一道题:

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;
}

这里的结果是什么呢?自己看着先想一想。

在这里插入图片描述

那么来讲一下为啥:
在这里插入图片描述

2. 指针数组

在指针初阶的最后我们提到了这个东西,那么在这就细🔒一下。

根据我最开始讲的,指针数组就是一个存放指针的数组。

先看代码:

char arr1[10];//这是字符数组,存放字符的数组

int arr2[5];//这是整型数组,存放整型的数组

那么什么是指针数组呢:

int* arr3[10];//这是整型指针数组,存放整型指针的数组

char* arr4[5];//这是字符指针数组,存放字符指针的数组

我这里就只列举了两个例子,当然不止这点,还可以有别的类型(short、long、float等等)

那么指针数组怎么来用呢?

先来看一个字符指针数组
在这里插入图片描述
可能有的同学不太理解,那么看图:
在这里插入图片描述

再来看一个整型指针数组
在这里插入图片描述

那么指针数组就讲到这里,只要记住数组里面装的是指针就行了。

再来讲一下指针数组的兄弟,数组指针。

3.数组指针

3.1 数组指针的定义

还是根据我前面说的,根据这个字面意思就知道,我们现在要讲的是一个指针,一个指向数组的指针。

int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?

有的同学看到上面的代码估计就迷糊了,不要迷糊,听我解释:

先不说p1和p2,就说p
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,那么p就叫做数组指针。

这里要注意:[ ]的优先级要高于*号的,所以必须加上()来保证p先和*结合。才能保证p是一个数组指针

那么看例子:
在这里插入图片描述
下面这样就不会报警告了。
在这里插入图片描述

那么上面出现了&arr这个东西,下面就要细🔒这个。

3.2 数组名和&数组名

那么我们都知道,数组名代表数组首元素的地址,那么&arr呢,来看代码:

在这里插入图片描述

看上去好像是一个东西,实则不然,我们给这三个地址+1试试看:

在这里插入图片描述

可以看到,arr+1后只跳过了4个字节,而&arr+1跳过了12个字节。而且arr这个数组有3个元素,每个元素的类型是int(4字节),所以arr的大小就是12个字节,那么我们就可以推断出,&arr代表的是整个数组的地址,而arr仅代表首元素的地址。

3.3 数组指针的使用

既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。(对于一维数组来说,这里的地址指的是&数组名,可不仅仅是数组名)

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
    //但是我们一般很少这样写代码
    return 0;
}

再来个二维数组的例子:

对于普通的二维数组传参
在这里插入图片描述

如果我们用指针来接收的话是这样的:
在这里插入图片描述

下面我讲讲为啥:
首先我们都明白,数组名代表首元素的地址,对于一位数组来说,首元素的地址就是&arr[0],但是对于二维数组来说,首元素的地址可不是简单的&arr[0][0],那么二维数组的首元素的地址是什么呢。
看图:
在这里插入图片描述

所以当我们要用指针来接收二维数组时,就得要用到数组指针,可不是什么二级指针,差远了,但是呢,我们可以看到用指针去接收的话,看起来比较费劲,所以还是推荐大家二维数组传参的时候就直接用两个方括号就行了,不要搞这个数组指针,比较费解。

好,学了这么些指针,数组啥的,奖励你几个代码让你好好理解一下:

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

这些都是什么,你能看出来吗?
若是我上面讲的没懂,那我还是推荐你去B站学学鹏哥的视频(指针进阶那一块)。

4. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

4.1 一维数组传参

我么来看一下,下面的参数给的有问题吗?

void test(int arr[])//ok?
{}
void test(int arr[10])//ok?
{}
void test(int *arr)//ok?
{}
void test2(int *arr[20])//ok?
{}
void test2(int **arr)//ok?
{}
int main()
{
 	int arr[10] = {0};
  	int *arr2[20] = {0};
 	test(arr);
 	test2(arr2);
}

上面的都可以,你答对了吗?

4.2 二维数组传参

void test(int arr[3][5])//ok?1
{}
void test(int arr[][])//ok?2
{}
void test(int arr[][5])//ok?3
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok?4
{}
void test(int* arr[5])//ok?5
{}
void test(int (*arr)[5])//ok?6
{}
void test(int **arr)//ok?7
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
}

1、3、6可以,其他的不行,对了吗?

上面的代码只要牢记住数组名代表的是首元素的地址,每一个数组的首元素地址搞清楚就行了。

4.3 一级指针传参

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;
}

在这里插入图片描述

4.4 二级指针传参

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;
}

二级指针传参
在这里插入图片描述

5. 函数指针

还是顾名思义,函数指针,就是指向函数的指针。
那么怎么实现呢,看图:

首先,得要是个指针,那么*pf代表一个指针,那么指向的类型怎么表示呢:
假如说以这个函数为例:

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

这个函数参数为两个int,返回值为int,那么这些就够了,指针指向的类型就为:int (int,int),这就可以了,那么和到一块就是int (*pf)(int , int),这里把x和y省略了,这是可以的,但是里面(*pf)的括号是不能省略的,如果省略了就不是指针了。

再来个具体点的例子:
在这里插入图片描述

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

看看这两个是什么?

先说第一个
(*(void (*)())0)();
我们从0开始出发,因为这里面一眼能认识的就只有0了,0左边的括号里包含了这个(void (*)()),这个括号里的是函数指针,没有参数,返回值为void,那么加上外面的括号的话,这就是强制类型转换,将0强制类型转换成了void (*)()这个类型,然后剩下了这个(*)();这里是解引用了强转了之后的0,并调用了它。听起来好麻烦,我也尽力在讲了,若没听懂的话,我真没办法。这个代码出自《C陷阱与缺陷》这本书:
在这里插入图片描述

然后看第二个:
void (*signal(int , void(*)(int)))(int);
我们从signal出发,也是因为这个跟语法里的括号啊什么的都不相关,我们也认识这单词,signal的左边是*\,右边是(,那么根据优先级可以判断其是跟(先结合的。那么我们及来看右边的这一对括号包起来的是什么:(int , void(*)(int)),这可以看出来signal就是个函数了,括号里的是两个参数,一个是int的,一个是void(*)(int)的,后者又是一个函数指针,那么再去掉上面讲的东西,还剩下这些:void (*)(int),这个东西看上去是一个函数指针,那么代表着什么呢,其实是signal这个函数的返回值,不要问我为什么,龟腚。
这个东西的出处我就不知道是哪了。

6. 函数指针数组

首先还是顾名思义,这讲的是一个数组,数组内存放的是函数指针。

如何定义呢,看图:
在这里插入图片描述
在这里插入图片描述
int(*pf[])(int, int),这里要注意的是(*pf[])的括号不能省略。

讲这么些就够了。直接上例子:
在这里插入图片描述
运行起来是这样的:
在这里插入图片描述

7. 指向函数指针数组的指针

首先,这是个指针所以还是*pf开头,那么指向的是什么?根据名字我们可以得知指向的是函数指针数组,这个怎么表示呢? 我们还是以int Add(int, int)和int Sub(int, int)这两个函数为例子,是这样表示的:int (*()[2])(int, int), 然后我们就再把*pf放到括号里就行了。
就是这样:int (*(*pf)[2])(int, int)。

看图:在这里插入图片描述

其实这些用处不会很多,只是为了以防万一以后看到了不知道是什么。
像上面的还可以继续套娃,意思就是我们刚开始学的是指针->指针数组->指针数组指针->指针数组指针数组……这些都是可以实现的,只不过套得越多,用的就越少,就不再继续套了,讲到这里就够了。

8.回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

那么简单理解呢,就是A和B两个函数,我们在B的参数中给了一个形参p,该形参的类型是A函数类型的指针,并且在B函数中我们通过该指针p使用了A函数。那么当我们在调用B函数时,实参传的是A函数,这时候A就是回调函数。

好像讲的不是很清楚,没关系,看图:
在这里插入图片描述

那么上面的看懂了之后我们就来把qsort(quick sort)这个库函数讲一讲。
首先这个函数是用来排序的,任何类型都可排序,整型的,字符型的,结构体的等等。
qsort原理是用快排来实现的,也就是快速排序,排序的算法我会以后再讲,等到开数据结构了再讲这些东西。我么现在比较常见的冒泡排序也在这些算法里面。

在这里插入图片描述

这里面有四个参数,base,num,size,compar。两个指针,两个size_t。
在这里插入图片描述
翻译过来是这样的:
在这里插入图片描述

这里的compar的函数需要自己来定义,不然没法使用qsort。

看例子:
在这里插入图片描述

上面这个例子提到了void*这个指针,下面细🔒:
在这里插入图片描述在这里插入图片描述
这时候我们可以将两者结合一下,void*p类型的指针可以接受任何类型的地址,将p强制类型转化为char类型,我们就可以访问一个字节,p+n就可以访问p后的第n个字节。
如果前面这句话听不懂的话,我建议先停下来看下这篇博客指针进阶(这个可以点)中的指针加减整数。

下面我们来用冒泡排序的原理来模仿实现一下bsort(bubble sort)。

//变量两两交换
void swap(char* p1, char* p2, int sz)
{
	for (int i = 0; i < sz; i++)
	{
		char temp = *p1;
		*p1 = *p2;
		*p2 = temp;

		p1++;
		p2++;
	}
}

//对比两个变量的大小
int cmp(const void* p1, const void* p2)
{
	return *(int*)p1 - *(int*)p2;
}

//冒泡排序模拟实现任何类型的排序
void bsort(void* base, size_t num, size_t width, int (*cmp)(const void* p1, const void* p2))
{
	for (size_t i = 0; i < num - 1; i++)
	{
		int flag = 0;
		for (size_t j = 0; j < num - 1 - i; j++)
		{
			//这里传参时,先强制类型转换成char*类型,这样可以一个字节一个字节的访问
			//然后加上j×width就是第j个位置的变量,width是元素的大小,乘以width是为了向后挪width字节的空间
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
				flag = 1;
			}
		}
		if (flag == 0)
		{
			break;
		}
	}
}

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

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

	return 0;
}

这个运行起来是这样的:
在这里插入图片描述

上面的bsort的用法和qsort的用法一模一样,只是当我们对比不同的数据类型时需要将对比的函数自己来写一下,这样就可以很好的使用了。

好了,知识点就是这么多,我已经是尽力讲了,如果上面的哪里比较模糊,就去B站看对应的鹏哥的C语言视频。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

先搞面包再谈爱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值