C语言复习----------指针

一. 基本介绍:

1.1 内存:

内存是电脑上特别重要的存储器,计算机中所有的程序的运行都是在内存中进行的.
在这里插入图片描述

为了有效地使用内存,就把内存划分为一个个小的内存单元,每个内存单元的大小是 一个字节,为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该 内存单元的地址
在这里插入图片描述如果假设:一个内存单元是1bit
二进制数2的32次转换成10进制数后是4294967296
4294967296 bit
/8 = 536870912 byte
/1024 = 524288 kb
/1024 = 512 mb
/1024 = 0.5gb
可见当一个bit是一个内存单元时:计算机可管理的内存太小了
并且一个char 占一个字节 = 8 bit ------ 8个地址 离谱!!
那么一个内存单元是一个字节时
char 占一个字节 = 1type ----------1个地址 合理!!
最终,一个内存单元是一个字节(一个字节给一个编号),然后分配地址的*

举个例子:

# include<stdio.h>
int main() {
	int a = 10;
	return 0;
}

在这里插入图片描述在这里插入图片描述在这里插入图片描述内存中数据以二进制存储
如:10的二进制是00000000 00000000 00000000 00001010
转换为16进制: 00 00 00 0a
在内存中倒着展示:0a000000

1.2 指针:

指针就是地址,地址就是指针,存放指针的变量就是指针变量
在这里插入图片描述

# include<stdio.h>
int main() {
	int a = 10; //a在内存中要分配空间的   -4个字节
	printf("%p", &a);//%p是专门用来打印地址的,并且打印出的是4个字节中
					 //第一个字节的地址
	int* p = &a; //p是用来存放指针的,在c语言中p是指针变量
	//* 说明p是指针变量
	// int 说明p指向的对象是int 类型的
	*p = 20;//通过p访问a的地址并把20赋值给a,
	// *是解引用操作 *p就是通过a里边的地址,找到a
	pritnf("%d", a);
	return 0;
}

在这里插入图片描述在这里插入图片描述*p 解引用操作,通过访问p里面存放的地址访问p所指向的对象
p 就是p所指向的对象
p = 10;
指针的大小:

# include<stdio.h>
int main() {
	printf("%d\n", sizeof(char*));
	printf("%d\n", sizeof(int*));
	printf("%d\n", sizeof(short*));
	printf("%d\n", sizeof(long*));
	printf("%d\n", sizeof(long long*));
	printf("%d\n", sizeof(float*));
	printf("%d\n", sizeof(double*));
	return 0;
}

在这里插入图片描述64位计算机为8,32位计算机位4
两个十六进制位占1bite
一个十六进制位有4个二进制位
因为二进制要表示数字10时为1100,占用4个二进制位
指针的大小是相同的!
因为指针是用来存放地址的,指针需要多大空间,取决于 地址的存储需要多大空间,32位机器是 32个比特位组成的二进制序列作为一个地址,同理64位机器是 64个比特位组成的二进制序列作为一个地址
32位 32bit -4 type
64位 64bit -8 type

1.3 指针的意义

指针类型决定了指针在被解引用的时候访问几个字节
如果是int* 的指针,解引用访问4个字节
如果是char* 的指针, 接引用访问1个字节
可推广到其他类型

二. 指针初阶:

当出现如下代码时:

char* pc = (char*)&a;

我们知道char类型的指针在32位的机器中可以存储四个字节的数字,所以刚好可以储存&a
但是解引用时:

*pc = 0;

解引用只是访问一个字节,只能改变a中一个字节的值
在这里插入图片描述上面例子说明了指针类型虽然决定不了指针的空间大小,但是能决定指针在±1的时候跳过j几个字节,即字节的步长

注意当指针类型所决定的步长相等时,该两种类型不能混用,下面给出例子:

int * pi = &a;
float * pf = &a;

