初阶C语言-指针

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;
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring】“请求“ 之传递 JSON 数据

文章目录 JSON 概念JSON 语法JSON 的语法JSON 的两种结构 JSON 字符串和 Java 对象互转JSON 优点传递 JSON 对象 JSON 概念 JSON&#xff1a;JavaScript Object Notation【JavaScript 对象表示法】 JSON 就是一种数据格式&#xff0c;有自己的格式和语法&#xff0c;使用文本…

Vue组件库Element-ui

Vue组件库Element-ui Element是一套为开发者、设计师和产品经理准备的基于Vue2.0的桌面端组件库。Element - 网站快速成型工具 安装element-ui npm install element-ui # element-ui版本&#xff08;可以指定版本号引入ElementUI组件库&#xff0c;在main.js中添加内容得到&…

IDEA的lombok插件不生效了?!!

记录一下&#xff0c;防止找不到解决方案&#xff0c;已经遇到好几次了 前面啰嗦的多&#xff0c;可以直接跳到末尾的解决方法&#xff0c;点击一下 问题现场情况 排查过程 确认引入的依赖正常 —》&#x1f197; idea 是否安装了lombok插件 --》&#x1f197; 貌似没有问题…

保姆级手把手使用YOLOv11训练自己数据集(含源代码、网络结构、模型检测和转换 、数据集查找、模型训练)

文章目录 前言项目地址项目内容&#xff1a;网络模型结构性能测试任务描述任务内容 项目运行模型训练 前言 本教程内含YOLOv11网络结构图训练教程推理教程数据集获取等有关的内容~ 项目地址 YOLO11是Ultralytics YOLO系列实时目标检测器的最新迭代版本&#xff0c;它以尖端的…

北交大研究突破:塑料光纤赋能低成本无摄像头AR/VR眼动追踪技术

北交大研究&#xff1a;探索无摄像头低成本AR/VR眼动追踪新路径 在AR/VR技术领域&#xff0c;眼动追踪作为一项关键技术&#xff0c;对于提升用户体验、优化渲染效率具有重要意义。然而&#xff0c;传统的眼动追踪方案多依赖于高成本的摄像头&#xff0c;这不仅增加了设备的制造…

Python 工具库每日推荐 【Pandas】

文章目录 引言Python数据处理库的重要性今日推荐:Pandas工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:销售数据分析案例分析高级特性数据合并和连接时间序列处理数据透视表扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 TypeScrip…

市面上8款AI论文大纲一键生成文献的软件推荐

在当前的学术研究和写作领域&#xff0c;AI论文大纲自动生成软件已经成为提高写作效率和质量的重要工具。这些工具不仅能够帮助研究人员快速生成论文草稿&#xff0c;还能进行内容优化、查重和排版等操作。本文将分享市面上8款AI论文大纲一键生成文献的软件&#xff0c;并特别推…

一文了解构建工具——Maven与Gradle的区别

目录 一、Maven和Gradle是什么&#xff1f; 构建工具介绍 Maven介绍 Gradle介绍 二、使用时的区别&#xff1a; 1、新建项目 Maven&#xff1a; Gradle&#xff1a; 2、配置项目 Maven&#xff1a; Gradle&#xff1a; 3、构建项目——生成项目的jar包 Gradle&…

用小学生可以理解的语言讲一下什么是大模型

好的&#xff0c;用小学生的语言来说&#xff0c;大模型就像是一个超级聪明的机器人老师&#xff0c;它懂得很多东西&#xff0c;可以帮助我们做很多事情。 1. **懂得很多**&#xff1a;大模型知道很多知识&#xff0c;就像一个巨大的图书馆&#xff0c;里面有很多书&#xff0…

IDEA 2024.3 预览:把开发者感动到哭了

