深入理解指针(详解 图文)

目录

前言

1.内存和地址

2.指针变量和地址

2.1取地址操作符(&)

2.2指针变量和解引用操作符(*)

2.2.1指针变量

2.2.2解引用操作符

2.3指针变量的大小

3.指针变量类型的意义

3.1指针的解引用

3.2指针 +- 1

3.3 void* 指针

4.const修饰指针

4.1const修饰变量

4.2const修饰指针变量

5.指针运算

5.1指针 +- 整数

5.2指针-指针

5.3指针的关系运算

6.野指针

6.1野指针成因

6.1.1.指针未初始化

6.1.2.指针越界访问

6.1.3.指针指向的空间释放

6.2如何规避野指针

6.2.1指针初始化

6.2.2小心指针越界访问

6.2.3指针变量不再使用时要置空且使用前检查有效性

6.2.4避免返回局部变量的地址

7.指针传值调用与传址调用

7.1传值调用

7.2传址调用


前言

在学习指针之前,我们先浅了解一下指针是个什么东西,指针其实是一种数据类型,用于存储变量的内存地址。简单来说,可以将指针想象成一个指向内存中某个存储单元的箭头,通过这个箭头可以找到存储单元中实际存储的值。

1.内存和地址

对于指针,我们将它理解为地址,生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。每个内存单元也都有⼀个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间,C语言中给地址起了新的名字叫:指针

            

内存单元的编号 == 地址 == 指针

2.指针变量和地址

2.1取地址操作符(&)

在C语⾔中创建变量其实就是向内存申请空间

⽐如,上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址。

那如何才能得到a的地址呢?

现在就学习了一个操作符——(&)取地址操作符

按照我画图的例子,会打印出:0137FCEC

&a取出的是a所占4个字节中地址较⼩的字节的地址。

虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可行的。

2.2指针变量和解引用操作符(*)

2.2.1指针变量

那我们通过取地址操作符(&)拿到的地址是⼀个数值,比如:0x0137FCEC,这个数值有时候也是需要存储起来,方便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?答案是:指针变量中。

#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;//取出a的地址并存储到指针变量p中

	return 0;
}

指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。

我们看到 p 的类型是 int*,那该如何去理解这个指针的类型呢?

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

这⾥ p 左边写的是类型的对象 int* ,* 是在说明p是指针变量,而前⾯的 int 是在说明 p 指向的是整型 (int) 类型的对象。

在指针眼里只要放在指针变量里面,什么东西都是地址,很好的类比,在锤子眼里什么都是钉子。

2.2.2解引用操作符

C语言中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针) 指向的对象,这⾥必须学习⼀个操作符叫解引用操作符(*)。

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

上面代码中 *p = 0 就使⽤了解引用操作符, *p 的意思就是通过 p 中存放的地址,找到指向的空间, *p 其实就是 a 变量了;所以 *p = 0,这个操作符是把 a 改成了0。

学到这大家可能会有疑问,指针有什么用呢?这里如果目的就是把a改成0的话,写成 a = 0;不就行了,为啥非要使用指针呢? 其实这里是把 a 的修改交给了 p 来操作,这样对 a 的修改,就多了⼀种的途径,写代码就会更加灵活,下面我举一个例子来帮助大家理解。

相信大家都看过最近一个很火的电视剧《狂飙》,在电视剧中有个人叫强哥,他是个黑社会,在早期他想做一件事都是自己直接动手,而后面他有了老莫后,他有不方便动手的事情,他就会跟老莫说他想吃鱼了,然后老莫就会动手帮他摆平这件事了。

而放到指针中这也很适用,a = 0就相当于强哥亲自动手了,p 就相当于老莫,&a后就相当于把事情的经过告诉了老莫,然后老莫搞定了这件事。相信大家通过这个例子,也对指针有了更多的了解。

2.3指针变量的大小

指针变量的大小取决于地址的大小

32位平台下地址是32个bit位(4个字节)

64位平台下地址是64个bit位(8个字节)

