C语言指针基础

news2024/10/7 16:23:26

目录

前言

本期介绍内容:

一、指针是什么?

二、指针和指针类型

指针类型的意义:

三、野指针

3.1什么是野指针?

3.2野指针的成因

1.指针未初始化

2.指针越界访问

3.指针指向的那块空间已经释放(还给操作系统了)

3.3如何规避野指针

四、指针运算

4.1指针加减整数

4.2指针-指针

4.3指针的关系运算

五、指针和数组

六、二级指针

6.1什么是二级指针?

七、指针数组 


前言

我们经常听到很多大佬都说指针是C语言中的精髓、指针很重要,但对于很多初学者都觉得指针很难、不都不知道指针是什么?指针到底指向哪里??指针和地址是什么关系?本期小编将带您弄清楚指针的基本问题!

本期介绍内容:

1.指针是什么

2.指针和指针类型

3.野指针

4.指针运算

5.指针和数组

6.二级指针

7.指针数组

一、指针是什么?

1.指针是内存中一个最小的单元编号,也就是地址(唯一标识一个内存单元)。

2.我们一般所说的指针是指针变量,是用来存放内存地址的变量。

也就是说:指针就是地址,口语中的指针是指针变量

为了更好地管理内存,计算机把内存分为了若干的内存单元,每一个内存单元都有一个唯一的编号也就是地址。由于该地址(编号)指向该内存空间因此也形象的把地址(编号)称为指针!

OK,画个图理解一下:

指针变量:我们知道地址在内存中是以十六进制的形式存储,通过&操作符就可以取出变量的地址,取出来要一个变量接收与存储,这个变量就是指针变量!

举个栗子:

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

int* pa 的*号说明pa是一个指针变量(口语中是指针),前面的int说明pa指向对象的类型是一个整型。后面的&a取出a的地址赋值给pa此时pa就指向a;

总结:指针变量是用来存放地址的变量(存放在指针变量的值都被当成地址来处理)

到这里我们就基本了解清楚了指针是什么以及指针和地址的关系,指针和指针变量的关系!

通上面的介绍我们知道指针唯一标识一块内存单元。但又有一些新的问题出现了:例如

一个内存单元的大小是多大呢?

还有地址是十六进制的数他又是如何进行编址的呢?

指针变量的大小是多少?

下面我们一起来分析一下:

通过了解计算机组成原理发现一个内存单元给一个字节较为合适(既不浪费也不会不够)。

解释:我们知道C语言中char类型的变量占一个字节,如果说每一个内存单元大于1个字节,char类型的变量开辟空间的时候就会出现浪费的情况,因此综合下来一个内存单元为一个字节刚好!

另外,它的编址是由地址线产生的高低电频来进行的

对于32位平台(x86)机器,假设有32根地址线(物理电线),那么假设每根地址线在寻址的时候都会产生高电平(高电压)和低电频(低电压)就可以把电信号转换换为数字信号也就是0和1(这块是电路方面的数电和模电小编了解过一二,它里面还介绍了加法器,二极管等等有兴趣的可以看看);

那么32根地址线就会是下面这中情况:

这里就是2^32个地址。而我们上面知道每一个地址标识唯一的一个内存单元,我们就可计算一下2^32个地址可以给多大的空间进行编址(2^32Byte == 2^32/1024KB == 2^32/1024/1024MB == 2^32/1024/1024/1024GB == 4GB)也就是说2^32个地址可以为4GB的内存空间编址。64位平台的机器同理,感兴趣的可以算一算!

32位平台的机器上,地址是32个0或1组成的二进制序列,那地址就得用4个字节的空间来存储,所以一个指针变量的大小是4个字节

64位平台的机器上,地址是64个0或1组成的二进制序列,那地址就得用8个字节的空间来存储,所以一个指针变量的大小是8个字节

举个栗子验证一下:

int main()
{
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(float*));
	printf("%d\n", sizeof(double*));
	printf("%d\n", sizeof(long*));
	return 0;
}

32位平台(x86):

64位平台(x64):

 

这里还要注意一点sizeof返回值是size_t实际上是unsigned int

 打印的时候要用%zd否则会报警告! ! !

总结

指针变量是用来存放地址的,地址是唯一标识一个内存单元的。

指针的大小在32位平台上是4个字节,在64位平台上是8个字节。

二、指针和指针类型

我们都知道,变量有不同的类型,有整型、浮点型,字符型等,那么指针变量有没有类型呢?

答案是:有的!

