一网打尽:指针和数组

系列文章目录

第一回  三十二个关键字  心性修持大道生
没看过第一回? 点这里
第二回  悟彻指针真妙理  归本数组合元神


前言

大家好!我们又见面了,今天更新了第二回的内容,一起来看看叭。

在这里插入图片描述


提示:以下是本篇文章正文内容

一、指针

1.1 指针的内存布局

先看下面的例子:

int a = 5int *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)

你可以把一大群函数指针放进去。


总结

第二回至此结束,博主是小白一枚,讲解自有缺漏甚至错误,劳请斧正。

猜你喜欢

转载自blog.csdn.net/m0_63742310/article/details/124182860