2024-2025-1 学号20241315《计算机基础与程序设计》第十二周学习总结
作业信息
这个作业属于哪个课程 | 2024-2025-1-计算机基础与程序设计) |
---|---|
这个作业要求在哪里 | <作业要求的链接>https://www.cnblogs.com/rocedu/p/9577842.html#WEEK12 |
这个作业的目标 | <写上具体方面>《C语言程序设计》第11章并完成云班课测试 |
作业正文 | https://www.cnblogs.com/tanzitian11/p/18606651 |
教材学习内容总结
《C语言程序设计》第11章
一、指针基础
指针的定义
指针是一个变量,其值为另一个变量的地址。在 C 语言中,通过&运算符可以获取一个变量的地址。例如,如果有一个变量int a = 10;,那么&a就是变量a的地址。定义一个指针变量来存储这个地址,如int *p; p=&a;,这里p就是一个指向int类型变量的指针,它存储了变量a的地址。
指针的类型
指针的类型决定了它所指向的对象的类型。例如,int *类型的指针用于指向int类型的变量,char *类型的指针用于指向char类型的变量。指针类型的重要性在于指针的算术运算和指针解引用时的操作。
指针算术运算:当对指针进行加 1 或减 1 操作时,它实际移动的字节数是根据指针类型来确定的。例如,对于int 指针,加 1 操作会使指针的值增加sizeof(int)个字节;对于char 指针,加 1 操作会使指针的值增加sizeof(char)(通常为 1)个字节。
指针解引用:通过运算符可以对指针进行解引用,获取指针所指向的变量的值。例如,对于上面定义的p指针,p就表示变量a的值,即 10。
二、数组与指针的关系
数组名作为指针
在 C 语言中,数组名在大多数情况下可以看作是一个指向数组第一个元素的常量指针。例如,对于数组int arr[5];,arr等价于&arr[0],它是一个指向int类型的指针。可以使用指针的方式来访问数组元素,如*(arr + 1)等价于arr[1]。
通过指针遍历数组
可以定义一个指针变量,让它指向数组的第一个元素,然后通过指针的算术运算来遍历数组。例如:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for(int i = 0; i < 5; i++){
printf("%d ", *(p + i));
}
- 这里
p
指向arr
的第一个元素,在循环中通过*(p + i)
来访问数组的每个元素,其效果与使用arr[i]
相同。
- 指针数组与数组指针
- 指针数组:是一个数组,其元素为指针。例如
int *pArray[5];
,这是一个包含5个元素的数组,每个元素都是一个指向int
类型的指针。可以用来存储多个字符串(在C语言中,字符串实际上是字符数组,字符串常量可以看作是字符指针)或者指向不同int
数组的指针等。 - 数组指针:是一个指针,它指向一个数组。例如
int (*pArr)[5];
,这里pArr
是一个指针,它指向一个包含5个int
元素的数组。在使用时,要注意其与指针数组的区别,特别是在函数参数传递等场景中。
- 指针数组:是一个数组,其元素为指针。例如
三、指针和数组在函数中的应用
- 数组作为函数参数
- 当数组作为函数参数传递时,实际上传递的是数组的首地址(即数组名),函数接收到的是一个指针。例如:
for(int i = 0; i < size; i++){
printf("%d ", arr[i]);
}
}
int main(){
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5);
return 0;
}
在printArray函数中,参数int *arr可以接收一个指向int类型的指针,实际上就是数组arr的首地址。函数内部可以通过这个指针来访问数组的元素。
返回指针的函数
函数可以返回一个指针。例如,可以编写一个函数来返回一个动态分配的数组或者指向数组中的某个元素的指针等。但是要注意,返回的指针所指向的内存空间必须是有效的,不能返回局部变量的地址(除非这个局部变量是静态变量)。
int *createArray(int size){
int *arr = (int *)malloc(size * sizeof(int));
for(int i = 0; i < size; i++){
arr[i] = i;
}
return arr;
}
int main(){
int *p = createArray(5);
for(int i = 0; i < 5; i++){
printf("%d ", p[i]);
}
free(p);
return 0;
}
这里createArray
函数返回一个指向动态分配的int
数组的指针,在main
函数中使用这个指针来访问数组元素,最后要记得使用free
函数释放动态分配的内存。
一、动态数组的概念
什么是动态数组
在 C 语言中,普通数组的大小是在编译时确定的,一旦定义就不能改变大小。而动态数组允许在程序运行时根据实际需要来分配内存空间,从而可以灵活地控制数组的大小。这对于处理不确定大小的数据集合非常有用,例如读取文件中的数据,事先不知道数据的数量,就可以使用动态数组来存储。
动态内存分配函数
malloc函数:malloc函数用于在堆内存中分配指定字节数的连续内存空间。它的函数原型为void *malloc(size_t size);,其中size是要分配的字节数。例如,要分配一个可以存储n个int类型元素的动态数组,可以使用int *arr = (int *)malloc(n * sizeof(int));。这里(int *)是强制类型转换,因为malloc函数返回的是void *类型的指针,需要将其转换为实际要使用的指针类型。
calloc函数:calloc函数的功能与malloc类似,但是它会在分配内存后将内存空间初始化为 0。函数原型为void *calloc(size_t num, size_t size);,其中num是元素个数,size是每个元素的大小。例如,int *arr = (int *)calloc(n, sizeof(int));会分配n个int类型大小的内存空间,并将每个元素初始化为 0。
realloc函数:realloc函数用于重新分配内存。当需要改变已经分配的动态数组的大小时,可以使用realloc。它的函数原型为void *realloc(void *ptr, size_t size);,其中ptr是之前通过malloc或calloc分配的内存指针,size是重新分配后的字节数。例如,如果要将一个已经分配的动态数组arr的大小扩大,可以使用arr = (int *)realloc(arr, new_size * sizeof(int));。需要注意的是,realloc可能会移动原来的数据到新的内存位置,所以使用时要谨慎。
二、动态数组的使用
分配内存和访问元素
一旦使用malloc或calloc分配了动态数组的内存,就可以像使用普通数组一样访问其元素。例如,对于上面分配的int *arr动态数组,可以通过arr[i](i是索引)的方式来访问和修改元素。例如:
int *arr = (int *)malloc(5 * sizeof(int));
if(arr == NULL){
// 内存分配失败的处理
printf("Memory allocation failed!\n");
return 1;
}
for(int i = 0; i < 5; i++){
arr[i] = i;
}
- 这里首先检查了内存分配是否成功(如果
malloc
返回NULL
,表示内存分配失败),然后通过循环对动态数组的元素进行赋值。
- 释放动态数组内存
- 动态数组所占用的内存是在堆中分配的,当不再需要这些内存时,必须手动释放,以避免内存泄漏。可以使用
free
函数来释放动态数组的内存。例如,对于上面的动态数组arr
,在使用完后,应该使用free(arr);
来释放内存。需要注意的是,一旦释放了内存,就不能再访问该内存区域,否则会导致程序出现错误(如段错误)。
- 动态数组所占用的内存是在堆中分配的,当不再需要这些内存时,必须手动释放,以避免内存泄漏。可以使用
- 动态数组作为函数参数
- 动态数组作为函数参数传递时,和普通数组类似,传递的是数组的首地址。例如:
void printArray(int *arr, int size){
for(int i = 0; i < size; i++){
printf("%d ", arr[i]);
}
}
int main(){
int *arr = (int *)malloc(5 * sizeof(int));
// 赋值操作省略
printArray(arr, 5);
free(arr);
return 0;
}
在printArray函数中,通过指针来接收动态数组的首地址,并可以访问和操作数组中的元素。
三、动态数组的应用场景
数据存储的灵活性
在处理用户输入的数据集合时,如从用户输入中读取一系列整数,由于不知道用户会输入多少个整数,就可以使用动态数组来存储这些数据。可以根据用户输入的数量动态地分配足够的内存空间。
数据结构的实现
许多数据结构,如链表、栈、队列等,在实现过程中会涉及到动态内存分配。例如,在链表中,每个节点的内存空间通常是动态分配的,节点中的数据部分可以看作是一个动态数组(如果存储的数据是数组形式)。这使得数据结构可以根据实际需要灵活地增长或收缩。
一、常见内存错误
(一)数组越界访问
错误描述
当程序试图访问数组范围之外的元素时,就会发生数组越界访问。例如,对于数组int arr[5];,如果访问arr[5]或arr[-1]等超出数组下标的元素,就会出现这种错误。这种错误可能不会立即导致程序崩溃,但会产生不可预测的结果,如修改其他变量的值、破坏程序的栈结构等。
示例代码
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int i;
for (i = 0; i <= 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
- 在这个例子中,循环条件
i <= 5
导致了数组越界访问,当i = 5
时,arr[5]
已经超出了数组arr
的范围。
(二)野指针
- 错误描述
- 野指针是指指向一个已被释放的内存区域或者未初始化的指针。当使用野指针进行解引用操作(通过
*
运算符访问指针所指向的值)时,会导致程序出现错误,如段错误(访问非法内存地址)。
- 野指针是指指向一个已被释放的内存区域或者未初始化的指针。当使用野指针进行解引用操作(通过
- 示例代码
int main() {
int *p;
*p = 10;
return 0;
}
这里的指针p没有被初始化就进行解引用操作,p是一个野指针,程序会出现错误。
(三)内存泄漏
错误描述
内存泄漏是指程序动态分配了内存,但在使用完后没有释放这些内存。随着程序的运行,内存泄漏会导致系统可用内存越来越少,最终可能导致程序或系统崩溃。这种情况通常发生在忘记调用free函数释放通过malloc、calloc或realloc分配的内存时。
void func() {
int *p = (int *)malloc(sizeof(int));
*p = 10;
// 没有释放内存
}
int main() {
func();
// 多次调用func函数会不断分配内存但不释放
func();
return 0;
}
- 在
func
函数中,动态分配了内存但没有释放,每次调用func
函数都会占用额外的内存,导致内存泄漏。
(四)悬空指针
- 错误描述
- 悬空指针是指当一个指针所指向的内存已经被释放,但指针仍然存在并可能被错误地使用。与野指针不同的是,悬空指针曾经指向的是合法的内存地址,但该内存现在已经无效。例如,先释放了一个动态分配的数组,然后又试图通过原来指向该数组的指针访问数据,就会出现问题。
- 示例代码
int main() {
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
// 错误地使用已经释放的指针
*p = 20;
return 0;
}
这里在释放了p所指向的内存后,又对p进行解引用操作,这是一个悬空指针错误。
二、对策
(一)防止数组越界访问
严格检查数组下标
在使用数组时,要确保数组下标在合法的范围内。对于循环访问数组的情况,可以仔细检查循环的起始条件和结束条件。例如,对于长度为n的数组arr,循环访问数组元素应该是for (int i = 0; i < n; i++),而不是for (int i = 0; i <= n; i++)。
使用边界检查工具或技术
一些编译器提供了数组边界检查的选项,可以开启这些选项来帮助检测数组越界访问错误。另外,也可以使用一些静态分析工具,如Cppcheck等,它们可以在一定程度上发现潜在的数组越界访问问题。
(二)避免野指针
初始化指针
在定义指针时,尽量对其进行初始化。如果指针暂时没有指向有效的内存地址,可以将其初始化为NULL。例如,int *p = NULL;。这样,在使用指针之前可以先检查它是否为NULL,避免对未初始化的指针进行解引用操作。
遵循正确的内存分配和使用流程
在使用指针之前,确保它已经被正确地分配了内存。例如,通过malloc等函数分配内存后,再对指针进行解引用操作。并且在内存释放后,将指针设置为NULL,以避免再次错误地使用该指针。
(三)解决内存泄漏
养成良好的内存释放习惯
对于通过malloc、calloc或realloc分配的每一块内存,都要确保在适当的时候调用free函数进行释放。可以在代码中添加注释或者遵循一定的代码结构来提醒自己释放内存。例如,在函数结束之前,检查并释放所有在函数内部动态分配的内存。
使用内存泄漏检测工具
有一些工具可以帮助检测内存泄漏,如Valgrind(在 Linux 环境下)。它可以跟踪程序的内存分配和释放情况,发现并报告内存泄漏的位置和原因。
(四)防止悬空指针
及时将释放后的指针置为 NULL
在释放指针所指向的内存后,立即将指针赋值为NULL。这样,在后续的代码中,如果不小心对该指针进行操作,可以通过检查指针是否为NULL来避免错误。例如,free(p); p = NULL;。
注意指针的生命周期和内存的释放顺序
确保在指针的整个生命周期内,它所指向的内存是有效的。在释放内存时,要考虑到其他指针是否还在引用该内存。如果有多个指针指向同一块内存,在释放内存后,要确保所有相关的指针都被正确地处理,避免出现悬空指针。
基于AI的学习
其他
还是练不够