我们将一个变量的地址取出来(&)存到pa中,pa就是一个指针变量,给他的类型是什么呢?

答案是:指针指向的那个变量的类型加*即:type + *

举个栗子:

int main()
{
	int a = 3;
	int* pa = &a;
	char b = 'a';
	char* pb = &b;
	float f = 1.2f;
	float* pf = &f;
	double d = 13.4;
	double* pd = &d;
	return 0;
}

这里int*类型的指针存放的是int类型变量的地址,char*的指针存放的是char类型的地址...上面我们知道指针类型要么都是4个字节要么8个字节,那为什么还要分类型呢?

指针类型的意义:

先说结论:

(1)指针类型可以决定指针解引用的时候的访问权限(可以访问多好个字节)

(2)指针类型可以决定指针+/-的步长

例如:int类型指针+1跳过4个字节,char类型的指针+1跳过1个字节,那+/-n 就是跳过n *sizeof(type)个字节!!!

解释(1):

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

关于这段代码解释两点,首先a存的这个数(16进制)不会溢出,十六进制的1位相当于二进制的4位,int类型的a32个二进制位所以11223344刚好把a放满!其次,会发现他在内存中是倒着存的,这个是大小端字节序的问题,后面会专门有一期介绍!

这是执行到*pa = 0;发现变成了00 00 00 00

 我们把int *改为char *再来看看:

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

一开始还是和int*的一样!我们接着往下看:

我们发现只改掉了 一个字节的内容!这就完美的证明了上面的结论1!

解释(2):

