浅谈C++复合类型(下半篇)—— 指针

一、指针

指针是一种类型,指针变量存储的是其他变量的地址,指针变量是基于其他已经存在的类型创建的。

1. 地址介绍

计算机通过给内存空间编号来进行管理,这个编号就叫做地址。就像快递员送快递需要知道买家的地址,才能进行配送。我们给变量赋值,知道变量的空间地址,才能把值放进去。地址有32位的也有64位的,具体因实现而异。我们用十六进制来简单表示一下32位的地址:
在这里插入图片描述
1字节=8位,在内存中一般都是以字节位单位的。一个地址编号标记一个字节的开头,代表这一整个字节内存的地址。

2. 变量名的实质

变量名是某个内存空间的标识符,实际上它代指这块空间的首地址,在编译过程中,变量名会被解释为为这块内存空间的首地址。如:int age = 18; 首先这是一条声明语句,编译器看到int类型知道需要四个字节的空间,然后找到一块四个字节的空间,把这块空间标识为age,然后把18转换为二进制存入其中。如下:
在这里插入图片描述
而实际上age代表的是地址00000008,因为我们已经声明了age的类型为int,占四个字节,然后编译器从00000008地址这个位置往后取出四个字节就是age变量存储的值18。使用变量名只是为了方便程序员写代码和他人阅读,你看到age就会想到年龄认为这应该是个整型变量,给你看地址00000008你能知道代表啥意思?

3. 指针变量的声明与初始化

指针变量存储的是其他变量的地址,那我们如何得到其他变量的地址?在C++中我们可以通过地址运算符&来获取变量的地址。如:
在这里插入图片描述
上述代码通过地址运算符得到变量age的地址,然后使用cout显示。由于地址一般使用十六进制表示,而且我的电脑是64位的,所以输出16位十六进制数。那么我们现在来声明一个指向上面age变量的指针:

// 声明指向age变量的指针
int *page = &age;

首先,*运算符被称为间接值或解除引用运算符,代表变量page是一个指针,int则是该指针指向的对象的类型。可以称呼page为指向int类型的指针,然后把page初始化为指向变量age。也可以采用下面这种格式:

// 声明指向age变量的指针
int* page = &age;

这两种格式就是解除引用运算符的位置不同,第一种强调page是一个指针,第二种强调int*是一种类型——指向int的指针。注意:同时声明多个指针变量时,每个指针变量前面都要加上解除引用运算符*,如下:

int *p1, p2;  // 创建了一个指向int的指针和一个int类型的变量
int *p1, *p2;  // 创建了两个指向int的指针

第一条语句创建了一个指向int的指针和一个int变量,第二条语句创建了两个指向int的指针。指向其他类型的指针的创建和初始化也是如此。

4. 指针变量的使用

通过指针可以间接访问指向的对象,对指针使用解除引用运算符*的得到的就是变量本身,如:
在这里插入图片描述
首先,我们可以看到通过对指针pa使用解除引用运算符*,可以得到变量a中的值,而且修改*pa后,变量a的值也跟着变。可以这么理解,首先pa存储的是变量a的地址,然后可以通过这个地址找到变量a的内存空间,最后通过*pa来使用这块空间。也就是说*pa和a代表的是一个意思,*pa改变a跟着改变,a改变*pa也跟着改变。再讲的通俗一点,就是*pa也是变量a这块空间的标识符,只不过a是创建的时候就标识的,*pa是后面标识的。如图:
在这里插入图片描述
那如果我们使用了没有初始化的指针会怎么样?首先,在函数内部声明的变量如果没有初始化,那么它里面存储的是随机值。也就是说如果我们定义了一个指针变量,如:int *p; ,那么p存储的地址是随机的,也不知道p指向哪块空间。如果指向的这块空间上面刚好存储了重要的信息,接着又通过*p去改变这块空间上存储的值,那么将会产生很严重的后果。所以,创建指针的时候最好初始化,如果实在不知道初始化时指向哪里,可以把指针初始化为空指针(nullptr),然后通过if条件语句进行判断,if条件语句将在后面介绍。如下代码创建了一个指向int的指针,并初始化为空指针:

// 创建一个指向int的指针并初始化为空指针
int* p = nullptr;

我们也可以给pa赋值,让它指向其他int变量。如下:
在这里插入图片描述
本来pa是指向a的,可以通过*pa来使用a。然后通过给pa赋值,使pa指向b,就可以通过*pa来使用b。

5. 指针的运算

指针存储的是地址,但计算机通常把它当作整数处理。然而从概念上来看它们是有本质区别的,整数是可以进行加、减、乘和除等运算的数字,而地址只是对位置的描述,用地址来进行算数运算是没有意义的。但是,指针变量可以加减整数,我们通过下面代码来进行说明:
在这里插入图片描述
首先,我们创建了一个int变量a,然后创建了一个指向int的指针pa指向它。我们分别输出了pa和pa加1之后的值,发现原本pa的值是变量a的地址,加1之后往后移动了四个字节。我们来分析一下这个过程,首先pa是指向int类型的指针,int类型占4个字节大小,所以对pa加1相当于增加4个字节,减1就是减少四个字节,加减其他整数也是一个道理。然后pa把从这个新地址开始的后面四个字节当作一个int类型,可以通过*pa来访问,但是这块空间我们并没有使用权限,这样的访问是非法的。当然可以通过强制类型转换把整数转换为对应的指针类型,但是这并没有什么意义。

