目录
一维数组
数组名不是指针,但是除了单独作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。
作为sizeof()和&的操作数时,数组名表示整个数组。
具体的解释说明请看下面的程序以及注释
int main()
{
int a[] = { 1, 2, 3, 4 };
printf("数组a[0]占用内存的字节数:%d\n", sizeof(a[0]));
//注意sizeof的参数只需要填a,不可以填a[]。计算整个数组占用内存的大小,单位为Byte
printf("数组a[]占用内存的字节数:%d\n", sizeof(a));
printf("数组a[]的元素个数:%d\n", sizeof(a)/sizeof(a[0]));
printf("\n");
//a 和 &a[0] 的数值和意义都是一样的,都是数组首元素的地址,是一个int类型的指针
// &a 和上面二者的数值上虽然也是一样的,但是意义不一样,&a 表示的是整个数组的地址,是一个数组a[]类型的指针
printf("a = %p\n", a); //数组首元素的地址,是一个int类型的指针
printf("&a[0] = %p\n", &a[0]); //数组首元素的地址,是一个int类型的指针
printf("&a = %p\n", &a); //整个数组的地址,数值上等于数组首元素的地址,是一个数组a[]类型的指针
printf("\n");
printf("a+1 = %p\n", a+1); //数组首元素的地址 + 一个数组元素的大小,跳过了第一个元素
printf("&a[0]+1 = %p\n", &a[0]+1); //数组首元素的地址 + 一个数组元素的大小,跳过了第一个元素
printf("&a+1 = %p\n", &a+1); //整个数组的地址 + 整个数组的大小,跳过了整个数组
return 0;
}
运行结果如下:
同时,注意区分【数组名】与【数组元素】。
对于数组元素中的方括号,[ ],其实是一个操作符,下标引用操作符,专门用来访问数组元素的操作符。它有两个操作数,一个是前面的数组名,一个是方括号里面的下标
(两个操作数的位置可以交换,不影响结果,即a[3]与3[a]是一样的,但不建议这样写)。
它的功能是这样的:
数组元素,a[i]转换成*(a+i)
例如:a[3]转换成*(a+3)
//*(a+3)与*(3+a)是一样的,这就解释了,为什么两个操作数的位置可以交换,不影响结果
还有一些容易搞混淆的概念:
&arr[2] == arr + 2; //数组arr的第三个元素的地址
arr[2] == *(arr + 2); //数组arr的第三个元素的值
*arr + 2 //数组arr第一个元素的值加2
一维数组的传参与调用
将数组作为参数传递给函数,有两种方法(两种方法的本质是一样的,只是书写形式不一样)
void myFunction(int* array, int size); //指针表示法
void myFunction(int array[], int size); //数组表示法
//上面两种方法都表示array是一个指向int类型的指针变量,是等价的
//但是只有在函数原型或者函数定义头中,才可以用int array[]代替int* array
//指针表示法,可以使函数名隐式转换成指针变量这个属性表示得更加明显,也更加接近机器语言
//数组表示法,可以让函数是处理数组的这一意图更加明显,比较直观,更容易理解
调用方式是一样的,都是
myFunction(myArray, size);
//不需要在myArray后面加上中括号[ ],如果加了,就会报错
在函数内部操作数组元素的方法,和在数组定义处操作数组元素的方法是一样的:
array[i] = array[i] * 2;
//array[i] 会自动转换成 *(array + i)
在开头就说了, 数组名不是指针,但是除了作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。
所以将数组作为参数传递给函数时,实际上传递的是地址。仅仅有地址,我们通常是无法计算数组的长度的,所以在将数组作为参数传递给函数时,通常还会吧数组的大小给传进去。
二维数组以及多维数组
弄明白二维数组对于我们弄明白多维数组的帮助是非常大的。
理解二维数组的一个比较好的方法是:将二维数组看做是数组的数组
//我们创建一个二维数组(不初始化它)
int dimension_2[3][2];
//主数组(master array)有3个元素(3行)
//这3个元素的元素名分别为dimension_2[0]、dimension_2[1]、dimension_2[2]
//每个元素都是一个内含两个int值的数组
//比如元素dimension_2[0]就包含了int类型的dimension_2[0][0]和dimension_2[0][1]
我们上面说到
数组名不是指针,但是除了单独作为sizeof()和&的操作数,编译器会将数组名隐式转换为一个指向数组首元素的指针来处理(数组名被隐式转换成数组首元素地址)。
对于一维数组
//对于一维数组
int array1[] = {1, 2, 3, 4, 5, 6};
array1 == &array1[0];
那么对于二维数组呢?
//对于二维数组
int array[3][4] = { { 1, 2, 3, 4},
{ 5, 6, 7, 8},
{ 9, 10, 11, 12} };
printf("array = %p\n", array);
printf("array[0] = %p\n", array[0]);
printf("array[0][0] = %p\n", array[0][0]);
printf("\n");
printf("&array = %p\n", &array);
printf("&array[0] = %p\n", &array[0]);
printf("&array[0][0] = %p\n", &array[0][0]);
printf("\n");
printf("array + 1 = %p\n", array + 1);
printf("array[0] + 1 = %p\n", array[0] + 1);
printf("array[0][0] + 1 = %p\n", array[0][0] + 1);
printf("\n");
printf("&array + 1 = %p\n", &array + 1);
printf("&array[0] + 1 = %p\n", &array[0] + 1);
printf("&array[0][0] + 1 = %p\n", &array[0][0] + 1);
printf("\n");
从运行结果可以看出来,在数值上,
array == array[0] == &array == &array[0] == &array[0][0]
为什么会这样呢?看明白下面这张图就可以理解了。
记住一个概念,数组名被隐式转换成数组首元素地址
//相信现在,你对数组指针有了更深的理解了
// 1.
array == &array[0]
// 2.
array[0] == &array[0][0]
//那么我们现在对 1 进行解引用
// 3.
*array == *&array[0]
*array == array[0]
//我们把 2 和 3 结合一下
// 4.
*array == array[0] == &array[0][0]
*array == &array[0][0]
//对④进行解引用
// 5.
**array == *&array[0][0]
**array == array[0][0]
//我们对array进行了两次解引用,得到了一个int类型的数据,这说明,array是地址的地址
//或者说array是指针的指针,它指向的指针指向int类型的数据array[0][0]
//地址的地址或者指针的指针就是【双重间接】(double indirection)的例子
二级指针(变量)中存放的是一级指针(变量)的地址
对二维数组名解引用两次,就可以得到数组中的值。
- 使用两个解引用操作符(*)
- 或者使用两个方括号([ ])
- 或者使用一个解引用操作符(*)和一个方括号([ ])
都可以达到这个目标。
下面的内容参考了《C Primer Plus》的P303
//与array[2][1]等价的指针表示法是*(*(array + 2) +1)。
//这看起来有点复杂,但是可以利用下面的思路来帮助理解
array //二维数组的首元素的地址(每个元素都是内含4个int值的一维数组)
array + 2 //二维数组的第3个元素(一维数组)的地址
* (array + 2) //二维数组的第3个元素(一维数组)的首元素(1个int值)的地址
* (array + 2) + 1 //二维数组的第3个元素(一维数组)的第2个元素(也是1个int值)的地址
* (* (array + 2) + 1) //二维数组的第3个元素(一维数组)的第2个元素(也是1个int值)的值,即二维数组的第3行第2列的值(array[2][1])
以上的分析只是说明,指针表示法( *(*(array + 2) +1) )和数组表示法(array[2][1])之间是可以相互转换的,如果你在程序中恰好用到了指向二维数组的指针,而且要通过该指针获取值时,最好使用简单的数组表示法,而不是使用指针表示法。
编译器会将数组表示法转换成指针表示法。
二维数组的传参与调用
在说二维数组前,先复习一下一维数组的
void myFunction(int* array, int size); //指针表示法
void myFunction(int array[], int size); //数组表示法
二维数组同样有好几种方法
- 将二维数组看做一维数组时
- 将二维数组看做一维数组的数组时
请参考下面的文章,如果有时间,我可能会单独写一篇笔记来写这个:
109. 【C语言进阶】指针进阶_数组指针的使用,数组参数和指针参数_哔哩哔哩_bilibili
二维数组作为形参--传值的3三种方式_二维数组做形参的表示方法-CSDN博客
二维数组做形参写法_二维数组作为形参的定义格式-CSDN博客
我在这里只说一点:
在声明一个指向N维数组的指针时,只能省略最左边方括号中的值(写上值也可以,但是值将被忽略)。
第一对方括号只用于表明这是一个指针,而其他的方括号则用于描述指针所指向数据对象的类型。
比如,下面的声明是等价的
int TestFunction(int arr[][12][20][30], int rows);
int TestFunction(int (*arr)[12][20][30], int rows); //注意(*arr)的括号是不能省略的
//这里把四维四组看做是三维数组的数组
//arr是指向12*20*30的int数组
int array[10];
这整个数组的类型就是 int [10]
void myFunction(int* array[]);
void myFunction(int* (*array));
void myFunction(int** array);
//上面三种表示方法等效,都是将二维数组看做是一维数组
//但是前两种写法不建议,太不常规了,需要思考才可以理解意思
void myFunction(int array[][4]);
void myFunction(int (*array)[4]);
//上面两种表示方法等效,都是将二维数组看做“内含4个int值的一维数组”的数组
//无论是一维数组还是N维数组,在声明时,都需要告诉编译器,这个指针所指向数据对象的类型是什么
//一维数组很容易解决这个问题,因为一维数组的元素都是基础数据类型
//但是当我们把N维数组看做(N-1)维数组的数组时,就需要注意,这个(N-1)维数组是什么类型的?
//除第一个方括号外的其余方括号就起到告诉编译器这个(N-1)维数组的类型的作用。
//对于二维数组写成这样(int array[][4]),就是告诉编译器
//我的(N-1)维数组的类型是包含4个int类型的一维数组
那么,将二维数组看做一维数组,和将二维数组看做一维数组的数组都可以正确声明,那么他们之间有什么区别吗?
他们所包含的信息不一样
- 将二维数组看做一维数组,第一维、第二维的大小信息都没有
- 将二维数组看做一维数组的数组,没有第一维的大小信息,有第二维的大小信息
计算二维数组的行数、列数、元素个数
//创建一个二维数组,并初始化为0
int arr[5][12] = {0};
//计算二维数组的行数
int row = 0;
row = sizeof(arr) / sizeof(arr[0]);
printf("row = %d\n", row);
//计算二维数组的列数
int column = 0;
column = sizeof(arr[0]) / sizeof(arr[0][0]);
printf("column = %d\n", column);
//计算二维数组的元素个数
int total = 0;
total = sizeof(arr) / sizeof(arr[0][0]);
printf("total = %d\n", total);
运行结果为:
二维数组的初始化
无论是多少维的数组,在初始化时,只有第一维的下标可以省略。
对应到二维数组,就是可以不指定行数,但是必须指定列数。
内部的花括号是可以省略的,但是最外面的花括号不可以省略。
下面是正确的初始化例子:
int a1[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
int a2[3][4] = {{ 0, 1, 2, 3}, { 4, 5, 6, 7},{ 8, 9, 10, 11}};
int a3[3][4] = {{ 0, 1, 2, 3},
{ 4, 5, 6, 7},
{ 8, 9, 10, 11}};
//不完全初始化,编译器会认为这是一个2*4的数组,后面没有初始化的自动补0
int a4[][4] = { 1 ,2, 3, 4, 5, 6};
//不完全初始化,就初始化了三个元素,其余元素被初始化为0
//分别是第一行的第一个元素、第二行的第一个元素、第三行的第一个元素
//a[0][0] = 1 , a[1][0] = 2 , a[2][0] = 3
int a5[3][4] = {{1}, {2}, {3}};
下面是错误的初始化例子:
//无法确定列数,无法确定数组的布局
int e1 = [3][] = {1, 2, 3, 4, 5, 6};
//创建了数组,但是没有初始化,无法确定行数
int e2 = [][4];
//创建了数组,但是没有初始化,无法确定行数、列数
int e3 = [][];
在C99标准中,还可以使用指定初始化器来进行初始化,具体内容可以参考《C Primer Plus》的P281,这里不多介绍。
二维数组在内存中的布局体现
无论是多少维的数组,在内存当中都是连续存放的,存放的顺序按照维度的顺序来。
对应到二维数组,就是先行后列。
int arr1[12] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
int arr2[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
//arr2[3][4]也可以写成这样
int arr3[3][4] = {{ 0, 1, 2, 3},
{ 4, 5, 6, 7},
{ 8, 9, 10, 11}};
在内存的视角去看,一维数组和二维数组的布局是一样的。
字符串
创建字符串的两种方法
char arr[] = "hello";
//(hello)是存储在数组里面的,在栈区,可以修改它的内容
char* arr = "hello";
//指针指向字符串时,字符串是常亮,存储在常量区(常量区数据不可修改),而指针存储在栈区
//arr是一个指向常亮字符串的指针,可以改变arr的指向,但是不能改变指向的内容(hello)
指针
有一个指针变量p
*p++ , 即*(p++) , 先取内容,后指针+1
(*p)++ , 先取内容,后内容+1
*++p , 即*(++p) , 先指针+1,后取内容
在定义变量的语句中,会出现两个东西:变量名、类型。
把变量名去掉,剩下的就是类型。
//有这样一个一维数组
int arr1[10] = {0};
//变量名(数组名):arr1
//类型:int[10]
//如果我们想创建一个指向这一整个一维数组的指针,该如何操作?
int(*p1)[10] = &arr1;
//变量名(指针名):p1
//类型:int(*)[10],,,指向有十个元素的一维数组的指针,p1是指针,指向int[10]
//升级一下难度
//有这样一个一维指针数组
int* arr1[10] = {0};
//变量名(数组名):arr1
//类型:int*[10]
//如果我们想创建一个指向这一整个一个一维指针数组的指针,该如何操作?
int*(*p1)[10] = &arr1;
//变量名(指针名):p1
//类型:int*(*)[10],,,指向有十个元素的一维指针数组的指针,p1是指针,指向int*[10]
//同样,有一个这样的二维数组
int arr2[3][4] = {0};
//变量名(数组名):arr2
//类型:int[3][4]
//如果我们想创建一个指向这一整个二维数组的指针,该如何操作?
int(*p2)[3][4] = &arr2;
//变量名(指针名):p2
//类型:int(*)[3][4],,,指向三行四列大小的二维数组的指针,p2是指针,指向int[3][4]
typedef int arr9[9];
//这样的一句代码,是什么意思呢?是可以用arr9[9]来代替int的意思吗?
//不是
//是可以用arr9来代替int[9]这种类型
//arr9 test;
//这就相当于——int test[9]
//数组名(变量名):test
//类型:int [9]
写完之后,总感觉不够满意,后续应该会进行修改