int main()
{
	int a = 3;
	int* pa = &a;
	char* pc =(char*) &a;
	printf("%p\n", pa);
	printf("%p\n", pc);

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

我们分析,前两个打印的是一样的,主要看后两个:

前两个果然一样(都存的是第一个字节的地址),后面是因为一个是int类型一次访问4个字节,另一个是char类型一次访问一个字节 ,访问权限不一样步长也就不一样!

三、野指针

3.1什么是野指针?

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

3.2野指针的成因

1.指针未初始化

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

此时,p是一个指针变量未初始化,也是局部变量(在函数内)由前面的函数栈帧的创建与销毁可知,局部变量未初始化是随机值(0hccccc),把随机的一块内存给改成10,这多少有点危险!

2.指针越界访问

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

arr数组只有十个元素,下标0--9,你直接访问10将是一个随机值!

3.指针指向的那块空间已经释放(还给操作系统了)

int* test()
{
	int a = 3;
	return &a;
}

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

这里a 是在test函数里面创建的,当return &a后test的函数栈帧就已经销毁了(还给操作系统了),当下面在去解引用的时候就指向随机位置了!

3.3如何规避野指针

(1)指针初始化

(2)小心指针越界

(3)指针指向的空间释放及时置为NULL(NULL空指针实际上就是0)

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

(5)指针使用之前检查其有效性(if判断和断言)

举个栗子:

#include<assert.h>
int main()
{
	int* a = NULL;//指针初始化
	int p = 8;
	int* pp = &p;
	assert(pp);
	*pp = 20;
	printf("%d ", *pp);

	int b = 12;
	int* pb = &b;
	if (pb != NULL)
	{
		*pb = 20;
	}
	printf("%d\n", *pb);
	return 0;
}

四、指针运算

4.1指针加减整数

指针加减整数的结果我们知道还是一个指针,我们可以通过指针的加减的偏移量来访问指针指向的变量的值(例如数组)!

举个栗子:

int main()
{
	int arr[10] = { 1 };
	int* pp = &arr[0];
	for (int i = 0; i < 10; i++)
	{
		*pp++ = 0;
	}

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

这个栗子就很好的展现了指针加一个整数去通过其偏移量来访问元素的值!

补充:

这里我们可以看到在打印的时候是以*(p + i)打印的,我们在没有了解指针的时候是以arr[i]打印的,而p里面存的是arr数组的首元素的地址,也就是说*(arr + i) 和arr[i]是等价的!而由加法的交换律我们可以变形一下: *(arr + i)  == *(i + arr)而*(arr + i)  ==arr[i] 我们可以推断 *(i + arr) == i[arr]是否也成立呢?

验证一下:

答案是成立的!这里小编验证这个不是为了说以后建议大家写代码就这样写,这样写多少有点太装了!!!这里介绍这个主要是为了再次说明一下:下标引用操作符[ ] 它的两个参数一个是数组名一个是索引(下标),他和 + 一样,a + b == b + a 一个道理!

4.2指针-指针

不知您是否想过指针减指针(指向同块内存)的结果是什么?是指针还是一个数?下面小编带您一起来讨论一下:

先来看个栗子:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	printf("%d", &arr[9] - &arr[0]);
	return 0;
}

考虑一下答案是多少?

答案是9!为什么是9呢?下面我来画个图解释一下 :

我们通过对数组和内存的了解画出了上面的这个图!并得出结论:

指针减指针得到的绝对值(可能是低地址减高地址也有可能是高地址减低地址,所以取绝对值)是两指针之间的元素个数!并且注意的是:指针-指针的前提是两指针指向同一块内存!!否则无意义!

介绍到这里我就想到了一个与这个高度贴合的题(模拟实现strlen 指针-指针)当然模拟实现strlen 的方式有三种这里我将介绍这一种(后面会一一把其他两种介绍):

int  MyStrlen(char* str)
{
	char* p = str;
	while (*str)
	{
		str++;
	}

	return str - p;
}

int main()
{
	char str[] = "abcdef";
	int len = MyStrlen(str);
	printf("%d ", len);
	return 0;
}

strlen 是求字符串长度(‘\0’之前)的函数!当然我们这仅仅是一个雏形,基本功能已经实现,已经能精准的测量出字符串的长度了,后面还有优化的点等到后面介绍了断言和const了再来优化!

看一下结果:

4.3指针的关系运算

我们知道地址有大小,而指针的关系运算就是比较指针的大小!

OK!举个栗子:

int main()
{
	int arr[5] = { 1 };
	int* p = NULL;
	for (p = &arr[5]; p > &arr[0]; )
	{
		*--p = 0;
	}
	return 0;
}

这段代码不是您是否看的懂?我们一起来看一看:

 此时聪明的你肯定能想到优化方案:

int main()
{
	int arr[5] = { 1 };
	int* p = NULL;
	for (p = &arr[4]; p >= &arr[0]; p--)
	{
		*p = 0;
	}
	return 0;
}

OK,这个方案的确看起来好多了!但,这样写的代码对吗?

答案是:在有的编译器上是不一定对的会报越界的警告!

C语言标准规定:

允指向数组元素的指针与指向数组左后一个元素后面的那个内存中位置的指针比较,但不允许与指向数组第一个元素的那个内存位置的指针比较!

什么意思我们画个图理解一下:

虽然现在部分编译器对上面的第二段代码不会报错但有的编译器会,为了以后不会出现类似的问题还请大家遵循标准!!!

我们现在来看看他为什么不对:

我们发现当你初始化到&arr[0] 的空间时候还是成立的,那就先初始化再调整!p--就到数组前面的一块空间去了!

五、指针和数组

我们说:指针是指针,数组是数组,一般无关系!只有数组名和首元素地址有关!

OK,我们先来看个栗子:

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

他的结果是什么呢?

哦吼~他两的结果一样,所以可以得出结论:数组名一般情况下表示数组首元素的地址!两种情况除外(1.sizeof(数组名)计算的是整个数组的大小 2.&数组名取出的是整个数组的地址)。这块在数组和操作符都介绍过,这里不在多介绍了!

既然数组名是首元素的地址也就是指针,那么能不能用指针的偏移量来不访问数组元素呢?

答案是肯定的:

六、二级指针

我们知道指针变量里面存的是变量的地址,不知您有没有想过对指针变量取地址该存到哪里?

6.1什么是二级指针?

其实指指针变量也是变量,他也有地址(和普通变量一样)&后就得用指针变量存起来,而这个指针变量里面存的就是指针的地址(指针),也就是说一个指针变量里面存的是另一个一级指针变量的地址这样的指针变量就叫二级指针

举个栗子:

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

 ppa就是一个二级指针!!  int** ppa后面的一颗*说明paa是一个指针变量!前面的int*则说明ppa指向的对象是(一级)指针类型!

理解到这里就可以以二级指针类比三级指针了:三级指针就是里面存的是二级指针变量地址的指针变量!

int main()
{
	int a = 3;
	int* pa = &a;
	int** ppa = &pa;
	int*** pppa = &ppa;
	*(*(*pppa)) = 10;
	printf("%d ", a);
	return 0;
}

我我们这里pppa就是一个三级指针,*pppa指向ppa,*ppa指向pa,*pa指向a!然后赋值10;

七、指针数组 

我们经常听说的两个东西:一个是指针数组一个是数组指针很多人都搞不清楚这个东西,其实数组指针本质是指针!而指针数组本质是数组!下面小编带您look一look一下指针数组,数组指针会在后面的指针进阶里面讲!本期仅仅在这里提一下,后面重点对比!!!

指针数组本质是一个数组!是存放指针的数组!

我们知道一个整型或字符行的数组,里面都存放的是相对应类型或比该类型小的元素,那指针数组里面存的应该就是地址!

举个栗子:

int main()
{
	int arr1[] = { 1,2 };
	int arr2[] = { 3,4 };
	int arr3[] = { 5,6 };
	int* arr[] = { arr1,arr2,arr3 };
	return 0;
}

此时的arr就是一个指针数组!画个图理解一下:

看到这里就很应该很清楚了!不知您看到这个图的时候有没有一点感觉很熟悉? 这个是不是和二维数组很相似。arr管理的三个一维数组,我们前面说过二维数组是数组的的数组!是不是和这个很相似?那我们能不能访问呢?

int main()
{
	int arr1[] = { 1,2 };
	int arr2[] = { 3,4 };
	int arr3[] = { 5,6 };
	int* arr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 2; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");

		for (int j = 0; j < 2; j++)
		{
			printf("%d ", *(*(arr + i) + j));
		}
		printf("\n");
		
	}
	return 0;
}

