深入理解指针

指针的本质是内存地址。它是一个变量,其存储的值是另一个变量(或数据)在计算机内存中的位置(地址)。

在了解指针之前,首先需要知道变量与内存地址之间的联系。

在C语言中,创建变量实际上都是向内存申请一块空间

比如下面图中的代码就是创建了整型变量a,向内存中申请4个字节(一个整形占用4个字节),用于存放整数10,其中每个字节都有地址,图中4个字节的地址分别是:

1.        0x000000D20775F5E4
2.        0x000000D20775F5E5
3.        0x000000D20775F5E6
4.        0x000000D20775F5E7

那么如果想得到a的地址怎么办呢? 这时就需要一个操作符‘&’,‘&’的作用就是取出4个字节中较小的那一个地址。
进行&a之后,如果将其打印出来,就可以得到 0x000000D20775F5E4.

那如果我们想把&a的结果放到一个变量中,又该怎么办呢?这时候就需要引入一个知识--指针变量

一、指针变量

        顾名思义,指针变量就是用于存放 地址 的变量,它的格式为 (类型)*(变量名),

        继续沿用前面的例子,假如这个时候我们需要把a的地址存放到一个名为p的变量中,如何进行操作呢?

        很简单,创建一个int类型的指针变量来存储a的地址即可,如下图所示
将&a的结果与变量p中的内容输出,可以确定,a的地址成功存入到了变量p中。

        让我们来看a和p的具体内容。如图所示,每个方块代表申请的内存空间:变量a存储的数值是10,而变量p保存的是a的地址。既然p专门申请了空间来存储a的地址,那么p本身也必然拥有对应的内存地址。

        既然我们现在掌握了变量a的地址,那么能否通过这个地址来访问a呢?答案是肯定的,只需对指针p进行解引用操作即可访问到a。解引用操作符--> ( * )

        使用解引用操作符,我们可以通过地址来修改指针所指向变量的值,还是沿用上述例子,如果想要将a的值改为20,只需要 *p =20;便可以间接地修改掉a的值;

        在VS2022中,运行 *p = 20;可以发现a的值确实被更改成为了20。 

如何理解指针变量呢?

        例如 int* p = &a  int*代表的是p的类型,它指向&a,也就是指向a的地址。

如何区分int 与 int* ?

        很好理解,int就是变量中为int类型,而int*代表变量中存放的是地址,地址指向的内容是int类型

 

指针变量的大小 

        指针变量的大小由操作系统决定,与类型无关,在x64(64位操作系统)中占用8个字节,在x86(32位操作系统)中占用4个字节,这里可以通过sizeof来验证:

VS2022 X64环境下
VS 2022 X86环境下

二、指针运算

        1.指针+-整数

        指针+-整数本质上就是将指针向前或者向后移动x位。
        如图所示,将arr+1后可以看见地址向后移动了4个字节,因为一个int占用4个字节这里如果将int更改为char类型+1,那么地址也将是只会向后移动一个字节。

注:指针的类型决定了指针向前或者向后走一步有多大(距离)。
注:这里是+-的操作是地址,与前面的指针变量所占用空间大小不是一个东西。

        2.指针-指针

         指针-指针可以得到两个地址之间的元素个数。
        比如如下代码:

         可以看到,当把arr数组最后一个元素的地址减去首元素的地址,最终的计算结果为10. 
需要注意的是:指针-指针仅在同一块内存空间中才有意义,不同空间相减毫无意义。
另外需要知道是,指针+指针是毫无意义的,就如同日期+日期,没有意义。

        3.指针的关系运算

        指针与指针之间可以进行大小的比较。高地址>低地址 。

        仅当两指针 ​指向同一连续内存块内的元素时​(如数组),比较结果才有实际意义。

三、void*指针 

        void*指针是无类型的指针,其特点是无法修改void*指向的内容,如果想要使用,则需要将void*指针转换成相应类型的指针才可进行操作

        void*指针出现的意义:可指向任意类型的内存地址,在动态内存分配中占据重要地位。在函数接受参数时候可以接受不同类型数据的地址。

四、野指针

        野指针是指指针指向的地址是不可知的。
        野指针成因:

        指针未定义

#include <stdio.h>
int main()
{
	int* p;//指针未正确初始化
	return 0;
}

        指针越界访问

#include <stdio.h>
int main()
{
	int arr[5];
	int* p = &arr + 1;//越界访问
	return 0;
}

  指针指向的空间被释放

int main()
{
	int* p = malloc(4 * sizeof(int));
	free(p);//free后p为野指针
	return 0;
}

   那么如何避免野指针的出现?

        1.正确初始化
        2.避免指针越界访问
        3.指针指向的空间释放后将指针置空

        4.在使用指针前检查指针其有效性

        5.不要返回临时变量的地址

int main()
{
	int* p = malloc(4 * sizeof(int));
    if(p == NULL)
    {
        perror("malloc:");
        return 1;
    }
	free(p);
    p = NULL;   
	return 0;
}

