C语言——对数组和数组名、二维数组、指针和const的理解和总结

引言

基于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字节,最后指向了一个包含五个未知数的数组。
第五个问题,数组作为函数参数时是如何传递的。在进行数组传递时采用的是传址,对于一维数组,把它的首地址和数组长度传过去。相对于传值方式主要有两方面优点。

  1. 节省空间。采用传值方式的话,需要额外开辟一个和原数组一样大小的数组,尤其当数组很大的时候十分浪费空间。
  2. 当我们需要修改传进来的数组时,传值方式无法完成,而传址却可以;当不允许修改时,只需要加一个 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] 类型的数组

指针需要注意的地方如下

  1. 指针变量的加减运算并不是简单的加上或者减去一个整数,而是跟指针指向的数据类型有关
  2. 使用指针之前一定要初始化,否则就不能确定指针指向哪里(野指针),如果它指向的内存没有使用权限,程序就会崩溃,对于暂时没有指向的指针,可以赋值为NULL
  3. 当两个指针指向同一个数组中的元素时,两指针相减的结果就是两个指针之间相差的元素个数
  4. 除去 sizeof和& 两种情况,数组名一般都会被转换为一个指向数组首元素的指针
  5. 指针的类型不同时(虽然大小都相同),不能互相赋值,必须进行强制类型转换
    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的一些分析和总结,内容算不上全面,后续会进行修改和补充。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值