6. 指针和数组

6.1 数组名和指针的关系

在上半篇讲数组的时候我提了一嘴,说数组名其实是数组第一个元素的地址,那现在我们就来解释一下。首先,我们是通过下标来访问数组的每个元素的,如:

// 创建一个包含三个元素的int数组,并给最后一个元素赋值
int arr[3];
arr[2] = 10;

上述代码通过下标访问arr数组的第三个元素,并把它的值设置为10。而实际上arr[2]代表的是这个形式*(arr+2),由于数组在内存中是连续的,而数组名又是数组第一个元素的地址,arr+2刚好跳过前面两个元素,指向第三个元素,然后对其使用解引用操作*(arr+2),来访问第三个元素。而我们使用下标去访问只是为了方便和更好理解,当然我们也能直接通过上面对指针的解引用去访问数组元素,这两中方式是等价的,如下代码:
在这里插入图片描述
可以看到两种方式都可以访问数组中的元素。通过上述讲解,数组名实际上就是对应类型的指针,如上述数组arr就是指向int的指针(int*类型),只不过它指向的对象是固定的,不能改变,因为这是静态数组,在创建数组时就已经分配好了空间,这个空间的首地址已经固定死了。我们可以通过对数组第一个元素取地址来证明数组名是第一个元素的首地址:
在这里插入图片描述
显然它们的值相等,都表示为数组第一个元素的首地址。通常情况下数组名表示的都是数组第一个元素的首地址,但是有两个特例:
1)对数组名使用sizeof运算符时,得到的是整个数组的大小,这时数组名代表的是整个数组。
2)对数组名使用取地址运算符,得到的是整个数组的地址,这时数组名表示整个数组
我们通过代码来进行演示:
在这里插入图片描述
我们可以看到数组名加1和数组第一个元素的地址加1均只加了4个字节,证明它们表示的都是一个int的地址,而&arr+1增加了12个字节,也就是整个数组的大小,则&arr是整个数组的地址,这时arr代表整个数组,下面的sizeof(arr)也是如此。

6.2 两个指向数组的指针相减

在这里插入图片描述
上述代码创建了一个包含5个元素int数组,然后创建了两个指向的int的指针,分别指向数组的第一个元素和最后一个元素,用指向后面元素的指针减去指向前面元素的指针可以的到两个指针中间相隔的元素个数。由于数组在内存中是连续存储的,p2 - p1的结果其实是16(单位字节),但是编译器自动除以了数组单个元素的大小,也就是int类型的大小4(单位字节),然后得到的结果4就是两指针之间相差的元素个数。只有两个指向同一个数组的指针相减(大的减小的)才能得出它们中间相隔的元素个数。

7. 指针和字符串

有了上面指针和数组的讲解,接下来的指针和字符串就好理解了。首先,字符串是由一个个字符组成的,而C风格字符串用字符数组存储字符串。我们来看代码:
在这里插入图片描述
创建了一个字符数组初始化为字符串"ABCDEF",然收通过cout输出。那么cout是如何来输出字符串的?首先,前面说了数组名是数组首元素的地址,而char类型占1个字节,于是cout从字符’A’的地址开始,把后面的每个字节作为字符进行输出,直到遇到空字符停止,如下图所示:
在这里插入图片描述
这就是为什么C风格字符串要以空字符’\0’结尾的原因。总之,给cout提供一个字符的地址(char*),则它将从该处字符开始往后打印,直到遇到空字符。**在C++和多数表达式中,char数组名、char指针以及用双引号括起的字符串常量都被解释为字符串第一个字符的地址。**我们来通过字符指针来输出字符串:
在这里插入图片描述
首先str代表数组首元素’A’的地址,然后把这个地址赋值给了指向char类型的指针pstr,也可以说pstr指向了这个字符串,然后cout从这个地址开始依次往后输出字符,直到遇到空字符。

8. 指针和结构

对于已经声明了的结构类型,可以创建该结构的指针,也可以通过指针来访问结构成员,如下代码:
在这里插入图片描述
首先,声明了一个Student结构,然后创建了一个该结构的变量Li,并创建了一个指向Student结构类型的指针pLi指向Li。可以看出结构指针的创建格式和普通变量一样,然后指针通过箭头成员运算符(->)来访问结构成员并打印成员信息。当然,也能通过对指针解引用操作,拿到pLi这个结构变量,然后再通过成员运算符(.)来进行访问,由于成员运算符(.)的优先级更高,所以我们要对解引用操作使用括号,如下代码:
在这里插入图片描述
所以通过指针有两种方式可以访问结构成员:
1)直接使用箭头成员运算符(->)访问结构成员
2)先对指针进行解引用操作,再通过成员运算符(.)来访问结构成员

