初阶C语言-指针

news2024/11/27 4:37:14

1.指针是什么?

理解指针的两个要点:

1.指针是内存中一个最小单元的编号,也就是地址

2.口头语中说的指针,通常是指指针变量,是用来存放内存地址的变量

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

        用图理解如下:

        内存的最小单元是一字节,对每一字节去编号对应的就是指针(地址)。 

#include <stdio.h>

int main()
{
	int a = 5;//是向内存中的栈空间申请4个字节的空间,这4个字节用来存放5这个数值
	int* pa = &a;//pa存的是a的首地址(第一个字节的地址)

	return 0;
}

        这里指针变量pa存放了变量a的首地址0x00112233(假设是这个地址)。指针变量是用来存放地址的变量。(存放在指针中的值都会被当成地址处理)。

        所以这里的问题是:

一个小单元到底是多大?(1个字节)

如何编址?

         经过仔细计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

        对于32位机器,假设有32根线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111

        这样就能找到我们的内存单元。一共2^32个地址,对应这么多个字节,所以2^32字节的空间是4GB空间。同理,如果是64位机器,那就是2^64字节空间。

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

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

         总结:指针变量是用来存放地址的,地址是唯一标识一个内存单元的;指针变量的大小在32位平台上是4个字节,在64位平台上是8个字节。

         注意:语法上:int* p和int *p都是可以的。int *p, *q;当这样连续定义好几个指针时,需要这样写,*q的*是不能省略的int* 也是,但最好分开定义,一行定义一个指针变量,分开初始化。

2.指针和指针类型

        目前我们对指针的应用大多停留在取地址&解引用*->

2.1指针类型 

        既然这些指针类型的大小都是4(x86)或者8(x64平台),那为什么不用一个通用性指针ptr_t p来代替这么多些个指针类型呢?C语言没有这样设计,是因为不同类型的指针是有区别的->

2.2指针的解引用操作

#include <stdio.h>

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

	return 0;
}

        我们按F11调试看看这段代码在内存中发生了什么-> 

        我们关闭监视窗口打开内存,并显示4列->

        在地址那块我们能输入&a,就能出现对应的a的地址。我们能看到a初始化完,在内存中是这样存储的,我们再看*pa = 0会发生啥->

        这个*pa = 0操作把内存中的四字节空间全部变成了0。 

#include <stdio.h>

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

	return 0;
}

        char* 和int* 的大小都是同样大的,都能存放a的地址,那这样会发生什么呢->

        我们发现这次操作只改了这四个字节的一个字节。我们这两段代码唯一不同的地方就是一个是int*,一个是char*,在解引用的时候,一个是访问了四个字节,另一个是访问了一个字节。因此,指针类型是有意义的,指针类型决定了指针进行解引用操作的时候,访问几个字节。一个char*的指针在解引用的时候访问了一个字节。一个int*的指针在解引用的时候访问了四个字节。因此,如果我们想从某个地址向后访问一个字节就可以解引用char*,访问四个字节可以解引用int*。当我们想从某个地址向后访问两个字节的时候可以使用short*类型的指针。

        虽然有个小警告:&a的类型是int*的,=两边类型不一致,编译器弹出了警告,不想看到这个警告可以把&a强制类型转换为char*。 

#include <stdio.h>

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

	return 0;
}

         很明显,如果是一个int*的指针+1跳了四个字节,而char*的指针+1跳了一个字节。因为指针类型的不一样,导致+1出现不同的结果,int*的指针,指向的变量是一个int类型的变量,它+1就跳过四个字节的空间,char*的指针,指向的是一个char类型的变量,它+1就跳过了一个字节的空间。

        所以,指针类型是有意义的。指针类型决定了指针+1/-1跳过了几个字节。

char*的指针+1,跳过1个字节

short*的指针+1,跳过2个字节

int*的指针+1,跳过4个字节

double*的指针+1,跳过8个字节

         总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。

         这样访问起来就比较舒服了,不管是解引用一次还是跳过一次,都是一个整形一个整形访问的。假设也有一个short*的指针也找到了首元素地址那个位置,这样访问两个字节和+1跳过两个字节就比较别扭了,就不合适,循环十次才访问了五个整形空间。拿一个int*的指针,循环十次,就能访问完。如果你就是想一个字节一个字节访问,那也可以拿char*指向那个地址,一字节一字节的访问也是ok的。想以什么方式去访问,就应该拿什么样的指针去访问。

         ->int*一次修改四字节空间。

        ->char*一次修改一字节空间。

        在内存中,只要我们得到一个地址,我们就能利用指针对其进行向前后者向后的访问。

3.野指针

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

eg:

#include <stdio.h>

int main()
{
	int* p = (int*)0x11223344;
	*p;

	return 0;
}

        这个0x11223344是一个随便捏造的地址,然后访问这段未知空间,这是不合适的,是很危险的!就像有人给你打骚扰电话一样。 

