c指针笔记---各类型

C 指针学习笔记与理解

  1. 什么是指针?
    学习指针之前,先要理解什么是指针。
    所有的数据都要储存在内存中,需要用时,再到内存中取出。在取出数据之前,如何确保计算机能够找到数据在内存块中的对应的位置?
    如果将内存块分成一块一块的空间,并且对每一块的内存给上编号,这样每一个数据都有对应的地址了,计算机就可以根据数据的地址准确的寻找到这个数据,这个内存的地址就是指针,而用于储存这个指针的变量就是指针变量,通过解引用操作即可取出地址中的数据
    在这里插入图片描述

    如何定义一个指针变量

int main()
{
	int a = 1;
	int* a_p = &a;	// 在类型后加*可定义指针变量,而通过&可取出数据的地址

	printf("a = %d\n", a);
	printf("*a_p = %d\n", *a_p);	// 通过*解引用操作,取出地址的数据
	return 0;
}

运行结果

a = 1
*a_p = 1

获取数据除了可以直接使用变量,还可以通过指针变量进行操作
只指针变量的大小在不同的系统上也有不同的体现,常见的32位系统上,指针变量大小为4byte,64位系统上指针变量大小则为8byte;原因是32位系统,通过32根地址总线去处理数据,即,可以产生2^32个地址编号,也就是说有4byte的数据长度,而64位系统的数据宽度就有8byte。因此,指针变量的大小与系统息息相关。

2. 指针在内存中如何表示?
指针在内存中以16进制进行展示,实际上还是2进制的。
还是以上面的代码为例,可以看到a的地址编号事宜十六进制的形式进行表示的。虽然本质上是二进制,但若是以二进制展示,则会显得太长了
在这里插入图片描述

3. 指针的类型
指针与常规的数据一样同样存在数据类型,但是指针不管是什么类型的指针,它的大小都是不变的,指针大小只和系统有关;但是不同类型的指针决定他能操作的空间大小不同

#include <stdio.h>
#include <math.h>

int main()
{
	int a = pow(2, 9);
	int b = pow(2, 9);
	char* pa = &a;
	int* pb = &b;

	*pa = 0;
	*pb = 0;

	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}

512
0

为什么会产生这样的结果?
原因很简单,虽然char* 和 int* 类型的指针大小虽然都相同,但是它能够操作的空间不同,什么意思?用下面这张图来说明一下

整型数据的大小位4byte,也就是说占据4个内存块儿(内存的最小单位为byte), 那么一个指针如何管理int*型的指针如何操作这个数据,答案肯定是需要占用4个地址的编号,即1-4,只有同时拥有这4个编号,才能对这4个内存块儿进行管理,就相当于不属于自己的房子,跑去住可能会被别人锤一样的道理。

在这里插入图片描述

知道了指针的地址操作宽度不同,那么理解为什么这个结果就不难了,int类型的指针可以操作1-4的内存块儿,而char的指针便只能操作5编号的内存块。
那么从二进制进行说明

00000000‬ 00000000‬ 00000010 00000000‬ //512
pb = 0 时,这四个内存块儿0的二进制分别塞进四个内存块儿中得到了
00000000‬ 00000000‬ 00000000 00000000
再来看看pa
pa = 0时,
00000000‬ 00000000‬ 00000000 00000000原本需要四个内存块才能装得下
现在它只能放一个内存块儿,即它从低地址开始塞进去,高于第8位(从右数)以后的数据就全部被截断了
00000000‬ 00000000‬ 00000010 00000000‬,这8位塞进来相当于没变

在这里插入图片描述

那么将pa修改成1,最后发现结果变成513了
00000000‬ 00000000‬ 00000010 00000001‬,原因就是这样了
在编号上又怎么体现,我们可以看地址+1得到的结果
在这里插入图片描述

可以看到,pa+1以后大于pa 1, 而pb+1则大于pb 4,刚好是char类型的大小,和int类型的大小,花了一大堆说明这个原因,则是在不同的应用场景中,使用合理就无法达到预期的效果
而关于指针的定义则是数据类型加上*

如:int*, char*, float*

当然并不是说int数据就要用int*,具体看引用场景的

4. 指针的使用注意问题
关于指针的定义

类型名+ * +指针变量名
如int* a ;
由于指针变量储存的是变量的地址,通过*即可操作内存块中的变量
printf("%d", *pa);

关于指针的使用时,需要注意野指针
什么是野指针?从名字来观察不难理解,野指针即是不可控的指针,这个不可控指的是没有明确的地址。
产生野指针的以下几个原因
指针变量未初始化

