作业信息
这个作业属于哪个课程 <班级的链接>(如2024-2025-1-计算机基础与程序设计)
这个作业要求在哪里 <作业要求的链接>(如2024-2025-1计算机基础与程序设计第一周作业)
这个作业的目标 指针与一维,二维数组的关系,指针数组及其应用,动态数组,缓冲区溢出攻击
作业正文
教材学习内容总结
1.指针与一维,二维数组的关系
指针与一维数组的关系
数组名是指针常量:在 C 和 C++ 等编程语言中,一维数组名实际上是一个指针常量,它指向数组的第一个元素。例如,对于一维数组int arr[5] = {1, 2, 3, 4, 5};,数组名arr等价于&arr[0],它的类型是int* const(指向int类型的常量指针)。这意味着可以用指针的方式来访问数组元素。
通过指针访问数组元素:可以定义一个指针变量来指向数组的元素。例如,int p = arr;,此时p指向arr的第一个元素。可以通过p来访问数组元素,如(p + 1)就表示数组的第二个元素arr[1]。这是因为p + 1实际上是p的地址值加上sizeof(int)(假设int类型占 4 个字节),而(p + 1)则是对p + 1这个地址进行解引用,从而得到数组中的第二个元素。
指针算术与数组下标访问的等价性:使用指针算术和数组下标访问数组元素在本质上是等价的。对于数组arr,arr[i]和(arr + i)是完全相同的。这是因为数组元素在内存中是连续存储的,arr + i计算出的是第i个元素的地址,再通过解引用(arr + i)就可以得到第i个元素的值。
指针与二维数组的关系
二维数组的存储方式:二维数组在内存中是按行存储的,即先存储第一行的元素,再存储第二行的元素,以此类推。例如,对于二维数组int matrix[3][4];,它在内存中的存储顺序是matrix[0][0]、matrix[0][1]、matrix[0][2]、matrix[0][3]、matrix[1][0]、matrix[1][1]等。
二维数组名与指针的关系:二维数组名(如matrix)是一个指向数组的指针,它指向二维数组的第一行。matrix的类型是int ()[4](指向包含 4 个int元素的数组的指针)。这意味着matrix的值是第一行数组的首地址。
通过指针访问二维数组元素:可以使用指针来访问二维数组的元素。例如,定义一个指针int (p)[4]= matrix;,此时p指向二维数组的第一行。要访问matrix[i][j]元素,可以使用((p + i))[j]或者((p + i)+j)。p + i指向二维数组的第i + 1行,(p + i)是第i + 1行的首地址,(p + i)+j是第i行第j列元素的地址,((p + i)+j)就是第i行第j列元素的值
2.指针数组及其应用
指针数组的定义
指针数组是一个数组,其元素为指针类型。例如,在 C 和 C++ 中,int *pArray[5];就定义了一个指针数组pArray。这个数组有 5 个元素,每个元素都是一个指向int类型的指针。
从内存角度来看,指针数组本身是一块连续的内存空间,用于存放指针变量。每个指针变量占用一定的字节数(在 32 位系统中,通常是 4 字节;在 64 位系统中,通常是 8 字节),这些指针变量可以指向不同类型的数据,如整数、字符、结构体等。
指针数组的初始化
可以在定义指针数组时进行初始化。例如,int a = 1, b = 2, c = 3; int *pArray[3]={&a, &b, &c};。这里将指针数组pArray的三个元素分别初始化为变量a、b、c的地址。这样,pArray[0]指向a,pArray[1]指向b,pArray[2]指向c。
也可以用字符串来初始化字符指针数组。例如,char *stringArray[3]={"Hello", "World", "!"};。这里stringArray是一个字符指针数组,它的元素分别指向三个字符串常量。
指针数组的应用
字符串数组:指针数组经常用于表示字符串数组。相比二维字符数组,指针数组在存储多个字符串时更加灵活。例如,当字符串长度参差不齐时,使用指针数组可以避免浪费内存空间。如上面的char *stringArray[3]例子,每个字符串在内存中的长度可以不同,而使用二维字符数组存储相同的字符串可能会因为要适应最长的字符串而浪费很多空间。
函数指针数组:可以创建函数指针数组来实现根据不同条件调用不同函数的功能。例如,在一个简单的计算器程序中,可以定义一个函数指针数组来存储加法、减法、乘法、除法等函数的指针。
动态内存分配:指针数组可用于动态分配内存。例如,要创建一个二维数组,其中第二维的大小在运行时确定。可以先定义一个指针数组,然后为每个指针动态分配内存。
3.动态数组
动态数组的概念
动态数组是一种在程序运行时可以根据需要动态地改变大小的数组。与静态数组不同,静态数组的大小在编译时就已经确定,而动态数组的大小可以在程序运行过程中根据用户输入、计算结果或者其他因素进行灵活调整。
动态数组在内存管理方面提供了更大的灵活性。它允许更有效地利用内存资源,特别是在处理不确定大小的数据集合时。例如,在读取一个文件的内容到数组中时,如果不知道文件的大小,使用动态数组就可以根据文件的实际大小来分配足够的内存。
动态数组的实现方式(以 C++ 为例)
使用new和delete操作符(C++)
在 C++ 中,可以使用new操作符来动态分配数组内存。例如,int *dynamicArray = new int[size];,这里size是一个变量,表示动态数组的大小。这个语句在堆内存中分配了size个int类型元素的连续空间,并返回指向这个空间首地址的指针dynamicArray。
当动态数组不再需要时,需要使用delete[]操作符来释放内存,以避免内存泄漏。例如,delete[] dynamicArray;会释放之前使用new分配的数组内存。
使用std::vector(C++)
std::vector是 C++ 标准模板库(STL)中的一个容器类,它提供了动态数组的功能。例如,std::vector
可以使用push_back方法向vector中添加元素,如dynamicVector.push_back(1);,vector会自动管理内存,根据需要动态地扩展数组大小。同时,vector还提供了许多其他方便的方法,如size用于获取数组中元素的个数,at用于安全地访问元素等。
使用malloc和free(C 语言)
在 C 语言中,可以使用malloc函数来动态分配内存。例如,int *dynamicArray = (int *)malloc(size * sizeof(int));,这与 C++ 中的new操作符类似,它在堆内存中分配了size个int类型元素的空间。
要释放内存,需要使用free函数,如free(dynamicArray);。需要注意的是,在 C 语言中,如果使用malloc分配的是一个数组,在释放时只需要传递数组的起始指针即可,而不是像 C++ 中的delete[]那样有特殊的语法。
动态数组的应用场景
数据读取和存储:当从外部数据源(如文件、网络)读取数据时,动态数组可以根据数据的实际大小来存储。例如,一个程序需要读取一个文本文件中的所有行,但是不知道文件中有多少行,就可以使用动态数组来存储这些行。
算法实现:在一些算法中,如排序算法和搜索算法,动态数组可以用于存储待处理的数据。例如,在合并排序算法中,需要将待排序的数组不断地分解和合并,动态数组可以方便地适应不同大小的数据集合。
动态内存管理模拟:在操作系统课程中,动态数组可以用于模拟内存的分配和释放过程。例如,通过动态数组来表示内存块,模拟操作系统如何为进程分配内存和回收内存。
4.缓冲区溢出攻击
缓冲区溢出攻击的概念
缓冲区溢出攻击是一种常见的软件安全漏洞利用方式。简单来说,当程序向缓冲区写入数据时,如果写入的数据超过了缓冲区本身的容量,多余的数据就可能会覆盖相邻的内存区域。这些被覆盖的内存区域可能包含程序的关键数据,如返回地址、函数指针等。攻击者可以通过精心构造的数据来利用这种溢出,改变程序的正常执行流程,从而达到获取系统控制权、窃取数据或者执行恶意代码等目的。
从内存角度看,程序的内存空间包括代码段、数据段(包括全局变量和静态变量)、堆(动态分配的内存)和栈(用于函数调用和局部变量存储)。缓冲区溢出通常发生在栈或者堆上。例如,在栈上的局部变量缓冲区,如果输入的数据过长,就可能溢出并覆盖栈帧中的返回地址。
栈缓冲区溢出攻击示例
考虑以下简单的 C 语言代码:
include <stdio.h>
include <string.h>
void vulnerableFunction(char *input) {
char buffer[10];
strcpy(buffer, input);
printf("Data in buffer: %s\n", buffer);
}
int main() {
char userInput[100];
scanf("%s", userInput);
vulnerableFunction(userInput);
return 0;
}
教材学习中的问题和解决过程(先问 AI)
问题1:指针在什么时候用?
解决问题:一、函数参数传递
1.修改实参的值
当我们希望在一个函数内部修改传递给它的变量的值时,指针就非常有用。例如,在交换两个整数的函数中:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int num1 = 5, num2 = 10;
swap(&num1, &num2);
// 此时num1的值为10,num2的值为5
return 0;
}
这里,printArray函数通过指针接收数组的起始地址,而不是复制整个数组。这在处理大型数组时可以节省时间和内存空间。
二、动态内存分配
在堆上分配内存
在 C 和 C++ 等语言中,指针用于在程序运行时动态地分配内存。例如,使用malloc(在 C 中)或new(在 C++ 中)来分配内存:
int *dynamicArray;
int size = 10;
dynamicArray = (int *)malloc(size * sizeof(int));
if (dynamicArray == NULL) {
// 内存分配失败处理
} else {
// 使用dynamicArray
free(dynamicArray);
}
在这里,next指针用于将链表中的节点连接起来,构建一个链式的数据结构。通过指针,可以方便地遍历、插入和删除链表中的节点。
三、多态性和面向对象编程(在 C++ 等语言中)
实现虚函数和动态绑定
在面向对象编程中,指针可以用于实现多态性。例如,有一个基类Shape和派生类Circle和Rectangle。
问题2:动态数组有什么优点?
解决问题:1.内存使用灵活性高
在 C 语言中,静态数组的大小是在编译时确定的。例如,如果定义int staticArray[10];,这个数组的大小就固定为 10 个int类型元素。而动态数组的大小可以在程序运行时根据实际需求来确定。
假设要编写一个程序来处理用户输入的一系列整数,但是在编译时并不知道用户会输入多少个整数。使用动态数组就可以先获取用户输入的整数个数n,然后通过malloc函数分配n个int类型元素所需的内存空间来创建动态数组,如下所示:
int n;
printf("请输入整数的个数:");
scanf("%d", &n);
int *dynamicArray = (int *)malloc(n * sizeof(int));
if (dynamicArray == NULL) {
// 内存分配失败处理
printf("内存分配失败!\n");
return 1;
}
在这个栈的push操作中,当栈满时,通过重新分配更大的内存空间并复制数据,实现了栈的动态扩展,使得栈可以存储更多的元素。
2.方便数据共享和传递
动态数组可以方便地在函数之间传递,并且多个函数可以共享同一块动态分配的内存。例如,有一个函数用于读取文件中的数据并存储到动态数组中,另一个函数用于处理这些数据。
假设要读取一个文本文件中的整数序列,函数readDataFromFile可以用来读取数据并返回动态数组,另一个函数processData可以用来处理这些数据:
int *readDataFromFile(const char *filename, int *size) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
// 文件打开失败处理
printf("文件打开失败!\n");
return NULL;
}
int capacity = 10;
int *data = (int *)malloc(capacity * sizeof(int));
if (data == NULL) {
// 内存分配失败处理
fclose(fp);
return NULL;
}
int count = 0;
int num;
while (fscanf(fp, "%d", &num) == 1) {
if (count == capacity) {
// 数组满,扩容
capacity *= 2;
int *newData = (int *)malloc(capacity * sizeof(int));
if (newData == NULL) {
// 内存分配失败处理
fclose(fp);
free(data);
return NULL;
}
for (int i = 0; i < count; i++) {
newData[i] = data[i];
}
free(data);
data = newData;
}
data[count++] = num;
}
fclose(fp);
*size = count;
return data;
}
void processData(int *data, int size) {
// 处理数据的代码,例如求和、排序等
int sum = 0;
for (int i = 0; i < size; i++) {
sum += data[i];
}
printf("数据总和为:%d\n", sum);
}
int main() {
int size;
int *data = readDataFromFile("data.txt", &size);
if (data!= NULL) {
processData(data, size);
free(data);
}
return 0;
}
问题3:怎么避免缓冲区溢出?
解决问题:1.正确使用字符串处理函数
在 C 语言中,一些不安全的字符串处理函数如strcpy和strcat很容易导致缓冲区溢出。这些函数在复制或拼接字符串时不会检查目标缓冲区的大小。
例如,以下代码存在缓冲区溢出风险:
char buffer[10];
char source[] = "This is a long string";
strcpy(buffer, source);
这里strncpy的第三个参数指定了最多复制的字符数(sizeof(buffer) - 1是为了给字符串结束符\0留出空间),最后手动添加\0以确保字符串的正确结束。
2.数组边界检查
对于数组操作,一定要注意数组的边界。在访问和写入数组元素时,确保索引在合法范围内。
例如,下面的代码可能导致缓冲区溢出:
int array[5];
for (int i = 0; i <= 5; i++) {
array[i] = i;
}
另外,可以使用宏或者内联函数来帮助进行边界检查。例如,定义一个宏来检查数组索引:
define CHECK_INDEX(array, index) \
((index) >= 0 && (index) < sizeof(array)/sizeof(array[0]))
int main() {
int array[5];
int index = 3;
if (CHECK_INDEX(array, index)) {
array[index] = 10;
}
return 0;
}
3.动态内存分配安全检查
当使用malloc、calloc或realloc等函数进行动态内存分配时,需要检查返回值是否为NULL。如果内存分配失败,这些函数会返回NULL,若不检查就直接使用可能会导致程序崩溃或出现未定义行为。
例如:
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr == NULL) {
// 内存分配失败处理
printf("内存分配失败!\n");
return 1;
}
// 正常使用动态分配的内存
free(ptr);
4.使用安全的编译器选项
许多 C 编译器提供了一些选项来帮助检测缓冲区溢出。例如,GCC 编译器提供了-fsanitize=address选项。
当使用这个选项编译程序时,编译器会在运行时检查内存访问错误,包括缓冲区溢出。如果发现问题,它会输出详细的错误信息,帮助开发者定位和修复问题。
例如,在命令行中编译程序:
gcc -fsanitize=address -g your_program.c -o your_program