int float 步长都为4,解引用操作时能访问的字节也一致,但是不能混用,下面通过运行代码来解释:
运行* pi = 100;
在这里插入图片描述而当运行* pf = 100.0时:
在这里插入图片描述这两个赋值结果并不相同,原因是pi所指向的为int类型的数据,而pf所指向的为float类型的数据,这两种类型在存储数据时存储方式是不一样的,对内存的解读方式不一样使得两次赋值不一致.至于存储方式的区别,后面会出文章聊
这里需要注意的是指针变量所占的内存大小和指针所指向的空间大小是不一样的
double * pc;// pc是一个指针变量在32位机器中所占内存为4个字节,但是这个指针所能访问的空间大小为8个字节,这是因为pc是一个double类型的指针,访问的是double类型的数字,而double类型的数字所占内存大小就是八个字节,而指针变量所占内存大小为4个字节

2.1 野指针:

野指针是指指针指向位置不确定(随机的 不正确的 没有明确限制的)

2.1.1 成因:

  1. 没有初始化指针:
    在这里插入图片描述2. 指针越界访问:
    当p所指向的内容超出arr数组的范围时,p就是野指针.
    在这里插入图片描述3. 指针所指向的空间释放:
    具体会在动态分配内存那里具体阐述,这里先简单提一下
    t在这里插入图片描述test函数会将a的地址返回并赋值给p,但是出了test函数之后,由于a是一个局部变量,所以会被销毁,所谓销毁就是将这片内存空间还给操作系统了,所以p只能得到一个地址,即使能找到这片空间,但是这片空间已经不再是当前程序的空间了,属于非法访问,当这片空间的值没有被改变并且空间未被覆盖时,在main函数中printf输出时依然可以输出10,但是对这片空间的操作仅可如此,不可删除,增加,修改.使得p成为野指针,说到这里就又会想起那段无奈的感情经历,噫嘘唏 哎!
    在这里插入图片描述当程序跳出test函数时,test函数栈帧就会空出来在执行printf函数时会占用被释放的栈帧空间导致原来的a所在的空间被覆盖,所以在输出
    p的值时不为10,而是一个随机值

2.1.2 在一定程度上避免野指针的出现:

  1. 明确的对指针进行初始化,当不知道该初始化为何值时,可先赋值为NULL(空值)
    如此做的时候注意在计算机中,空地址是不能被访问的,即不能直接对空指针赋值,否则会报错写入访问权限冲突
    在这里插入图片描述2. 小心指针越界
  2. 指针指向空间释放及时置NULL
  3. 避免返回局部变量的地址
  4. 指针使用前检查有效性

2.2 指针的加减:

*vp++=0; => *vp=0 + vp++
(*vp)++: vp所指向的内容++
*- -vp: 先–vp然后解引用
指针-指针的绝对值得到的是指针和指针之间元素的个数
不是所有指针都可以相减,指向同一块空间(同一类型)的两个指针才能相减
指针+指针并无实际意义,有时二分法会用到(指针+指针)/2 但是完全可以用指针+整数代替
注意标准规定:允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较(即使二者均已越界)
在这里插入图片描述

2.3 const修饰的指针

一般的变量用const修饰之后就不能直接对其进行修改,但是可以通过指针解引用的方式来进行修改,但如果是const修饰的指针,会有以下两种情况:

  1. const 位于*前面
    const int *p <=> int const p
    const修饰
    p,意思是p所指向的变量的值不能通过解引用修改,但是p的指向可以更改,意思就是说p可以指向另一个变量(p本身的值可以改变),但是指向的对象不能更改
  2. const 位于*右面
    int * const p
    const 修饰p , p指向的对象是可以通过p来改变的,但是p的值不能被改变

2.4 二级指针:

int *pa: 表示pa是一个指针,int表示pa所指向的地址存着的数字是int类型的
int **ppa:第二个
说明pa是一个指针,而int *说明了ppa所指向的对象是int *类型
二级指针变量是用来存放一级指针变量的地址的

2.5 指针数组:

存放指针的数组就是指针数组
在这里插入图片描述指针数组可以用来模拟实现一个二维数组:
在这里插入图片描述在这里插入图片描述parr[i][j] <=> *(parr[i] + j)

2.3. 练习发现:

在这里插入图片描述由上机调试可知char* p = (char*)&b;并不会使b的类型由int变为char,只能用于初始化指针p,要使b类型发生变化,需要b = (char)b;

三. 指针进阶:

3.1 字符指针

在指针的类型中我们知道有一种指针类型为字符指针char*;
有两种使用方式:

