指针进阶
讲前声明
在我前面的博客中也讲了指针初阶,那一篇博客讲的东西都比较浅显,还没那么深,但是足以让小白们应付学校的考试。
那么这一篇的博客就会讲的比较深一点。如果觉得这篇内容还可以,点个赞再走呗😊。
首先我们可以从指针的表面看到一些东西。
比如说整形指针,这里我们可以知道该指针指向的是整数类型的量,再比如说数组指针,那么这个指针指向的就是数组,好了打住,在多说点就有点空谈了。
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语言视频。