2025年c语言作业复盘

实验一

sy1-2

image-20251224202522533

  • 短整型占2个字节(有符号) 取值范围-32768~32767
  • 无符号短整型占两个字节,取值0~65535
  • c=-1 因为c为无符号短整型 而-1的补码全为1,赋值无符号短整型时即 1111 1111 1111 1111 (65535)
  • b=d b为短整型 (长整型d赋值给b发生截断,即截断d的后16位给b)
  • hd(短整型) hu(无符号短整型) hx(短整型十六进制)

sy1-3

image-20251224203411170

  • 注意求值的顺序会根据编译系统的不同而不同
1
2
printf("i=%d,j=%d,i=%d,j=%d",i,j,i++,j++)
// 这里是从右到左
  • “短路现象” 不是所有表达式都被运算!!!

对于或‘a|b‘ 只要a为真则表达式b就不用算

sy1-4

image-20251224204044990

可用常变量来替代宏定义(const )

sy1-5

image-20251224204550205

用条件运算符来判断字母是否产生回绕

1
2
c>'z' || c>'Z' && c<'a' ? c-=26:c;
//因为大写与小写字母之间有其他字符存在

实验二

sy2-3

image-20251224205455119

  • 在switch选择中不需要加12月份 直接加day即可

  • 只需要在二月做好区分即可

  • 这里不用加break跳出选择,只用找到匹配的月份后往下执行相加即可(这也是为什么选择倒序的原因)

sy2-4

image-20251224205959947

  • 注意在条件表达式中注意区分等于 ==赋值 =算术符

实验三

sy3-3

image-20251226192503018

  • 用循环语句输出倒序金字塔
1
2
3
4
5
6
7
8
9
10
11
12
13
int i,j,n;
scanf("%d",&n);
for(i=n;i>=1;i--)
{
for(j=i;j<=n;j++) //这里打循环空格的方式不一样(我自己想的) 不过也可以按上面(j=1;j<=n-i;j++)
printf(" ");
for(j=1;j<=i;j++)
printf("%c",'A'+j-1);
for(j=i-1;j>=1;j--)
printf("%c",'A'+j-1);
printf("\n");
}
return 0;
  • 如果是打入正序金字塔
1
2
3
4
5
6
7
8
9
10
11
12
13
int i,j,n;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
for(j=1;j<=n-i;j++)
printf(" ");
for(j=1;j<=i;j++);
printf("%c",'A'+j-1);
for(j=i-1;j<=1;j--);
printf("%c",'A'+j-1);
printf("\n");
}
return 0;

sy3-4

image-20251226194538072

1.利用t原来循环的值继续除以i这样就不用特地的求每一次的阶乘

2.精度 当加上的某一项小于精度时,即退出循环

实验四

sy4-3

image-20251226201807042

插入数据

1
2
3
4
5
6
for(k=0;k<n;k++)   //这里用一个新的变量来记录应该插入的位置
if(x<a[k])
break;
for(i=n-1;i>=k;i--)
a[i+1]=a[i];
a[k]=x; //原来的a[k]~a[n-1]均往后移一位 故a[k]则空出来给插入变量x

选择排序法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int i,j,k;
for(i=0;i<n-1;i++)
{
k=i; //假设每一轮中k都为最小数的下标 则比较a[k]~a[n-1]数的大小 eg.当k=2时则比较a[2]~a[n-1]之间数的大小 故每循环一次都能确定a[k]~a[n-1]之间的一个最小数 故一共只用比较n-1次
for(j=i+1;j<n;j++)
if(a[k]>a[j])
k=j; //这里进行下标交换,即此步找到最小数的下标为j 但并未进行排序(即把他排在左边)
if(k!=i)
{
t=a[i]; //这里进行数的交换 发现下标不是原来的小标 把k下标对应的数与此次循环i下标所对应的数进行交换 其实可以看成每次循环的a[i]相当于已经确定它是排在第i个的数 这些过程只是在寻找与之对应的数而已
a[i]=a[k];
a[k]=t;
}
}

实验五

sy5-3

image-20251226204740422

  • 删除尾部的某一种符号 可以利用循环从右到左找到第一个不是这种符号的字符 然后记住下标 并且把此下标的下一个字符赋值为‘\0’这样就不用一直想着说如何删除

sy5-4

image-20251226205319906

  • 如何把小写字母转换为大写
    • 如果是小写 (x-‘a’(找到偏移量)+‘A’) (对应的可以找到此偏移量在大写中应该为什么字母)

实验六

