一、数组
1.数组的概念
数组是⼀组相同类型元素的集合;从这个概念中我们就可以发现2个有价值的信息:
- 数组中存放的是1个或者多个数据,但是数组元素个数不能为0。
- 数组中存放的多个数据,类型是相同的。
2.数组的分类
数组主要分为一维数组和多维数组,其中多维数组里二维数组较为常见。
- 一维数组:类似于一条直线上排列的元素序列。比如,可以用一维数组存储一组温度数据,每个元素依次代表不同时刻的温度值。
- 二维数组:可视为一个有行有列的矩阵。例如,在处理图像数据时,二维数组的行可以表示图像的高度像素,列表示宽度像素,每个元素存储对应像素的颜色信息或灰度值。
二、一维数组
1. 数组的创建
基本语法:
type_t arr_name[const_n]; //type_t 是指数组的元素类型 //const_n 是一个常量表达式,用来指定数组的大小
- 元素类型(type):
- 可设为 char、short、int、float 等基础或自定义类型,依数据处理需求而定。
- 数组名(arr_name):
- 按需取有意义之名,助于代码理解与操作。
- 数组大小([] 中的常量值):
- 依实际数据量预估确定,创建后常不可变。
注:数组创建,在C99标准之前,[]中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,数组的大小可以使用变量指定,但是数组不能初始化。
数组创建的实例
⽐如:我们现在想存储某个班级的36⼈的英语成绩,那我们就可以创建⼀个数组,如下:
double English[36];
当然我们也可以根据需要创建其他类型和⼤⼩的数组:
char arr1[10]; float arr2[1]; int arr3[20];
2. 数组的初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)。
初始化的实例
//完全初始化 int arr[5] = {1,2,3,4,5}; //不完全初始化 int arr2[6] = {1};//第⼀个元素初始化为1,剩余的元素默认初始化为0 //错误的初始化 - 初始化项太多 int arr3[3] = {1, 2, 3, 4};
如果在创建时不进行初始化
- 对于全局数组:默认初始值是0
- 对于局部数组,根据编译器的不同有所差异,不过一般是随机值
3. 数组的类型
数组类型本质
数组是自定义类型,去掉数组名后的部分即为其类型,这是精准操作数组的关键依据。
例如:
int arr1[10]; //arr1数组的类型是 int [10] int arr2[12]; //arr2数组的类型是 int [12] char ch[5]; //ch数组的类型是 char [5]
4. 一维数组的使用
(1)数组下标运用
- 下标规范:C 语言数组下标从 0 起,有 n 个元素时,最后下标为 n - 1,如 int arr [10],下标 0 到 9。
- 访问操作:借助 [] 操作符,像 arr [7]、arr [3] 可分别访问对应下标元素。
例如:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", arr[6]);//7 printf("%d\n", arr[2]);//3 return 0; }
(2)数组元素打印
- 下标生成:用 for 循环(如 for (i = 0; i < 10; i++))生成 0 到数组大小减 1 的下标。
- 打印输出:在循环里以 printf ("% d", arr [i]); 打印元素,展示整个数组数据。
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; for(i=0; i<10; i++) { printf("%d ", arr[i]); } return 0; }
(3)数组输入设置
- 数据录入:在循环(for (i = 0; i < 10; i++))中用 scanf ("% d", &arr [i]); 接收用户输入数据并存入数组。
- 结果确认:输入后再次循环打印数组,检查数据是否正确存储,保障后续处理准确。
#include <stdio.h> int main() { int arr[10]; int i = 0; for(i=0; i<10; i++) { scanf("%d", &arr[i]); } for(i=0; i<10; i++) { printf("%d ", arr[i]); } return 0; }
5. 一维数组在内存中的存储
依次打印数组元素的地址:
#include <stdio.h> int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int i = 0; for(i=0; i<10; i++) { printf("&arr[%d] = %p\n ", i, &arr[i]); } return 0; }
仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。
由此可以得出结论:数组在内存中是连续存放的。
6.sizeof计算数组元素个数
(1)sizeof 关键字的作用
- 计算类型或变量大小:
- sizeof 是 C 语言中的关键字,它能够计算数据类型或者变量所占的字节大小。
- 例如,对于基本数据类型 int,在常见的 32 位系统中,sizeof (int) 通常为 4 字节。
- 计算数组大小:
- 当应用于数组时,sizeof 可以返回整个数组所占的字节数。比如对于 int arr [10] 数组,使用 sizeof (arr) 会得到该数组总共占用的字节数,在这种情况下结果为 40 字节(因为每个 int 元素占 4 字节,共 10 个元素)。
#include<stdio.h> int main() { int arr[10] = { 0 }; printf("%d\n", sizeof(arr));//40 return 0; }
(2)计算数组元素个数的方法
- 原理:
- 由于数组中所有元素类型相同,我们只要知道一个元素所占字节数,用数组总字节数除以单个元素字节数就能得到元素个数。通常选择数组的第一个元素来计算其大小,即 sizeof (arr [0])。
- 计算示例:
- 对于 int arr [10] 数组,先通过 sizeof (arr) 得到数组总大小,再除以 sizeof (arr [0])。在代码中,int sz = sizeof (arr)/sizeof (arr [0]); 这样计算后,变量 sz 的值即为数组的元素个数,在此例中结果为 10。
#include<stdio.h> int main() { int arr[10] = { 0 }; int sz = sizeof(arr) / sizeof(arr[0]); printf("%d\n", sz); return 0; }
三、二维数组
1. 二维数组的创建
- 概念引入:
- 基于一维数组,当把一维数组作为数组元素时,就形成了二维数组。若进一步拓展,二维数组作为元素可构成三维数组,二维及以上的数组统称为多维数组。二维数组提供了一种更复杂的数据组织方式,适用于处理具有行和列结构的数据。
- 语法结构:
type arr_name[常量值1][常量值2];
例如:
int arr[3][5]; //3表⽰数组有3⾏ //5表⽰每⼀⾏有5个元素 //int表⽰数组的每个元素是整型类型 //arr是数组名,可以根据⾃⼰的需要指定名字
2. 二维数组的初始化
(1)不完全初始化
- 部分赋值:
- 如 int arr1 [3][5] = {1, 2}; ,仅初始化部分元素,余者为 0。适用于部分值已知场景。
- int arr2 [3][5] = {0}; 可将数组全置 0,常用于计数数组初始化。
int arr1[3][5] = {1,2}; int arr2[3][5] = {0};
(2)完全初始化
- 顺序赋值:
- int arr3 [3][5] = {1, 2, 3, 4, 5, 2, 3, 4, 5, 6, 3, 4, 5, 6, 7}; 依次赋所有值,用于精确设定各元素,像图像像素值初始化。
int arr3[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
(3)按行初始化
- 分组设行:
- int arr4 [3][5] = {{1, 2}, {3, 4}, {5, 6}}; 按行分组赋初值,未指定元素为 0,直观体现行结构,如学生成绩表初始化。
int arr4[3][5] = {{1,2},{3,4},{5,6}};
(4)省略行初始化(列不能省略)
- 依值推行:
- int arr5 [][5] = {1, 2, 3}; 等省略行的情况,编译器依列数和初值算行数。列数必明,使代码简洁,适用于行数可推导场景。
nt arr5[][5] = {1,2,3}; int arr6[][5] = {1,2,3,4,5,6,7}; int arr7[][5] = {{1,2}, {3,4}, {5,6}};
3. 二维数组的使用
(1)二维数组下标运用
- 下标起始规则:
- 二维数组行与列下标均从 0 起,如 int arr [3][5],行 0 至 2,列 0 至 4,借此可精准定位元素.
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
(2)二维数组输入输出操作
- 下标生成方式:
- 借嵌套循环生成下标,外层循环控行(for (i = 0; i < 3; i++)),内层循环控列(for (j = 0; j < 5; j++))以遍历数组。
- 数据输入要点:
- 循环内用 scanf ("% d", &arr [i][j]); 依行列顺序接收用户输入并存入对应元素,适用于手动录入矩阵数据等场景。
- 数据输出方法:
- 以 printf ("% d", arr [i][j]); 按行列输出元素,内层循环结束后加 printf ("\n"); 换行,直观展示矩阵数据,利于调试与分析。
#include <stdio.h> int main() { int arr[3][5]; int i = 0;//遍历⾏ //输⼊ for(i=0; i<3; i++) //产⽣⾏号 { int j = 0; for(j=0; j<5; j++) //产⽣列号 { scanf("%d", &arr[i][j]); //输⼊数据 } } //输出 for(i=0; i<3; i++) //产⽣⾏号 { int j = 0; for(j=0; j<5; j++) //产⽣列号 { printf("%d ", arr[i][j]); //输出数据 } printf("\n"); } return 0; }
4. 二维数组在内存中的存储
打印二维数组所有元素的地址:
#include <stdio.h> int main() { int arr[3][5] = { 0 }; int i = 0; int j = 0; for (i = 0; i < 3; i++) { for (j = 0; j < 5; j++) { printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]); } } return 0; }
1. 行内元素存储
- 紧密相邻:
- 每行内元素相邻,因整型元素占 4 字节,地址差 4 字节,便于按行高效访问与处理。
2. 跨行元素存储
- 连续无隙:
- 像 arr [0][4] 与 arr [1][0] 等跨行元素也差 4 字节,二维数组整体连续存放,按行或列访问依下标算偏移量即可定位元素,虽按列可能稍慢,但连续存储保障了操作基础并助力理解高维数组存储。
四、数组越界
1. C 语言对越界处理
- 无自动检查:
- C 语言本身及编译器可能不检查越界,代码编译通过不代表无问题。
- 编程责任:
- 程序员要自行检查,编写循环等代码时严格控制下标范围,如修正 for 循环边界。
2. 一维数组越界
- 合法界定:
- 数组下标从 0 起,有 n 个元素时,合法范围是 0 至 n - 1,如 int arr [10],下标 0 到 9 有效。
- 越界判定:
- 小于 0 或大于 n - 1 即为越界,像访问 arr [10](n = 10)就超出合法空间。
#include <stdio.h> int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int i; // 下面的循环会出现越界访问的情况,当 i = 10 时,访问 arr[10] 超出了数组的合法下标范围 for (i = 0; i <= 10; i++) { printf("arr[%d] = %d\n", i, arr[i]); } return 0; }
3.二维数组越界
- 行列越界情形:
- 对于 int arr [m][n],行下标 0 至 m - 1,列下标 0 至 n - 1,超出此范围如 arr [3][0](行越界)或 arr [0][5](列越界)会致错误,处理二维数组时务必确保行列下标合法。
#include <stdio.h> int main() { int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int i, j; // 下面的循环会出现行越界访问的情况,当 i = 3 时,访问 arr[3][j] 超出了二维数组行下标的合法范围 for (i = 0; i <= 3; i++) { for (j = 0; j < 4; j++) { printf("arr[%d][%d] = %d ", i, j, arr[i][j]); } printf("\n"); } return 0; }
五、拓展:变长数组
1. C99 前数组创建限制
- 数组大小仅能用常量、常量表达式指定,或初始化时省略让编译器推导。如 int arr1 [10]; int arr2 [3 + 5]; int arr3 [] = {1, 2, 3}; 这种方式缺乏灵活性,易造成空间分配不合理。
2. 变长数组概念
- C99 引入变长数组特性,可使用变量确定数组大小。如 int n = a + b; int arr [n]; 这里 arr 数组长度取决于运行时变量 n 的值,编译时无法确定。
3. 变长数组特性
- 不能初始化,因其长度在运行时才明确。
- 优势在于运行时能按需分配精确长度,避免开发时预估失误。
- 注意其大小仅在运行时确定,确定后不可再变,并非可动态改变大小。
4.变长数组使用示例
- 如下代码,先由用户输入确定 n 值来创建变长数组 arr,再输入数组元素并输出。
#include <stdio.h> int main() { int n = 0; scanf("%d", &n); int arr[n]; int i = 0; for (i = 0; i < n; i++) { scanf("%d", &arr[i]); } for (i = 0; i < n; i++) { printf("%d ", arr[i]); } return 0; }
5.编译器支持情况
- VS2022 虽支持多数 C99 语法,但不支持变长数组,而 devc 等编译器可使用。
六、数组作为函数参数
在写代码过程中,常常会把数组作为参数传递给函数,比如我们想要实现一个对整形数组进行排序的冒泡排序函数。
现在我们来详细探讨一下数组作为函数参数时涉及到的一些要点以及容易出现的问题。
1.冒泡排序算法及错误示例
冒泡排序是通过重复比较相邻元素并交换来排序,像气泡上浮般将小元素 “浮” 到数列顶端(升序)。
看如下代码:
#include <stdio.h> 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); int i = 0; for(i = 0; i < sizeof(arr)/sizeof(arr[0]); i++) { printf("%d ", arr[i]); } return 0; }
调试之后可以看到 bubble_sort 函数内部的sz ,是2难道数组作为函数参数的时候,不是把整个数组的传递过去?
错误原因
- 1. 计算错误根源
在
bubble_sort
函数里,int sz = sizeof(arr)/sizeof(arr[0]);
计算数组长度的方式有误。
- 2. 参数传递本质
数组作函数参数时,传递的是首元素地址,类似指针。
- 3.
sizeof
偏差所以在函数内,
sizeof(arr)
得到的是指针大小,32 位系统为 4 字节,64 位系统为 8 字节,并非数组真实大小。
- 4. 排序异常原因
错误的
sz
值使排序循环依赖出错,无法按数组实际元素个数正常遍历,导致排序异常。2. 数组名是什么?
先看看以下代码:
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5 }; printf("%p\n", arr); printf("%p\n", &arr[0]); printf("%d\n", *arr); printf("%d\n", sizeof(arr)); //输出结果 return 0; }
结论
数组名在多数情况下是数组首元素的地址,但有两个例外:
- 当使用
sizeof(数组名)
时,数组名表示整个数组,此时sizeof
计算的是整个数组的大小,比如int arr[10] = { 1,2,3,4,5 };
,sizeof(arr)
输出的结果是整个数组所占字节数(这里int
类型占 4 字节,数组共 10 个元素,所以结果是 40)。- 当使用
&数组名
时,取出的是数组的地址,此时数组名表示整个数组。
除了这两种情况之外,所有的数组名都表示数组首元素的地址。3. 冒泡排序函数的正确设计
(1)原理
由于数组传参时只是把数组的首元素的地址传递过去,即便函数参数写成
int arr[]
的形式,它实际表示的依然是一个指针,在函数内部sizeof(arr)
结果通常是指针的大小(如常见的 4 字节)。(2) 正确代码示例
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; }
这里通过在函数参数中额外接收数组元素个数
sz
,避免了在函数内部错误计算数组长度的问题,从而能够正确地对传入的数组进行冒泡排序操作。
七、实战演练
1. 字符两端汇聚展示
实现字符从两端向中间移动汇聚的效果,步骤清晰。
初始化准备:
定义字符数组arr1
存放目标字符串,如"welcome to CHN..."
,arr2
初始化为#
组成的相同长度字符串。再设left
为 0,right
指向arr1
末尾。核心循环:
while (left <= right)
循环中,先暂停 1 秒(Sleep(1000)
),接着将arr1
对应位置字符赋给arr2
,然后更新left
与right
并打印arr2
,逐步呈现汇聚效果。示例代码:
#include <stdio.h> #include<windows.h> int main() { char arr1[] = "welcome to CHN..."; char arr2[] = "#################"; int left = 0, right = strlen(arr1) - 1; printf("%s\n", arr2); while (left <= right) { Sleep(1000); arr2[left] = arr1[left]; arr2[right] = arr1[right]; left++; right--; printf("%s\n", arr2); } return 0; }
2. 高效二分查找
在有序数组中查找特定数字,二分查找优势明显。
初始化关键变量:
给定有序整型数组arr
,设left
为 0,right
为数组末尾下标。确定要查找的key
值,初始化记录中间下标mid
与查找结果标记find
。查找循环:
while (left <= right)
内,先计算mid
(推荐mid = left + (right - left) / 2
)。依arr[mid]
与key
大小关系更新left
或right
。若相等则标记find
并跳出。最后依find
输出结果。示例代码:
#include <stdio.h> int main() { int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int left = 0, right = sizeof(arr) / sizeof(arr[0]) - 1; int key = 7, mid = 0, find = 0; while (left <= right) { mid = left + (right - left) / 2; if (arr[mid] > key) right = mid - 1; else if (arr[mid] < key) left = mid + 1; else { find = 1; break; } } if (find) printf("找到了,下标是%d\n", mid); else printf("找不到\n"); return 0; }
注意:
二分查找依赖数组有序性,乱序时不可用。