int main()
{
int* b;
// 由于指针变量定义没有初始化,所以这时候内存给指针变量随机分配了一个值
printf("%p\n", b);
return 0;
}

结果为报错

C4700 使用了未初始化的局部变量

解决办法

int <stdio.h>
int main()
{
	int a = 1;
	int* p = &a;	// 首先给指针初始化
	// 如果确实需要用到指针,但是暂时又不给指针变量的明确地址时,就可以用到		NULL
	int* p1 = NULL;
	return 0;
}

指针越界
使用指针超过了它的界定范围进行操作,就会造成指针越界成为野指针,上代码

#include <stdio.h>

int main()
{
	char a = 1;
	char* pa = &a;
	
	*(pa + 1) = 3;
	printf("%p\n", pa);	// 指针指向a的地址
	printf("%p\n", pa + 1);	// 但是pa + 1后,就越过了a的地址,指向了另一个未知的地址
	return 0;
}

当程序跑起来就出现了错误
在这里插入图片描述

虽然pa+1的地址时可预测在哪里的,但是这一块空间并没有进行分配,相当于是一块儿没有开发权的土地,使用是不合理的。
另外在使用数组的时候,也要切不能让指针越界

int main()
{
	int a[5] = {1, 2, 3, 4, 5};
	int* pa = a;
	int i = 0;

	for (i = 0; i < 6; i++)
	{
		printf("%d\n", *(pa + i));	// 当pa + 5时,指针变越过了数组的范围
	}
	// 展示数组的地址
	// 五个元素的地址假设为
	// 0, 4, 8, 12, 16
	// a的首元素地址为0,当pa+5时,就相当于0 + 4 * 5 = 20, 而20的空间时没有开发权的,所以越界了,因此在使用指针时
	// 要注意指针不能越界
	return 0;
}

使用释放的变量地址
这个理解起来也并不是很难,还是老样子,上代码

#include <stdio.h>

int* fun()
{
	int x = 4;
	printf("%p\n", &x);

	return &x;
}

int main()
{
	int* a = NULL;

	a = fun();

	printf("%d\n", *a);
	printf("%p\n", a);
	return 0;
}

虽然运行结果没有报错,但是x却在fun()函数结束后,生命周期结束,内存也重新处于未分配的状态,这块空间除了a能够指向它,却并不存在任何一个变量中
在这里插入图片描述

  1. 一级指针与二级指针

    一级指针
    一级指针很好理解,上述例子的指针都是一级指针,理解为一级指针是指向某个变量,这个变量是除指针变量外的变量,即这个指针变量指向的地址直接指向另一个变量的内存中,如

int a = 0;
int* pa = &a;
pa就是直接指向a的内存

二级指针
二级指针则是指向指针的指针,什么意思?

int a;
int* pa = &a;
int** ppa = &pa;
printf("%p\n", a);
printf("%p\n", pa);
pa 指向的a,而ppa指向的则是pa的地址,a的地址为1, 那么pa = 1,它就可以通过地址编号1,找到a所在的地方,而ppa 则是储存的是pa的地址,ppa通过pa的地址找到pa,再在pa中找到a的地址。
运行结果
004FFE44
004FFE38

那么如何通过二级指针来使用操作a呢?使用两个**ppa就可以,理解为第一个*通过地址004FFE38,找到pa,第二个 *再通过地址 004FFE44找到a

int main()
{
	int a = 1;
	int* pa = &a;
	int** ppa = &pa;

	printf("%d\n", *pa);
	printf("%d\n", **ppa);
	return 0;
}

运行结果

1
1

当然也还有三级指针,n级指针,但是一般来说二级指针已经是足够了。

数组指针
数组指针是什么?数组指针是一个指针,它指向一个数组,关于它的定义和使用,先上一段代码

int main()
{
	int arr[4] = {1, 2, 3, 4};
	int (*p_arr)[4] = &arr;
	int i = 0;

	for (i = 0; i < 4; i++)
	{
		printf("%d\n", *(*p_arr + i));
	}
	return 0;
}

运行结果
1
2
3
4

int (p_arr)[4] = &arr; 在这一段代码与普通的指针定义大体上是相同的,由于[]的优先级高于 * 所以在进行定义的时候必须要加上() 确保在[] 之前,而数组指针类型就是 int ( * )[],那么为什么不是int ( * )[4] p_arr 这样呢?在数组指针定义是规定就是类型 + ( * 指针名)[]。
当然,若觉得不习惯也可以进行重命名

typedef int (* int_arr_p)[];	// typedef 定义数组指针重新的类型名也必须在()中
int a[] = {1, 2, 3, 4};
int_arr_p arrp = &a;	// 这样就顺眼许多了


