这篇文章中提到的问题主要是由于调试平台Visual Studio和测试平台Online Judge的一些小差异,造成在Visual Studio中调试通过的代码,在输入OJ时要手动修改函数,后知后觉,可以设置一下编译参数来让Visual Studio尽量使用标准C++库中的函数。这样解决已经能够极大缓解这个矛盾,但是由于最新的Visual Studio 2015的缘故,有些函数完全不被支持了,例如gets()
(后来发觉,这个函数似乎确实不那么好用),这时,会有静态编译错误提示,在Visual Studio中按下F1键进入微软的官方文档查询相关函数的使用说明是最快的解决办法,百度谷歌此时并不会比MSDN更有帮助,当然,更不要像我一样怀疑自己:忘记了这么多?
要看某个函数在标准库中什么位置,这里是个好去处。
遇到这个问题的,估计都是和我一样的入门级选手,不要怀疑自己,一个逻辑,只有自己实现过、调试过,才能真正理解;一个逻辑,只有多犯几次错误并一次次修正,才能真正掌握。
最初的想法
typedef struct {
int no; //【错误】学号是整型最好,但这是你简化问题的想法,看样例中学号似乎不是整型
char name[10];
char gender[2];
int age;
} info;
info data[1000];
int query[10000];
int biSearch(info data[], int length, int queryNo);
int main()
{
int n, m;
scanf_s("%d", &n);
for (int i = 0; i < n; i++) //【错误】过度简化问题,样例中的学号是升序的,不代表所有样例都是升序输入的。应该考虑将输入的数据排序。
{ //【错误】如果升序输入了学号,就不要查找了,直接放入info[]中,直接使用下标就可以访问相应元素。
scanf_s("%d %s %s %d", &data[i].no, &data[i].name, &data[i].gender, &data[i].age);
}
scanf_s("%d", &m);
for (int i = 0; i < m; i++)
{
scanf_s("%d", &query[i]);
}
return 0;
}
int biSearch(info data[], int length, int queryNo)
{
int start = 0;
int end = length - 1;
int middle = (start + end) / 2;
while (start < end)
{
if (middle < queryNo) //【错误】根据上面分析,学号为字符串,这里的比较就是错误的,于是改为if (middle < (int)queryNo),仍然【错误】
{
start = middle;
}
if (middle > queryNo)
{
end = middle;
}
if (middle == queryNo)
{
return middle; //【错误】写到这里感觉有些奇怪,这里的几个分支似乎是没有意义的,因为做了一个过度简化:输入的学号是升序的,见上面注释。
}
}
}
调试中遇到的问题
scanf_s
的使用- 在Visual Studio中,为加强安全性,使用
scanf_s
代替scanf
,更安全的版本在输入字符串时,有讲究。 - 若按照
scanf_s("%s%s", buf1, buf2);
使用,会提示错误:C4477,“scanf_s”: 格式字符串“%s”需要类型“int”的参数,但可变参数 2 拥有了类型“char *”
。 - 原因是,没有为存储字符串的变量指定其长度。
- 指定了格式
%s
后,可变参数中对应一个字符串指针char buf[10]
,后面跟一个存储该字符串的变量的长度,可以使用_countof(buf)
来获取该变量的长度。 - 这样就要对字符串的长度做仔细的考虑,比如这题中有个字符串存放性别,
char gender[2]
会引起错误,因为汉语的男或女占用2B,这里的长度刚刚够存放字符串而没有空间存放结束符\0
,于是出现错误。char gender[5]
就可以正确保存。
- 在Visual Studio中,为加强安全性,使用
- 从文件中读取
fscanf_s()
- 调试过程肯定要多次启动程序,不想每次都输入非常长的测试用例,于是想把测试用例存入文件,将文件注入标准输入流。
- 因此尝试了在Visual Studio中使用
fscanf_s
,由于安全原因,在Visual Studio中使用fscanf
与其他环境中也不太一样。参数类型不一样。 - 【TODO】需要好好读一下MSDN,了解这个用法。
FILE* fp;
errno_t err;
err = fopen_s(&fp, "student", "r"); //在标准C++中,第一个参数直接使用FILE*即可,这里要使用FILE**
if (err == 0) //fopen_s返回0则说明读取文件正确
{
...
}
else
{
printf_s("无法读取文件");
}
- 在控制台直接粘贴
- 后来发现,与上面从文件读取相比,还是直接在控制台右键粘贴测试用例更方便。
- 唯一遇到的小问题是,粘贴时,最后一行没有换行符,调试时到了这里就不动了,也没有错误提示。
- 是什么问题卡住了?很简单——到了最后一组测试数据的最后一行时,按下Enter键将换行符补充上即可!
scanf
和scanf_s
- 在VS中总提示
scanf
不安全,代码调试好后在OJ上验证,又要把所有的scanf_s
换成scanf
。 - 所谓的不安全,是
scanf
不会对字符串的长度做检查,有可能一直从控制台的缓存中读数据而超出字符串的长度限制,自己注意这一点就是安全的。 - 避免这种繁琐的步骤,在VS的编译选项中添加
\D _CRT_SECURE_NO_WARNINGS
,“右击工程-属性-配置属性-C/C++-命令行”,从而在VS中也使用标准的函数scanf
。
- 在VS中总提示
- Visual Studio中一个奇怪的错误
- 在
stdio.h
中出现了一个错误???!!!错误代码是_CRT_END_C_HEADER
,错误提示是此类型没有存储类或类型说明符
,这是什么问题???
- 在
- OJ上遇到的问题
- 总提示错误,添加空格,修改输出方式,都没用。
- 结果原因是,存放学号的字符数组太小了!
char no[50]
不够,修改为cha no[100]
就通过了。 - 测试样例会很大,不要太小气,内存要求很宽裕时,尽量使用大数组!
编程实践中需要改进的地方
- 小心使用强制类型转换
char queryNo[50]
对这个字符串强制转换为整型,会出现什么结果?将得到一个非常大的整数,因为这会把50B的长度截取4B然后转换???- 因此
if (middle < (int)queryNo)
将整型的middle和这个转换来的未知的整数比较大小,会得到错误的结果。 - 更好的做法是,是使用
strcmp()
,将字符串形式的学号直接使用该函数比较。
- 字符串数组
- 有一个问题,每读入一个查询学号,就输出一个学生的信息。样例中是把所有的学生信息放在一起输出。(后来发现,无需这样做!)
- 因此,想将待查询的学号存入一个字符串数组,这里出现较大问题。字符串一直都没有掌握好,总出现内存越界的问题。
_countof()
是计算一个数组的元素个数的,如果是二维数组,将返回行数。若希望得到二维数组的列数(即字符串长度),就引入sizeof()
,二者配合得出。- 在
scanf_s("%s", ..., sizeof(array) / _countof(array))
中指定字符串长度的参数的正好就是字符串数组中每个字符串的长度。
char query[10000][50];
scanf_s("%s", query[i], 50);
/*_countof(query));起初想用VS中的宏来获得字符串的长度
*这里使用`_countof(query)`无法计算出字符串数组的单个元素的长度,会出现编译错误,因此采用硬编码,将50直接作为`scanf_s`的参数。
*后来自己测试了一下宏`_countof()`究竟是什么含义:_countof()计算一个数组的元素的个数
*/
#include <stdio.h>
#include <stdlib.h> //_countof()在这里定义
int main()
{
char array[10][30];
printf_s("%d", sizeof(array) / _countof(array));
return 0;
}
完整代码
// 机试指南-例2.10.cpp : 二分查找。
//
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
struct info {
char no[50];
char name[20];
char gender[5];
int age;
bool operator < (const info & A) const
{
return strcmp(no, A.no) < 0; //【学习】使用strcmp
}
} buf[1000];
char query[10000][50];
int biSearch(int length, char queryNo[]);
int main()
{
int n, m;
scanf_s("%d", &n);
for (int i = 0; i < n; i++)
{
scanf_s("%s%s%s%d", buf[i].no, _countof(buf[i].no),
buf[i].name, _countof(buf[i].name),
buf[i].gender, _countof(buf[i].gender),
&buf[i].age);
} //【错误】VS中scanf输入字符串与标准C++不太一样,多一个参数来指定字符串的长度
sort(buf, buf + n);
scanf_s("%d", &m);
//输入要查询的学号
for (int i = 0; i < m; i++)
{
scanf_s("%s", query[i], 50);//, _countof(query)); //【疑问】保存字符串数组,如何指定存入何处???不是地址的问题,是scanf_s需要字符串的长度
}
for (int j = 0; j < m; j++)
{
int rank = biSearch(n, query[j]);
if (rank == -1)
{
printf_s("No Answer!\n");
}
else
{
printf_s("%s %s %s %d\n", buf[rank].no, buf[rank].name, buf[rank].gender, buf[rank].age);
}
}
/*
*while (m-- != 0) //每输入一个学号,二分查找一次;得到的输出与样例不符
*{
* char query[50];
* //scanf_s("%s", query, _countof(query));
* int rank = biSearch(n, query);
* if (rank == -1)
* {
* printf_s("No Answer!\n");
* }
* else
* {
* printf_s("%s %s %s %d\n", buf[rank].no, buf[rank].name, buf[rank].gender, buf[rank].age);
* }
*}
*/
return 0;
}
//二分查找,返回找到的目标所在的下标
int biSearch(int length, char queryNo[])
{
int start = 0;
int end = length - 1;
int middle = (start + end) / 2;
while (start <= end) //【错误】这里必须有等号!
{
if (strcmp(buf[middle].no, queryNo) < 0) //【错误】使用库函数strcmp。起初二分的逻辑搞错了。
{
start = middle + 1;
middle = (start + end) / 2;
}
else if (strcmp(buf[middle].no, queryNo) > 0)
{
end = middle - 1;
middle = (start + end) / 2;
}
else
{
return middle;
}
}
return -1;
}