初步理解指针

 1.内存和地址

1.1 内存:

        在生活中,每个人都有自己的居住地,为我们遮风挡雨。同样数据也会有自己的”居住地“,即内存,计算机CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数 据也会放回内存中。同时内存也会划分成一个个内存单元,并且每个内存单元大小为一个字节,一个字节里面有八个比特位。

1.2 地址:

        顾名思义,地址在生活中即表示位置,就比如房间号,如果需要找一个人,有了房间号就能立刻定位一个人具体住在哪里,而不需要挨个房间找,大大提高了查找效率。同样,给内存编上序号,也能大大提高查找某一内存,进而读取使用,这个序号称之为“地址”,又名“指针”。

        CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节 很多,所以需要给内存进⾏编址(就如同宿舍很多,需要给宿舍编号⼀样)。然而计算机的编址并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
        首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要互相协同工作的。所谓的协同,至少相互之间要能够进行数据传递。 但是硬件与硬件之间是互相独立的,那么如何通信呢?答案很简单,用"线"连起来。 而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。 不过,我们今天关心⼀组线,叫做地址总线。 我们可以简单理解,32位机器有32根地址总线, 每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每⼀种含义都代表⼀个地址。 地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入 CPU内寄存器。
2.指针变量和地址
2.1取地址操作符(&):
        先来看一段代码:
#include <stdio.h>

int main()
{
	int a = 6;
	int* p = &a;
	printf("%p\n", p);

	return 0;
}

        代码中创建了变量a,在C语言中创建变量其实就是向内存申请一片空间,由于int类型占4个字节,所以a向内存申请了4个字节来存放整数10,并且这4个字节的地址是连续的(打印显示第一个地址)

        那我们该怎么取得a的地址呢,那得使用 取地址操作符 & 了,如:存在变量a,&a就是变量a的地址。我们可以将其打印出来:
#include <stdio.h>

int main()
{
	int a = 1;
	printf("%p\n", &a);

	return 0;
}

        多次执行就会发现,每次执行打印的地址都会不一样,这是因为每次执行都会为a重新开辟空间,所以会有所不同。

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

2.2.1指针变量

        从上面可以看到,把a的地址值赋值给p,并存在变量p中这一操作,p就称之为指针变量。

int a = 1;
int* p = &a; 

指针变量也是一种变量,这种变量是用来存放地址的,存放在指针变量中的值都会理解为地址,而指针变量也会被叫做指针。这就有人会有疑惑,不是说地址是指针吗?现在怎么又说指针变量也是地址?欸其实地址是指针这是没有错的,指针变量本质还是变量,被叫成指针是人们对其的简称,所以有时候要辨别一下。

        接下来就对上面的类型进行拆解,以便更好理解。

还是一上面那段代码为例,p的左边是int*,*在说明p是一个指针变量,int在说明这个指针指向的对象是int类型(即a)(这两句话要牢牢记住),int*即为p的指针类型。

同理:

char c = 's';
char* p2 = &c;

2.2.3解引用操作符

        既然我们把地址存在指针变量中,也不能只存着啊,总不能占着茅坑不拉*吧,总要发挥指针变量的用处,所以就有了解引用操作符(*),我们就可以拿着房间号(地址)去找某人了(指向对象)。先上代码:

#include <stdio.h>

int main()
{
	int a = 1;
	int* p = &a;
	printf("%d\n", a);
	printf("%d\n", *p);
	printf("%d\n", *(&a));

	return 0;
}

结果为

那么a == *p == *(&a)。

2.3指针变量的大小

        64位机器(x64环境),假设有64根地址总线,一个地址就是64个二进制组成的二进制序列,存储就需要8个字节的空间,那么指针变量的大小就是8个字节。

        同理,32位机器(x86环境),指针变量的大小就是4个字节。

        所以,一个指针变量的大小是4或8个字节,与指向对象的类型无关。

#include <stdio.h>