char ch = 'w';
		char* pc = &ch;
		*pc = 'w';
char* p = "abcdef";
printf("%s\n", p);
return 0;

在这里插入图片描述需要注意的是第二种使用方式中,并非是将字符串储存到了指针里面,p指向的对象仅为a

3.2 指针数组

上面已经介绍了数组指针,这里稍微复习一下
指针数组是一个存放指针的数组

	int* arr1[10];//整形指针的数组
	char *arr2[4];//一级字符指针的数组
	char** arr3[5];//二级字符指针的数组

指针数组可以按照二维数组的方式访问元素,但是指针数组不是二维数组,二维数组的每个元素是连续存放的,但是指针数组不是

3.3 数组指针

3.3.1 数组指针介绍

数组指针是指针,我们已经熟悉:
整形指针:int* p;是能够指向整形数据的指针
浮点型指针:float* p;是能够指向浮点型数据的指针
那么数组指针就应该是:能够指向数组的指针
例如:
int (*p2)[10];
p2是一个数组指针,p2可以指向一个数组,该数组有10个元素,每个元素是一个int类型

解释:
int (* p)[10];
p先于结合,说明p是一个指针变量,然后指向一个大小为10个整型的数组,所以p是一个指针,指向一个数组,叫做数组指针
这里要注意,[]的优先级高于
号,所以加上()

在这里插入图片描述

int arr[10] = { 0 };
	int* p = arr;
	int(*p1)[10] = &arr;

注意,数组指针和二级指针没有任何关系,二级指针是用来存放一级指针变量的地址的

	char* arr[5] = { 0 };
	//arr是指针数组,存放的元素是char*类型
	char* (*p)[5] = &arr;
	//p是整个数组的地址,*p其实就相当于数组名(整个数组),数组名又是首元素地址
	//所以*p本质上是数组首元素的地址

注意,数组指针中括号中的数字不可省略

3.3.2 数组指针的常见用法

用于传参:

