系列文章目录
第一章 指针初阶——什么是指针?如何使用?【链接: link】
第二章 指针进阶 01——数组和指针【本文】
第三章 指针进阶 02——不同类型的指针【待更新】
第四章 指针进阶 03——存放不同指针的数组【待更新】
数组和指针
前言
本章主要针对数组和指针间的关系进行讲解,因此请读者自行掌握数组相关知识,本文着重基础知识的讲述以及数组和指针关系间的衔接。这里默认大家对指针初阶【链接: link】的内容掌握熟练,并以此为基础扩展。
第一章 数组
数组是相同变量类型的集合,在内存上排列成一直线。因此,数组在内存中是连续存放的,如图1-1所示。
数组的声明形式:元素类型 变量名 [元素个数]。其中的元素个数必须是常量,这里可以使用对象式宏或者枚举常量。
图1-1 数组的存储
1.1 一维数组
这里只对一维数组的创建和赋初值的理解方式做简单展示,直接看示例1-2。值得一提的是,数组赋初值时可以根据所赋的初值个数自动指定数组的元素个数,即数组元素个数自动等于所赋初值的个数,因此数组的元素个数可以省略。请注意理解,这是后续二维数组的行数可以省略,但列数不能省略的原因所在。
//示例 1-2
int a[5]; //声明一个数组,元素类型为 int ,元素个数为 5 。数组初始内容为随机值
int a[5] = {1, 2}; //对数组 5 个元素的前 2 个元素分别赋初值为 1 和 2,未赋初值的元素自动用 0 初始化
//等价形式 int a[5] = {1, 2, 0, 0, 0};
int a[5] = {0}; //等价形式 int a[5] = {0, 0, 0, 0, 0}; 手动赋值 a[0] = 0,剩余四个元素的赋初值是自动用 0 初始化的结果
int a[] = {1, 2, 3, 4, 5}; //根据初始值的个数自动指定元素个数,等价形式 int a[5] = {1, 2, 3, 4, 5};
int a[3] = {1, 2, 3, 4 ,5}; //error 初始值设定项值太多 [声明的元素个数和元素初值的数量不对等]
char a[5] = { 'q', '2', '0'};//等价形式 char a[5] = { 'q', '2', '0', '\0', '\0'}; 注意区分 '0' 和 '\0'
char a[] = { 'q', '2', '0'};//等价形式 char a[3] = { 'q', '2', '0'};
char a[] = "abc"; //等价形式 char a[4] = {'a', 'b', 'c', '\0'};字符串形式对字符数组初始化,末尾自动存储一个 '\0' 作为字符串的结束标志,占一个字节的空间
char a[3] = "abc"; //等价形式 char a[3] = {'a', 'b', 'c'};
1.2 多维数组
多个数组集合在一起形成的数组,即元素本身是数组的数组就是多维数组。以数组为元素的数组是二维数组,以二维数组为元素的数组是三维数组,当然也可以生成维数更高的数组,二维数组以上的数组统称为多维数组。如图1-3所示。下面以具体示例来加深对二维数组的理解。
图1-3 一维数组和二维数组的生成
有 int a[4][3]; 二维数组的声明,大家是如何理解这个二维数组呢?请先思考,再继续往下阅读印证。
int a[4][3] 是一个4行3列的数组,表示一个二维数组,该数组有4个元素(a[0] ,a[1] ,a[2],a[3]),每个元素都是一维数组。请注意理解这句话:二维数组是以一维数组为元素的数组,后续数组指针中将继续用到二维数组的元素的概念,请务必熟练掌握。来看示意图1-4加深理解。
图1-4 二维数组的理解
再来看一下这个二维数组的声明 int a[ ][3] = {{1,2,3}, {2, 3 ,4}, {3, 4, 5}, {4, 5 ,6}}; 为什么可以省略行数呢?其实,二维数组的行数就是其元素个数,数组a赋初值的元素个数直接自动指定了元素个数为4。本质还是使用了数组可以省略元素个数的特性,请暂时没有理解的读者再次回到前文重新阅读并思考。
1.3 数组下标
对数组内各个元素的访问(读取)是自由的,需要使用下标运算符“ [ ] ”,下标运算符中的操作数称为下标。下标表示该元素是首个元素之后的第几个元素,而不是数组中的第几个元素,第一个元素的下标为 0 。数组随着下标的增加,地址也逐渐增加,如图1-5所示。
图1-5 数组下标和地址的变化
第二章 数组名的含义
数组名原则上会被解释为指向该数组起始元素的指针。但在两个特例情况下会表示整个数组,如下。
2.1 数组名和 sizeof ()
当数组名单独出现在sizeof()内部时,数组名表示整个数组,"sizeof(数组名)"表示整个数组所占空间的大小,单位是字节。如示例代码2-1所示。
//示例代码 2-1
#include <stdio.h>
//代码在VS Stduio 64位环境下运行
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
printf("%zd\n", sizeof(arr)); //结果【20】 计算的是整个数组的大小,因此sizeof(arr)中的arr表示确实表示整个数组
printf("%zd\n", sizeof(arr + 1)); //结果【8】 计算的是指针的大小,此时arr表示指向数组首元素的指针
return 0;
}
2.2 数组名和 “&”
当数组名前加上取地址操作符时,数组名表示整个数组。"&数组名"表示的是整个数组的地址,也就是数组首元素的地址,这两个地址的值是一样的,但含义不同,后续的指针中会详细讲解,来看示例代码2-2。
//示例代码 2-2
#include <stdio.h>
//代码在VS Stduio 32位环境下运行
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
printf("%zd\n", sizeof(&arr)); //结果【4】 指针大小
printf("%p\n", &arr); //结果【00D3FE80】数组的地址
printf("%p\n", &arr[0]); //结果【00D3FE80】数组首元素的地址
printf("%zd\n", sizeof(&arr + 1)); //结果【4】 指针大小
printf("%p\n", &arr + 1); //结果【00D3FE94】
return 0;
}
%p 形式打印的是地址,在32位环境中,地址会显示32个比特位,为了方便分析,以十六进制展示,长度由32位变为8位。" &arr "取的是整个数组的地址,加一后会越过 int arr[5] 大小的字节,因此两个地址的差值正是数组的大小4*5=20字节。来看打印出的地址差值是14,这是十六进制,转换为十进制就是:1*16(16的一次方)+4*1(16的零次方)=20,这和分析的结果一致。来看示意图2-3加深理解。
图2-3 数组名和取地址操作符
2.3 sizeof() 运算符
指针初阶中对 sizeof 运算符有过介绍,这里将其再次单独拿出来,是为了说明它的一个重要性质:sizeof 不会对括号内的运算表达式进行计算,它是根据对象的类型来推断对象的存储空间大小。见示例代码2-4所示。请读者仔细对比不同,后续指针中也将用到这个重要性质。
//示例代码2-4
#include <stdio.h>
//代码在VS Stduio 2022中运行
//short 2字节
//long 4字节
//long long 8字节
typedef struct student
{
char name[27];
int age;
double weight;
double height;
double schools;
} stu;
int main()
{
int i_a = 7;
int i_b = 10;
int i_c = 0;
short s = 0;
long L = 3;
long long LL = 33;
printf("%zd\n", sizeof(i_a)); //结果【4】
printf("%zd\n", sizeof(int)); //结果【4】放变量名i_a和放数据类型int,结果一致。这和sizeof根据数据类型判断大小的结论一致
printf("\n%zd\n", sizeof(stu)); //结果【56】
printf("%zd\n", sizeof(struct student)); //结果【56】
printf("\n原来i_c = %d\n", i_c); //结果【原来i_c = 0】
printf("%zd\n", sizeof(i_c = i_a + i_b)); //结果【4】sizeof内部 i_a+i_b 的和并没有赋值给i_c,说明其内部的运算表达式确实没有进行计算
printf("后来i_c = %d\n", i_c); //结果【后来i_c = 0】
printf("\n原来s = %d\n", s); //结果【原来s = 0】
printf("%zd\n", sizeof(s = i_a + i_b)); //结果【2】赋值表达式结果的数据类型和[赋值号左边的]数据类型保持一致
//因此只需知道s的类型为short,则sizeof最后的计算结果和short的大小保持一致
printf("后来s = %d\n", s); //结果【后来s = 0】
printf("\n原来LL = %lld\n", LL); //结果【原来LL = 33】
printf("%zd\n", sizeof(LL = s + i_a)); //结果【8】
printf("后来ll = %lld\n", LL); //结果【后来LL = 33】
printf("\n%zd\n", sizeof(s + i_a)); //结果【4】计算结果和[数据类型最大的]变量的数据类型保持一致,如short+int得到int
printf("%zd\n", sizeof(s + L)); //结果【4】
printf("%zd\n", sizeof(s + LL)); //结果【8】
printf("%zd\n", sizeof(i_a + L)); //结果【4】
printf("%zd\n", sizeof(i_a + LL)); //结果【8】
printf("%zd\n", sizeof(L + LL)); //结果【8】
return 0;
}
第三章 数组名和指针
数组的下标访问本就是指针运算和解引用操作的结合。如示例代码3-1所示,着重比较使用数组下标进行元素访问和使用数组指针进行元素访问的写法1,是不是形式一致呢?再回忆下前文那句,数组名表示指向数组首元素的指针,现在能理解了吗?程序运行结果如图3-2所示。
//示例代码 3-1
#include <stdio.h>
int main()
{
int arr1[2][3] = { 1, 2, 3, 4 ,5, 6 };
int i = 0;
int j = 0;
puts("用【数组下标】进行元素遍历");
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", arr1[i][j]);
}
putchar('\n');
}
puts("\n\n用【数组指针】进行元素遍历");
int(*p)[3] = arr1;
puts("写法1:p[i][j]");
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", p[i][j]);
}
putchar('\n');
}
puts("\n写法2:*(*(p + i) + j)");
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", *(*(p + i) + j));
}
putchar('\n');
}
puts("\n写法3:j[i[p]]");
for (i = 0; i < 2; i++)
{
for (j = 0; j < 3; j++)
{
printf("%d ", j[i[p]]);
}
putchar('\n');
}
return 0;
}
图3-2 示例代码3-1运行结果
3.1 数组传参
学习自定义函数时,有这样一句话,形参是实参的一份临时拷贝,因此函数中对形参的修改并不会影响实参。但在之前介绍的扫雷代码中,对数组进行函数间的传参时,为什么对形参的修改却影响到了实参?难道这句话是错误的吗?来看如下代码。
//示例代码 3-3
#include <stdio.h>
#define col 3
void Print_01(int e_arr1[], int e_sz1)
{
int i = 0;
for (i = 0; i < e_sz1; i++)
printf("%d ", e_arr1[i]);
putchar('\n');
}
void Modify_01(int e_arr1[], int e_sz1)
{
int i = 0;
for (i = 0; i <e_sz1; i++)
e_arr1[i] = 7;
}
void Ptr_Print_01(int *ptr, int sz1)
{
int i = 0;
for (i = 0; i < sz1; i++)
printf("%d ", ptr[i]);
putchar('\n');
}
void Ptr_Modify_01(int *ptr, int sz1)
{
int i = 0;
for (i = 0; i < sz1; i++)
ptr[i] = i + 1;
}
Print_02(int e_arr2[][col], int e_sz2)
{
int i = 0;
for (i = 0; i < e_sz2; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
if (1 == i && 0 == j)
{
printf("\t %d ", e_arr2[i][j]);
continue;
}
printf("%d ", e_arr2[i][j]);
}
putchar('\n');
}
putchar('\n');
}
void Modify_02(int e_arr2[][col], int e_sz2)
{
int i = 0;
for (; i < e_sz2; i++)
{
int j = 0;
for (; j < 3; j++)
e_arr2[i][j] = 7;
}
}
void Ptr_Print_02(int (*ptr)[col], int sz2)
{
int i = 0;
for (; i < sz2; i++)
{
int j = 0;
for (; j < col; j++)
{
if (1 == i && 0 == j)
{
printf("\t %d ", ptr[i][j]);
continue;
}
printf("%d ", ptr[i][j]); //等价 printf("%d ", *(*(ptr + i) + j));
}
putchar('\n');
}
putchar('\n');
}
void Ptr_Modify_02(int (*ptr)[col], int sz2)
{
int i = 0;
for (; i < sz2; i++)
{
int j = 0;
for (; j < col; j ++)
ptr[i][j] = i + 1; //等价 *(*(ptr + i) + j) = i + 1;
}
}
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[][col] = {1, 2, 3, 4, 5, 6};
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
int sz2 = sizeof(arr2) / sizeof(arr2[0]); //二维数组arr2的元素个数为2
//一维数组
puts("【一维数组的传参和修改】");
printf("[原数组]arr1 = "); Print_01(arr1, sz1);
puts("[数组传参]");
printf("\t传参前:"); Print_01(arr1, sz1);
printf("\t传参后:");
Modify_01(arr1, sz1); Print_01(arr1, sz1);
puts("\n[指针传参]");
printf("\t传参前:"); Ptr_Print_01(arr1, sz1);
printf("\t传参后:");
Ptr_Modify_01(arr1, sz1); Ptr_Print_01(arr1, sz1);
//二维数组
puts("\n\n\n【二维数组的传参和修改】");
printf("[原数组]arr2 = "); Print_02(arr2, sz2);
puts("[数组传参]");
printf("\t传参前:"); Print_02(arr2, sz2);
printf("\t传参后:");
Modify_02(arr2, sz2); Print_02(arr2, sz2);
puts("\n[指针传参]");
printf("\t传参前:"); Ptr_Print_02(arr2, sz2);
printf("\t传参后:");
Ptr_Modify_02(arr2, sz2); Ptr_Print_02(arr2, sz2);
return 0;
}
其实,对数组进行传参时,虽然可以用数组和指针两种形式接收数组的实参,但数组传参时,传递的本质还是指针,数组下标访问的本质也还是指针运算和解引用操作的结合。既然传的是地址,那么函数中对形参的修改当然会影响到实参。这里请结合前文以及数组名的含义进行理解,通过示例代码3-3加深理解,程序运行结果如图3-4所示。
图3-4 示例代码3-2的运行结果
总结
本次的数组和指针主要以基础为主,请加深理解,便于后续内容的进一步掌握。感谢阅读 ^ - ^