【c语言】指针就该这么学(1)

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:C语言

目录

前言

一、指针是什么

​编辑二、指针变量

1.取地址操作符:&

2.指针变量的定义

3.解引用操作符:*

4.指针变量的大小

三、指针类型的意义

1.指针解引用访问的字节数

2.指针+-整数

四、const修饰

1.const修饰变量

2.const修饰指针

2.1 const放在 * 前面

2.2 const放在 * 后面

3.3  * 左右两边都放const

五、指针的运算

1.指针+-整数

2.指针-指针

3.指针的关系运算(大小比较)

六、野指针

1.野指针的概念

2.野指针出现的情况

2.1 指针变量未初始化

2.2 使用指针越界访问

2.3 指针指向的空间已经释放

3.如何规避野指针

3.1 指针初始化赋值NULL

3.2 防止指针越界

3.3 避免函数返回局部变量的地址

总结


前言

        指针是我们学习c语言的重要环节之一,可以说学好指针,你才能学好c语言。对于很多初学者来说,指针之前的内容就是“洒洒水”,从指针开始就什么也搞不懂了。希望通过我的讲解,能够加深你对指针的理解。

一、指针是什么

        首先,让我们举一个生活中的例子:假设有一个酒店,这个酒店当中有一百个房间,每一个房间都有一个唯一的编号(001,002,003......100),现在你在这个酒店订房,成交之后前台会告诉你房间的具体编号,这就便于你找到该房间然后入住。

        我们将上述例子运用到计算机当中:计算机也是用类似的方式高效管理内存。内存被计算机划分为一个个的内存单元,一个单元就是一个字节。而这些内存单元就相当于酒店的房间,房间的编号就是内存的地址,也叫做指针。地址本质是一个十六进制数字,每一个字节的内存都有其唯一的地址。


二、指针变量

1.取地址操作符:&

        我们都知道,在c语言中,要创建一个变量,就会申请对应字节的内存空间。比如:

#include <stdio.h>

int main()
{
	int a = 10;
	return 0;
}

我们通过内存窗口来观察一下它们的地址:

可以看到,在我们定义变量a时,向系统申请了四个字节的内存空间,然后将10这个值存入其中。这四个字节的的地址分别是:

0x012FFE0C

0x012FFE0D

0x012FFE0E

0x012FFE0F

在这四个地址当中,a的地址就是其中第一个字节的地址,也就是最小的0x012FFE0C。

那么,我们该如何得到它的地址呢?这就需要我们学习一个新的操作符——&(取地址操作符)

我们在使用它时,在变量名之前加上&符号,就表示这个变量的地址。我们尝试打印一下变量a的地址:

#include <stdio.h>

int main()
{
	int a = 10;
	printf("%p\n", &a);//打印地址用%p
	return 0;
}

运行结果:

屏幕上出现了一个16进制数字,这就是它的地址。不过这个值在程序每次运行时都是不同的,因为为变量开辟的内存空间是不一定的

2.指针变量的定义

        我们刚才使用&操作符来取得变量的地址,但是为了方便后期使用,我们可以将这个地址存入到一个变量中,而这个变量就叫做指针变量。也就是说,指针变量是专门存放地址的变量

指针变量的定义方式:

#include <stdio.h>

int main()
{
	int a = 0;
	int* p = &a;//指针变量p当中存放a的地址
	return 0;
}

在定义的变量名前加一个 * 号,这个 * 号说明这是一个指针变量。接着用&取出a的地址,将其赋值给p变量即可。

注意:这里的变量怕,它的类型是 int*,说明是个整形指针变量。如果定义一个浮点型变量,就用float* 类型的指针去指向(存放该变量的地址)它。

3.解引用操作符:*

既然我们已经定义了一个指针变量,那么该如何使用它呢?这就像我们订了一间房需要入住,那就需要用钥匙。解引用操作符就相当于钥匙,通过钥匙就能通过地址找到变量,然后间接性地对变量进行操作。我们尝试使用以下解引用操作符:

#include <stdio.h>

int main()
{
	int a = 0;
	int* p = &a;
	*p = 10;//解引用操作
	printf("%d\n", a);
	return 0;
}

运行结果:

不难看出,p存了a的地址,解引用操作符通过地址找到了相应的值并且改为10,此时*p就等价于a。这里要注意:解引用操作符的 * 和定义指针变量时的 * 是不一样的,定义变量时 * 只是表示它是一个指针变量。

想必你会有疑问了:想要改变a的值,直接改不就可以了嘛,为什么还要这么麻烦地定义一个指针去改它呢?我们这里写一个程序来说明指针的作用:

