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

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

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

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

相关文章

双指针_有效三角形个数三数之和四数之和

有效三角形个数 思路&#xff1a; 我们可以通过暴力枚举&#xff0c;三重for循环来算但&#xff0c;时间复杂度过高。 有没有效率更高的算法呢&#xff1f; 我们知道如果两条较短的边小于最长的一条边&#xff0c;那么就可以构成三角形。 如果这个数组是升序的&#xff0c;两…

python交互式命令时如何清除

在交互模式中使用Python&#xff0c;如果要清屏&#xff0c;可以import os&#xff0c;通过os.system()来调用系统命令clear或者cls来实现清屏。 [python] view plain copy print? >>> import os >>> os.system(clear) 但是此时shell中的状态是&#xff1a;…

鼓组编写:SsdSample鼓映射 GM Map 自动保存 互换midi位置 风格模板 逻辑编辑器

SsdSample音源的键位映射 方便编写鼓的技巧 可以这样去设置键位关系的面板和钢琴卷帘窗的面板&#xff0c;方便去写鼓。 可以先按GM的midi标准去写鼓&#xff0c;然后比对下鼓的键位映射的关系&#xff0c;去调整鼓。 可以边看自己发b站等处的图文笔记&#xff0c;然后边用电…

iptables 的NDAT报错bash: 9000: command not forward

外网主机设置&#xff1a; iptables -t nat -A PREROUTING -d 192.168.3.51 -p tcp --dport 9000 -j DNAT --to-destination 192.168.3.61:22 本地shell连接&#xff1a; PS C:> ssh root192.168.3.51 9000 显示如下操作&#xff1a; PS C:> ssh root192.168.3.51 9000…

Linux 位置无关(PIC)代码和代码重定位(relocate)

在 Linux 和其他 Unix-like 操作系统中&#xff0c;位置无关代码&#xff08;Position Independent Code, PIC&#xff09;和代码重定位&#xff08;Relocation&#xff09;是两个重要的概念&#xff0c;它们对于共享库、动态链接和执行效率有着至关重要的影响。下面将详细介绍…

谷歌开源AI统计学专家DataGemma

谷歌开源AI统计学专家DataGemma&#xff1a;引领大数据时代的精准决策 在数字化浪潮席卷全球的今天&#xff0c;人工智能&#xff08;AI&#xff09;已成为推动社会进步的重要力量。其中&#xff0c;大语言模型&#xff08;LLM&#xff09;作为AI领域的前沿技术&#xff0c;以其…

CTFshow 命令执行 web29~web36(正则匹配绕过)

目录 web29 方法一&#xff1a;include伪协议包含文件读取 方法二&#xff1a;写入文件 方法三&#xff1a;通识符 web30 方法一&#xff1a;filter伪协议文件包含读取 方法二&#xff1a;命令执行函数绕过 方法三&#xff1a;写入文件 web31 方法一&#xff1a;filter伪…

基于Springboot+Vue的零食批发商仓库管理系统(含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 在这个…

关键字:sizeof

1.sizeof sizeof&#xff1a;确定一种类型&#xff0c;对应在开辟空间的时候的大小&#xff1b; 1.1求内置类型 printf("%d\n",sizeof(char)); //这些学过C语言的基本都没问题 printf("%d\n",sizeof(short)); printf("%d\n",sizeof(int…

Open-Sora全面开源?

来源 | 机器之心 ID | almosthuman2014 不久前 OpenAI Sora 以其惊人的视频生成效果迅速走红&#xff0c;在一众文生视频模型中突出重围&#xff0c;成为全球瞩目的焦点。继 2 周前推出成本直降 46% 的 Sora 训练推理复现流程后&#xff0c;Colossal-AI 团队全面开源全球首个类…

03 去重排序

题目&#xff1a; 桶排序变体&#xff1a; #include<iostream> #include<algorithm> using namespace std; #define M 100005 int a[M];int main() {int N;cin>>N;int count0;for(int i1;i<N;i){int temp;cin>>temp;if(a[temp]1){continue;}else{a…

Vue中watch监听属性的一些应用总结

【1】vue2中watch的应用 ① 简单监视 在 Vue 2 中&#xff0c;如果你不需要深度监视&#xff0c;即只需监听顶层属性的变化&#xff0c;可以使用简写形式来定义 watch。这种方式更加简洁&#xff0c;适用于大多数基本场景。 示例代码 假设你有一个 Vue 组件&#xff0c;其中…

vue双向绑定/小程序双向绑定区别

Vue双向绑定与小程序双向绑定在实现方式、语法差异以及功能特性上均存在显著区别。以下是对这两者的详细比较&#xff1a; 一、实现方式 Vue双向绑定 Vue的双向绑定主要通过其响应式数据系统实现。Vue使用Object.defineProperty()方法&#xff08;或在Vue 3中使用Proxy对象&am…

MindSearch 部署到Github Codespace 和 Hugging Face Space

一&#xff1a;概述 MindSearch是一个创新的AI搜索框架&#xff0c;由中国科技大学的科学家以及上海人工智能实验室的学者联合研发。 随着硅基流动提供了免费的 InternLM2.5-7B-Chat 服务&#xff08;免费的 InternLM2.5-7B-Chat 真的很香&#xff09;&#xff0c;MindSearch 的…

Spring Cloud之OpenFeign的具体实践

1 基本概念 OpenFeign基于Feign框架开发&#xff0c;而Feign是Netflix开源的一个声明式Web服务客户端。OpenFeign通过定义接口、注解和动态代理等方式&#xff0c;将服务调用的过程封装起来&#xff0c;使得开发者只需要定义服务接口&#xff0c;而无需关心底层的HTTP请求和序列…

1688商品详情关键词数据-API

要利用 Python 爬虫采集 1688 商品详情数据&#xff0c;需要先了解 1688 网站的页面结构和数据请求方式。一般使用 requests 库请求网站的数据&#xff0c;使用 BeautifulSoup 库解析网页中的数据。 以下是一个简单的 Python 爬虫采集 1688 商品详情数据的示例代码&#xff1a…

YOLO11改进|注意力机制篇|引入上下文锚注意力机制CAA

目录 一、【CAA】注意力机制1.1【CAA】注意力介绍1.2【CAA】核心代码 二、添加【CAA】注意力机制2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、【CAA】注意力机制 1.1【CAA】注意力介绍 CAA注意力机制的结构图如下&#xff0c;下面根据…

RAG:检索增强生成技术概览

Why 将大模型应用于实际业务场景时会发现&#xff0c;通用的基础大模型基本无法满足我们的实际业务需求&#xff0c;主要有以下几方面原因&#xff1a; 知识的局限性&#xff1a;大模型对于一些实时性的、非公开的或离线的数据是无法获取到的。幻觉问题&#xff1a;所有的AI模…

828华为云征文 | 利用FIO工具测试Flexus云服务器X实例存储性能

目录 一、Flexus云服务器X实例概要 1.1 Flexus云服务器X实例摘要 1.2 产品特点 1.3 存储方面性能 1.4 测评服务器规格 二、FIO工具 2.1 安装部署FIO 2.2 主要性能指标概要 三、进行压测 3.1 测试全盘随机读IO延迟 3.2 测试全盘随机写IO延迟 3.3 测试随机读IOPS 3.4…

KEYENCE Programming Contest 2024(AtCoder Beginner Contest 374) 题解

A - Takahashi san 2 Problem Statement KEYENCE has a culture of addressing everyone with the suffix “-san,” regardless of roles, age, or positions. You are given a string S consisting of lowercase English letters. If S ends with san, print Yes; otherwi…