int main()
{
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(short*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(double*));
	return 0;
}

3.指针变量类型的意义

3.1指针的解引用

        既然指针变量的大小与指向对象的类型无关,指针变量在同一环境大小都一样,那有没有必要分那么多的指针类型呢?--其实是有必要,设计者这么设计一定是有他的深意吧......看代码:

调试我们可以看到,代码1会将a的4个字节全部改为0,但是代码2只是将b的第⼀个字节改为0。
结论:指针的类型决定了,对指针解引用的时候有多大的权限(⼀次能操作几个字节)。
比如: char* 的指针解引用就只能访问⼀个字节,而 int* 的指针的解引用就能访问四个字节。

 3.2指针 +-整数

先上代码:

#include <stdio.h>

int main()
{
	int a = 66;
	int* pi = &a;
	char* pc = (char*)&a;
	printf("%x\n", &a);
	printf("%x\n", pi);
	printf("%x\n", pi + 1);
	printf("%x\n", pc);
	printf("%x\n", pc + 1);

	return 0;
}

char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.3void* 指针
        
        在指针类型中有⼀种特殊的类型是 void* 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以⽤来 接受任意类型地址 。但是也有局限性, void* 类型的指针不能直接进行指针的+-整数和解引⽤的运算。 ⼀般 void* 类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以 实现泛型编程的效果。使得⼀个函数来处理多种类型的数据(后面再说)。
4.const修饰指针
4.1const修饰变量
        当一个变量被const修饰时,该变量就不能修改了如:
但a被const修饰后就一定不能改了吗?还有一个办法,还记得上面对一个地址解引用就是指向对象本身吗?我们可以试试这个办法:
可以看到a被修改了,有没有一种被偷家的感觉,通过指针解引用能间接修改a的值,所以const修饰的是a,只要我们想到其他办法绕过a就i能修改a的值。但如果指针变量p也被const呢?
可以看到,这个办法就失灵了。但是如果我把const放在*后面呢?
欸?
4.2const修饰指针变量
        看代码分析:
#include <stdio.h>
//代码1
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test2()
{
	//代码2
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = 20;//ok?
	p = &m; //ok?
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
void test4()
{
	int n = 10;
	int m = 20;
	int const* const p = &n;
	*p = 20; //ok?
	p = &m; //ok?
}
int main()
{
	//测试⽆const修饰的情况
	test1();
	//测试const放在*的左边情况
	test2();
	//测试const放在*的右边情况
	test3();
	//测试*的左右两边都有const
	test4();
	return 0;
}

        const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本⾝的内容可变;
        const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5.指针运算
5.1指针+-整数
        因为数组在内存中是连续存放的,只要知道第一个元素的地址,就可以找到后面的所有元素。( 数组名为首元素地址
#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa = arr;
	size_t num = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < num; i++)
	{
		printf("%d ", *(pa + i));
	}

	return 0;
}

5.2指针-指针

        指针-指针得到的是这两指针之间的元素个数。

#include <stdio.h>

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int* pa1 = arr + 1;//指向的是2
	int* pa2 = arr + 5;//指向的是6
	printf("%d\n", pa1 - pa2);
	printf("%d\n", pa2 - pa1);

	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 sz = sizeof(arr)/sizeof(arr[0]);
 while(p<arr+sz) //指针的⼤⼩⽐较
 {
 printf("%d ", *p);
 p++;
 }
 return 0;
}

6.野指针

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

6.1野指针成因

1.指针未初始化

2.指针越界访问

3.指针指向的空间已经释放

6.2如何规避野指针

6.2.1初始化指针

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

6.2.2小心指针越界

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

6.2.3当指针变量不再使用时,及时置为NULL,在指针使用前检查有效性。

assert(p != NULL)

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

        因为出了某个区域局部变量空间就会释放,所以不应返回该地址。

指针部分暂时讲这么多,如果内容存在错误或者需要补充的,欢迎评论区留言,感谢观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饼干烧饼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值