一网打尽:指针和数组
Posted 二球悬铃木丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一网打尽:指针和数组相关的知识,希望对你有一定的参考价值。
系列文章目录
第一回 三十二个关键字 心性修持大道生
没看过第一回? 点这里
第二回 悟彻指针真妙理 归本数组合元神
前言
大家好!我们又见面了,今天更新了第二回的内容,一起来看看叭。
文章目录
提示:以下是本篇文章正文内容
一、指针
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];//式子1
int(*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)
你可以把一大群函数指针放进去。
总结
第二回至此结束,博主是小白一枚,讲解自有缺漏甚至错误,劳请斧正。
以上是关于一网打尽:指针和数组的主要内容,如果未能解决你的问题,请参考以下文章
干货 | Elasticsearch Nested 数组大小求解,一网打尽!
干货 | Elasticsearch Nested 数组大小求解,一网打尽!