输入输出优化

一般来说,使用

cin >> a;
cout << a;

会比

scanf("%d", &a);
printf("%d", a);

要慢
因为前者有很多参数需要配置,检查,所以会慢很多。而后者

_CRTIMP int __cdecl __MINGW_NOTHROW printf (const char*, ...);
_CRTIMP int __cdecl __MINGW_NOTHROW	scanf (const char*, ...);

第一个参数在某些编译器里面叫做format,也就是在这里你已经提供了他的格式。注意这里一定要匹配。如果

int i = -1;
printf("%u", i);

输出的就不是-1。这是因为在printf函数里面,他按照unsigned的方式输出-1,所以得到的结果就不一样


但是这样还是很慢,因为这两个函数都要检查format里面提供的参数。
所以我们开始考虑自己写快读快写
我们目前知道比较简单的输入输出操作就是

_CRTIMP int __cdecl __MINGW_NOTHROW	getchar (void);
_CRTIMP int __cdecl __MINGW_NOTHROW	putchar (int);

那么我们能不能用这两个函数来写快读快写呢?
肯定可以的
每次读入一个字符,如果不是数字字符就跳过,是数字字符就把当前的数乘以10再加上。
简单说来就是这样

int read()
{
  int x = 0, flag = 1;
  char ch = getchar();
  while(ch < '0' or ch > '9')
  {
    if(ch == '-') f = -1;
    else f = 1;
    ch = getchar();
  }
  while(ch >= '0' and ch <= '9')
  {
    // x = (x * 2) + (x * 8) + (ch - 48);
    x = (x << 1) + (x << 3) + (ch ^ 48);
    ch = getchar();
  }
  return x * flag;
}

同样的,我们也能写出输出。注意这里逆序输出推荐使用递归

void write(const int x)
{
  if(x < 0)
  {
    putchar('-'); write(-x);
  }
  if(x > 9) write(x / 10);
  putchar((x % 10) ^ 48);
}

至于为什么是按位异或48……自己试一下0^48, 1^48, ... , 9^48, 48^48, 49^48 ... 55^48的结果你就知道了。


注意:下面介绍的方法在输入量较小的时候反而效率更低
但是这样还不够快!
频繁调用

_CRTIMP int __cdecl __MINGW_NOTHROW	getchar (void);
_CRTIMP int __cdecl __MINGW_NOTHROW	putchar (int);

会多次打开缓存输入,造成效率低下
我们就想
能不能把输入数据一次性读入进来,保存在一个数组里面,直接访问数组元素呢?
比如说

char pI[], *Ip1, *Ip2;
char getchar()
{
  if(Ip1 == Ip2)
  {
    pass();
    Ip1 = pI;
  }
  if(Ip1 == Ip2) return -1;
  return (*Ip1++)
}

这里pI数组是读入的数据,Ip1是当前读入到的位置(指针),Ip2是整个pI的尾指针,如果Ip1==Ip2就说明当前的读入到头了,要不重新读入,要不返回EOF(-1)
pass()函数是用来处理读入数据的

思路讲清楚了,现在看看具体怎么实现
现在告诉你,stdio.h库里面有

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fread (void*, size_t, size_t, FILE*);

的作用是从FILE* 里面读入size_t个大小为size_t的字符保存到void*中,返回size_t作为成功读入的长度
比如说我用

int a = fread(pI, 1, 128, stdin);

读入"123\26",这里'\26'是结束的意思,相当于Ctrl+Z,这样就读入了4个字符,分别是'1', '2', '3', ''(我也不知道为什么是这个)。
注意使用这个读入的话'\n'也算一个字符
库里面还有

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fwrite (const void*, size_t, size_t, FILE*);

是把const void里面的size_t个大小为size_t的内容写入到FILE里面
比如

char a[10] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'h', 'h'};
fwrite(a, 1, 10, stdout);

输出的就是abcdefghhh
然后回到我们之前的代码

char pI[], *Ip1, *Ip2;
char getchar()
{
  if(Ip1 == Ip2)
  {
    pass();
    Ip1 = pI;
  }
  if(Ip1 == Ip2) return -1;
  return (*Ip1++)
}

怎么实现pass()函数呢?
初始化的时候,Ip1=Ip2=0, 使用fread()读入,更新Ip2的位置,每次返回*Ip1并且Ip1++
然后我们又知道C++里面指针是可以加减整数的
所以我们就可以

#define fastIO
#ifdef fastIO
#define IOMxn 1001
char pI[IOMxn], *Ip1, *Ip2;
#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
#undef IOMXn
#endif

其实最主要就是这一行

#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))

