C语言——指针详解

目录

概念普及

内存

指针变量

指针的类型

指针 +-

void* 指针

const修饰指针变量

深入了解   const 与 * 

指针运算

指针加减整数

指针 - 指针

指针关系比较

野指针

指针未初始化

指针越界访问

指针指向的空间被释放

如何规避野指针

1. 指针在创建出来时应该初始化

2. 小心指针越界

3. 避免指针指向局部变量

assert 断言

传值调用与传址调用

strlen函数模拟实现(传址调用)

指针进阶

二级指针

指针数组

指针数组模拟实现二维数组

字符指针

数组指针

二维数组传参的本质

函数指针

两段有趣的代码

函数指针数组

函数指针数组的用法——计算器

回调函数

结语


概念普及

今天我们来介绍一个新的东西——指针,这是一个可以通过地址找到对应内容的东西

先来看点有意思的东西:

1. int *p   这是一个整形指针,p与*结合代表p是一个指针,int代表这个指针指向的内容是一个int型的变量

2. char *p 同理,p与*结合代表p是一个指针,char表示p指向一个char类型的变量。再来

3. int**p  这是一个二级指针,p与*结合之后,说明这是一个指针,指向的是一个int*类型的变量,也就是指向一个整形指针

4. int * p[ ]  这是一个指针数组,是一个数组!p先与[ ]结合说明这是一个数组 (因为[ ]的优先级>*),然后里面放的元素都是int*,也就是全是int型的指针

5. int(*p)[ ]  这是一个数组指针,是一个指针,指向一个数组,因为有(),所以p与*先结合,说明这还是一个指针,然后再看外面,就是int()[ ],就是一个数组且元素都为int,所以这个名字叫做p的指针指向一个元素类型为int的数组

6. int (*p) (int, int)   这是一个函数指针,p与*先结合说明这是一个指针,然后外面的int ( ) (int, int)说明只是一个函数,需要传递两个类型为int的参数,返回类型为int

7. int (*p[ ]) (int, int)  这是一个函数指针数组,p与[ ]先结合表示这是一个数组,而后剩下的int(*)(int,int)表示这是一个函数指针,即这是一个元素类型为函数指针的数组

8. int (* (*p) [ ]) (int, int)   这是一个函数指针数组指针,顾名思义,就是一个指向函数指针数组的指针,p与*结合说明是一个指针之后,剩下的就是int (*  变量名  [ ]) (int, int),一个函数指针

如果你是一个萌新的话,相信你看到这里已经有点懵了,上述每一个我都会在下文具体讲解,接下来我们先从基础的开始讲起

内存

在介绍指针之前,我们需要先知道指针到底是什么?一个形象的例子:如果一栋宿舍楼,你要找一个朋友但是不知道地址,那就只能一间一间找,相当浪费时间。而如果,你知道了你的朋友在3105或是在C608,那是不是就可以直接去到对应的宿舍从而找到你的朋友啊!

指针,就是一个记住了宿舍门牌号的小纸条,通过这个小纸条,我们就可以找到其对应的宿舍,而记在上面的门牌号就是所谓的地址

而在计算机当中,会有32或64根地址线,每一根都为0或1,计算机会将所有的0或1随机排列做成一个地址,比如0x006FFD22就是一个地址,也就是0000  0000  0110  1111 1111 1101 0010  0010,当然,32位代表有32根地址线,64位的就代表有64根地址线。指针就是放置地址的一个变量

指针变量

现在我们知道指针变量是用来放地址的了,接下来讲讲我们该如何取出地址以及如何将地址放进指针中,如下:

#include<stdio.h>

int main()
{
	int i = 0;
	printf("%p\n", &i);
	return 0;
}

&,使用这个符号就可以取出变量的地址,如上代码中的 &i 就是将变量 i 的地址取出来之后,通过%p的形式将 i 的地址打印出来

而现在我们已经有了 i 的地址,接下来我们就需要将其存进指针中,如下:

int(类型) * p (变量名) = 需存入的地址

#include<stdio.h>

int main()
{
	int i = 6;
	int* p = &i;
	return 0;
}

这时候来看指针变量本身,将其拆解一下:int*    p,p代表变量名,int*则代表这是一个指向int型的指针,或者换一种说法

将其拆解成 int    *p,那么 *p 代表这是一个指针变量,而 int 则代表这个指针指向的类型是一个整形,所以这是一个整型指针

现在我们知道了如何将地址存进指针中,那我们该如何通过这个地址找到对应的变量呢?

