深入理解C语言中的指针

一、内存和地址

1.1内存

  在讲内存和地址之前,我们想一个生活案例:假设有一栋没有门牌的房子,你想在这栋房子里找一个东西,由于没有门牌号,你并不知道这个东西在哪个房间,于是就要挨个房间去找,这种查找方式会导致效率低下,但是,如果我们在每个房间编上号,那么你就可以快速找到你想取的房间。

  计算机中的内存是同理,计算机在CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那么这些内存空间如何高效管理呢?其实就是把内存划分为一个个内存单元,每个内存单元的大小为一个字节。一个字节等于8个比特位,每个比特位存储二进制的0或1,每个内存单元都有一个编号,这个编号我们称为指针。

1.2.地址

  计算机中的编制不是把每个字节的内存单元的地址记录下来,而是通过硬件设计完成的。首先我们必须理解,计算机内有很多的硬件单元,而硬件单元是要相互协作的,如何将每个独立的硬件联系起来呢?就是通过线连接起来,不过跟指针有关的是地址总线。32位机器有32根地址总线,每根线只有两态,表示0或1(电脉冲的有无),那么一根线就能表示两种含义,32根线就能表示2^32种含义,每种含义表示一个地址。

二、与指针相关的操作符和关键字

2.1 取地址操作符(&)

  知道了C语言中内存和地址的关系,那么我们想知道数据在内存中的地址该怎么做呢?C语言提供了取地址操作符&,使用方法如下:

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

  我们通过取地址操作符得到了地址,那么地址储存在哪里呢?答案是指针变量int*p中,其中的p表示指针的名字,后续我们可以通过p找到该指针变量,因此,指针变量是用来存放地址的。

  当我们知道了变量在内存中的地址之后,我们可与不可以通过地址找到该变量呢?答案依旧是肯定的,C语言提供了解引用操作符*,后期我们可以通过*找到该变量。

2.3 指针变量的大小及意义

  32位机器下,指针变量的大小是4个字节;64位机器下,指针变量的大小是8个字节。指针变量可以加减整数,解引用操作等,其类型决定了指针变量解引用一次能操作多少个字节,以及向前向后走一步距离是多大。

2.4 const修饰指针

  变量可以通过直接修改以及通过指针变量修改,为了不让变量被修改,我们引入了const。

int main()
{
	int n = 10;//n可以修改
	const int m = 10;
	m = n;//m不可以修改
	return 0;
}

我们对上述代码进行分析,m本质上是变量,之所以不能修改,是因为我们用const在语法上加了限制,使其变为常变量,但我们可以绕过const从而将m修改

int main()
{
	int n = 10;
	const int m = 20;
	int* p = &m;
	*p = n;
	printf("%d\n", m);
	return 0;
}
2.5 const修饰指针变量
void test1()
{
	int n = 10;
	int m = 20;
	int* p = &n;
	*p = m;
	printf("n=%d m=%d\n", n, m);
}
void test2()
{
	int n = 10;
	int m = 20;
	const int* p = &n;
	*p = m;
	printf("n=%d m=%d\n", n, m);
}
void test3()
{
	int n = 10;
	int m = 20;
	int* const p = &n;
	*p = m;
	printf("n=%d m=%d\n", n, m);
}
void test4()
{
	int n = 10;
	int m = 20;
	const int*const p = &n;
	*p = m;
	printf("n=%d m=%d\n", n, m);
}
int main()
{
	test1();
	test2();
	test3();
	test4();

	return 0;
}

const修饰指针变量的时候(左定值右定向)

如果放在*的左边:限制*p,即不能通过指针变量p修改p指向空间的内容,但p是不受限制的,

如果放在*的右边:限制p变量,即p变量无法修改,但*p不受限制,还是可以通过p修改p所指对象的内容。

2.6  野指针

野指针是指指针所指向的位置是不可知的。

野指针的成因:

1.指针未初始化;2.指针越界访问;3.指针指向空间已释放

如何规避野指针:

1. 指针初始化:

  如果明确知道指针指向哪里就明确赋值,如果不知道就制空NULL。

2.小心指针越界:

  一个程序向内存申请了哪些空间通过指针就只能访问哪些空间,不能超出范围访问。

3.指针变量不再使用时及时制NULL,使用前检查指针的有效性。

4.避免返回局部变量的地址。

2.7 assert断言

  assert.h头文件定义了宏assert(),用在运行时确保程序满足指定条件,如果不符合,就报错终止运行。

  assert对于程序员是很友好的,使用assert()有几个好处:不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问腿,不需要再做断言,就在#include<assert.h>前定义一个宏NDEBUG。然后重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序出现问题,可以移除#define NDEBUG这条指令。

三、指针的使用和传址调用

3.1 传值调用和传址调用

学习指针的目的是使用指针解决问题,那么是什么问题?为什么非指针不可呢?下面我们将讨论传值调用和传址调用,从而更加深入理解指针的作用,我们观察下面的代码:

  我们发现a和b的值并没有像我们想象的那样相互交换,这是为什么呢?事实上,实参传递给形参的时候,形参会单独创建一份临时空间来接受实参,对形参的修改不会影响实参。

  那么我们该怎么办呢?重新审视指针的定义,我们不难发现,指针是内存单元的编号,为了达到我们想要的效果,我们只需要让形参和实参所指向的地址是同一块空间即可,根据这个原理,我们在传参的时候需要把实参的地址传给形参,那么我们改进上述代码:

将变量的地址传给函数,这种函数调用方式叫:传址调用

传址调用可以让函数和主函数之间建立真正的联系,在函数内部可以修改主函数的变量,所以未来函数中只是需要主函数中的变量值来实现计算,就可以采用传值调用,如果函数内部要修改主调函数中的变量值,就需要传址调用。

