【C语言】指针初阶

news2024/11/24 19:31:18

 ✨个人主页: Anmia.
🎉所属专栏: C Language

🎃操作环境: Visual Studio 2019 版本


1.指针是什么?

指针理解的2个要点:

  1. 指针是内存中一个最小单元的编号,也就是地址
  2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

 总结:指针就是地址,口语中说的指针通常指的是指针变量

内存

  

通过上面我们可以了解到指针就是地址。那么每个地址在内存中的大小是1byte,每个地址都有属于自己独一无二的编号。如上图:0xFFFFFFFF...(16进制)。

  • 我们可以通俗的理解为,内存是一个大酒店,每个地址(指针)都是酒店中的一个小房间,它们都有自己专属的房间号。

指针变量

我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	return 0;
}

int a=10;//在内存中开辟一块空间

int* p=&a;//这里我们对变量a,取出它的地址,可以使用&操作符。

a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。

    

  • 首先,定义的局部变量在栈区,先定义,后分配(栈:先进后出的数据结构)
  • 先定义的先入栈(在栈内不分配空间),因此a在栈底p在栈顶。
  • 出栈的时候地址先分配给p,后分配给a,因而a的地址比p的大,如下图(16进制下)

  • 很明显,a的地址大于p的地址。

总结:
指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那这里的问题是:

  • 一个小的单元到底是多大?(1个字节)
  • 如何编址?

经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的
对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
那么32根地址线产生的地址就会是:

这里就有2的32次方个地址。
每个地址标识一个字节,那我们就可以给

(2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

上面的int a = 10;为例整型在32位架构下是4个字节,会在堆区中申请4个字节的空间。

  • 同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算哈。
  • 对于一个64位的机器,如果给出64根地址线,它可以编址的空间大小是2的64次方。由于每根地址线可以表示2个不同的状态(0或1),所以64根地址线可以表示2的64次方个不同的地址。这意味着该机器可以编址的空间大小为18,446,744,073,709,551,616个地址,或者约为18.4亿亿个地址。这是一个非常大的地址空间,远远超过目前大多数计算机系统所需的空间。

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


那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。

总结:

指针是用来存放地址的,地址是唯一标示一块地址空间的。
指针的大小在32位平台是4个字节,在64位平台是8个字节。


2.指针和指针类型 

这里我们在讨论一下:指针的类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
准确的说:有的。

int num = 10;
p = &num;

要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?我们给指针变量相应的类型。

char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;

这里可以看到,指针的定义方式是: type + * 。

char* 类型的指针是为了存放 char 类型变量的地址。
short* 类型的指针是为了存放 short 类型变量的地址。
int* 类型的指针是为了存放 int 类型变量的地址。

那指针类型的意义是什么?

如下代码,如果分别用int和char型对它进行操作,会有什么区别? 

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	return 0;
}

int* p对其操作

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	int* p = &a;
	*p = 0;
	return 0;
}

int指针对int变量进行操作

char* p对其操作

#include <stdio.h>
int main()
{
	int a = 0x11223344;
	char* p = &a;
	*p = 0;
	return 0;
}

char指针对int变量进行操作

通过上面两个视频,不难看出int型的指针可以一次操作4个字节,而char型的指针只能一次操作1个字节。所以指针类型必须对应指向的变量的数据类型。


3.指针+-整数 

用指针加减一个整数会怎么样

#include <stdio.h>
int main()
{
	int n = 10;
	char* pc = (char*)&n;
	int* pi = &n;
	printf("int型n的地址:%p\n", &n);
	printf("\n");
	printf("char指针指向int型变量的地址:%p\n", pc);
	printf("char指针指向int型变量的地址 +1 后:%p\n", pc + 1);
	printf("\n");
	printf("int指针指向int型变量的地址:%p\n", pi);
	printf("int指针指向int型变量的地址 +1 后:%p\n", pi + 1);
	return 0;
}

通过运行结果可以看出它们的首地址是一致的,但char指针对int变量操作的内存大小不一样,char指针操作int 变量一个字节的空间大小,而int 对int变量操作出4个字节的空间大小。

  • 总结:指针的类型决定了指针向前或者向后走一步有多大(距离),因此我们用指针+-整数时,我们应该要保证类型的对应。