*,这个符号的名字叫做解引⽤操作符,通过这个符号我们就可以找到对应的变量,如下:

int main()
{
	int i = 6;
	int* p = &i;
	printf("i=%d\n", *p);
	return 0;
}

可以看到,通过*p,我们找到了 i 的值并且将其打印了出来,而如果要改变 i 的值的话,通过指针也是可以做到的

int main()
{
	int i = 6;
	int* p = &i;
	printf("i=%d\n", *p);
	*p = 3;
	printf("i=%d\n", *p);
	return 0;
}

或许有人会说,如果要改变 i 的值的话,为什么不能直接i=3而是要选择指针的方法呢?这是因为,我们是通过地址来改变 i 的值的,如果我们现在面对的是一个函数传参,我们都知道形参是实参的一份临时拷贝,如果传值调用所要传的值过大的话,就会很耗时间。而如果是传址调用的话,我们就只传了一个地址过去,4/8个字节,大大降低了所需要消耗的时间。

指针的类型

在平时我们会看到很多类型的指针:int*,char*,double*......

难道指针也分高低贵贱?这些类型又代表什么?

先回答第一个问题,指针中没有高低贵贱之分,每一个指针的大小都是4/8个字节,因为存的都是地址,在32位的机器里有32个比特位,每8个比特位又等于1个字节,所以大小为4个字节。而如果是在64位的机器下,就有64个比特位也就是8个字节

既然指针的大小都是相同的,又为什么要创造出那么多类型呢?

我们可以这么理解,指针的类型,代表着他要去寻找对应变量时的步长,如下:

当我内存里存的是44 33 22 11(小端存储)时,char* 就只能访问到最前面的11,因为char*的步长就是一个字节

而int*能访问到全部,因为int*的步长为4个字节,所以刚好能访问11 22 33 44

再来看一个例子:

#include<stdio.h>

int main()
{
	int n = 0x44332211;
	char* pc = (char*)&n;
	*pc = 0;
	return 0;
}

通过调试我们会看到,char*只改变了最前面的11,因为其步长只有一个字节

#include<stdio.h>

int main()
{
	int n = 0x44332211;
	int* pc = &n;
	*pc = 0;
	return 0;
}

但是当我们换成int*的时候,我们就发现全都被改了,因为int*的步长是4个字节

指针 +-

当我们有一个数组如下时:

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

我们可以将其首元素地址拿出来,也就是arr(数组名大部分情况下代表的都是首元素地址,具体的后面数组指针部分会细讲),此时我们得到了元素1的地址,那我们该如何找到其他元素呢?

或许你会说,可以用for循环与arr[ i ]来找到全部元素,但是我们今天来学一个新的方法:指针

#include<stdio.h>

int main()
{
	int arr[5] = { 1, 2, 3, 4, 5 };
	*(arr + 1) = 0;
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", *(arr + i));
	}
	return 0;
}

首先我们需要知道,数组的地址是连续开辟的,如上,而当我们将arr进行加减运算时,arr就会指向下一个地址对应的东西,如果指向的地方是我们没有开辟的,那么这时系统就会报错,这样的指针就叫做野指针。

我们再来看一串代码

int main()
{
	int i = 10;
	char* a = (char*)&i;
	int* b = &i;

	printf("%p\n", &i);
	printf("%p\n", a);
	printf("%p\n", a + 1);
	printf("%p\n", b);
	printf("%p\n", b + 1);
	return 0;
}

我们可以看到,指针变量相加减的结果还是一个地址,不过加减要看具体的指针类型,如上的char*指针,每次加减就只能加减一个字节。而int*的指针每次加减能加减4个字节,这时因为步长不同所导致的。

void* 指针

在指针家族中还有一个特殊的例子,就是void*(无类型指针),这种指针可以接受任意类型的地址,或许我们可以将其粗俗地理解成是一个垃圾桶,因为什么类型的地址都可以放在void*的指针中。如下:(编辑器没有报错,证明a的地址存进p里了)

这样的指针有什么用呢?了解过qsort(快速排序)的朋友应该会知道,这是一种可以排序任意类型数据的函数,而其实现时最关键的就是void*——因为qsort函数不知道要排序的是什么类型的数据,所以会用void*指针去接收数据,而后再强制类型转换成使用者需要的类型的指针,qsort函数的具体实现各位可以看看这篇博客:

https://blog.csdn.net/2302_80023639/article/details/133972117?spm=1001.2014.3001.5501

