C语言基础 -- 指针(上),基本概念这样讲会更清楚。

零、前言

指针,是C语言中很重要的一个概念,也是C语言与其他高级语言的的一个重要区别。指针,像一把双刃剑,运用得当,可以灵活的操作内存;运用不当,也容易让我们写的程序在运行时崩溃掉。

很多初学C语言的同学来都觉得指针很难理解,运用起来更难。但是所谓的“指针难学”,并不是同学们的原因,更不是“指针”本身的问题,而是因为很多博主甚至老师对“指针”的理解和表述不太准确,有时会把把指针和地址混为一谈,导致同学们学习指针时条理不清晰。下面我们就用一个更容易理解的逻辑条理,为大家讲解指针。

一、地址及其妙用

我们知道有一句话,叫作“变量要先声明(或'先定义',这里更准确的说法是'先声明')再使用”。这背后的底层原理是这样的。所谓的使用一个变量无非就是读取或者改变一个变量的值。就拿赋值来说把,当你把一个值3赋值给一个变量a时,底层的操作就是把3这个值按照一定的整数编码的形式,填入到一块与变量a对应的内存里。于是在赋值前你首先需要分配到一块内存,这块内存只属于变量a,只能以a的名义使用。这就像是宣誓一块土地的所有权一样,因此我们用“声明”(declare)这个词来命名这个操作。小结一下,我们通过变量声明语句在底层帮我们完成了内存分配的工作,进而我们能够使用到这块内存。观察下面的代码及其运行结果:

代码1:

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

输出1:

6422044 4

在上面的代码中我们声明了一个int型变量a,输出1中的第一个值6422044就是分配给变量a的那块内存的地址编号(简称为地址)表示为十进制整数后的值(这里这个地址编号的值是因人而异的)。提醒一下,变量a的地址其实是内存中一块内存的编号,这个编号可以表示为十进制整数,也可以表示为十六进制数;我们为了方便,用十进制整数来输出了这个地址。输出1中的第2个值4,就是分配给变量a的内存的大小。这里稍微展开讲一下,我们能够分配的最小的内存单元是一个字节(byte),我们把CPU能够后控制的所有内存(也叫寻址空间)按照最小单位,分成很多份,每一份有唯一的一个地址编号。例如在上例中变量声明语句为变量a分配到了4个字节的内存,地址编号分别是6422044、6422045、6422056、6422047;其中6422044就定义为变量a的地址(编号最小的那份内存的地址)。

以上简单说明了变量和它的地址的概念,下面我们要讲一下变量地址的作用。

我们知道C语言是编译语言,即我们所写的C语言源代码,必须通过编译转换成可执行程序(也叫可执行代码,也叫机器码)才能运行。于是我们说在源代码这个层面,对变量a的任意一个运算操作(例如:a++,a += 2,5%a等等吧),转化到程序(也叫可执行代码,也叫机器码)这个层面,都是对变量a所对应的地址(包括该地址里面存放的值)的一个相对应的操作。

我们不禁要问,既然在C语言源代码层面,我们可以通过地址运算符&后面跟变量名a的形式,即&a,来引用到变量a的地址,那么我们能不能同样在源代码层面,用变量a的地址来对变量a进行运算操作呢?答案是:能,方法是间接引用。请看下面两的代码。

代码2-1:

#include <stdio.h>
int main () {
	int a = 3;
	printf("%d\n", a);
	a += 2;
	printf("%d\n", a);
	return 0;
}

输出2-1:

3
5

代码2-2:

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

输出2-2:

3
5

在代码2-1中,我们初始化了一个变量a=3,然后通过对a进行+=2的操作,改变a的值为5;而在代码2-2中,我们同样是改变了a的值为5,但是这并不是通过对变量a本身的操作完成,的而是对&a,即变量a的地址的操作完成的。这个操作叫作“间接引用”,操作符是*。具体来说,我们用间接引用运算符*,结合变量地址&a,获得了对变量a本身的引用效果;后面的+=2,如同是对a的操作一样。(这里可能会有同学提出疑问,觉得这个例子不够严谨,虽然是对地址的间接引用,但是获取地址的时候还是用到了变量名。我们后面会举另外例子,更能说明问题。)

讲到这里,我们看到了变量地址的妙用,能够通过对地址的间接引用进而改变量的值。那么就会有这样的需求,比如在写前面的源代码时声明过一个变量a,后面因为某种原因忘记了或者没办法知道这个变量的变量名,但是我们还想改变这个变量的值。假设我们记住了a的地址是p,那么我们就可以"间接引用",对*p操作,来改变a的值。那么如何记住一个变量的地址呢?最朴素的想法就是,在声明了这个变量a之后,我们立即把a的地址,即&a储存起来,比如把&a赋值给新的变量p,即p=&a。于是我们自然会想到,这个新的变量p,在赋值之前,必须要先声明。如何声明变量p?把p声明成什么类型的变量呢?对这个问题的回答,就自然而然的引出了"指针"的概念。

二、指针 -- 特殊的变量、用来存放变量地址的变量