sy6-1

  • 值传递(相当于形参为实参的简单copy件)
    • 在调用函数时如果函数自成语句像这样 eg.swap(x,y); 函数swap交换x,y的值,在函数调用时形参x,y的值已经交换成功,但当函数结束时,形参被分配的内存空间被释放,可以认为函数所改变的值没来得及传递给主函数)

image-20251228175034115

这个就是很典型的值传递,但函数调用结束完后直接清除空间,我觉得可以相当于没有调用函数或者说调用函数没有用)

  • 但如果是在赋值语句中,即表达式中,虽然实参x,y的值也没有被改变,但是其函数运行的结果有效的传递到主函数中 ( eg. sum=max(x,y);取最值函数的结果有效赋值给sum,虽然函数最后也是被释放,但与简单的调用不同)

image-20251228174809200

如上述代码 虽然只是简单的值传递,但是函数运行的结果有被传送到主函数中 (但实参x,y的值并没有改变)

  • 地址传递(传递地址,但要看你是改变指针变量的值还是改变指针变量所指向的值的值)

    • 改变指针变量的值(如果在交换函数中,一般是把中间变量 int *t指针变量直接交换)—其实就为简单的值传递
    1
    2
    3
    4
    5
    6
    7
    int swap(int *x,int *y)
    {
    int *t; //与指针变量交换的话 中间变量应为与指针变量同一个类型,即指针类型
    t=x;
    x=y;
    y=t;
    }
    • 改变指针变量所指向的值的值(在交换函数中,把中间变量与指针变量所指向的值进行交换)—这种才能有效的改变实参的值
    1
    2
    3
    4
    5
    6
    7
    int swap(int *x,int *y)
    {
    int t; //这里的中间变量为了与指针变量所指向的值进行交换 ,所以t为整型
    t=*x;
    *x=*y;
    *y=t;
    }

image-20251228180823423

sy6-3

使用半折法寻找数(前提已经排好序的一组数)

—ps:下面方法是在一组升序排法的数中

image-20251228190759929

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int search(int a[],int n,int x)
{
int low,mid,hig; //定义三个变量
low=0;hid=n-1; //lpw是最左边的(最小) hig是最右边(最大)
while(low<hig)
{
mid=(low+hig)/2; //所以 mid(中间值)就为两数相加除以2
if(a[mid]==x) //这里会出现3种情况 是中间值== 还是< 还是>
return mid+1; //直接找到下标 然后按照题目要求返回下一个数的下标
else if(a[mid]>x)
hig=mid-1; //x<中间值 把hig移到中间值左边(mid-1) 缩小范围
else
low=mid+1; //x>中间值 把low移到中间值右边(mid+1)
}
return -1; //一直循环,直到最小大于最大,即找不到这个数,结束循环,返回-1
}

sy6-4**(有关取模运算)**

image-20251228192032052

其实有关于加密代码的程序

1
2
3
s[i]=(s[i]+d-'A'+26)%26+'A';  //前提是已经确定字母是大写还是小写 然后在各自的表达式中运算  
//这个取模运算是原来字母+偏移量(shift)-'A'/'a'(这里就可以知道原字母距离首字母差多少 这时候就会出现两种情况:此时数字已经>26 或者<26 小于的很好算但为了表达式统一(找到统一的规律) 这时就统一+26 然后再整体进行取模运算
s[i]=(s[i]+d-'a'+26)%26+'a';

sy6-5(理解变量的定义域)

image-20251228194228348

  • 全局变量和静态变量如果未赋初始值,那么默认为0

实验七

sy7-3(用链表来操作学生信息)

  • 定义结构体变量
1
2
3
4
5
6
7
8
9
10
//首先需要用结构体来表示一个结点 每一个结构体的尾部放有指向下一个结点的指针

//定义结构体
struct student
{
char num[11];
char name[11];
int age;
struct student * next;
}; //这里如果没有直接定义结构体变量需直接加分号';',就算有定义完也要加分号

image-20251228195549686

  • 使用头插法创建带有头结点的链表,并且返回头指针
    • 注意后续调用此函数应为这种 head=create();把头指针h赋值给指针变量head
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct student * create()   //创建一个返回结构体类型的指针
{
struct student *h,*p;
int i=0,n;
h=(struct student *)malloc(sizeof(struct student));
h->next=NULL;
scanf("%d",&n);
for(i=0;i<n;i++)
{
p=(struct student *)malloc(sizeof(struct student));
p->next=NULL; //开辟新结点
scanf("%s%s%d",p->num,p->name,&p->age);
p->next=h->next; //先把头结点的尾部赋值给新结点的尾部
h->next=p;
}
return h; //返回头结点
}
  • 插入新的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