4.指针的解引用

#include <stdio.h>
int main()
{
	int n = 0x11223344;
	char* pc = (char*)&n;
	int* pi = &n;
	*pc = 0; 
	*pi = 0; 
	return 0;
}

上面用*对指针解引用操作,

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


5.野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),因此野指针是我们编程需要避免的。

为什么会出现野指针?

1.指针未初始化

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

上面指针未初始化(没有任何指向的地方),这种代码是不好的。当我们需要用一个指针时,即使还没确定它要指向的地方时,可以用NULL对其赋值(初始化)。如下

int* p = NULL;

2.指针越界访问

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

可以看到运行发生了异常,因为指针越界访问,因为指针只能对指向的内存空间进行操作,不可以操作自己没指向的区域,否则会发生异常!如图

因为数组只有10个元素,*(p++)=i 每次对一个数组元素进行依次赋值操作,但问题就出现在for循环体的条件表达式i<=11,指针访问完数组继续访问不该访问的地方就会出现问题。

3.指针指向的空间释放

这里涉及动态内存开辟的时候讲解。


那如何避免野指针的出现呢?

总结为以下主要方法:

1. 指针初始化
2. 小心指针越界
3. 指针指向空间释放即使置NULL
4. 避免返回局部变量的地址
5. 指针使用之前检查有效性

前三种前面提及就不再讲解。

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

#include <stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

 这个代码是典型的错误案例。

上面代码的意思是,在main函数中我们用指针p,来接收test函数的返回值。但我们用return来返回test函数中的变量地址,我们都知道局部变量在栈区,只要出了test函数就释放了,地址也就不存在了,因此指针p实际是不可能接收到地址值。

但为什么在VS中运行不会出现问题呢?

你目前只需要知道VS暂时帮你保留了值,为了避免代码出现异常。但只是注意只是“暂时”肯定不可能一直用下去吧?以后大工程函数之间互相调用,问题就大了。因此,我们要养好代码规范,不要返回局部变量的地址。


5. 指针使用之前检查有效性 

#include <stdio.h>
int main()
{
	int* p = NULL;
	int a = 10;
	p = &a;
	if (p != NULL)
	{
		*p = 20;
	}
	return 0;
}

 上面代码用了if(p!=NULL)来验证指针是否有效也是其中一种方法,当然还有很多方法,需要根据实际情况使用。


6.指针运算

指针+-整数

#define N 5
float values[N];
float* vp;
#include <stdio.h>
int main()
{
	for (vp = &values[0]; vp < &values[N];)
	{
		*vp++ = 0;
	}
	
	return 0;
}
上面代码的是利用for来依次访问数组values的地址,*vp++实际是*(vp++),但因为是后置++,所以先解引用赋值,后地址++,具体代码实现如下图。


指针-指针

 下面我们来模拟实现一下strlen函数

#include <stdio.h>
int my_strlen(char* s)
{
	char* p = s;
	while (*p != '\0')
		p++;
	return p - s;
}
int main()
{
	char str[10] = "Hello";
	int len=my_strlen(&str);
	printf("%d\n", len);
	return 0;
}

上面代码先写了一个my_strlen函数并传入一个字符串变量的地址,在my_strlen函数中,创建一个指向char类型的指针,把它指向传入的字符串地址。我们都知道char类型的指针一次只能操作一个字节的空间,一个字符刚好是一个字节的空间。因为while循环的意思是指针p依次访问字符串,如果*p=='\0'字符串结束字符,说明字符个数统计结束。最后返回p-s的值,p的值为字符串最后一个字母的地址,s的值是第一个字母的地址。

所以我们记住结论:

  • 指针-指针的前提:两个指针指向同一块区域,指针类型是相同的。
  • 指针-指针的差值的绝对值,是指针和指针之间的元素个数。 

当然上面的代码中while循环条件中的*p != '\0'可以改成*p,因为指针p在while循环中++最后会++到 '\0'这个位置,我们知道'\0'是转义字符"  \ddd类型 "因此它是八进制中的0,八进制中的0也是0,所以我们*p可以直接写成while循环的判断条件,当*p指向'\0'是相当于指向0,0为假,结束循环。