#include <stdio.h>

void swap(int a,int b)
{
	int t = 0;
	t = a;
	a = b; 
	b = t;
}

int main()
{
	int a = 3;
	int b = 5;
	swap(a, b);
	printf("a=%d,b=%d", a, b);
}

swap函数用于交换a和b的值,可以想一想,这个函数能否实现功能呢?让我们看看运行结果:

a和b的值并没有交换过来。为什么呢?还记得函数传参的本质吗?函数传参时,形参只是实参的一份临时拷贝,你在函数内改变形参是无法影响到实参的要做到改变实参,我们就需要传入实参的地址,然后在函数中通过地址来找到值去改变,这叫做传址调用。让我们用代码实现一下:

#include <stdio.h>

void swap(int* pa, int* pb)//传入地址,用指针去接收
{
	int t = 0;
	t = *pa;
	*pa = *pb;
	*pb = t;
}

int main()
{
	int a = 3;
	int b = 5;
	swap(&a, &b);//地址作为函数参数
	printf("a=%d,b=%d", a, b);
}

运行结果:

可以看到,a和b的值交换过来了。

4.指针变量的大小

        接下来,我们来探讨一下指针变量的大小。指针变量既然是一个变量,那么它也肯定是占用内存空间的,它占用的内存大小是多少呢?我们使用sizeof操作符查看一下:

#include <stdio.h>

int main()
{
	printf("%zd\n", sizeof(char*));
	printf("%zd\n", sizeof(short*));
	printf("%zd\n", sizeof(int*));
	printf("%zd\n", sizeof(float*));
	return 0;
}

首先在X86环境下运行:

然后再X64环境下运行:

可以看出,在X86环境下,指针无论是什么类型,它的大小都是4个字节,而X64环境下大小是8个字节。所以指针的大小和它的类型是无关的

        既然指针大小与类型无关,那为什么还有这么多种类型的指针变量?其实,指针变量类型是有它独特的意义的。

三、指针类型的意义

1.指针解引用访问的字节数

        我们写一段代码,观察一下不同类型指针指向相同类型变量的结果:

#include <stdio.h>

int main()
{
	int a = 0x11223344;//这里设置成十六进制数字方便调试观察
	int b = 0x11223344;
	int* pa = &a;
	char* pb = (char*)&b;//用char指针接收int型变量,为保证严谨性将地址强制转换为char*类型
	*pa = 0x0;
	*pb = 0x0;
	printf("a=%#x b=%#x", a, b);//%#x可以在输出的十六进制数字前带0x
	return 0;
}

运行结果:

可以发现,a的值直接被改为0,而b的最后两位被改为了0。这里要注意:这是一个十六进制数字,十六进制数字的一位就代表四位二进制数字,两位被改成0,就说明八位2进制数字被改成0,而八位刚好是一个字节。这说明我们使用char*类型的指针去修改变量的值时,修改的是一个字节的内容。而int*类型的指针修改了四个字节。

结论:指针变量的类型决定了它访问变量时的权限(能够操作的字节个数)。

为了便于让我们理解,我们画图表示一下:

2.指针+-整数

        我们写一段代码观察对指针加或减一个整数的效果:

#include <stdio.h>

int main()
{
	int a = 0;
	char* p1 = (char*)&a;
	int* p2 = &a;
	printf("%p\n", p1);
	printf("%p\n", p2);
	printf("%p\n", p1 + 1);
	printf("%p\n", p2 + 1);
	return 0;
}

运行结果:

不难看出,char型的指针+1,它存放的地址就+1,相当于就向前走了一个字节,而int型指针+1就走了4个字节。

结论:指针变量的类型决定了它向前或者向后走的步长有多大。

四、const修饰

1.const修饰变量

        在我们编写程序的时候,如果我们想要一个变量不可被修改,那么就可以在变量定义时加一个const,例如:

可以看到,当我想要重新给a赋值时,会报错。

但是如果我们使用指针去修改呢?

#include <stdio.h>

int main()
{
	const int a = 0;
	int* p = &a;
	*p = 10;
	printf("%d\n", a);
	return 0;
}

运行结果:

如果使用指针去间接修改a的值,const就形同虚设了。很显然,这样做就相当于是锁了门却从窗户爬出去的行为。那么,我们是否可以对指针变量使用const修饰,关上“这扇窗”呢?

2.const修饰指针

const修饰指针变量的形式有三种,分别是:

const int * p;//const放在 * 前边
int * const p;//const放在 * 后边
const int * const p;// * 前后都放const

2.1 const放在 * 前面

const放在 * 前面时,指针所指向的内容不能改变,但是指针变量可以改变。例如:

