5. 初识数组
1. 数组的概念
数组(Array)是C语言中用于存储相同类型数据的集合。数组中所有元素的类型相同,并且它们在内存中是连续存储的。数组的大小在定义时必须指定,并且一旦定义,大小就不能更改。
- 数组的索引是从0开始的,也就是说,第一个元素的索引为0,第二个元素的索引为1,以此类推。
- 数组可以是任何数据类型,比如
int
、float
、char
等。 - 数组中存放的是1个或者多个数据,但是数组元素个数不能为0。
2. 一维数组的创建和初始化
2.1 数组创建
数组创建时,需要声明数组的类型、名称以及数组的大小(元素个数)。数组大小必须是一个正整数。
语法:
数据类型 数组名[数组大小];
- 数据类型:指定数组元素的类型,如int、char、float等。
- 决定了每个数组元素占用的内存大小
- 影响数组可以存储的值的范围和精度
- 数组名:用于标识和访问数组的唯一标识符。
- 遵循C语言变量命名规则
- 通常选择有意义的名称,反映数组用途
- 数组大小:方括号[]内指定数组的元素个数。
- 必须是正整数或常量表达式
- 决定了数组占用的总内存空间
例如:
int arr[5]; // 创建一个包含5个整数的数组
这里,arr
是一个可以存储5个整数的数组,数组大小为5。因此,数组下标从arr[0]
到arr[4]
。未被初始化的数组元素通常会包含垃圾值(未定义的值)。
2.2 数组的初始化
数组的初始化是指在数组声明时为数组元素分配初始值。可以在声明数组的同时,通过花括号{}
提供初始值。
2.2.1 不完全初始化
在不完全初始化的情况下,我们可以只为部分数组元素赋值,剩余的元素将会被自动初始化为零。
示例:
int arr[5] = {1, 2}; // 只有前两个元素被初始化,其他元素自动初始化为 0
这个数组中的元素如下:
arr[0] = 1
arr[1] = 2
arr[2] = 0
arr[3] = 0
arr[4] = 0
注意,只有在静态或全局数组中,未被初始化的元素才会自动初始化为零。在局部数组中,未初始化的元素通常是垃圾值,除非手动进行初始化。
2.2.2 完全初始化
完全初始化是指为数组中的每个元素都显式地提供一个初始值。如果数组大小与初始化列表中的元素数量相同,数组将被完全初始化。
示例:
int arr[5] = {1, 2, 3, 4, 5}; // 为每个元素指定了初始值
这个数组的初始化结果为:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
在这种情况下,所有数组元素都已经被明确初始化。
2.2.3 数组的类型
数组在C语言中被视为一种复合数据类型。每个数组都有其特定的类型,这个类型由两个主要因素决定:元素的数据类型和数组的大小。理解数组的类型对于正确使用数组和进行数组操作至关重要。
数组类型的组成
数组的类型由以下两部分组成:
- 元素类型:指定数组中每个元素的数据类型(如int、char、float等)
- 数组大小:指定数组中元素的数量
数组的类型可以通过去掉数组声明中的数组名来得到。例如:
int arr1[10];
int arr2[12];
char ch[5];
在这些声明中:
- arr1的类型是
int [10]
,表示一个包含10个整型元素的数组 - arr2的类型是
int [12]
,表示一个包含12个整型元素的数组 - ch的类型是
char [5]
,表示一个包含5个字符的数组
数组类型的重要性
理解数组类型的概念很重要,原因如下:
- 内存分配:数组类型决定了数组在内存中占用的总空间大小
- 指针运算:在进行指针运算时,编译器需要知道数组的类型来正确计算偏移量
- 函数参数:当数组作为函数参数传递时,了解数组类型可以帮助正确声明函数参数
- 类型检查:编译器使用数组类型信息来执行类型检查,确保操作的合法性
数组类型和数组名
值得注意的是,数组名本身就是一个指向数组第一个元素的指针。例如,对于int arr[10]
,arr
的类型是int *
,但它指向的是整个数组int [10]
。这种微妙的区别在进行指针运算和数组传递时特别重要。
总之,理解数组类型不仅有助于正确声明和使用数组,在学习指针之后还能帮助我们更好地理解C语言中数组和指针的关系。
3. 一维数组的使用
3.1 数组下标
C语言规定数组是有下标的。C语言中的数组下标是从0开始的,这意味着第一个元素的下标是0,最后一个元素的下标是数组大小 - 1
。
示例:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
这里,arr[0]
表示数组的第一个元素,输出结果为1。
在C语言中数组的访问提供了一个操作符[] ,这个操作符叫:下标引用操作符。
有了下标访问操作符,我们就可以轻松的访问到数组的元素了,比如我们访问下标为7的元素,我们就可以使用arr[7]
,想要访问下标是3的元素,就可以使用arr[3]
,如下代码:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d", arr[7]); // 输出数组的第八个元素,结果为8
printf("%d", arr[3]); // 输出数组的第四个元素,结果为4
3.2 数组元素的打印
我们可以通过循环遍历数组来打印每个元素,通常使用for
循环。
示例:
#include <stdio.h>
int main(int argc, char const *argv[])
{
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]); // 打印数组中的每个元素
}
return 0;
}
输出结果:
3.3 数组的输入
数组的元素可以通过用户输入来获取。我们可以使用循环和scanf
函数从键盘读取数组的元素值。
示例:
#include <stdio.h>
int main(int argc, char const *argv[])
{
int arr[6];
for(int i = 0; i < 6; i++) {
scanf("%d", &arr[i]); // 输入数组的每个元素
}
return 0;
}
这里,通过scanf
函数读取用户输入的5个整数并将其存储到数组中。
4. 一维数组在内存中的存储
数组在内存中是以连续的方式存储的。数组的每个元素占用相同的数据空间。假设数组的第一个元素的地址是1000
,且每个int
类型占用4个字节,那么第二个元素将存储在地址1004
,第三个元素存储在地址1008
,依此类推。
例如:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
如果arr[0]
存储在地址1000
,那么:
arr[0]
的地址是1000
,值是1
arr[1]
的地址是1004
,值是2
arr[2]
的地址是1008
,值是3
- ······
后面通过指针的学习,我们可以通过指针,也可以访问数组中的元素。数组名本质上是指向第一个元素的指针。例如:
int *ptr = arr;
printf("%d", *ptr); // 输出 arr[0] 的值,即 1
5. sizeof 计算数组元素个数
在C语言中,sizeof
运算符可以用来计算数组占用的总字节数。如果我们想知道数组中的元素个数,可以使用sizeof
来计算整个数组的大小,再除以单个元素的大小。
详细解释
sizeof(arr)
:返回整个数组占用的总字节数。在这个例子中,如果int占4字节,那么sizeof(arr)
将返回20(5 * 4字节)。sizeof(arr[0])
:返回数组中单个元素的字节大小。在这个例子中,它将返回4(假设int占4字节)。- **相除操作:**20 / 4 = 5,正确计算出数组元素个数。
示例:
int arr[] = {1, 2, 3, 4, 5};
int count = sizeof(arr) / sizeof(arr[0]);
在上面的例子中:
sizeof(arr)
返回整个数组的大小,以字节为单位。sizeof(arr[0])
返回数组中单个元素的大小。 因此,size
将会得到数组中元素的个数,这里为5。
优点
- 适用性广:这种方法可以用于任何类型的数组,包括
int
、float
、char
等。 - 自动适应:当数组大小改变时,不需要手动更新代码。
- 编译时计算:
sizeof
是在编译时计算的,不会影响运行时性能。
6. 二维数组的创建
6.1 二维数组的概念
二维数组可以被视为数组的数组,即一个矩阵。它由行和列组成,每个元素由两个下标来表示,第一个表示行,第二个表示列。二维数组在内存中也是连续存储的。
6.2 二维数组的创建
语法:
数据类型 数组名[行数][列数];
创建二维数组时,必须同时指定行数和列数。
示例:
int matrix[3][4]; // 创建一个包含3行4列的二维数组
解释:上述代码中出现的信息
-
3表示数组有3行
-
4表示每一行有4个元素
-
int 表示数组的每个元素是整型类型
-
matrix
是数组名,可以根据自己的需要指定名字 -
这个二维数组可以存储
3*4=12
个整数。
7. 二维数组的初始化
7.1 不完全初始化
与一维数组类似,二维数组的初始化也可以是不完全的,未初始化的元素会自动被设置为零。
示例:
int arr1[3][5] = {1,2}; // 仅初始化了部分元素
int arr2[3][5] = {0};
未初始化的元素将会自动设置为0。例如:
-
arr1[0][0] = 1
-
arr1[0][1] = 2
-
arr1[0][2] = 0
-
arr1[1][0] = 0
-
arr1[1][1] = 0
-
arr1[1][2] = 0
-
arr1[2][0] = 0
-
······
-
arr2[0][0] = 0
-
arr2[0][1] = 0
-
arr2[0][2] = 0
-
arr2[1][0] = 0
-
arr2[1][1] = 0
-
arr2[1][2] = 0
-
arr2[2][0] = 0
-
······
7.2 完全初始化
二维数组的完全初始化是指为数组的每个元素都提供初始值。
示例:
int arr3[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
该数组的元素初始化为:
-
arr3[0][0] = 1
-
arr3[0][1] = 2
-
arr3[0][2]= 3
-
arr3[0][3] = 4
-
······
7.3 按照行初始化
二维数组也可以按行初始化,通过嵌套花括号。
示例:
int arr4[3][5] = {{1,2},{3,4},{5,6}};
7.4 初始化时省略行,但不能省略列
在声明二维数组时,可以省略行数,让编译器根据初始化元素自动推导出行数,但列数必须指定。
示例:
int arr5[][5] = {1,2,3};
int arr6[][5] = {1,2,3,4,5,6,7};
int arr7[][5] = {{1,2}, {3,4}, {5,6}};
8. 二维数组的使用
8.1 二维数组的下标
其实二维数组访问也是使用下标的形式的,二维数组的元素通过两个下标来访问,第一个下标表示行,第二个下标表示列。例如:
int arr[3][5] = {1,2,3,4,5, 2,3,4,5,6, 3,4,5,6,7};
printf("%d", arr[0][1]); // 输出2
图中最左侧绿色的数字表示行号,第一行绿色的数字表示列号,都是从0开始的,比如,我们说:第2行,第3列,快速就能定位出4。这里,arr[0][1]
表示第1行第2列的元素,值为2
。
8.2 二维数组的输入和输出
二维数组的输入和输出通常通过嵌套循环来实现。
示例:
int matrix[2][2];
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 2; j++) {
scanf("%d", &matrix[i][j]); // 输入每个元素
}
}
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 2; j++) {
printf("%d ", matrix[i][j]); // 打印每个元素
}
printf("\n");
}
9. 二维数组在内存中的存储
二维数组在内存中是以行优先顺序(Row-major order)存储的,这意味着数组按行存储,第一个行的所有元素存储在连续的内存位置,接下来是第二行的元素,依此类推。
#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;
}
从输出的结果来看,每一行内部的每个元素都是相邻的,地址之间相差4个字节,跨行位置处的两个元素(如:arr[0][4]
和arr[1][0]
)之间也是差4个字节,所以二维数组中的每个元素都是连续存放的。
10. C99中的变长数组
在C99标准之前,C语言在创建数组的时候,数组大小的指定只能使用常量、常量表达式,或者如果我们初始化数据的话,可以省略数组大小。
C99标准引入了变长数组(variable-length array,简称 VLA),允许数组的大小在运行时动态确定。例如:
int n;
scanf("%d", &n); // 输入数组大小
int arr[n]; // 创建一个大小为n的数组
在这里,数组arr
就是变长数组,数组arr
的大小取决于用户在运行时输入的n
值。这使得数组更加灵活。
特点:
- 只能在函数内部声明,不能在全局范围或结构体中声明。
- 不能用
static
或extern
修饰。 - 变长数组的根本特征,就是数组长度只有运行时才能确定,所以变长数组不能初始化。
- 可以作为函数参数。
优点:
- 提高了代码的灵活性,可以根据运行时的需求分配内存。
- 避免了使用动态内存分配(如malloc)的复杂性。
缺点:
- 可能导致栈溢出,特别是当数组大小很大时。
- 不是所有编译器都支持。比如在VS2022上,虽然支持大部分C99的语法,但是没有支持C99中的变长数组。
11. 数组练习
练习1:多个字符从两端移动,向中间汇聚
这个练习目的是演示如何通过操作字符数组来实现字符从两端向中间移动的效果。
以下是详细的代码解释:
#include <stdio.h>
#include <string.h>
#include <windows.h> // 为了使用 Sleep 函数
int main()
{
char arr1[] = "welcome to bit..."; // 目标字符串
char arr2[] = "#################"; // 初始显示的字符串,与 arr1 长度相同
int left = 0; // 左侧起始索引
int right = strlen(arr1) - 1; // 右侧起始索引(字符串长度减1)
printf("%s\\n", arr2); // 打印初始状态
while (left <= right) // 当左右索引未相遇时继续循环
{
Sleep(1000); // 暂停1秒,增加视觉效果
arr2[left] = arr1[left]; // 从左侧替换字符
arr2[right] = arr1[right]; // 从右侧替换字符
left++; // 左索引向右移动
right--; // 右索引向左移动
printf("%s\\n", arr2); // 打印当前状态
}
return 0;
}
代码解析:
- 我们定义了两个字符数组:arr1 存储目标字符串,arr2 初始化为全#号
- 使用 left 和 right 两个索引分别从字符串的两端开始移动
- 在每次循环中,我们将 arr1 的对应字符复制到 arr2 中,然后移动索引
- 使用 Sleep 函数增加了视觉效果,让字符替换的过程更容易观察
- 循环继续直到 left 和 right 相遇,此时整个字符串已经完全替换
练习2:二分查找
在一个升序的数组中查找指定的数字n,很容易想到的方法就是遍历数组,但是这种方法效率比较低。
二分查找是一种高效的查找算法,适用于已排序的数组。它的基本思想是将查找区间不断二分,每次都与区间的中间元素比较,从而快速缩小查找范围。
下面是二分查找算法的详细实现和解释:
#include <stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 已排序的数组
int left = 0; // 左边界
int right = sizeof(arr) / sizeof(arr[0]) - 1; // 右边界
int key = 7; // 要查找的目标值
int mid; // 中间元素的索引
int found = 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
{
found = 1; // 找到目标值
break;
}
}
if (found)
printf("找到了,下标是 %d\\n", mid);
else
printf("找不到\\n");
return 0;
}
代码解析:
- 我们使用
left
和right
两个变量来表示当前搜索区间的边界 - 在每次循环中,我们计算中间索引
mid
,并将arr[mid]
与目标值key
比较 - 根据比较结果,我们调整搜索区间:如果
key
小于mid
处的值,搜索左半部分;如果大于,搜索右半部分 - 如果找到目标值,我们设置
found
标志并退出循环 - 循环结束后,我们根据
found
标志来输出结果
注意:计算 mid
时使用 mid = left + (right - left) / 2
而不是 (left + right) / 2
,这是为了避免在 left
和 right
都很大时可能发生的整数溢出问题。
二分查找的时间复杂度是 O(log n),这意味着它比简单的线性搜索(时间复杂度 O(n))要快得多,尤其是对于大型数组。
—完—
标签:初始化,arr,int,元素,初识,数组,left From: https://blog.csdn.net/weixin_44643253/article/details/143088245