\ddd可参考

【C语言】初识C语言+进阶篇导读-CSDN博客中有讲解


指针的关系运算  

#define N 5
float values[N];
float* vp;
#include <stdio.h>
int main()
{
	for (vp = &values[N]; vp > &values[0];)
	{
		*--vp = 0;
	}
	return 0;
}

如上代码,vp开始指向values[5]的首地址,然后循环判断条件是vp>&values[0]的首地址时,vp先解引用赋值后地址 --。直到循环条件不成立后结束循环。

我们假设一个数组元素是一个格子,每个元素前面的表示首地址,vp依次每次指向数组每个元素的首地址,然后修改它们的值为0,运行过程如图。


那我把这个代码稍微改下,情况就截然不同了。

#define N 5
float values[N];
float* vp;
#include <stdio.h>
int main()
{
	for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
	{
		*vp = 0;
	}
	return 0;
}

这里vp开始指向values[4],当vp>=&values[0]则*vp=0,然后vp--,直到vp<&values[0]结束。

看似好像没有什么问题,而且感觉比上面的代码更好理解。

但这里有个反C语言标准的地方。

C语言标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

上面代码运行过程如下:

 当vp指向values[0]的地址时,vp>=&values[0]循环条件成立,*vp=0;执行,vp--。但此时vp指向的是values数组前一块地址空间,我们把它叫做values[-1]吧,C语言规定是不允许与指向第一个元素之前的那个内存位置的指针进行比较。所以这个写法是不推荐的,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证
它可行。

至于为什么它一定要这样,你可以简单的理解先,前面有着重要的数据,不能进行指针比较即可。


7.指针和数组

我们似乎感觉指针和数组有点像,容易混淆。通过下面例子就可以分清楚它们的区别了。

  • 指针就是指针,指针变量就是一个变量,用来存放地址,指针变量的大小是4/8个字节
  • 数组就是数组,用来存放一组数,数组的大小由数组元素的类型和个数决定的。

数组名表示数组首元素的地址

#include <stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
	printf("arr = %p\n", arr);
	printf("&arr[0] = %p\n", &arr[0]);
	return 0;
}

 

 可以论证数组名就是数组首元素的地址,但也有2个例外

  1. sizeof(数组名),数组名单独放在sizeof内部,数组名表示是整个数组,计算的是整个数组的大小,单位是字节。
  2. &数组名,数组名表示整个数组,取出的是数组的地址,数组的地址和首元素的地址是一样的,但类型和意义不同。

数组名的两个例外意义

1. sizeof(数组名)

int arr[10] = {0};
printf("%d\n",sizeof(arr));

可见此时数组名的意思因为sizeof的影响改变成,数组的大小。 

2. &数组名

我们来看看下面这段代码

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr + 1);//跳过4个字节

	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0] + 1);//跳过4个字节

	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);//跳过40个字节

	return 0;
}

可见,&数组名,数组名此时 表示整个数组,取出的是数组的地址,虽然值和arr首元素一样但类型和意义完全不同。可以看下图

 因为类型和意义的不同,现阶段可以简单的理解为,arr是指向数组的首元素地址的,&arr[0]同理,但&arr是指向整个arr数组的,虽然他还是以首元素地址为值,但通过指针+1就可以看出区别所在了。

既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

#include <stdio.h>
int main()
{
	int i = 0;
	int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("&arr[%d] = %p  <====> p+%d = %p\n", i, &arr[i], i, p + i);
	}
	return 0;
}

所以 p+i 其实计算的是数组 arr 下标为i的地址。
那我们就可以直接通过指针来访问数组。

#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}


8.二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里
这就是 二级指针 。

对于二级指针的运算有:
*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .

int b = 20;
*ppa = &b;//等价于 pa = &b;

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30

9.指针数组

指针数组是指针还是数组?
答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。

int arr1[5];
char arr2[6];

那指针数组是怎样的?

int* arr3[5];//是什么?

