目录
- 一维数组:以学生成绩列表做例子
- 定义:
type arrayName [ arraySize ];
,arraySize必须是常数 - 初始化:
{}
,memset
- 元素访问:下标索引
- 遍历数组:使用循环遍历数组
- 课堂练习:结构体数组遍历
- 定义:
- 二维数组:以图像作为例子
- 定义
- 初始化
- 元素访问
- 遍历数组
- 字符串:
- 初始化
- 基本函数
- 输入输出
- 扩展:
静态数组
与动态数组
变长数组
(C99)
0 前言和引入
- 我们来思考一个问题,假设我们要做一个
学生成绩管理程序
,要求我们对一个班里头50位同学进行成绩登记和存储,同时我们要输出最高分,最低分和平均分以及挂科同学的人数。 - 为了简化问题我们假定同学的成绩是以
int
类型存储的成绩,分数范围在[0,100]
,结合之前学习的相关知识,我们搜先需要创建50个int
类型的数据进行存储如下:
int student1Score=90;
int student2Score=54;
int student3Score=88;
int student4Score=90;
int student5Score=24;
int student6Score=56;
int student7Score=65;
int student8Score=78;
int student9Score=100;
//......省略......
- 然后对这50个变量进行最高分,最低分,平均分的处理,肉眼可见,这简直太麻烦了!!!
- 那是否有这样的一个数据结构,可以帮助我们存储像这样具有相同类型且同质化可以方便统一管理的数据类型呢?那就是
数组
。
1 一维数组
1-1 介绍
-
数组
是一种数据结构,用于存储多个具有相同类型
的数据元素。数组非常有用,因为它们允许我们将多个相关数据项组织在一起,并通过对一个共同的名称进行引用来访问它们。 -
同样是上面那个例子,我们可以创建一个
int
类型的数组来存储整个班级同学的成绩。
int studentScores[50] = {90, 85, 78, 92, 88,};
- 这里,
studentScores
是一个包含50个int类型的的数组,每个整数代表一个学生的成绩。可以理解就如同上面的Excel表格所示,数组提供了一个类似于容器的操作把50个独立的int变量进行统一存储。
1-2 定义
- 下面是一个基础C语言一维数组的定义方法(
一维
是啥后面会说)
type arrayName [arraySize];
- 其中特别注意是
arraySize
必须是一个大于零的整数常量(变长数组
后面说),type
可以是任意有效的 C 数据类型。(结构体数组
后面说)
int array_int[5];
double array_double[5];
-
数组定义就如同创建了一个5个数据类型大小的数组,接着我们需要往里头填充数据,也就是初始化。
1-2-1
- 值得一提的是如果在不使用
变长数组
的情况下,我们可以通过改变常量的方式去人为“动态”地修改数组大小
#define ARRAY_SIZE 5
int array_int[ARRAY_SIZE];
1-2-2
- 还可以这样定义加上初始化数组
int numbers[] = {1, 2, 3, 4, 5};
1-3 初始化
- 这边介绍两种数组初始化的方法:
1-3-1 直接初始化:
int array_int[10] = {100, 82, 35, 75, 50};
- 注意大括号 { } 之间的值的数目不能大于我们在数组声明时在方括号 [ ] 中指定的元素数目。
- 数组初始化就是往上述格子里头填数据
- 那这时候你可能会有两个疑问:
- 不初始化会怎么样
- 如果有初始化一个长度为100的数组全部为0我是不是要写100个0
1-3-2 不初始化数组会发生啥
- 未初始化的全局或静态数组:在C语言中,如果一个全局或静态数组未被显式初始化,它的元素将被自动初始化为0。这意味着数组中的每个元素都将具有确定的初始值,即0。\
int global_array[10]; // 全局数组,所有元素自动初始化为0
- 未初始化的局部数组:局部数组(在函数内部声明的数组)如果没有被显式初始化,它们的元素将包含不确定的值。这是因为局部变量的存储位置(如栈帧)在函数调用之间可能会被其他函数的局部变量覆盖,因此它们的初始值是不确定的。
void function() {
int local_array[10]; // 局部数组,元素包含不确定的值
// 使用local_array之前,必须确保它被正确初始化
}
- 部分初始化的数组:如果你只初始化了数组的一部分,剩余的元素将根据数组的存储类型自动初始化。对于全局和静态数组,剩余的元素将被初始化为0;对于局部数组,剩余的元素将包含不确定的值。
int array_partially_initialized[10] = {1, 2, 3}; // 前3个元素被初始化,其余元素包含不确定的值
1-3-3 快速初始化memset
(进阶)
- 初始化一个长度为100的数组全部为0我是不是要写100个0
- 肯定不用,C语言提供了方便的初始化函数
void *memset(void *s, int ch, size_t n);
- s,指针,要赋值的内存的起始地址。(下一节我们会讲)
- ch,用于设置给内存块的值。
- n,指定设置为ch值的字节数
int arr_int[100] = {0};
memset(arr_int, 0, sizeof(arr_int));
1-4 数组访问和数组长度
-
在开始讲访问和数组之前,需要先明确一个基本的概念,数组的索引
Index
。 -
不知道有没有通过一个笑话:
程序员是从0开始计数的。
-
为什么这样说,数组的索引可以很好的诠释,因为数组的索引就是从0开始的。
-
什么是索引:
Index
,顾名思义就是表示元素在数组中的位置的标识,就行书本的页码一样,每个存储在数组中的元素都有对应独立的索引,且索引的计算是从0开始计数的,因此一个长度为10的数组的最大索引是9!!!
1-4-1 数组的访问
- 数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。
int array[5]={10,21,12,23,43};
int a = array[5];
- 那么请问请问a等于多少?弹幕里回答一下
-
答案是你被骗了!!!会报错!!!==因为数组的索引最大不超过数组长度-1!!!==因为数组的索引是从0开始计算的!!!
-
因此这道题没有答案,记住了吗。
int array[5]={10,21,12,23,43};
/*
array[0]--10
array[1]--21
array[2]--12
array[3]--23
array[4]--43
array[5]--报错,数组越界!!!!!!!!
*/
- 边界检查:在实际编程中,数组越界是一个常见的错误。应强调在访问数组元素之前检查索引的有效性,以避免数组越界访问。
1-4-2 数组长度计算
- 数组长度可以使用 sizeof 运算符来获取数组的长度
int arr[] = {1, 2, 3, 4, 5};
int array_size = sizeof(arr) / sizeof(arr[0]);
- 直接使用数组长度:如果你知道数组的长度,你就可以知道数组长度(废话)
灵魂拷问
:那么为啥要计算数组长度,这不是显而易见的吗,数一数写上去就行了吗?- 在编程中直接书写数字(硬编码)通常被认为是一种不好的做法,因为它会导致代码的可读性、可维护性和可扩展性变差。
- 可读性差:例如,
int numbers[42];
,这里的数字42没有提供任何关于数组应该有多大或它代表什么的信息。 - 可维护性差:例如,如果数组的大小是基于某个配置文件或用户输入的,那么硬编码的数字就需要在多个地方更新,增加了出错的可能性。
- 难以调试:如果代码中出现错误,硬编码的数字可能会使得调试变得更加困难。编译器生成的错误信息可能不会提供足够的信息来定位问题,因为数字本身没有上下文。
- 灵活性差:硬编码的数字使得代码难以适应不同的需求或环境。如果需要改变数组的大小,可能需要在代码中查找并更新所有硬编码的数字,这是一个容易出错且费时的过程。
1-5 数组遍历
- 啥叫遍历?
数组遍历
是指按顺序访问数组中的每一个元素,通常用于对数组中的数据进行操作,如读取、修改、计算等。 - 我们可以使用循环遍历数组
#include <stdio.h>
int main() {
int array[] = {1, 2, 3, 4, 5};
int length = sizeof(array) / sizeof(array[0]);
for (int i = 0; i < length; i++) {
printf("%d ", array[i]);
}
return 0;
}
1-6 结构体数组和数组遍历
- 我们来看看结构体数组,假设我们学生结构体如下
struct Student
{
int ID; //学号
int score; //分数
};
- 初始化
struct Student students[3] =
{ {1, 90}, // 第一个学生的学号是1,分数是90
{2, 85}, // 第二个学生的学号是2,
分数是85 {3, 92} // 第三个学生的学号是3,分数是92
};
课堂练习–2分钟
- 那么现在尝试去遍历数组输出:
"Student ID: %d, Score: %d\n"
,要求需要代码实现计算数组长度,不允许直接写数字s
- 很简单,如下
// 计算学生结构体数组的大小
int length = sizeof(students) / sizeof(students[0]); // 遍历学生结构体数组
for (int i = 0; i < length; i++)
{
printf("Student ID: %d, Score: %d\n", students[i].ID, students[i].score);
}
2 二维数组
- 上头一直一直在说一维一维数组,一维是啥,可以这样理解:
- 一维:线
- 二位:面
- 三维:体
- C 语言支持多维数组。多维数组声明的一般形式如下:
type name[size1][size2]...[sizeN];
2-1 定义
- 多维数组最简单的形式是二维数组。一个二维数组,在本质上,是一个一维数组的列表。声明一个 x 行 y 列的二维整型数组,形式如下:
type arrayName [ x ][ y ];
- 如上图是一个5行两列的二维数组
int table[5][2];
- 如果有同学学过
矩阵
的话(线性代数),二位数组可以通过矩阵来理解。如下,二维数组中的每个元素是使用形式为 a[ i , j ] 的元素名称来标识的,其中 a 是数组名称,i 和 j 是唯一标识 a 中每个元素的下标。
2-2 初始化
- 多维数组可以通过在括号内为每行指定值来进行初始化。下面是一个带有 3 行 4 列的数组。
int arr[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
- 或者可以这样写
int arr[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
- 同样使用
memset
int arr[3][4]; // 声明一个 3x4 的整数数组
// 使用 memset 将整个数组初始化为 0
memset(arr, 0, sizeof(arr));
2-3 元素访问和数组大小计算
2-3-1 元素访问
- 二维数组中的元素是通过使用下标(即数组的行索引和列索引)来访问的。例如:
int val = a[2][3];
- 那么再来一题目,回答一下下述二维数组的
a[2][3]
分别是什么?
int arr[3][4] = {
{0, 1, 2, 3} , /* 初始化索引号为 0 的行 */
{4, 5, 6, 7} , /* 初始化索引号为 1 的行 */
{8, 9, 10, 11} /* 初始化索引号为 2 的行 */
};
- 答案是11,最后一个元素
2-3-2 二维数组长度
sizeof(arr)
是整个二维数组的大小,sizeof(arr[0])
是第一行的大小,因此sizeof(arr) / sizeof(arr[0])
可以得到行数。- 同样,
sizeof(arr[0][0])
是单个元素的大小,所以sizeof(arr[0]) / sizeof(arr[0][0])
可以得到列数。
#include <stdio.h>
int main() {
int arr[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 获取行数
int rows = sizeof(arr) / sizeof(arr[0]);
// 获取列数
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);
printf("行数: %d\n", rows);
printf("列数: %d\n", cols);
return 0;
}
2-4 二位数组数组遍历
- 如下
#include <stdio.h>
int main() {
int arr[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 获取行数
int rows = sizeof(arr) / sizeof(arr[0]);
// 获取列数
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);
printf("行数: %d\n", rows);
printf("列数: %d\n", cols);
// 遍历数组并打印每个元素
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
3 字符串
3-1 初始化
- 在 C 语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组。因此,\0 是用于标记字符串的结束。
- *空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。
- 以上面的
'Hello!'
为例子
char str[] = "Hello!";
3-2 基础函数
- C语言标准库提供了一些处理字符串的函数,例如
strcpy
,strcat
,strlen
,strcmp
等。
strcpy
- 复制字符串:将源字符串复制到目标字符串中。
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello";
char target[10]; // 确保目标数组足够大,能够容纳源字符串
strcpy(target, source);
printf("Copied string: %s\n", target);
return 0;
}
strcat
- 连接字符串:将源字符串连接到目标字符串的末尾。
#include <stdio.h>
#include <string.h>
int main() {
char source[] = " World";
char target[20] = "Hello"; // 确保目标数组足够大,能够容纳连接后的字符串
strcat(target, source);
printf("Concatenated string: %s\n", target);
return 0;
}
strlen
- 获取字符串长度:返回字符串的长度,不包括空字符\0
。
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "Hello";
int length = strlen(str);
printf("Length of string: %d\n", length);
return 0;
}
strcmp
- 比较字符串:比较两个字符串,如果相等则返回 0,如果第一个字符串小于第二个字符串则返回负数,如果第一个字符串大于第二个字符串则返回正数。
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "Hello";
char str2[] = "World";
int result = strcmp(str1, str2);
if (result == 0) {
printf("Strings are equal.\n");
} else if (result < 0) {
printf("First string is less than second string.\n");
} else {
printf("First string is greater than second string.\n");
}
return 0;
}
3-3 输入输出
在C语言中,字符串的输入输出可以通过几种不同的方式进行。
字符串输出
- 使用
printf
函数:printf
函数可以用来输出字符串。使用%s
格式说明符来打印字符串。
#include <stdio.h>
int main() {
char str[] = "Hello, World!";
printf("%s\n", str); // 输出字符串
return 0;
}
- 使用
puts
函数:puts
函数专门用于输出字符串,并在字符串末尾自动添加一个换行符。
#include <stdio.h>
int main() {
char str[] = "Hello, World!";
puts(str); // 输出字符串并添加换行符
return 0;
}
字符串输入
- 使用
scanf
函数:scanf
函数可以用来读取字符串。使用%s
格式说明符来读取字符串,但需要注意的是,scanf
会在遇到空白字符(如空格、制表符或换行符)时停止读取。
#include <stdio.h>
int main() {
char str[100]; // 假设输入的字符串不会超过99个字符
printf("Enter a string: ");
scanf("%99s", str); // 读取字符串,最多读取99个字符
printf("You entered: %s\n", str);
return 0;
}
- 使用
fgets
函数:fgets
函数可以读取一行数据,包括空格,直到遇到换行符或文件结束符。它是读取字符串的更安全的选择,因为它允许指定最大读取长度,并且会保留换行符。
#include <stdio.h>
int main() {
char str[100]; // 假设输入的字符串不会超过99个字符
printf("Enter a string: ");
fgets(str, sizeof(str), stdin); // 读取字符串,最多读取99个字符
printf("You entered: %s", str);
return 0;
}
- 使用
gets
函数:gets
函数已经不被推荐使用,因为它不会检查目标缓冲区的大小,容易造成缓冲区溢出。在某些环境中,它已经被移除。如果你在旧代码中看到gets
,应该避免使用它,并使用fgets
替代。
#include <stdio.h>
int main() {
char str[100]; // 假设输入的字符串不会超过99个字符
printf("Enter a string: ");
gets(str); // 不推荐使用
printf("You entered: %s\n", str);
return 0;
}
4扩展
4-1 静态数组
与动态数组
静态数组
:静态数组在声明时分配内存,并且其大小在编译时确定,不能在运行时改变。静态数组通常存储在栈(stack)上,其生命周期与声明它们的函数或代码块相同。
int arr[5];
int arr[] = {1, 2, 3, 4, 5};
动态数组
:动态数组在运行时分配内存,其大小可以在运行时确定,并可以根据需要重新分配。动态数组通常存储在堆(heap)上,其生命周期可以通过代码来控制。
int size = 5;
int *array = malloc(size * sizeof(int));
free(array);
4-2 变长数组
(C99)
- 变长数组(VLA),也称为可变长度数组,是C99标准引入的一个特性,它允许数组的大小在运行时确定。变长数组是静态数组的一种,但其大小不是在编译时确定的,而是在运行时确定的。
#include <stdio.h>
int main() {
int n;
printf("请输入数组的大小: ");
scanf("%d", &n);
// 定义变长数组
int arr[n];
// 初始化数组
for (int i = 0; i < n; i++) {
arr[i] = i;
}
// 打印数组
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
总结
- 本节课介绍了C语言数组的概念,分别介绍了一维数组,二维数组,以及字符串,和一些进阶的C语言内容,下一节我们来讲讲指针。