结论:指针变量的大小和类型无关,只要指针类型的变量,在相同的平台下,大小都是相同的。

3.指针变量类型的意义

3.1指针的解引用

对比下面两段代码,在调试是观察内存的变化

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


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

调试我们可以看到,代码1会将 a 的4个字节全部改为0,但是代码2只是将 a 的第⼀个字节改为0

这里我们可以总结,指针的类型决定了指针进行解引用操作符的时候访问几个字节,也就是决定指针的权限。比如:char* 的指针解引用就只能访问 1 个字节,而 int* 的指针解引用能访问 4 个字节

3.2指针 +- 1

观察下面代码的地址

int main()
{
	int a = 10;
	int* pa = &a;
	char* pc = &a;

	printf("%p\n", pa);
	printf("%p\n", pa + 1);

	printf("%p\n", pc);
	printf("%p\n", pc+1);
	return 0;
}

我们可以看出, char* 类型的指针变量+1跳过1个字节,int* 类型的指针变量+1跳过了4个字节, 这就是指针变量的类型差异带来的变化。

总结:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)

3.3 void* 指针

在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为⽆具体类型的指针(或者叫泛型指 针),这种类型的指针可以⽤来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针+-整数解引⽤运算。

例1:

int main()
{
	int a = 10;
	int* p = &a;
	char* p1 = &a;

	return 0;
}

在上面的代码中,将⼀个int类型的变量的地址赋值给⼀个char*类型的指针变量。编译器给出了⼀个警告(如下图),是因为类型不兼容。而使⽤void*类型就不会有这样的问题。

例2:

int main()
{
	int a = 10;
	void* p1 = &a;
	void* p2 = &a;

	*p1 = 10;//err
	*p2 = 0;//err
	return 0;
}

编译显示报错

可以看到,void* 类型的指针可以接收不同类型的地址,但是⽆法直接进⾏指针运算

4.const修饰指针

4.1const修饰变量

变量是可以修改的,如果把变量的地址交给⼀个指针变量,通过指针变量的也可以修改这个变量。

但是如果希望变量加上一些限制就变得不能被修改,这就用到了 const

int main()
{
	int m = 0;
	m = 1;//可以被修改
	const int n = 0;
	n = 2;//不能被修改
	return 0;
}

上述代码中 b 是不能被修改的,其实 b 本质是变量,只不过被 const 修饰后,在语法上加了限制,只要我们在代码中对 b 进行修改,就不符合语法规则,就报错,致使没法直接修改 b .

4.2const修饰指针变量

看如下代码分析

int main()
{
	int a = 1;
	int b = 2;
	int const* p = &a;
	*p = 3;
	p = &b;


	int c = 4;
	int d = 5;
	int* const pc = &c;
	*pc = 6;
	pc = &d;

	return 0;
}

结论:

1.const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变

2.const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指 向的内容,可以通过指针改变


5.指针运算

指针的运算有三种,分别为1.指针+-整数 2.指针-指针 3.指针的关系运算

5.1指针 +- 整数

众所周知,数组在内存中是连续存放的,只要知道第一个元素的地址,那么就可以顺藤摸瓜找到后面的所有元素。

int main()
{
	int arr[8] = { 1,2,3,4,5,6,7,8 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//这里p + i就是指针+整数
	}
	return 0;
}

5.2指针-指针

指针-指针的前提条件是:两个指针指向的同一块空间

#include <stdio.h>
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;    //指针-指针
}

int main()
{
	printf("%d\n", my_strlen("abc"));
	return 0;
}

5.3指针的关系运算

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	while (p < arr + sz) //指针的⼤⼩⽐较
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

6.野指针

野指针(英语:Dangling pointer)是指在程序中指向已经释放的内存地址的指针。当一个指针被释放之后,但是程序中仍然存在对该指针的引用,那么这个指针就成为野指针。

6.1野指针成因

6.1.1.指针未初始化

#include <stdio.h>
int main()
{
	int* p;//局部变量指针未初始化,默认为随机值
	*p = 20;
	return 0;
}

6.1.2.指针越界访问

