C语言深入理解指针

下午好诶,今天小眼神给大家带来一篇C语言枚举类型详解的文章~

目录

一、了解指针变量

地址和解引用操作符

指针变量的大小

指针 + - 整数

void* 指针

const修饰指针

野指针

如何规避野指针

assert断言

 二、指针与数组

使用指针访问数组

二级指针

指针数组

​编辑

 指针数组模拟二维数组

​编辑 三、字符、数组、函数指针

字符指针变量

数组指针变量

函数指针变量

 函数指针的使用

函数指针数组


一、了解指针变量

地址和解引用操作符

在前面的学习中我们知道创建一个变量的时候会开辟一段空间,比如下面创建一个 int 变量a,就会开辟四个字节的内存空间,每个字节都有自己的地址。 

在我们每次书写scanf函数的时候,都需要再变量的前面打上一个&,这就是取地址,取出a的地址 

scanf("%d",&a);

于是我们便在a的地址处存入所输入的数据。

此时我们发现a地址处的值变成了所输入的1。那么我们通过取地址操作符(&)拿到的地址是一个数值,比如:0x005EFE78,这个数值有时候也是需要存储起来,方便以后使用的,那我们将这样的值存放在哪里呢?

这便是我们接着要学习的:指针变量

在初次学习指针的时候,总会有人将 int * 认为是 int 的一个扩展,其实不然,他们都是一个不同的类型,就像int、char、double、float等等一样,指针也属于类型,int*、char*等等

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

当我们学会调用地址后,我们接着要存入的值,这时候就需要用到解引用符( * )

此时p作为指针变量,存储了a的地址,在给p进行赋值的时候,我们就会发现,a的值发生了改变,我们可以通过调用地址来改变地址所存储的值!

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

 同样的,当变量为char类型的时候,我们就需要用char*类型的指针变量来存储,而int*可以访问四个字节,char*只能访问一个字节

#include<stdio.h>
int main()
{
	char a = 'w';
	char* p = &a;
	*p = 'a';
	return 0;
}

指针变量的大小

我们假设32位机器有32根地址总线,每根的信号为0或1,而他们组成的32根地址线产生的2进制序列就当做一个地址,那么一个地址就是32个bit位,需要个4字节的空间才能储存。

#include<stdio.h>
int main()
{
	//指针变量的大小取决于地址的大小
	//32位平台下地址是32个bit位(即4个字节)
	//64位平台下地址是64个bit位(即8个字节)
	printf("%zd\n", sizeof(char*));//4
	printf("%zd\n", sizeof(short*));//4
	printf("%zd\n", sizeof(int*));//4
	printf("%zd\n", sizeof(double*));//4
	return 0;
}

指针 + - 整数

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

	int n = 10;
	char* pc = (char*)&n;
	int* p = &n;
	printf("%p\n", &n);//n的地址
	printf("%p\n", pc);//pc内存的地址
	printf("%p\n", pc + 1);//pc的下一个字节地址
	printf("%p\n", p);//p内存的地址
	printf("%p\n", p + 1);//p向后四个字节的地址
	return 0;
}

 char* 的指针变量+1跳过一个字节,int* 类型的指针变量+1跳过了4个字节,所以不同的指针变量的类型具有不同的效果,在后面的应用中可以利用强制转换来达到想要的效果。

void* 指针

void*类型是一种特殊的无具体类型的指针(或叫泛指型指针),这种类型指针可以接收任意类型地址,但因为它本身没有类型,所以不能直接进行指针的+-整数和解引用的运算

这里可以看到VS编译器已经报错,在输出中可以看到void*类型指针可以接收不同类型的地址,但是无法直接进行指针运算。 

实际上,一般void*类型的指针是使用在函数参数的部分,用来接收不同类型的地址,这样的设计可以实现泛型编程的效果。

const修饰指针

指针变量在被const修饰后,在语法上加了限制,如果在代码中进行修改,就不符合语法,会报错。

 结论:const修饰指针变量的时候

①const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本身的内容可变。

②const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。

野指针

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

野指针就是定义的指针未初始化指针空间已经释放,已归还内存(多出现于参数使用中)或者是指针越界访问(多见于数组指针中)

如何规避野指针

在定义指针的时候,不知道指针应该指向哪里,可以给指针赋值NULL。NULL是C语言中定义的一个标识符常量,值为0,0也是地址,并且这个地址是无法使用的,读写该地址就会报错。

int* p = NULL;

assert断言

assert需要包含头文件<assert.h>,在运行时需要确保程序符合指定条件,如果不符合,就报错终止运行。我们可以用“断言”来判断指针是否为野指针,保证程序安全。

assert一般在Debug中使用,在Release版本中就顶部输入#define NDBUG指令,选择禁用assert就行,在VS的集成开发环境中,在Release版本中,直接就优化掉了。

#define NDEBUG
#include <assert.h>
    assert(pa != NULL);

 二、指针与数组

使用指针访问数组

我们首先要知道,数组名就是地址,而且是数组首元素的地址。那么当我们拿到数组名后,就可以借用指针来调用地址操作数组!

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);//当数组名出现在sizeof或者&中时,代表的是整个数组
	int* p = arr;
	for (int i = 0; i < sz; i++)
	{
		scanf("%d", p + i);//p+i相当于arr+1,与arr[i]达到相同效果。
	}
	return 0;
}