想要通过指针去修改变量a的值就会报错。

2.2 const放在 * 后面

const放在 * 后面时,指针所指向的内容可以改变,但是指针变量不能改变。例如:

当使用指针去指向变量b时,就会报错。

3.3  * 左右两边都放const

当 * 左右两边都放const时,指针变量本身和其所指向的内容都不能改变。例如:

可以看到,无论想要通过指针改变a的值,还是让指针指向b,都是无法进行的。

五、指针的运算

        接下来,我们学习一下指针的三种运算方式,便于我们加深对指针的理解,易于上手指针操作。

1.指针+-整数

        刚才已经提到,指针+-整数可以跳过特定单位的字节。那么它的实际作用是什么?我们都知道,数组在内存中是连续存放的,那么我们就可以利用指针跳过特定字节的特性,来访问数组当中的每一个元素。示例如下:

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = &arr[0];//p中存放第一个元素的地址
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *p);//通过指针访问元素并打印
		p++;//指针自增1,跳过一个整形元素(4个字节)
	}
	return 0;
}

我们通过循环的方式,运用指针访问元素并且打印,之后让指针自增1,就跳过了四个字节,也就是一个整形元素,就可以访问下一个元素了

以上代码也可以这样写:

#include <stdio.h>

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

指针变量p先加i之后再解引用,就能跳过特定数量来访问元素。

        注意:第一段代码中指针变量p由于自增,所以它在循环结束之后已经不再指向第一个元素了,如果要重新访问这个数组,就需要先将第一个元素的地址赋值给p,防止越界访问。

2.指针-指针

指针-指针,也就是两个地址相减,得到的是两个指针之间的元素个数。

注意:在进行指针相减运算时,一定要保证这两个指针指向的是一块连续的空间,比如数组。否则就是无意义的。

举个例子:

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p1 = &arr[0];
	int* p2 = &arr[9];
	printf("%d\n", p2 - p1);
	return 0;
}

运行结果:

在数组arr当中,从第一个元素的地址开始,到第十个元素,中间确实有九个元素。

        既然有指针-指针的操作,那么有没有指针+指针呢?这个问题很简单,比如我们有一本日历,我们要算算两个日期之间有多少天,那么就用大的日期减去小的日期。但是两个日期相加,就是没有意义的。同样的,指针+指针也是无意义的。

3.指针的关系运算(大小比较)

        由于地址有大小,所以指针也可以进行大小比较。我们改造一下刚才打印数组元素的代码:

#include <stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	while (p <= &arr[9])//指针的关系运算
	{
		printf("%d ", *p);
		p++;
	}
	return 0;
}

可以看出,指针的相关操作还是十分灵活的。

六、野指针

        接下来我们学习一个概念:野指针。

1.野指针的概念

如果一个指针指向的位置不可知或者并非自己申请的,那么它就是一个野指针。

2.野指针出现的情况

2.1 指针变量未初始化

#include <stdio.h>

int main()
{
	int a = 0;
	int* p;//未初始化的指针
	*p = 20;//使用了野指针
	return 0;
}

2.2 使用指针越界访问

#include <stdio.h>

int main()
{
	int arr[5] = { 0 };
	int* p = &arr[10];
	*p = 20;//越界访问,此时p为野指针
	return 0;
}

2.3 指针指向的空间已经释放

#include <stdio.h>

int* fun()//int*表示函数返回值是一个int*类型的地址
{
	int a = 0;
	int* p = &a;
	return p;
}

int main()
{
	int* p = fun();//函数退出,返回的地址已经被释放
    printf("%d\n", *p);
	return 0;
}

野指针总是会在不经意的地方出现,并可能产生意想不到的后果。我们在实际编程的时候,要学会规避野指针

3.如何规避野指针

3.1 指针初始化赋值NULL

        在指针变量初始化时,如果不知道存入谁的地址,那么就先制成空指针(NULL)

NULL是C语言中定义的一个常量,它的值是0,同时也是一个地址,表示内存地址为0的地方。0地址处的空间是不可使用的。如果对NULL进行解引用操作,就会发生报错。

例如:

int *p = NULL;

3.2 防止指针越界

        在使用指针访问一片连续的内存时,一定要检查指针是否会越界,防止出现野指针。

3.3 避免函数返回局部变量的地址

        我们在定义一个函数的时候,要避免返回局部变量的地址,这样就不会在外部函数中出现使用野指针的情况。

总结

        以上就是对指针的定义和基本操作,真正运用指针的方式还有很多很多。后续博主还会继续和大家探索指针的更多知识。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

  • 61
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值