C语言——深入理解指针(2)(数组与指针)
文章目录
- 数组名的理解
- 使用指针访问数组
- 一维数组传参的本质
- 冒泡排序
- 二级指针
- 指针数组
- 指针数组模拟二维数组
数组名的理解
之前我们在使用指针访问数组内容时,有这样的代码:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int* p=&arr[0];
这里我们使用&arr[0]
的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址。
我们来做个测试,看数组名到底是不是首元素的地址:
从打印出来的值来看,它们确实是一样的,所以数组名确实是首元素的地址。
那么数组名是首元素地址的话,我们去求地址长度,在32位平台下应该为4个字节(地址的长度与多少位的平台有关)
我们用sizeof
计算下地址长度是多少:
这是为什么呢?
数组名就是数组首元素(第一个元素)的地址,但是有两个例外:
sizeof(数组名)
,sizeof中单独放数组名时,这里的数组名表示整个数组,不是首元素的地址,此时计算的是这整个数组的大小,单位是字节。&数组名
,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
从值的角度来看的话,取整个数组的地址与取首元素的地址打印出来的值一模一样。
这是因为无论是取首元素地址,还是取整个数组的地址,它们的值都是从首元素的地址开始的。
但是我们平时写代码的时候要知道,数组名前+取地址符号是取整个数组的地址。
如果我们想看看它们的区别还是可以看到的,我们给首元素的地址、整个数组分别都+1:
我们发现,前两个地址+1都只跳过了一个整形的大小,而第三个地址+1却跳过了一个数组的大小(40个字节)
所以我们将三个地址打印出来只能看到打印出来的值是一样的,但是类型绝对是不同的,因为int*
类型+1跳过的应该是一个整形,而不是40个字节。
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
使用指针访问数组
有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。
为什么在访问数组的时候可以使用指针呢?
1.因为数组在内存中是连续存放的,只要是连续存放的,只要找到第一个,就可以顺藤摸瓜找到其他的了。
2.指针±整数的运算,方便我们获得每一个元素的地址
接下来我们写个代码,实现用指针访问数组,给数组的每个元素输入值,再输出值。
注意:
数组就是数组,是一块连续的空间(数组的大小与元素个数和元素类型都有关系)
而指针(变量)就是指针(变量),是一个变量(通常是4/8个字节)
那么它俩之间的联系是什么呢?
数组名是地址,是首元素的地址,可以使用指针来访问数组
拓展:
我们知道arr[i]
与*(arr+i)
这两种写法是完全等价的,并且*(arr+i)
里面的式子是加法,加法是支持交换律的。所以arr[i]==*(arr+i)==*(i+arr)
我们代入代码中看会不会错:
结果也是正确的。
既然这三个等价:arr[i]==*(arr+i)==*(i+arr)
*(arr+i)
可以写成arr[i]
,那么*(i+arr)
可不可以写成i[arr]
呢?
我们带入代码试试:
我们发现即使是这样程序也没有问题。那么这说明了什么呢?
我们知道[]
是个下标引用操作符,既然加号操作符+
可以实现:1+2
可以写成2+1
;那么,arr[i]
也就可以写成i[arr]
了
提示:这种方式虽然可行,但是不推荐。
因为这样写不利于阅读代码。
一维数组传参的本质
数组我们学过,数组是可以传递给函数的,现在我们讨论一下数组传参的本质。
首先从第一个问题开始:
写个函数实现数组元素的打印,我们之前都是在函数外部计算数组元素的个数:
那我们可以只把数组传给函数,少传一个参数。然后在函数内部求数组的元素个数再打印数组嘛?
此时我们这样写却出现错误,数组里的元素只打印了一个,这是为什么呢?我们进行调试:
- 按F11开始调试,创建数组执行完后打开监视,查看创建的数组有没有问题:
- 发现创建数组没有问题后按F11进入函数内,在监视里输入
arr,10
查看数组传参有没有问题:
此时数组的传参也没有问题,我们继续调试。 - 当我们继续往下执行的时候却发现原本期望的值是10,这里
sz
却显示的是1
如果sz
是1的话,在下面循环中只能循环一次,所以也就只能打印第一个元素的。
此时就找到了问题所在,是sz
出问题了。
所以这为什么会算错呢?
我们之前讲过,数组传参有两个例外:sizeof(数组名)
, &数组名
。
只有这两种例外表示的是整个数组,而现在这段代码中的Print(arr)
并不算这两种情况,所以此时传的是首元素的地址。
那我们将首元素的地址传过去,Print()
函数接收不应该用指针嘛?
这个函数接收的正确写法就应该是int* p
这样的指针来接收。
那为什么我们之前用int arr[10]
这样数组的形式去接收呢?
因为数组传参的时候,形参的部分是可以写成数组的形式的。对于我们之前初学来说,传的是数组,就用数组来接收,这样讲法对于我们初学接受度很好。
形参的部分虽然可以写成数组的形式,但是本质上还是指针变量(地址)。就相当于这个地方是int* arr
既然Print()
不属于那两种例外,所以传的就是个地址,形参也就是个指针变量。
那么下面的sizeof(arr)
就不是求一个数组的大小了,而是求一个指针变量的大小:
在x86
的环境下,无论什么类型的指针,都是4个字节。
所以数组传参的时候,形参是可以写成数组的形式的,但是本质上还是一个指针变量(地址),下面要求大小的时候
sizeof(arr)
求的就不是一整个数组的大小,而是首元素地址的大小。
所以int sz = sizeof(arr) / sizeof(arr[0])
是得不到元素个数的
还有一点要说明:
我们说数组传参的时候传的是首元素的地址(假设是0x0012ff40
),所以传给函数的地址也是0x0012ff40
。
所以我们在这个函数里使用的都是0x0012ff40
这个地址
既然实参使用的是0x0012ff40
,形参也是使用0x0012ff40
,那么实参跟形参使用的数组不就是一样的嘛?
所以我们得到以下结论:
- 数组传参的本质是传递了数组首元素的地址,所以形参使用的数组跟实参使用的数组一定是同一个数组。
- 既然形参使用的数组跟实参使用的数组是同一个数组,所以形参的数组是不会单独再创建数组空间的,形参的数组是可以省略掉数组大小的:
注意:无论形参部分写不写10,都不影响下面int sz = sizeof(arr) / sizeof(arr[0])
这个表达式(正确写法时这个表达式一定要放在函数外面!)
接下来我们将这段错误代码改过来:
1.既然数组传的是地址,函数的形参就写成指针
2.求数组元素的表达式放在函数外面
正确代码:
冒泡排序
冒泡排序解决的是排序的问题。
冒泡排序的核心思想就是:两两相邻的元素进行比较。
假设我们这里有一串降序的数字:9 8 7 6 5 4 3 2 1 0
我们要排成升序:0 1 2 3 4 5 6 7 8 9
,此时该怎么用冒泡排序排成升序呢?
就这样一对一对的比较下去,最后9会到最后:
因为9是最大的一个元素,所以它无论与谁进行比较,都来到最后一位。
这样将9放在最后,我们叫做一趟冒泡排序
所以剩下该解决9前面的数字
所以,一趟冒泡排序解决一个数字。第一趟解决了最大的,第二趟解决了次大的…
那么这个地方有10个元素,要有几趟冒泡排序呢?
9趟。因为前9个数字在进行冒泡排序后,最后一个数字应该已经在它们应该在的位置上了。
所以是n
个元素的时候,我们需要进行n-1
趟冒泡排序。
我们开始写代码进行实现:
-
先将元素和排序的函数创建出来
-
当我们把排序函数名字写好之后就是传参了,因为我们要排的是这个数组,所以数组需要传进函数。
并且冒泡排序的趟数得依据元素的个数,而函数内部不能求元素个数,所以得在主函数里面求元素的个数 -
接下里写冒泡排序里的内容
-
写形参:指针接收数组(用数组形式也行),
int sz
接收元素个数。
因为这个函数只用排好序就行了,所以返回类型写个void
就可以了。 -
因为冒泡排序是一趟解决一个元素,所以首先要考虑趟数
上面我们说过,当有n
个元素的时候,我们需要进行n-1
趟冒泡排序,所以i<sz-1
-
趟数确定后就思考一趟的排序过程:
一趟排序的过程就是两个相邻元素之间相比较,我们创建一个变量j
视为元素下标,使得相邻两元素之间相比较。 -
接下来我们分析:一趟冒泡排序要进行多少对比较。
以上面的例子来说,当我们进行第一趟冒泡排序的时候,待排序的元素有10个,需要进行9对比较
所以代码循环次数可以写成这样,控制9次相邻元素进行比较
但是我们发现:当我们进行第二趟冒泡排序的时候,待排序的元素只有9个,需要进行的是8对比较
所以比较次数也与趟数有关:
当第一趟冒泡排序时有10个待排元素,我们要进行9对比较;
当第二趟冒泡排序时有9个待排元素,我们要进行8对比较;
即第三趟冒泡排序时有8个待排元素,我们要进行7对比较…
所以我们将循环次数改为:j<sz-1-i
(因为i
控制了趟数)
此时当第一趟冒泡排序的时候,就可以进行9对比较;第二趟冒泡排序的时候,就可以进行8对比较了…
-
-
在冒泡排序函数执行完之后,我们写个函数将这个被排完序的数组打印出来:
完整的代码为:
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-1-i ; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ",arr[i]);}
}int main()
{int arr[] = {9,8,7,6,5,4,3,2,1,0};//排序int sz = sizeof(arr) / sizeof(arr[0]);//元素个数bubble_sort(arr,sz);//打印print_arr(arr,sz);return 0;
}
打印出的结果:
注意:这段代码是可以优化的。
当需要排序的数组本身是接近有序的情况时:9 0 1 2 3 4 5 6 7 8
,此时只需要进行一趟冒泡排序就可以变成:0 1 2 3 4 5 6 7 8 9
那我们该怎么进行优化呢?
-
我们定义一个变量
flag
,在进行每趟冒泡排序之前都假设这趟元素已经有序: -
如果这趟数组不是有序的,将
flag
改为0:
例如9 0 1 2 3 4 5 6 7 8
这个数组第一趟冒泡排序时9与0交换了,所以这趟并不是有序的,就将flag
改为0,进行下一趟的比较。
-
下一趟全部比较完后发现并没有进入到
if
语句中(0 1 2 3 4 5 6 7 8 9
8次比较全部不符合if中的表达式,没进入if语句中),所以flag
还是为1,此时就可以跳出循环,不再进行第3趟、第4趟的比较排序了。
注意:这个break
跳的是这个循环,break
是在这个循环里的
所以,当任何一对元素交换了,就说明这一趟的元素不是有序的,就得进行下一趟的元素比较、交换。
若是下次没有元素的交换,就说明这一趟的元素就是有序的,不用再进行下一趟了,所以直接跳出循环。
二级指针
我们写段常见的代码:
这就是我们之前用的一级指针。
那么什么是二级指针呢?
我们根据上面这段代码画出图:
既然p
也有地址,那么我们就可以通过&p
拿到p
的地址,然后放进变量pp
里。此时pp
就存放着一个一级指针变量的地址,我们叫pp
为二级指针。
那么pp
的类型该怎么写呢?
pp
的类型为int**
,完整写法:int** pp=&p;
对于这个类型int**
我们该怎么去理解呢?
首先我们要知道,int**
是二级指针的类型。
所以一级指针与二级指针的类型理解思路是一样的,分两部分理解:
- 最后一个
*
说明该变量是指针 - 前面一部分说明该变量指向的对象类型
那么,此时p+1
与pp+1
各自跳过几个字节呢?
我们知道,指针±整数与指针的类型有关。因为p
是整形指针,指向的对象为整形,所以p+1
跳过4个字节。
那么pp+1
呢?
因为pp
指向的是int*
类型的变量,也就是指针变量。
所以我们就要看指针变量的大小为多少,就跳过几个字节。有可能是4个字节,也有可能是8个字节。
如果我们想取出pp
的地址可以吗?
当然是可以的,此时得写成:int*** ppp=&pp;
同样的:int**
说明ppp
指向的对象(pp
)是int**
类型的,最后一个*
说明ppp
是指针变量。此时ppp
就是一个三级指针。
可以一直往下推,但是不建议。
那么二级指针到底是怎么样用的呢?
如果我们要通过pp
找到p
有什么办法呢?
解引用pp
将p
的地址打印出来:
*pp
——访问pp
里存的地址,找到这个地址后拿该地址里的数据(对pp里的地址进行解引用)
我们也将a
的地址打印出来,看看*pp
的值是不是a
的地址
发现值确实一样。
我们通过对二级指针解引用,可以找到一级指针内存的数据,再进行解引用,就可以找到10了.
*pp==p==&a;
//通过对pp里的地址解引用找到p(a的地址)
//对*pp再次解引用:
**pp==*p=10
所以,对二级指针变量两次解引用可以间接的找到10.
指针数组
指针数组是指针还是数组呢?
答案是数组。
我们类比一下:整型数组,是存放整型的数组;字符数组,是存放字符的数组。
所以指针数组,是存放指针的数组。
指针数组的每个元素都是地址,可以指向⼀块区域
那么指针数组有什么用呢?
我们可以用指针数组模拟二维数组。
指针数组模拟二维数组
举例:
当我们写下这样的代码的时候,我们知道,这三个数组各自是一块内存空间,在内存里也并不是连续存放的,可能离得很远。
如果我们想把这三块空间弄成二维数组:将这三个数组分别当成二维数组的第一行、第二行、第三行。
我们该怎么办呢?
因为数组名是首元素的地址,我们将这三个数组首元素的地址放入到一个指针数组arr
中,通过访问这个指针数组,再访问到这三个数组。
这样就可以模拟出一个二维数组了:
当我们写上arr[0]
的时候,就是访问arr
数组中arr1
这个元素,而这个元素是arr1
数组首元素的地址,所以我们就可以在这个地址的基础上进行+整数,进行arr1
数组元素的遍历。
同理,当我们写上arr[1]
的时候,就是访问arr
数组中arr2
这个元素,而这个元素是arr2
数组首元素的地址,所以我们也就可以找到arr2
数组中的每个元素了…
//可以看作访问第一行所有元素
arr[0][j]; j:0~4//访问第二行所有元素:
arr[1][j]; j:0~4//访问第三行所有元素:
arr[2][j]; j:0~4
这样就实现了指针数组模拟二维数组
因为arr1
共有5个元素,首元素地址+0为元素1的地址,首元素+1为元素2的地址…首元素+4就为元素5的地址了,所以整数j
的取值范围就为0~4
我们继续写代码:
实现了每个元素的打印。
注意:这种写法并不是真的二维数组,真的二维数组在内存中是连续存放的,而这种写法只是通过地址将三个分散的数组看为三行,再进行打印,这里的arr
并不是真的二维数组。
那么既然arr
并不是真的二维数组,那么代码中的打印为什么要写成二维数组的形式呢?
因为: