当前位置: 首页 > news >正文

指针(1)

1.内存和地址

1.1内存

内存就好比一栋宿舍楼的一间一间的房间,而地址好比房间号,告诉你的朋友你的房间号才可以更快的找到你;在计算机中将内存划分为一个一个的内存单元,每个内存单元的大小是取1个字节。

计算机中常见的单位:

bit - 比特位

byte - 字节

KB

MB

GB

TB

PB

1 byte = 8 bit

1 KB = 1024 byte

1 MB = 1024 KB

1 GB = 1024 MB

1 TB = 1024 GB

1 PB =1024 TB

生活中我们把门牌号叫做地址,在计算机中我们把内存单元的编号叫做地址,在C语言中给地址起了新的名字:指针。

所以我们可以理解:

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

1.2 该如何理解编址

CPU访问内存中的某个字节的空间,必须知道这个字节空间在内存的什么位置,又因为内存中的字节有很多,所以需要给内存进行编址(同宿舍编号一样)。

计算机中有许多的硬件单元,而硬件单元是相互协同工作的。用 "线" 将相互独立工作的硬件相连接起来,今天只了解一组线:地址总线。

32位机器有32根地址总线,每两根线有两种状态:0 和 1【电脉冲有无】,那么32根地址线就有2^32种含义,每种含义都代表一个地址。地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据再通过数据总线传入CPU内寄存器。

2.指针变量和地址

2.1取地址操作符(&)

在C语言中创建变量其实就是向内存申请空间,比如:

如何得到 a 的地址呢?

取地址取得的是 a 所占4个字节中地址较小的地址。只要知道第一个地址我们就可以顺藤摸瓜访问到4个字节的数据。

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

2.2.1 指针变量

我们通过取地址操作符(&)得到了变量的地址,例如上面的例子中:0x010FFB68,但是这个16进制的数据也是需要存储起来的,这个时候就要用 指针变量 将 地址存储起来。

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

2.2.2 拆解指针变量类型

pa 左边的 int * 中的 * 说明 pa 是指针变量,而前面的 int 是在说明 pa 指向的是整型(int)类型的的对象。(int 指向的是在指针变量 pa 中所存储的内容是 int,也就是 a 的值(int 类型))。

char 类型也是一样的:

2.2.3 解引用操作符

在C语言中,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,这个时候就要用到一个操作符叫做解引用操作符(*) 

int main()
{
	int a = 10;
	int* pa = &a;
	*pa = 20;// * 是解引用操作符,*pa 等价于a
	//a=20;等价于重新给a赋值
	printf("%d\n",a);

	//char ch = 'w';
	//char* pc = &ch;

	return 0;
}

2.3 指针变量的大小

前面我们提到32位机器有32根地址总线,没跟地址线出来的电信号转换成数字信号后是1或者是0,我们将32根地址线产生的2进制序列当作一个地址,那么一个地址就是32个bit位,需要4个字节才能存储。如果指针变量是用来存放地址的,那么指针变量的大小就的是4个字节的空间才可以。

同样64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。

结论:

32位平台下地址是32个bit位,指针变量大小是4个字节

64位平台下地址是32个bit位,指针变量大小是8个字节

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

 

3.指针变量类型的意义

前面的例子中我们发现不管是什么类型的指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的类型呢?

从下面的代码就能感受到指针类型的意义:

int main()
{

	int a = 0x11223344;
	int* pa = &a;
	*pa = 0; //将内存中的 44 33 22 11 全部改为 00 00 00 00
           //这是因为 pa 是 int *的类型,一次访问4个字节,所以能将4个字节全部修改为0 
	//在调试中打开内存就可以看见 *pa 中的 * 的作用,
	//对 pa 进行解引用,间接地来改变 a 的值 
	return 0;

}

在调试中查看,感受更为直观: 

int * 类型的指针变量:

一下就操作了4个字节的大小的内容,将4个字节的内容全部置为0。

char * 的指针变量类型:

将 a 的指针类型 int * 转换为 char * 类型,在下一步的调试中会有区别吗?

从上面的调试中我们发现只改变了一个字节大小的内容,这就充分体现了指针变量类型的重要性。 

结论:

指针类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)char * 的指针解引用就只能访问1个字节,而 int * 的指针的解引用就能访问4个字节

3.2 指针 + - 整数

从上面的例子我们就可以看见虽然 pa 和 pc 指针变量大小是一样的,但是 +1 操作后的数据是完全不同的,这个便与指针类型有关了。

指针变量类型的意义在于:
1.对指针解引用的时候有多大的权限(一次能操作几个字节)
2.指针向前或者向后走一步有多大(距离)

3.3 void * 指针

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

int main()
{
	int a = 5;
	int* pa = &a;
	char* pc = &a;//需要转换一下类型,因为&a的类型是 int *,编译的时候会报错
	return 0;
}

会出现以下的报错:

但是用 void * 来接收的话,就不会出现这样的问题:

但是无法进行指针运算:

int main()
{
	int a = 5;
	void * pa = &a;
	void * pc = &a;
	*pa = 11;
	*pc = 7;
	return 0;
}

 

一般 void * 类型的指针是使用在函数的参数部分,用来接收不同类型的地址。 

4.const修饰指针

4.1const 修饰变量

变量是可以被修改的,如果把变量的地址交给指针变量,那么也可以通过指针变量来修改这个表变量。如果我们不想让变量被修改,这个时候就要用上const了。

int main()
{
	const int a = 8;// a 不能被修改了,但是a的本质还是变量,const仅仅是在语法上
	                //做了限制,所以我们习惯上叫a是常变量。
	a = 15;// error 执行时就会出现错误 加上const a的值就不能被修改了
	printf("a=%d\n", a);

	return 0;
}

从上面的代码中我们可以看见,在 a 的前面加上了 const 后,后续对 a 进行修改会发现修改不了,但是我们将 a 的地址存放在指针变量中,通过指针变量来进行对 a 的值的修改,这样可以成功吗?

从上面的运行结果来看,是可以通过指针变量来对 a 进行修改的,这样就打破了 const 的限制,这是不合理的,应该让 pa 拿到 a 的地址也不能修改 a,那应该怎样编写代码呢?

4.2 const 修饰指针变量

一般来讲 const 修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不一样的。

int const * p;

int * const p;

int main()
{
	const int a = 10;
	//const 在 * 的左边
	int const * pa=&a;//等价于 const int * pa =&a;
	//*pa = 13;//error 这个时候就会出现错误:不能修改 *pa的值
	int b = 17;
	pa=&b;//ok
	printf("%d\n", *pa);

	return 0;
}
int main()
{
	const int a = 10;
	//const 在 * 的右边
	int * const pa = &a; //不能修改 pa 的值
	*pa = 13;//ok
	int b = 17;
	//pa = &b;//error
	printf("%d\n", *pa);

	return 0;
}

int main()
{
	const int a = 10;
	//const 在 * 的左右两边,不能修改 * pa(解引用对象的内容)的值和pa(地址)
	int const *const pa = &a;
	//*pa = 13;//error 这个时候就会出现错误:不能修改 *pa的值
	int b = 17;
	//pa = &b;//error 这个时候就会出现错误:不能修改 pa的值
	
	printf("%d\n", *pa);

	return 0;
}

结论:

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

5.指针运算

指针的基本运算有的三种,分别是:

  • 指针 + -  整数
  • 指针 - 指针
  • 指针的关系运算

5.1 指针 + - 整数

数组是在内存中连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能访问到后面的所用元素。

int arr[10]={10,9,8,7,6,5,4,3,2,1};

 

int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };
	int *p=&arr[0];
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p+i));//p+i 加的是 i * sizeof(int)
		//p=0x001122ff10
		//p+1=0x001122ff14
		//p+2=0x001122ff18
		//p+3=0x001122ff1c

	}
	return 0;
}

