初阶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;
}
这只是模拟二维数组,并不是真的二维数组,因为二维数组的内存分布是连续的,这个内存分布是不连续的。
相关文章:

初阶C语言-指针
1.指针是什么? 理解指针的两个要点: 1.指针是内存中一个最小单元的编号,也就是地址 2.口头语中说的指针,通常是指指针变量,是用来存放内存地址的变量 总结:指针就是地址,口语中说的指针通常是指…...

论文笔记:微表情欺骗检测
整理了AAAI2018 Deception Detection in Videos 论文的阅读笔记 背景模型实验可视化 背景 欺骗在我们的日常生活中很常见。一些谎言是无害的,而另一些谎言可能会产生严重的后果。例如,在法庭上撒谎可能会影响司法公正,让有罪的被告逍遥法外。…...

智能家居有哪些产品?生活中常见的人工智能有哪些?
智能家居有哪些产品? 1、智能照明设备类:智能开关、智能插座、灯控模块、智能空开、智能灯、无线开关。 2、家庭安防类:智能门锁、智能摄像机、智能猫眼、智能门铃。 3、智能传感器类:烟雾传感器、可燃气体传感器、水浸传感器、声光报警器…...

洗车行软件系统有哪些 佳易王洗车店会员管理系统操作教程#洗车店会员软件试用版下载
一、前言 【试用版软件下载可点击本文章最下方官网卡片】 洗车行软件系统有哪些 佳易王洗车店会员管理系统操作教程#洗车店会员软件试用版下载 洗车管理软件应用是洗车业务的得力助手,实现会员管理及数据统计一体化,助力店铺高效、有序运营。 洗车项…...

【Java】springboot 项目中出现中文乱码
在刚创建的springboot项目中,出现乱码,跟走着解决一下 1、Ctrl Shift S 打开idea设置,根据图片来,将③④这三个地方都修改为UTF-8 2、返回配置查看,解决...

开放式耳机是什么意思?漏音吗?开放式的运动蓝牙耳机推荐
目前运动耳机市场主要分为入耳式、骨传导和开放式三类。入耳式耳机占比30%-40%,虽目前占比较大,但因在运动场景下有闷塞感、出汗不适、屏蔽外界环境音带来安全隐患等缺点,占比会逐渐下降。 骨传导耳机占比也为30%-40%,其不堵塞耳…...
如何优雅的处理NPE问题?
1.什么是NPE? NPE,即NullPointerException,是开发中最常见的问题之一,有必要知道如何正确地处理NPE。 对于 Java 开发者来说,null 是一个令人头疼的类型,一不小心就会发生 NPE (空指针…...

k8s 中存储之 NFS 卷
目录 1 NFS 卷的介绍 2 NFS 卷的实践操作 2.1 部署一台 NFS 共享主机 2.2 在所有k8s节点中安装nfs-utils 2.3 部署nfs卷 2.3.1 生成 pod 清单文件 2.3.2 修改 pod 清单文件增加 实现 NFS卷 挂载的 参数 2.3.3 声明签单文件并查看是否创建成功 2.3.4 在 NFS 服务器 创建默认发布…...

Redis中BitMap实现签到与统计连续签到功能
服务层代码 //签到Overridepublic Result sign() {//1.获取当前登录的用户Long userId UserHolder.getUser().getId();//获取日期LocalDateTime now LocalDateTime.now();//拼接keyString keySuffix now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String …...

【Spring】“请求“ 之传递 JSON 数据
文章目录 JSON 概念JSON 语法JSON 的语法JSON 的两种结构 JSON 字符串和 Java 对象互转JSON 优点传递 JSON 对象 JSON 概念 JSON:JavaScript Object Notation【JavaScript 对象表示法】 JSON 就是一种数据格式,有自己的格式和语法,使用文本…...

文心一言 VS 讯飞星火 VS chatgpt (359)-- 算法导论24.3 1题
一、在图 24-2上运行Dijkstra算法,第一次使用结点 s s s作为源结点,第二次使用结点 z z z作为源结点。以类似于图 24-6 的风格,给出每次while循环后的 d d d值和 π π π值,以及集合 S S S中的所有结点。如果要写代码,…...

Redis-预热雪崩击穿穿透
预热雪崩穿透击穿 缓存预热 缓存雪崩 有这两种原因 redis key 永不过期or过期时间错开redis 缓存集群实现高可用 主从哨兵Redis Cluster开启redis持久化aof,rdb,尽快恢复集群 多缓存结合预防雪崩:本地缓存 ehcache redis 缓存服务降级&…...

jvisualvm学习
系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程:封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…...

Gazebo环境下开源UAV与USV联合仿真平台
推荐一个ROS2下基于Gazebo环境的开源UAV与USV联合仿真平台。平台是由两个开源项目共同搭建的。首先是UAV仿真平台,是基于PX4官方仿真平台(https://docs.px4.io/main/en/sim_gazebo_gz);其次是USV仿真平台,是基于VRX仿真…...

Linux进程调度和进程切换
并行(Parallel) 含义:并行是指多个任务在同一时刻同时执行。 硬件要求:需要多个处理器(如多核CPU)或者多台计算设备来实现,这些执行单元能够真正地同时处理不同的任务。例如,一个具…...

机器学习基本上就是特征工程——《特征工程训练营》
作为机器学习流程的一部分,特征工程是对数据进行转化以提高机器学习性能的艺术。 当前有关机器学习的讨论主要以模型为中心。更应该关注以数据为中心的机器学习方法。 本书旨在介绍流行的特征工程技术,讨论何时以及如何运用这些技术的框架。我发现&…...

Android Framework AMS(01)AMS启动及相关初始化1-4
该系列文章总纲链接:专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明: 说明:本章节主要涉及systemserver启动AMS及初始化AMS相关操作。同时由于该部分内容分析过多,因此拆成2个章节,本章节是第一章节&…...

基于基于微信小程序的社区订餐系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…...

[单master节点k8s部署]29.Istio流量管理(五)
测试istio熔断管理。 采用httpbin镜像和fortio镜像,其中httpbin作为服务端,fortio是请求端。这两个的配置yaml文件都在istio的samples/httpbin目录下,fortio的配置文件在samples-client目录下。 [rootmaster httpbin]# ls gateway-api ht…...
Something for 24OI
zyj老师希望我给24OI的同学们写一点东西,虽然感觉我也没有什么先进经验,还是尽力写一些主观的感受吧。 如何平衡文化课和竞赛的关系?不要以牺牲文化课的代价学习竞赛。首先,绝大多数的竞赛同学,或早或晚都会在退役后回…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...
稳定币的深度剖析与展望
一、引言 在当今数字化浪潮席卷全球的时代,加密货币作为一种新兴的金融现象,正以前所未有的速度改变着我们对传统货币和金融体系的认知。然而,加密货币市场的高度波动性却成为了其广泛应用和普及的一大障碍。在这样的背景下,稳定…...