arr3是一个数组,有五个元素,每个元素是一个整形指针。

所以指针数组就是 存放指针的数组。


本文简单入门指针的基础,后续将更新指针进阶内容。

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

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

相关文章

Vue elemen ui 移除上次校验与部分清除上次校验

场景&#xff1a; 可以切换类型&#xff0c;下面的输入框参数也会随着改变。 如果不清除上次的校验就会出现&#xff0c;之前的大陆企业的校验还会出现在香港企业的校验中 方法&#xff1a; watch:{ruleForm.paymentSubjectType:{ 通过监听表单的类型来调用 clearValidate方…

计算机网络之网络层(全)

网络层的功能 互联网在网络层的设计思路是&#xff0c;向上只提供简单灵活的、无连接的、尽最大努力交付的数据报服务。 路由器在能够开始向输出链路传输分组的第一位之前&#xff0c;必须先接收到整个分组&#xff0c;这种机制称为&#xff1a;存储转发机制 异构网络互连 用…

transformers-Causal lanuage modeling

https://huggingface.co/docs/transformers/main/en/tasks/language_modelinghttps://huggingface.co/docs/transformers/main/en/tasks/language_modelingcausal lanuage model常用于文本生成。预测token系列中的下一个toekn&#xff0c;并且model只能关注左侧的token&#xf…

超声波清洗清洁力强怎么选、适合家用超声波清洗机推荐

因为各种原因很多导致很多小朋友从小就开始近视&#xff0c;佩戴眼镜&#xff0c;眼镜只要是戴上了就很难再摘下来&#xff0c;也有很多朋友从小到大都不知道清洗眼镜的重要性&#xff0c;眼镜长时间不清洗的话上面的细菌堪比茅厕这么脏&#xff01;所以眼镜清洗千万别忽视了&a…

vue2导出数据生成xlsx文件