五、const修饰指针

        我们知道,变量的值是可以被修改的,如果我们不想让其被更改的话,就可以使用到const来修饰变量,以达到锁定其值的作用。

1.const修饰变量

        const修饰的变量表示其数值不可变,因此在这种情况下,可以使用const修饰出生年份变量,确保其数值不能被更改。

const int birth_year = 1990;

这样定义后,birth_year的值将永远保持为1990,无法被改变。

当然,可以通过其他方式来更改birth_year的值,比如创建指针变量

const int birth_year = 1990;
int* p = &birth_year;
*p = 2025;//可以通过该方法更改,但是非法

通过这种方式绕过birth_year,使⽤birth_yaer的地址修改birth_year就能做到,但是这样做是在打破语法规则。

2.const修饰指针

        const修饰指针变量一般分为两种情况 *号在const的左侧还是右侧(如下:)

#include <stdio.h>
int main()
{
	int a = 10;
	const int* p1 = &a;
	int const* p2 = &a;
	int* const p3 = &a;
	return 0;
}

如果const在*左边,可以看出const修饰的是int类型,也就是指针指向的内容不可被修改

如果const在*右边,可以看出const修饰的是p3,p3是指针,也就是指针的内容无法被修改
 

但如果想要指针和变量都不被改变该怎么办呢,这时候只需要双重锁定即可,如下:

int a = 10;
const int* const p = &a;

六、传值调用与传址调用 

        传值调用与传址调用的区别在哪里呢?通过下面的例子来了解:

void Swap(int x, int y)
{
	int tmp = x;
	x = y;
	y = x;
}
int main()
{
	int x = 10;
	int y = 20;
	Swap(x, y);
	printf("x = %d y = %d",x,y);
}

 上述代码中想要交换x与y的值,调用了Swap函数,进行交换x与y的值,那么运行结果如何呢?
下图就是运行结果,可以看见,实际上并没有交换x与y的值,那么是为什么呢?

 问题出现在调用Swap函数时候传的是x与y的值,只是把x与y的值拷贝了一份赋给了Swap中的x与y,其在内容空间中存储方式如图中所示。

        那么该怎么办呢?通过前面的介绍,我们知道可以通过指针间接修改变量的值,由此我们是否可以通过传x与y的地址过去达到交换x与y的值呢?答案是肯定的。 

传址调用

图中传给函数x与y的地址,就叫做传址调用

而先前的传给的是x与y的值,就叫做传值调用 

 七、指针与数组

        1.数组名

我们知道,访问数组可以通过 数组名[下标] 的方式来访问数组中的元素,那么数组名与其中的关系是怎样的呢?

int main()
{
	int arr[10];
	printf("arr	= %p \n", arr);
	printf("&arr[0] = %p \n", &arr[0]);
	return 0;
}

运行上述代码后,结果如下,可以看见,数组名与数组首元素的地址一致,由此可以推断出数组名代表的是 首元素的地址。


那么如果我们将数组名+整数结果会是怎样的呢?

从图中可见,将arr+1后,跳过了一个整形的大小,可以得到数组名+整数就是跳过数组内对应整数个元素。
从示意图中可以观察到,当我们&arr并对其进行 +1 操作时,指针会跳过 ​整个数组,最终指向数组最后一个元素 ​之后​ 的那个内存地址。
而当我们 &arr[0]并对其进行 +1 操作时,指针仅仅向前移动了一个元素的大小,最终指向数组中的第二个元素​的地址。

下面我们通过示意图,具体观察数组上这两种 +1 操作的区别。​

通过以上内容,我们可以发现,数组的访问本质上是通过指针算术实现的,那么我们是不是就可以通过指针来访问数组呢?

2.. 使用指针访问数组

通过指针访问数组元素 一般是通过指针指向已有数组的首地址,通过移动指针访问元素,如下图所示

 八、一维数组传参

        一维数组传参实际上传的是数组首元素的地址,本质上是指针,所以在函数中通过sizeof计算数组个数是不可取的。

计算结果为2,因为在x64环境下指针大小为8,8除以第一个元素所占字节数(4),得到的结果为2。

X64环境下

九、二级指针

       二级指针,存放的是指针变量(一级指针)的地址

图解二级指针如下:

二级指针的使用:

        需要解引用两次

参考图中例子

        第一次解引用   *pp 后得到指向变量p,此时进行修改,修改的是p中的内容

        第二次解引用    **pp 后指向变量a,此时进行修改,修改的a中的内容

下列代码运行后可以看到,当两次解引用变量pp之后,成功修改了a的值,说明两次解引用后指针的确指向了变量a

十、字符指针变量

        在C语言中,字符指针变量指向一个字符类型的数据。
        比如下面的例子

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

该代码表示创建了一个char类型的名为str的变量,存放的是'w',此时创建一个字符指针变量p来存放str,如果将 其打印出来会看到输出了字符w。

