【C语言】超详讲解☀️指针是个什么针?(一次性搞定指针问题)
目录
前言
一、 什么是指针?
引例
计算机是怎么对内存单元编号的呢?
内存空间的地址如何得到
想存地址怎么办?
本质目的不是为了存地址
二、指针和指针类型
为什么有不同类型的指针
1.指针的解引用
2.指针+-整数
三、野指针
造成野指针的原因
1.未主动初始化指针
2.指针越界访问
3.指针指向的空间释放
规避野指针
四、指针运算
1.指针+-整数
2.指针-指针
3.指针的关系运算
五、指针与数组
六、二级指针
七、指针数组
前言
指针这一部分可能很多人在学习的时候都觉得很难,但在这里我想说的是:不要自己吓自己,想一想,你当初刚上大学的时候可能觉得高数非常难,最后学完整本书的时候回过头再看还觉得很难吗? 肯定已经觉得没有刚开始学那么难了,那么其实指针也是这样的,只要把里面的东西都搞清楚,你就不觉得难了。
一、 什么是指针?
在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的内存单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。
其实计算机在设计的时候很多东西都是参考的生活中的东西
引例
eg.我国的国土面积:960万平方公里,这么大的国土面积是怎么合理使用的呢?聪明的国家领导人就想到,将这一大块区域进行划分,先划分为各个省,每个省再划分为各个市区,市区又划分为县城,县划分为镇,镇又划分为村,每个村的每户人家都有门牌号,可以很方便的找到,这样就对这样一大块区域进行了很好的管理。
那么其实电脑的设计也是这样的,在学C语言的这个阶段,我们常说的计算机的内存划分为3部分,栈区,堆区和静态区。
计算机在最终也是将内存划分为了很小的空间--内存单元。
我们平时要在学校找一个同学,也是先确定他的宿舍编号然后才去找的,不可能盲目的去找,而这里宿舍编号就是地址。
那在计算机内存中,我们想要找到某个内存单元,也是要知道它的编号呀,所以这里就对每个内存单元都进行了编号处理,将编号就称为内存单元的地址。
那么问题又来了
计算机是怎么对内存单元编号的呢?
1.计算机中的32位或者62位
32位的机器,它是有32根地址线(32根物理的电线),通电之后会将电信号转换成数字信号(正电--1,负电--0),32根地址线通电之后会产生以下的二进制序列数字信号:
00000000000000000000000000000000(32位)
00000000000000000000000000000001
00000000000000000000000000000010
………………………………………………
11111111111111111111111111111111
这个时候呢将二进制序列与内存单元一一对应,那么与内存单元相对应的二进制序列就是它的编号--地址。
2.每一个内存单元到底多大?
这个我们也是可以来计算一下
比如一个内存单元是1bit,那么32位机器的所产生的序列共计2^32个,即2^32bit,转化为MB就是512MB,要想想我们买的计算机都是起步2G/4G,所以内存单元以bit为单位肯定是不行的。在C语言中,我们创建一个变量所申请的空间最小都是char--1byte,所以其实内存单元单位其实是字节。
请看如下代码:
int main()
{
int a = 10; //创建变量a,向内存申请4个字节的空间,将10存储进去
return 0;
}
内存空间的地址如何得到
我们创建了变量a,如何得到它的地址呢? 我们只需要在a的前面加上 & (取地址操作符
)符号,便可以得到它的空间地址
int main()
{
int a = 10; //创建变量a,向内存申请4个字节的空间,将10存储进去
printf("%p\n", &a); //%p--打印地址(16进制)
return 0;
}
我们可以把打印出来的地址00EFF6E8(16进制)转化成二进制来看看1110 1111 1111 0110 1110 1000
想存地址怎么办?
我们平时可以创建字符变量来存字符,创建整型变量来存整型,那如果想将地址存起来呢?那我们就得创建一个指针变量----用来存放地址
int main()
{
int a = 10; //创建变量a,向内存申请4个字节的空间,将10存储进去
int* p = &a; //&a取出a的地址,创建指针变量p,接收a的地址
return 0;
}
* 说明p是指针变量,前面的int说明p指向的是整型变量
指针变量p里面存放的就是a的地址,我们可以通过p里面所存储的值(地址)来找到变量a的内存空间,所以我们就说p指向了a,所以将p形象的称为指针(注意变量名字是p,不是*p)
那么我们以后想要存储地址的时候,就可以用指针
int main()
{
float b = 2.5;
float* p = &b;
return 0;
}
本质目的不是为了存地址
我们创建指针存放变量地址的本质目的并不是为了存放变量的地址,而是为了将来能够通过地址来找到这个变量的内存空间,使用它。
我们上面已经存放了a的地址在p里面,那么我们怎么找到a呢
int main()
{
int a = 10;
int* p = &a;
*p = 20; //这里*---解引用操作符
return 0;
}
这里的 * --解引用操作符(间接访问操作符),p里面存的是a的地址,那么*p就是通过地址来找到a
二、指针和指针类型
通过上面的代码我们会发现,一个char类型的变量的地址放在char*的指针里面,整型变量的地址存放在整型指针里面,为什么要这样呢?
为什么有不同类型的指针
1.指针的解引用
如下代码:
int main()
{
int a = 10;
char ch = 'f';
int* pa = &a;
char* pc = &ch; //两个指针都是4个字节
}
这里要存放a的地址和ch的地址,都只需要4个字节(32位平台)的指针就可以,那为什么还要用不同类型的指针呢?
虽然它们都是4个字节,但是也是有区别的,我们通过以下代码的对比来看看
代码1:
int main()
{
int a = 0x11223344;
int* pa = &a;
*pa = 0;
}
在内存里面看
a的内存里面的内容(执行*pa = 0;这个语句前后对比)(先不要管为什么内存里面是倒着存放的)
代码2:
int main()
{
int a = 0x11223344;
char* ca = &a;
*ca = 0;
}
a的内存里面的内容(执行*ca = 0;这个语句前后对比)
我们通过代码1和代码2以及他们的内存情况可以看出在代码1int*类型的指针在解引用的时候将a里面4个字节的内容全部改成了0(可访问4个字节),但是代码2中char*类型的指针在解引用的时候只改了1个字节的内容(只能访问一个字节),这就是指针不同类型的差异
2.指针+-整数
通过以下代码来体验一下:
#include <stdio.h>
int main()
{
int a = 0x11223344;
int* pa = &a;
char* ca = &a;
printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("\n");
printf("%p\n", ca);
printf("%p\n", ca + 1);
return 0;
}
通过这段代码我们可以发现int*类型的指针进行+1操作地址加了4,char*类型的指针进行+1操作地址加了1
我们也可以通过下面的例子再来看看(通过指针来打印整型数组的内容)
代码1(通过整型指针来访问整型数组)
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* parr = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(parr + i));
}
return 0;
分析:
整型指针进行+1操作,意味着一次跳过一个整型(如下图)
代码2(通过字符指针来访问整型数组)
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
char* parr = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(parr + i));
}
return 0;
}
字符类型的指针+1每次跳过一个char(1个字节)
总结:
指针类型的意义:
指针的解引用:指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节),能一次性访问几个字节
指针+-整数:指针的类型决定了指针向前或向后走一步有多大(它是什么类型的指针,+1就跳过该类型大小的字节,eg.char*+1跳过一个字节,short*+1跳过2个字节,int*+1跳过4个字节,float*+1跳过4个字节,double*+1跳过8个字节)
三、野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
造成野指针的原因
1.未主动初始化指针
请看如下代码:
int main()
{
int* p;
*p = 8;
}
分析:
这段代码里面的p就是野指针,因为p是一个局部变量,指针变量,未主动初始化,那么它里面就是随机值(随机地址),就是说这个地址是不清楚的,然后后面又对它进行解引用,将8存放到这块不明确的地址里面,这就是又问题的
运行结果:
2.指针越界访问
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = arr;
int i = 0;
for (i = 0; i <= 10; i++)
{
*pa = 1;
pa++;
}
return 0;
}
分析:
这里的数组长度为10,但明显可以看到循环执行了11次,pa最后所指向的已经不是数组里面的内存单元,越界访问了
可以来看看内存
这是在数组初始化之后数组中的内容
在执行完11次循环之后
此时会发现指针pa最后一次循环时越界,并且将数组arr后面的一个内存单元里面的内容改成了1,但是要知道这块内存并不属于该程序(这就是非法的)
3.指针指向的空间释放
如下代码
int* app()
{
int b = 3;
return &b;
}
int main()
{
int* ps = app();
return 0;
}
分析:
这段代码中,app这个函数返回了临时变量b的地址,ps这个指针也接受到了b的地址,但是要知道在这个函数调用完之后所创建的b这块内存空间就已经销毁了(或者说还给操作系统了),那么ps再记住这块空间的地址就没有意义了(保存了一个非法的地址)
规避野指针
指针初始化
小心指针越界
指针指向空间释放即使置NULL
指针使用之前检查有效性
四、指针运算
1.指针+-整数
示例:(通过指针打印数组元素)
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(pa + i));
}
return 0;
}
运行结果:
2.指针-指针
示例:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", &arr[9] - &arr[0]);
return 0;
}
请问这段代码的运行结果是什么呢? 是36?
需要知道的是 指针-指针:得到的是两个指针之间元素的个数(前提:两个指针指向同一块空间)
运行结果:
3.指针的关系运算
代码1:
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* ps = &arr;
for (ps = &arr[5]; ps > &arr[0];)
{
*--ps = 0;
}
return 0;
}
这段代码将数组中的元素改成了0
代码2:
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* ps = &arr;
for (ps = &arr[4]; ps >= &arr[0];ps--)
{
*ps = 0;
}
return 0;
}
代码2与代码1相比更容易理解,但是要知道这样写是很大有问题的,C标准并不保证它可行
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许
与指向第一个元素之前的那个内存位置的指针进行比较
五、指针与数组
再来啰嗦一下
1.数组名表示首元素地址
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", &arr[0]);
printf("%p\n", arr);
return 0;
}
可以看到这种方式打印出来的结果是一样的
数组名表示数组首元素的地址在绝大多数情况下都成立
两个例外:
sizeof(数组名)
&数组名
可以通过指针来访问数组
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* pa = arr;
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(pa + i));
}
return 0;
}
六、二级指针
我们都知道指针是用来存地址的,指针也是在内存中开辟了一块空间然后将地址存储在里面,所以指针本身也是有自己的地址的
那我们就可以取出指针的地址来进行存放
int main()
{
int a = 8;
int* ps = &a; //ps称为一级指针
int** pps = &ps; //pps称为二级指针 用来 存储一级指针的地址
}
存储地址就是为了后面能用得到的
现在对它进行解引用操作
#include <stdio.h>
int main()
{
int a = 8;
int b = 55;
int* ps = &a; //ps称为一级指针
int** pps = &ps; //pps称为二级指针 用来 存储一级指针的地址
*pps = &b;
**pps = 666;
printf("%d\n", b);
printf("%d\n", **pps);
}
*pps 通过对ppa中的地址进行解引用,这样找到的是ps,*pps 其实访问的就是ps
**pps 先通过*pps 找到ps ,然后对ps 进行解引用操作
运行结果:
七、指针数组
我们前面用过的整型数组拿来存放整型,字符数组里面存放字符,那么现在里面存放指针的数组就叫做指针数组
现在先简单说下,后面在指针进阶阶段还会再详细说明
通过以下代码来体验一下
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
int c = 2;
int* arr[3] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *arr[i]);
}
}
这里是创建了一个整型指针数组,并对它进行了初始化,里面存放有a,b,c的地址,再通过解引用去打印出a,b,c
运行结果:
如下图:
这个数组里面每个元素都是一个整型指针
-----------------------------------------------------------------
-----------------C语言操作符部分完结-------------------
关于C语言,每个知识点后面都会单独写博客更加详细的介绍
欢迎大家关注!!!
一起学习交流 !!!
让我们将编程进行到底!!!
--------------整理不易,请三连支持------------------
————————————————
【C语言】超详讲解☀️指针是个什么针?(一次性搞定指针问题)的更多相关文章
- 全面系统讲解CSS工作应用+面试一步搞定
[TOC] 一.课程介绍 二.HTML基础强化 html常见元素和理解 html常见元素分类 head区元素:(不会在页面上留下元素) * meta * title * style * link * ...
- 全面系统讲解CSS 工作应用+面试一步搞定
- 在mac上安装gradle(超详细,直接按步骤操作即可轻松搞定)
第一步, 就是先download最新版本的gradle,网址如下: http://gradle.org/gradle-download/ 然后将下载下来的zip包放解压到本地任意的路径上, 例如,我本 ...
- C语言超全学习路线(收藏让你少走弯路)
刚入门是否觉得C语言很难?那可能是你还没找到正确的C语言学习路线,收藏以防找不到,让你少走弯路. 基本语法 选择控制语句 if,swith 循环控制语句 while,for 控制语句相关关键字分析 变 ...
- C语言字符数组超细讲解
看到标题,有不少朋友会想:字符数组不也是数组吗?为什么要单独拿出来讲哩?莫非它是朵奇葩? 哈哈,确实,一起来认识一下这朵数组界的奇葩吧! 一.字符数组的定义.引用.初始化 大家好!我是字符数组,看我的 ...
- JUC中的AQS底层详细超详解
摘要:当你使用java实现一个线程同步的对象时,一定会包含一个问题:你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒? 本文分享自华为云社区<JUC中的AQS底层详细超详解,剖 ...
- 超细讲解Django打造大型企业官网
本文为知了课堂黄勇老师讲的<超细讲解Django打造大型企业官网>的笔记. 第一章 Django预热 1.创建virtualenv虚拟环境 2.URL组成部分详解 3.Django介绍 4 ...
- Python3调用C程序(超详解)
Python3调用C程序(超详解) Python为什么要调用C? 1.要提高代码的运算速度,C比Python快50倍以上 2.对于C语言里很多传统类库,不想用Python重写,想对从内存到文件接口这样 ...
- 彻底搞定C语言指针(精华版)
1.语言中变量的实质 要理解C指针,我认为一定要理解C中“变量”的存储实质, 所以我就从“变量”这个东西开始讲起吧! 先来理解理解内存空间吧!请看下图: 内存地址→ 6 7 8 9 10 11 12 ...
随机推荐
- 4.文件共享总结上篇-Windows之间文件共享
本文章包含上篇和下篇两部分,今天我们主要讨论Windows系统之间的文件互访 Windows系统之间文件互传 1)利用Windows自带的文件共享服务 本次试验以Win7为服务器端,win10为客户端 ...
- svelte组件:svelte3.x自定义美化虚拟滚动条组件svelte-scrollbar
基于svelte3.0自定义pc端虚拟滚动条组件svelteScrollbar. svelte-scrollbar:运用svelte3.x创建的桌面pc版自定义美化滚动条组件.支持是否原生滚动条.自动 ...
- C++进阶-3-6-map/multimap容器
C++进阶-3-6-map/multimap容器 1 #include<iostream> 2 #include<map> 3 using namespace std; 4 5 ...
- CTF简介
最近在学习渗透测试,后来发现CTF很有趣,发现对学习有所帮助,于是找了几个网站,下面推荐几个我觉得不错的网站 https://www.ctfhub.com/#/index https://adworl ...
- HTML表格以及表单
学习内容: 1.HTML表格 代码实例: <%@ page language="java" import="java.util.*" pageEncodi ...
- MySQL之SQL语句优化
语句优化 即优化器利用自身的优化器来对我们写的SQL进行优化,然后再将其放入InnoDB引擎中执行. 条件简化 移除不必要的括号 select * from x where ((a = 5)); 上面 ...
- synchronized真的很重么?
synchronized 是java中常见的保证多线程访问共享资源时的安全的一个关键字.很多人在讲到synchronized 时都说synchronized 是一把重量级的锁,那么synchroniz ...
- C#/VB 数据库连接字符串大全
C#/VB 数据库连接字符串大全 https://www.connectionstrings.com/ SQL Server常用 Standard Security Server=myServerAd ...
- 【Java面试】为什么引入偏向锁、轻量级锁,介绍下升级流程
Hi,我是Mic 一个工作了7年的粉丝来找我,他说最近被各种锁搞晕了. 比如,共享锁.排它锁.偏向锁.轻量级锁.自旋锁.重量级锁. 间隙锁.临键锁.意向锁.读写锁.乐观锁.悲观锁.表锁.行锁. 然后前 ...
- 当JAVA注解、AOP、SpEL相遇,更多可能变为了现实
常规情况下,我们可以通过业务定制化的注解,借助AOP机制来实现某些通用的处理策略.比如定义个@Permission注解,可以用于标识在具体的方法上,然后用来指定某个方法必须要指定角色的人才能够访问调用 ...