5.2 指针 - 指针

//指针-指针=地址-地址=元素的个数

int main()
{
	int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };

	printf("%d\n", &arr[9] - &arr[0]);
	//指针-指针的绝对值是指针和指针之间的元素个数
	//指针相减的运算在底层其实是(arr[9] - arr[0])/ sizeof(元素类型)
	//指针-指针运算的前提条件是:两个指针指向同一块空间
	char ch[6] = {0};
	printf("%d\n", &ch[0] - &arr[0]);//error 但是输出的结果是 -16 why?	
	    //有可能地址差位 -64 字节,若按 int 小计算元素个数:-64/4=-16
	return 0;
}

指针 - 指针的作用??

可以简单地实现一下 strlen函数的作用:

int my_strlen(char * s)
{
	char* start = s;
	while (* s)// \0 的ASCII码值是0
	{
		s++;
	}
	return s - start; //指针-指针
}

int main()
{   //strlen 求字符串的长度—统计的是 \0 前面出现的字符的个数
	//printf("%d\n", strlen("abcdefgh"));//8
	int len = my_strlen("abcdefghi");
	printf("%d\n", len);
	return 0;
}

5.3 指针的关系运算

int main()
{
 int arr[10] = {10,9,8,7,6,5,4,3,2,1};
 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.指针未初始化

int main()
{
	
	int* ptr;//野指针
	*ptr = 17;//非法访问内存
	//printf("%d\n", *ptr);

	return 0;
}

2.指针越界访问

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

3.指针指向的空间释放

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

}

int main()
{
	int* p = test();
	printf("hehe\n");//这里加了一句代码,*p 的值就变了 why?
	printf("%d\n", *p);
	return 0;
}

6.2如何规避野指针

6.2.1 指针初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针指向哪里,可以给指针赋值NULL。

NULL是C语言中定义的一个标识符常量,值是0,0也是地址,不过这个地址是无法使用的,读写该地址是会报错的。

int main()
{
	int num = 19;
	int* pn = &num;
	int* p  = NULL;
	return 0 ;
}

6.2.2 指针越界

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

6.2.3 指针变量不再使用是,需要及时置为NULL,使用之前检查有效性

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

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

	//此时p已经越界了,可以把p置为NULL
	p = NULL;
	//使用p之前,先要判断p是否为空,不为NULL才可以使用
	//……
	p = &arr[0];//重新让 p 获得地址
	if (p != NULL)//判断
	{

		//……
	}
	
	return 0;
}

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

造成野指针的第3个例子,不要返回局部变量的地址。

7.assert断言

assert.h 头文件定义了 宏 assert ( ),用于在运行时确保程序符合指定条件,如果不符合,就报错,终止运行。这个宏常常被称为 “断言”

assert( p ! = NULL );

运行到这一行的语句时,验证变量 p 是否等于 NULL。如果确实不等于 NULL,程序就会继续运行,否则就会终止运行,并报错。

assert 宏 接收一个表达式作为参数。如果该表达式为真(返回值非0),assert()不会产生任何作用,程序继续运行。如果表达式为假(返回值为0),assert ( )就会报错,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

assert 的好处:

  • 能自动的表示文件和出问题的行号
  • 无需更改代码就能开启或者关闭 assert()的机制

 如果确认程序没有问题,不需要再做断言的操作了,就在 #include <assert.h> 语句前面,定义一个宏 NDEBUG。

#define NDEBUG
#include <assert.h>

 使用上述的代码后在编译程序的话,编译器就会禁用掉文件中的所有 assert ( ) 语句。程序出现问题,就注释掉这条语句,再次编译,就重新启用了assert()语句。

assert ( ) 缺点:

assert ( ) 因为引入了额外的检查,就会增加程序的运行时间。