为什么scanf里面不需要&取地址了?那是因为p是指针变量,本身就是地址!

而当我们需要输出的时候,只需要用解引用就可以了

printf("%d ",*(p));

二级指针

指针变量也是变量,是变量就有地址,那么当需要将指针变量的地址放到一个地方存储起来的时候,就需要用到二级指针

​
#include<stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
	return 0;
}

​

 此时若是使用 *ppa 访问的就是 pa**ppa 就是通过 *ppa 找到 pa,然后对 pa 进行解引用操作,二级指针所能产生的效果与一级指针一样,都是调用地址来改变变量,储存地址。

指针数组

指针数组是指针还是数组呢?

整形数组,是存放整形的数组,字符数组是存放字符的数组,那么指针数组自然是存放指针数组,它的每个元素都是用来存放地址(指针)的。

 指针数组模拟二维数组

我们可以将几个数组名放在一个指针数组里面,也就是存入了首元素地址

//指针数组模拟二维数组
#include<stdio.h>
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 5,6,7,8,9 };
	int arr3[] = { 9,8,7,6,5 };
	int arr4[] = { 5,4,3,2,1 };
	int arr5[] = { 0,1,3,5,7 };
	int* parr[5] = { arr1,arr2,arr3,arr4,arr5 };
	int i = 0, j = 0;
	for (i = 0; i < 5; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

 parr[ i ]访问的是parr数组的元素,但实际上上述代码并非完全是二维数组,因为二维数组的每一行是连续的此处模拟的二维数组并不是

 三、字符、数组、函数指针

字符指针变量

#include<stdio.h>
int main()
{
	const char* pstr = "hello student";
	printf("%s\n", pstr);
	return 0;
}

此处的代码const char* pstr = "hello student"; 容易让初学者认为是把字符串“hello student”放到字符指针pstr中,但pstr正是指针,那么它存的必然是地址,也就是字符串首字符‘ h ’的地址放入了pstr中。

《剑指offer》中有这样一个相关题目:

#include<stdio.h>
int main()
{
	char str1[] = "hello student";
	char str2[] = "hello student";
	const char* str3 = "hello student";
	const char* str4 = "hello student";
	if (str1 == str2)
		printf("str1 and str2 are same\n");
	else
		printf("str1 and str2 are not same\n");
	if (str3 == str4)
		printf("str3 and str4 are same\n");
	else
		printf("str3 and str4 are not same\n");
	return 0;
}

它的输出则是

str1 and str2 are not same
str3 and str4 are same

 此时在char类型的str1str2中存储的就是两个不同的首地址,因为在定义两个数组的时候,就开辟了不一样的地址空间。而在char* 类型的str3str4中存的则是同一个常量字符串,指向的是同一块内存。这里其实还有一个知识点:C/C++会把常亮字符串储存到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存,但当用相同的字符串去初始化数组的时候就会开辟不同的内存块,所以地址不同。

数组指针变量

上面提到的指针数组是一种数组,数组中存放的是地址(指针)

数组指针变量顾名思义它是一个指针变量

我们已经熟悉了整形指针变量:

int* p 存放的是整形变量的地址,指向整形数据的指针。

char* p 存放的是字符变量的地址,指向字符数据的指针。

float* p 存放浮点型变量的地址,指向浮点型数据的指针。

int (*p)[10];

根据优先级,我们可以推断出,p先与 结合,说明p是一个指针变量,它指向一个存有10个整形空间的数组,所以叫数组指针(此处(*p)的括号不能掉,因为[ ]的优先级要高于 * 号的)

int (*p)[10] = &arr;

如果要存放数组的地址,就得放在数组指针变量里,如上图

 ps:数组的类型和元素个数要一一对应!

函数指针变量

同上,函数指针变量自然是应该用来存放函数地址的,方便以后通过地址调用函数的。

我们通过直接使用函数名,或者&函数名都可以取得函数的地址。(这一点和数组名相似但又有区别)

要储存函数的地址,就要创建函数指针变量,函数指针变量的写法和数组指针非常相似:

//没有参数的情况
void test()
{
	printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;
//有参数的情况
int test(int x,int y)
{
	return x + y;
}
int (*pf1)(int, int) = &test;
int (*pf2)(int x, int y) = test;//(此处的x和y写上或者省略都是可以的)

int (*)(int x,int y)//是函数指针变量pf3的类型

 函数指针的使用

函数指针调用指针指向的函数

#include<stdio.h>
int test(int x, int y)
{
	return x + y;
}
int main()
{
	int (*pf2)(int x, int y) = test;
	printf("%d\n", (*pf2)(2, 3));
	printf("%d\n", pf2(3, 5));
	return 0;
}

这里有两道源自于《C陷阱与缺陷》这本书的有趣的代码,有兴趣的伙伴可以研究一下:

	(*(void(*)())0)();

	void(*signal(int, void(*)(int)))(int);

函数指针数组

数组是一个存放相同类型数据的储存空间,那要把函数的地址存到一个数组中,就把这个数组叫作函数指针数组

int (*parr1[3])();

parr1先与[ ]结合,说明parr1数组,数组的内容就是 int (*)( ) 类型的函数指针

感谢阅读本篇文章

希望能对您解题有所帮助,记得点赞关注收藏。

  • 44
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值