看结果,他这两种打印结果一样:

OK,本期指针基础就分享到这里!好兄弟我们下期再见! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/716177.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像处理--边缘检测算子

算子推导过程 1、知识引入&#xff1a; 在一维连续数集上有函数f(x),我们可以通过求导获得该函数在任一点的斜率&#xff0c;根据导数的定义有&#xff1a; 在二维连续数集上有函数f(x,y),我们也可以通过求导获得该函数在x和y分量的偏导数&#xff0c;根据定义有&#xff1a; …

更改VS code Jupyter 插件的默认快捷键

更改vscode 中Jupyter插件的默认快捷键&#xff0c;解放插入空行的系统快捷键 替换Jupyter默认快捷键 更改vscode 中Jupyter插件的默认快捷键&#xff0c;解放插入空行的系统快捷键打开keyboard shortcuts 设置方法一方法二 更换快捷键 end Jupyter 插件很好的在VS code中集成了…

在私家车上安装车载电台合法吗?

答案是合法的&#xff0c;不过需要办理相关手续才行。想在自己的车上安装车载无线电台&#xff0c;需要先考取一个业余无线电操作证&#xff0c;然后再将车载电台拿到当地的无线电管理委员会进行验机&#xff0c;如果符合业余广播电台的机器要求&#xff0c;将颁发《中华人民共…

淘宝数据采集员(如何快速高效地采集淘宝数据)

目录 淘宝数据采集的重要性 淘宝数据采集的意义 淘宝数据采集的应用场景 淘宝数据采集的流程 淘宝数据采集的准备工作 淘宝数据采集的技巧与注意事项 淘宝数据采集的工具推荐 淘宝数据采集的流程 如何快速高效地采集淘宝数据 淘宝数据采集的注意事项 淘宝数据采集的重…

安装qt qmake assistant 错误:could not find a Qt installation of ‘‘

1、执行qmake,提示下图的错误 Command qmake not found, but can be installed with: sudo apt install qtchooser 解决方法&#xff1a; sudo apt install qtchooser 2、执行qmake,提示一下错误 qmake: could not find a Qt installation of 解决步骤&#xff1a; 步骤一&a…

spring源码分析-ApplicationContext----扩展组件event listener

我们知道 spring中的ApplicationContext在beanFactory(提供基础bean处理)基础上增加了扩展组件&#xff0c;例如国际化&#xff0c;资源&#xff0c;发布事件和监听事件&#xff0c;今天主要针对发布和监听事件做一次源码分析&#xff0c;看到底发布和监听是如何实现的&#xf…

Promise的常见面试题

四、相关面试题 1. 多个 .catch var p new Promise((resolve, reject) > {reject(Error(The Fails!)) })p.catch(error > console.log(error.message)) p.catch(error > console.log(error.message))以上代码的输出将会是什么&#xff1f; 打印两次 The Fails! 解析…

纯粹数学作业

1、列举一个真命题、一个假命题、一个不知道真假的命题、一个不是命题的陈述。 真命题&#xff1a;太阳是发光发热的。 同位角相等&#xff0c;两直线平行。 假命题&#xff1a;三角形的三个内角和不是180 不知真假的命题&#xff1a;树叶是黄色的。 不是命题的陈述&#x…

伦敦银实时行情软件日常使用