每次用getchar()都会变成((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
那就让我们看看他是怎么工作的
首先我们要知道程序是很懒的,当你用&&, ||连接两个布尔变量的时候

bool A, B, C, D;
C = A && B;
D = A || B;

如果A为假,那么B无论真还是假C都为假,所以C就是假,程序根本不看B是啥
同理,如果A为真,那么D肯定为真,所以程序根本不看B是啥
那么你会说:这不是很正常嘛
但是如果

bool fun1()
{
  cout << "三千世间疾," << endl;
  return false;
}
bool fun2()
{
  cout << "相思最难医。" << endl;
}
void main()
{
  if(fun1() && fun2());
  return ;
}

这个时候虽然两个函数都返回bool类型,但是这两个函数还有其他事情要干啊
这个时候系统调用fun1()发现是假,那么他就不会调用fun2(),所以fun2()的返回值我都懒得写了(我和程序一样懒)
不信的话去试试?程序只会输出第一句。
虽然有时候这个很麻烦,但是我们也不妨利用他的这个性质。
在讲正文之前,还有一个前置知识要告诉你们的
你们写某些题,是不是说读入以0结束
然后是怎么读入的?

while(scanf("%d", &n), n);

当时可能有很多小伙伴就在问,这个逗号是来干嘛的。
现在我告诉你,语句

int n = (fun1(), fun2());

的作用就是先执行fun1(),然后把fun2()的返回值赋给n
可以试试这段代码

int fun1(int & a)
{
  a = 20;
  return 100;
}
void main()
{
  int n = 10;
  cout << (fun1(n), n) << endl;
  return ;
}

就会发现我讲的是对的

回到正文

((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))

这里用的是三目运算符 ?:, 我在这里写清楚一点

(
  (Ip1 == Ip2) 
  && 
  (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)
  ?

 -1 : *(Ip1++))

这里有很多个括号。如果担心的话其实还可以多加一点,毕竟#define是直接替换,如果和外面某些东西发生了冲突就不好了
如果检测发现Ip1 == Ip2为假,也就是现在pI数组还没读入完,所以不管&&后面发生了什么,整个?前面的表达式就是假,三目表达式的值就是:后面的*(Ip1++),就是返回*Ip1然后Ip1++
但是如果Ip1 == Ip2,也就是说现在pI数组读入完了。我们也知道有些数据很大,不能全部搬到程序当中,每次只能搬IOMxn个,具体多少个根据实际自行定义。
如果搬完了Mxn个还有数据,就等待下一次搬运吧……
现在发现pI数组读入完了,那么就重新读入,首先Ip1=pI重新指向整个数组的开头,然后fread()读入并返回个数
然后C++指针可以加减整数,如果

int a[];
cout << *(a + i);
cout << a[i];

第二行和第三行输出结果一样。因为第二行得到了a[i]的地址,然后使用*转换为整形,就是a[i]
所以我们得到一个惊天动地的结论:(a + i) == & a[i] \((i \in \N)\), 计算机访问a[i]实际上就是访问*(a + i), 有时候指针飘走了,才会发生运行错误
所以我们Ip2就可以通过pI + fread(pI, 1, IOMxn, stdin)得到
如果得到了Ip2之后,也就是重新读入之后,Ip1还是等于Ip2,就说明遇到了文件末尾EOF,那么我们也返回EOF好了。
否则的话,和前面一样返回*(Ip1++)
然后有人就要问了:如果我是文件输入输出怎么办?
别急,还记不记得fread的函数原型

_CRTIMP size_t __cdecl __MINGW_NOTHROW	fread (void*, size_t, size_t, FILE*);

里面有一个FILE*参数,当时我们给的是stdin,也就是控制台输入输出。
现在如果要文件输入输出,只需要使用fopen新建一个FILE*参数即可。

FILE* const Ifile = fopen("my.in", "r");
fread(a, 1, IOMxn, Ifile);

这里注意FILE* const中const 的位置。定义一般的常量const随便放,但是指针常量const的位置不同会有不同的意思
比如

int a, b;
const int* pa = &a;
int* const pb = &b;
pa = &b;
*pb = 100;

经常性指针指向的变量是会更改的。如果在*前面加上const只是表明指针指向的是常量,不能通过指针来改变这个量。如下操作是非法的:

*pa = 100;

如果在*后面加上const就是说明这个指针指向的量可以通过指针改变,但是这个指针一生都只能指向这个变量,矢志不渝。如下操作是非法的:

pb = &a;

又因为我们的FILE* Ifile指向输入文件,而且只能(会)指向输入文件。但是输入/输出文件由于读入/写入就会改变,所以需要通过指针改变这个量。最后定义就是

FILE* const Ifile;

现在就来讲一下缓存优化快写
其实和快读差不多,把要写入的字符先储存到数组里面,等数组满了再一次性输出。注意主程序结束之前要输出一次,保证未满的数组中的数据也能全部输出。
这里提醒一下,对于数组中的((char)Op1)==0,虽说这个字符的Ascii码是0,但是使用fwrite输出会把size_t个字符全部输出,(char)0也会当做一个空格输出(迷)

char pO[IOMxn];
char *Op = pO;
#define putchar(a) (((Op - pO == IOMxn) && (fwrite(pO, 1, IOMxn, stdout), Op = pO), *(Op++)) = a)

由于网上没有快写宏定义的代码,这是手敲的,肯定慢(逃)
解释一下,由于快写不是快读,所以只需要一个数组一个指针就够了。指针说明现在填充到数组的第几位。
又因为C++指针可以加减,所以Op - pO == IOMxn,当然也可以写成Op - pO == sizeof(pO),就说明数组现在已经满了,需要执行后面的代码,输出之后重新把指针指向数组开头。
然后就向*Op填充数据,然后Op++


总结下来,快读快写的代码就下面这几行:

#define fastIO
#ifdef fastIO
#define IOMxn 1 << 20
char pI[IOMxn], *Ip1, *Ip2;
#define getchar() ((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, 1, IOMxn, stdin), Ip1 == Ip2)? -1 : *(Ip1++))
char pO[IOMxn];
char *Op = pO;
#define putchar(a) (((Op - pO == IOMxn) && (fwrite(pO, 1, IOMxn, stdout), Op = pO), *(Op++)) = a)
#undef IOMXn
#endif

int main()
{
  ///...
  fwrite(pO, 1, Op - pO, stdout);
  return 0;
}

P.S.:如果发现不需要输入直接结束程序的情况,不妨调小IOMxn的值试试。

让我们先去Luogu试一下效果
时间上根据网上的资料肯定是快了的
看看正确性
好家伙
525ms变成了479ms
都看到这里了
最后贴一个可以直接复制拿来用的模板代码

#include <cstdio>

#define fastIO
#ifdef fastIO
#define IOMxn 1 << 10
char pI[IOMxn], *Ip1, *Ip2;
char pO[IOMxn], *Op = pO;
#define getchar() \
(((Ip1 == Ip2) && (Ip2 = (Ip1 = pI) + fread(pI, sizeof(pI[0]), sizeof(pI), stdin), Ip1 == Ip2)? EOF : *(Ip1++)))
#define putchar(a) \
((Op - pO) == sizeof(pO) && (fwrite(pO, sizeof(pO[0]), Op - pO, stdout), Op = pO), *(Op++) = a)
#endif

template <typename T> inline T read(T* const = 0);
template <typename T> void write(T);
template <typename T> inline void read_list(int, ...);

int main()
{
    
#ifdef fastIO
    fwrite(pO, sizeof(pO[0]), Op - pO, stdout);
#endif
    return 0;
}

template <typename T>
inline T read(T* const ptr)
{
    T x = 0;
    register char ch = getchar();
    register int f = 1;
    while(ch < '0' or ch > '9')
    {
        f = ch == '-'? -1 : 1;
        ch = getchar();
    }
    while(ch >= '0' and ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= f;
    if(ptr) *ptr = x;
    return x;
}

template <typename T>
void write(T x)
{
    if(x < 0) putchar('-'), x = -x;
    if(x > 9) write(x / 10);
    putchar((x % 10) ^ 48);
    return ;
}

template <typename T>
inline void read_list(int argc, ...)
{
    T** ptr = (T**)((&argc) + 1);
    while(argc--)
        read(*(ptr++));
    return ;
}

其中

template <typename T> inline void read_list(int, ...);

可以一次性读入多个相同类型变量,第一个参数表示读入的个数,写错了就完蛋了
(但是需要传递参数类型)
比如说这样用

read_list<int>(3, &a, &b, &c);

结构体封装

update 20211123
曾经在网上看见一个很NB的写法
这里解析一下大概思路:(因为煮熟的代码飞了。。。我也忘了保存去哪了)
首先用一个结构体封装我们上面的pI,pO数组。
然后我们在析构函数里面,释放内存的时候,一次性把pO里面剩下的字符全部输出。
这样就可以不用在主函数里面写输出了。
然后有了结构体我们还能很整洁地实现各种功能。
比如实现及时关闭文件(在析构函数末尾加上fclose(file);就可以了,不需要在主函数里面写)
比如我们还可以定义gets(char,size_t)来把输入流中size_t个字符strncpy到char
(很快啊)
我们还可以用nxt_seek()来探测下一个字符(毕竟有时候普通的快读是读到非数字字符就退出。但是如果这个非数字字符我要用。。。就很尴尬)
上面的功能据说cin是可以实现的。。。
具体实现方法。。。请读者自行思考。如果实在想不明白可以文末留言,我就会勉为其难鞠躬尽瘁写个代码出来
正所谓

即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难

posted @ 2021-03-16 13:57  IdanSuce  阅读(171)  评论(0编辑  收藏  举报