这是字符指针的一种用法。

其实字符指针还能存放字符串,在这里有一个误区,许多初学者会认为str2与str3存放的是abcd与'\0',其实这是错误的,实际上只存放了首元素‘a’的地址

十一、数组指针变量

        顾名思义,数组指针变量是存放数组的指针,可以通过指针指向数组

int (*p)[10];

p与*先结合,说明p是一个指针变量,该指针变量指向了一个类型为int,大小为10个整形的数组 。

        数组指针变量的初始化: 

int arr[10];
int (*p)[10] = &arr;

        数组指针变量的使用如下: 

十二、函数指针变量

        顾名思义就是存放函数的指针变量,可以通过该变量间接调用函数。

函数指针变量的初始化:

返回值(*变量名)(参数1,参数2····)  = 函数名;

例如下面的Swap函数: 

void Swap(int* x,int* y){ 
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main(){
	int x = 10;
	int y = 20;
	void(*p)(int*, int*) = Swap;
	p(&x, &y);
	printf("%d %d", x, y);
}

那么为什么可以通过这种方式间接调用函数呢?

我们不妨试试下面的代码:

void Swap(int* x, int* y){
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main(){
	printf("&Swap = %p\n", &Swap);
	printf(" Swap = %p\n", Swap);
	return 0;
}

执行结果如下:

 可以看到,函数也是有自己对应的地址的,而且函数名就是地址,所以可以通过指针来访问该函数。

十三、函数指针数组

        函数指针数组,顾名思义 ,就是存放函数指针的数组,参考上述的函数指针,只需将其转换成数组的形式即可。

整形变量        int a;

整形数组        int a[x];

字符变量        char c;

字符数组        char c[x];

参考上述例子,那么函数指针数组便可以参照着写出来。

函数指针变量        (函数返回值)(*变量名)(函数参数);

函数指针数组        (函数返回值)(*变量名[x])(函数参数);

        知道了函数指针数组如何初始化后,那么函数指针如何使用呢?
还是沿用之前的的知识。

整形数组的使用:        数组名[下标];

字符数组的使用:        数组名[下标];

参考上述例子,那么函数指针数组使用方式也一致:

函数指针数组的使用:        数组名[下标];

**需要注意的一点:如果所指向的函数返回值不是void,最好设置一个对应类型变量,用于接收返回的值。

十四、回调函数

        C语言中,​回调函数(Callback Function)是一种通过函数指针调用的函数。它的核心思想是将一个函数(回调函数)作为参数传递给另一个函数(主调函数),以便在适当的时候被调用执行。。

        回调函数可以看做成一个黑箱,用户将一系列数据及操(用户预定义的逻辑)作传入黑箱中,黑箱再给出输出,下面我举一个计算器的例子。
        在下面的代码中,void calc(double x, double y,double(*Calculation)(double,double))便是回调函数,用户只需要给出函数名字,以及对应函数所需要的参数内容,然后回调函数便会将该值传入到用户指定的函数中,之后在执行一系列其他操作(这里我只进行了值的输出,再往后可以添加其他内容)。

        当然,在这个例子中它的缺点很明显,参数的类型已经锁死。

void calc(double x, double y,double(*Calculation)(double,double))
{
	double result = Calculation(x, y);
	printf("计算结果为(保留两位小数):%.2lf\n", result);
}
void menu()
{
	printf("****************************************\n");
	printf("*******   计算器(保留两位小数)   *******\n");
	printf("*******      1.Add    2.Sub      *******\n");
	printf("*******	     3.Mul    4.Div      *******\n");
	printf("****************   0.exit   ************\n");
	printf("****************************************\n");
}
double Add(double x, double y)
{
	return x + y;
}
double Sub(double x, double y)
{
	return x - y;
}
double Mul(double x, double y)
{
	return x * y;
}
double Div(double x, double y)
{
	return x / y;
}
int main()
{
	int input = 0;
	double	x, y;
	do
	{
		menu();
		printf("请选择-->");
		scanf("%d", &input);
		if (input)
		{
			printf("请输入操作数x y:");
			scanf("%5lf %5lf", &x, &y);
		}
		switch (input)
		{
		case 1:
			calc(x, y, Add);
			break;
		case 2:
			calc(x, y, Sub);
			break;
		case 3:
			calc(x, y, Mul);
			break;
		case 4:
			calc(x, y, Div);
			break;
		case 0:
			printf("Exit\n");
			break;
		default:
			printf("输入错误,重新输入\n");
			break;
		}

	} while (input);
	return 0;
}

十五、qsort函数

        关于qsort函数的使用方法已在个人博客中详细介绍,如需了解可参考以下链接:C语言qsort函数使用详解 - 优快云博客
C语言qsort函数使用详解-优快云博客https://blog.youkuaiyun.com/MuL__/article/details/148809704?spm=1001.2014.3001.5501

完. 

         

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值