void*指针也有一点很特殊,就是他不能进行加减运算和解引用

如果要进行运算或解引用的话,就需要将其强制类型转换,如下:

注意!此处不能写成(int*)p++,因为p会与++先一步结合

const修饰指针变量

在生活中,有诸多常量比如20(数字),同时也有变量,比如n=20

const,就是一个给变量加上常属性。比如const int b = 20。加上const之后我们的变量 b 就不能够再被改变了。但是,b 的本质还是变量,只是带上了常属性而已。

但是如果我非要改变这个 b 变量可不可以?当然,如下:

int main()
{
	int a = 0;
	a = 20;
	const int b = 5;
	int* p = &b;
	*p = 0;
	printf("%d\n", b);
	return 0;
}

我们会看到,明明变量b的前面加上了const,但是他的值还是被改了,我们是先将b的地址放进了指针p中,然后通过指针将b变量进行了修改。这就相当于说你只能走正门进去,正门锁起来了,所以你翻墙进去了一样。这样子写代码是非常危险的,对此,我们可以这样:

int main()
{
	int a = 0;
	a = 20;
	const int b = 5;
	const int* p = &b;//指针前加上const
	*p = 0;
	printf("%d\n", b);
	return 0;
}

深入了解   const 与 * 

const int *p 与 int const *p是一样的,都是在 * 的左边,修饰的都是*p,也就是你不能改变指针 p 所指向的内容

如上,如果const在 * 的左边,那我们就无法通过指针去改变a的值,即10是无法通过指针改变的。但是我们我们可以修改存在p内部的地址——0x0012ff34。假如我这时有另一个变量b,我将&b放进指针p中可以吗?当然可以,因为const修饰的是 *p 也就是p指向的内容

如果,const在 * 的右边,那么这个const修饰的就是p,这时我有另一个变量b,想将b的地址再放进指针p中还可以吗?答案是不行但是我们此时可以修改p所指向的变量的值,也就是说我可以将 a 改成其他任意整数。

如果我此时霸道一点,在 * 的左边和右边都加上const,那这时代表的就是:p与p所指向的变量都不能被更改

//const在*左边
int main()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;
	p = &m; 
	return 0;
}

//const在*右边
int main()
{
	int n = 10;
	int m = 20;
	int * const p = &n;
	*p = 20;
	p = &m; 
	return 0;
}

//const在*两边
int main()
{
	int n = 10;
	int m = 20;
	const int* const p = &n;
	*p = 20;
	p = &m; 
	return 0;
}

指针运算

指针运算共有三大类

1. 指针加减整数

2. 指针 - 指针

3. 指针关系比较

指针加减整数

指针加减整数我们上文有提到过,指针里面存的是地址,而指针加减整数也是拿地址进行相加减的

#include<stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	printf("%p\n", &a);
	printf("%p\n", p);
	printf("%p\n", p+1);
    printf("%p\n", p+2);
	return 0;
}

如上,我们可以看到指针p里存的是变量a的地址,而当我们将p加了一之后,其内部的地址居然加了4,而加了2之后他的值居然加了8。这是因为他是一个整型指针,每一次+1都是加了4个字节,你可以这么理解:

现在内存中有四个字节的位置是属于变量a的,而整形指针指向的是整个四个字节,当指针p+1之后,他就会指向下一个四个字节

由此我们还能发现一个有趣的东西:

#include<stdio.h>

int main()
{
	int i = 0;
	int arr[5] = { 1,2,3,4,5 };
	for (i = 0; i < 5; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(arr + i));
	}
	printf("\n");
	for (i = 0; i < 5; i++)
	{
		printf("%d ", i[arr]);
	}
	return 0;
}

如上,你会发现:arr[ i ] 和 *(arr+i) 甚至是 i [ arr ] 的效果是一样的

我们可以这么理解 arr[ i ] :这是指针的一种特殊写法,这个[ ]就相当于是*( ),而我们的计算机在看到 arr[ i ] 的时候会自动将其翻译成 *(arr+i)。由此我们不难理解,i [ arr ] 在计算机眼里其实就是 *(i+arr),这和 *(arr+i)是一个东西

而同时,数组在内存上的空间开辟是一整块的,所以我们只要知道了第一个元素的地址,我们就可以知道整个数组,这也是为什么我们可以将数组以指针的方式打印出来的原因

指针 - 指针

指针 - 指针,究其本质,就是地址减地址。我们先来看一个例子:

#include<stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d\n", &arr[9] - &arr[0]);
	return 0;
}

