一网打尽:指针和数组
系列文章目录
第一回 三十二个关键字 心性修持大道生
没看过第一回? 点这里
第二回 悟彻指针真妙理 归本数组合元神
前言
大家好!我们又见面了,今天更新了第二回的内容,一起来看看叭。
文章目录
- 系列文章目录
- 前言
- 一、指针
- 1.1 指针的内存布局
- 1.2 小兔子要开几扇门?
- 1.3 int *p = NULL 和 *p = NULL 的区别
- 1.4 如何将数值存储到指定的内存地址
- 二、数组
- 2.1 数组的内存布局
- 2.2 &array[0]和&array的区别
- 2.3 数组名a作为左值和右值的区别
- 三、金箍棒和九齿钉耙---指针和数组的区别
- 3.1 以指针的形式访问和以下标的形式访问指针
- 3.2 a和&a的区别
- 3.3 指针和数组的定义和声明
- 3.4 指针和数组的对比
- 四、指针数组和数组指针
- 4.1 指针数组和数组指针的内存布局
- 4.2 数组指针的步长
- 4.3 地址的强制转换
- 五、多维数组和多级指针
- 5.1 二维数组布局是棋盘吗?
- 5.2 经典例子
- 六、函数指针
- 6.1 函数指针的定义
- 6.2 函数指针的使用
- 6.3 函数指针数组
- 总结
提示:以下是本篇文章正文内容
一、指针
1.1 指针的内存布局
先看下面的例子:
int a = 5;
int *p = &a;
这里定义了一个指针p,但是指针到底是什么呢?
我们前面已经学习了int、char等数据类型,他们大小不一。
我么可以把它们看做是不同型号的模具压出来的,如下图:
int类型的模具压一下是4个字节,char类型压一下是1个字节…
我们定义一个整型变量a,就好比用int类型的模具在内存上压了一下,发现压了四个字节。
指针也是一种数据类型,那我们就用int*作为模具在内存上压一下, (在32位系统下)发现指针变量也占4个字节,于是这四个字节的空间被命名为p,p里面只能存放某个内存地址。并且这个某个内存的地址开始的连续4个字节只能存放int类型的数据。
图解:
思考:为什么p只能存放某个内存地址呢??
在锤子眼里,什么东西都是钉子。
在指针眼里,什么东西都是地址。
也就是说,不论我们在指针变量里放什么,都会被视作地址。
Tips:指针变量的大小与类型无关,32位系统下均为4.
1.2 小兔子要开几扇门?
* 是解引用操作符,* 通过对指针进行解引用操作,可以修改被指向地址存放的值。
举个例子:
#include<stdio.h>int main()
{int a = 0;int* p = &a;*p = 10;printf("%d", a);
}
打印结果为10,这说明*pa找到了内存中的某个区域,将其改为10.
这时细心的小伙伴就发现了,你只举了int * 类型的例子,可是指针变量有这么多类型,大小也都是4个字节,有什么区别吗?
我们看以下例子:
int main()
{int a = 64;char* p = &a;*p = 10; //只有一个字节的访问权限,只能修改a中的一个字节
}
我们首先定义整型变量a,其值为64,十六进制存储为0x00000400
p是一个char*类型的指针,*p = 10; 这条语句只能改变了a的一个字节里面的数据。
因而修改后a的值为0x0000040a
指针变量类型实际上决定了在解引用时能访问几个字节。
1.3 int *p = NULL 和 *p = NULL 的区别
先看下面的代码:
int *p = NULL;
这段代码的意思是:定义一个指针变量p,它指向的内存里存放一个int类型的数据;定义p的同时把p的值设为0x00000000,这个过程叫做初始化。
int *p ;
*p = NULL;
这段代码的意思是:定义一个指针变量p,它指向的内存里存放一个int类型的数据;定义p的同时,我们不知道p里面存的是哪个地址,这个地址可能是非法的;接着我们把*p的值改为0x00000000,也就是说,我们在内存里随便找了个地址,对它进行了一些操作,这种做法明显是错误的。
对于这种指针,我们称为野指针,又称为野狗。
野狗的特点: 1.没人要,可能出现在任何地方。
2.和野狗玩耍,可能出现严重的后果。
为了避免出现野狗伤人,我们在定义指针变量时一定要初始化。
1.4 如何将数值存储到指定的内存地址
如果我们想往一个指定的地址写入数据,比方说想在0x004ffd80中写入0x100,可用以下代码:
int *p = (int*)0x004ffd80;*p = 0X100;
这玩意必须得强制类型转换,这样0x001ffd80才不会被看做是一个整型数据。
为什么是0x004ffd80这个地址呢?
因为并不是所有内存中的地址我们都有权限访问,
这个地址是通过定义整型变量 i 从监视窗口得到的,所以我们可以偷偷使用。
Tips:VS2019这样做是不行的,因为每次编译器为整型变量 i 分配的地址是不同的。
第一次分配的可用地址第二次使用不一定合法。
经过尝试,编译器会报如下的错误。
二、数组
2.1 数组的内存布局
先看下面的例子:
int array[5];
定义一个整型数组array,数组有五个元素,每个元素的类型是int。
当我们定义一个数组array时,编译器根据其元素类型和元素个数为其分配内存空间,并将其命名为array。
名字array一旦和这块内存匹配就不能被改变,array[0]是数组的元素,但不是元素的名字,数组的每个元素都是没有名字的。
2.2 &array[0]和&array的区别
请看以下代码:
#include<stdio.h>int main()
{int array[5] = { 0 };printf("%p\n", &array);printf("%p\n", &array[0]);
}
执行后我们发现结果是一样的,但是意义是不一样的。
&array[0]是取数组首元素的地址,&array是取整个数组的地址,整个数组的地址在数值上和首元素地址是一样的,因而打印结果一样。
执行以下代码,
#include<stdio.h>int main()
{int array[5] = { 0 };printf("%p\n", &array[0]);printf("%p\n", &array[0]+1);printf("%p\n", &array);printf("%p\n", &array + 1);
}
我们发现他们的步长是不同的。
&array[0]+1步长为4,跳过了一个数组元素大小的内存空间
&array+1步长为20,跳过了一整个数组大小的内存空间
2.3 数组名a作为左值和右值的区别
一般来说,赋值运算符左边的是左值,右边的是右值。
左值和右值有什么区别呢?
我们执行以下代码:
#include<stdio.h>int main()
{const int a = 5;a = 10;
}
编译器报错了,内容是 表达式必须是可修改的左值 。
这说明左值是可修改的,而要修改一个变量的值,我们就需要它的地址,通过地址去找到它并修改。
假定有一条语句a = b;
左值:编译器认为a的含义是a代表的地址。
右值:编译器认为b的含义是b所在地址存储的内容。
(图隔开)
既然明白了左值和右值的区别,我们来看数组作为左值、右值的情况。
左值:
#include<stdio.h>int main()
{int array[5] = { 0 };array[0] = 10;
}
我们知道数组的每个元素是没有名字的,但是可以通过它的地址找到它,这样就把10赋进去了。这说明array[0]的含义是array[0]代表的地址。
#include<stdio.h>int main()
{int array[5] = { 0,1,2,3,4 };array[0] = array[1];
}
array[0] = array[1];这条语句,执行胡array[0]的内容变为1,说明数组元素作为右值时,array[1]的含义是这个地址存储的内容。
Tips:数组名不能作为左值。
三、金箍棒和九齿钉耙—指针和数组的区别
指针和数组看似有许多共同点,实际二者完全不同。
指针就是指针,数组就是数组。
你可以认为他们是两个串通好的坏女人,经常穿着对方的衣服来哄骗你。
3.1 以指针的形式访问和以下标的形式访问指针
请看以下代码:
char *p = "abcdef";
这里我们定义了一个指针变量p,它本身在栈上占四个字节,他存储了一块内存空间的首地址,这块内存空间位于静态区,大小为7个字节。(字符串末尾带’\0’)如果我们现在想访问字符‘e’,有两种方式:
1.以指针形式访问:*(p+4)
找到p里面存储的地址,加上4个字符的偏移量,得到新的地址,然后解引用得到‘e’
2.以下标形式访问: p[4]
找到p里面存储的地址,加上4个元素的偏移量,得到新的地址,然后解引用得到‘e’
经过这么一折腾,我们发现:这不是一样吗?
没错,以指针形式访问和以下标形式访问没有本质区别。
但这似乎是指针和数组唯一的联系了。
3.2 a和&a的区别
我们先看一个例子:
int main()
{int a[5] = {5, 4, 3, 2, 1};int *ptr = (int *)(&a + 1);printf( "%d,%d", *(a + 1), *(ptr - 1));return 0;
}
程序的输出是什么呢?
&a+1,取数组的首地址,向后走一整个数组的步长,变成了a[5],显然已经越界,但是有问题吗?没有问题。
为什么呢?
打个比方,我没有访问银行柜台的权限,但是我总有看一眼柜台的权限吧?
我确实指向了a[5],但并不代表我要去做什么非法的事情。
int *ptr = (int *)(&a + 1);将刚刚得到的地址强制类型转换赋值给ptr
*(a + 1), a是数组首元素的地址,+1变为第二个元素地址,解引用后输出4
*(ptr - 1), ptr指向a[5],-1指向a[4],解引用得到1
3.3 指针和数组的定义和声明
先思考:定义是什么? 声明又是什么?
int i;
extern int x ;
谁是定义?谁是声明?
话不多说,上图。
声明的经典例子:函数的声明
void Print(int x,int y)
我们已经知道声明是不分配空间的,
那么下面的式子完全等价:
extern int a[];
extern int a[100];
编译器完全不需要知道这个数组有几个元素,他只要知道a数组的定义在别的地方,还知道这个数组a的起始地址。
这样数组内的每个元素的地址都可以通过起始地址计算出来。
(配图分割)
如果我们定义了一个数组,在另外一个文件中使用,其声明也必须是数组。
举例:
如果定义为数组,声明为指针,会是什么效果呢
编译器认为a是一个指针变量占四个字节,它直接取了数组前四个字节作为存储的地址,= =
这个地址的有效性不得而知。
一样的,如果定义指针,声明为数组,也是错的!!!
(配图)
3.4 指针和数组的对比
四、指针数组和数组指针
指针数组?函数指针?绕晕了吗,没关系,博主带你一探究竟。
4.1 指针数组和数组指针的内存布局
指针数组: 指针数组是一个数组,数组的每个元素是指针,数组占多少字节由元素个数决定即N*sizeof(a[0])。
数组指针: 数组指针是一个指针,指针指向一个数组,32位系统下,指针占4个字节,至于其指向的数组大小是未知的。
猜猜看,下面谁是指针数组,谁是数组指针?
Tips:注意操作符的优先级。
int* a[10];//式子1int(*a)[10];//式子2
式子1:由于[ ]的优先级高于 * ,式子1中a先和[ ]结合,说明a是一个数组,接着与 * 结合,说明数组里的每个元素是 int * 类型,即数组p里面存放了10个指针,最后和int结合,说明指针指向的内容的数据类型为int,所以p是一个由指向整型数据的指针组成的数组。
式子2:由于*p有小括号,p先和 * 结合,说明p是一个指针,接着与[ ]结合,说明p指向的内容是数组,接着与int 结合,说明数组的元素类型为int,所以p是一个指向整型数组的指针。
4.2 数组指针的步长
通过这个报错,我们可以知道:
把变量名去掉,剩下的部分就是数组指针的类型。
再看一个例子:
#include<stdio.h>int main()
{char a[5] = { 'A','B','C','D' };char(*p3)[5] = &a;//char(*p4)[5] = a;printf("%p\n", p3);printf("%p\n", p3+1);return 0;
}
输出后我们发现p3+1的地址比p3的地址大5,
这说明数组指针的步长就是所指向的数组的大小。
4.3 地址的强制转换
请看这个例子:
struct Test
{int num;char* pc_Name;short Date;char a[2];short b[4];
}*p;
假设p的值为0x00000000,试求下列式子:
p+0x1=0x_________;
(unsigned int*)p+0x1=_________;
(unsigned long)p+0x1=0x______;
一个指针变量和整数相加减,结果是什么呢?
前面我们已经多次聊到步长,
p+0x1,这个0x1可不是普通的整数,实际上它是sizeof(Test) * 0x1,
由于结构体Test的大小为20个字节,因而答案为0x00000014.
不会判断结构体大小?点我!!!
(unsigned long)p+0x1,()是强制类型转换,指针变量p被转换成一个无符号长整型数,这样就变成了普通的加减法,加上+0x1就是直接加上1
(unsigned int*)p+0x1,()是强制类型转换,指针变量p被转换成一个指向无符号整型变量的指针,0x1实际上是0x1 * sizeof(unsigned int),因而答案为0x00000004
五、多维数组和多级指针
5.1 二维数组布局是棋盘吗?
初学时,我们把二维数组认为是几行几列的棋盘,这样方便理解。
实际上并不是这样,因为内存是线性的。
实际上的布局是这样:
没错,就是把他们拼起来2333…
即学即用,来看这题:
#include<stdio.h>int main()
{int a[3][2] = { (0,1),(2,3),(4,5) };int* p;p = a[0];printf("%d", p[0]);
}
我想你已经想出了答案,是0吗?NONONO,答案是1;
为什么呢?
因为这题是有坑的!!!
我们注意到初始化时使用了( ),括号表达式的结果是最右边那个表达式,因此实际上的初始化为:
int a[3][2] = { 1,3,5 };
p是数组第一行的地址,第一行的地址是第一个元素的首地址,所以p[0]作为右值时候是1.
5.2 经典例子
还没过瘾?再来一题。
int a[5][5];int(*p)[4];p = a;
求 &p[4][2] - &a[4][2] 的值。
直接运用图解:
六、函数指针
6.1 函数指针的定义
经过前面的学习,我们应该能猜到:函数指针是一个指向函数的指针。
举例:
int* (*p)(int n);
这是一个函数指针,函数的参数是int,返回值类型为int*
6.2 函数指针的使用
例1:用(*p)代替函数名
#include<stdio.h>
void Print(int x)
{printf("%d", x);
}
int main()
{void (*pf)(int x);pf = &Print;int a = 5;(*pf)(a);
}
例2:回调函数
回调函数就是一个通过函数指针调用的函数。
如果把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,用于对该事件或者条件进行响应。
请看以下代码:
#include<stdio.h>void menu()
{printf("***************\n");printf("*** 1.Add ***\n");printf("*** 2.Sub ***\n");printf("*** 3.Mul ***\n");printf("***************\n");
}int Add(int x, int y)
{return x + y;
}int Sub(int x, int y)
{return x - y;
}int Mul(int x, int y)
{return x * y;
}void Calc(int (*pf)(int x, int y))
{int x = 0;int y = 0;int ret = 0;printf("please input x and y:");scanf("%d%d", &x, &y);ret = pf(x, y);printf("%d", ret);
}int main()
{menu();int input = 0;scanf("%d", &input);switch (input){case 1:Calc(Add);break;case 2:Calc(Sub);break;case 3:Calc(Mul);}
}
我们通过使用回调函数,代码整体非常简洁,原因是Calc以函数指针为参数,只需要接受函数。
试想,如果没有Calc这个函数,我们需要在每个单独的函数里都要插入“请输入两个数x和y”,还有一系列相关的重复语句。
如果不用回调函数,我们就会遇到下面的代码:
int main()
{menu();int input = 0;scanf("%d", &input);int x = 0;int y = 0;int a = 0;int b = 0;int c = 0;int d = 0;switch (input){case 1:printf("please input x and y:");scanf("%d%d", &x, &y);Add(x, y);break;case 2:printf("please input a and b:");scanf("%d%d", &a, &b);Sub(a, b);break;case 3:printf("please input c and d:");scanf("%d%d", &c, &d);Mul(c,d);break;}
}
这太繁杂了,光是重复的语句就是一大堆,如果有十来个这样的函数,代码惨不忍睹。而回调函数就解决了重复语句过多的问题。
6.3 函数指针数组
没错,就是在p后面加一个[10],
int (*p[10])(char * pf)
你可以把一大群函数指针放进去。
总结
第二回至此结束,博主是小白一枚,讲解自有缺漏甚至错误,劳请斧正。
一网打尽:指针和数组相关推荐
- char *a 和char a[] 的区别(指针和数组的区别)
2019独角兽企业重金招聘Python工程师标准>>> 在C/C++中,指针和数组在很多地方可以互换使用,这使得我们产生一种错觉,感觉数组和指针两者是完全等价的,事实上数组和指针是有 ...
- 用指针查找数组中温度值为32的天数
<程序设计基础实训指导教程-c语言> ISBN 978-7-03-032846-5 p92 5.1.2 上级实训内容 [实训内容4]假设一个数组用于存储一周7天,每天24小时温度的度数,数 ...
- 浅谈C中的指针和数组(一)
本文转载地址:http://www.cnblogs.com/dolphin0520/archive/2011/11/09/2242138.html 在原文的基础上加入自己的想法作为修改. 指针是C/C ...
- IOS笔记-C语言中的指针与数组
1.指针与数组 1)指针与一维数组 i.数组指针(指向数组元素的指针) 类型 *指针变量名: 指针定义完成后要初始化(不想让指向任何内容,=0,=NULL) int a[10]; int *p = ...
- C语言杂谈:指针与数组 (上) (转)
转自:http://blog.jobbole.com/86400/ 介绍 1> 指针定义:指针是保存变量地址的变量. 2> 本文重点 >> 指针与数组之间的关系 >> ...
- 国2c语言中指针与数组的赋值运算,C语言到汇编-指针与数组2
本章剩下的内容主要有: 1.指针与数组 & 地址算术运算 2.字符指针与函数 3.多维数组 & 指针与多维数组 4.指向函数的指针 每个内容举一个例子,第一个例子: int a[10] ...
- 【C 语言】数组与指针操作 ( 数组符号 [] 与 指针 * 符号 的 联系 与 区别 | 数组符号 [] 与 指针 * 符号 使用效果 基本等价 | 数组首地址 与 指针 本质区别 )
文章目录 前言 一.数组符号 [] 与 指针 * 符号 使用效果 基本等价 二.数组首地址 与 指针 本质区别 前言 参考 [C 语言]指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方 ...
- 【C 语言】指针 与 数组 ( 指针 | 数组 | 指针运算 | 数组访问方式 | 字符串 | 指针数组 | 数组指针 | 多维数组 | 多维指针 | 数组参数 | 函数指针 | 复杂指针解读)
相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...
- 指针和数组的区别是什么?
指针和数组的区别是什么? 其实数组是一个地址,指针则是指向地址的地址. 举个例子: char array[10]; char *pt; pt=array; char array[10];的含义是,在内 ...
- 指针数组、数组指针、数组的区别与联系
指针数组.数组指针.数组的区别与联系! 一:基本定义 1.指针数组 char *arr[4] = {"hello", "world", "shannx ...
最新文章
- linux通过串口读取文件,Linux 串口读写(二)
- BZOJ3239 Discrete Logging
- .net框架读书笔记---虚方法
- Nodejs之view中的视图模板之——EJS模板语言,快速入门
- 利达主机联网接线端子_拾遗●接线端子或连接器(1)
- 法拉科机器人编程软件_发那科机器人编写简单的程序教程
- HFSS - GSM 900 和 DCS 1800 双频PIFA天线的设计与仿真
- [转载]使用 Abbot 框架自动化测试 Eclipse 插件的用户界面,第 2 部分
- ps太卡怎么办?几步帮您解决问题
- 华为p9总显示切换服务器中,怎么更改华为p9的多任务切换 | 手游网游页游攻略大全...
- uygurqa输入法android,uygurqa维语输入法
- 【C语言编程练习】20050. 计算存款利息
- 【可穿戴技术】相关资料
- Python进行Excel数据统计
- 【XSY2515】管道(pipe)(最小生成树+倍增lca)
- 端到端的图像压缩----《Variational Image Compression With A Scale Hyperprior》论文笔记
- gitlab编译安装史----虽败犹荣
- i5 11320h和r5 5600u参数对比选哪个好
- jQuery重定向如何跳转到另一个网页
- 日冕物质抛射检测matlab,中国科学技术大学 日冕物质抛射研究取得重要进展