1.一维数组
目标:通过思维导图了解学习一维数组的核心知识点:
1.1定义
使用 类型名 数组名[数组长度];
定义数组。
// 示例:
int arr[5];
1.2一维数组初始化
数组的初始化可以分为静态初始化和动态初始化两种方式。
它们的主要区别在于初始化的时机和内存分配的方式。
1.2.1一维数组静态初始化
定义:
在声明数组的同时为数组元素赋初值。这种方式在编译时就确定了数组的大小和元素的值,因此也称为编译时初始化。
特点:
编译时确定:数组的大小和元素的值在编译时就已经确定,编译器会为数组分配足够的内存空间。
自动计算大小:如果在初始化时省略数组的大小,编译器会根据初始化列表中的元素个数自动计算数组的大小。
默认值:如果初始化列表中的元素个数少于数组的大小,未初始化的元素会被自动初始化为0(对于数值类型)或空字符(对于字符类型)。
示例:
//完全初始化:
int arr[5] = {1, 2, 3, 4, 5};。//部分初始化:
int arr[5] = {1, 2};//自动计算长度:
int arr[] = {1, 2, 3};
1.2.2一维数组动态初始化
定义 :
在程序运行时为数组分配内存并初始化数组元素。
动态分配可以通过 malloc
、calloc
和 realloc
函数来实现,因此也称为运行时初始化。
运行时确定:数组的大小和元素的值在程序运行时确定,需要手动调用动态内存分配函数来分配内存。
手动管理内存:使用动态内存分配函数分配的内存需要手动释放,否则会导致内存泄漏。
灵活性:可以根据程序的需要动态地调整数组的大小。
动态内存分配函数定义:
(1)malloc
//函数原型
void* malloc(size_t size);
功能:
- 分配指定大小的内存块,并返回指向该内存块的指针。
- 分配的内存块未初始化,内容是未定义的(即可能包含垃圾值)。
参数:
size
:要分配的内存大小(以字节为单位)。
返回值:
- 成功:返回指向分配内存的指针。
- 失败:返回
NULL
。
示例:
int *ptr;ptr = (int *)malloc(5 * sizeof(int)); // 分配5个整数的内存空间if (ptr == NULL)
{printf("内存分配失败\n");return 1;
}
(2) calloc
//函数原型
void* calloc(size_t num, size_t size);
功能:
- 分配
num
个大小为size
的内存块,并将所有字节初始化为0。 - 返回指向分配内存的指针。
参数:
num
:要分配的内存块数量。size
:每个内存块的大小(以字节为单位)。
返回值:
- 成功:返回指向分配内存的指针。
- 失败:返回
NULL
。
示例:
int *ptr;ptr = (int *)calloc(5, sizeof(int)); // 分配5个整数的内存空间,并初始化为零if (ptr == NULL) {printf("内存分配失败\n");return 1;
}
(3) realloc
//函数原型
void* realloc(void* ptr, size_t size);
功能:
- 调整已分配内存块的大小。
- 如果成功,返回指向新内存块的指针;如果失败,返回
NULL
,原内存块不变。
参数:
ptr
:指向要调整大小的内存块的指针。size
:新的内存大小(以字节为单位)。
返回值:
- 成功:返回指向新内存块的指针。
- 失败:返回
NULL
,原内存块不变。
示例:
int *ptr;ptr = (int *)malloc(5 * sizeof(int)); // 分配5个整数的内存空间if (ptr == NULL)
{printf("内存分配失败\n");return 1;
}// 重新分配内存,扩大到10个整数的空间ptr = (int *)realloc(ptr, 10 * sizeof(int));if (ptr == NULL)
{printf("内存重新分配失败\n");return 1;
}
1.2.3注意事项
(1)内存释放:
使用完动态分配的内存后,必须调用 free 函数释放内存,以避免内存泄漏。
//示例:free(arr);
(2)空指针检查:
在分配内存后,检查返回的指针是否为 NULL,以确保内存分配成功。
//示例:if (arr == NULL)
{printf("内存分配失败\n");return 1;}
(3)内存对齐:
malloc 和 calloc 返回的内存地址是按系统要求对齐的,可以安全地存储任何类型的对象。
(4)内存调整:
使用 realloc 时,如果调整大小失败,返回 NULL,原内存块不变,需要手动处理这种情况。
//示例:int *new_arr = (int *)realloc(arr, new_n * sizeof(int));if (new_arr == NULL)
{printf("内存调整失败\n");free(arr); // 释放原内存return 1;}
1.3一维访问元素
使用下标访问:数组名[下标]
// 示例:int value = arr[0];
1.4求一维数组长度
使用 sizeof
操作符:sizeof(数组名) / sizeof(数组名[0])
// 示例:int length = sizeof(arr) / sizeof(arr[0]);
1.5一维数组边界检查
1.5.1边界检查的作用
防止数组越界访问,从而避免未定义行为和潜在的安全漏洞。数组越界访问可能导致程序崩溃、数据损坏或其他不可预测的行为。
1.5.2 什么是数组越界
数组越界是指访问数组中不存在的元素。例如,对于一个长度为5的数组 arr[5]
,访问 arr[5]
或 arr[-1]
都是越界访问。
1.5.3边界检查的重要性
-
避免未定义行为:访问越界的内存可能导致程序崩溃或产生不可预测的结果。
-
提高程序安全性:防止潜在的安全漏洞,如缓冲区溢出攻击。
-
调试方便:边界检查有助于快速定位和修复数组访问错误。
1.5.4如何进行边界检查
在访问数组元素时进行检查。
在访问数组元素之前,确保下标在合法范围内。
(2)使用循环时进行检查
在使用循环遍历数组时,确保循环变量在合法范围内。
1.5.5边界检查代码示例
(1)单个元素访问时的边界检查
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int index;printf("请输入要访问的数组索引 (0-4): ");scanf("%d", &index);// 边界检查if (index >= 0 && index < 5)
{printf("arr[%d] = %d\n", index, arr[index]);} else {printf("错误: 索引 %d 越界\n", index);}return 0;
}
(2)使用循环时的边界检查
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int n = sizeof(arr) / sizeof(arr[0]);printf("数组元素: ");for (int i = 0; i < n; i++)
{printf("%d ", arr[i]);}printf("\n");// 边界检查在循环中已经隐含int index;printf("请输入要访问的数组索引 (0-4): ");scanf("%d", &index);if (index >= 0 && index < n)
{printf("arr[%d] = %d\n", index, arr[index]);} else {printf("错误: 索引 %d 越界\n", index);}return 0;
}
(3)动态数组的边界检查
#include <stdio.h>
#include <stdlib.h>int main()
{int n;printf("请输入数组的大小: ");scanf("%d", &n);// 动态分配内存int *arr = (int *)malloc(n * sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 初始化数组元素for (int i = 0; i < n; i++) {arr[i] = i + 1;}// 打印数组元素printf("数组元素: ");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");// 边界检查int index;printf("请输入要访问的数组索引 (0-%d): ", n-1);scanf("%d", &index);if (index >= 0 && index < n) {printf("arr[%d] = %d\n", index, arr[index]);} else {printf("错误: 索引 %d 越界\n", index);}// 释放内存free(arr);return 0;
}
(4)使用宏进行边界检查
#include <stdio.h>#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))int main() {int arr[5] = {1, 2, 3, 4, 5};int index;printf("请输入要访问的数组索引 (0-%d): ", ARRAY_SIZE(arr) - 1);scanf("%d", &index);if (index >= 0 && index < ARRAY_SIZE(arr)) {printf("arr[%d] = %d\n", index, arr[index]);} else {printf("错误: 索引 %d 越界\n", index);}return 0;
}
(5)边界点的注意事项
-
边界检查:在访问数组元素之前,确保下标在合法范围内。
-
循环边界:确保循环变量在合法范围内,避免越界访问。
-
动态数组:动态数组的大小在运行时确定,边界检查尤为重要。
-
宏定义:使用宏定义简化边界检查。
1.6一维数组常规的遍历
一维数组遍历以下分析一维数组遍历的7种方法:
1.6.1 for
循环遍历
最常用的方法,适用于大多数情况。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5。int arr[] = {1, 2, 3, 4, 5}; //sizeof(arr)返回整个数组占用的字节数,sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数。int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//p打印提示信息,表示接下来将使用for循环遍历数组printf("使用 for 循环遍历数组:\n");//i从0开始,每次循环增加1,直到i小于数组的元素个数n。for (int i = 0; i < n; i++) //遍历,n是指sizeof()求出的元素个数
{//在每次循环中,打印数组中当前索引i对应的元素值。arr[i]表示数组arr中索引为i的元素printf("arr[%d] = %d\n", i, arr[i]);}return 0;
}/*使用 for 循环遍历数组输出的结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.2 while
循环遍历
适用于需要手动管理循环变量的情况。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5int arr[] = {1, 2, 3, 4, 5};//sizeof(arr)返回整个数组占用的字节数//sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//定义一个整型变量i,并初始化为0,用于作为数组的索引int i = 0;//打印提示信息,表示接下来将使用while循环遍历数组printf("使用 while 循环遍历数组:\n");//这是一个while循环,只要i小于数组的元素个数n,循环就会继续执行while (i < n) {//在每次循环中,打印数组中当前索引i对应的元素值//arr[i]表示数组arr中索引为i的元素printf("arr[%d] = %d\n", i, arr[i]);//每次循环结束后,将索引i的值增加1,以便访问数组中的下一个元素i++;}return 0;
}/*使用 while 循环遍历数组输出的结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.3 do-while
循环遍历
适用于需要至少访问一次数组元素的情况。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5int arr[] = {1, 2, 3, 4, 5};//sizeof(arr)返回整个数组占用的字节数//sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//定义一个整型变量i,并初始化为0,用于作为数组的索引int i = 0;//打印提示信息,表示接下来将使用do-while循环遍历数组printf("使用 do-while 循环遍历数组:\n");//这是一个do-while循环,它会先执行一次循环体中的代码//然后再检查循环条件i < n。只要条件为真,循环就会继续执行do {//在每次循环中,打印数组中当前索引i对应的元素值printf("arr[%d] = %d\n", i, arr[i]); //arr[i]表示数组arr中索引为i的元素//每次循环结束后,将索引i的值增加1,以便访问数组中的下一个元素i++;} while (i < n);return 0;
}/*使用 do-while 循环遍历数组输出结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.4指针遍历
通过指针操作可以更灵活地遍历数组。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5int arr[] = {1, 2, 3, 4, 5};//sizeof(arr)返回整个数组占用的字节数//sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//定义一个指向整型的指针ptr,并将其初始化为数组arr的首地址int *ptr = arr;//打印提示信息,表示接下来将使用指针遍历数组printf("使用指针遍历数组:\n");//这是一个for循环,i从0开始,每次循环增加1,直到i小于数组的元素个数nfor (int i = 0; i < n; i++) {//在每次循环中,打印数组中当前索引i对应的元素值//*(ptr + i)表示指针ptr指向的地址加上偏移量i后所指向的元素值printf("arr[%d] = %d\n", i, *(ptr + i));}return 0;
}/*使用指针遍历数组输出结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.5指针和 while
循环遍历
结合指针和 while
循环可以更灵活地遍历数组。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5int arr[] = {1, 2, 3, 4, 5};//sizeof(arr)返回整个数组占用的字节数//sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//定义一个指向整型的指针ptr,并将其初始化为数组arr的首地址int *ptr = arr;//定义一个指向整型的指针end,并将其初始化为数组arr的末尾地址(即arr + n)int *end = arr + n;//打印提示信息,表示接下来将使用指针和while循环遍历数组printf("使用指针和 while 循环遍历数组:\n");//这是一个while循环,只要指针ptr小于指针end,循环就会继续执行while (ptr < end) {//在每次循环中,打印数组中当前指针ptr所指向的元素值//ptr - arr计算当前指针相对于数组首地址的偏移量,*ptr表示指针ptr所指向的元素值printf("arr[%ld] = %d\n", ptr - arr, *ptr);//每次循环结束后,将指针ptr向后移动一个元素的位置ptr++;}return 0;
}/*使用指针和 while 循环遍历数组输出结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.6指针和 for
循环遍历
结合指针和 for
循环可以更灵活地遍历数组。
#include <stdio.h>int main()
{//定义一个名为arr的整型数组,并初始化数组的元素为1、2、3、4、5int arr[] = {1, 2, 3, 4, 5};//sizeof(arr)返回整个数组占用的字节数//sizeof(arr[0])返回数组中一个元素占用的字节数,两者相除得到数组的元素个数int n = sizeof(arr) / sizeof(arr[0]); //计算数组arr的元素个数//定义一个指向整型的指针ptr,并将其初始化为数组arr的首地址int *ptr = arr;//定义一个指向整型的指针end,并将其初始化为数组arr的末尾地址(即arr + n)int *end = arr + n;//打印提示信息,表示接下来将使用指针和for循环遍历数组printf("使用指针和 for 循环遍历数组:\n");//这是一个for循环,i从0开始,每次循环增加1,直到指针ptr小于指针end//在每次循环中,i用于表示当前元素的索引,ptr用于指向当前元素for (int i = 0; ptr < end; i++, ptr++) {//在每次循环中,打印数组中当前指针ptr所指向的元素值//i表示当前元素的索引,*ptr表示指针ptr所指向的元素值printf("arr[%d] = %d\n", i, *ptr);}return 0;
}/*使用指针和 for 循环遍历数组输出结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.6.7 foreach
循环遍历
C11 引入,模拟 foreach
风格,不常用。
#include <stdio.h>//定义一个宏 foreach,用于简化数组遍历
#define foreach(type, var, arr, len) \//宏展开后的 for 循环,用于遍历数组 //type *var = arr 定义了一个指向数组首元素的指针 var//*end = arr + len 定义了一个指向数组末尾的指针 end//var != end 是循环条件,var++ 用于移动指针到下一个元素for (type *var = arr, *end = arr + len; var != end; var++) int main() {//定义并初始化一个整型数组 arrint arr[] = {1, 2, 3, 4, 5};//sizeof(arr[0]) 计算数组 arr 的长度 nint n = sizeof(arr) / sizeof(arr[0]);//打印提示信息printf("使用 foreach 循环遍历数组:\n");//int 是元素类型,ptr 是循环变量名,arr 是数组名,n 是数组长度foreach(int, ptr, arr, n) //使用 foreach 宏遍历数组 arr{//在每次循环中,打印当前元素的值及其在数组中的索引//ptr - arr 计算当前元素的索引,*ptr 获取当前元素的值printf("arr[%ld] = %d\n", ptr - arr, *ptr);}return 0;
}/*使用 foreach循环遍历数组输出结果:
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
arr[4] = 5
*/
1.7一维数组动态遍历
动态遍历数组通常涉及动态分配内存来创建数组,并使用循环来访问和操作这些元素。
动态分配内存可以通过 malloc
、calloc
或 realloc
函数来实现。
遍历方法可通过6中方法实现,以下代码我会用for循环来详细解析代码,其他用于汇总方法实现。
方法:
(1)使用 for
循环遍历
(2) 使用 while
循环遍历
(3)使用 do-while
循环遍历
(4)使用指针遍历
(5)使用指针和 while
循环遍历
(6)使用指针和 for
循环遍历
1.7.1一维数组动态遍历函数原型介绍
之前在一维数组动态初始化内有介绍过这三个函数,在这里再拆分解析下,用于强化记忆。
1.7.1.1 malloc函数原型介绍
//1.malloc函数原型 void* malloc(size_t size);//2.使用 malloc 函数动态分配内存//3.malloc 函数接受一个参数,表示需要分配的字节数//4.在这里,n * sizeof(int) 计算出需要分配的字节数,以存储 n 个整数//5.malloc 函数返回一个指向分配内存的指针,如果分配失败,则返回 NULL//6.(int *) 是类型转换,将malloc返回的void *指针转换为 int *指针,以便可以正确地访问分配的内存//7.如果内存分配失败,返回 1,表示程序异常结束int *arr = (int *)malloc(n * sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;
}
1.7.1.2 calloc函数原型介绍
//1. calloc函数原型 void* calloc(size_t num, size_t size);//2. 使用 calloc 函数动态分配内存//3. calloc 函数接受两个参数,第一个参数 n 表示要分配的元素数量,第二个参数 sizeof(int) 表示每个元素的大小(以字节为单位)//4. calloc 函数会在分配内存后将所有字节初始化为零。(int *) 是类型转换,将 calloc 返回的 void * 指针转换为 int * 指针,以便可以正确地访问分配的内存。//5.检查 calloc 是否成功分配了内存。如果 arr 为 NULL,表示内存分配失败。//6.如果内存分配失败,打印一条错误信息//7. 如果内存分配失败,返回 1,表示程序异常结束int *arr = (int *)calloc(n, sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}
1.7.1.3 realloc
函数原型介绍
//1. 函数原型 void *realloc(void *ptr, size_t size);//2. 使用 realloc 函数尝试重新分配内存//3.realloc 函数接受两个参数,第一个参数 ptr 是一个指向之前已经分配的内存块的指针,第二个参数 10 * sizeof(int) 表示新的内存块大小,即 10 个整数的大小//4. realloc 函数会尝试将 ptr 指向的内存块调整为新的大小//5.如果调整成功,realloc 会返回一个指向新内存块的指针;如果调整失败,realloc 会返回 NULL//6.(int *) 是类型转换,将 realloc 返回的 void * 指针转换为 int * 指针,以便可以正确地访问分配的内存//7. if检查 realloc 是否成功重新分配了内存。如果 new_ptr 为 NULL,表示重新分配失败//8. perror 作用是打印错误流的,如果重新分配失败,使用 perror 函数打印一条错误信息,该信息会包含系统错误信息//9. return 1;如果重新分配失败,返回 1,表示程序异常结束int *new_ptr = (int *)realloc(ptr, 10 * sizeof(int));if (new_ptr == NULL) {perror("realloc failed");free(ptr); // 释放原始内存块return 1;}
1.7.2使用malloc函数实现一维数组动态遍历
#include <stdio.h>
#include <stdlib.h>int main() { //定义一个整型变量n,用于存储用户输入的数组大小int n;//提示用户输入数组的大小printf("请输入数组的大小: ");//从标准输入读取用户输入的数组大小,并存储在变量n中scanf("%d", &n);//malloc函数返回一个指向分配内存的指针,如果分配失败,则返回NULL.//(int *)是类型转换,将malloc返回的void *指针转换为int *指针,//以便可以正确地访问分配的内存int *arr = (int *)malloc(n * sizeof(int)); //动态分配一个大小为n的整型数组if (arr == NULL) //检查内存分配是否成功{// 如果arr为NULL,表示内存分配失败printf("内存分配失败\n"); return 1; //打印错误信息并返回1,表示程序异常结束}//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) //使用for循环初始化数组元素{//在每次循环中,将数组元素arr[i]初始化为i + 1arr[i] = i + 1;}//提示用户,打印提示语句使用 for 循环遍历数组printf("使用 for 循环遍历数组:\n");//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) //使用for循环遍历数组并打印每个元素的值{// //在每次循环中,打印数组元素arr[i]的值printf("arr[%d] = %d\n", i, arr[i]);}// 释放内存free(arr); //free函数用于释放之前通过malloc分配的内存return 0;
}
使用malloc函数,实现6种遍历方法:
#include <stdio.h>
#include <stdlib.h>int main()
{int n;printf("请输入数组的大小: ");scanf("%d", &n);// 动态分配内存int *arr = (int *)malloc(n * sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 初始化数组元素for (int i = 0; i < n; i++) {arr[i] = i + 1;}// 使用 for 循环遍历数组printf("使用 for 循环遍历数组:\n");for (int i = 0; i < n; i++) {printf("arr[%d] = %d\n", i, arr[i]);}// 使用 while 循环遍历数组printf("\n使用 while 循环遍历数组:\n");int i = 0;while (i < n) {printf("arr[%d] = %d\n", i, arr[i]);i++;}// 使用 do-while 循环遍历数组printf("\n使用 do-while 循环遍历数组:\n");i = 0;do {printf("arr[%d] = %d\n", i, arr[i]);i++;} while (i < n);// 使用指针遍历数组printf("\n使用指针遍历数组:\n");int *ptr = arr;for (int i = 0; i < n; i++) {printf("arr[%d] = %d\n", i, *(ptr + i));}// 使用指针和 while 循环遍历数组printf("\n使用指针和 while 循环遍历数组:\n");ptr = arr;int *end = arr + n;while (ptr < end) {printf("arr[%ld] = %d\n", ptr - arr, *ptr);ptr++;}// 使用指针和 for 循环遍历数组printf("\n使用指针和 for 循环遍历数组:\n");ptr = arr;end = arr + n;for (int i = 0; ptr < end; i++, ptr++) {printf("arr[%d] = %d\n", i, *ptr);}// 释放内存free(arr);return 0;
}
1.7.3使用calloc函数实现一维数组动态遍历
#include <stdio.h>
#include <stdlib.h>int main()
{//定义一个整型变量n,用于存储用户输入的数组大小int n;//提示用户输入数组的大小printf("请输入数组的大小: ");//从标准输入读取用户输入的数组大小,并存储在变量n中scanf("%d", &n);// 动态分配内存并初始化为0//动态分配一个大小为n的整型数组,并将所有元素初始化为0//calloc函数返回一个指向分配内存的指针,如果分配失败,则返回NULL//(int *)是类型转换,将calloc返回的void *指针转换为int *指针,可以正确地访问分配的内存int *arr = (int *)calloc(n, sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 打印初始数组元素(初始值为0)printf("初始数组元素: ");//使用for循环遍历数组并打印每个元素的初始值(0)//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) {//在每次循环中,打印数组元素arr[i]的值printf("%d ", arr[i]);}printf("\n");//使用for循环初始化数组元素//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) {//在每次循环中,将数组元素arr[i]初始化为i + 1arr[i] = i + 1;}// 使用 for 循环遍历数组//使用for循环遍历数组并打印每个元素的值//循环变量i从0开始,每次循环增加1,直到i小于nprintf("使用 for 循环遍历数组:\n");for (int i = 0; i < n; i++) {//在每次循环中,打印数组元素arr[i]的值printf("arr[%d] = %d\n", i, arr[i]);}//释放动态分配的内存。free函数用于释放之前通过calloc分配的内存free(arr);return 0;
}
使用calloc函数,实现6种遍历方法:
#include <stdio.h>
#include <stdlib.h>int main()
{int n;printf("请输入数组的大小: ");scanf("%d", &n);// 动态分配内存并初始化为0int *arr = (int *)calloc(n, sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 打印初始数组元素(初始值为0)printf("初始数组元素: ");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");// 初始化数组元素for (int i = 0; i < n; i++) {arr[i] = i + 1;}// 使用 for 循环遍历数组printf("使用 for 循环遍历数组:\n");for (int i = 0; i < n; i++) {printf("arr[%d] = %d\n", i, arr[i]);}// 使用 while 循环遍历数组printf("\n使用 while 循环遍历数组:\n");int i = 0;while (i < n) {printf("arr[%d] = %d\n", i, arr[i]);i++;}// 使用 do-while 循环遍历数组printf("\n使用 do-while 循环遍历数组:\n");i = 0;do {printf("arr[%d] = %d\n", i, arr[i]);i++;} while (i < n);// 使用指针遍历数组printf("\n使用指针遍历数组:\n");int *ptr = arr;for (int i = 0; i < n; i++) {printf("arr[%d] = %d\n", i, *(ptr + i));}// 使用指针和 while 循环遍历数组printf("\n使用指针和 while 循环遍历数组:\n");ptr = arr;int *end = arr + n;while (ptr < end) {printf("arr[%ld] = %d\n", ptr - arr, *ptr);ptr++;}// 使用指针和 for 循环遍历数组printf("\n使用指针和 for 循环遍历数组:\n");ptr = arr;end = arr + n;for (int i = 0; ptr < end; i++, ptr++) {printf("arr[%d] = %d\n", i, *ptr);}// 释放内存free(arr);return 0;
}
1.7.4使用realloc函数实现一维数组动态遍历
#include <stdio.h>
#include <stdlib.h>int main()
{//定义一个整型变量n,用于存储用户输入的数组初始大小int n;//提示用户输入数组的初始大小printf("请输入数组的初始大小: ");//从标准输入读取用户输入的数组初始大小,并存储在变量n中scanf("%d", &n);//动态分配一个大小为n的整型数组,并将所有元素初始化为0//calloc函数返回一个指向分配内存的指针,如果分配失败,则返回NULL//(int *)是类型转换,将calloc返回的void *指针转换为int *指针,可以正确地访问分配的内存int *arr = (int *)calloc(n, sizeof(int))//检查内存分配是否成功,如果arr为NULL,表示内存分配失败if (arr == NULL) {//打印错误信息printf("内存分配失败\n");//返回1,表示程序异常结束return 1;}//使用for循环初始化数组元素//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) {//在每次循环中,将数组元素arr[i]初始化为i + 1arr[i] = i + 1;}// 提示用户接下来将打印初始数组元素printf("初始数组元素: ");//使用for循环遍历数组并打印每个元素的初始值//循环变量i从0开始,每次循环增加1,直到i小于nfor (int i = 0; i < n; i++) {//在每次循环中,打印数组元素arr[i]的值printf("%d ", arr[i]);}printf("\n");// 定义一个整型变量new_size,用于存储用户输入的新数组大小int new_size;//提示用户输入新的数组大小printf("请输入新的数组大小: ");//从标准输入读取用户输入的新数组大小,并存储在变量new_size中scanf("%d", &new_size);//realloc函数接受两个参数,第一个参数是指向之前分配的内存块的指针,第二个参数是新的内存块大小//realloc函数会尝试将之前分配的内存块扩展到新的大小,并返回一个指向新内存块的指针//如果扩展失败,realloc函数会返回NULL。(int *)是类型转换,将realloc返回的void *指针转换为int *指针,可以正确地访问分配的内存int *new_arr = (int *)realloc(arr, new_size * sizeof(int)); //realloc扩展数组的大小//检查内存重新分配是否成功//如果new_arr为NULL,表示内存重新分配失败if (new_arr == NULL) {printf("内存重新分配失败\n");//打印错误信息并释放原始内存块free(arr); // 释放原始内存块//返回1,表示程序异常结束return 1;}// 更新指针,使其指向新的内存块arr = new_arr;// 初始化新元素//使用for循环初始化新的数组元素//循环变量i从n开始,每次循环增加1,直到i小于new_sizefor (int i = n; i < new_size; i++) {//在每次循环中,将数组元素arr[i]初始化为i + 1arr[i] = i + 1;}// 使用 for 循环遍历扩展后的数组printf("扩展后的数组元素: ");//使用for循环遍历扩展后的数组并打印每个元素的值//循环变量i从0开始,每次循环增加1,直到i小于new_sizefor (int i = 0; i < new_size; i++) {//在每次循环中,打印数组元素arr[i]的值printf("%d ", arr[i]);}printf("\n");// 释放之前通过calloc或realloc分配的内存free(arr);return 0;
}
使用realloc函数,实现6种遍历方法:
#include <stdio.h>
#include <stdlib.h>int main()
{int n;printf("请输入初始数组的大小: ");scanf("%d", &n);// 动态分配内存int *arr = (int *)malloc(n * sizeof(int));if (arr == NULL) {printf("内存分配失败\n");return 1;}// 初始化数组元素for (int i = 0; i < n; i++) {arr[i] = i + 1;}// 打印初始数组元素printf("初始数组元素: ");for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");int new_n;printf("请输入新的数组大小: ");scanf("%d", &new_n);// 调整内存大小int *new_arr = (int *)realloc(arr, new_n * sizeof(int));if (new_arr == NULL) {perror("realloc failed");free(arr); // 释放原始内存块return 1;}// 初始化新分配的内存部分(如果需要)for (int i = n; i < new_n; i++) {new_arr[i] = i + 1;}// 打印调整后的数组元素printf("调整后的数组元素: ");for (int i = 0; i < new_n; i++) {printf("%d ", new_arr[i]);}printf("\n");// 释放内存free(new_arr);return 0;
}
1.8一维数组的排序算法
一维数组的排序算法有多种,每种算法都有其特点和适用场景。以下是对常见排序算法的介绍:
(1) 冒泡排序(Bubble Sort)
-
简介:重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
-
时间复杂度:平均和最坏情况下为O(n^2),最好情况下为O(n)。
-
空间复杂度:O(1)。
-
稳定性:稳定。
-
适用场景:数据规模较小且基本有序的情况。
(2) 选择排序(Selection Sort)
-
简介:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
-
时间复杂度:O(n^2)。
-
空间复杂度:O(1)。
-
稳定性:不稳定。
-
适用场景:数据规模较小且对稳定性要求不高的情况。
(3)插入排序(Insertion Sort)
-
简介:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
-
时间复杂度:平均和最坏情况下为O(n^2),最好情况下为O(n)。
-
空间复杂度:O(1)。
-
稳定性:稳定。
-
适用场景:数据规模较小且基本有序的情况。
(4)快速排序(Quick Sort)
-
简介:选择一个基准元素,将数组分为两部分,一部分的所有元素都比基准元素小,另一部分的所有元素都比基准元素大,然后对这两部分分别进行快速排序。
-
时间复杂度:平均情况下为O(nlogn),最坏情况下为O(n^2)。
-
空间复杂度:平均情况下为O(logn),最坏情况下为O(n)。
-
稳定性:不稳定。
-
适用场景:数据规模较大且对平均性能要求较高的情况。
(5)归并排序(Merge Sort)
-
简介:将一个数组分成两个子数组,对每个子数组进行排序,然后将排序好的子数组合并成一个有序数组。
-
时间复杂度:O(nlogn)。
-
空间复杂度:O(n)。
-
稳定性:稳定。
-
适用场景:数据规模较大且对稳定性要求较高的情况。
(6)堆排序(Heap Sort)
-
简介:首先将数组构建成一个最大堆,然后将堆顶元素与最后一个元素交换,再对剩余的元素进行堆调整,重复这个过程直到整个数组有序。
-
时间复杂度:O(nlogn)。
-
空间复杂度:O(1)。
-
稳定性:不稳定。
-
适用场景:数据规模较大且对空间复杂度要求较高的情况。
1.8.1一维数组冒泡排序
用于对整型数组 arr
进行升序排序。
1.8.1.1原理
冒泡排序的原理:依次比较相邻的元素,如果前面的比后面的大(小)就进行交换每一趟比较后,就会得到一个最大值(小),该值不进行下一轮的比较对剩余的部分,再次进行前面的操作,直到剩余一个元素为止,排序成功。
天蓝色:待排序 绿色:正在比较的数组元素 橙色:已经排好的数组元素
1.8.1.2冒泡排序的要素
(1) 需要双重循环来解决:
外层循环:控制遍历的次数。总共需要 n-1 次遍历。
i 从 0 到 n-2。
内层循环:比较相邻的元素并交换它们。
j 从 0 到 n-i-2。
(2)每趟的比较次数 +所在趟数 == 元素总个数.
(3)每次内层循环结束后,最大的元素会被“冒泡”到数组的末尾。
(4)比较和交换:
如果 arr[j] 大于 arr[j + 1],则交换这两个元素。
使用临时变量 temp 来交换元素的值。
1.8.1.3冒泡排序算法
//定义了一个名为 bubbleSort 的函数,接受一个整型数组 arr 和数组的长度 n 作为参数void bubbleSort(int arr[], int n)
{//外层循环控制所有的遍历次数//对于长度为 n 的数组,最多需要进行 n-1 次遍历for (int i = 0; i < n - 1; i++) {//内层循环进行相邻元素的比较和交换//随着外层循环的进行,每次遍历后,最大的元素会被“冒泡”到数组的末尾,因此内层循环的范围可以逐渐减小for (int j = 0; j < n - i - 1; j++) {//比较当前元素 arr[j] 和下一个元素 arr[j + 1]//如果当前元素大于下一个元素,则交换它们的位置if (arr[j] > arr[j + 1]) {// 交换 arr[j] 和 arr[j+1],交换操作通过一个临时变量 temp 来完成int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}
1.8.2一维数组选择排序
用于对整型数组 arr
进行升序排序。
1.8.2.1原理
选择排序是一种简单直观的排序算法。其基本思想是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
1.8.2.2选择排序的要素
(1) 需要双重循环来解决:
外层循环:
for (int i = 0; i < n - 1; i++)
:外层循环控制选择的起始位置,从数组的第一个元素开始,到倒数第二个元素结束。
内层循环:
for (int j = i + 1; j < n; j++)
:内层循环从当前位置的下一个元素开始,遍历到数组的最后一个元素,寻找最小值的索引。
(2)寻找最小元素:
if (arr[j] < arr[min_idx])
:如果找到比当前最小值还小的元素,则更新最小值的索引。
(3)交换元素:
如果在内层循环结束后,找到的最小元素索引 min_idx
不是当前外层循环的位置 i
,则交换这两个位置的元素。
1.8.2.3选择排序算法
//定义了一个名为 selectionSort 的函数,接受一个整型数组 arr 和数组的长度 n 作为参数void selectionSort(int arr[], int n)
{//外层循环控制选择的起始位置,从数组的第一个元素开始,到倒数第二个元素结束for (int i = 0; i < n - 1; i++) {// 假设当前位置的元素是最小的int min_idx = i;// 内层循环查找从i+1到n-1中最小元素的索引//内层循环从当前位置的下一个元素开始,遍历到数组的最后一个元素,寻找最小值的索引for (int j = i + 1; j < n; j++) {if (arr[j] < arr[min_idx]) {min_idx = j; // 更新最小元素的索引}}//如果找到比当前最小值还小的元素,则更新最小值的索引if (min_idx != i) {//如果在内层循环结束后,找到的最小元素索引 min_idx 不是当前外层循环的位置 i,//则交换这两个位置的元素。int temp = arr[i];arr[i] = arr[min_idx];arr[min_idx] = temp;}}
}
1.8.3一维数组插入排序
用于对整型数组 arr
进行升序排序。
1.8.3.1原理
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
1.8.3.2插入排序的要素
(1) 需要双重循环来解决:
外层循环:
for (int i = 1; i < n; i++)
:外层循环从数组的第二个元素开始,到最后一个元素结束。假设第一个元素已经被排序。
(2)关键元素:
int key = arr[i]
:将当前元素保存为 key
,这是需要插入到已排序部分的元素。
(3)内层循环:
while (j >= 0 && arr[j] > key)
:内层循环从当前元素的前一个位置开始,向前遍历已排序部分,找到 key
应该插入的位置。如果已排序部分的元素大于 key
,则将该元素向后移动一位。
4()插入元素:
arr[j + 1] = key
:将 key
插入到正确的位置。
1.8.3.3插入排序算法
//定义了一个名为 insertionSort 的函数,接受一个整型数组 arr 和数组的长度 n 作为参数
void insertionSort(int arr[], int n) {//从第一个元素开始,该元素可以认为已经被排序//外层循环从数组的第二个元素开始,到最后一个元素结束for (int i = 1; i < n; i++) {//将当前元素保存为 key,这是需要插入到已排序部分的元素int key = arr[i]; // 当前需要插入的元素int j = i - 1;//内层循环从当前元素的前一个位置开始,向前遍历已排序部分,找到 key 应该插入的位置//如果已排序部分的元素大于 key,则将该元素向后移动一位while (j >= 0 && arr[j] > key) //将大于key的元素向后移动一位{arr[j + 1] = arr[j];j = j - 1;}//将 key 插入到正确的位置。arr[j + 1] = key;}
}
1.8.4一维数组快速排序
用于对整型数组 arr
进行升序排序。
快速排序是一种高效的排序算法,采用分治法策略
1.8.4.1原理
快速排序是一种高效的排序算法,采用分治法(Divide and Conquer)策略来把一个序列分为两个子序列,然后递归地排序子序列。
1.8.4.2快速排序的要素
(1)`quickSort`函数逻辑:
如果 low
小于 high
,说明数组中至少有两个元素需要排序。
调用 partition
函数对数组进行分区,并获取基准元素的索引 pi
。
递归地对基准元素左边和右边的子数组进行快速排序。
(2)`partition` 函数逻辑:
选择数组的最后一个元素作为基准 pivot
。
初始化较小元素的索引 i
为 low - 1
。
遍历数组,从 low
到 high - 1
:
如果当前元素 arr[j]
小于基准 pivot
,则增加 i
并交换 arr[i]
和 arr[j]
,将较小元素移到左边。
遍历结束后,交换 arr[i + 1]
和 arr[high]
,将基准元素放到正确的位置。
返回基准元素的索引 i + 1
。
1.8.4.1快速排序算法
/*1. `quickSort` 函数参数:arr[]:要排序的数组。low:数组的起始索引。high:数组的结束索引。
*/void quickSort(int arr[], int low, int high)
{//如果 low 小于 high,说明数组中至少有两个元素需要排序if (low < high) {//pi 是分区后的基准元素的索引//调用 partition 函数对数组进行分区,并获取基准元素的索引 piint pi = partition(arr, low, high);//递归排序基准元素左边的子数组quickSort(arr, low, pi - 1);//递归排序基准元素右边的子数组quickSort(arr, pi + 1, high);}
}/*2. `partition` 函数参数:arr[]:要排序的数组。low:数组的起始索引。high:数组的结束索引。
*/int partition(int arr[], int low, int high) {int pivot = arr[high]; //选择数组的最后一个元素作为基准 pivotint i = (low - 1); //初始化较小元素的索引 i 为 low - 1for (int j = low; j <= high - 1; j++) //遍历数组,从 low 到 high - 1{//如果当前元素 arr[j] 小于基准 pivot,//则增加 i 并交换 arr[i] 和 arr[j],将较小元素移到左边if (arr[j] < pivot) {i++; // 增加较小元素的索引// 交换 arr[i] 和 arr[j]int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}}//遍历结束后,交换 arr[i + 1] 和 arr[high],将基准元素放到正确的位置int temp = arr[i + 1];arr[i + 1] = arr[high];arr[high] = temp;return (i + 1); // 返回基准元素的索引i+1
}
1.8.5一维数组归并排序
用于对整型数组 arr
进行升序排序。
归并排序是一种稳定的排序算法,采用分治法策略。
1.8.5.1原理
归并排序通过递归地将数组分成两半,分别对这两半进行排序,然后将排序好的两半合并在一起。这个过程一直重复,直到数组被完全排序。
1.8.5.2归并排序的要素
(1)`mergeSort` 函数
如果 l 小于 r,说明数组中至少有两个元素需要排序。
计算中间点 m,将数组分成两半。
递归地对左半部分和右半部分进行归并排序。
调用 merge 函数合并两个已排序的部分。
(2)`merge` 函数
计算左半部分和右半部分的长度,并创建临时数组 L 和 R。
将数据拷贝到临时数组 L 和 R 中。
合并临时数组回原数组 arr,确保合并后的数组是有序的。
处理剩余的元素(如果有)。
1.8.5.3归并排序算法
/*
`1. mergeSort` 函数参数:
arr[]:要排序的数组。
l:数组的起始索引。
r:数组的结束索引。*/void mergeSort(int arr[], int l, int r) {//如果 l 小于 r,说明数组中至少有两个元素需要排序if (l < r) {//计算中间点 m,将数组分成两半,,避免溢出int m = l + (r - l) / 2; mergeSort(arr, l, m); //对左半部分进行归并排序mergeSort(arr, m + 1, r); //对右半部分进行归并排序merge(arr, l, m, r); //调用 merge 函数合并两个已排序的部分}
}/*
2. `merge` 函数参数:
arr[]:要排序的数组。
l:数组的起始索引。
r:数组的结束索引。
*/void merge(int arr[], int l, int m, int r)
{int i, j, k;int n1 = m - l + 1; //左半部分的长度int n2 = r - m; //右半部分的长度int L[n1], R[n2]; //创建临时数组 L 和 R//拷贝数据到临时数组 L[] 和 R[]for (i = 0; i < n1; i++) {L[i] = arr[l + i];}for (j = 0; j < n2; j++) {R[j] = arr[m + 1 + j];}//合并临时数组回原数组arr,确保合并后的数组是有序的i = 0; // 初始化左半部分的索引j = 0; // 初始化右半部分的索引k = l; // 初始化合并子数组的索引while (i < n1 && j < n2) {if (L[i] <= R[j]) {arr[k] = L[i];i++;} else {arr[k] = R[j];j++;}k++;}// 拷贝 L[] 的剩余元素while (i < n1) {arr[k] = L[i];i++;k++;}// 拷贝 R[] 的剩余元素while (j < n2) {arr[k] = R[j];j++;k++;}
}
1.8.6一维数组堆排序
用于对整型数组 arr
进行升序排序。
堆排序是一种基于比较的排序算法,利用堆这种数据结构来实现。
1.8.6.1原理
堆排序是一种基于二叉堆数据结构的排序算法。它首先将数组构建成一个最大堆,然后将堆顶元素与最后一个元素交换,再对剩余的元素进行堆调整,重复这个过程直到整个数组有序。
1.8.6.2堆的要素
(1)`heapify` 函数
初始化最大元素的索引为根节点 i。
计算左子节点和右子节点的索引。
如果左子节点存在且大于根节点,则更新最大元素的索引。
如果右子节点存在且大于当前最大值,则更新最大元素的索引。
如果最大值不是根节点,则交换根节点和最大值节点,并递归地对受影响的子树进行堆化。
(2)`heapSort` 函数
构建最大堆:从最后一个非叶子节点开始,向前遍历并对每个节点进行堆化,最终构建一个最大堆。
排序过程:
将堆顶元素(最大值)与数组末尾元素交换。
减少堆的大小(忽略已排序的部分)。
对新的堆顶元素进行堆化,确保剩余部分仍然是一个最大堆。
重复上述过程,直到堆的大小为1。
1.8.6.3堆排序算法
/*
1. `heapify` 函数
参数:
arr[]:要排序的数组。
n:数组的长度。
i:当前子树的根节点索引。
*/void heapify(int arr[], int n, int i)
{int largest = i; //初始化最大元素的索引为根节点int l = 2 * i + 1; //左子节点的索引int r = 2 * i + 2; //右子节点的索引//如果左子节点存在且大于根节点if (l < n && arr[l] > arr[largest]) {largest = l;}//如果右子节点存在且大于当前最大值if (r < n && arr[r] > arr[largest]) {largest = r;}//如果最大值不是根节点if (largest != i) {//交换 arr[i] 和 arr[largest]int temp = arr[i];arr[i] = arr[largest];arr[largest] = temp;// 递归地对受影响的子树进行堆化heapify(arr, n, largest);}
}/*
`heapSort` 函数
构建最大堆:从最后一个非叶子节点开始,向前遍历并对每个节点进行堆化,最终构建一个最大堆。
排序过程:
将堆顶元素(最大值)与数组末尾元素交换。
减少堆的大小(忽略已排序的部分)。
对新的堆顶元素进行堆化,确保剩余部分仍然是一个最大堆。
重复上述过程,直到堆的大小为1。
*/void heapSort(int arr[], int n)
{//构建最大堆for (int i = n / 2 - 1; i >= 0; i--) {heapify(arr, n, i);}// 一个个从堆中取出元素for (int i = n - 1; i >= 0; i--) {//将当前最大的元素(根节点)移到数组末尾int temp = arr[0];arr[0] = arr[i];arr[i] = temp;//重新对堆进行堆化,忽略已排序的部分heapify(arr, i, 0);}
}
1.9一维数组与指针
1.9.1一维数组与指针的定义
一维数组与指针有着密切的关系。
数组名在大多数情况下会被隐式地转换为指向数组第一个元素的指针。
指针算术的单位是数组元素的大小。
数组下标访问实际上是通过指针算术实现的。
当数组作为函数参数传递时,实际上传递的是指向数组第一个元素的指针。
可以使用 malloc 或 calloc 函数动态分配数组,并通过指针访问和操作数组元素。
arr + 1 //指向数组的第二个元素。
当数组作为函数参数传递时,实际上传递的是指向数组第一个元素的指针。
1.9.2一维数组与指针的区别
(1)数组名:
数组名是一个常量指针,指向数组的第一个元素。
不能修改数组名的值(即不能指向其他内存地址)。
(2)指针:
指针是一个变量,可以指向任何内存地址。
可以修改指针的值(即指向其他内存地址)。
(3)示例:
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr = arr;// 数组名作为指针//打印数组名 arr 和指针 ptr 的地址,两者相同printf("数组名作为指针: %p\n", (void *)arr);printf("指针变量: %p\n", (void *)ptr);// 修改指针变量ptr = &arr[2];printf("修改后的指针变量: %p\n", (void *)ptr);// 不能修改数组名//将数组名 arr 修改为指向数组的第三个元素 arr[2],这是不允许的,因为数组名是一个常量指针,不能被修改// arr = &arr[2]; // 错误:数组名是常量指针,不能修改return 0;
}
1.9.3一维数组与指针的关系
(1)数组名作为指针:
数组名在大多数情况下会被解释为指向数组第一个元素的指针。
int arr[5] = {1, 2, 3, 4, 5};//这里,arr 被隐式地转换为指向 arr[0] 的指针,因此 ptr 指向数组的第一个元素int *ptr = arr; //等价于 int *ptr = &arr[0];
(2)指针运算:
可以对指针进行加减运算,以访问数组中的其他元素。
int value = *(ptr + 2); // 访问第三个元素
int value2 = *(arr + 2); // 访问第三个元素
(3)数组下标与指针的关系:
数组下标操作符 []
实际上是基于指针运算实现的。
int arr[5] = {1, 2, 3, 4, 5};int *ptr = arr;int x = ptr[2]; //等价于 int x = *(ptr + 2),这里可以理解指针的偏移
(4)数组作为函数参数
/*
`modifyArray` 函数
参数:
int *arr:指向整型数组的指针int size:数组的大小,即数组中元素的个数*/#include <stdio.h>void modifyArray(int *arr, int size)
{//使用一个 for 循环遍历数组中的所有元素for (int i = 0; i < size; i++)
{//在每次循环中,将当前元素的值乘以2,并更新数组中的该元素arr[i] *= 2;}
}int main()
{//定义一个包含5个整数的数组 arr,并初始化为 {1, 2, 3, 4, 5}int arr[5] = {1, 2, 3, 4, 5};//调用 modifyArray 函数,传递数组 arr 和数组的大小 5modifyArray(arr, 5); //modifyArray 函数会将数组中的每个元素都乘以2//使用一个 for 循环遍历修改后的数组for (int i = 0; i < 5; i++) {//使用 printf 函数打印每个元素printf("%d ", arr[i]);}printf("\n"); // 输出: 2 4 6 8 10return 0;
}
1.9.4一维数组与指针的注意事项
(1)数组名是常量指针:
-
数组名是一个常量指针,不能被重新赋值。
-
例如,
arr = NULL;
是非法的。
(2)越界访问:
-
访问数组时需要注意不要越界,否则会导致未定义行为。
-
例如,
arr[5]
访问第六个元素(假设数组长度为5),这是越界访问。
(3)指针的初始化:
-
指针在使用前必须初始化,否则可能会导致未定义行为。
-
例如,
int *ptr; *ptr = 10;
是非法的,因为ptr
未初始化。
(4)指针类型匹配:
-
指针类型必须与其指向的数据类型匹配,否则会导致类型不匹配错误。
-
例如,
int *ptr = "hello";
是不合法的,因为字符串常量的类型是char *
。
(5)内存管理:
-
动态分配的内存需要手动释放,否则会导致内存泄漏。
-
例如,使用
malloc
分配的内存需要使用free
释放。
(6)多维数组与指针:
-
多维数组在内存中是按行优先顺序存储的。
-
访问多维数组元素时需要注意指针的运算。
2.二维数组
目标:通过思维导图了解学习二维数组的核心知识点:
2.1二维数组的基础概念
2.1.1定义
二维数组是一个由行和列组成的矩阵结构,每个元素可以通过两个下标来访问。
//定义了一个3行4列的整数二维数组//其中a[i][j](i表示行,0 <= i < 3;j表示列,0 <= j < 4)可以访问特定的元素int a[3][4];
2.1.2内存布局
在C语言中,二维数组在内存中是按行优先存储的。例如对于int a[2][3]
,其内存中元素的存储顺序为a[0][0]
、a[0][1]
、a[0][2]
、a[1][0]
、a[1][1]
、a[1][2]
。以下是示例代码来验证:
#include <stdio.h>
int main()
{//这行代码定义了一个2行3列的二维整型数组a,并初始化了其所有元素int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //这行代码声明了一个整型指针pint *p; //二维数组a的首地址(即第一个元素a[0][0]的地址)赋值给指针p//这里使用了类型转换(int*),因为二维数组名a在大多数表达式中会被解释为指向其第一个元素(即一维数组a[0])的指针,而a[0]本身也是一个指针(指向a[0][0]),但类型是int (*)[3]。为了将其转换为int*类型,以便与整型指针p匹配,进行了显式类型转换p=(int*)a; //打印指针p当前指向的值(即a[0][0]的值1)printf("%d ", *p); //然后将指针p递增,使其指向下一个整型元素(即a[0][1])p++; //然后打印该值(即2)printf("%d ", *p); return 0;
}
这里将二维数组a
强制转换为int*
类型的指针p
,然后通过指针的移动可以看到是按照行优先的顺序访问元素的。
2.2二维数组的定义与初始化
2.2.1定义格式
格式为类型符 数组名[常量表达式1][常量表达式2];,如int b[2][5];
2.2.2初始化方法
(1)完全初始化:
可以按行初始化,例如int c[2][3]={ {1, 2, 3}, {4, 5, 6}};;也可以顺序初始化,如int c[2][3]={1, 2, 3, 4, 5, 6};,这两种方式是等价的。
(2)部分初始化:
如果按行初始化,如int d[2][3] = { {1, 2}, {4} };,未初始化的元素会被默认初始化为0。如果顺序初始化,如int d[2][3]={1, 2, 4};,也是前面的元素被初始化,后面的为0。
2.3二维数组的引用与访问
2.3.1元素访问
通过下标访问和修改二维数组中的元素,如a[i][j]
表示访问第i
行第j
列的元素。
#include <stdio.h> int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //修改数组中第二行第三列的元素值为10a[1][2] = 10; //打印数组中第二行第三列的元素值printf("%d", a[1][2]); //返回0,表示程序正常结束return 0;
}
2.3.2边界条件处理
在访问二维数组元素时,要确保下标不越界。例如,对于int a[3][4]
,行下标范围是0 - 2
,列下标范围是0 - 3,
可以通过条件判断来避免越界。
#include <stdio.h> int main()
{//定义一个3行4列的二维数组int a[3][4]; //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 3; i++) {for (j = 0; j < 4; j++) {//检查索引是否在有效范围内if (i >= 0 && i < 3 && j >= 0 && j < 4) {//根据索引计算并赋值给数组元素a[i][j] = i * j; }}}// 返回0,表示程序正常结束return 0;
}
2.4二维数组的遍历
2.4.1嵌套循环
使用嵌套的for
循环来遍历二维数组的每一个元素。
#include <stdio.h> int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 2; i++) {for (j = 0; j < 3; j++) {// 打印数组中的每个元素printf("%d ", a[i][j]); }// 每行打印完毕后换行printf("\n"); }// 返回0,表示程序正常结束return 0;
}
2.4.2遍历应用
在矩阵运算中,如两个矩阵相加,需要遍历两个二维数组对应的元素进行相加操作。在图像处理中,二维数组可以表示图像的像素矩阵,遍历数组可以对像素进行处理,如灰度化、滤波等操作。
2.5二维数组的操作与应用
2.5.1行列交换
交换二维数组的行与列。
#include <stdio.h> int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 2; i++) {for (j = 0; j < 3; j++) {// 打印数组中的每个元素printf("%d ", a[i][j]); }// 每行打印完毕后换行printf("\n"); }// 返回0,表示程序正常结束return 0;
}
2.5.2查找操作
查找最大元素及其位置。
#include <stdio.h> int main()
{//定义并初始化一个3行4列的二维数组int a[3][4] = { {1, 5, 3, 4}, {6, 2, 9, 7}, {8, 1, 3, 5} }; //初始化最大元素为数组的第一个元素int max = a[0][0]; //初始化最大元素的行索引和列索引int max_i = 0, max_j = 0; //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 3; i++) {for (j = 0; j < 4; j++) {// 如果当前元素大于最大元素,则更新最大元素及其位置if (a[i][j] > max) {max = a[i][j]; max_i = i; max_j = j; }}}//打印最大元素的值和位置printf("最大元素为%d,位置在第%d行第%d列\n", max, max_i, max_j); //返回0,表示程序正常结束return 0;
}
2.5.3实际实际应用
在矩阵运算方面,如矩阵乘法,需要对二维数组进行复杂的操作。设A
是m×n
矩阵,B
是n×p
矩阵,结果C = A×B
是m×p
矩阵,其计算过程需要遍历A
和B
的行和列进行乘法和加法运算。
在数据存储方面,二维数组可以用来存储表格数据,如学生成绩表,每行表示一个学生,每列表示不同的科目成绩。
2.6二维数组与指针关系
2.6.1指针表示
二维数组可以用指针来表示。对于int a[3][4]
,a
可以看作是一个指向包含4个int
类型元素的数组的指针。可以通过指针来访问数组元素。
#include <stdio.h> int main()
{//定义并初始化一个3行4列的二维数组int a[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; //定义一个指向包含4个整数的数组的指针int (*p)[4]; //将指针p指向数组a的第一行p = a; //打印指针p所指向的行的第二个元素printf("%d", (*p)[1]); //返回0,表示程序正常结束return 0;
}
2.6.2指针遍历
使用指针遍历二维数组。
#include <stdio.h> int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //定义一个指向整数的指针int *p; //定义循环变量i和jint i, j; //将指针p指向数组a的第一个元素p = (int*)a; //使用嵌套循环遍历数组for (i = 0; i < 2; i++) {for (j = 0; j < 3; j++) {// 打印指针p所指向的元素printf("%d ", *p); // 指针p指向下一个元素p++; }} // 返回0,表示程序正常结束return 0;
}
2.7二维数组的高级应用与技巧
2.7.1多维数组
三维数组的定义,例如int a[2][3][4];
,它可以看作是由2个3×4
的二维数组组成。初始化可以按层(类似二维数组按行)进行。
// 定义并初始化一个2层3行4列的三维数组int a[2][3][4] = { { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }, { {13, 14, 15, 16}, {17, 18, 19, 20}, {21, 22, 23, 24} } };
2.7.2算法实现
矩阵转置算法:对于int a[3][4]
,转置后的矩阵b
为int b[4][3]。
#include <stdio.h> /*** @brief 将一个3x4的矩阵转置为4x3的矩阵* * @param a 原始矩阵,3x4* @param b 转置后的矩阵,4x3*/
void transpose(int a[3][4], int b[4][3])
{//使用嵌套循环遍历原始矩阵a的每个元素for (int i = 0; i < 3; i++){for (int j = 0; j < 4; j++) {// 将a[i][j]的值赋给b[j][i],实现矩阵的转置b[j][i] = a[i][j];}}
}int main()
{//定义并初始化一个3x4的矩阵aint a[3][4] = {{1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};//定义一个4x3的矩阵b,用于存储转置后的结果int b[4][3];//调用transpose函数,将a转置为btranspose(a, b);//打印转置后的矩阵for (int i = 0; i < 4; i++) {for (int j = 0; j < 3; j++) {// 打印b[i][j]的值printf("%d ", b[i][j]);}// 每行打印完毕后换行printf("\n");}// 返回0,表示程序正常结束return 0;
}
2.8二维数组在函数间传递
2.8.1传地址
当在函数间传递二维数组时,传址是常用的方式。
这里printArray
函数接收一个指向包含3个int
类型元素的数组的指针,通过这个指针可以在函数内部访问和操作二维数组。
#include <stdio.h> /** @brief 打印一个二维数组* * @param a 指向二维数组的指针,数组的列数为3* @param rows 二维数组的行数*/
void printArray(int (*a)[3], int rows)
{//定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < rows; i++) {for (j = 0; j < 3; j++){// 打印数组中的每个元素printf("%d ", a[i][j]); }// 每行打印完毕后换行printf("\n"); }
}int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //调用printArray函数,打印数组aprintArray(a, 2); //返回0,表示程序正常结束return 0;
}
2.8.2传值
传值方式传递二维数组效率较低,因为需要将整个数组的值进行复制。
在函数modifyArray
中,虽然形式上是传值,但实际上由于数组名作为函数参数时会退化为指针,所以这里本质上还是通过指针来操作数组。如果要在函数中修改原数组,传址是更好的选择。
#include <stdio.h> /** @brief 将一个二维数组中的每个元素乘以2* * @param b 指向二维数组的指针,数组的行数为2,列数为3*/
void modifyArray(int b[2][3])
{//定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 2; i++) {for (j = 0; j < 3; j++) {// 将数组中的每个元素乘以2b[i][j] = b[i][j] * 2; }}
}int main()
{//定义并初始化一个2行3列的二维数组int a[2][3] = { {1, 2, 3}, {4, 5, 6} }; //调用modifyArray函数,将数组a中的每个元素乘以2modifyArray(a); //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 2; i++) {for (j = 0; j < 3; j++) {// 打印数组中的每个元素printf("%d ", a[i][j]); }// 每行打印完毕后换行printf("\n"); }// 返回0,表示程序正常结束return 0;
}
2.9二维数组操作的注意事项
2.9.1数组越界检测和处理
数组越界是常见错误,如对于int a[3][4]
,如果访问a[3][0]
就会越界。可以通过在访问数组元素时添加条件判断来检测越界情况。
#include <stdio.h>
int main()
{//定义一个3行4列的二维数组int a[3][4]; //定义循环变量i和jint i, j; //使用嵌套循环遍历数组for (i = 0; i < 3; i++) {for (j = 0; j < 4; j++) {// 检查数组索引是否越界if (i >= 0 && i < 3 && j >= 0 && j < 4) {// 给数组元素赋值a[i][j] = i * j; } else {// 打印数组越界访问的警告信息printf("数组越界访问可能发生在i = %d, j = %d\n", i, j); }}}// 返回0,表示程序正常结束return 0;
}
2.9.2调试方法
使用调试工具,如gdb
(在Linux环境下),可以设置断点,查看变量的值等。例如,在编译程序时加上-g
选项,然后在gdb
中运行程序,可以通过break
命令设置断点,print
命令查看变量的值。
#include <stdio.h>int main()
{int a = 10;int b = 20;int sum = a + b;printf("The sum of %d and %d is %d\n", a, b, sum);return 0;
}1.编译程序:(1)使用-g选项编译程序,以便在调试时包含调试信息。(2)打开终端,进入代码所在的目录,然后输入以下命令:bashgcc -g -o example example.c这将生成一个名为example的可执行文件。2.启动gdb:在终端中输入以下命令启动gdb:bashgdb example3.设置断点:在gdb中,使用break命令设置断点。例如,在main函数的开始处设置断点:bashbreak main4.运行程序:使用run命令运行程序:bashrun5.查看变量的值:程序会在断点处停止,此时可以使用print命令查看变量的值。例如,查看a、b和sum的值:bashprint aprint bprint sum6.继续执行程序:使用continue命令继续执行程序,直到下一个断点或程序结束:bashcontinue7.退出gdb:使用quit命令退出gdb:bashquit
3.字符数组
目标:通过思维导图了解学习字符数组的核心知识点:
3.1字符数组的基本概念
字符数组的定义:字符数组是C语言中用于存储一系列字符的数据结构,其元素类型为char。
//就定义了一个长度为10的字符数组c
char c[10];
字符数组与字符串的关系:在C语言中,字符串被视为以'\0'结尾的字符数组。
//这里系统会自动在字符'h'、'e'、'l'、'l'、'o'后面添加'\0'
char str[] = "hello";
3.2字符数组的初始化与赋值
3.2.1初始化方法
逐个字符赋值:通过指定每个数组元素的值来初始化字符数组。
char c[5] = {'h', 'e', 'l', 'l', 'o'};
整体赋值:在定义字符数组时,使用花括号将一组字符(包括结束符'\0')整体赋值给数组。
char c[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
字符串常量初始化:可以直接使用字符串常量来初始化字符数组,系统会自动添加结束符'\0'。
char str[] = "world";
3.2.2赋值注意事项
字符数组的大小在定义后是固定的,不能像整型数组那样动态扩展。
赋值时,如果提供的字符个数大于数组长度,会导致语法错误。
//是错误的
char c[3] = {'a', 'b', 'c', 'd'};
如果提供的字符个数小于数组长度,未赋值的元素将被自动定为空字符(即'\0'),但也可能无法确定。
字符数组的赋值只能对其元素进行一一赋值,不能用字符串常量直接赋值(除非在初始化时)。
//是错误的
char c[5]; c = "hello";//是正确的
char c[5]; c[0]='h'; c[1]='e';
3.3字符串与字符串结束标记
字符串的定义:在C语言中,字符串是以'\0'作为结束标记的字符数组。
字符串的实际长度与数组长度的关系:实际有效字符串的长度由'\0'的位置决定,而不是由数组的长度决定。
//实际长度为3,虽然数组可能分配了更多的空间
char str[] = "abc";
字符串结束标记的重要性:在程序中往往依靠检测'\0'的位置来判定字符串是否结束。
3.4字符数组的输入与输出
3.4.1输入方法
使用scanf函数从键盘输入字符串,scanf函数在读取字符串时,遇到空格、制表符或换行符会停止读取,但可以通过%[^\n]格式说明符来读取包含空格的字符串。
char str[100]; scanf("%[^\n]", str);
使用gets函数从键盘输入字符串,可以包含空格,但只能输入一个字符串。
char str[100]; gets(str);。
不过gets函数存在缓冲区溢出的风险,不建议在实际编程中使用。
3.4.2输出方法
使用printf函数输出字符串,使用%s格式说明符。
char str[] = "hello"; printf("%s", str);。
使用puts函数输出字符串,并自动换行。
char str[] = "world"; puts(str);。
3.5字符串处理函数
3.5.1 strcat函数
连接两个字符串,将第二个字符串连接到第一个字符串的后面。
#include <stdio.h> // 包含标准输入输出库
#include <string.h> // 包含字符串处理库int main() {// 定义一个字符数组str1,并初始化为"hello"char str1[100] = "hello"; // 定义一个字符数组str2,并初始化为" world"char str2[] = " world"; // 使用strcat函数将str2连接到str1的末尾strcat(str1, str2); // 打印连接后的字符串printf("%s", str1); // 返回0,表示程序正常结束return 0;
}
3.5.2 strcpy函数
复制字符串,将一个字符串复制到另一个字符串中。
#include <stdio.h> // 包含标准输入输出库
#include <string.h> // 包含字符串处理库int main() {// 定义一个字符数组str1,并初始化为"original"char str1[100] = "original"; // 定义一个字符数组str2,并初始化为"new"char str2[] = "new"; // 使用strcpy函数将str2复制到str1中strcpy(str1, str2); // 打印复制后的字符串printf("%s", str1); // 返回0,表示程序正常结束return 0;
}
3.5.3strlen函数
strlen函数:计算字符串长度。
#include <stdio.h> // 包含标准输入输出库
#include <string.h> // 包含字符串处理库int main() {// 定义一个字符数组str,并初始化为"hello"char str[] = "hello"; // 使用strlen函数计算字符串str的长度int len = strlen(str); // 打印字符串的长度printf("The length is %d", len); // 返回0,表示程序正常结束return 0;
}
3.5.4strcmp函数
strcmp函数:比较两个字符串。
#include <stdio.h> // 包含标准输入输出库
#include <string.h> // 包含字符串处理库int main() {// 定义一个字符数组str1,并初始化为"abc"char str1[] = "abc"; // 定义一个字符数组str2,并初始化为"abd"char str2[] = "abd"; // 使用strcmp函数比较str1和str2的大小int result = strcmp(str1, str2); // 根据比较结果打印相应的信息if (result < 0) {printf("str1 is less than str2"); } else if (result > 0) {printf("str1 is greater than str2"); } else {printf("str1 is equal to str2"); }// 返回0,表示程序正常结束return 0;
}
3.6字符数组与字符指针比较
3.6.1字符数组
由若干个元素组成,每个元素放一个字符,有确定的内存地址。
//c的地址是确定的,且数组在内存中占用连续的空间来存储字符
char c[5];
3.6.2字符指针
存放的是地址(字符串/字符数组的首地址),通过指针可以访问字符串中的字符。
//p中存储的是字符串"hello"的首地址
char *p = "hello";
在内存中,字符指针本身占用一定的空间来存储地址,而它指向的字符串常量存储在常量区,字符数组存储在全局数据区或栈区。对于字符数组,不能直接用一个字符串对其赋值,而字符指针可以指向一个字符串常量。在函数传递中,字符数组传递的是数组的首地址,而字符指针直接传递地址。
3.7字符数组的应用实例
存储并处理用户输入的字符串。例如编写一个程序,接收用户输入的名字,然后进行一些处理,如统计名字的长度等。
实现字符串的拼接、复制、比较等操作。如在文本处理中,将多个小的字符串拼接成一个大的字符串。
用于文本处理、数据解析等场景。比如从一个包含很多信息的文本文件中,通过字符数组来解析出特定的字符串内容。
3.8字符数组的多维应用
3.81定义
二维字符数组可以用来存储多个字符串。
//这里定义了一个二维字符数组,可以存储3个长度不超过9(加上'\0')的字符串char str[3][10];
3.8.2初始化
可以像下面这样初始化二维字符数组。
char str[3][10] = {{"hello"}, {"world"}, {"!"}};
3.8.3使用示例
#include <stdio.h> // 包含标准输入输出库int main() {// 定义一个3行10列的字符数组,并初始化为三个名字char names[3][10] = {{"dididi"}, {"dadada"}, {"dududu"}};int i;// 打印存储的名字for (i = 0; i < 3; i++) {printf("%s\n", names[i]); // 打印每个名字,并换行}// 返回0,表示程序正常结束return 0;
}
3.9字符数组的动态分配和释放
3.9.1动态分配
可以使用malloc、calloc、realloc函数来动态分配字符数组的内存。
例如,使用malloc分配一个长度为n的字符数组。
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库函数,如mallocint main() {// 定义一个整型变量n,并初始化为10int n = 10; // 使用malloc函数动态分配一块内存空间,大小为n个字符char *p = (char *)malloc(n * sizeof(char)); // 返回0,表示程序正常结束return 0;
}
3.9.2释放
使用free函数来释放动态分配的内存。
在动态分配内存时,要注意内存泄漏问题,如果不再使用动态分配的内存,一定要及时释放。
#include <stdio.h> // 包含标准输入输出库
#include <stdlib.h> // 包含标准库函数,如mallocint main() {// 定义一个整型变量n,并初始化为10int n = 10; // 使用malloc函数动态分配一块内存空间,大小为n个字符char *p = (char *)malloc(n * sizeof(char)); // 检查内存是否分配成功if (p == NULL) {printf("内存分配失败\n");return 1;}// 使用分配的内存空间// 释放内存free(p);// 返回0,表示程序正常结束return 0;
}
3.10字符数组常见错误和注意事项
缓冲区溢出:如使用gets函数时,如果输入的字符串长度超过了字符数组的大小,就会发生缓冲区溢出。解决方法是使用更安全的输入函数,如fgets函数。
字符串越界:在对字符数组进行操作时,要确保操作不会超出数组的边界。例如在复制字符串时,如果目标数组的空间不够,就可能导致字符串越界。
未初始化的字符数组:如果没有对字符数组进行初始化就使用,可能会导致不可预测的结果。在使用前尽量对字符数组进行初始化。