可能有人会想:&arr[9]指向的是最后一个元素所代表的四个字节,而&arr[0]代表的是第一个四个字节,而两个相减之后的结果就应该是9个四个字节也就是9*4=36,但是结果如下:

因为地址减地址计算的是两个地址之间的元素个数。或者我们可以这么看:

第十个元素的地址的写法可以是:arr+9。第一个元素的地址的写法可以是arr+。而两数相减的话,就是(arr+9) - (arr+0),也就是 9 (注:仅供理解

同时我们可以想一想,既然指针减指针的结果是指针中间的元素个数,那我们是不是可以利用这种原理来实现strlen函数啊!只需要知道最后一个元素的地址和第一个元素的地址,再将两数相减,就可以得到元素的个数啦!!!而首元素地址就是数组名,最后一个元素的地址我们可以先用sizeof函数求出总个数,然后    数组名+总个数-1     就是最后一个元素的地址啦。具体的实现方法我们可以看下面这篇博客:

https://blog.csdn.net/2302_80023639/article/details/133699959?spm=1001.2014.3001.5501

指针关系比较

举个例子,如下:

#include<stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6 };
	int* p = arr;//数组名是首元素地址
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);

	while (p < arr + sz)//指针大小比较
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

我们这串代码的作用是数组打印,但是是用while循环打印。而我们此处的指针比较就是地址比较,如上,变量p的地址是首元素地址,每循环一次就p++,当这个地址指向的元素越过arr+sz指向的元素的时候,循环结束(注:arr+sz指向的是元素6后面的位置)

野指针

野指针于指针而言是相当危险的,因为这种指针指向的空间不是我们主观开辟的,是不属于我们的。我们可以用生活中的例子来比喻一下:假设我们订了十间房间,但是你非要住在第十一间房间。这第十一间房间可能没人,也可能有人,但是这是不属于我们的,没人还好,有人可就犯事儿咯。

而野指针也是同样的性质,那野指针的成因是什么呢?

指针未初始化

我们看一看下面这种情况

int main()
{
	int* p;
	*p = 20;
	return 0;
}

如上,当指针未初始化时,指针会默认为是随机值,学过函数栈帧的同学应该会知道,变量在初始化之前是CCCCCCCC(VS2022环境下),用这个值当初始值就意味着,找到地址CCCCCCCC处的变量,但是此处的变量不是我们开辟的,这时候*p=20就相当于在大街上随便找了一件房子就想进去睡觉了,当计算机面对这种情况就会报错如上

指针越界访问

这种情况是指针指向了一块不属于自己的空间并且想改变这块空间的内容,如果你指向一块随机的空间可不可以?当然可以,就像我站在银行门口行不行?毋庸置疑是可以的。但是如果动了想抢银行的念头的话,那就不行了。同理:指针可以指向随机的空间,但是指向之后这块随机空间的内容我们不能改变,如下:

#include<stdio.h>

int main()
{
	int arr[5] = { 0 };
	int* p = arr;
	for (int i = 0; i < 10; i++)
	{
		*p = 1;
		p++;
	}
	return 0;
}

我们的数组一共就只有5个元素的大小,但是for循环却循环了10次,并且让指针p每次都在改变完指向空间的内容之后都p++指向下一个元素,但是数组没有那么多元素啊,所以当指向第6个元素并且改变他的时候,就已经发生越界访问了,结果如下:

计算机上报错的内容是:栈区上的变量arr被破坏了

指针指向的空间被释放

学过函数栈帧的同学会知道,函数在调用的时候会临时开辟一块空间,然后在使用完之后会将空间销毁,详见下方链接:https://blog.csdn.net/2302_80023639/article/details/134412692?spm=1001.2014.3001.5501

而试想,当我们开辟了一块空间之后,指针指向了其中一个变量,但是在函数调用结束之后,该变量就被销毁了,但是指针还记着这个变量的地址,当我们再想去通过指针找到这个变量并且对其做出调整的话,编辑器就会报错!且看如下代码:

#include<stdio.h>

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

我们先调用test函数,随后在test函数内部创建了变量a并且返回了变量a的地址。但是我们在调用完函数之后,变量a这块空间的使用权限已经还给操作系统了,但是我们返回了变量a的地址并且还通过*p的方式想将其打印出来,这样的代码就是错的。

