指针基础大总结

C++ 基础之 指针数组转圈圈

很多刚刚接触C/C++的同学都会对指针操作颇为迷惑。指针,作为C语言的一大特色,在赋予了程序员极大自由的情况下也对程序员的水平提出了更高的要求。灵活地运用指针可以极大得提升程序的运行效率,但在此之前我们要先搞懂几个基本的概念和用法。

1.何为指针
指针的定义为一个存储地址变量。那么既然是变量我们可以暂时放心,他是一个编程中常用的定义:变量。类比我们常见的变量类型int double bool,指针有何特殊之处呢?指针的存储对象是一个量在内存中的地址。这里我们可以把计算机内存想象成一个个小房子,每个小房里存储一些东西比如整数啦,布尔值啦。但是为了更方便的找到这些值,还有一些小房子充当起来居委会的作用,在那里我们可以找到这个值存储位置!位置!位置!位置!(重要的事情说三遍)。这些位置就是指针变量里存储的东西了。在清楚什么是指针后我们再来谈谈数组。

2.指针和数组
数组,作为一种数据格式,他可以存储多个同类型的值,在内存中是连续的。也就是说数组的索引是连续的:索引为零的位置对应该数组中的第一个元素,索引为1的位置对应该数组中的第二个元素,索引为2的位置对应该数组中的第三个元素,以此类推。对于一维数组来说我们可以使用数组名加方括号的形式索引。同样,在建立数组指针后,我们也可以使用和数组名的方式进行索引*。这个两个方法对于指针变量来说也是通用的。我们可以先暂时认为,在当前的操作上数组名和指针名是等价的。其中,星号的学名叫做解除引用运算符。

	int num[5] = { 1,2,3,4,5 };//建立一个由五个元素组成的整数数组。
	int* pointToNum = num;//用类型 + * + 名称的形式创建整数数组的指针。
	cout << *(pointToNum + 2) << endl;//用指针名与解除运算符来索引。(输出 3)
	cout << pointToNum[2] << endl;//用指针名与方括号来索引。(输出 3)
	cout << pointToNum<< endl;//查看索引为0元素的地址。//输出004FFA54
	//这是一个简单的整数数组的例子,但总有一些特殊的意外,比如字符串数组。这个我们稍后进行讨论。

对于以上代码还有一点——指针算数。在第三行,我们可以这样理解,一个int占有四个字节而这个加法操作即是在内存中顺序向后移动8个字节指导索引为2的元素上(实际是第三个元素)。我们可以在脑子里有这样一幅图像,对指针加法和减法运算使得一个指示标识,沿着数组的维度向前或向后移动n个指向类型的字节数。还有一个值得一说的区别,便于稍后讨论二维数组与二级指针。且看代码。

	int num[5] = {1,2,3,4,5};
	int* pointToint = num;
	cout<<sizeof(num)<<endl;//输出20,也就是整个数组大小
	cout<<sizeof(pointToint)<<endl;//输出4,也就是数组中第一个元素的大小
	//这里需要记住的是使用sizeof()时,数组名输出的是整个数组大小,数组指针输出的数组中第一元素的大小

3.字符串数组和其指针
还记得刚才的那个特殊情况吗?现在我们来具体讨论一下,字符串数组到底是何方神圣。有过C语言基础的小伙伴应该对下面的代码输出是比较熟悉的。

	char word[6] = "hello";
	char* pointer = word;
	printf("%p", pointer);//输出首地址(稍后讨论首地址是啥)
	printf("%s",pointer);//输出hello

