目录
一、字符串处理函数常见漏洞及防御措施
(一)常见漏洞
1.1 缓冲区溢出
调试的诡异崩溃现场:
char buffer[10];
strcpy(buffer, "这个字符串绝对不止10个字节!"); // 直接触发内存雪崩
strcpy
函数会一直复制源字符串,直到遇到字符串结束符'\0'为止,而不会去判断目标数组是否足够大来容纳源字符串。如果源字符串长度大于目标数组长度,就会导致溢出。
1.2 格式化字符串漏洞
某次代码审计发现的典型问题:
char name[20] = "%s%sBingo";
printf(name); // 若name中包含格式化字符串如"%s%s",可能会泄露栈信息
当printf
函数的格式字符串参数是由用户输入控制且未正确转义时,攻击者可以构造特殊的格式化字符串,如%x
、%s
等,来读取或写入栈中的数据,从而获取敏感信息或改变程序执行流程。
(二)防御措施
1. 使用安全函数
用strncpy
、strncat
等带有长度限制的函数替代strcpy
、strcat
,它们可以指定最多复制或连接的字符数量,有效防止缓冲区溢出。
char dest[10];
char src[15] = "Hello, World!";
strncpy(dest, src, sizeof(dest) - 1); // 复制时限制长度,避免溢出
dest[sizeof(dest) - 1] = '\0'; // 确保字符串以'\0'结尾
strncpy
函数在复制字符串时,最多复制指定的长度,这样可以确保不会超出目标缓冲区的范围,避免了缓冲区溢出问题。
2. 严格检查输入
对所有来自外部的输入进行严格长度检查和合法性验证,确保输入数据在合理范围内,避免因异常输入触发漏洞。
char input[100];
printf("请输入您的名字(最多99个字符):");
scanf("%99s", input); // 限制输入的最大长度,防止缓冲区溢出
3. 避免危险用法
在使用printf
等函数时,确保格式字符串是固定的,不直接使用用户输入作为格式字符串,或者对用户输入的格式字符串进行严格转义和限制。
char name[20] = "Bingo";
printf("%s", name); // 安全用法,固定格式字符串
二、动态数据实现
在C语言中,动态数据的实现主要依赖于动态内存分配函数,如malloc
、calloc
、realloc
和free
。这些函数允许程序在运行时根据需要分配和释放内存,从而灵活地处理数据。
malloc
函数
-
功能:分配指定大小的内存块,返回指向该内存块的指针。
-
函数原型:
void *malloc(size_t size);
使用示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n = 5;
// 分配存储5个整数的内存
arr = (int*)malloc(n * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "内存分配失败!\n");
return 1;
}
// 使用分配的内存
for (int i = 0; i < n; i++) {
arr[i] = i * i;
printf("%d ", arr[i]);
}
printf("\n");
// !!!释放内存
free(arr);
arr = NULL; // !!!避免悬空指针,因为可能还存有地址
return 0;
}
malloc
函数的注意事项
在调用malloc
后,必须检查其返回值是否为NULL
,以确保内存分配成功。如果返回NULL
,表示内存分配失败,此时应采取相应的错误处理措施。动态分配的内存必须手动释放,否则会导致内存泄漏。可以使用free
函数释放由malloc
分配的内存。确保每次malloc
都有对应的free
。一旦内存被释放,应避免继续使用已释放的内存,这可能导致未定义行为。可以将指针设置为NULL
以防止误用。
三、结构体
(一)基本语法
// 基本语法
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
...
};
// 示例
typedef struct
{
char name[128]; // 姓名
char sex[5]; // 性别
short id; // 学号
int age; // 年龄
int sco; // 成绩
} stu_data;
(二)结构体的使用
声明结构体变量
struct Student s1;
访问结构体成员
strcpy(s1.name, "Bingo");
s1.age = 18;
s1.sco = 66;
初始化结构体
struct Student s2 = {"Bingo", 18, 66}; //我感觉最简便的
(三)结构体与指针
结构体指针
可以定义指向结构体的指针,通过指针访问结构体成员时,可以使用->
运算符。
struct Student *p = &s1;
printf("%s", p->name); // 输出"Bingo"
动态分配结构体内存
使用malloc
函数为结构体分配内存。
struct Student *s3 = (struct Student *)malloc(sizeof(struct Student));
if (s3 != NULL) {
strcpy(s3->name, "Bingo");
s3->age = 18;
s3->score = 66;
free(s3); // 使用完毕后释放内存
}
(四)嵌套结构体
结构体可以包含其他结构体作为成员,这称为嵌套结构体。
struct Address {
char street[100];
char city[50];
char state[50];
};
struct Person {
char name[50];
int age;
struct Address addr; // 嵌套的结构体
};
struct Person p1 = {"小彬", 18, {"莲花路", "株洲", "湖南"}};
// 访问嵌套结构体的成员时,可以使用.运算符链式访问
printf("%s", p1.addr.city); // 输出"株洲"
(五)结构体数组
可以声明结构体数组,用于存储多个具有相同结构的数据。
struct Student students[3] = {
{"Bingo", 18, 66},
{"彬彬", 18, 66},
{"小彬", 18, 66}
};
for (int i = 0; i < 3; i++) {
printf("学生%d: %s, %d岁, 成绩:%.1f\n", i+1, students[i].name, students[i].age, students[i].sco);
}
四、练习
学生管理系统
结构体定义
-
stu_data
:表示单个学生的信息,包括姓名、性别、学号、年龄和成绩。
-
stu_class
:表示一个班级,包含一个最多20个学生的数组和一个记录当前学生人数的变量。
功能函数
-
tail_ins_list
:在班级的尾部插入一个新学生。检查班级是否已满,并确保学号唯一。
-
tail_del_list
:删除班级最后一个学生。
-
id_del_list
:根据学号删除对应的学生,通过移动数组元素实现。
-
change_list
:根据学号查找学生并修改其信息,可修改姓名、性别、学号、年龄或成绩。
-
find_list
:根据学号查找学生,返回其在数组中的索引。
-
binary_find
:实现了二分查找,但未在主函数中调用。
-
sort_list
:用的简单冒泡排序,还可以用其他更快的比如快排等。
-
printf_Sql_List
:打印班级中所有学生的信息。
完整代码
#include <stdio.h>
#define MAX_stu 20
// 单个学生
typedef struct
{
char name[128]; // 姓名
char sex[5]; // 性别
short id; // 学号
int age; // 年龄
int sco; // 成绩
} stu_data;
// 一个班
typedef struct
{
stu_data data[MAX_stu]; // 学生
int len; // 记录学生人数
} stu_class;
/************************* 增 *************************/
/*
对 class 结构体中进行尾部数据的插入
函数 void pos_ins_list( stu_class * class , int pos);
参数
@ class 要操作的班级
返回值
无返回值
*/
/************************* 增 *************************/
void tail_ins_list(stu_class *class);
/************************* 删 *************************/
void tail_del_list(stu_class *class);
void id_del_list(stu_class *class);
/************************* 改 *************************/
void change_list(stu_class *class);
/************************* 查 *************************/
int find_list(stu_class *class);
/************************* 排序 *************************/
void sort_list(stu_class *class);
/************************* 打印 *************************/
void printf_Sql_List(stu_class *class);
// 如果你现在没听懂 一定是老师的问题 不是你的问题
// 我不喜欢 pua 自己的人
int main(int argc, char const *argv[])
{
/* 初始化结构体 */
// 创建一个 班级结构体
stu_class stu = {
// 学生数据初始化
.data = {
{"小明", "男", 1, 18, 80},
{"小红", "女", 2, 18, 81},
{"彬彬", "男", 3, 18, 66},
{"小绿", "男", 4, 18, 83},
{"小紫", "女", 5, 18, 84},
},
// 学生个数变量初始化
.len = 5};
printf_Sql_List(&stu);
// 插入数据
// tail_ins_list(&stu);
// printf_Sql_List(&stu);
// 删尾
// tail_del_list(&stu);
// printf("已删除:\n");
// printf_Sql_List(&stu);
// 删指定学号
// id_del_list(&stu);
// printf("已删除:\n");
// printf_Sql_List(&stu);
// 修改学生信息
change_list(&stu);
printf_Sql_List(&stu);
return 0;
}
/************************* 增 *************************/
void tail_ins_list(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级");
return;
}
// 判断满
if (MAX_stu < class->len)
{
puts("满了 塞钱都没用(ps:除非给系统的开发者(小声bb))"); // 嘿嘿~
return;
}
// 定义标志位 判断插入学生学号是否重复
int flag = 0;
// 输入数据
stu_data data;
printf("请输入学生数据:\n");
while (1)
{
printf("姓名:");
scanf("%s", data.name);
printf("性别:");
scanf("%s", data.sex);
printf("学号:");
scanf("%hd", &data.id);
printf("年龄:");
scanf("%d", &data.age);
printf("成绩:");
scanf("%d", &data.sco);
for (int i = 0; i < class->len; i++)
{
if (data.id == class->data->id)
{
flag = 1;
break;
}
}
if (1 == flag)
{
flag = 0;
printf("已有重复学号,请重新输入:\n");
continue;
}
else
{
// 插入数据
class->data[class->len] = data;
// 迭代长度
class->len++;
break;
}
}
}
/************************* 删 *************************/
// 直接删除尾部学生
void tail_del_list(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级");
return;
}
// 判断班级人数是否以为空
if (0 >= class->len)
{
puts("没有人了,再删就负数啦\n");
return;
}
class->len--;
}
// 删除指定学号学生
void id_del_list(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级\n");
return;
}
// 判断班级人数是否以为空
if (0 >= class->len)
{
puts("没有人了,再删就负数啦\n");
return;
}
int del_id;
int flag = 0;
while (1)
{
printf("请输入你要删除学生的id:");
scanf("%d", &del_id);
for (int i = 0; i < class->len; i++)
{
if (del_id == class->data[i].id)
{
for (int j = i; j < class->len - 1; j++)
{
class->data[j] = class->data[j + 1];
}
class->len--;
flag = 1;
break;
}
}
if (0 == flag)
{
printf("没有该学号的学生\n");
printf_Sql_List(class);
}
else
{
break;
}
}
}
/************************* 改 *************************/
void change_list(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级\n");
return;
}
int index = 0; // 查找到的下标
int op = 0; // 选项
// 查
index = find_list(class);
if (-1 == index)
{
printf("未找到数据\n");
return;
}
// 修改
printf("请输入你需要修改的项目:\n");
printf("1、姓名\n");
printf("2、性别\n");
printf("3、学号\n");
printf("4、年龄\n");
printf("5、成绩\n");
scanf("%d", &op);
switch (op)
{
case 1:
printf("姓名:");
scanf("%s", class->data[index].name);
break;
case 2:
printf("性别:");
scanf("%s", class->data[index].sex);
break;
case 3:
printf("学号:");
scanf("%hd", &class->data[index].id);
break;
case 4:
printf("年龄:");
scanf("%d", &class->data[index].age);
break;
case 5:
printf("成绩:");
scanf("%d", &class->data[index].sco);
break;
default:
printf("输入错误\n");
break;
}
}
/************************* 查 *************************/
int find_list(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级\n");
return -1;
}
printf("请输入你需要查找的id号:");
int id = 0;
scanf("%d", &id);
for (size_t i = 0; i < class->len; i++)
{
if (id == class->data[i].id)
{
return i;
}
}
return -1;
}
// 二分查找
int binary_find(stu_class *class)
{
// 判定是否为空指针
if (NULL == class)
{
puts("你没有传入班级\n");
return -1;
}
int id = 0;
printf("请输入你需要查找的id号:");
scanf("%d", &id);
// 二分查找
int left = 0;
int right = class->len - 1;
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (id == class->data[mid].id)
{
return mid;
}
else if (id < class->data[mid].id)
{
right = mid - 1;
}
else
{
left = mid + 1;
}
}
}
/************************* 排序 *************************/
// 按成绩降序排序
void sort_list(stu_class *class)
{
if (NULL == class)
{
puts("你没有传入班级\n");
return;
}
for (int i = 0; i < class->len - 1; i++)
{
for (int j = 0; j < class->len - 1 - i; j++)
{
if (class->data[j].sco < class->data[j + 1].sco)
{
stu_data temp = class->data[j];
class->data[j] = class->data[j + 1];
class->data[j + 1] = temp;
}
}
}
}
/************************* 打印 *************************/
void printf_Sql_List(stu_class *class)
{
// 循环
for (size_t i = 0; i < class->len; i++)
{
// 打印数据
printf("name = %s sex = %s id = %d sco = %d age = %d \n",
class->data[i].name, class->data[i].sex, class->data[i].id, class->data[i].age, class->data[i].sco);
}
}
在学习C语言的道路上,如何将基础的语法知识转化为实际的编程能力是一个关键的挑战。今天,我要和大家分享一个实战项目——用C语言实现的学生信息管理系统。这个系统不仅巩固了我对结构体、数组以及函数的理解,还让我在实践中掌握了数据的组织、存储和操作技巧。