void print2(int(*p)[5], int r, int c) {
	int i = 0;
	for (i = 0; i < r; i++) {
		int j = 0;
		for (j = 0; j < c; j++) {
			printf("%d	", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main() {
	int arr[3][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
	print2(arr, 3, 5);
	system("PAUSE");
	return 0;
}

在这里插入图片描述在这里插入图片描述*(*(p + i) + j) <=> arr[i][j]

int (* p)[5];
p的类型是:int (*)[5]
p是指向一个整形数组的,数组5个元素 int[5]
p + 1 ->跳过一个5个int元素的数组
*(p + 1)含义是第二行整一行的地址,表现为第二行首元素地址,数组指针类型加1跳过的是一个数组,此处数组有五个元素

那么问题来了
int ( *parr3[10])[5];表示什么意思呢
首先先把parr3[10]去掉,得到int (*)[5]我们知道int (*)[5]是一个类型,进而就知道是一个数组指针类型,再将parr3[10]加上去就知道这是一个存放数组指针的数组
在这里插入图片描述parr3是一个数组名,这个数组有10个元素,数组的类型是数组指针,即数组的每个元素都是数组指针,所以parr3是一个存放数组指针的数组

3.4 数组参数和指针参数

3.4.1 一位数组传参

在这里插入图片描述

3.4.2 二维数组传参

在这里插入图片描述形参部分该如何设计,取决于要传过去的是什么,传数组名要分析数组名具体表示的是什么,看他代表的是什么样的地址,再考虑应该用什么样的指针接收

3.4.3 一级指针传参

在这里插入图片描述

3.4.4 二级指针传参

在这里插入图片描述

3.5 函数指针

3.5.1 介绍

顾名思义,函数指针就是指向函数的指针
& 数组名可取出数组的首地址,那么
& 函数名也可取出函数的地址
在这里插入图片描述

由图可知,类似于数组不仅&函数名可以得到函数的地址,而且输出&Add与直接输出Add结果完全相同,这一点与数组也完全一致

那么如何定义一个函数指针呢?

int (*pf)(int, int) = &Add;

让我们仔细解读解读
首先*pf指明了pf是一个指针
其次()说明了指针指向的是一个地址,括号里面和函数的参数相一致
前面的int和函数的返回类型相一致

那么函数指针有什么有用呢?
可以通过解引用函数指针来调用函数

int ret = (*pf)(2, 3);

在这里插入图片描述补充:

//以下这几种写法相互等价
int ret = (*pf)(3, 2);
int ret = pf(3, 2);
对函数指针进行解引用其实没有什么实际意义,这里加上*是为了便于理解
int ret = (*Add)(3, 2);
int ret = Add(3, 2);

函数指针可以实现将函数作为另一个函数的参数从而实现函数的链式调用

int Add(int x, int y) {
	return x + y;
}
void calc(int (*pf)(int, int)) {
	int a = 3;
	int b = 5;
	int ret = pf(a, b);
	printf("%d\n", ret);
}
int main()
{
	//对于函数来说,&函数名和函数名都是函数的地址
	/*int (*pf)(int, int) = &Add;
	int ret = (*pf)(3, 2);
	printf("%d", ret);*/
	calc(Add);
	system("PAUSE");
	return 0;
}

在这里插入图片描述

3.5.2 函数指针用途

结合函数指针数组,可以先看函数指针数组,之后结合来谈原理

3.6 函数指针数组

再研究函数指针数组前,先分析两段代码:

int main() {
	(*(void(*)() 0)();
	system("PAUSE");
	return 0;
}

解读:

  1. 突破口在于0,0是什么角色,可以看到0前面是void (*)()的形式,如果是void (*p)() 那么就是说p是函数指针,那么void (*)()就可以理解为一个函数指针类型
  2. 0是一个整数,0前面加了一个类型,类比(float)10就可以理解这里是在对0进行强制类型转换,转换成为一个函数指针,那么此时0就成为了一个函数的地址,所以(void(*)() 0就连在一起就可以理解成一个函数的地址,意思是在0地址中放一个函数,函数没有参数并且返回类型为void

以上两步就讲0强制类型转换为:无参,返回类型是void的函数的地址

  1. 那么(*函数地址)()(*(void(*)() 0)()就可以理解为是在调用0地址中的函数,因为0地址中的函数是没有参数的,所以最右边的括号里也为空

第三步调用0地址处的这个函数

  1. 所以以上代码是一次函数调用,调用的是0作为地址处的函数

以上内容出自<<C陷阱与缺陷>>第二章,如果想要电子版可以私信我

在这里插入图片描述同样出自<<C陷阱与缺陷>>还有以下代码:

int main() {
	/*(*(void(*)() 0)();*/
	void (*signal(int, void(*)(int)))(int);
	system("PAUSE");
	return 0;
}

解读:

  1. 首先分析signal, signal没有和前面的*加括号,所以肯定是和后面的括号相结合,这就说明signal是一个函数名, 再看括号中是两个类型,一个整数类型一个函数指针类型,现在看来signal(int, void(*)(int))既不是函数调用,也不是函数声明,因为函数调用需要括号里面是变量,而函数声明需要在signal(int, void(*)(int))加上一个类型,所以这里既不能算调用也不能算声明
  2. 上面分析了signal(int, void(*)(int)),先将signal(int, void(*)(int))搁置剩下部分就是void(*)int,发现恰好void(*)int就是一个类型,而且是函数指针类型

那么结合1和2就可以得出signal是一个函数,函数返回类型也是一个函数指针类型,这么看来这句代码就是一个函数声明,声明的signal函数的第一个参数的类型是int,第二个参数的类型是函数指针,该函数指针指向的函数参数是int,返回类型是void,signal函数的返回类型也是一个函数指针,该函数指针指向的函数参数是int,返回类型是void

一个简单的功能却需要如此复杂难懂的代码,用typedef可以简化代码
typedef,我们知道typedef可以重命名,类似于typedef unsigned int uint,但是不同于typedef unsigned int uint的是函数指针的重命名不能typedef void (*)(int) pf_t,这样的语法会报错,需typedef void (*pf_t)(int),那么代码就可以写成

tf_t signal(int, fp_t);

3.7 指向函数指针数组的指针

3.8 回调函数

3.9 指针和数组题解析


指针部分就此完结,江湖茫茫,咱有问题评论区见!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

With Order @!147

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值