一般我们在 Debug 版本中使用,在 Release 版本中选择禁用 assert 就行。这样在 Debug 版本中写有利于程序员排查问题,在 Release 版本中不影响用户使用程序的效率。

8.指针的使用和传址调用

8.1 strlen 的模拟实现

库函数 strlen 的功能时求字符串长度,统计在 \0 前面的字符的个数。

函数原型如下:

size_t  strlen (const char * str ); 

参数 str 接收一个字符串的起始地址,统计字符串中 \0 之前的个数,最终返回长度。

int my_strlen(char* s)
{
	int count = 0;
	while (*s != '\0')
	{
		count++;
		s++;
	}
	return count;
}

int main()
{   //strlen 求字符串的长度—统计的是 \0 前面出现的字符的个数
	//printf("%d\n", strlen("abcdefgh"));//8
	int len = my_strlen("abcdefghi");
	printf("%d\n", len);
	return 0;
}

8.2 传值调用和传址调用

例如:用代码实现两个整数的交换

传值调用:

Swap1(int x, int y)
{
	int z = 0;
	z = x;
	x = y;
	y = z;
}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a,&b);
	printf("交换前 a=%d b=%d\n", a, b);
	//交换a、b的值
	Swap1(a, b);//传值调用
	printf("交换后 a=%d b=%d\n", a, b);
	//运行代码会发现:ab的值并没有交换,这是因为在Swap1函数中进行了两数的交换
	//xyz都属于局部变量,出函数即被销毁,所以传值调用中形式参数的改变不会影响
	//实际参数的改变
	return 0;
}

 从上面的代码运行结果就可以看出来,并没有实现两个整数的交换。

结论:

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

传值调用:

Swap2(int* x,int *y)//传过来的是地址,用指针来接收
{
	int tmp = 0;
	tmp = *x;   //tmp=x
	*x = *y;    //x=y
	*y = tmp;   //y=tmp

}

int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前 a=%d b=%d\n", a, b);
	//交换a、b的值
	Swap2(&a,&b);//传址调用
	printf("交换后 a=%d b=%d\n", a, b);
	
	return 0;
}

传值调用可以让函数和主函数之间建立真正的关系,可以实现在函数内部通过地址就可以修改主函数中的变量。所以希望在函数内部达到修改主调函数中的变量的值,就需要传址调用。 

相关文章:

  • 短波红外高光谱相机:高光谱成像在塑料分选中的应用
  • PHP + Go 如何协同打造高并发微服务?
  • NAS-RAID方案之snapRAID
  • Spark-SQL简介与编程
  • 4月14日星期一今日早报简报微语报早读
  • 2025年常见渗透测试面试题-红队面试宝典上(题目+回答)
  • AI 项目详细开发步骤指南
  • 构造HTTP请求
  • mapbox V3 新特性,加载风粒子动画
  • VSCode 降低适用版本并且关闭自动更新
  • 代码随想录第17天:二叉树
  • Spring Boot 集成 RocketMQ 全流程指南:从依赖引入到消息收发
  • 【Three.js基础学习】35.Particles Cursor Animation Shader
  • 【笔记】对抗训练-GAN
  • 论文精度:双分支图Transformer网络:视频驱动的3D人体网格重建新突破
  • 记一次某网络安全比赛三阶段webserver应急响应解题过程
  • Spring Cloud主要组件介绍
  • 解密Oracle数据库RAC技术原理:构建高可用、可扩展的集群数据库
  • sward安装及入门指南
  • 揭秘大数据 | 20、软件定义数据中心
  • 88岁罗马教皇方济各突然去世,遗嘱内容对外公布
  • 周继红当选中国泳协主席,曾为国摘得首枚奥运跳水金牌
  • 花3000元就能买“国际机构”的证书?揭秘假证产业链
  • 大国重器飞天背后,有一位上海航天的“老法师”
  • 申花迎来中超三连胜,这一次终于零封对手了
  • “特朗普的欧洲耳语者”:梅洛尼白宫之行真能打破美欧关税僵局?