⭐ 前言
本文将介绍C语言中数组;
通过数组我们可以更方便地存储和访问这些元素。
另外,数组也可以和指针相结合,组成数组指针或者指针数组。(这是重点放在后面单独出一篇笔记)
本章将带大家重点了解:一维数组和二维数组。
Let’s get it!
文章目录
1. 数组简介
先来从整体上认识一下数组,然后再深入理解!
🎃 顺序存储
顺序存储结构,是指用一段地址连续的存储单元来依次存储数据。
如图所示,每个蓝色方块都对应了数组中的一个数据:
数据有类型,例如:32位整型int、单精度浮点型float、双精度浮点型double、字符型char,64位整型long long、16位短整型short等等。
🎃 存储方式
在编程语言中,用一维数组来实现顺序存储结构;
在C语言中,把第一个数据元素存储到下标为 0 的位置中,把第 2 个数据元素存储到下标为 1 的位置中,以此类推。
C语言中,数组的定义如下:
int arr[7];
我们也可以将数组元素进行初始化,如下:
int arr[7] = {5, 2, 0, 1, 3, 1, 4};
由于编译器能够通过后面的初始化列表知道到底有多少个数据元素,所以中括号中的数字可以省略,如下:
int arr[] = {5, 2, 0, 1, 3, 1, 4};
当然,我们还可以定义一个大数组,但是只初始化其中几个元素:
int arr[15] = {5, 2, 0, 1, 3, 1, 4};
🎃 长度和容量
数组的长度指的是数组当前有多少个元素,数组的容量指的是数组最大能够存放多少个元素。
如果数组元素大于最大能存储的范围,在程序上是不允许的,可能会产生意想不到的问题,实现上是需要规避的。
如上图所示,数组的长度为 5,即红色部分;容量为 8,即红色 加 蓝色部分。
例如下面这个数组:
int arr[8] = {9, 8, 7, 6, 5};
🎃 数组的索引
数组中的元素索引,我们可以采用[]
运算符来完成,例如:
int a[7] = {5, 2, 0, 1, 3, 1, 4}; int b = a[0]; // 5 int c = a[6]; // 4 int d = a[7]; // error
这段代码中,a[0]
表示的是数组的第一个元素,a[6]
表示的数组的最后一个元素。
而a[7]
则是非法的,原因是数组元素一个 7 个,而这个调用,索引到了第 8 个元素,如果程序这样调用,就可能引起位置的错误。
这种错误调用,我们一般称它为:数组下标越界。
以上简介出自英雄哥的《LeetCode零基础指南》 一维数组
接下来正式进入数组的学习!
2. 一维数组的创建和初始化
数组的创建方式:
type_t arr_name[const_n]; //type_t 是指数组的元素类型 //const_n 是一个常量表达式,用来指定数组的大小
例如:
int arr1[5]; char arr2[10]; float arr3[8]; double arr4[20];
以上这种创建形式都符合数组的创建语法。
🐱🏍注:数组创建,[ ]
中要给一个常量才可以,不能使用变量。
例如:
int main() { int i = 20; int arr[i]; return 0; }
运行结果:
🌳 数组的初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
如果实在想不到赋什么初值,可以在初始化的时候直接赋值为0;
代码示例:
int main() { int arr1[5] = { 1,2,3,4,5 }; int arr2[5] = { 0 };//如果只传一个0,会将数组的内容全部初始化为0 char arr3[3] = { 'a',98, 'c' };//char变量直接存放数字,放的是数字所对应的ASCII码字符 char arr4[3] = { 0 };//将数组中的内容初始化为'\0',注意,这里不是字符0 return 0; }
🐱👓我们通过调试看一看:
如果数组不完全初始化,那么数组剩下的内容将会被自动初始化为0,char类型的自动初始化为\0
代码示例:
int main() { int arr1[10] = { 1,2,3,4,5 }; char arr2[10] = { 'a',98, 'c' }; return 0; }
调式结果:
数组在创建的时候如果想不指定数组的确定的大小就必须初始化。
数组的元素个数根据初始化的内容来确定。
📄代码示例:
int main() { int arr1[] = { 1,2,3,4,5 }; char arr2[] = { 'a','b','c' }; char arr3[] = "abc"; return 0; }
调试结果:
上面的代码中,arr1数组并没有指定大小,因此在初始化中将自动为其分配5个int大小的空间;
同理arr2和arr3也一样,但是arr2和arr3大小并不相同。
这就涉及到字符数组的初始化问题了。
🟠 字符数组的初始化
当我们想要给一个字符数组初始化时,可以用{ ' ', ' '}这样的形式将每个字符单独存放;
也可以用" "的形式直接存放一个字符串。
这两种类型是有区别的:
int main() { char arr1[] = { 'a','b','c' }; char arr2[] = "abc"; return 0; }
调试结果:
从监视中可以看出来,直接存放一个字符串的形式实际上就是将字符串的每个字符单独存放在字符数组中;
但是存放字符串要比单独存放字符在结束时多存放一个\0,这就导致存放字符串的空间大小比直接存放字符多1
关于字符数组和字符串数组的区别可以看我之前写过的一篇博客:深入理解字符数组和字符串数组
🌳 一维数组在内存中的存储
一维数组元素在内存中是连续存放的,随着数组下标的增长,地址由低到高变化;
我们通过 &(取地址符号) 来得到每个数组的地址
代码示例:
int main() { int arr[10] = { 0 }; int i = 0; for (i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) { printf("&arr[%d] = %p\n", i, &arr[i]); } return 0; }
运行结果:
✨解析:
地址都是16进制的,转换成10进制可以发现,数组每个元素之间的差为4,这也正是int型变量的大小;
因此可以发现一维数组元素在内存中是连续存放的,且地址在不断增大。
同理char类型的数组也是连续存放的,但是char类型的大小为1,因此每个元素之间的差为1;
整形数组在内存中是这样存储的:
🌳 一维数组的使用
我们可以通过下标的方式访问和使用一维数组;
📄代码示例:
int main() { int arr[10] = { 0 };//数组的不完全初始化 int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数 //对数组内容赋值,数组是使用下标来访问的,下标从0开始。 int i = 0;//做下标 for (i = 0; i < 10; i++) { arr[i] = i; } //输出数组的内容 for (i = 0; i < 10; ++i) { printf("%d ", arr[i]); } return 0; }
运行结果:
由于数组在内存中连续存储,因此我们也可以通过指针来访问和修改数组元素;
通常情况下,数组名arr
代表的是数组的首元素地址,对数组的首元素地址+1就是跳过第一个元素地址找到第二个元素地址,再通过解引用操作符*
就可以找到对应的数组内容。
📄修改代码:
int main() { int arr[10] = { 0 };//数组的不完全初始化 int sz = sizeof(arr) / sizeof(arr[0]);//计算数组的元素个数 //对数组内容赋值,数组是使用下标来访问的,下标从0开始。 int i = 0;//做下标 for (i = 0; i < 10; i++)//大小为10的数组,其下标为0-9 { *(arr+i) = i;//通过指针找到对应的元素 } //输出数组的内容 for (i = 0; i < 10; ++i) { printf("%d ", *(arr + i)); } return 0; }
运行结果:
总结:
数组是使用下标来访问的,下标是从0开始。
数组的大小可以通过计算得到。
✨数组大小计算方法:
int arr[10]; int sz = sizeof(arr)/sizeof(arr[0]);
一维数组的数组名是什么
通常情况下一维数组的名是数组的首元素地址,但有两种情况除外:
sizeof(数组名):这种情况下数组名是整个数组的数组名,sizeof计算的也是整个数组的大小,如果想计算数组的某个元素大小则要加入下标,比如sizeof(arr[0])则是计算arr第一个数组元素的大小
&数组名,这种情况下数组名也是整个数组的数组名,如果&数组名+1,则会跳过整个数组,而不是跳过数组的一个元素。
代码示例:
int main() { int arr[10] = { 0 }; int sz = sizeof(arr) / sizeof(arr[0]); printf("%d\n", sz); printf("%p\n", &arr); printf("%p\n", &arr + 1); return 0; }
运行结果:
从上面的程序可以看出sizeof(arr)计算的是整个数组的大小40;
sizeof(arr[0])计算的是数组第一个元素的大小为4,两者相除即可得到数组的元素个数10;
&arr得到的是整个数组的地址,对其+1,则是跳过整个数组,跳过的大小为数组的大小40;
3. 二维数组
🌳 二维数组的创建
二维数组在一维数组的基础上加入了 行 和 列 的概念,创建的语法如下:
type_t arr_name [const_line][const_column]; //type_t 是指数组的元素类型 //arr_name 是数组的名字 //const_line数组每行的元素个数 //const_column数组每列的元素个数
使用
//数组初始化 int arr[3][4] = {1,2,3,4}; int arr[3][4] = {{1,2},{4,5}}; int arr[][4] = {{2,3},{4,5}};
🌳 二维数组的初始化
二维数组在初始化时可以省略行,但不能省略列,整形二维数组如果没完全初始化,则会自动初始化为0;
代码示例:
int main() { int arr1[3][4] = { 1,2,3,4 }; int arr2[3][4] = { {1,2},{4,5} }; int arr3[][4] = { {2,3},{4,5} }; return 0; }
调试结果:
arr1
数组:
arr2
数组:
arr3
数组:
✨结论:
可以看到,如果不使用{ }进行初始化,比如arr1,编译器会默认先放满第一行,然后再放第二行,最后把剩下没初始化的元素初始化为0;
如果加入了{}比如arr2,编译器就会按照我们的要求进行初始化,同样的最后没初始化的元素也会被初始化为0;
省略行后,编译器会自己计算行的大小。
🌳 二维数组在内存中的存储
二维数组和一维数组一样,在内存中也是连续储存的;
代码示例:
int main() { int arr[3][4]; int i = 0; for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 4; j++) { printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]); } } return 0; }
运行结果:
通过结果我们可以分析到,其实二维数组在内存中也是连续存储的。
🌳 二维数组的使用
和一维数组一样,我们可以通过下标和指针的方式来访问和使用二维数组。
通过下标访问:
int main() { int arr[3][4] = { 0 }; int i = 0; for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 4; j++) { arr[i][j] = i * 4 + j; } } for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 4; j++) { printf("%d ", arr[i][j]); } printf("\n"); } return 0; }
运行结果:
通过指针访问:
int main() { int arr[3][4] = { 0 }; int i = 0; for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 4; j++) { *(*(arr + i) + j) = i * 4 + j; } } for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 4; j++) { printf("%d ", *(*(arr + i) + j)); } printf("\n"); } return 0; }
运行结果:
在二维数组中,数组名是第一行的地址;
数组名+1跳过第一行,指向的是第二行的元素
代码示例:
int main() { int arr[3][4] = { 0 }; printf("%p\n", arr); printf("%p\n", arr+1); printf("%p\n", *(arr + 1)+1); return 0; }
运行结果:
4. 数组名的含义
代码示例:
int main() { int arr[10] = { 1,2,3,4,5 }; printf("%p\n", arr); printf("%p\n", &arr[0]); printf("%d\n", *arr); return 0; }
运行结果:
结论:
数组名是数组首元素的地址。(有两个例外)
如果数组名是首元素地址,那么:
int main() { int arr[10] = { 0 }; printf("%d\n", sizeof(arr)); return 0; }
运行结果:
为什么输出的结果是:40?
✨解析:
sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除了上面sizeof(数组名)和&数组名表示整个数组的地址两种情况外,数组名都表示数组的首元素地址。
重点:如果是sizeof(数组名+0),则表示的不是整个数组地址,而是数组首元素地址,因为必须要单独将数组名放到sizeof中才表示整个数组地址。
🌳 冒泡排序函数的设计
先来看一段错误代码:
//方法一 void bubble_sort(int arr[]) { int sz = sizeof(arr) / sizeof(arr[0]);//这样对吗? int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (j = 0; j < sz - i - 1; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } int main() { int arr[] = { 3,1,7,5,8,9,0,2,4,6 }; bubble_sort(arr);//是否可以正常排序? for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { printf("%d ", arr[i]); } return 0; }
当我们运行的时候,发现会报错!
出问题,那我们找一下问题,调试之后可以看到 bubble_sort 函数内部的 sz 是1。
难道数组作为函数参数的时候,不是把整个数组的传递过去?
当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。
所以即使在函数参数部分写成数组的形式:int arr[] 表示的依然是一个指针:int *arr。
那么,函数内部的sizeof(arr) 结果是4。
如果 方法1 错了,该怎么设计?
📃代码示例:
void bubble_sort(int arr[], int sz) { int i = 0; for (i = 0; i < sz - 1; i++) { int j = 0; for (j = 0; j < sz - i - 1; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } int main() { int arr[] = { 3,1,7,5,8,9,0,2,4,6 }; int sz = sizeof(arr) / sizeof(arr[0]);//先主函数里计算数组的大小 bubble_sort(arr, sz); for (int i = 0; i < sz; i++) { printf("%d ", arr[i]); } return 0; }
运行结果:
其实关于冒泡排序的原理和实现方法,可以看我之前写的一篇博客:深入理解冒泡排序算法
5. 结语
关于数组的总结就到此结束啦!
那我们就可以用数组来写几个小游戏,比如三子棋和扫雷
三子棋的实现原理和思路也可以看我之前写的一篇博客:升级版三子棋小游戏(棋盘界面美化+动画演示)
只要好好理解,多加练习,没有办不到的事情!
关于数组的练习题我也会在后续剖析完以后发出来!
🌟你知道的越多,你不知道越多,我们下期见!