目前伦敦金市场上很多交易都是通过MT4平台来完成&#xff0c;这个行情软件的优势&#xff0c;除了操作简洁、分析功能全面和交易稳定外&#xff0c;支持不同的手机和电子设备版本也是很重要的一点。但很多用户&#xff0c;特别是对于新手日常在操作的过程中&#xff0c;都会或多…

jdk1.8官网下载

地址&#xff1a; https://www.oracle.com/java/technologies/downloads/#java8 选linux x86位的&#xff1a;

【C语言】从水仙花构建思维

前言 &#x1f388;大家好&#xff0c;我是何小侠&#x1f388; &#x1f343;大家可以叫我 小何或者小侠 &#x1f343; &#x1f490;希望能通过写博客加深自己对于学习内容的理解&#x1f490; &#x1f338;也能帮助更多人理解和学习&#x1f338; 击石乃有火&#xff0c;…

【反图+拓扑排序】ABC245 F

F - Endless Walk (atcoder.jp) 题意&#xff1a; 思路&#xff1a; 首先&#xff0c;我们什么时候需要建反图&#xff1a;在一个有向图中&#xff0c;我们需要找出可以到达指定的结点的结点时&#xff0c;我们可以建立反图 这道题中&#xff0c;我们需要找出所有能够到达环…

剑指offer刷题笔记--Num31-40

1--栈的压入、弹出序列&#xff08;31&#xff09; 直观思路&#xff1a;用两个指针 i 和 j 指向压入和弹出的 vector&#xff0c;终止条件是&#xff1a;所有元素都压入了辅助栈&#xff08;i > len&#xff09;&#xff0c;且辅助栈当前的栈顶元素与弹出的元素 popped[j] …

Maven Snapshot 更新策略配置方法

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 在微服务的项目开发中&#xff0c;特别是更新比较频繁的项目&#xff0c;经常会遇到一些项目依赖的问题&#xff0c;依赖的一个项目经常更新&#xff0c;所以我下拉更新项目时候经常出现代码不一致&a…

Ansys Zemax | 使用软件建立立方体卫星系统(一)

在航空航天工业领域中&#xff0c;立方体卫星&#xff08;CubeSats&#xff09;已然是一种低成本、易制造的航天光学系统的解决方案。通过制造一组更小、更实惠的系统&#xff0c;使得为航天产品开发生产线方法成为可能。 立方体卫星光学系统的制造商们需要一个准确并可靠的方法…

基于微信小程序学校部门年终绩效考核自动评分系统(源码+文档+数据库+PPT)

基于微信小程序的部门年终绩效考核系统&#xff0c;为加强学校运营队伍建设提高学校管理力&#xff0c;合理评价教师及部门年度工作计划完成情况&#xff0c;促进整体绩效改进&#xff0c;鼓励管理团队注重对下属进行帮助、提升&#xff0c;促进团队扩张和发展&#xff0c;特制…

【花雕】全国青少年机器人技术一级考试备考实操搭建手册10

随着科技的不断进步&#xff0c;机器人技术已经成为了一个重要的领域。在这个领域中&#xff0c;机械结构是机器人设计中至关重要的一部分&#xff0c;它决定了机器人的形态、运动方式和工作效率。对于青少年机器人爱好者来说&#xff0c;了解机械结构的基础知识&#xff0c;掌…

idea篇2:常用插件

1、汉化插件 下载完成后点击Restart IDE 如果你这边插件搜不出来&#xff0c;还可以去官网插件地址下载 打开 https://plugins.jetbrains.com/ 然后点击Instatll To IDE 弹出Success的时候我们切换回IDEA,然后点击ok 如果你这种方式还是无法成功 那么还可以点击插件商城的Ve…

半年面试数百场,我总结出了这份 10w 字 Java 面试复盘笔记

Java 面试 Java 作为编程语言中的 NO.1,选择入行做 IT 做编程开发的人&#xff0c;基本都把它作为首选语言,进大厂拿高薪也是大多数小伙伴们的梦想。以前 Java 岗位人才的空缺&#xff0c;而需求量又大&#xff0c;所以这种人才供不应求的现状&#xff0c;就是 Java 工程师的薪…

【javascript】导航栏

要实现这样的效果主要有两点。第一&#xff0c;当鼠标经过主导航栏里面的内容就会被放大&#xff0c;鼠标离开后就会恢复原来的样子&#xff1b;第二&#xff0c;当鼠标经过主导航时对应的副导航的内容就会呈现。 <!DOCTYPE html> <html lang"en"> <…