【C语言深度解刨】指针与数组(全)

时间:2021-04-08 01:26:31

前言

基本目标

  • 为什么要有指针?
  • 指针与指针变量的区别?
  • 指针与数组的区别?

一.指针

1.指针的认识

  1. 想象你在这样楼里的一个屋子
    【C语言深度解刨】指针与数组(全)
  2. 前提条件
  • 所有屋子没有门牌号
  • 你不知道是自己所处位置——楼层
  • 这座楼密封且隔音很好
  • 你只能呆在屋子里,不能移动。
  1. 给你的X朋友发信息——来找我玩吧!

问题:你的X朋友怎么找到你呢?1

代入计算机内存

  1. 作为程序员的你想让修改一个变量。
  2. 假如:计算机不知道这个变量在哪?
  3. 计算机只能遍历查找——效率极低!

因此:楼房的门牌号就好比内存变量的标识——地址,地址(门牌号)可极大地提高寻找变量(屋子)的效率!

补充

  • 计算机一般使用的最小存储单元——字节
  1. 4GB=212MB=222KB=232 byte
  2. 8bite = 1byte,1bite存储0/1
  3. 因此:区分4GB的每一byte至少需要32位数(二进制)——32bite来存储,即4字节.

疑问:地址都要存储呢?2
【C语言深度解刨】指针与数组(全)
简单的理解:CPU内做了特殊的处理,当内存的指定位置的变量向CPU传递信息(电信号)时,CPU会将此电信号转换为数字信号(此变量的地址),通过地址总线再返还给内存。

  • 为什么要有指针?

2.指针与指针变量

值探究的维度

  • 值属性:变量所存内容
  • 类属性:值的类型
  • 空间属性:有无空间存储

常量与变量

  • 常量只具备值属性不具备空间属性
  • 变量具备值属性,也具备空间属性

代入:指针是一个常量,指针变量是变量,里面存放的值是指针
例:

#include<stdio.h>
int main()
{
	int a = 10;
	int* p = &a;
	
	*p = 9;
	 p = NULL;
	printf("%d\n", a);
	
	*&a = 12;
	//相当于a = 12;
	int b = *p;
	//等于int b = a;
	
	printf("%d\n", a);
	return 0;
}

代码分析图:
【C语言深度解刨】指针与数组(全)
因此:

  • 指针变量不完全等同与指针
  • 指针是地址——值
  • 指针变量是用来保存地址的
  • 对指针变量解引用使用的是其内容——地址/指针
  • 指针变量(单独)当左值,使用的是指针变量的空间。
  • 指针与指针变量的区别

3.指针的强转

来看一段有意思的代码:

int main()
{
	int* p = NULL;
	p = (int*)&p;
	*p = (int)p;
	printf("%p %p", (int*)*p, p);
	return 0;
}
  • (int*)&p,把p的地址取出来(二级指针),强转为一级指针,从而两边类型相同。
  • *p = (int)p,把p的类型转换为整形,赋值给*p——类型为int,两边类型相同。
  • %p只能打印指针类型的值,否则会报警告!

再看一段雷同的代码:

#include<stddef.h>
int main()
{

	float* p = NULL;
	p = (float*)&p;
	*p = (float)p;
	printf("%p %p", (float*)*p, p);
	return 0;
}

结论:报错!
【C语言深度解刨】指针与数组(全)
这就好比:本就跟你是两个世界的人,明知不可能还要骗自己。但结果不会欺骗自己。
指针本质上存的是一个32位数(整形家族),无法转换成浮点数(浮点家族),两个世界的,不可能进行转换!

  • 指针类型不能强转为浮点数!

4.void指针

  • 用于接收任意类型的同级指针
  • void指针是不能进行解引用的(Vs2019)
  • 通常搭配着强转进行使用
  • 主要在通用类型的函数中使用——qsort

5.空指针

  • NULL是一个宏
  • 所在的头文件——stddef.h
  • 我们平常使用的头文件stdio.h包含的头文件correct.h中也定义了NULL
  • #define NULL ((void *)0)——将0强制转换为void*类型
  • void*类型是不能进行解引用的!——*(NULL)会直接从语法上报错
  • 补充:*((char*)0)=1;不会从语法上报错,但代码会崩溃!

正常退出:
【C语言深度解刨】指针与数组(全)
异常退出:
【C语言深度解刨】指针与数组(全)