3.2  strlen的模拟实现

库函数strlen是统计\0之前的字符个数,参数str是字符串首元素的地址,将其传给my_strlen函数时开始遍历统计\0之前的字符个数,只要不是\0计数器count就+1,这样我们就可以模拟实现一个strlen函数。

四、数组名的理解以及一维数组传参的本质

4.1 数组名的理解

在介绍数组名之前我们先看以下代码:

我们发现打印出来的首元素地址和arr数组名的地址是一样的,那么是不是可以说:数组名就是首元素的地址呢?如果是,那么下面代码该怎么解释?

但事实上,数组名就是首元素的地址,但存在两个特例:

(1.)sizeof(数组名):sizeof中单独放数组名计算的是整个数组的大小。

(2.)&数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和首元素的地址是有区别的)。为此我们再看以下代码:

我们发现在给&arr+1之后跳过了80个字节,这是整个数组的长度。

那么在我们知道数组名代表首元素的地址之后,我们是否可以这样写代码:

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	for (i = 0;i < sz; i++)
	{
		scanf("%d", p+i);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}
4.2  一维数组传参的本质

我们通过上述论证知道了:数组名是首元素的地址,那么在数组传参的时候传递的是数组名,也就是说数组传参的本质是首元素的地址,因此,一维数组传参,形参的部分可以写成数组的形式也可以写成指针的形式。

五、冒泡排序

5.1 冒泡排序

其核心在于:两两相邻元素进行比较,代码如下:

void bubble_sort(int* arr, int sz)
{
	int i = 0;
	for (i = 0; i < sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < sz - i - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{
	int arr[] = { 2,5,9,7,6,3,4,1,8,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr, sz);
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(arr + i));
	}
	return 0;
}
5.2 二级指针

指针变量也是变量,存放指针变量的地址的指针叫做二级指针,对二级指针解引用找到的是一级指针存放的地址。

六、数组和指针

6.1 指针数组

  什么是指针数组?顾名思义,就是存放指针的数组。形如:int*parr[ ] 。那么指针数组有什么作用呢?我们可以通过它来模拟二维数组。

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };
	int* parr[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 5; j++)
		{
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}
	return 0;
}
6.2 数组指针变量

  我们知道,指针数组是存放指针的数组,那么数组指针变量是指针变量还是数组?答案是指针变量,是存放数组的地址,能够指向数组的指针变量,形如:int(*p)[ ]

  由于数组指针变量是存放指针的数组,那么它应该指向地址,因此数组指针变量的初始化应该为:

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

了解了数组指针变量后,我们就可以知道二维数组传参的本质:传递的是第一行这个一维数组的地址,代码如下:

void test(int(*p)[5], int x, int y)
{
	for (int i = 0; i < x; i++)
	{
		for (int j = 0; j < y; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
	test(arr, 3, 5);
	return 0;
}
6.3 函数指针变量

函数指针变量是用来存放函数的地址的,未来通过地址可以调用函数的。形如:int (*p) (int x,int y)

那么,函数是否有地址呢?我们观察以下代码:

我们看到,函数是有地址的,而且函数名就是地址,也可以通过&函数名得到函数的地址,如果我们要把函数的地址存放起来就要有函数指针变量.

函数指针变量的使用:

6.4 函数指针数组

函数指针数组就是存放函数的数组,形如:int (*p[ ]) ()。函数指针数组的一个用途是转移表,即:将类型相同的几个函数存放在一个数组中,从而实现函数的连续调用。

计算器的一般实现

void menu()
{
	printf("******************************\n");
	printf("******  1.加法  2.减法   *****\n");
	printf("******  3.乘法  4.除法   *****\n");
	printf("******      5.退出程序   *****\n");
	printf("******************************\n");
	printf("请选择:");
}
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;
}
int main()
{
	int a, b;
	int input = 0;
	int ret = 0;
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入两个操作数\n");
			scanf("%d %d", &a, &b);
			ret = Add(a, b);
			printf("ret=%d\n", ret);
			break;
		case 2:
			printf("输入两个操作数\n");
			scanf("%d %d", &a, &b);
			ret = Sub(a, b);
			printf("ret=%d\n", ret);
			break;
		case 3:
			printf("输入两个操作数\n");
			scanf("%d %d", &a, &b);
			ret = Mul(a, b);
			printf("ret=%d\n", ret);
			break;
		case 4:
			printf("输入两个操作数\n");
			scanf("%d %d", &a, &b);
			ret = Div(a, b);
			printf("ret=%d\n", ret);
			break;
		case 5:
			printf("退出程序\n");
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

使用函数指针数组实现:

void menu()
{
	printf("******************************\n");
	printf("******  1.加法  2.减法   *****\n");
	printf("******  3.乘法  4.除法   *****\n");
	printf("******      5.退出程序   *****\n");
	printf("******************************\n");
	printf("请选择:");
}
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;
}
int main()
{
	int a, b;
	int input = 0;
	int ret = 0;
	int(*p[5])(int x, int y) = { NULL,Add,Sub,Mul,Div };//转移表
	do
	{
		menu();
		scanf("%d", &input);
		if (input <= 4 && input >= 1)
		{
			printf("请输入两个操作数:\n");
			scanf("%d %d", &a, &b);
			ret = (*p[input])(a, b);
			printf("ret=%d\n", ret);
		}
		else if (input = 5)
		{
			printf("退出计算器\n");
		}
		else
			printf("输入错误,请重新输入");
	} while (input);
	return 0;
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱编码的傅同学

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

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

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

打赏作者

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

抵扣说明:

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

余额充值