void insert(struct student *h,struct student *x)
{
struct student *p,*q;
q=h; //q为插入结点的前一个结点 从第一个结点开始查找
p=h->next; //p指向要插入的结点
while(p!=NULL && strcmp(p->num,x->num)<0) //查找中
{
q=p; //q往下移
p=p->next; //p也往下一个移
}
x->next=p; //当p的学号大于要插入结点的学号则退出循环(此时已经找到要插入的位置,即p的上一个结点,即q结点的后面)
q->next=x;
}
  • 删除数据(删除时只有进行学号比较即可,故引入的新数据为一个数组就好)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void dele(struct student *h,char x[])  //想要删除需要先查找到要删除的结点,故前面与查找函数类似
{
struct student *p,*q;
q=h;
p=h->next;
while(p!=NULL && strcmp(p-num,x)!=0) //当p与x的学号相同时即查找到要删除的结点 即p结点
{
q=p;
p=p->next;
}
if(p!=NULL) //排除p结点为空的可能
{
q->next=p-next;
free(p);
}
}

sy7-4(结构体,联合体,共用体的区分)

image-20251228204114670

1
2
3
4
5
b.ch[0]='a';
b.ch[1]='b';
b.k=0x4241;
printf("%c,%c\n",b.ch[0],b.ch[1]);
//共用体即一起用一块内存的组合体,其内存大小由最大长度的成员决定 故此共用体由短整型b.k决定 而b.ch[0] b.ch[1]为数组成员各占一个字节 且b.ch[1]的地址高于b.ch[0]的地址 所以b.ch[1]取高字节42(十六进制) b.ch[0]取低字节41(十六进制)

实验八

sy8-1(用文件来输入输出)image-20251229201337928

  • 第一步 定义文件指针
1
FILE *fp_in,*fp_out;
  • 以写的方式打开输入文件 (如果原来没有该文件则重新创建)
1
2
3
4
5
if((fp_out=fopen("d:\\data\\sy8-1-out.txt","w"))==NULL)    //fopen函数的两个参数都为字符串,两个部分:即文件在哪 和以那种方式打开文件  (fopen函数是为该文件分配一个缓冲区,并且返回该缓冲区的首地址  所以要把fopen函数的返回值赋值给文件指针)
{
printf("can't open file!\n"); //为了以防文件打不开,那么则运行if语句使文件关闭
exit(0);
}
  • 输入字符到该文件中 fputc 函数 一次写入一个字符到该文件中
1
2
3
while((ch=getchar())!='#')  //字符ch由getchar函数输入(输入到屏幕上) 如果不为字符'#'则用fputc函数输入到文件中
fputc(ch,fp_out);
fclose(fp_out); //关闭文件
  • 用只读的形式打开前面写入的文件**(为了以防文件没有被正确的输入)**
1
2
3
4
5
if((fp_in=fopen("d:\\data\\sy8-1-out.txt","r"))==NULL)
{
printf("can't open file!\n"); //如果出现这种情况就关闭文件
exit(0);
}
  • 以只写的形式打开输出文件(如果原来没有输出文件就重新创建)
1
2
3
4
5
if((fp_out=fopen("d:\\data\\sy8-1-out1.txt","w"))==NULL)
{
printf("can't open file!\n");
exit(0);
}
  • 在该文件中把小写字母转换为大写字母(以循环)
1
2
3
4
5
6
7
8
9
ch=fgetc(fp_in);  //先读取该文件的第一个字符
while(!feof(fp_in)) //判断是否到达文件末端
{
if(ch>='a' && ch<='z')
ch=ch-'a'+'A'; //大小写字母转换
fputc(ch,fp_out); //转换成功就写入该文件
putchar(ch); //将字符ch显示到屏幕上
ch=fgetc(fp_in); //利用循环继续读取下一个字符
}

sy8-2

image-20251229205001780

  • 从输入文件中读取数据(fscanf函数)
1
2
3
4
5
6
7
int n=0;
while(!feof(fp_in))
{
fscanf(fp_in,"%d",&a[n]); //以格式化形式读取该文件的数据,并把数据存储到数组中,顺便通过循环得知数组的个数(n)ps:先a[0]后n再自加
n++;
}
fclose(fp_in);
  • 然后关闭文件在主函数中排序

  • 打开输出文件

    image-20251229205655862

1
2
3
4
5
6
//通过循环有效去除最小数和最大数
for(i=1;i<n-1;i++)
{
printf("%d",a[i]); //显示到屏幕上
fprintf(fp_out,"%d",a[i]); //把n-2个成绩写入文件中
}