举一个形象的例子:一天晚上我去找了一家酒店住,房间号是101,并且将这个房间号告诉了我的怨种朋友同时告诉他明天来住。结果在第二天早上的时候我把这间房子给退了,但是你的朋友还记得这间房间的门牌号,当他第二天晚上再想来住的时候,就会发现得重新买过了。

但是你会发现,编辑器将其打印出来了,如下:

这是为什么呢?我们再来看另一个代码:

#include<stdio.h>

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("hello world\n");
	printf("%d\n", *p);
	return 0;
}

这是因为此时的内存空间刚好没有被覆盖,所以那块空间的内容还是10,虽然是非法访问,但是还是能够将其给打印出来的。可是当我们在这之间任意打印一个东西破坏了内存空间,那么那块空间就会被覆盖成其他任意值,所以打印出来的时候就会不一样。

如何规避野指针
1. 指针在创建出来时应该初始化

如果不知道赋什么值得话,可以令该指针为NULL,也就是空指针。当我们后面需要使用这个指针的时候,我们可以对指针再进行赋值而不报错。即使我们忘记了修改地址,直接去改写其指向的内容,那么0地址是无法使用的所以编辑器会报错

int main()
{
	int a = 10;
	int* p = NULL;
	p = &a;
	printf("%d\n", *p);
	return 0;
}

int main()
{
	int a = 10;
	int* p = NULL;
	*p = 20;
	return 0;
}

2. 小心指针越界

我们每一次创建变量都会向操作系统申请空间,而每一次申请完空间之后我们都应该留意一下:如果有指针在的话,那么会不会指向我们未开辟的空间。

3. 避免指针指向局部变量

因为生命周期的不同,所以我们需要考虑改局部变量还能不能存在

assert 断言

头文件  #include<assert.h>定义了宏assert,这个宏被称之为 “断言”

其用法在指针中颇为常见,在执行时会判断括号内的条件是否为真,若为真则无事发生,若为假则会在标准错误 流 stderr 中写⼊错误内容的详细信息,如下:

#include<stdio.h>
#include<assert.h>

int main()
{
	int a = 10;
	assert(a == 5);
	printf("hehe\n");
	return 0;
}

我们在创建了变量a之后将其初始化为5,assert断言a==5这个条件是否为真,为假之后就报错,如上。他不仅报出了错误的内容,甚至还报出了错误的文件和第几行

而我们在指针中的用法通常如下:

assert ( p != NULL)

假如我们在写一个庞大的项目,而我们在后面如果要使用指针变量的话,我们可能会忘记之前是否将该指针置为空,所以我们会assert断言一下判断该指针是否为空指针。若为空,则编辑器会报错;若不为空,则程序正常进行。

#include<stdio.h>
#include<assert.h>

int main()
{
	int a = 10;
	int* p = &a;
	//......
	p = NULL;
	//......
	assert(p!=NULL);

	return 0;
}

但是assert断言也有自己的缺点

这是一种判断语句,每一次判断都会耗费一定的时间,但是我们判断完了之后这些assert语句就都没有用了,但是一个一个删除又太耗费时间了。那是否有一个像开关一样的程序能另所有assert断言都失效呢?答案是肯定的,如下:

#define NDEBUG

#include <assert.h>

当我们在头文件上方加一个#define NDEBUG,则所有的assert断言都会自动失效,如下:

#include<stdio.h>
#define NDEBUG
#include<assert.h>

int main()
{
	int a = 10;
	int* p = &a;
	//......
	p = NULL;
	//......
	assert(p!=NULL);

	return 0;
}

传值调用与传址调用

我们先来看一段代码:

#include<stdio.h>

void Swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 12;
	int b = 30;
	printf("%d %d\n", a, b);
	Swap(a, b);
	printf("%d %d\n", a, b);
	return 0;
}

我们可以看到,换前换后的结果并没有改变,这是因为我们将a和b的值传了过去,但是形参是实参的一份临时拷贝,我们确实交换了两个数,但是交换的是形参,而形参在函数调用结束之后就被销毁了,所以对实参没有影响,因此交换就不起作用。如下:

上述的方式就是传值调用,我们来看一下另一种做法(传址调用):

void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 12;
	int b = 30;
	printf("%d %d\n", a, b);
	Swap(&a, &b);
	printf("%d %d\n", a, b);
	return 0;
}

可以看到,这次我们将变量a和b的地址传了过去,函数内部的两个指针通过地址找到了实参,随后将其进行了交换。

传值调用与传址调用你可以这么理解:

A让B将a和b两个方块交换一下

