目录
一、数组名的理解
通常情况下,我们使⽤ &arr[0] (随便写的一个数组名)的⽅式拿到数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且是数组⾸元素的地址。测试:
我们发现数组名和数组⾸元素的地址打印出的结果⼀模⼀样,数组名就是数组⾸元素(第⼀个元素)的地址。但是,有两种特殊情况,数组名不表示数组首元素的地址。
1、 sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩,单位是字节。
2、 &数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)。
除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。测试:
这里sizeof(arr)确实是整个数组的大小,但是 &arr 和数组首元素地址相同,这是为什么呢?其实,&arr 取出的的确是整个数组的地址,不过因为数组在内存中是连续存放的,只要拿到首元素地址就能找到其他元素,所以整个数组的地址就是数组首元素的地址。但是它们也是有区别的。
打印结果:(这里是十六进制,计算相差字节数需要转换为十进制)
这⾥我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,这是因为&arr[0] 和 arr 都是⾸元素的地址,+1就是跳过⼀个元素。但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组。
二、使用指针访问数组
使用指针访问数组有两种基本方式,第一种:
打印效果:
上述代码中,先使用指针记住数组首元素地址,在通过指针+i 使指针指向不同的数组元素,进而实现赋值和打印。
第二种:
打印效果:
这里是因为,数组名arr是数组⾸元素的地址,指针p也是数组首元素的地址,所以它们是等价的,那我们可以使⽤arr[i]访问数组的元素,就可以使用p[i]访问数组的元素。同时,这里将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 还等价于 *(p+i)。同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移量求出元素的地址,然后解引⽤来访问的。
三、一维数组传参的本质
我们知道 ,数组是可以传递给函数的,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,在函数内部求数组的元素个数吗?
我们发现在函数内部是没有正确获得数组的元素个数的。
其实,数组名是数组⾸元素的地址;那么在数组传参的时候,传递的是数组名,也就是说数组传参本质上传递的是数组⾸元素的地址。所以函数形参的部分理论上应该使⽤指针变量来接收⾸元素的地址。那么在函数内部我们写sizeof(arr) 计算的是⼀个地址的⼤⼩(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
打印结果:
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
四、冒泡排序
上述代码演示的是升序排列,冒泡排序的核⼼思想就是:两两相邻的元素进⾏⽐较。如图:
从图中可以看出,第一次内层循环遍历结束后,数组中最大的数被放置到数组末尾,所以第二次进入内层循环时,最后一个元素没有必要参与比较,第二次内层循环结束后,次大的数又被排好,所以每次都会有一个参与比较的数据中的最大值被排好,所以每次内层循环都可以少排一个数据,所以内层循环的条件是sz-i-1,第一次 i 等于0,sz个数据需要比较sz - 1次,之后每次 i +1,内层循环次数每次就减少一次。又因为每次都是排好一个数据,一共sz个数据,当sz - 1个都排好后,最后一个就不需要比较了,所以外层循环需要sz - 1次。
这个代码还可以改进,图中演示的是最坏的情况,不一定所有情况都要排sz - 1次。如图:
这种情况一次就排好了。所以代码可以改进为:
五、二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪⾥呢?存放指针变量的就叫⼆级指针 。
图中的ppa就是二级指针,int * 代表它指向的对象类型是整型指针,后面的第二个 * 代表它自己是指针。对于二级指针而言,*ppa 通过对ppa中的地址进⾏解引⽤,这样找到的是 pa , *ppa 其实访问的就是 pa ,**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的就是 a
六、指针数组
首先我们要知道,指针数组是指针还是数组?我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。所以指针数组,是存放指针的数组。指针数组的每个元素都是⽤来存放地址(指针)的。如图:
指针数组的每个元素是地址,⼜可以指向⼀块区域。例如:
打印效果:
原理图:
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
七、字符指针变量
一般情况下,我们可以这样使用字符指针:
但是还有一种使用方式:
这种方式的本质是把字符串 hello bit. 的⾸字符的地址放到了pstr中。
我们可以看这样一道题:
结果:
**解释:**上述代码中,str1和str2是两个数组,它们会分别在内存中开辟自己的空间,存放相同的字符串,前面说过,数组名是数组首元素的地址,而str1和str2都有各自的空间,只是空间里存的内容一样而已,地址并不相同,所以第一个打印出来的是 not same,而str3和str4指向的是同⼀个常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域,当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str3和str4打印出的是 same。
八、数组指针变量
(8.1)什么是数组指针变量
类比以前学过的内容:整形指针变量: int * pint; 存放的是整形变量的地址,能够指向整形数据的指针;浮点型指针变量: float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
数组指针和指针数组的基本格式很像,我们要区分开,首先我们需要知道 [ ] 的优先级要⾼于 * 号的,所以数组指针要加上(),例如上图:p2先和*结合,表明p2是⼀个指针变量,然后指向的是⼀个⼤⼩为10个整型的数组。所以p2是⼀个指针,指向⼀个数组,叫数组指针。p1没有(),先和 [ ] 结合,成为一个数组,再和 * 结合,表明数组中存放的是整型指针,所以p1是指针数组。
(8.2)数组指针变量的初始化
数组指针变量是⽤来存放数组地址的,要想获得数组的地址,就要用到前面说的:&数组名
例如:
通过调试我们可以看到 &arr 和 p 的类型是完全⼀致的。
数组指针类型解析:
九、二维数组传参的本质
一般情况下, 二维数组传参,形参也写成⼆维数组的形式。
二维数组传参还有另一种写法,不过我们要再次理解⼀下⼆维数组,⼆维数组起始可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。如图:
所以,根据数组名是数组⾸元素的地址这个规则,⼆维数组的数组名表⽰的就是第⼀⾏的地址,是⼀维数组的地址。根据上⾯的例⼦,第⼀⾏的⼀维数组的类型就是 int [3] ,所以第⼀⾏的地址的类型就是数组指针类型 int(*)[3] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
p 是数组指针,p + i 每次会跳过二维数组的一行,*(p+i)是得到当前数组指针指向的一维数组,*(p+i)+j是让指针移动到当前数组指针指向的这一行的一维数组的第 j 个元素,再解引用就能拿到这个元素了。
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
十、函数指针变量
(10.1)函数指针变量的创建
函数指针变量是⽤来存放函数地址的,未来通过地址能够调⽤函数的。如何取出函数地址呢?如图:
从上述代码中可以看出:函数名就是函数的地址,当然也可以通过 &函数名 的⽅式获得函数的地址。如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针⾮常类似。如下:
函数指针类型解析:
(10.2)函数指针变量类型的使用
可以通过函数指针调⽤指针指向的函数。例如:
这里可以通过解引用来找到函数指针指向的函数并进行调用,也可以直接用指针进行调用,因为函数名就是函数的地址,函数指针也是函数的地址,所以函数指针等价于函数名,可以直接用函数指针对函数进行调用。
(10.3)typedef关键字
typedef 是⽤来类型重命名的,可以将复杂的类型简单化。⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:
指针类型也是可以重命名的,⽐如,将 int* 重命名为 ptr_t ,这样写:
但是对于数组指针和函数指针稍微有点区别:
⽐如我们有数组指针类型 int(*)[5] ,需要重命名为 parr_t ,那可以这样写:
函数指针类型的重命名也是⼀样的,⽐如,将 void(*)(int) 类型重命名为 pfun_t ,就可以这样写:
十一、函数指针数组
基本格式:
解释:parr1 先和 [ ] 结合,说明 parr1是数组,数组的内容是int (*)() 类型的函数指针。