一、数组名的理解
数组名就是地址,而且是数组首元素的地址。
任务:运行以下代码,看数组名是否是地址。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
输出结果:
运行以上代码,我们发现数组名和数组首元素的地址打印的结果一模一样,数组名就是数组首元素(第一个元素)的地址。
疑问:数组名如果就是首元素的地址,那么下面的代码证明理解?
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%d\n", sizeof(arr));
return 0;
}
输出结果:40,如果arr是数组首元素的地址,那么输出的应该是4/8才对。
其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:
- sizeof(数组名):sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
- &数组名:这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
要了解arr和&arr有什么区别,请看以下代码:
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
printf("&arr[0] = %p\n", &arr);
printf("&arr[0]+1 = %p\n", &arr+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;
}
输出结果:
这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1相差4个字节,这是因为&arr[0]和arr都是首元素的地址,+1就是跳过一个元素。
都是&arr和&arr+1相差40个字节,这里是因为&arr是整个数组的地址,+1操作就是跳过整个数组的。
到这里大家应该搞清楚数组名的意义了吧。
数组名是数组首元素的地址,但是有两个例外( sizeof(arr)和&arr )。
二、使用指针访问数组
有了前面的知识支持,再结合数组的特点,我们可以很方便的使用指针访问数组了。
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int* p = arr;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);//p+i 是地址
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//*(p+1) 解引用操作符(*),是数值
}
return 0;
}
这个代码搞明白了,我们再试一下,如果我们再分析一下,数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]访问数组元素,那p[i]是否也可以访问数组呢?
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int* p = arr;
//输入
for (i = 0; i < sz; i++)
{
scanf("%d", p + i);
//scanf("%d", arr + i);//也可以写成这样
}
//输出
for (i = 0; i < sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
在第17行的地方,将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]等价于*(p+i)。
因为数组名arr和p是等价的,所以arr[i]等价于*(arr+i)。数组元素的访问在编译器的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。
三、一维数组传参的本质
我们知道数组是可以传递给函数的,这里我们讨论一下数组传参的本质。
首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给一个函数后,在函数内部求数组的元素个数吗?
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
输出结果:
在函数内部求数组的元素个数,输出的结果是1,并没有正确获得元素个数。
这就要学习数组传参的本质了,第一节数组名的理解,我们知道:数组名就是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说数组传参本质上传递的是数组首元素的地址。
所以函数形参的部分理论上应该是用指针变量来接收首元素的地址。那么在函数内部写sizeof(arr)计算的是一个地址的大小(单位字节),而不是数组的大小(单位字节)。正是因为函数的参数部分本质是指针,所以在函数内部是没有办法求数组元素个数的。
#include <stdio.h>
void test2(int arr[])
{
int sz2 = sizeof(arr) / sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
void test3(int *p)
{
int sz3 = sizeof(p) / sizeof(p[0]);
printf("sz3 = %d\n", sz3);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test2(arr);
test3(arr);
return 0;
}
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
四、冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较。
方法一:
#include <stdio.h>
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;
}
}
}
}
int main()
{
int arr[10] = { 3,1,7,5,8,9,0,2,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
//冒泡排序
bubble_sort(arr,sz);
int i = 0;
//打印输出
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
方法二:优化
#include <stdio.h>
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设这⼀趟已经有序了
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
flag = 0;//发⽣交换就说明,⽆序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1)//这⼀趟没交换就说明已经有序,后续⽆序排序了
break;
}
}
int main()
{
int arr[10] = { 3,1,7,5,8,9,0,2,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
//冒泡排序
bubble_sort(arr, sz);
int i = 0;
//打印输出
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
}
五、二级指针
5.1 指针的定义
一级指针:是一个指针变量,指向一个普通变量,并保存该普通变量的地址
二级指针:是一个指针变量,指向一个一级指针,并保存该一级指针的地址
二级指针是一个指向指针的指针变量。它存储了一个指针的地址,该指针又指向另一个变量的地址。
5.2 引入二级指针
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
int* pa = &a;
int** ppa = &pa;
//一次解引用*ppa,此时类型int*
*ppa = &b;
//二次解引用**ppa,此时类型int
**ppa = 200;
return 0;
}
逻辑关系如下:
a是一个int类型的变量,一级指针pa指向a,并保存a的地址。
二级指针变量ppa指向一级指针pa,并保存pa的地址
二级指针ppa解引用操作:
- 一次解引用:*ppa的类型变成 int*(代表一级指针pa)间接改变了pa的指向,从a的地址变成了b的地址。
- 二次解引用:ppa的类型变成了 int (代表变量b),此时ppa = 200;(等价于b=200)。
(1)下面举个例子:
#include <stdio.h>
int main()
{
//普通变量
int a1 = 1;
int a2 = 1;
int a3 = 1;
//一级指针
int* p1 = &a1;
int* p2 = &a2;
int* p3 = &a3;
//二级指针
int** s = &p1;
}
(假设a1,a2.a3空间连续,p1,p2,p3空间连续)逻辑图如下:
表达式 | 移动字节数/值的变化 | 类型 |
---|---|---|
s+1 | sizeof(int*)*1 = 4*1 = 4 | int** |
*s+1 | sizeof(int)*1 = 4*1 = 4 | int* |
**s+1 | a1+1 = 1+1 = 2 | int |
分析:
- s+1 表示二级指针s指向了p2,移动的字节数需要根据指向的数据的空间大小进行计算sizeof(int*)*1 = 4字节,此时s+1还是二级指针,所以类型是int*。
- *s+1 先对s进行一次解引用为*s,相当于操控一级指针p1,然后*s+1,相当于p1指向了a2的地址,所以移动sizeof(int)*1 = 4字节,此时的类型为int*。
- **s+1 先对s进行二次解引用为**s,相当于操控变量a1,然后a1加1,所以a1=2;a1的类型是int。
总结:在对二级指针变量s的移动时,s都会将已经保存的一级指针的类型进行解析步长(s+sizeof(p)*n);而一级指针*s(相当于p一级指针变量)会以保存的变量的类型进行解析步长(*s+sizeof(a)*n)。
六、指针数组
指针数组是多个指针变量,以数组的形式存储在内存中,数组中的每个元素都是一个地址(指针),占有多个指针存储空间。指针数组即存放指针的数组。
指针数组的声明方式为:数据类型 *数组名[数组长度]。
6.1 指针数组模拟二维数组
#include <stdio.h>
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素的地址,类型是int*,可以存放在parr数组中
int* parr[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for(j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
return 0;
}
}
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
上述代码模拟出的二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的
七、数组指针变量
7.1 数组指针变量是什么
上一节我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
数组指针是指针变量,是存放数组的地址,能够指向数组的指针。
任务: int (*p)[5] = { },如何理解?
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | -- |
() | 圆括号 | (表达式)/函数名(形参表) | -- | ||
. | 成员选择(对象) | 对象.成员名 | -- | ||
-> | 成员选择(指针) | 对象指针->成员名 | -- |
这里()和[ ]优先级相同,根据结合律,从左到右运算
()里是*p,p先和*结合,先定义了指针,说明p是一个指针变量,然后指向的是一个大小为5个整数的数组。所以p是一个指针,指向一个数组,叫数组指针。
7.2 数组指针变量怎么初始化
数组指针变量是依赖存放数组地址的,那么怎么获取数组的地址呢?这就要用到&数组名。
如果要存放数组的地址,就得存放在数组指针变量中,如下:
nt arr[10] = {0};
&arr;//得到的就是数组的地址
int (*p)[10] = &arr;
我们调试可以看到&arr和p的类型是完全一致的。
数组指针类型解析:
int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
7.3 二维数组传参的本质
有了数组指针的理解,我们来讲解一下二维数组传参的本质。
过去我们有一个二维数组,徐亚传参给一个函数的时候,我们是这样写的:
#include <stdio.h>
void test(int arr[3][5],int r,int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
这里实参是二维数组,形参也写成二维数组的形式,那还有什么其他写法吗?
这里我们再次理解一下二维数组,二维数组起始可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是一维数组。
所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上面的例子,第一行的一维数组的类型就是int[5],所以第一行的地址的类型就是数组指针类型iny(*)[5]。那就意味着二维数组传参本质上也是传递地址,传递的是第一行这个一维数组的地址。那么形参也是可以写成指针形式的。如下:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
for (i = 0; i < r; i++)
{
int j = 0;
for (j = 0; j < c; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
test(arr, 3, 5);
return 0;
}
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
八、函数指针变量
8.1 函数指针变量的创建
函数指针即定义一个指向函数的指针变量。
定义格式如下:
int (*p)(int x, int y); //注意:这里的括号不能掉
这个函数的类型是有两个整型参数,返回值是整型。
思考:函数是否有地址?
让我们看看以下代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
输出结果如下:
观察发现,确实打印出来了地址,所以函数是有地址的,函数名技术函数的地址,当然也可以通过&函数名的方法获取函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针变量非常类似。如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)() = test;
int Add(int x, int y)
{
return x + y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以
函数指针类型解析:
int (*pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
8.2 函数指针变量的使用
通过函数指针调用指针指向的函数。
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
printf("%d\n", (*pf)(2, 3));
printf("%d\n", pf(3,5));
return 0;
}
输出结果:
九、函数指针数组
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组。如下:
int *arr[10];
//数组的每个元素是int*
那要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
parr1 先和 [] 结合,说明parr1是数组,数组的内容是什么呢?
是 int (*)() 类型的函数指针。
十、转移表
函数指针数组的用途:转移表
举例:计算机的一般实现
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
使用函数指针数组的实现
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("请选择:");
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输⼊有误\n");
}
} while (input);
return 0;
}