9. 指针和动态内存空间

在上一篇讲述数组的时候,讲述了平时在函数中创建的都是静态数组,数组大小是在编译之前就确定的,只能使用常量或者常量表达式来表示数组大小。而现在我们来学习动态数组,也就是在编译过程中确定数组的大小,可以使用变量来表示数组大小。

9.1 简述存储空间的分类——栈、堆

简单介绍一下存储空间的分类。首先,我们在函数中创建的变量一般都是自动变量,存储在每称为栈(stack)的内存空间中,这种变量的特性是在函数开始执行的时候被创建,在函数执行结束的时候销毁。也就是在函数开始执行的时候,给变量分配空间,函数执行结束了这块空间被操作系统回收了。还有一个特性就是后进先出(last in first out)。栈中内存空间的分配和回收是操作系统来完成的,所以你无法创建动态数组。

但是在堆(heap)中就可以创建动态数组,因为堆中的空间的分配和回收是由程序员来进行管理的。程序员通过运算符new来获取堆中的空间,告诉new需要为哪种类型的数据分配空间,然后new去堆中找到一块合适的内存块,返回该内存的首地址,然后程序员就有了这块内存的访问权限。

栈和堆的区别:
1)栈中的内存空间分配是由操作系统完成的,而堆中的内存空间分配是由程序员来管理的。
2)存储在栈中的变量的随所在函数的运行而诞生,函数运行结束而销毁,而堆中的变量什么时候创建和什么时候销毁都是由程序员来决定的
3)栈的内存空间通常至少为1MB,具体取决于操作系统和编译器。而堆的内存空间至少都是GB起步,可以存储较大的数据。

9.2 使用new运算符来分配内存

上面我们讲了可以使用new运算符在堆中申请内存,现在通过代码来实操一下:
在这里插入图片描述
首先,在堆中申请空间的格式为:new 类型,然后返回该类型大小空间的首地址,所以我们需要使用对应类型的指针来接收这个地址。如上述代码,在堆中申请了一个int类型大小的空间,然后创建指向int类型的指针变量来b接收这块空间的首地址,然后就可以通过对指针b解引用操作来访问这块空间。同时从后面输出的地址就可以看出,这两块空间来自不同的地方,变量a存储在栈中,而b指向的空间存储在堆中。其他类型在堆中申请也是类似的操作,我们来看数组和结构:

// Student结构声明
struct Student
{
	char name[20];  // 存储学生姓名
	int age;  // 年龄
	double weight;  // 体重
};

int main()
{
	// 输入动态数组的元素个数
	int arr_size;
	cout << "请输入数组的元素个数: ";
	cin >> arr_size;
	// 在堆上申请对应元素个数的int数组
	int* parr = new int[arr_size];
	parr[0] = 1;
	*(parr + 1) = 2;

	// 在堆上申请Student结构
	Student* ps = new Student;
	ps->age = 19;
	(*ps).weight = 62.8;

	return 0;
}

创建动态数组之前,需要一个int类型的变量存储输入的元素个数,然后创建对应大小的动态数组。申请动态数组的格式为:new 类型[元素个数],中括号表明申请对象是一个数组。然后便可以通过接收返回地址的指针来访问这个数组。可以使用下标或者解引用操作来访问数组元素。在堆上申请结构也同样如此,通过接收返回地址的指针来对结构进行访问。可以通过指针使用箭头成员运算符(->)来访问结构成员,也可以先对指针解引用,再使用成员运算符(.)来访问结构成员。

9.3 使用delete运算符来释放内存

常言道:有借有还,再借不难。通过new申请的内存也是需要归还的,虽然堆中的内存不少,但是如果你每次借都不还,终有内存被耗尽的时候。所以需要在使用内存的时候申请,不需要的时候释放内存,这样可以提高内存利用率。C++同过使用delete运算符来释放在堆中申请的内存,非数组类型释放格式:delete 接受返回地址的指针,如下代码:

// 非数组类型的内存申请和释放
// 申请内存
int *p = new int;
// 操作
...
// 使用完毕,释放内存
delete p;

其他非数组类型也是如此。
数组类型释放格式:delete[]接收返回地址的指针,如下代码:

// 数组类型的内存申请和释放
int *parr = new int[10];
// 操作
...
// 使用完毕,释放内存
delete[]parr;

[]表明释放的类型是数组,其他数组类型也是如此。

注意:
1)delete运算符只能释放在堆中申请的内存
2)对同一块内存只能释放一次,不可多次释放

二、总结

本文通过指针的使用方法,指针和数组、结构的搭配,还有动态内存的管理三个方面简单讲述指针类型。指针是学习C++强有力的工具,是学习C++的重要基础,学习难度也不小。希望本文可以指针的初学者有所帮助。

作者本人水平不高,在写作的过程中难免会犯错,如果有错请读者及时指出,作者看到了一定会及时更正。谢谢!

  • 71
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值