(传值调用):B变出了和方块a、b一摸一样的方块c、d并将c、d进行了交换,并且在交换了之后把方块c和d给扔掉了

(传址调用):B通过A留下的的纸条找到了a与b,并将a与b进行了交换

另外,传址调换比之传值调换还有一个十分明显的优点:

传地址过去,拷贝的形参的大小就只有4/8个字节

但如果是传值调用,当传递的参数非常大的时候,就会耗费很多时间去拷贝一份形参

strlen函数模拟实现(传址调用)

#include<stdio.h>

int my_strlen(const char* str)
{
	int count = 0;
	assert(str);
	while (*str)
	{
		count++;
		str++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	return 0;
}

具体细节详见这篇博客:

https://blog.csdn.net/2302_80023639/article/details/133699959?spm=1001.2014.3001.5501

指针进阶

二级指针

二级指针就是存放指针(一级)地址的指针

指针的大小为4/8字节,那么既然有大小,在内存上就一定有属于自己的空间,而这块空间就一定有属于自己的地址,如下图:

#include<stdio.h>

int main()
{
	int a = 0;
	int* p = &a;
	int** pa = &p;
	return 0;
}

我们可以看到,指针pa里的是指针p的地址,所以当我们*pa时,我们就可以找到指针p

当我们*(*pa)时,就可以找到变量a,或者可以直接写成**pa

int main()
{
	int a = 0;
	int* p = &a;
	int** pa = &p;
	**pa = 10;
	printf("%d\n", a);
	return 0;
}

同理,我们还可以推断出三级指针的本质

指针数组

首先,指针数组,是一个数组,这个数组里的每一个元素,都是指针

int    arr  [ 5 ]          这是一个整形数组,存放的是5个整形

char arr  [ 5 ]          这是一个字符数组,存放的是5个字符

int*   arr  [ 5 ]          这是一个指针数组,存放的是5个整型指针( int* )

#include<stdio.h>

int main()
{
	int a = 1, b = 2, c = 3;
	int* p1 = &a; 
	int* p2 = &b;
	int* p3 = &c;
	int* arr[3] = { p1,p2,p3 };//整形指针数组
	*arr[1] = 6;//找到变量b
	printf("%d\n", b);
    *(*(arr + 2)) = 9;//找到变量c
    printf("%d\n", c);
	return 0;
}

如上,我们可以通过指针数组找到对应的指针,再通过解引用找到指针指向的变量

指针数组模拟实现二维数组

对于二维数组,我们一般情况下会理解成一个矩阵,但是那样子理解的话不大准确。我们换一个方式来理解:二维数组是一个每个元素都为数组的数组

int main()
{
	int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	return 0;
}

arr是一个数组,数组里每个元素都是一整个数组,所以我们用指针数组以如下方式实现二维数组:

#include<stdio.h>

int main()
{
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,6 };
	int* ptr[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", *(*(ptr + i) + j));
                    //ptr[i][j] == *(*(ptr+i)+j)
		}
		printf("\n");
	}
	return 0;
}

因为数组名一般情况下代表首元素地址,所以我们可以直接拿数组名作为指针数组的元素

我们先通过*(ptr+i)找到了每个一维数组的首元素地址,再对其进行解引用就能找到对应的元素了

在计算机看来,ptr[ i ][ j ] 就等于*(*( ptr+i )+ j ),如下:

#include<stdio.h>

int main()
{
	int arr1[4] = { 1,2,3,4 };
	int arr2[4] = { 2,3,4,5 };
	int arr3[4] = { 3,4,5,6 };
	int* ptr[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", *(*(ptr + i) + j));
		}
		printf("\n");
	}
	printf("\n");
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", ptr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

字符指针

字符指针的一般写法就是 char* p = 字符

#include<stdio.h>

int main()
{
	char a = 'w';
	char* p = &a;
	printf("%c\n", *p);
	return 0;
}

我们定义了一个字符w,然后将其地址存进指针p里

我们再来看另一种情况:

#include<stdio.h>

int main()
{
	char* a = "hello world";
	char* p = a;
	printf("%c\n", *p);
	return 0;
}

当我们将一整个字符串赋给一个字符指针时,是一整个字符串都放进指针里吗?如上:我们可以看到,当我们打印出指针的内容的时候,屏幕上就只打印了一个' h '。这是因为字符串是将首元素地址(h)的地址放进指针里,而要打印的话我们就只能打印出第一个元素,如果要打印出整个字符串的话,我们就需要用到循环语句,如下:

#include<stdio.h>

