C语言:指针详解

往期文章

  1. C语言:初识C语言
  2. C语言:分支语句和循环语句
  3. C语言:函数
  4. C语言:数组
  5. C语言:操作符详解

前言

大家好,文章已经更新了五篇了,今天我们来更新第六篇指针。遥记得当年学指针的痛苦,希望这次重新学习能够有别样收获。本博主之前也写过两篇关于指针的文章,也比较详细,不过无论排版还是内容上还是有一些遗憾,希望这篇博客可以弥补之前文章中的遗憾。也欢迎大家点击阅读之前博主关于指针的文章:安得指针千万间大庇天下指针具欢颜上
安得指针千万间大庇天下地址具欢颜中

1. 指针是什么

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向
(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以
说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址
的内存单元。

计算机是有32位和64位之分的。所谓32位就是有32位地址线,64位就是有64位地址线。每根地址线有正负电之分,我们可以对应为0和1来表示,也就是二进制来表示。
我们以32位的计算机为例,它的表示范围为00000000000000000000000000000000到11111111111111111111111111111111这之中的每一个编号都可以作为一个地址,也是一个字节。

指针变量是用来存放地址的。

#include <stdio.h>
int main()
{
	int a = 10;//在内存中开辟一块空间
	int *p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
	//将a的地址存放在p变量中,p就是一个之指针变量。
	return 0;
}

在这里插入图片描述
指针指向的是首地址。

在这里插入图片描述

2. 指针和指针类型

虽然指针大小相同,但是指针类型仍然有意义。
定义指针的类型,目的是决定指针访问几个字节。
指针类型不仅可以决定指针访问几个字节,也可以决定指针加一后,跳过几个字节。

在这里插入图片描述

在这里插入图片描述

3. 野指针

在这里插入图片描述
野指针成因:
1.指针未初始化
在这里插入图片描述

2.指针越界访问
在这里插入图片描述

3.指针指向的空间释放
在这里插入图片描述

如何避免野指针

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放即使置NULL
  4. 指针使用之前检查有效性

4. 指针运算

在这里插入图片描述

4.1 指针±整数

#include<stdio.h>

int main()
{
	int arr[5] = { 0 };
	int *p = arr;
	int i = 0;
	for (int i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 5; i++)
	{
		printf("%d	", *(p + i));
	}	
	return 0;
}

在这里插入图片描述

#include<stdio.h>

int main()
{
	int arr[5] = { 0 };
	int *p = arr;
	int i = 0;
	for (int i = 0; i < 5; i++)
	{
		*p++ = i;
	}
	p = arr;
	for (int i = 0; i < 5; i++)
	{
		printf("%d	", *(p + i));
	}
	return 0;
}



在这里插入图片描述

指针与整数的加减法,有时候可以产生新的地址,有时候可以改变指针指向的地址。

4.2 指针-指针

#include<stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);
	return 0;
}

在这里插入图片描述
指针-指针得到的值的绝对值是指针之间的元素个数。我们这里用的是绝对值,因为我们用arr0-arr9会得到同样的结果-9.
在这里插入图片描述
注意,指针减指针的前提是两个指针指向的是同一块连续的空间。

在这里插入图片描述

4.3 指针的关系运算

在这里插入图片描述
在这里插入图片描述
也就是说p是可以和R进行比较滴,但不可以和L进行比较。

5. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针。
在这里插入图片描述

6. 字符指针

有一种指针类型为字符指针,我们通常这样写:

int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}

但有些时候,我们会遇到这样的写法:

int main()
{
	char* pstr = "hello.";//这里是把一个字符串放到pstr指针变量里了吗?
	printf("%s\n", pstr);
	return 0;
}

上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中
在这里插入图片描述
由此,我们来看一道非常经典的题目:

#include <stdio.h>
int main()
{
	char str1[] = "hello bit.";
	char str2[] = "hello bit.";
	char *str3 = "hello bit.";
	char *str4 = "hello bit.";
	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;
}

这段代码的执行结果是什么呢?
在这里插入图片描述
在这里插入图片描述

7. 指针数组

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

存放指针的数组为指针数组。

#include<stdio.h>
int main()
{
	int a=0;
	int b=5;
	int c=10;
	int *arr[3] = { &a, &b, &c };//指针数组
	return 0;
}

在这里插入图片描述

8. 数组指针

数组指针本质上是指针。能够指向数组的指针。

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

我们知道数组名是数组的首地址,那么&数组名又是什么呢?

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	printf("arr = %p\n", arr);
	printf("&arr= %p\n", &arr);
	printf("arr+1 = %p\n", arr + 1);
	printf("&arr+1= %p\n", &arr + 1);
	return 0;
}

在这里插入图片描述

在这里插入图片描述

注意:除了sizeof(数组名)和&数组名中数组名代表整个数组之外,其他时候数组名都代表数组首元素的地址。
数组指针的使用:

