第17课 指针的初阶

指针是什么?

1:指针是内存中最小单元的编号,和地址是一样的

2:平时我们口中的地址,其实是指针变量的意思,指针变量表示存放地址的变量

我们写一串简单代码加强我们对指针的理解

int main()
{
	int a = 10;
	return 0;
}

使用监视

我们知道,在内存中。每一个字节都占有一个地址,也就是内存编号

我们创建一个整型a,a占四个字节,所以a应该存有四个编号,但取地址a只取出了一个地址

实际上,&a取出的只是首个字节所对应的地址,也就是0x0065f7c0  剩下的三个地址分别为0x0065f7c1  ,0x0065f7c2  ,0x0065f7c3

接下来,我们介绍指针变量

指针变量,顾名思义,存放指针的变量

我们用代码表示一下指针变量

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

 其中的pa就是指针变量,pa中存放的是a的地址,pa的类型是int* ,*表示pa存放的是一个指针,int*则表示存放的指针的类型是整型,这里的pa其实可以叫做整型指针。

总结一句话:地址就是指针,口语中所说的指针指的是指针变量

指针变量中存放的是地址,通过指针变量,能够找到其所指向的地址,也就找到了地址所代表的内存单元。

 这里会有一个问题:为什么内存的最小单元是字节

答:例如char类型的变量,占一个字节

int类型的变量,占四个字节

而内存单位分别有bit byte  kb mb gb 

假设我们设计的最小单元是bit的话,一个char类型的变量(字符)就需要八个单元,空间浪费比较严重,假设我们设计的最小单元是kb的话,那就需要用很多char类型 或int类型的变量才能够填满一个单元

所以最合适的最小单元就是byte

如何编址?

答:对于32位的机器,我们可以假设有32根地址线,每根电线在寻址时会产生两种电压,高电压代表1,低电压代表0

那么产生的全部情况就有

00000000000000000000000000000000

00000000000000000000000000000001

-----------------------------------------------------

11111111111111111111111111111110

11111111111111111111111111111111

一共能够产生的结果有2^32次方中结果,对应2^32个地址

2^32/1024=kb /1024=m/1024=4gb

 这里我们就明白

1:在32位平台的机器上,一个地址由32个0或者1组成,又因为1个字节=8个比特位,4个字节=32个比特位,所以存储一个地址需要四个字节,所以1个指针变量的大小是4个字节

2:在64位平台的机器上,同理,一个指针变量的大小等于8个字节

指针和指针类型

我们先看一下指针的大小

int main()
{
	char*pc = NULL;
	short*pe = NULL;
	int*pf = NULL;
	double*pg = NULL;
	printf("%d\n", sizeof(pc));
	printf("%d\n", sizeof(pe));
	printf("%d\n", sizeof(pf));
	printf("%d\n", sizeof(pg));
	return 0;
}

我们看运算结果

 由此可见,不同类型的指针所占字节的大小是相同的。

需要注意的是:

1:在有些编译器下,这串代码会报错,原因是sizeof运算符返回的类型是无符号整型,也就是unsigned int ,所以最好的写法是

u表示无符号整型

 既然不同类型的指针所占的字节数是相同的,那么类型是否是可有可无的,或者是否可以把类型统一呢?

答案是不能

我们写一串代码 

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

0x表示16进位制,8个16进制位表示32个2进制位,为什么呢?

答:15用2进制位表示为1111  15用16进位制表示为f  ,我们可以发现,四个二进制位能够完全表达一个16进制位。

我们进行调试,

 可以发现,虽然是倒置的,但我们的确成功设置了a的地址,接下来,我们补充以下代码

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

pa中存放的是a的地址,我们对pa进行解引用

进行调试,监视

 发现pa的确等于&a,这时候我们观察内存

我们再进行解引用,观察内存 

 我们可以发现,整型指针在解引用时,把内存中的四个字节数全部更改为0

接下来,我们将int型的指针改成char*看看

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

 进行调试,监视

 可以发现,pa和&a的结果是相同的,我们再看内存中的a的地址

 我们进行解引用后

 我们可以发现,char*指针在解引用时,仅仅改变了一个字节由此

由此,我们可以得出:不同的指针类型决定了指针在解引用时访问的字节数的大小

1:int*的指针,解引用时访问4个字节

2:char*的指针,解引用时访问1个字节

接下来,我们介绍指针参数的另一个意义

首先写一个代码

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

pa表示int型的指针,pb表示char型的指针,他们存储的都是&a,我们进行打印,结果如图

 接下来,我们让pa和pb分别+1

int main()
{
	int a = 0x11223344;
	int*pa = &a;
	char*pb = (char*)&a;
	printf("pa=%p\n", pa);
	printf("pa+1=%p\n", pa+1);
	printf("pb=%p\n", pb);
	printf("pb+1=%p\n", pb+1);
	return 0;
}