int main()
{
	char* a = "hello world";
	char* p = a;
	for (int i = 0; i < 11; i++)
	{
		printf("%c", *(p+i));
	}
	return 0;
}

 

《剑指offer》一书中有一串很有意思的代码:

#include<stdio.h>

int main()
{
	char arr1[] = "hello world";
	char arr2[] = "hello world";
	const char* str3 = "hello world";
	const char* str4 = "hello world";
	if (arr1 == arr2)
		printf("hehe\n");
	else
		printf("haha\n");

	if (str3 == str4)
		printf("hihi\n");
	else
		printf("hoho\n");

	return 0;
}

如上,arr1和arr2是不一样的,因为C语言通常会将常量字符串存进一个单独的内存区域即使字符串的内容完全一致

但是arr3和arr4却是相同的,这是因为操作系统为hello world开辟了一块内存空间之后,指针arr3和arr4同时指向了常量字符串的首元素——h,所以这两个指针是一样的

数组指针

首先我们需要知道,数组指针是一个指针,举个例子:

整型指针,是指向整形的指针

字符指针,是指向字符的指针

数组指针,是指向数组的指针

我们再来看看下面两个代码分别代表了什么:

int  *p  [ 5 ]

int (*p) [ 5 ]

第一个是指针数组,变量p先与[ ]结合(括号的优先级高于星号)说明这是一个数组,而这个数组的每个元素的类型都是int*(整型指针)

第二个是数组指针,因为有括号,所以变量p先与 * 结合说明这是一个指针,指针指向的类型是一个整型数组(int*    [ 5 ])

但是现在有一个问题是:数组指针该怎么初始化呢?

&数组名表示整个数组的地址

知道了这个之后,我们就可以知道,如若要存放整个数组的地址的话,我们就需要用到数组指针,而我们可以用&数组名的方式将整个数组的地址取出来从而将其初始化,如下:

#include<stdio.h>

int main()
{
	int arr[5] = { 1,2,3,4,5 };
	int(*p)[5] = &arr;
    //&arr表示整个数组的地址
	return 0;
}

二维数组传参的本质

由上文可知,二维数组可以是一个每个元素都为一维数组的数组,而二维数组传参的本质其实是将第一个元素(第一个数组)传给了函数

#include<stdio.h>
void array(int(*p)[4], int i, int j)
{
	for (int a = 0; a < i; a++)
	{
		for (int b = 0; b < j; b++)
		{
			printf("%d ", *(*(p + a) + b));
		}
		printf("\n");
	}
}

int main()
{
	int arr[3][4] = { 1,2,3,4,2,3,4,5,3,4,5,6 };
	array(arr, 3, 4);
	return 0;
}

我们可以看到,在printf打印的时候,我们是先通过 *( p+a ) 找到打印元素所在的行,再通过 * (*( p+a ) +b) 找到对应的元素,所以第一次解引用找到的是对应的行的首元素地址,再解引用才是找到对应的元素

如上,我们二维数组传参放个数组名上去代表的是将第一行整个数组的地址传给了函数

函数指针

通过前文诸多例子我们可以知道,函数指针是一个指针,指向了一个函数

我们来看一下下面这两串代码:

int   * p   ( int, int ) 

int  (* p)  ( int, int )

int   * p   ( int, int ) 首先变量 p 先和括号结合,说明这是一个函数,一个需要两个整形参数的,返回类型为int*的函数

int  (* p)  ( int, int ) 首先变量p先和 * 结合,说明这是一个指针指向的是一个需要两个整形参数的,返回类型为int的函数,故,这是一个函数指针

但或许有人会疑惑:函数也有地址?如果有的话函数的地址又该怎么表示呢?我们来做个实验:

#include<stdio.h>

void ptr()
{
	printf("hello world\n");
}

int main()
{
	ptr();
	printf("%p\n", ptr);
	printf("%p\n", &ptr);
	return 0;
}

如上,无论是&函数名还是单独的函数名的都可以表示函数的地址

下面是一串有趣的代码

#include<stdio.h>

void print(char* p)
{
	printf("%s\n", p);
}

int main()
{
	void(*p)(char*) = print;
	(*p)(" love xzy!!!");
	return 0;
}

两段有趣的代码

1. ( * ( void ( * )( ) ) 0 ) ( );

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

1. * 说明这是一个指针,右边的()说明这是一个函数,返回类型为void