在这段代码中如果用C语言的输出方式,我们可以调整函数内的参数来得到一个地址一个值,其中的地址叫做首地址,数字上来说是与字符串数组的第一个元素是重合的(索引为零),但是意义却有很大不同。首地址是把整个字符串数组当做一个整体来看了,如果我们对这个指向首地址的指针进行指针运算的话,比如对其加一, 那么我们得到的不是’e’的地址,而是整个数组末尾的后面,也就是这个加一的运算跨越了整个字符串数组。但是对于C++而言在使用cout进行输出的时候。会出现以下情况。

	char word[6] = "hello";
	char* pointer = word;
	cout<<pointer<<endl;//输出hello,其实是对首地址包含内容的输出
	//那么我们如果想要看word中元素的地址怎么办呢?
	//我们需要先进行类型转换
	cout<<(int*)pointer<<endl;//输出第一个元素的地址。
	cout<<(int*)(pointer + 1<<endl;//输出第二个元素的地址。我们可以看出这时的指针运算是以单个字符为单位的。

为了理解这个现象,我们常试从刚才C语言的输出找找原因。我们发现使用printf("%p", pointer)时是对首地址进行输出,而首地址是把整个数组看成一个整体(或者更形象地说,一个大地址)所以我们用printf("%s", pointer)进行输出的时候就打印出了整个字符串。这也就不难理解为什么在C++中使用cout输出到控制台时会打印出整个字符串了,cout本质上输出了首地址所代表的内容——整个字符串。但是首地址的事就算完了吗?当然不会!我们下面一个例子来更形象地了解一下首地址。请看代码(代码里的注释很重要,这关乎我们能否顺利地理解二维数组和二级指针!)

//这是一个源自于C++ primer plus的例子 (其中&为取地址运算符)
	short tell[10];//建立一个short数组,总共有10个元素,每个元素占2个字节
	cout<<tell<<endl;//按照我们熟悉的理解,使用数组名显示第一个元素的内存也就是 &tell[0]
	cout<<&tell<<endl;//输出首地址,也就是整个short数组的地址,数字上与 &tell[0]相同
	//可以看出我们取首地址的方式与char 类型还是有所不同的,我们这里并不需要类型转换!
	cout<<tell + 1<<endl;//这里我们取到了第二个元素的地址,跨越了2个字节
	cout<<&tell + 1<<endl;//这里我们直接跨越了整个short数组,跨越了20个字节
	//对于指针变量tell,他是一个short的指针,而对于指针变量&tell,他指向了包含10个short的数组
	//对于tell我们可以通过
	short* pointToTell = tell;//的方式创建指针变量pointToTell,但这种方式对于&tell将不再可行,
	//因为tell和&tell所表达的逻辑完全不同。应当使用如下方法声明&tell。
	short*newPointer)[10] = &tell;
	//对于以上声明的意义,想必大家已经明白了,newPointer指向了一个包含10个元素的short数组,而非第一个元素。

4.二级指针
到现在为止,大家对于指针和数组应该已经可以描绘出一个比较清晰的轮廓了,我们这里再探讨一个稍微绕口一点的东西,二级指针,也就是指向指针的指针,按照之前指针的定义,我们不难想象二级指针存储的是一个一级指针的地址。所以用来描述一个指针变量的重要元素包括了他指向的地址他所在的地址!请看代码。

//先看一个简单一点例子
	int i = 3;//一个整数
	int* p = &i;//整数 i 的地址
	int**pp = &p;//用pp二级指针变量接收指针p的地址&p。还记得刚开始的居民类比吗,这里相当于到派出所(pp)查询了某个居委会的地址(&p)。


//再看一个比较简单的例子
	int num[4] = { 1,2,3,4 };
	int* pointer = num;
	int** pp = &pointer; //一个数组指针的指针,存储的是数组指针的地址。
	cout << num <<" "<<pointer<<" "<<&num<<" "<<&pointer<< endl;
	//依次输出为第一个元素的地址,第一个元素的地址,数组num的首地址和数组指针的地址