上一小结我们引出了一个问题,就是怎样储存一个变量a的地址,我们说要用一个新的变量p来存放,那么如何声明新的变量p呢?我们首先来看一下一个变量a的地址,需要多大的内存空间呢?

代码3:

#include <stdio.h>
int main () {
	char a = 'c';
	int b = 2;
	float c = 3;
	double d = 4;
	printf("%d %d %d %d\n", sizeof(&a), sizeof(&b), sizeof(&c), sizeof(&d));
	return 0;
}

输出3:

8 8 8 8

通过代码3的输出我们可以看出,不管变量的类型是char、int、float还是double,它们的地址的大小都是8个字节。这个数字适和我们操作系统和cpu支持的寻址能力有关系的,比如说我的电脑是64位系统(这里的64位是指64bit位,也就是支持最大64比特寻址能力),于是在我的系统里变量的地址所占的内存就是8个字节,正好是64bit。

那么要让新的变量p存放下一个变量a的地址,它是一个8个字节的值,我们用什么类型关键字去声明p呢?纵观我们学习过的变量类型关键字,如:char、int、long long int、float还有double,只有用double和long long int声明的变量所占内存是8个字节。于是我们会想能不能用double或者long long int去声明一个新的变量p,然后用p去存放变量a的地址呢?答案是:不能。因为C语言的语法不允许,并且C语言有专门地语法来声明这样地变量。

C语言为我们设计了专门的数据类型来声明存放一个变量地址的变量。这个类型就是“指针类型”,用“指针类型”声明的变量就叫做“指针变量”,简称为“指针”。但要说明一下,C语言虽然有“指针类型”,但并没有一个专门的关键字,比如说pointer、address来表示“指针类型”,而是通过我们之前学过的数据类型结合一个*号,来表示"指针类型" ,如:char*、int*、float*和double*。声明指针变量的语法也很简单,就是:指针类型 + 指针变量名。

上文提到的char*、int*、float*和double*都是“指针类型”,用他们都可以声明指针变量。那么区别在哪?看下面这段代码中的写法。

#include <stdio.h>
int main () {
	char a = 'c';
	char* pa = &a;

	int b = 2;
	int* pb = &b;
	
	float c = 3;
	float* pc = &c;
	
	double d = 4;
	double* pd = &d;

	printf("%d %d %d %d\n", &a, &b, &c, &d);
	printf("%d %d %d %d\n", pa, pb, pc, pd);
	return 0;
}

从上面的代码可以看出,用char* 声明的指针变量pa,pa能够存放一个变量a的地址,但仅限于变量a也是char型的;从另一方面来说,如果你有一个char型的变量a,现在你需要用一个指针变量p去存放&a,那么你必须用char*去声明变量p。我们为了简便地说明指针变量pa所存放的地址是一个char型变量a的地址,我们习惯称“指针p指向char型变量a”。

int*、float*、double*同理。

以int为例,我们理解“指针类型”时可以把int和*的结合写成int*,即int后面紧接*,之间没有空格。但是在写源代码的实践中以下三种写法在声明指针变量时都是对的,即

#include <stdio.h>
int main () {
	int a = 2;
	int* pa = &a;

	int b = 2;
	int * pb = &b;
	
	int c = 3;
	int *pc = &c;
	
	printf("%d %d %d\n", &a, &b, &c);
	printf("%d %d %d\n", pa, pb, pc);
	return 0;
}

并且,更推荐最后一种写法,即int和*之间有一个空格,*和指针变量名之间没有空格。这样写的好处是我们还可以在一行中,既声明普通变量,同时又声明指针变量。例如:

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

这里提醒一下:不管用那种写法,变量名pa前面的*都只是用来标志这个变量是一个“指针类型”,*本身并不是指针变量名的一部分,也不要理解为是间接引用符号。指针变量声明好以后,它前面出现的*才是间接引用符号,功能是对这个指针变量所储存的地址的间接引用。

三、用“指针类型”强制转换得到地址

最后我们要说,“指针类型”不仅可以用来声明指针变量。还可以把一个整数强制转化为一个内存地址并赋给对应类型的指针变量。看下面的代码:

代码4:

#include <stdio.h>
int main () {
	int a = 3, *pa;
	pa = (int *)6422036;
	(*pa) += 2; 
	printf("%d %d\n", &a, a);
	return 0;
}

输出4:

6422036 5

甚至可以不用把强制转换来的地址赋值给一个指针变量,直接用强制转换来的地址,通过间接引用,来改变以这个地址对应的变量的值。前提时你已经猜到的一个变量的地址的十进制表示的数值,恰好就是你强制转换的那个值。例如:

代码5:

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

输出5:

6422044 5

在上例中,我们猜到了变量a的地址编号的十进制表示值为6422044(其实并不是猜到的,而是提前编译运行了代码,打印出来&a的值是6422044,代码修改后并没有声明其他新的变量,所以编译器大概率还会把编号为6422044的地址分配给a)。我们用(int *)把这个值强制转为一个地址,由于a的地址编号是6422044,所以我们强制转换来的地址就正好是a的地址,然后就像第一节讲过的那样,以间接引用地址的方式改变了变量a的值。

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值