然后在其两边加上了 ( ),右边又有一个0,说明这是一个强制类型转换,也就是将0定义为该函数的地址,而 * 与最右边的括号则是找到该函数并进行函数调用

void( * )( )                          ————             函数指针

void( * )( ) ) 0                   ————             强制类型转换:另0为该函数地址

( * void( * )( ) ) 0) ( )         ————             找到函数并函数调用

2. void ( * ) ( int ) 明显是一个函数指针,我们再看到signal,右边的括号表示这是一个函数,有两个参数,一个为int,一个为void ( * ) ( int )。我们再看到外面的void( *       )( int )说明这个函数的返回类型是一个指向返回类型为void,需要一个int型参数的函数的函数指针

void ( * ) ( int )                              ————              函数指针

signal ( int , void ( * ) ( int ) ) )      ————              名为signal函数的函数调用

void ( * signal ( int , void ( * ) ( int ) ) ) ( int )   ————   返回类型是一个函数指针

函数指针数组

函数指针数组的本质就是一个数组,里面的每一个元素都是函数指针

void ( *p ) ( int )      这是一个函数指针

void ( *p[ ] ) ( int )   这是一个函数指针数组

如上,p 先与[ ] 结合说明这是一个数组,而外头的 void ( * ) ( int ) 则说明该数组的每一个元素都为函数指针,指针指向的是无返回类型,需要一个整形参数的函数。

函数指针数组的用法——计算器
#include<stdio.h>

int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf(" 0:exit \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
			printf("ret = %d\n", ret);
		}
		else if (input == 0)
		{
			printf("退出计算器\n");
		}
		else
		{
			printf("输⼊有误\n");
		}
	} while (input);
	return 0;
}

我们可以先定义加减乘除四个函数,再将其存进函数指针数组中,找到代表想要的功能的函数并对其解引用,由此就是我们函数指针数组的其中一种用法啦,结果如下:

回调函数

假设我们现在有A、B两个函数,我们将A函数的地址作为参数传递给B函数,当我们在B函数内调用A函数时,A函数就被叫做回调函数

回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应

我们的qsort函数的实现就需要用到回调函数的相关知识,代码如下:

int compare(const void* e1, const void* e2)
{
	return *(int*)e1 - *(int*)e2;
}
 
void swap(char* buf1, char* buf2, int width)
{
	int i = 0;
	for (i = 0; i < width; i++)
	{
		char tmp = *buf1;
		*buf1 = *buf2;
		*buf2 = tmp;
		buf1++;
		buf2++;
	}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(void*, void*))
{
	int i = 0, j = 0;
	for (i = 0; i < sz - 1; i++)
	{
		for (j = 0; j < sz - 1 - i; j++)
		{
			if (cmp((char*)base + j * width, (char*)base + (j + 1) * width) > 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
			}
		}
	}
}
void test()
{
	int arr[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz, sizeof(arr[0]), compare);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
{
	test();
	return 0;
}

详解请看下方博客:

https://blog.csdn.net/2302_80023639/article/details/133972117?spm=1001.2014.3001.5501

结语

如上就是指针相关知识的讲解了,如果喜欢的话希望可以多多关注!

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
引用\[1\]:C语言字节对齐问题详解中提到了C语言中的字节对齐问题。在结构体中,为了提高内存访问的效率,编译器会对结构体进行字节对齐。这意味着结构体的成员在内存中并不是紧凑排列的,而是按照一定的规则进行对齐。具体的对齐规则取决于编译器和编译选项。\[1\] 引用\[2\]:在C语言中,可以使用宏offsetof来获取结构体成员相对于结构体开头的字节偏移量。这个宏非常有用,可以帮助我们计算出每个结构体成员相对于结构体开头的偏移字节数。通过这个宏,我们可以更好地理解结构体的内存布局。\[2\] 引用\[3\]:在C语言中,指针和结构体的组合常常用于处理复杂的数据结构。指针可以指向结构体的成员,通过指针可以方便地对结构体进行操作。指针和结构体的组合可以实现更灵活的数据处理和内存管理。\[3\] 综上所述,C语言中的指针结构体组合可以用于处理复杂的数据结构,而字节对齐问题则是在结构体中为了提高内存访问效率而进行的优化。通过使用宏offsetof,我们可以更好地理解结构体的内存布局。 #### 引用[.reference_title] - *1* *3* [结构体指针C语言结构体指针详解](https://blog.csdn.net/weixin_34069265/article/details/117110735)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [C语言之结构体详解](https://blog.csdn.net/m0_70749276/article/details/127061692)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值