引言
基于Windows10,Visual Studio 2019,x86,对C语言中的数组和数组名、二维数组、指针和const的理解和总结。
1.数组
首先定义一个整型数组。
int arr[5] = { 1,2,3,4,5 };
对于数组需要注意的问题有:第一,数组 arr 在定义的时候有什么约束;第二,怎么求数组的长度;第三,数组元素在内存中是怎么存储的;第四,arr 代表什么意思;第五,数组作为函数参数时是如何传递的。
对于第一个问题,代码如下
#include<stdio.h>
int main() {
int n = 5;
int arr[n] = { 1,2,3,4,5 };
return 0;
}
运行之后会像下面一样报错
原因在于,C语言中数组的大小是在编译阶段就由编译系统分配,也就说在编译阶段就需要知道数组的大小,而 n 作为一个局部变量是在运行阶段才赋值的,所以会报错。因此,要么将 arr[n] 直接改成 arr[5],要么将 n 定义为 const int 类型。
对于第二个问题,比较常用的方法就是使用运算符 sizeof
int len = sizeof(arr) / sizeof(arr[0]);
这里需要额外注意的是 sizeof(arr),当 sizeof 的使用和 arr 的定义在同一个作用域时,sizeof(arr) 求得的是整个数组的字节数,当不在同一作用域时 sizeof(arr) 求得的是 arr 这个指针本身的字节数,看如下代码
之所以结果不同,是因为,当 arr 的定义和 sizeof 的使用在同一个作用域时, arr 是一个数组名,代表整个数组(这一点在第四个问题中进一步介绍),所以求得的是整个数组的大小。而当 arr 传递给函数时,arr 退化成一个指针(而这个指针的大小是由编译系统决定的,x86下的指针大小是32位,即4字节,x64下的指针大小是64位,8字节)。因此,sizeof 的使用应该和数组定义在同一个作用域,否则运行结果大概率是不对的。
第三个问题,数组中的元素在内存中的存储(基于Windows10,小端模式),代码如下
#include<stdio.h>
int main() {
int arr[5] = { 1,2,3,4,5 };
return 0;
}
对其进行调试并查看内存
从上图可以看出,数组在内存中的存储空间是连续的,而且数组元素由低地址空间向高地址空间依次存储(单个元素按照小端模式存储,低地址存储低位数据,高地址存储高位数据)。
第四个问题,arr 代表什么含义。一般情况下,arr 都会被编译器当作一个指针来处理,该指针指向数组首元素的地址。但当出现下面两种情况的时候,arr 被认为是数组名,代表整个数组。
//第一种情况就是上面使用sizeof求数组元素个数时
sizeof(arr) //而且要求sizeof和arr的定义在同一个作用域
//第二种情况
&arr
第一种情况前面已经说明,来看一下第二种情况。
可以发现的是,arr 和 &arr 的值是一样的,arr + 1 和 arr 相差4个字节,&arr + 1 和 &arr 相差20个字节,怎么理解呢?前者的 arr 可以理解为一个指针(但实际上其类型并不是指针,而是 int [5]),该指针指向数组首元素的地址也就是指向 arr[0] 的地址,所以 arr + 1 增加了一个 int 的长度,也就是4字节。而后者中的 arr 可以理解为数组名(而且确实是数组名),代表整个数组,&arr 取的是整个数组的地址,&arr 的类型是 int (
∗
\ast
∗)[5],是一个数组指针(关于指针和数组的关系在下一篇文章中介绍), 所以 &arr + 1 增加了一个 int [5] 的长度,也就是20字节,最后指向了一个包含五个未知数的数组。
第五个问题,数组作为函数参数时是如何传递的。在进行数组传递时采用的是传址,对于一维数组,把它的首地址和数组长度传过去。相对于传值方式主要有两方面优点。
- 节省空间。采用传值方式的话,需要额外开辟一个和原数组一样大小的数组,尤其当数组很大的时候十分浪费空间。
- 当我们需要修改传进来的数组时,传值方式无法完成,而传址却可以;当不允许修改时,只需要加一个 const,操作起来也很简单,如下
void Print(const int* arr, int len);
二维数组
定义一个二维数组
int arr[2][3] = { {1,2,3}, {4,5,6} };
对二维数组需要注意的问题有:第一,二位数组 arr 在定义的时候有什么约束;第二,如何求得二维数组的行和列;第三,二维数组在内存中是如何存储的;第四,二维数组作为函数参数时如何传递。
第一个问题,和一维数组一样不能使用变量去定义数组的大小。二维数组特殊的一点是可以只指定列数,省略行数,如下
int arr[][3] = { {1,2,3}, {4,5,6} };
对于上面这个例子,似乎只指定行数或者列数,又或者都不指定都不会出现歧义,为了解释为什么只可以省略行数,而不能省略列数,另外定义一个数组(当数据不足时,会用0填充)
int brr[][3] = { 1,2,3,4,5 };
brr:
1 2 3
4 5 0
C语言中数组的定义是嵌套的,也就是说,二维数组本质上也是个一维数组,只不过它的元素是一维数组,三维数组也是个一维数组,它的元素是二维数组,以此类推。如果只指定行数如下
int brr[2][] = { 1,2,3,4,5 };
brr:
1 2 3
4 5 0
这样看起来似乎没有问题,但是如果想定义一个下面这样的二维数组
int brr[][4] = { 1,2,3,4,5 };
brr:
1 2 3 4
5 0 0 0
int brr[2][] = { 1,2,3,4,5 };
brr:???
计算机会自觉理解我们的想法,输出一个2
∗
\ast
∗ 4 的矩阵吗?大概率不会。。。
对数组进行空间分配的时候,首先要知道数组元素的大小,其次才是元素的个数,数组元素的大小必须指定,而元素的个数,计算机可以自己推算出来。如果把任意维度的数组都看成二维数组,那么第一维代表的就是元素个数,其他维度组合起来加上类型代表的就是元素大小。这也是为什么对于所有维的数组来说,只能省略第一维,而不能省略其他维度。
第二个问题,求二维数组的行和列,如下
//行数
int rows = sizeof(arr) / sizeof(arr[0]);
//列数
int columns = sizeof(arr[0]) / sizeof(arr[0][0]);
其中,sizeof(arr[0]) 正是二维数组一维化后元素的大小,arr[0] 的类型是 int [3],arr[0] + 1 的结果也会是增加4个字节。
第三个问题,二维数组在内存中是如何存储的。
从图中可以看出来,二维数组在内存中的存储方式是,从低地址到高地址,按行优先存储。
第四,二维数组作为函数参数时如何传递。相较于一维数组,二维数组及更高维数组在进行函数传递时除了第一个维度可以省略,其他维度都应该指明,如下
对于二维数组参数传递过程中,数组名 arr 将退化为一个 int (
∗
\ast
∗) [3] 类型的指针。
指针和const
上述关于数组的描述,内容其实已经涉及到指针。
int a = 10;
int* p = &a;
指针本身也是一个变量,只不过这个变量比较特殊,它存储的是地址,而地址的大小是由编译系统决定的,那么指针变量的大小就是固定的,无论是几级指针(x86下是4字节,x64下是8字节)。由于指针本身也是一个变量,它也存在地址,所以存储指针地址的指针被定义为二级指针。指针的类型是 int
∗
\ast
∗,二级指针的类型是int
∗
\ast
∗
∗
\ast
∗,但他们的大小是一样的
常见的几种指针如下
int *p;//指向 int 类型的指针
int **p;//指向 int * 类型的二级指针,也就是指向指针的指针
int *p[5];//指针数组,由于[]的优先级比*高,所以p是一个数组,包含五个元素,每个元素是一个 int * 类型的指针
int (*p)[5];//数组指针,()和[]的优先级一样,所以从左往右看,p是一个指针,该指针指向一个 int[5] 类型的数组
指针需要注意的地方如下
- 指针变量的加减运算并不是简单的加上或者减去一个整数,而是跟指针指向的数据类型有关
- 使用指针之前一定要初始化,否则就不能确定指针指向哪里(野指针),如果它指向的内存没有使用权限,程序就会崩溃,对于暂时没有指向的指针,可以赋值为NULL
- 当两个指针指向同一个数组中的元素时,两指针相减的结果就是两个指针之间相差的元素个数
- 除去 sizeof和& 两种情况,数组名一般都会被转换为一个指向数组首元素的指针
- 指针的类型不同时(虽然大小都相同),不能互相赋值,必须进行强制类型转换
const的位置不同,代表着不同的含义
int a = 10;
const int *p = &a;//const修饰的是*p,而*p表示的是a,所以通过*p修改a的值是不允许的,但这里却可以修改p的值,也就是修改p存储的地址
int * const q = &a;// const修饰的是q,所以q存储的地址不能改变,但却可以修改*q的值,也就是可以通过*q修改a的值
由于const的修饰,在进行指针赋值的时候,要保证类型一致
const int b = 20;
const int * p = &b;//等号右边的类型是const int *
另一个比较绕的地方是const修饰二级指针的时候,分为下面两种情况
//第一种情况
//定义整型变量
int a = 10;
int* k = &a;//正确
const int *p = &a;//正确,这里的约束是不能通过*p来修改a的值
//下面定义了四个个二级指针
int ** q = &p;//错误
int ** const q = &p;//错误,这里只是约束q的值不能改变
int * const * q = &p;//错误,这里约束的是*q的值不能改变,也就是p的值不能改变
//上面三条语句如果成立,就能实现**q=10,然而这是不允许的
//*q == p,所以**q == *p
const int ** q = &p;//正确,因为右侧的类型就是是const int **
//第二种情况
//定义一个整型常量
const int b = 20;//b的值不允许改变
int* p = &b;//错误,通过*p似乎可以更改b的值,然而这是不允许的
const int* p = &b;//正确
//下面定义了四个二级指针
int ** q = &p;//错误
int ** const q = &p;//错误,约束的是q的值不能变
int * const * q = &p;//错误,约束的是*q的值不能变,也就是p的值不能改变
const int ** q = &p;//正确
函数指针
#include<stdio.h>
int add(int num1, int num2) {
return num1 + num2;
}
int main() {
//定义一个函数指针
int (*Pfun)(int, int);
//函数指针赋值操作
Pfun = add;
//调用
int res = Pfun(3, 4);
printf("%d\n", res);
return 0;
}
指针的特别之处
int* p, q;
上面的代码看似定义了两个 int ∗ \ast ∗ 类型的指针变量,但其实q只是一个普通的整型变量,类型是 int。正确定义两个指针的写法是
int *p, *q;
这里引出了两个关键字 define 和 typedef
//define的使用
#define Point1 int*
Point1 p, q;
//typedef的使用
typedef int* Point2;
Point2 p, q;
这里需要注意的是,define 仅是在编译阶段进行简单的替换,所以用 Point1 来定义 p 和 q 的结果仍是 p 是类型为 int ∗ \ast ∗ 的指针,q 为普通的整型变量。而是用 typedef 定义的 p 和 q 都是 int ∗ \ast ∗ 类型的指针。
总结
以上就是对数组和二维数组、指针和数组、指针和const的一些分析和总结,内容算不上全面,后续会进行修改和补充。