3.1野指针成因

1.指针没有初始化

#include <stdio.h>

int main()
{
	int* p;
	*p = 20;

	return 0;
}

        在vs2022下直接报错。 

        因为局部变量p未初始化,默认为随机值。 

2.指针越界访问

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

        上述*p = i;p++可以写成*p++,因为优先级的原因,++先和p结合,所以是*(p++),假设p的值为0x1122344,*对后面(p++)这个表达式解引用,这个表达式的值是0x11223344,但先进行计算的是p++,此时p变成了0x11223348。也可以简单理解为先使用p再++。

3.指针指向的空间释放

#include <stdio.h>

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

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

	return 0;
}

         虽然报的是警告,但这里还是挺致命的,当test函数结束的时候,局部变量a就要被销毁,空间就会被回收,这个时候,访问一个被回收的空间就是野指针,为啥说它致命呢?当这个函数结束时,这块内存空间会被回收,但如果这时,这块空间又被申请了,你的p指针还指向那块空间,你以为还是a。有点像你去租房子,上一间租客还留着钥匙,万一发疯当自己房子,拿钥匙开门了,那不就完蛋了吗。

        动态内存开辟的时候会仔细讲解,现在只是提一下。

3.2如何规避野指针

1.指针初始化

2.小心指针越界

3.指针指向的空间释放,及时置NULL

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

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

         如果明确知道指针应该指向哪里,就指向正确的地址;如果不知道指针初始化什么值,为了安全初始化为空指针NULL(本质是0)。

         0作为地址时,用户程序是不能访问的!

#include <stdio.h>

int main()
{
	int* p = NULL;
	if (p != NULL)
	{
		//...
	}


	return 0;
}

        在使用指针之前可以检查指针的有效性。 至于为什么要增加一个宏定义NULL,是为了可读性,虽然在值上,二者是相等的并且int* p = 0;也没问题,但可读性没那么高,0可以表示的东西多多了。一但发现一个东西被赋值NULL那一定知道这个东西是个指针。

4.指针运算

1.指针+-整数

2.指针-指针

3.指针的关系运算

#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* p = arr;
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", *(p + i));
		//p指向的是数组首元素
		//p+i是数组中下标为i的元素的地址
		//p+i其实是跳过了i*sizeof(int)个字节
	}

	return 0;
}

        观察易得知: 

 arr == p;它两都是数组首元素的地址

arr+i == p+i

*(arr+i) == *(p+i) == arr[i]//都是数组第i个元素

*(arr+i) == arr[i]

*(i+arr) == i[arr]

        所以[]仅仅只是个操作符,他和+一样,支持操作数的交换律。同时也说明,arr[i]只是数组第i个元素的表示形式,编译器在处理的时候会转化为*(arr+i),可以简单理解为语法糖吧。 这就是数组第i个元素访问的本质——数组名是个地址,i是个偏移量。不信的话看看p[i];

         本质都是一个地址加一个偏移量就能得到一个新地址,对其解引用就能访问了。其实这里并不是教大家去学“茴”的四种写法,而是知道原来指针的偏移量和数组元素的访问是有关系的。其实还就是地址、偏移量、解引用等一系列操作。

        在指针的关系运算中->

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}
        实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免第二种写法,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

         第二种写法,出循环的时候,指针已经指向了第一个元素之前的那个内存位置,并且是做了比较的。可能是因为(推测):当向内存申请40字节空间时,其实操作系统会多给你一块空间用来存你申请了多少字节空间。此时来说向后越界比向前越界相对安全。

#include <stdio.h>

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

	return 0;
}

         指针-指针的前提是:两个指针指向同一块区域,指针类型也是相同的,得到的是指针和指针之间的元素个数。

         小地址-大地址为负的元素个数。

        之前在函数那章节写过两种模拟实现strlen,一种是计数器;另一种是递归初阶C语言-函数-CSDN博客,现在利用这个特性可以模拟实现一下strlen->

#include <stdio.h>

size_t My_strlen(char* str)
{
	char* start = str;//记录初始位置
	while (*str != '\0')
		str++;
	return str - start;
}

int main()
{
	char arr[] = "abcdef";
	size_t len = My_strlen(arr);
	printf("%zd\n", len);

	return 0;
}

         '\0'也是一个字符,是八进制形式(\ddd),它的ascii码值是0。

5.指针与数组

指针就是指针,指针变量就是一个变量,存放地址,指针变量的大小是4/8字节

数组就是数组,可以存放一组数,数组的大小取决于元素个类型与个数

联系就是:

数组的数组名是数组首元素地址,地址是可以存放在指针变量里面中。

#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0]);

	return 0;
}

         绝大多数情况下,数组名表示数组首元素的地址,有两个例外->