//现在我们要把之前的所学进行融合了,ready? Go!!!!
	int a = 1; int b = 3; int c = 5; //三个整型变量a,b,c
	int* array[3] = { &a, &b, &c };	 //一个指针数组array,其包含了三个元素,分别是a,b和c的地址。
	//对于指针数组的声明我们结合以下代码思考,就不再赘述
	/*
	int numArrray[6] = {1,2,3,4,6,8};
	int* pN = numArray;
	*/
	int** pointer = array;			 //一个指针数组的指针
	cout << pointer << endl;		 //打印数组指针的地址
	cout<<array<<endl;				 //打印数组指针的地址
	/*对于一个数组来说,打印他的数组名将打印他的第一个元素的地址,
	这里我们把(*array)看成一个数组名,
	所以,打印(*array)将打印第一个元素的地址,
	而打印array将打印该 数组指针 的地址(注意!不是数组的地址)*/
	
	cout << *pointer << endl;//打印a的地址。
	cout << &(**pointer) << endl;//打印a的值。	

对于类似的情况,也适用于结构体,一下是对结构体的代码。至于结构体,他不是今天讨论的主要话题,就不再赘述了。

	struct Year
	{
		int currentYear;
		int nextYear;
		int pastYear;
	}so1, so2, so3;
	so1 = {2020, 2021, 2019};
	so2 = { 2024,2025,2023};
	so3 = { 2001,2002,2000};
	Year time[3] = { so1, so2,so3 };
	//这是一个结构数组

	cout << time[0].nextYear << endl;
	//访问第一个结构里的结构变量nextYear

	const Year* charr[3] = { &so1, &so2, &so3 };
	//这是一个结构指针构成的数组

	cout << charr[1]->currentYear << endl;//用箭头运算符访问指针数组中第二个结构中的currentYear

	const Year** pointTocharr = charr;
	//创建一个指向该数组的指针

	cout << pointTocharr[2]->pastYear << endl;
	cout << (int*)pointTocharr << endl;//强制类型转换查看第一个结构储存的地址

5.二维数组与二级指针
之前的讨论应该还不至于太复杂,带着刨根问底的心态,下一个要讨论的主题是二维数组和二级指针,先说下结果,二维数组不是二级指针,相反他是使用一级指针的对象,也就是刚开始我们所讨论的那种最为 人畜无害 常见的指针。还有一个更加广泛的结论要说一下,无论什么维度的数组,都是一级指针服务的对象,二级指针,三级指针等更高级的指针只服务于比他第一级别的指针!
想要讨论二位数组和二级指针,我们要先来看看二维数组真实的存储形态,印象中,提到二维,首先我们想到的是一个平面,有行有列。但事实上却不是,二维数组也是一个在内存中的以一维状态出现的,只不过分成了好多的子片段这些片段也就是我们数组的第二个维度。
这个是真实情况下二维数组的表现样子,这是一个4*2的二维整型数组,第零轴有四个元素,每个元素里又包括一个长度为2的数组。所以二维数组实际上还是以一维的形式存在,只是可以表达二维的意义了,这好比用在纸上画正方体,图形依旧是一个平面图形,只是可以表达三维的意义。

{1,2}{5,6}{8,9}{11,12}

理解了这个概念,那么对于以下情况的二维数组我们就不应该再感到陌生了。这个二维数组中总共有四个一维数组,每个一维数组的长度是不一样长的。

{1,2,5,3,4}{5,6,7}{8,9}{11,12,13}

继续看代码

	int array[3][4] = { {1,9,3,4}, {5,0,7,8}, {10,7,12,13} };
	int(*pointer)[4] = array;
	//这是声明了一个二维数组的指针,其意义是:他是由四个int指针所组成的数组,指向第一组{1,9,3,4},随着指针加法该指针将不断指向下一组。
/*还记得吗对于一个指针使用sizeof()和对于一个数组使用sizeof()其大小是不同的
假设我们的二维数组是是一个二级指针,那么对她解除引用后应该是一个一级指针,对其使用sizeof的结果应该是4,也就是一个整型的大小。可事实是,看代码*/
	cout << sizeof(*array) << endl;//输出16!array是包含四个int指针的数组
	cout<<array<<endl;//输出第一组元素中第一个元素的地址。
	//这说明*array是一个数组!而非指针!

结语
这次很高兴我们一起讨论了指针,数组的指针,指针数组等一些有意思的概念和用法,希望能够对大家有所帮助,如果各位看官有发现任何不妥的地方欢迎给我留言,我及时更正。谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值