另外在进行解引用之前,先说明几个点,数组名是首元素的地址,地址+1时不是横跨整个数组,而是到下一个元素的地址

int main()
{
	int arr[] = { 1, 2, 3, 4 };
	int* arr_p = &arr;
	int i = 0;
	
	for (i = 0; i < 4; i++)
	{
		printf("%d	", *(arr_p + i));
		printf("%p\n", arr_p + i);
	}
	return 0;
}

运行结果

1 00FFE00
2 00FFE04
3 00FFE08
4 00FFE0C

如果arr_p为整个数组的话,+1就应该为 00FFE10,因此,int*类型只是定义为整数的数组,所以+1就相当于把每个元素独立分开。
既然数组名为首元素地址,指针也是首元素地址,那么这种方式的指针是否能够像数组名一样使用[]而不使用 * 取出元素

int main()
{
	int arr[] = {1, 2, 3, 4};
	int* arr_p = &arr;
	int i = 0;

	for (i = 0; i < 4; i++)
	{
		printf("%d	", arr_p[i]);
	}
	return 0;
}

运行结果
在这里插入图片描述

逆向过来,把arr解引用是否又可行?

int main()
{
	int arr[] = {1, 2, 3, 4};
	int* arr_p = &arr;
	int i = 0;

	for (i = 0; i < 4; i++)
	{
		printf("%d	", *(arr + i));
	}
	return 0;
}

运行结果
在这里插入图片描述

结果也是可行的,因此可以得出一个结论,数组名本身就是一个指向首元素地址的元素类型的指针,它并不是横跨整个数组的,除开在&操作和sizeof求大小时表示的是整个数组

所以要想真正用一个指针表示整个数组,使用数组指针是真正能够体现的

int main()
{
	int arr[] = {1, 2, 3, 4};
	int(*arr_p)[4] = &arr;

	printf("%p\n", arr_p);
	printf("%p\n", arr_p + 1);
	printf("%p\n", arr);
	printf("%p\n", arr + 1);
	return 0;
}

运行结果
在这里插入图片描述

arr_p + 1 比arr 大16,恰好是一整个数组的到小。
讲完了数组指针和普通的指针表示数组的区别,再说说解引用为什么是

*(*arr_p)
// 将这段代码分成两部分来理解
// *arr_p相当于int* arr_pp = &arr中的arr_pp
// 将*(*arr_p) ==> *arr_pp	 

验证

int main()
{
	int arr[] = {1, 2, 3, 4};
	int(*arr_p)[4] = &arr;

	printf("%d\n", (*arr_p)[1]);
	printf("%d\n", *((*arr_p) + 1));	// ==> *(arr_pp + 1)
	return 0;
}

运行结果

2
2

指针数组
指针数组为一个数组,用于存放指针变量

int main()
{
	int a, b, c, d;
	int* a1 = &a;
	int* b1 = &b;
	int* c1 = &c;
	int* d1 = &d;
	int* arr[4] = { a1, b1, c1, d1 };
	int i;


	for (i = 0; i < 4; i++)
	{
		*(arr[i]) = i;
		printf("%d\n", *(*(arr + i)));
		printf("%d\n", *(arr[i]));	// 两种使用方式都可以用
	}
	return 0;
}

运行结果

0
0
1
1
2
2
3
3

指针数组与普通的数组并无特别大的区别,主要在于储存的元素为指针

函数指针

函数与数组一样有地址

include <stdio.h>

int fon()
{
	return 1;
}

int main()
{
	printf("%p\n", fon);
	return 0;
}

结果展示
在这里插入图片描述

既然函数拥有地址,函数也可以使用指针来调用函数
指向函数的指针就是函数指针了

#include <stdio.h>

int fon()
{
	printf("1\n");
	return 1;
}

int main()
{
	int(*fn)() = &fon;	// 函数指针的定义方法与数组指针并无特别大区别
	// 只是[]换成(), 前边的类型是函数的返回类型
	// ()中也可以写出类型
	// int (*fn)(void) = &fon;比如这样

	fn();	
	(*fn)();	// 两种方式均可以调用函数,但是需要注意()的优先级是高于*的
	return 0;
}

函数与数组相似,函数名也是地址,所以实际上函数也通过*进行调用,

#include <stdio.h>

int fon()
{
	printf("1\n");
	return 1;
}

int main()
{
	(*fon)();
	return 0;
}

运行结果

1

函数指针数组
像指针数组一样,函数也可以存放在数组中,用于存放函数指针的数组就是函数指针数组。
函数指针数组的定义方法
如何去定义一个函数指针数组的

