哈喽哈喽,各位兄弟姊妹们,我们的深入理解指针已经来到了(2),距离C语言指针神功大成又进了一步.
1.数组名的理解
在深入理解指针(1)中我们有写过一个类似这样的代码
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int* p1 = &arr[0];
- 在这个代码里我们是通过对数组的第一个元素进行取地址操作(&),来获得了数组第一个元素在内存中的地址。
- 其实,在我们的C语言中,数组名本身就是地址,并且这个地址和数组的第一个元素的地址是一样的。
接下来我们来验证一下:
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p1 = &arr[0];
int* p2 = arr;
printf("%p\n", p1);
printf("%p\n", p2);
return 0;
}
运行结果如图:
- 那我们说如果数组名是数组首元素的地址,那么它是不是应该和数组首元素地址一样也是4个字节呢?
接下来让我们看一段代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%zd\n", sizeof(&arr));
printf("%zd\n", sizeof(arr));
return 0;
}
接下来让我们来看运行结果(x64平台下):
- 哎呦!!!怎么回事,这两个数怎么没有按照我们的想法来输出啊,这是为什么捏???那是不是数组名根本就不是数组首元素的地址啊?
- 其实,数组名的确是数组首元素的地址。但是,同志们,在这里我们是有两个特例的:
- sizeof(数组名),这里的数组名表示的不再是数组首元素的地址名,而是,整个数组的地址,这时,sizeof(数组名)计算的就不再是数组首元素所占的大小,而是整个数组在内存中所占的大小,单位是字节(byte)。
- &数组名, 这里的数组名也是表示整个数组,取出的是整个数组的地址,计算的是整个数组在内存中所占的大小,单位是字节(byte)。
- 除此之外,遇到的所有的数组名都是 数首元素的地址。
让我们通过代码来看一下:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
代码运行结果如图(x86平台下):
- 从这里我们看到这三者确实都是数组首元素的地址。
大家知道了这个东西,但是可能大家不太理解,没关系,接下来我们画图来表示一下:
- 那大家可能会更疑惑了,既然这样,那它们之间有什么区别呢?接下来我们来看下面一段代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
运行结果如图(为了好观察,我们这一次在x86平台下运行),来吧,展示:
- 这时我们就真实的感觉到了它们的不一样了,这里其实就验证了我们前面讲到的特例。
- 我们可以看到当&arr[0]进行+1操作之后跳过了4个字节(一个元素),arr进行+1操作之后也是跳过了4个字节(一个元素),而&arr进行+1操作之后跳过40个字节(整个数组)(按照十六进制进行计算).
这个时候如果画图表示的话是这样的(注意看对应指针指向的地址的变化):
- 这个时候又有小伙伴会有疑问,前面我们有提过arr是int的类型的,所以它跳过了4个字节,&arr[0]也是int类型的,所以它也跳过4个字节,那&arr一下跳过40个字节,它是什么类型的呢?这个问题我们会在后面给大家解答的,大家不要着急哦.
2.使用指针访问数组
- 我们之前访问数组都是通过下标的方式来访问的.
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
- 现在我们已经学习了指针,对指针有了一定的认识,我们可以通过指针来访问数组.
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//int* p = &arr[0]----深入理解指针(1)的写法
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;
}
return 0;
}
- 那我们能不能使用指针给数组输入10个值呢?当然可以了.
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;//int* p = &arr[0]----深入理解指针(1)的写法
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//输入10个值(使用下标的方式)
/*for (i = 0; i < sz; i++)
{
scanf("%d", &arr[i]);
}*/
//输入10个值(使用指针的方式)
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
}
//输出(方法1)
//for (i = 0; i < sz; i++)
//{
// printf("%d ",*p);
// p++;
//}
//输出(方法2)
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));//也可以写成 *(arr+i) 或者写成p[i]
}
return 0;
}
- 最后我们总结一下输出时的写法:arr[i]== *(arr+i) == *(p+i) == p[i] 这四种写法是完全等价的.
3.一维数组传参的本质
- 我们大家可以回忆一下我们在给一个函数传参的时候,如果我们要将一个数组传给函数时,我们要把数组和元素个数都传给这个函数。
- 所以说,我们之前都是都是在函数外部进行计算数组元素的个数,那么能不能在函数内部计算数组元素的个数呢?
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
大家认为这个代码运行的结果是什么呢?(x86平台下)
我们看到sz2居然是1,为什么是1呢?
- 我们可以看到我们在调用text()函数时,给函数传的参数是arr,我们传的既不是sizeof(arr),也不是&arr,所以我们传过去的是数组首元素的地址,相当于传过去的是&arr[0].所以说,我们本质上传过去的是一个地址,那我们在接收的时候就应该用一个指针(int* arr)来接收,而不是用数组来接收.
- 所以说,函数形参的部分理论上应该是用指针变量来接收数组首元素的地址.,然而我们是用一个数组来接收的,那么在text()函数内部我们用sizeof(arr)计算的是一个地址在内存中所占的大小,单位是字节,而不是整个数组.
- 我们要明确以下两点:
- 一维数组传参的时候,传过去的是数组首元素的地址.
- 形参的部分可以写成指针的形式,也可以写成数组的形式,但,本是上都是指针,写成数组的形式是为了方便理解.
4.冒泡排序
- 假设我们有一个无序的数组,要求把这个数组排成有序的.我们该怎么实现呢?大家知道排序有很多方法,今天我们主要给大家介绍一下冒泡排序.
我们先来了解一下冒泡排序的核心思想:
- 两两相邻的元素进行比较,如果不满足顺序就交换,满足顺序就交换,请看图:
- 我们就这样两两进行比较,一直走到和最后一个元素进行比较,我们将这个就叫一趟冒泡排序,当完成第一趟冒泡排序时,我们就可以看到已经将所有元素中最大的那个数字排到了最后,所以一趟冒泡排序就搞定了一个元素的顺序.
- 大家想一下,那我们最坏的情况下需要进行几趟冒泡排序呢?
答案是(sz-1)趟,当我们进行第(sz-1)趟冒泡排序时,最后一个元素已经被排序好了,这个大家琢磨一下就会明白的.或者可以自己用几个数字画一下冒泡排序就能很直观地get到.
来看一下代码实现:
//冒泡排序
#include<stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
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(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = {9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
代码运行结果如图:
- 我们在没有学习指针前这样写,那现在我们学习了指针,是不是能把它改成指针的形式呢?我们来实现一下吧!
//冒泡排序(指针)
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
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(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
- 但是,现在我们的代码是可以再进行优化的,比如:一个数组已经是有序的并且已经是我们按照我们想要的顺序排列好了,那么这个代码也会这样一直地比较下去,效率很低,所以,这个时候我们就想能不能让这个代码效率再高一些.
- 如果是这样的话,那我们是不是在进行第一趟冒泡排序的时候如果一次都没有发生交换,那是不是就代表这些元素已经是有序的了.有了这个思路,我们就可以按照这个思路来进行优化.
//冒泡排序(优化)
void bubble_sort(int* arr, int sz)
{
int i = 0;
int j = 0;
for (i = 0; i < sz - 1; i++)//冒泡排序的趟数
{
//一趟冒泡排序
int flag = 1;//我们规定,有序为1,无序为0;
for (j = 0; j < sz - 1 - i; j++)
{
//如果满足前一个数小于后一个数就交换
if (*(arr + j) > *(arr + j + 1))
{
flag = 0;//一旦发生交换我们就让flag为0
int tmp = *(arr + j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
}
}
//如果进行了一趟冒泡排序之后,flag仍然为1,
//就证明这个数组已经是有序的了,我们就跳出循环。
if (flag == 1)
{
break;
}
}
}
void print(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
}
int main()
{
int arr[10] = { 9,8,7,6,5,4,3,2,1,0 };//现在我们给定的是一个升序的数组,我们来给它排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
print(arr, sz);//打印排序前
bubble_sort(arr, sz);//冒泡排序实现
print(arr, sz);//打印排序后
return 0;
}
5.二级指针
是不是听着这个名字,感觉很高级,大家不要被唬住了,其实很简单的,接下来我们一起来看吧!
- 什么叫二级指针呢?在考虑这个问题前我们是不是应该考虑一下一级指针是什么?
- int a = 10;
int* p = &a;//p就是指针变量,一级指针变量- 我们知道指针变量p里面放的整型变量a的地址,那指针变量p也有地址,那我们能不把p的地址也存放起来呢?
- int** pp = &p;//pp是二级指针变量
所以说,二级指针就是用来存放一级指针变量的地址的.
在这里我们的(int * * p)我们该怎样来理解呢?
- 那我们能不能通过二级指针pp来找到a呢?答案是可以的.
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
**pp = 20;
printf("%d\n", a);
return 0;
}
来看一下结果:
6.指针数组
- 大家看到这个名字是不是就开始疑惑了,指针数组到底是指针呢?还是数组呢?我们来类比一下.
- 整型数组 int arr[10]; //存放整型的数组
- 字符数组 char arr[10]; //存放字符的数组
- 指针数组 -------->存放指针的数组???//yes yes yes
7.指针数组模拟二维数组
- 接下来我们试着来使用指针数组来模拟实现二维数组
/*
我们定义了一个指针数组arr用来存放三个数组的地址,
然后我们通过arr[i]来访问指针数组,假设此时i=0,
从上图中我们可以看到arr[0]指向的就是数组arr1,
然后我们再通过访问arr1的下标来获取arr1数组中的元素.
*/
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* arr[3] = { arr1, arr2, arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}