指针知识点总结

1.内存与地址

计算机上CPU在处理数据时,需要的数据都是在内存中读取的,处理后的得数据也会放回内存中。
内存其实是由一个个内存单元组成,每个内存单元占1字节(Byte)
1个字节空间可以放8个比特位(bit)
一个比特位可以存放一个2进制的0/1.

为了计算机能够快速找到对应的内存空间,每个内存单元都有一个编号,我们把内存单元的编号称为地址,C语言给地址了一个新名字:指针

即:
内存单元的编号=地址=指针

1.1内存编址

CPU访问内存中的某个字节空间,要知道他在内存中的位置,然而内存中的字节空间十分多,这就要给内存进行编址

CPU与内存之间存在大量数据交互,这时候就需要用(地址总线)将其联系起来。
在这里插入图片描述
这里可以简单理解,32位机器有32根地址总线,每根线有两态,表示为0/1,(电脉冲有无),这样一根线就表示2种含义,32根线就有2^32种含义,每种含义都代表一个地址。

当地址信息被下达给内存,在内存上,找到该地址对应的数据,通过数据总线传入CPU寄存器
(仅了解)

2.指针变量与地址

2.1取地址操作符&

在创建变量时,会向内存申请储存空间,
比如一个整型变量,向内存申请4个字节,而每个字节都有对应的地址

#include<stdio.h>
int main()
{
	int a = 10;
	printf("%p", &a);
	return 0;
}

对于此代码,当我们调试后,能够得到整型a所占的4个字节的地址

1.0x006FFD70
2.0x006FFD71
3.0x006FFD72
4.0x006FFD73

如果我们想要得到a的地址,就要使用&取地址操作符。
&a取出的地址是a所占字节的地址中较小的
即;x006FFD70
在这里插入图片描述
通常只要我们找到一个数据的第一个字节地址,就可以顺藤摸瓜访问到所有的地址

2.2指针变量与解引用

指针变量

指针变量说白了就是用来存放地址的。

#include<stdio.h>
int main()
{
	int a = 0;
	int* pa = &a;//取出a的地址,将他放在指针变量pa中
	return 0;
}

我们知道了pa是指针变量,那么int *是什么东西呢?
在这里插入图片描述
其实很容易理解,在这幅图中,a作为一个整型变量,他存放的内容是整数10。
在变量pa他存放的内容是整型a的地址,地址=指针,我们就可以理解,*是为了说明pa是一个存放指针的指针变量,而他储存的内容指向整型变量a,这就是int(pa指向的内容为int类型)

类比如果是一个字符的地址就要放在字符指针变量中

char c='b';
char *ch=&c;//将字符变量的地址放在字符指针变量中

解引用

在上面的例子中,我们知道pa中存放的是a的地址,那如果我们又想获取a的数值呢?
这里可以用到解引用操作符(
pa的意思是通过pa存放的地址。找到它所指向的空间
简单来说
pa=a,所以
pa=0;,就把a的值改成了0;

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

在这里插入图片描述

assert()断言

当提到了解引用,我们就不得不提到assert()断言
assert.h头文件定义了宏assert,这个宏就叫“断言”

assert(p!= NULL);

当指针变量p不是空指针时,程序继续运行,如果是空指针,就会报错
assert()接受一个表达式作为参数,表达式为真返回非零值,否则,返回0,assert就会报错,在标准错误流stderr中就会1.写入一个错误信息,显示未通过的表达式,以及表达式的文件名和行号。

那么回到最初,为什么我们说提到解引用就不得不提assert呢?
当我们在对一个指针变量进行解引用时,首先要确保这个指针变量不是空指针,因此为了增加的代码的可靠性,我们在解引用是可以在前面加上断言

#include<stdio.h>
#include<assert.h>
int main()
{
	int a = 10;
	int* p = &a;
	assert(p != NULL);//断言,确保p不是空指针
	printf("%d\n", *p);
	printf("%d\n", a);
	*p = 0;
	printf("%d\n", a);
	return 0;
}

当然assert的用处不仅如此
它不仅能完成上边1的功能,还有一个2.无需更改代码就可以实现开启或关闭assert的超绝机制。

#define NDEBUG
#include<assert.h>

那就是在头文件前面定义一个宏NDEBUG
如果确认程序没有问题,不必要在做断言,就定义这个宏,再重新编译的时候,编译器就会禁用掉文件中的assert()语句;相反如果程序又出现问题,那就注释掉这个宏,assert语句重新启用。

2.3const修饰指针变量

一般const修饰指针变量,可以放在 * 的左边,或者 * 的右边,两者意义不同。

const在 * 左边const在 * 的右边
修饰指针变量指向的内容,确保指针变量指向的内容不能通过指针改变,但指针变量本身可以更改修饰指针变量本身,确保指针变量本身的内容不能被更改,而他指向的内容可以通过指针更改
#include<stdio.h>
void test1()
{
	int n = 9;
	int m = 7;
	int * p = &n;
	printf("%p\n", p);
	*p = 7;//ok?--yes
	p = &m;//ok?--yes
	printf("%p\n", p);
	printf("%d\n", *p);
}
void test2()
{
	int n = 9;
	int m = 7;
	int const* p = &n;
	printf("%p\n", p);
	*p = 7;//ok?--no
	p = &m;//ok?--yes
	printf("%p\n", p);
	printf("%d\n", *p);
}
void test3()
{
	int n = 9;
	int m = 7;
	int*const p = &n;
	printf("%p\n", p);
	*p = 7;//ok?--yes
	p = &m;//ok?--no
	printf("%p\n", p);
	printf("%d\n", *p);
}

int main()
{
	test1();//无const
	test2();//在*左边
	test3();//在*右边
	return 0;
}

3.指针运算

1.指针±整数
2.指针-指针
3.指针的运算关系
4.void*

3.1指针±整数

数组在内存中是连续存放的,因此只要知道首元素的地址,就可以知道后面的所有元素
指针±的时候跳过的字节数,取决于指针指向内容的数据类型。
那么对于整型数组±跳过4个字节那就是跳过一个整型元素
,字符类型也是如此。
下边给一个int类型的数组
在这里插入图片描述

但对于*int p=&arr;取了整个数组的地址赋给指针变量,当他加1的时候,跳过的是整个数组

#include<stdio.h>
//遍历整个数组
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));//指针+-整数
	}
	return 0;
}

3.2指针-指针

指针-指针计算的是两个指针之间的元素个数

#include<stdio.h>
//模拟实现strlen
int my_strlen(char* p)
{
	char* m = p;
	while (*p!='\0')
	{
		p++;
	}
	return p - m;
}
int main()
{
	char p[] = "abcd";
	int ret=my_strlen(p);
	printf("%d\n", ret);
	return 0;
}

3.3指针的运算关系

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];
	while (p <( arr + sz))
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

3.4void*类型

在指针类型中有一种特殊的就是void类型,可以理解成无具体类型的指针,它可以接受任意类型的地址,但是因为它的类型是不确定的,所以他也无法进行解引用操作和指针±整数运算*

4.野指针

野指针就是指针指向的位置是不确定的,不可知的,随机的,无限制的

野指针的成因

有3点
1.指针未初始化

char *p;//未初始化,默认为随机值

2.指针越界访问

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 11; i++)
	{
		printf("%d ", *(p + i));//指针越界访问
	}
	return 0;
}

3.指针指向空间的释放

#include<stdio.h>
int* test()
{
	int n = 10;
	return &n;
}
int main()
{
	int* p = test();
	return 0;
}

这个代码就是在test函数中定义了局部变量n,当执行完test函数后,n所占用的空间就会被释放掉,,但是test却返回了n的地址,并由指针变量p来接受。明明n所在的内存空间已经被释放掉了呀,这样p就成为一个空指针了,这显然是不行的。

因此我们要避免返回局部变量的指针

如果需要返回指针指向的数据,应该通过动态分配内存(例如在test函数中使用malloc分配内存),就可以确保在函数返回后,数据仍然有效。

野指针的规避

1.对指针初始化
2.避免出现指针越界
3.在指针不用时,及时置为NULL,指针使用之前检查有效性(哈哈哈,这里就可以用到断言啦!)
4.避免返回局部变量的地址

5.指针的使用和传址调用

5.1传值调用和传址调用

我们写一个交换两个值的代码

#include<stdio.h>
void exchange(int x, int y)
{
	int tem = 0;
	tem=x;
	x = y;
	y = tem;
}
int main()
{
	int a = 5;
	int b = 0;
	printf("交换前a=%d b=%d\n", a, b);
	exchange(a, b);
	printf("交换后a=%d b=%d\n", a, b);
	return 0;
}

在这里插入图片描述
运行后,我们发现,a与b的值并没有被交换
为什么呢?
我们调试看看吧在这里插入图片描述
我们会发现在exchange函数中,x和y确实交换了数值呀,可为什么main函数中不交换呢?
仔细看我们会发现,exchange在接受参数时,内部创建了形参x,y但他们的地址与a,b的地址不同,相当于x,y是独立的空间,那他们变不变和a,b有什莫关系呢?a,b是不受影响的。自然当exchange函数调用结束后,a,b的值仍没被交换

在这个代码中,我们把a,b两个变量本身传给了函数。这就叫做传值调用

结论;实参传递给形参时,形参会单独创建一份临时空间来接受实参,对形参的修改不影响实参。

那么,要如何交换a,b呢,?
既然a,b 与x,y的地址不同,我们不妨试试把a,b的地址传给x,y,通过地址间接交换