#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i<row; i++)
	{
		for (j = 0; j<col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
void print_arr2(int(*arr)[5], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i<row; i++)
	{
		for (j = 0; j<col; j++)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	print_arr1(arr, 3, 5);
	//数组名arr,表示首元素的地址
	//但是二维数组的首元素是二维数组的第一行
	//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
	//可以数组指针来接收
	printf("*****************************\n");
	print_arr2(arr, 3, 5);
	return 0;
}

在这里插入图片描述
我们再来复习一下数组指针和指针数组:
在这里插入图片描述

9. 数组参数、指针参数

在写代码的时候难免要把【数组】或者【指针】传给函数,那函数的参数该如何设计呢?

9.1 一维数组

对于一维数组,这些传参方式都是可以的:

#include <stdio.h>
void test(int arr[])
{}
void test(int arr[10])
{}
void test(int *arr)
{}
void test2(int *arr[20])
{}
void test2(int **arr)
{}
int main()
{
	int arr[10] = { 0 };
	int *arr2[20] = { 0 };
	test(arr);
	test2(arr2);
}

9.2 二维数组

对于二维数组,这些传参方式都是可以的:

#include<stdio.h>
void test(int arr[3][5])
{}
void test(int arr[][5])
{}
void test(int(*arr)[5])//ok?
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。这样才方便运算。

9.3 一级指针

一级指针传参,以下传参方式是可以的:

#include <stdio.h>
void print(int *p, int sz)
{
	int i = 0;
	for (i = 0; i<sz; i++)
	{
		printf("%d\n", *(p + i));
	}
}
int main()
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int *p = arr;
	int sz = sizeof(arr) / sizeof(arr[0]);
	//一级指针p,传给函数
	print(p, sz);
	return 0;
}

思考:当一个函数的参数部分为整型一级指针的时候,函数能接收什么参数?

  1. 一个整型参数的地址
  2. 一个整型指针
  3. 一个整型数组的数组名

9.4 二级指针

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

思考:当一个函数的参数部分为整型二级指针的时候,函数能接收什么参数?

  1. 取地址一级指针
  2. 二级指针
  3. 每个元素是int*的数组名

10. 函数指针

先来看一段代码:

#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}

在这里插入图片描述
输出的是两个地址,这两个地址是 test 函数的地址。函数指针,就是指向函数的指针。

在这里插入图片描述
函数指针的使用:
在这里插入图片描述

解读两个复杂一些的函数指针:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以将代码2简化一下:
在这里插入图片描述
这样就好懂一些了。

11. 函数指针数组

要把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?

int (*parr1[10]])();

parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢? 是 int (*)() 类型的函数指针。

函数指针数组的用途:转移表。这个作用是怎么体现的呢?我们来举一个例子,加入我们要写一个计算器的程序,根据我们之前学过的知识,我们写出的代码是这样的:

#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;
}

int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	do
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("ret = %d\n", ret);
			break;
		case 2:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("ret = %d\n", ret);
			break;
		case 3:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("ret = %d\n", ret);
			break;
		case 4:
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("ret = %d\n", ret);
			break;
		case 0:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}

我们发现,这些加减乘除除了名字不一样,参数啊,返回类型啊都是一样的。我们却还要一个个去调用,有没有什么方法可以简化一下呢,我们看下面的代码:

#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;
}
int main()
{
	int x, y;
	int input = 1;
	int ret = 0;
	int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
	while (input)
	{
		printf("*************************\n");
		printf(" 1:add 2:sub \n");
		printf(" 3:mul 4:div \n");
		printf("*************************\n");
		printf("请选择:");
		scanf("%d", &input);
		if ((input <= 4 && input >= 1))
		{
			printf("输入操作数:");
			scanf("%d %d", &x, &y);
			ret = (*p[input])(x, y);
		}
		else
			printf("输入有误\n");
		printf("ret = %d\n", ret);
	}
	return 0;
}


在这里插入图片描述
这样我们的代码就得到了极大的简化。

12. 指向函数指针数组的指针

指向函数指针数组的指针是一个 指针 ,指针指向一个 数组 ,数组的元素都是 函数指针。

定义使用方法如下:

void test(const char* str)
{
	printf("%s\n", str);
}
int main()
{
	//函数指针pfun
	void(*pfun)(const char*) = test;
	//函数指针的数组pfunArr
	void(*pfunArr[5])(const char* str);
	pfunArr[0] = test;
	//指向函数指针数组pfunArr的指针ppfunArr
	void(*(*ppfunArr)[10])(const char*) = &pfunArr;
	return 0;
}

13. 回调函数

回调函数就是一个通过函数指针调用的函数,如果你把函数指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另一方调用的,用于对该事件或条件进行响应。

举一个例子:
在这里插入图片描述
我们来看上面的那个程序里的test1,它的参数放的是test2,而test2函数里的参数是一个函数指针,在这个程序里,我们并没有直接调用test1,而是把test1的地址传递给了另一个函数test2,然后我们通过p反过来去调用了test1函数,我们称test1函数为回调函数。这个程序可以很好地演示回调函数,我们再来用计算器的程序来演示一下:

#include<stdio.h>
void menu()
{
	printf("欢迎使用计算器>>\n");
	printf("*******************************\n");
	printf("*****      1.add        *******\n");
	printf("*****      2.sub        *******\n");
	printf("*****      3.mul        *******\n");
	printf("*****      4.div        *******\n");
	printf("*****      0.exit       *******\n");
	printf("*******************************\n");
}
int add(int x, int y)
{
	return x + y;
}
int sub(int x, int y)
{
	return x - y;
}
int mul(int x, int y)
{
	return x*y;
}
int div(int x, int y)
{
	return x / y;
}
void cal(int(*p)(int,int))
{
	int a = 0;
	int b = 0;
	int ret = 0;
	printf("请输入两个操作数>>\n");
	scanf("%d %d", &a, &b);
	ret = p(a, b);
	printf("ret=%d\n", ret);
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择>>\n");
		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");
			break;
		}
	} while (input);
	return 0;
}


我们增加了cal函数,我们在程序中,将加减乘除的函数名作为cal的参数,从而避免了使用多次重复打相同的代码。

后记

好的,这篇万字长文到这里就结束啦,这绝对是一篇诚意满满的博客,希望对大家有所帮助。感谢大家的关注和支持,我们下一篇博客再见。

在这里插入图片描述

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lxkeepcoding

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

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

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

打赏作者

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

抵扣说明:

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

余额充值