int main()
{
	int (*fun)()[3]; // 或许我们会认为这样,但其实这样是错的
	int (*fun[3])();	// 正确定义方法其实是这样的
	return 0;
}

定义方法为 函数返回类型 + ( * 函数指针数组名[])()

函数指针数组必须是相同返回类型的函数才能储存。

int one()
{
	printf("1\n");
	return 1;
}

int two()
{
	printf("2\n");
	return 2;
}

int three()
{
	printf("3\n");
	return 3;
}

int main()
{
	int (*funs[3])() = {&one, &two, &three};
	int i = 0;

	for (i = 0; i < 3; i++)
	{
		printf("%d\n", (funs[i])());
	}
}

运算结果

1
2
3

在需要调用多个相同参数的函数时,使用函数指针数组代码的可维护性是很好的。

比如在计算器程序中

double add(double x, double y)
{
	printf("x + y = ");
	return x + y;
}

double sub(double x, double y)
{
	printf("x - y = ");
	return x - y;
}

double mul(double x, double y)
{
	printf("x * y = ");
	return x * y;
}

double divs(double x, double y)
{
	printf("x / y = ");
	return x / y;
}

int main()
{
	int input = 0;
	int x, y;
	double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};

	do
	{
		printf("*****************************\n");
		printf("**** 0. exit* 1. 加法********\n");
		printf("**** 2. 减法* 3. 乘法********\n");
		printf("*****4. 除法*****************\n");
		printf("*****************************\n");
		printf("请输入:");
		scanf("%d", &input);

		switch (input)
		{
		case 0:
				break;
		default:
			printf("\n请输入数:");
			scanf("%d%d", &x, &y);
			printf("%lf\n", (funs[input - 1])(x, y));	// 此处只需一行代码即可调用需要的函数
		}
	} while (input);

	return 0;
}

这就是经常说的转移表,转移表实际上就是一个函数指针数组,在相同返回类型,相同参数的函数调用时,为了提高代码的可维护性,使用函数指针数组减少代码数,不同的部分在函数内实现,使得代码不那么冗长,可维护性更好

double add(double x, double y)
{
	return x + y;
}

double sub(double x, double y)
{
	return x - y;
}

double mul(double x, double y)
{
	return x * y;
}

double divs(double x, double y)
{
	return x / y;
}

int main()
{
	double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};
	int input = 0;
	int x, y;

	do
	{
		printf("*****************************\n");
		printf("**** 0. exit* 1. 加法********\n");
		printf("**** 2. 减法* 3. 乘法********\n");
		printf("*****4. 除法*****************\n");
		printf("*****************************\n");
		printf("请输入:");
		scanf("%d", &input);

		switch(input)
		{
		case 0:
			break;
		case 1:
			printf("请输入两个数: ");
			scanf("%d%d", &x, &y);
			printf("\nx + y = %lf\n", (funs[input - 1])(x, y));
		case 2:
			printf("请输入两个数: ");
			scanf("%d%d", &x, &y);
			printf("\nx - y = %lf\n", (funs[input - 1])(x, y));
		case 3:
			printf("请输入两个数: ");
			scanf("%d%d", &x, &y);
			printf("\nx * y = %lf\n", (funs[input - 1])(x, y));
		case 4:
			printf("请输入两个数: ");
			scanf("%d%d", &x, &y);
			printf("\nx / y = %lf\n", (funs[input - 1])(x, y));
		default:
			printf("请输入正确的数\n");
		}
	}while(input)
	return 0;
}

如果单纯这样去运行,使用的函数一旦数量多了,就会使得代码十分的冗长,可维护性十分的差。

指向函数指针数组的指针
身为一门套娃语言,指向函数指针数组的指针也是十分正常的,指向函数指针数组的指针,是一个指针,这个指针指向的是函数指针数组,与数组指针有异曲同工之处。
指向函数指针数组的指针的定义方法

	double (*funs[4])(double, double) = { &add, &sub, &mul, &divs};
	double (*(*fp)[4])(double, double) = &funs;

( * fp)说明fp是一个指针,与[]结合,说明fp指向一个数组,最后一个 * 说明这个数组里面的元素是指针类型,最后面这个 * 与指针数组指针是类似的

	int a, b, c;
	int* arr1 = &a;
	int* arr2 = &b;
	int* arr3 = &c;
	int* arr_p[3] = { arr1, arr2, arr3 };
	int (*(*arr_pp)[3]) = &arr_p;	// 比较两段代码发现是相似的

	a = 1;
	b = 2;
	c = 3;

	printf("%d\n", *(*(*arr_pp)));
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值