第五次C博客作业
Q0.展示PTA总分
Q1.本章学习总结
1.1 学习内容总结
- 指针做循环变量做法
- 指针作为一个循环变量时,经常伴随着一个数组,指针又指向该数组的首地址,我们常常采用的是地址法
- 有时候也会直接根据对指针里的内容进行判断决定是否继续循环,通常会出现在指向字符串的指针中(且常与fgets()函数一同出现)
int a[10];
int* p = a; //使指针p指向数组a的首地址
for(; p < a + 10; p++) //这里的a其实也是地址,p的地址不能越出a
{
...
}
char str[80];
fgets(str,80,stdin);
char* s = str;
while(*p && *p != '
') //循环条件直接使用p指针的内容
{
...
p++;
}
-
可以看出,指针作为循环变量时,不论循环条件是什么,通常都需要地址的自增
-
有时候,我们会遇到函数中传入的是指针,而函数中又要根据指针写循环,这时候我们常常会再定义一个新的指针等于传入的指针,再用这个新的指针作为循环变量,而不是直接使用传入的指针,这样子可以防止需要对传入的指针的内容进行处理时导致意外错误
-
字符指针表示字符串、指针数组及其应用
- 字符指针表示一维的字符串较为简单,不再赘述
- 字符指针数组(如char* pstr[10])对应着二维的字符数组,常在输入多个字符串、单个字符串长度未知的情况下使用它
- 要注意的是,在VS2017之后的版本,已经不能像课本中那样直接对字符指针进行赋值了(如char* p[5]={"a","bb",...,"sss"}),如果一定想这么写,只能将他的类型改为const char*,但这也意味着它不能再被修改了
- 在有字符串的编程中,我们更倾向使用字符指针,举个例子,我们用二维数组来存放字符串,但有一个字符串长度为1,又有一个字符串长度为10000,那我们在定义这个二维数组时就得设置它的列长为10000,但这样的设置对长度为1的字符串在空间上造成了极大的浪费
- 用一张图看一下字符指针数组和二维数组表示字符串,指针数组最常见的应用也是在字符串方面了
- 同样的,普通的数字数组也可以用指针数组进行替换,这部分与行指针较为类似
-
动态内存分配
- 这些函数都需要头文件stdlib.h
- 同时,由于这些函数都是void类型,但我们的变量没有这种类型啊!所以在使用时需要进行强制类型转换
- 举个栗子,我们将会有n个int类型的数要传给int类型的指针p,那么申请动态内存时需要这样写: p = (int*)malloc(n * sizeof(int));
- 其他动态分配的函数:calloc()、realloc()等,具体见课本第221页
- 当一块空间使用完毕后,要使用free()函数将这块空间释放!
-
二级指针与行指针
- 二级指针,指向的是一级指针的地址,称为指向指针的指针。有几个“*”号就代表是几级指针,目前(我自己)看来二级指针已经足矣,再多级没有必要
- 假设有p2=&*p1,p1=&p,对p2指针进行两次取值(p2或(p2))才能得到p的值
- 行指针实际上就是二级指针运用于二维数组,在c语言中,二维数组实际上是以一维数组为单位连续存储的,可以说二维数组是特殊的一维数组
- 假设有一个行指针int (*p)[5],它指向a[5][5],那么p指的是a[0]一整行,p+1指的是a[1]一整行,以此类推,他们都不表示一个特定的元素
- p[1]+1、*(p+1)+1两者都指的是a[1][1]的地址,其中p[1]、*(p+1)都代表着第1行的首地址,想要取得a[1][1]的值需要对他进行二次取值(如*(*(p+1)+1))
-
指针作为函数返回值
- 指针作为函数返回值返回的是一个地址
- 我们在前面的章节中有学习到变量生存的周期,函数在结束后会销毁在它内部定义的局部变量,但肯定会有同学说:“不对啊!我在PTA上面返回局部变量的地址也能过呀!”下面我们通过一个简单的例子来看看这个问题(摘自这个网站)
#include <stdio.h>
int *func(){
int n = 100;
return &n;
}
int main(){
int *p = func(), n;
//printf("cyuyanhaonanyiwuwuyi
");
n = *p;
printf("value = %d
", n);
return 0;
}
- 在有这行注释的时候运行程序,会得到正确的答案100,但是如果我们把注释号去掉,最终的答案会变得十分奇怪
- 所以在函数的返回值为指针,并且是函数中的局部变量时,一定要立刻使用!!!
-
上文中的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。
来自上面的网站的解释
1.2 本章学习体会
- 指针不愧为C语言的精华,真的很难学,个人感觉指针的概念就比较抽象,离的很远,到现在有的地方还是一知半解
- 刚开始做指针部分的题目的时候真的是一脸懵逼,*号、&号各种乱加乱删……改到VS不报错为止。到后面干脆自暴自弃能不用指针就不用指针(刚开始做2840中指针的题目的时候)
- 但现在经过一段时间的学习后,对指针的部分又有了新的认识,也去尝试着将以前的题进行修改,确实可以得到新的理解
- 本周因为高数小测、线代考试、蓝桥校选还有各种乱七八糟的讲座搞得我这周基本没有什么空闲,也没有更多的时间去打代码了,但是参加了蓝桥校选个人感觉还是意义很大的。首先是第一次非常直观的感受到了自己和其他厉害的人的差距,各种从来没有见过的算法、跳跃的思维、新奇的解答方式,同时自己也学到了一些新东西,感觉接下来的学习又有了新的方向。
还有就是提交列表基本中清一色的C++C++C++C++C++,我的CCCCCC显得格格不入 - 这段时间的代码量统计如下(包含换行等)
Q2.PTA实验作业
2.1 合并两个有序数组
2.1.1 伪代码
定义一个新的指针num,用于存储排列后的数据
定义变量i记录num中的数字个数,j、k分别用于记录a、b中当前是第几个数
为num申请动态内存
while (j+k < m+n) do
如果a中的数都记录完毕,则记录b剩下的数
如果b中的数都记录完毕,则记录a剩下的数
否则将a[j]与b[k]进行判断,记录小的那个数
end while
将num中的内容全部拷贝给a
释放num
2.1.2 代码截图
2.1.3 本题知识点总结
1.用malloc()函数申请动态内存,使用完毕后使用free()函数释放内存
num = (int*)malloc((m + n) * sizeof(int));
free(num);
2.建立新的指针(数组)保存目标内容,对原始内容进行分部分处理
3.使用memcpy()函数将一个数组的内容复制给另一个数组
memcpy(目标数组,要被复制的数组,要复制的大小)
/*注意事项:
1.需要头文件cstring
2.与strcpy不同,它可以复制任何内容*/
2.1.4 PTA提交列表及说明
- 前两个部分正确:50000个数据和100000个数据测试点超时,当时是采用嵌套循环进行遍历,再一个循环将内容还给a,严重超时
- 第三个部分正确:舍弃循环的嵌套和新变量,直接在a中进行更改、移动等操作
- 编译错误:重新构思代码,引入新变量来存储答案,同时更改循环内容,采用分部处理,
最后忘记加头文件了 - 答案正确:加上cstring的头文件就过了
2.2 说反话-加强版
2.2.1 伪代码
定义ch数组储存输入的字符串
定义变量i、n稍后用于遍历字符串
定义变量m稍后用于处理字符串
输入字符串
令n为字符串长度减一
处理字符串后面的空格
while n>0 do
让i等于n,从后往前遍历字符串,直到遇到空格或i小于0
令m等于单词尾部和空格之间的长度
for ++i to n do
输出字符ch[i]
end for
n -= m;
处理单词间的空格(遇到一个空格n就减1)
if 空格处理完毕后n仍然大于0 then
输出一个空格
end if
end while
2.2.2 代码截图
2.2.3 本题知识点总结
1.strlen()函数记录字符串的长度
2.遇到可能前后、中间都有多空格的情况要对空格进行单独处理
2.2.4 PTA提交列表及说明
- 编译错误:在VS中使用gets_s,复制到PTA中忘记改成gets
- 内部错误:PTA爆炸,与我无瓜
- 部分正确:全空格测试点过不了,测试后发现我的代码执行后还会输出一个空格,于是增加并修改了处理空格部分的代码
- 答案正确:修改后全部通过
2.3 删除字符串中的子串
2.3.1 伪代码
定义父串ch1,字串ch2
定义变量i作为循环条件
定义变量j、k用于遍历父串子串
读入父串
读入子串
for i=0 to strlen(ch1) do
if ch1[i]等于字串的第一个字符
判断字符串ch1接下来的字符是否与ch2的相等
if ch2能够读到
for j=i to strlen(ch1) - strlen(ch2)
ch1从第j+strlen(ch2)个开始左移
end for
i = -1;
ch1[j] = ' ';
end if
end if
end for
输出ch1
2.3.2 代码截图
2.3.3 本题知识点总结
1.使用strstr()函数返回父串中首次出现子串的位置(虽然我的代码中没有使用)
2.对字符串进行移动操作后要记得给字符串加上' '
3.可以将父串当前字符和子串当前字符作为循环条件,如果子串能跑到' '就说明父串中存在该子串
2.3.4 PTA提交列表及说明
- 第一、二个部分正确:记得不太清楚了,似乎只有样例和没有可删过了,其余都错,发现是定义数组ch1和ch2的长度时定义错误
- 第三个部分正确:全删空测试点过不了,之前第25行写的是i=0,但这样经过循环后从1开始而不是从0开始,于是改成i=-1
- 内部错误:PTA爆炸!与我无瓜
- 答案正确:修改代码后成功通过
Q3.代码阅读
-
题目说明:
-
参考代码:
#include<iostream>
#include<string>
#include<cstdio>
using namespace std;
const string weeks[]={"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
const string months[]={"January","February","March","April","May","June","July","August","September","October","November","December"};
const int cnt[2][12]=
{
31,28,31,30,31,30,31,31,30,31,30,31,
31,29,31,30,31,30,31,31,30,31,30,31
};
int leapyear(int y)
{
if(y%4==0&&y%100!=0||y%400==0)
return 1;
return 0;
}
int DayOfWeek(int M,int D,int Y)
{
if(M==1||M==2)
{
M+=12;
--Y;
}
if(Y<1752||(Y==1752&&M<9)||(Y==1752&&M==9&&D<3))
return(D+2*M+3*(M+1)/5+Y+Y/4+5)%7;
else
return (D+2*M+3*(M+1)/5+Y+Y/4-Y/100+Y/400)%7;
}
bool Check(int m,int d,int y)
{
if(!(m>=1&&m<13))
return false;
if(!(d>=1 && d<=cnt[leapyear(y)][m-1]))
return false;
if(y==1752&&m==9&&d>2&&d<14)
return false;
return true;
}
int main(void)
{
int m,d,y;
while(cin>>m>>d>>y,m+d+y)
{
if(Check(m,d,y))
printf("%s %d, %d is a %s
",months[m-1].c_str(),d,y,weeks[DayOfWeek(m,d,y)].c_str());
else
printf("%d/%d/%d is an invalid date.
",m,d,y);
}
return 0;
}
- 代码阅读
- 这题题意比较明了,输入月份、日期、年份,如果合法就输出它的日期和星期,直到输入0 0 0
- 他这里计算星期是有公式的,经过查阅后,发现1752年9月2日前后计算星期的公式不同,且因为奇怪的原因英国规定从1752年9月3日到1752年9月13日的11天并不存在(???)
- 该代码在函数封装的方面做的非常好:判断闰年、判断日期合法、计算星期,每个函数的功能和目的都非常清楚,主函数也十分简洁,这部分非常值得我学习
- 运用了bool类型的函数返回true和false代替我们平常的int类型函数返回0和1,也可以学着使用
- 判断日期合法中 if(!(d>=1 && d<=cnt[leapyear(y)][m-1])) 这个写的十分巧妙,相比我之前判断日期合法的代码来说短了一大截,值得参考