幸运的人&#xff0c; 一生都被童年治愈&#xff1b; 不幸的人&#xff0c; 一生都在治愈童年 只有勇敢的人 和有钱的人才能先享受世界 缘分就是我还不知道 会见到你就误打误撞般 遇见了你 最近 IDEA 又发布了最新的 2024.3 的预览版本 EAP&#xff0c;把开发者的心激动的…

今日指数-day08实战完整代码

今日指数-day08 1. 个股最新分时行情数据 1.1 个股最新分时行情功能说明 1&#xff09;个股最新分时行情功能原型 2&#xff09;个股最新分时行情数据接口分析 功能描述&#xff1a;获取个股最新分时行情数据&#xff0c;主要包含&#xff1a;开盘价、前收盘价、最新价、最…

AI周报(9.29-10.5)

AI应用-Elayne公司临终规划和自动化遗产结算 创业公司Elayne成立于2023年&#xff0c;由Adria Ferrier和Jake Grafenstein共同创立&#xff0c;Adria Ferrier担任CEO&#xff0c;总部位于科罗拉多州丹佛市。 Elayne公司专注于遗产规划和结算领域&#xff0c;通过人工智能技术…

【Diffusion分割】CTS:基于一致性的医学图像分割模型

CTS: A Consistency-Based Medical Image Segmentation Model 摘要&#xff1a; 在医学图像分割任务中&#xff0c;扩散模型已显示出巨大的潜力。然而&#xff0c;主流的扩散模型存在采样次数多、预测结果慢等缺点。最近&#xff0c;作为独立生成网络的一致性模型解决了这一问…

【C++】STL——list的模拟实现

目录 前言list介绍list的模拟实现总体结构节点类迭代器类链表类 默认成员函数构造函数拷贝构造赋值重载析构函数 迭代器实现双向迭代器迭代器的其他功能用多参数模板完成最终的迭代器类 list的容量相关和数据访问empty()和size()front()和back() list的修改操作任意位置插入和删…

数据结构 ——— C语言实现无哨兵位单向不循环链表

目录 前言 动态顺序表的缺陷 单链表的概念 单链表中节点的结构 单链表逻辑结构示意图​编辑 实现单链表前的准备工作 实现单链表 1. 定义节点的指针 2. 创建节点 3. 打印单链表中的所有数据 4. 在单链表头部插入数据 5. 在单链表尾部插入数据 6. 在单链表头部删除数…

脏读、不可重复读、幻读的解决方法

上一篇博客提到了脏读、不可重复读、幻读的含义&#xff0c;也知道了是因为什么情况导致出现的这些问题&#xff0c;这篇博客就带大家一起来了解一下他们的解决办法~ 脏读&#xff1a;脏读出现的原因主要是因为一个事务读取了另外一个事务未提交的数据&#xff0c;就可能出现脏…

掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系

在日常开发中&#xff0c;我们常常需要对复杂的 SQL 进行数据血缘分析。 本文重点讨论在具有 * 列的嵌套子查询中建立表和列之间正确关系的挑战。使用 Teradata SQL 代码示例来说明该过程。 本文聚焦于一个别名为 SUBSCRIBER_ 的子查询及其派生的列&#xff0c;这些列在外层查…

无需VPN!大厂力作:免费AI对口型神器登场,让你的视频制作更简单!

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 &#xff08;偶尔会因为推荐工具&#xff…

《深度学习》OpenCV 图像拼接 原理、参数解析、案例实现

目录 一、图像拼接 1、直接看案例 图1与图2展示&#xff1a; 合并完结果&#xff1a; 2、什么是图像拼接 3、图像拼接步骤 1&#xff09;加载图像 2&#xff09;特征点检测与描述 3&#xff09;特征点匹配 4&#xff09;图像配准 5&#xff09;图像变换和拼接 6&am…

实验3 选择结构

1、计算分段函数的值 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <math.h> int main() {double x,y0;scanf("%lf",&x);if(x<0){printf("error!\n");return 0;}if(0<x&&x<1){ylog10(x);}else if(1<…