1.在utils文件夹新建tool.js tool.js文件 import XEUtils from xe-utilsexport function exportCsv(csv, title) {const t XEUtils.toDateString(Date.now(), yyyy-MM-dd) // 当前日期const filename ${t title}.xlsx // 拼接文件名const blob new Blob([csv]) //创建一…

在Linux上编译gdal3.1.2指南

作者:朱金灿 来源:clever101的专栏 为什么大多数人学不会人工智能编程?>>> 以Ubuntu 18编译gdal3.1.2为例,编译gdal3.1.2需要先编译proj库和geos库(可选)。我选择的proj库版本为proj-7.1.0,编译proj-7.1.0需要先编译tiff库和sqlite3。我选择的sqlite3的版本为…

玩转多个数据库,一个Itbuilder在线工具就搞定!

随着需要使用的数据库类型日渐繁多&#xff0c;开发运维等技术人员如何高效便捷的访问、操作和管理数据&#xff0c;成了一个难题。设计一个好的数据库&#xff0c;就像孩子从小打下的基础&#xff0c;很多项目的失败是由于缺乏适当的数据库设计。因此&#xff0c;选择正确的数…

版本控制系统-SVN

SVN Apache Subversion 通常被缩写成 SVN&#xff0c;是一个开放源代码的版本控制系统。 官网&#xff1a;https://subversion.apache.org 资料&#xff1a;https://svnbook.red-bean.com、https://www.runoob.com/svn/svn-tutorial.html 下载&#xff1a;https://sourceforg…

【LeetCode刷题-哈希】--217.存在重复元素

217.存在重复元素 class Solution {public boolean containsDuplicate(int[] nums) {Set<Integer> hashSet new HashSet<>();for(int i 0;i<nums.length;i){if(hashSet.contains(nums[i])){return true;}hashSet.add(nums[i]);}return false;} }

U盘格式化恢复怎么做?常用的3个方法分享!

“前段时间由于我的u盘中病毒了&#xff0c;我不得已把它格式化了&#xff0c;但是今天我在找一份重要的资料时才发现我的资料在u盘中被一起删除掉了&#xff0c;有什么方法可以帮我找回我u盘中的数据吗&#xff1f;” U盘可以为我们存储各种类型的文件&#xff0c;同时它也很便…

C++标准模板(STL)- 类型支持 (类型属性,is_volatile,is_trivial,is_const)

类型特性 类型特性定义一个编译时基于模板的结构&#xff0c;以查询或修改类型的属性。 试图特化定义于 <type_traits> 头文件的模板导致未定义行为&#xff0c;除了 std::common_type 可依照其所描述特化。 定义于<type_traits>头文件的模板可以用不完整类型实例…

SOLIDWORKS PDM缩短图纸从设计到发布时间

SOLIDWORKS线上协同设计、线上审核、版本管理、任务等大大缩短图纸从设计到发布时间。 在SOLIDWORKS PDM 中工作流程是整个系统的骨架和脉络&#xff0c;所有的文档都需要进入某一工作流程&#xff0c;所有的操作及权限&#xff0c;都依附于特定的工作流程。SOLIDWORKS PDM的工…

一文掌握Java Stream API

引言 Java Stream API 自 Java 8 引入以来&#xff0c;已成为处理集合数据的强大工具。它不仅提高了代码的可读性&#xff0c;还优化了性能&#xff0c;使得集合操作变得更加简洁和高效。本文将深入探讨如何利用 Stream API 的常用操作&#xff0c;帮助你更好地掌握这一强大的…

算法模板之队列解密 | 图文详解

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;数据结构、算法模板、汇编语言 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️模拟队列1.1 &#x1f514;用数组模拟实现队列1.1.1 &#x1f47…

解决远程桌面 这可能是由于CredSSP加密数据库修正问题

运行环境 : Windows Server 2012 R2 Standard 解决方式 策略组 gpedit.msc&#xff0c;注册表 regedit 等方式都尝试无效时&#xff0c;可尝试把下面这个勾勾去掉。

【小黑嵌入式系统第七课】PSoC® 5LP 开发套件(CY8CKIT-050B )——PSoC® 5LP主芯片、I/O系统、GPIO控制LED流水灯的实现

上一课&#xff1a; 【小黑嵌入式系统第六课】嵌入式系统软件设计基础——C语言简述、程序涉及规范、多任务程序设计、状态机建模(FSM)、模块化设计、事件触发、时间触发 文章目录 一、PSoC 5LP主芯片二、PSoC 5LP I/O系统(1) I/O系统特性(2) I/O系统怎样运作&#xff1f;1、I/…

【PC】神秘市场2023

神秘市场2023 我们有一个令人振奋的消息要告诉大家&#xff0c;神秘市场要开张了&#xff01; 据可靠情报&#xff0c;这次全新的神秘市场将返场稀有度高的道具。全新黑货箱也将在藏匿处出现&#xff0c;工坊也会推出全新工匠通行证。不仅如此&#xff0c;特殊制作中也能看到…

《YOLOv8-Pose关键点检测》专栏介绍 CSDN独家改进创新实战 专栏目录

YOLOv8-Pose关键点检测专栏介绍&#xff1a;http://t.csdnimg.cn/gRW1b ✨✨✨手把手教你从数据标记到生成适合Yolov8-pose的yolo数据集&#xff1b; &#x1f680;&#x1f680;&#x1f680;模型性能提升、pose模式部署能力&#xff1b; &#x1f349;&#x1f349;&#…

【Java】医院云HIS信息管理系统源码:实现检验、检查、心理CT、B超等医技报告查看

云HIS采用主流成熟技术&#xff0c;软件结构简洁、代码规范易阅读&#xff0c;SaaS 应用&#xff0c;全浏览器访问前后端分离&#xff0c;多服务协同&#xff0c;服务可拆分&#xff0c;功能易扩展&#xff1b;支持多样化灵活配置&#xff0c;提取大量公共参数&#xff0c;无需…

Debian或Ubuntu静态交叉编译arm和aarch64

Debian或Ubuntu静态交叉编译arm和aarch64 介绍术语ARM架构前置条件从源代码编译一个简单的C程序configure和make交叉编译关于静态链接和依赖关系使用 musl libc 实现与 configure 和 make 进行交叉编译 ARM 正在获得越来越多的关注&#xff0c;并且越来越受欢迎。直接在这些基于…