#include<stdio.h>
void exchange(int* x, int* y)
{
	int tem = 0;
	tem = *x;
	*x = *y;
	*y = tem;
}
int main()
{
	int a = 5;
	int b = 0;
	printf("交换前a=%d b=%d\n", a, b);
	exchange(&a, &b);
	printf("交换后a=%d b=%d\n", a, b);
	return 0;
}

在这里插入图片描述
看!果然交换了,我们把变量地址传给函数的的调用方法就叫“传址调用

6.数组名和一维数组传参本质

6.1数组名

当我们想要获得一个数组的第一个元素地址时,可以用&arr[0];也可以直接写数组名

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	printf("&arr[0]=%p\n", &arr[0]);
	printf("arr    =%p\n", arr);
	return 0;
}

在这里插入图片描述
通过运行结果,我们可以看到数组名的本质就是首元素的地址。

#include<stdio.h>
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	printf("%zd", sizeof(arr));

	return 0;
}

在这里插入图片描述
嗯?如果说arr是首元素地址的话,在32位/64位的环境下,地*址的字节数应该是4/8(这里作者实在X64环境下运行的),怎么输出的结果是40?
这明明是整个数组的字节个数啊?
事实上,数组名是数组首元素的地址这句话是非常正确的,可万事都不是绝对的,数组名也不例外。
两个例外;
sizeof(数组名),sizeof中单独放数组名,表示的就是整个数组,计算整个数组的大小,单位字节
&(数组名):这的数组名也是指整个数组,取出的是整个数组的地址(整个数组的地址和首元素地址是不一样的)

除此之外,数组名就是首元素的地址

6.2一维数组传参的本质

#include<stdio.h>
void test1(int arr[])
{
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz2);
}
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz1);
	test1(arr);
	return 0;
}

在这里插入图片描述

我们发现当把arr传过去的时候,并不能正确计算出数组元素的个数
原因是,表面上我们的test函数接受的是数组,实际上接受的是首元素的地址,是指针
sizeof计算的是地址所占的字节数,而不是数组的大小,真因为形参的本质是指针,所以在这个函数内部是无法计算元素个数的。作者这里是在X64环境下运行的,地址的字节数就是8,计算的结果sz2=2;(各位宝宝们,如果是在x86环境下的话,计算出来就是1)

#include<stdio.h>
void test(int* arr)//参数携程指针形式
{
	printf("%zd\n", sizeof(arr));
	//计算一个指针变量的大小,sizeof(数组名)计算的是整个数组的地址
	
}
int main()
{
	int arr[10] = { 0,1,2,3,4,5,6,7,8,9 };
	test(arr);
	return 0;
}

在这里插入图片描述
我们传过去的arr是个指针,是地址,地址的字节数在64位环境下为8
结论:一维数组传参的本质上数组传参传递的是首元素的地址

7.二级指针

二级指针说白了就是为了存放一级指针变量
在这里插入图片描述
ppa存放的是pa的地址,pa中存放的是a的地址
*ppa,实质是对ppa内存放的pa的地址解引用,得到pa的内容,pa的内容是a的地址,*pa是对pa的内容即a的地址解引用,得到a的内容10;

8.指针数组和数组指针

这部分内容作者在之前的博客已经介绍过了
直通车:https://blog.csdn.net/2301_80151382/article/details/140967510?spm=1001.2014.3001.5501

9.函数指针变量

这部分内容作者在之前的博客已经介绍过了
直通车:https://blog.csdn.net/2301_80151382/article/details/141001124?spm=1001.2014.3001.5501

10.回调函数

回调函数就是通过函数指针调用的函数
如果你把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数的时候,被调用的函数就是回调函数

在作者之前函数指针变量的博客里边,就有提到一个转移表,那么用回调函数去实现它是一个很不错的选择

#include<stdio.h>
//回调函数实现转移表
int add(int a, int b)
{
	return a + b;
}
int sub(int a, int b)
{
	return a - b;
}
int mul(int a, int b)
{
	return a * b;
}
int div(int a, int b)
{
	return a / b;
}
void cal(int(*pf)(int x, int y))//回调函数
{
	int a = 0;
	int b = 0;
	printf("请输入你的操作数:");
	scanf("%d %d", &a, &b);
	int ret = 0;
	ret = pf(a, b);
	printf("%d\n", ret);
}
int main()
{
	int input = 0;
	do
	{
		printf("************************\n");
		printf("**1.add******2.sub******\n");
		printf("**3.mul******4.div******\n");
		printf("*****0.退出程序*********\n");
		printf("************************\n");
		printf("请输入你的选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			cal(add);
			break;
		case 2:
			cal(sub);
			break;
		case 3:
			cal(mul);
			break;
		case 4:
			cal(div);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("输入错误,请重新选择\n");

		}
	} while (input);
	return 0;
}

作者有话说:作者只是一只初学小白,以上的解释分析均是作者自己对已学知识的理解,如有错误,感谢指出;如感到有帮助。请留下一个👍

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值