6.多级指针

  • 指针变量也是有地址的,因此指针变量也能被其更高级的指针修改!
  • 修改多级指针指向的较低级的指针或低级指针指向的内容

阅读代码:
这段代码是不能正常执行的!我们只需认识每句代码的意思即可!
每句代码思考:

  1. 使用的是变量的左值还是右值
  2. 修改的内容

int main()
{
	int a = 10;
	int* p = &a;
	int** pp = &p;
	p = 100; //1
	*p = 100; //2
	pp = 100; //3
	*pp = 100;//4
	**pp = 100;//5
	return 0;
}
  1. 左值,一级指针变量的空间
  2. 右值,一级指针变量所指向的内容,整形变量的空间
  3. 左值,二级变量的空间
  4. 右值,二级指针变量所指向的内容,一级指针变量的空间
  5. 右值,二级指针变量指向的内容,一级指针变量的内容,整形变量的空间

7.数组指针

  • 存放数组的指针
    我们先要知道数组的类型是什么?
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10};
	return 0;
}
  1. 将变量名去掉就是其类型——int[10]
  2. &arr取出整个元素的类型
  3. 指针说明:其类型int[10]
  4. 指针变量名效仿数组的命名
  5. 我们在不知道语法的情况下应该写成:int *p[10]
  6. 但是[ ]的优先级比 * 高因此应加括号括起来
  7. 因此正确写法为:int(*p)[10]

8.函数指针

说明:平常我们都只调用函数,好像没接触过函数指针。
下面我们从函数说起!

#include <stdio.h>
void fun()
{
	printf("hello\n");
}
int main()
{
	fun();//这是一个简单的调用
	//这一个语句可以拆分成两部分fun和()
	//fun是函数,()是函数调用操作符
	//那fun呢?
	fun;
	//&fun呢?
	&fun;
	return 0;
}
  • 这里的fun可以当做变量进行理解
  • fun是变量,那就可以取地址
  • 那如何表示&fun的类型呢?

我们想描述一个指针类型,首先得描述其独特之处
函数的独特之处:

  • 函数的返回值类型
  • 函数的参数
    因此我们可以这样写:
//void/*返回类型*/ ()/*参数*/ *p = &fun;//这样写看起来是正确的,但跟语法还差了一点
//void fun ();//如果把fun看成变量的话,那指针变量的位置也应该出现在fun处
//因此:void *p()= &fun;离目标还差一点——优先级:()的优先级高于*,要说明p为指针得
//先把p与*号集合起来
//所以正确且符合语法的规范
void (*p)()=&fun;

实际上:
fun等同于&fun
因此:这样写也对

void (*p)()=fun;

函数指针的类型呢?
将变量名去掉就是指针类型。
so此函数指针的类型为:

void (*)()

如何理解:
【C语言深度解刨】指针与数组(全)
提高:

(*(void(*)())0))() ;//如何理解此代码

【C语言深度解刨】指针与数组(全)
拆解代码:

  1. (void(*)())0
    这是将0值强制类型转换为函数指针,函数指针类型上文提及
  2. (void(*)())0看做一个函数指针p
  3. 代换进我们要理解的代码:*(*(void(*)())0))()
  4. 问题转换理解:(* p )(),很显然这是函数调用。
  5. 结论:这是将0值强制类型转化为函数指针,再对函数指针进行解引用,对0值处的函数进行调用!
void(*signal(int,void(*)(int)))(int);//如何理解这样一段代码?

要理解这样一段代码我们先要理解函数指针作为返回值的函数声明该如何表示?
假设

  • 一个函数(fun)无参
  • 函数返回类型为void(*)(int)

由于函数组成:
【C语言深度解刨】指针与数组(全)
按照一般思路我们可能会写出:

void(*)(int)fun();
//甚至会写出函数指针当做函数声明
void(*fun)();
  • 函数体是一个整体——函数声明的函数体不带实现代码
  • 因此:函数指针作为返回值应该把函数体放在函数指针的*号旁边
  • 因此应这样写:void(*fun());

然后我们还要理解函数指针作为参数的函数声明该如何表示?

  • 一个函数(fun2)参数为:int和void(*)(int)
  • 函数返回类型为void
    按照我们想的应该这样写:void fun2(int,void(*)(int));
    没错,这样写是对的
    最后我们把这里的fun2的返回类型换成:void(*)(int)
    按照上面的fun的思路:把函数体放在*的右边就变成了:
    void(*fun2(int,void(*)(int)))(int);
    最后函数名一换就跟我们要理解的函数一模一样