1.sizeof 数组名,数组名单独放在sizeof内部,计算数组的大小,单位是字节。

2.&数组名,这里的数组名表示整个数组,取出的是数组的地址,数组的地址和数组首元素的地址值是一样的,但类型和意义是不一样的。类似char* p1 = 0x1122,int* p2 = 0x1122。

#include <stdio.h>

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

	return 0;
}

         arr+1跳过的是数组一个元素的地址,&arr+1跳过的是arr一个数组的地址->

        这里也能说明二者的指针类型是不一样的。 

6.二级指针

        指针变量也是变量,也有自己的地址,于是我们能用指针变量去存指针变量的地址,这就是二级指针。 

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int** pa = &p;
	

	return 0;
}

        p是指针变量,一级指针变量;pa也是指针变量,二级指针变量,还可以有三级,四级...指针变量。

7.指针数组

         指针数组是指针还是数组呢?答案是数组,类比字符数组,整型数组,字符和整型是类型,重点是数组。

字符数组——存放字符的数组        char arr[7];

整型数组——存放整型的数组        int arr[8];

->

指针数组——存放指针的数组        char* arr[7];or int* arr[7];等等

#include <stdio.h>

int main()
{
	//用指针数组来模拟二维数组
	int arr1[] = { 1, 2, 3, 4, 5 };
	int arr2[] = { 2, 3, 4, 5, 6 };
	int arr3[] = { 3, 4, 5, 6, 7 };
	int* arr[] = { arr1, arr2, arr3 };
	for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 5; ++j)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

         这只是模拟二维数组,并不是真的二维数组,因为二维数组的内存分布是连续的,这个内存分布是不连续的。

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

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

相关文章

详解单流Transformer架构中图像/文本/表格/视频/音频经过transformer提取模态特征并生成令牌特征以及CLS到底是什么作用?

系列论文研读目录 文章目录 系列论文研读目录论文中经常出现的如下图中所示结果&#xff0c;其中到底做了什么&#xff1f;1、一张图片 经过self attention 和Feed forward 生成的是什么&#xff1f;具体流程如下&#xff1a;结果&#xff1a; 举个具体计算的例子假设条件1. 自…

【hot100-java】【最长递增子序列】

dp篇 一眼单调栈 之前写的python题解 佬解 单调栈二分查找 我靠&#xff0c;大师我悟了 class Solution {public int lengthOfLIS(int[] nums) {if (nums.length<1){return 1;}List<Integer>stacknew ArrayList<>();stack.add(nums[0]);for(int i1;i<num…

吊打ChatGPT4o!大学生如何用上原版O1辅助论文写作(附论文教程)

目录 1、用ChatGPT生成论文选题2、用ChatGPT生成论文框架3、用ChatGPT进行文献整理4、用ChatGPT进行论文润色5、用ChatGPT进行问题求解6、用ChatGPT进行思路创新7、用ChatGPT进行论文翻译8、如何直接使用ChatGPT4o、o1、OpenAI Canvas 9、OpenAI Canvas增强了啥&#xff1f;10、…

Python中字符串的基本操作

文章目录 1、字符串序号2、字符串切片3、字符串切割4、字符串替换5、字符串删除6、字符串判断7、字符串拼接8、其他操作 字符串是 python 中常用的数据类型&#xff0c;python中可以使用单引号或双引号来创建字符串&#xff0c;python 中没有字符类型&#xff0c;字符在 python…

算法笔记(十二)——BFS 解决 FloodFill(洪水灌溉)

文章目录 图像渲染岛屿数量岛屿的最大面积被围绕的区域 FloodFill&#xff08;洪水灌溉&#xff09; 颜色填充 想Windows画图板中的油漆点一下可以把一个联通的块儿全部染色 本质就是找一块区域里性质相同的联通块 图像渲染 题目&#xff1a;图像渲染 思路 BFS一层一层搜索&…

4.资源《Arduino UNO R3 proteus 电机PID参数整定工程文件(含驱动代码)》说明。

资源链接&#xff1a; Arduino UNO R3 proteus 电机PID参数整定工程文件&#xff08;含驱动代码&#xff09; 1.文件明细&#xff1a; 2.文件内容说明 包含&#xff1a;proteus工程&#xff0c;内含设计图和工程代码。 3.内容展示 4.简述 工程功能可以看这个视频 PID仿真调…

通过 Groovy 实现业务逻辑的动态变更

Groovy 1、需求的提出2、为什么是Groovy3、设计参考1_引入Maven依赖2_GroovyEngineUtils工具类3_GroovyScriptVar类4_脚本规则表设计5_对应的实体类6_数据库访问层7_GroovyExecService通用接口 4、测试5、其他的注意事项6、总结 1、需求的提出 在我们日常的开发过程中&#xf…

一、图解C#教程