C语言常见安全漏洞与修复意见

  • 栈缓冲区溢出
    • strcpy(buffer input) strcpy不检查目标缓冲区大小 导致栈溢出
    • 应该用 strncpy snprintf
  • 格式化字符串漏洞
    • printf(user_input); //危险由用户控制字符串大小
    • 因为若用户所输入的包含格式字符如 %n(写入内存) %x(泄露信息) 那么 printf函数会把它当成格式字符来处理
    • 应使用 printf("%s",user_input)
  • 堆溢出(用户自己分配内存的变量)
    • strcpy(buffer,data) //buffer所分配的内存过小 data字符串的长度过长
    • 应该检查数据长度,使用安全复制函数
  • 整数溢出
    • char *buffer = (char *)malloc(size+1) //由用户来输入size的话如果此时为最大整数的话 就会产生溢出变为负数或者很小的值
    • 应该检查加法是否溢出 if(size >INT_MAX -1)
  • 释放后使用(Use-After-Free)
    • free(ptr); printf("%s\n",ptr); //危险访问已经释放内存的变量
    • 可能会导致内存泄漏 或者执行任意的代码
    • 应释放后立即置为NULL: free(ptr); ptr=NULL;
  • 双重释放(Double Free)
    • free(buffer); free(buffer); //重复释放同一块内存
    • 破坏堆管理结构,可能导致任意代码执行
    • 释放后应把指针置为NULL,这样第二次free就会失败
  • 未初始化内存就使用
    • int *ptr=(int*)malloc(sizeof(int) *10); 然后就直接if(ptr[0]==0) //因为未赋初始值所以值并不确定
    • 应使用calloc或者直接手动初始化内存
  • 竞态条件(TOCTOU)
    • sleep(1)在模拟完耗时操作才打开文件
    • 在检查和使用文件之间存在时间窗口
    • 攻击者可在此期间替换文件
    • 应该使用原子操作或者文件描述符
  • 命令注入
    • snprintf(command,sizeof(command)," ",username); system(command); //用户的输入直接拼接到命令中 那么可能就会有恶意代码输入
    • 应使用白名单验证,避免使用 system()
  • 数组越界访问
    • 没有检查输入小标的大小是否在数组的边界中
    • 可能会读取/写入相邻的内存,导致信息泄露或者内存损坏
    • 应检查索引范围 if(index>=0 && index<=5)
  • 越界读操作
    • 由于数组的下标未进行检查 可能读取到相邻内存
    • 就会导致泄露相邻内存中的敏感数据
    • 应该严格的验证索引范围
  • 不安全的内存拷贝
    • memcpy(buffer,input,copy_size) 内存拷贝有问题
    • memcpy 中的size_t(是无符号数) 故如果输入一个负数如-1 那么转化为无符号数的话会为极大值
    • 那么这样就会导致缓冲区溢出
    • 应在转换为无符号前检查size是否为负数
  • 错误的边界检查逻辑
    • 就是判断数组的下标时判断错误
    • c[256] 认为数组下标到达256
    • 应该检查输入的长度是否小于缓冲区大小
  • 未检查的动态内存分配结果
    • 即动态分配内存 malloc但却没有检查内存分配是否分配成功 然后就直接使用该内存
    • 应该检查malloc的返回值是否未NULL
  • 字符串连接溢出
    • 就是字符串连接时未检查被连接的数组的内存大小是否够长
    • strcat会直接连接字符串,不检查目标缓冲区剩余的空间
    • 使用 strncat函数会检查边界
  • 有符号整数作为数组索引
    • 输入数组下标时不能仅仅关注其是不是小于数组最大值的下标,还要关注他是不是下标>=0
    • 如果只关注其中一个就可能造成负数作为数组下标 这样就会访问错误
    • 应该检查索引是否大于等于0且小于MAX_SIZE
  • 不安全的指针运算
    • 定义一个指针 char *ptr=buffer 如果此时进行运算 ptr=ptr+offset offset是任意输入的数字 那么就会产生使指针的偏移量超出数字的范围
    • 这样就会破话其他内存区域
    • 应该验证偏移后的指针是否是在有效范围内
  • 不安全的文件读取
    • 打开文件时是使用文件的相对路径 那么可能就会打开意外的文件 应该使用绝对路径
    • fread读取文件可能会读取超过缓冲区范围的数据
    • 应该验证路径,并且限制读取的大小
  • 格式字符串写入漏洞
    • 格式字符串包含用户输入,这样攻击者就可以在字符串中任意写入格式字符,向任意地址写入数据
    • 修复:不使用用户的输入作为格式字符串
  • 多线程竞态过程
    • 多线程可能同时通过
    • 应该使用互斥锁保护临界区