9.函数指针数组的指针

void (*((*p)[3]))() = { NULL };
  1. 从里往外看
  2. p先与*结合说明是指针
  3. 再与外面的[3]结合说明:指针存放的是数组
  4. 再与void(*)()结合说明:数组里面的类型是函数指针
  5. 函数指针指向的内容为返回值为void ,参数:无

10.野指针

  • 未经允许就访问的指针
  • 可能能访问,也可能不能访问
  • 访问的空间的内容可以修改,也可能不让修改。
  • 野指针不让访问与修改深究是操作系统的知识。


不能访问:

#include<stdio.h>
int main()
{
	int* p = (int*)1;
	printf("%d", *p);
	return 0;
}

结果:
【C语言深度解刨】指针与数组(全)

不可被修改:

#include<stdio.h>
int main()
{
	int* p = (int*)1;
	*p = 1;
	return 0;
}

结果:
【C语言深度解刨】指针与数组(全)

11.指针的运算

  • 指针存的数本质上是一个整形大小的数据,可进行比较。
  • 指针数据是一个大于等于0的32位数据
  • 指针加1加的是所指类型的大小
  • 指针减指针指的是其相邻元素的个数(前提:类型相同,并且是在数组中
#include<stdio.h>
int main()
{

	int arr[10] = { 0 };
	int* p = arr;
	int* p1 = &arr[9];
	printf("%d\n", p1 - p);

	return 0;
}
  1. p存的是第一个元素的地址
  2. p1存的是第10个元素的地址
  3. p1与p之间存在9个元素
  4. 因此:p1-p = 9

图解:
【C语言深度解刨】指针与数组(全)

二.数组

1.数组传参

  • 数组传参是要降维成其元素的指针——例:二维数组传参传的是一维数组的地址
  • 数组传参发生降维指针使用,提高了空间的利用率
  • 降维成指针使[ ]与*()的使用相等,降低了语言使用的难度。
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int arr[9])//ok
{}
void test(int* arr)//ok
{}
void test2(int* arr[20])//ok
{}
void test2(int* arr[0])//ok
{}
void test2(int** arr)//ok
{}
int main()
{

	int arr[10] = { 0 };
	test(arr);

	int* arr2[20] = { 0 };
	test2(arr2);
	return 0;
}
  1. 这里的函数参数int arr[ ]本质上就是int*arr。
    说明:[]里面的数,编译器不做解释,因此不写也行!写了就要写大于0的整数。
  2. int arr[10];的元素为 int 其指针为 int* 因此参数为 int*
  3. int *arr[10]:其元素类型为int*,所以传int*的指针——int**

2.多维数组

  • 数组的开辟在内存中是连续的
  • 因此:多维数组在内存中可以看成一维数组
    二维数组证明:
#include<stdio.h>
int main()
{
	char a[3][4] = { 0 };
	for (int i = 0; i < 3; i++) 
	{
		for (int j = 0; j < 4; j++) 
		{
			printf("a[%d][%d] : %p\n", i, j, &a[i][j]);
		}
	} 
	return 0;
}

【C语言深度解刨】指针与数组(全)

  • 二维数组的元素是一维数组,传二维数组因此要用数组指针接收参数
  • 二维数组的数组名作为函数参数,意为二维数组第一个元素的地址,第一行的地址。
  • 函数参数写成数组形式,列标不可省去!

三维数组证明:

#include<stdio.h>
int main()
{
	char a[3][4][5] = { 0 };
	for (int i = 0; i < 3; i++) 
	{
		for (int j = 0; j < 4; j++) 
		{
			for (int k = 0; k < 5; k++)
			{
				printf("a[%d][%d][%d] : %p\n", i, j,k, &a[i][j][k]);
			}
		}
	} 
	return 0;
}

【C语言深度解刨】指针与数组(全)

  • 三维数组的元素是二维数组,因此传三维数组,要用二维数组的指针进行接收。
  • 三维数组的数组名作为函数参数,代表着三维数组的第一个元素,也就是三维数组的第一个二维数组的地址。
  • 函数参数写成数组,第一个可省略,第二个参数和第三个参数不可省略。

  1. 通过一间间房间的挨个查找↩︎

  2. 不要,内存会根据变量位置,自动生成变量的地址,再使用。 ↩︎