一、堆和栈 程序运行时&#xff0c;数据存储在内存中。 使用堆和栈来存储数据 1、栈 栈是一个内存数组&#xff0c;先进后出原则。 可以存储&#xff1a;某些类型变量的值&#xff1b;程序当前执行环境&#xff1b;传递给方法的参数&#xff1b; 入栈&#xff1a;把数据放…

【超级详细解释】力扣每日一题 134.加油站 48. 旋转图像

134.加油站 力扣 这是一个很好的问题。这个思路其实基于一种贪心策略。我们从整个路径的油量变化来理解它&#xff0c;结合一个直观的“最低点法则”&#xff0c;来确保找到正确的起点。 问题的核心&#xff1a;油量差值的累积 对于每个加油站&#xff0c;我们有两个数组&…

精选算法入门——day2

精选算法入门——day2 题目一题干解题思路一解题思路二解题思路三思路三代码 题目二题干解题思路代码 题目三题干解题思路一代码解题思路二代码解题思路三代码 题目四题干解题思路代码 题目一 题干 数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。…

提高顾客满意度,餐饮业如何开展客户调研?

餐饮行业需明确调研目的&#xff0c;选择合适工具&#xff0c;设计问卷&#xff0c;收集并分析数据&#xff0c;持续追踪优化。通过客户调研&#xff0c;提升服务质量、顾客满意度和竞争力&#xff0c;利用ZohoSurvey等工具实现高效调研。 一、明确调研目的 进行客户调研前&am…

ssm基于JAVA的酒店管理系统的设计与实现

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 第1章 绪论 1 1.1 选题动因 1 1.2 目的和意义 1 1.3 论文结构安排 2 第2章 开发环境与技术 3 2.1 S…

【AI知识点】置信区间(Confidence Interval)

置信区间&#xff08;Confidence Interval, CI&#xff09; 是统计学中用于估计总体参数的范围。它给出了一个区间&#xff0c;并且这个区间包含总体参数的概率等于某个指定的置信水平&#xff08;通常是 90%、95% 或 99%&#xff09;。与点估计不同&#xff0c;置信区间通过区…

i春秋云境靶场之CVE-2022-26965

1.环境搭建 提示我们后台存在rce,也就是命令执行漏洞 2.访问环境 cm - cmshttp://eci-2zeh0yf0ohu88wr26unq.cloudeci1.ichunqiu.com/ 我们可看到admin,我们点击&#xff0c;发现是一个登录页面&#xff0c;我们输入弱口令admin,登录成功 3.文件上传 我们在选项——选择主题…

C++:图的遍历

一、简介 图的遍历通常有深度优先遍历和广度优先遍历两种方式&#xff0c;这两种遍历次序对无向图和有向图都使用。 本文分别介绍基于邻接矩阵和邻接表的图的深度优先遍历和广度优先遍历&#xff0c;对于邻接矩阵和邻接表不熟悉的可翻阅&#xff1a;C&#xff1a;图的存储结构及…

dockerpull

20241006更新&#xff0c;亲测可用。 注意&#xff1a;这个方法随时可能会失效。 编辑配置文件&#xff0c;修改镜像源&#xff1a; vi /etc/docker/daemon.json {"registry-mirrors": ["https://do.nark.eu.org","https://dc.j8.work","…

TypeScript 第三部分 扩展

1. 声明文件 主要作用&#xff1a; 类型声明&#xff1a;为库或模块提供类型信息。全局声明&#xff1a;为全局作用域中的类型和变量提供声明。类型兼容性&#xff1a;确保第三方库或自定义代码的类型正确性。代码提示与检查&#xff1a;在开发环境中提供更好的代码提示和类型…

Sollong手机——一站式Web3生态解决方案

从定义上讲&#xff0c;Web3公司也属于互联网公司&#xff0c;不过与传统互联网公司相比&#xff0c;他们有一个很明显的特征&#xff1a;他们不断尝试做去中心化的事&#xff0c;一步步将数据和金融的控制权从美联储&#xff08;央行和金融机构&#xff09;、苹果&#xff08;…

2024/10/6周报

文章目录 摘要Abstract广西的一些污水处理厂工艺解析1. A/O工艺&#xff08;厌氧-缺氧-好氧工艺&#xff09;2. 氧化沟工艺3. MBR工艺&#xff08;膜生物反应器&#xff09;4. SBR工艺&#xff08;序批式活性污泥法&#xff09;5. 生物接触氧化法 其它补充一体化改良氧化沟工艺…

Linux的基础指令(下)

压缩包 这里不为打包和压缩做仔细的区分&#xff1b; 打包&#xff1a; 文件合并&#xff1b; 主要目的是在文件传输&#xff0c;移动时&#xff0c;能有效减少文件的缺失&#xff1b; 压缩&#xff1a;为了减小文件体积&#xff0c;内存&#xff1b; 主要目的是减小使用体…