int main()
{
	int arr[10] = { 0 };
	int* p = &arr[0];
	int i = 0;
	for (i = 0; i <= 11; i++);
	{
		*(p++) = i;//当指针指向的范围超出数组arr的范围时,p就是野指针
	}
	return 0;
}

6.1.3.指针指向的空间释放

#include <stdio.h>
int* test()
{
	int n = 100;
	return &n;
}                    //test出来n的内容就被释放掉了

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

通过上面代码举个例子帮助大家理解,今天小a很累不想回家,在附近酒店租了一晚,他在酒店201住,他把这个地址告诉了他的朋友小b,第二天就离开了,小b知道地址后也想去住,他拿到地址来到这个地方,那小b还能去这个房间住吗?答案肯定是不行的,因为小a只付了一晚上的房费,第二天离开后这个地方就不属于他了,虽然小b拿到了这个地址,但也是不能进去住的,如果进去了就算非法访问。

6.2如何规避野指针

6.2.1指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值为NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是⽆法使⽤的,读写该地址会报错如果知道指针应该指向哪里就给他初始化一个明确的地址,如果还不知道就初始化为NULL.

如图,我们在vs编译器上查看NULL定义,在cpp中NULL就是0,在C语言中也是0,只是强制类型转为为void*指针的0,但本质意义上都是0.

6.2.2小心指针越界访问

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问

6.2.3指针变量不再使用时要置空且使用前检查有效性

当指针变量指向⼀块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的 时候,我们可以把该指针置为NULL。因为约定俗成的⼀个规则就是:只要是NULL指针就不去访问, 同时使用指针之前可以判断指针是否为NULL.

举个生动形象的例子,我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找⼀棵树把野狗拴起来, 就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。

int main()
{
	int a = 10;
	int* p = &a;
	if (p != NULL)
	{
		//...
	}
	return 0;
}

就像上面代码一样,在使用指针前我们都先判断下指针是否为空,如何再去使用。

判断指针是否为空还有一种更好用的方法就是用 assert 断言,这个不仅能判空,还能报错告诉你具体错在哪,详情可以看我的上一篇博客,链接就放在这里 assert断言的使用-CSDN博客

6.2.4避免返回局部变量的地址

不要返回局部变量的地址,就如我们这篇文章 6.1.3 中例子

7.指针传值调用与传址调用

7.1传值调用

我们写一个函数来交换两个整型变量,观察以下代码:

#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	Swap1(a, b);
	printf("交换前:a = % d b = % d\n", a, b);
	printf("交换后:a = % d b = % d\n", a, b);
	
	return 0;
}

运行结果如下:

运行后会发现,为什么没有交换呢?

我们调试一下

我们发现在main函数内部,创建了a和b,a的地址是0x007ffae8,b的地址是0x007ffadc,在调⽤ Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是 x的地址是0x007ffa04,y的地址是0x007ffa08,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b,当Swap1函数调⽤结束后回到main函数,a和b的没法交换。Swap1函数在使⽤的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的方式我们之前在函数的时候就知道了,这种叫传值调用

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实 参。

那要怎么才能做到用函数交换两个数呢?这就要用到传址调用

7.2传址调用

我们现在要解决的就是当调⽤Swap函数的时候,Swap函数内部操作的就是main函数中的a和b,直接 将a和b的值交换了。那么就可以使⽤指针了,在main函数中将a和b的地址传递给Swap函数,Swap 函数⾥边通过地址间接的操作main函数中的a和b,并达到交换的效果就好了

void Swap2(int* px, int* py)
{
	int tmp = 0;
	tmp = *px;
	*px = *py;
	*py = tmp;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a = % d b = % d\n", a, b);
	Swap2(&a, &b);
	printf("交换后:a = % d b = % d\n", a, b);
	
	return 0;
}

运行结果如下

传址调用就很顺利的完成了数值的交换

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;

所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用。

                                        🌹🌹🌹看到这了点个关注吧🌹🌹🌹

                                        🌹🌹🌹        感谢各位佬       🌹🌹🌹

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值