目录
1. 数组名的两种意义
2. 指针访问数组(指针也能下标引用)
3. 一维数组传参的本质 和 sizeof在函数中失效的原因
4. 指针数组
4.1 指针数组的概念
4.2 一级指针数组
4.3 一级指针数组模拟实现二维数组
5. 数组、指针 与 字符串
6. 数组指针(变量)
6.1 数组指针的概念
6.2 一维数组指针(变量)
6.3 行指针(变量)
7. 二维数组传参的本质
1. 数组名的两种意义
数组名在三种不同的情况下共有2种意义:
❶数组名代表首元素【一种】
❷数组名代表整个数组【两种】
情况1 ---- 一般情况:数组名就是数组⾸元素的地址
情况2 ---- sizeof 数组名 或 sizeof(数组名):sizeof中单独放数组名时,这⾥的数组名表示整个数组,计算的是整个数组的大小, 单位是字节。
情况3 ---- &数组名:此时数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的)
虽然说数组名有2种意义,但是我们又发现有这样一个等价式:
数组名的值 == 整个数组的地址 == 数组首元素的地址
即:arr == &arr == &arr[0]
那到底arr,&arr和&arr[0]有什么区别呢?让我们看一下下面的代码:
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;
}
运行结果:
可以看到arr的变化 与 &arr[0]的变化是一样的,加1后都是相差了4个字节(1个int型),这也侧面说明了在一般情况下,数组名就是数组⾸元素的地址。而&arr加1前后相差了40个字节(1个int[10]型),而该数组arr的数据类型就是int[10]型,所以“&数组名”代表的是整个数组。
【没错,数组也是有数据类型的,详细请看《数组基础知识和冷知识(超详细总结)》中的“一维数组数据类型(需知)”和“二维数组数据类型(需知)”的知识点内容】
前面提到,sizeof中单独放数组名时,数组名才代表整个数组。所以如果sizeof中不仅仅只有数组名时,此时数组名还是代表首元素。
比如:
sizeof中的是arr+1,不是单独1个arr,所以此时的arr属于一般情况:arr等于&arr[0]。而arr[0]的数据类型是int型,是4个字节,根据“指针+-整数”的运算规则(即“地址+-整数”的规则),+1等于加上4个字节,所以sizeof(arr+1)的结果是8。
2. 指针访问数组(指针也能下标引用)
在指针之旅(1)中,我们就学过解引用操作符*的知识了。
如果写一个程序来打印数组元素,要求使用指针访问数组,我们会这样写:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);//输出int* p = arr;for (int i = 0; i < sz; i++){printf("%d ", *(p + i));}return 0;
}
而在指针之旅(1)中,我们也学过:我们对数组arr进行下标引用,其实在计算机中会自动转换成解引用操作,即arr[i]会先被计算机解读成*(arr+i)。
那我们能不能反其道而行,用指针p访问数组时写成p[i]呢?:
可以看到是可以这样操作的。于是就有了下面的等式:
*(p + i) == arr[i] == *(arr + i) == p[i]
3. 一维数组传参的本质 和 sizeof在函数中失效的原因
在《函数(子程序)的常见、易混淆概念详解》中,我们学习了一维数组传参的本质:
- 一维数组传参的时候,本质上传递的是一维数组首元素arr[0]的地址,即&arr[0]。
- 函数形参数组是一个伪(一维)数组,本质上是一个一级指针。
正因为它是个指针,所以sizeof对形参数组计算出来的结果是指针的大小(4或8个字节),而不是数组的大小。
代码举例:
4. 指针数组
4.1 指针数组的概念
指针数组是指针还是数组?
我们类⽐⼀下,整型数组,是存放整型的数组,字符数组是存放字符的数组。 所以指针数组是存放指针的数组。
指针数组的定义标准:
- 如果数组中每个元素都是用来存放同级别地址或指针的,那么该数组是一个指针数组。
代码举例:
int main()
{int a = 1, b = 2;int* pi = NULL;int* arr[5] = { &a,&b,pi };return 0;
}
这里的(整型一级)指针数组,既装有地址,如arr[1]的&a和arr[2]的&b;又装有一级指针,如arr[3]的pi。剩下的元素默认初始化为0,即NULL。
对于整型数组和字符型数组,它们的每一个元素的类型是这样的:
而指针数组的每一个元素的类型是类似下面这样的:
4.2 一级指针数组
(多级指针数组不常用,这里只讲解一级指针数组)
一级指针数组的数据类型:
- 去掉数组名,就是一级指针数组的类型:
- 即“ 内置类型 * [数组大小] ”
例如:
int* arr[7]的类型就是int* [7]。
4.3 一级指针数组模拟实现二维数组
我们先设置一个正常的(整型)二维数组:
int main()
{//二维数组的创建int arr[3][5] = { {1,1,1,1,1}, {2,2,2,2,2}, {3,3,3,3,3} };//输出for (int i = 0; i < 3; i++){for (int j = 0; j < 5; j++)printf("%d ", arr[i][j]);printf("\n");}return 0;
}
该二维数组的打印结果:
现在我们用一个(整型一级)指针数组来模拟这个二维数组:
int main()
{//指针数组的创建int arr1[] = { 1,1,1,1,1 };int arr2[] = { 2,2,2,2,2 };int arr3[] = { 3,3,3,3,3 };int* parr[3] = { arr1, arr2, arr3 };//输出for (int i = 0; i < 3; i++){for (int j = 0; j < 5; j++)printf("%d ", parr[i][j]);printf("\n");}return 0;
}
该指针数组打印的结果:
可以看到,两次打印的结果是一样的,这就是指针数组模拟实现二维数组。
虽然模拟出来了,但是两者的内存分布是不一样的。
这是二维数组的分布情况:
二维数组内部是连续的空间,且数组名arr指向的是数组首元素arr[0][0]。
指针数组属于数组,所以parr中的每个元素都是连续,即parr[0]、parr[1]、parr[2]是连续的,但是arr1,arr2,arr3的内存是相互独立的。数组名parr指向的是数组首元素parr[0]。
5. 数组、指针 与 字符串
当我们想用一个载体来装载所需要的字符串时,我们有两种方法。一种是用字符数组装载,比如char str[5] = "abc"; 另一种是用字符型指针装载,比如char* p = "abc";
其实这两种装载方式本质上是不一样的
先说结论:
(1)以字符数组为载体保存字符串,该字符串是可改变的。(可变字符串)
(2)以字符指针为载体保存字符串,该字符串是不可被更改的。(常量字符串)
i. 字符数组 与 可变字符串
我们先看这个代码:
int main()
{char str[10] = "hello";printf("str原来是%s\n", str);str[2] = 'X';printf("str后来是%s\n", str);return 0;
}
运行结果:
可以看到,hello变成了heXlo。
如果是指针的话,会发生什么?
ii. 字符指针(变量) 与 常量字符串
再看这个代码:
int main()
{char* pstr = "hello";printf("pstr原来是%s\n", pstr);*(pstr + 2) = 'X';printf("pstr是%s", pstr);return 0;
}
运行结果:
可以看到,程序运行到“*(pstr + 2) = 'X';”就异常退出了。
我们调试起来看看:
可以发现,我们没有权限去修改这个字符串。
深层知识补充:(了解即可)
- 用数组保存字符串时,系统会开辟新的空间。
- 用指针保存字符串时,不会开辟新的空间,常量字符串是本来就存在的。对指针输入字符串时(如“char* str = "hello";”),本质上是返回常量字符串的地址给指针。
- 常量字符串不可被修改的原因:常量字符串是存在于常量区的,进程看到的地址都是虚拟地址。虚拟地址空间经过页表的映射才能找到物理内存,而常量区经过页表映射的时候,权限就是只读的, 所以不允许修改
6. 数组指针(变量)
6.1 数组指针的概念
根据前⾯学习的整型指针和字符指针,我们列出类⽐推理:
- 字符指针-->是指针变量-->存放的是字符变量的地址
- 整型指针-->是指针变量-->存放的是整型变量的地址
- 数组指针-->是指针变量-->存放的是数组的地址
思考一下,那我们应该怎样得到数组的地址?
使用&arr[0]?错:&arr[0]是数组首元素的地址使用arr?错:arr仍然是首元素的地址- 使用&arr,这才是代表数组的地址。
由此,我们来总结一下
数组指针的定义:
- 数组指针也是一种指针变量,全称是“指向数组的指针变量”。
- 数组指针(变量)存放的是数组的地址。
6.2 一维数组指针(变量)
(因为多维数组指针变量用不上,这里只讲一维数组指针变量)
(一)一维数组指针变量的初始化:
- 类型 一维数组名[k]; //先要有一维数组
- 同类型 (*数组指针名)[同k] = &同一维数组名; //再有一维数组指针
(二)一维数组指针变量的数据类型:
- 去掉指针名后,中括号[ ]与小括号()的位置交换一下:
- 即“ 同类型[同k] (*) 或 同类型[同k]* ”
注意:下标引用[ ]的优先级 比 解引用*的优先级高。如果不写括号,会变成(一级)指针数组,其本质就成数组了。
比如:
1. int arr[5] = {0,1,2,3,4};
2. int (*p)[5] = &arr;
一维数组指针p的数据类型是:int[5] (*)
拆分解析:
int (*p) [5] = &arr;
| | |
| | p所指向的数组arr 的总元素个数
| *p声明p是一个指针
p所指向的数组arr的 单个元素的类型
不能不写或写错方括号中的数值,因为指针类型决定了指针+-整数的步长。
思考一下,&arr的数据类型是不是二级指针型?
调试举例:
可以发现,此处&arr的数据类型并不是二级指针型int**,而是数组指针型int[10]*
(arr[0]的类型是int,加上&后,&arr[0]的类型变成了int*;arr的类型是int[10],加上&后,&arr的类型变成了int[10]*。【总结:加&添* 】)
6.3 行指针(变量)
行指针是一种特殊的数组指针,它与一维数组指针相似,但行指针并不是一维数组指针。
(一)行指针的初始化:
- 类型 二维数组名[k1][k2];
- 同类型 (*数组指针名)[同k2] = 二维数组名;
(二)行指针的数据类型:
- 去掉指针名后,中括号[ ]与小括号()的位置交换一下:(这与一维数组指针类似)
- 即“ 同类型[同k2] (*) 或 同类型[同k2]* ”
例如:
int arr[3][5] = { 0 };
int (*p)[5] = arr;
行指针p的数据类型是:int[3]*
拆分解析:
int (*p) [5] = &arr;
| | |
| | p所指向的数组arr中,一行的元素个数或列数(变了)
| *p声明p是一个指针(没变)
p所指向的数组arr的单个元素的类型(没变)
为什么行指针不是一维数组指针?
原因如下:(假设指针都称作p,一维数组叫arr1,二维数组叫arr2)
(1)一维数组指针的定义是:一维数组指针(变量)存放的是整个一维数组的地址,即p = &arr1。(指针的数据类型决定指针+-整数的步长,一维数组指针p的一个步长是整个一维数组arr1的长度)
(2)行指针的定义是:行指针(变量)存放的是二维数组的第一行的地址,即p = arr2。(指针的数据类型决定指针+-整数的步长,行指针p的一个步长就是二维数组arr2一行的长度)
7. 二维数组传参的本质
思考一下,二维数组形参的数据类型是二级指针?还是数组指针?
代码演示:
//打印二维数组的函数
void f(int arr[][3], int row, int col)
{for (int i = 0; i < row; i++){for (int j = 0; j < col; j++)printf("%d ", arr[i][j]);printf("\n");}
}
//mian函数
int main()
{int* a[4][3] = { {1},{2},{3} };f(a, 3, 3);return 0;
}
从中可以发现,二维数组的形参是一个行指针。
总结,二维数组传参的本质:
- 二维数组传参的时候,本质上传递的是二维数组第一行arr[0]的地址,即&arr[0]。(不是&arr[0][0])
- 函数形参数组是一个伪(二维)数组,本质上是一个行指针。(不是二级指针,也不是一维数组指针。)
本期分享完毕,多多点赞,多多支持哦~Thanks♪(・ω・)ノ