看打印结果如图所示

 我们可以发现,不同的指针变量的类型+1跳过的字节数是不同的,int型的指针+1表示跳过4个字节  char型的指针+1表示跳过1个字节,我们把指针变量+1跳过的字节数叫做步长

总结:指针变量的类型决定了步长

我们在这里思考一个问题

如果两个指针类型不同,但他们的类型占用的字节数是相同的,那是不是他们的呈现的结果就相同呢?

答案是错误的

例:

int main()
{
	int a = 0;
	int*pa = &a;
	float*pb = &a;
	*pa = 100;
	return 0;
}

我们创建两个不同类型的指针,一个是整型,一个是浮点型,他们所接受的地址是相同的,并且整型和浮点型解引用都访问四个字节,+1也是跳过四个字节,我们进行调试并看内存

 我们可以发现,pa被解引用的结果是64,为什么是64呢?

因为地址的表示形式是16进制为,16进制的64其实=16*6+4=100 ,我们再对浮点型pb进行解引用

int main()
{
	int a = 0;
	int*pa = &a;
	float*pb = &a;
	*pb = 100.0;
	return 0;
}

 观察内存

可以发现,*pa和*pb的结果完全不同

得出结论:不同的类型,哪怕解引用访问的字节数和步长都相等,也不可以通用。浮点型的存储方式和整形的存储方式是不同的

3:野指针

野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)

3.1野指针成因

指针未初始化:

例如

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

1:p没有初始化,代表p没有明确的指向

2:如果指针没有初始化的话,指针就存放的是随机值:0xcccccccc

3:我们在解引用访问p的时候,会把随机值当作地址,但这个地址在内存中并不属于我们,所以造成非法访问,这里的p就是野指针了。

2:指针越界访问

int main()
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int *p = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		*p = i;
		p++;
	}
	return 0;
}

对代码进行解释:我们创建一个十个元素的数组,元素类型为整型,创建指针p,指针p存放的是数组首元素的地址,创建参数i,执行for循环,在循环内部对指针p进行解引用,向指针内部存放i值

错误原因:我们数组arr的内部只有十个元素,但我们却执行了11次for循环,导致越界访问

总结:当指针指向的范围超出数组arr的范围时,p就是野指针

3:指针指向的空间释放

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

对代码进行解释:在test函数部分,我们创建一个临时变量a,a=10;我们返回a的地址。在main函数部分,我们创建一个整形指针用来接受我们test函数的返回值。

野指针的原因是:

在test函数内部创建的参数a是局部变量,局部变量出范围自动销毁,假设我们的a的地址是0x11223344,test函数返回a的地址,我们用p来接受,这时候p还不是野指针,当p再进行使用时,p就是野指针。

如何减少野指针的产生呢?

我们先写一串代码

int main()
{
	int*p3 = NULL;
	*p3 = 100;
	return 0;
}

这个代码在运行时会崩溃,且在调试时会产生以下对话框

 由此可见,不能这样书写代码,错误原因是:

我们首先把p3指针赋值为空指针,空指针对应的0地址是不能访问的,由此可见,我们可以这种方法减少野指针的出现,例如

int main()
{
	int*p3 = NULL;

	if (p3 != NULL)
	{
		*p3 = 100;
	}
}

这里的NULL可以类比0,例如我们创建参数是int a=0;中间我们再进行操作,再通过if语句进行判断,如果p3=NULL,就意味着p3没有指向目标,所以其可能是野指针,如果p3!=空指针,我们再进行解引用,就不会产生野指针了

int main()
{
	int*p3 = NULL;

	if (p3)
	{
		*p3 = 100;
	}
}

这种方法也是可以的,if语句中的p3如果不等于NULL,也就是0时,就不是野指针。

我们再写一串代码,判断原因

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

这段代码我们进行分析:

在test函数部分:首先我们test函数返回的是一个整形指针,在函数内部,创建变量a=10,返回他的地址

在main函数部分:创建指针p来接受test函数的返回值,if语句进行判断,如果不是NULL,打印*p(也就是a的值)

在我们前面的分析当中,由于参数a在执行过test函数后,其占用的内存已被返回操作系统,我们的p接收其地址,我们再进行解引用操作访问就会构成非法访问,但打印结果如图所示

 为什么还是能打印出a的值呢?

原因是:虽然我们把参数a已经返回操作系统了,只是代表a的内存不再属于我们了,但是我们通过解引用依然能够得到a的值(前提条件是这块空间没有被使用),我们举一个例子

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

我们在访问*p之前再打印一个hehe,我们看最终的结果

 我们可以发现,结果发生了改变

如何避免野指针

1:指针的初始化

2:小心指针越界

3:指针指向的空间释放即置空指针

4:避免返回局部变量的地址

5:使用指针前检查其有效性

指针运算

指针加减整数

#define N_VALUES 5
int main()
{


	float values[N_VALUES];
	float*vp;
	for (vp = &values[0]; vp < &values[N_VALUES];)
	{
		*vp++ = 0;
	}
	return 0;
}

这串代码看上去非常复杂,我们将其简化

int main()
{
	float a[5];
	float*p;
	for (p = &a[0]; p < &a[5];)
	{
		*p++ = 0;
	}
	return 0;
}

a对应的是values,p对应vp,5对应的是[N_VALUES]

我们对这个代码进行解释:

首先创建一个数组,数组元素的类型是浮点型,数组元素个数是5,我们创建浮点型数组p,还没有初始化,有野指针的危险,我们在for循环中对指针进行了初始化后,野指针的可能性才消失,for循环的起始是p的数组a的首元素的地址,条件是p小于数组第六个元素的地址,如果满足条件,进入循环,*p++ = 0;其中的++为后置++,要先执行*p=0,再执行p++,所以在循环过程中,数组元素不断被赋值为0,直到第五次循环,第五次循环后,数组的第五个元素被赋值为0,p++,p定位到数组外的第六个元素,第六次循环的条件不满足,中断循环。

要注意,我们这里并没有越界访问,a[5]虽然不是数组内部的元素,但在内存中仍然存在,我们只是拿出来进行了比较,而没有对其进行使用,所以不构成越界访问。

数组下标的写法

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

这里,我们也实现了数组元素随数组下标的初始化

由此可得arr[i]=*(arr+i)

指针-指针

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

这个代码执行的结果是什么?

 我们可以发现结果为9,9是怎么来的呢?

答:第十个元素的地址-第一个元素的地址中间的元素个数

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

 我们将他倒置之后的结果是什么呢?

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

结果是什么呢?

 我们可以发现,结果是-9

总结:数组-数组的结果是数组间元素的绝对值

那是什么数组都能相减码?

不对

int main()
{
	int arr[10] = { 0 };
	char ch[5] = { 0 };
	printf("%d\n", &arr[5] - &ch[5]);
	return 0;
}

这两个代码的类型不同,相减结果无法预知,所以代码错误

总结:指向同一块空间的两个指针才能相减

那两个指针相减有什么意义呢?

我们在求字符串中元素个数可以使用,首先,我们介绍最简单的方法

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

这种方法比较简单,要注意的是char*str接受的是常量字符串“abcdef”,所以不能通过解引用更改字符串。

第二种方法

我们介绍一下指针-指针的版本

int my_strlen(char*str)
{
	char*start = str;
	while (*str != '\0')
	{
		str++;
	}
	return (str - start);
}
int main()
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	return 0;
}

start表示代表数组元素的第一个指针,str表示最后一个

指针+指针:指针+指针其实是没有意义的,类比日期+日期,日期-日期=天数 日期+日期啥也不是

指针的关系运算

for (vp = &valus[5]; vp > &values[0];)
{
	*--vp = 0;
}

vp是一个指针,从数组最末尾开始往前一次把数组内部元素中依次填0,结果为数组元素全部变成0

我们将代码进行简化


 

for (vp = &valus[5]; vp >= &values[0];vp--)
{
	*vp = 0;
}

在绝大多数编译器上,这串代码是可行的,但是我们应该避免这样写,因为这样并不符合标准

标准是:允许指向数组元素的指针与数组最后一个元素前面的内存对应的指针进行比较,但不允许指针元素的指针与数组第一个元素前面的内存对应的指针进行比较

指针和数组

数组:一组相同类型元素的集合

指针:是一个变量,存放的是地址

数组和指针之间的联系

int main()
{
	int arr[10] = { 0 };
	int*p = arr;
}

创建数组arr,指针p存放的是arr,arr是数组首元素的地址,我们可以通过这个地址访问到这个数组

如何访问呢?我们写一串代码

int main()
{
	int arr[10] = { 0 };
	int*p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d\n", *(p + i));
	}
	return 0;
}

sz表示数组的元素个数,我们使用for循环,通过数组首元素的地址,打印数组的每个元素。

所以arr[i]=*(p+i)

 实际上,p+i就等于&arr[i],我们写一串代码验证一下

int main()
{
	int arr[10] = { 0 };
	int*p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%p----%p\n", &arr[i],p+i);
	}
	return 0;
}

打印结果为

 int*p = arr; 所以p就等于arr,所以*(p+i)=*(arr+i)

数组传参的两种方法

1:

void test(int*p, int i)
{
	int j = 0;
	for (j = 0; j < i; j++)
	{
		printf("%d\n", *(p + j));
	}
}
int main()
{
	int arr[10] = { 0 };
	test(arr, 10);
	return 0;

}

第二种方法

void test(int arr[], int i)
{
	int j = 0;
	for (j = 0; j < i; j++)
	{
		printf("%d\n", arr[j]);
	}
}
int main()
{
	int arr[10] = { 0 };
	test(arr, 10);
	return 0;

}

实际上,这两种方法是一样的 因为arr[j]=*(arr+j)=*(p+j)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值