快读快写学习笔记
0x01 前置准备
所有代码依赖以下头文件,建议统一包含:
- <cstdio>:提供- getchar()、- putchar()、- fread()、- fwrite();
- <iostream>:提供- cin、- cout;
- <cctype>:提供- isspace();
0x02 基础 I/O 优化:基于 cin 和 cout
优化步骤
- 关闭流同步:
- 实现:通过 ios::sync_with_stdio(false)关闭 C++ 和 C 输入输出流的同步;
- 解释:为了确保混用 C++ 的 cin/cout和 C 的printf/scanf不会产生 I/O 混乱,C++ 和 C 的两种流之间进行了同步。这提高了兼容性,但是产生了大常数。关闭流同步之后就不要同时使用cin和scanf,也不要同时使用cout和printf,否则会造成 I/O 混乱。但可以同时使用cin和printf,也可以同时使用scanf和cout;
- 解除绑定:
- 实现:通过 cin.tie(nullptr)解除cin与cout的绑定;
- 解释:在 C++ 中,cin默认绑定的是&cout,这意味着每次读入都会调用flush()。可以用cin.tie(nullptr)函数解除这种绑定;
- 针对 endl的优化:
- 实现:用 '\n'替换endl;
- 解释:endl的作用是换行并刷新缓冲区,相当于cout<<'\n'<<flush。而刷新缓冲区会带来一定开销;
其中前两步一般合称「关流」。后文会沿用这个称呼。
代码实现
以下两种写法是等价的:
// 写法1:链式调用
cin.tie(0)->sync_with_stdio(0);
// 写法2:分步调用
ios::sync_with_stdio(0);
cin.tie(0);
可以这样将所有 endl 替换为 '\n':
// 注意:该宏需在包含<iostream>之后定义,避免与std::endl声明冲突
#define endl '\n'
0x03 进阶优化:快读
普通快读:基于 getchar()
通过 getchar() 函数逐字符读取,手动解析整数或字符串。
void read(int &x){  // 读整数(支持负数)
	int c,f=1;
	while((c=getchar())<'0'||c>'9') if(c=='-') f=-1;
	for(x=c^48;(c=getchar())>='0'&&c<='9';x=(x<<3)+(x<<1)+(c^48));
    x*=f;
}
void read(char &c){  // 读一个非空字符
	while(isspace(c=getchar()));
}
int read(char s[]){  // 读一个字符串,到空格/EOF为止,返回长度
	int len=0;
    char c;
	while(isspace(c=getchar()));
    do s[len++]=c;
    while(!isspace(c=getchar())&&c!=EOF);
    s[len]='\0';  // 补字符串结束符
    return len;
}
int getline(char s[]){  // 读一行字符串,返回长度
    int len=0;
    char c;
    while((c=getchar())!='\n'&&c!=EOF) s[len++]=c;
    s[len]='\0';  // 补字符串结束符
    return len;
}
缓冲区快读:基于 fread()
getchar() 每次从系统读取 1 个字符,频繁调用系统接口,开销大。fread() 一次性读取一整块数据到自定义缓冲区,后续从缓冲区取字符。这样可以减少系统调用次数,速度通常可以提升 5~10 倍。
缓冲区一般大小设为 1MB 左右,即 \(2^{20}\) 字节,这样既不会占太大空间,不会刷新太多次缓冲区。
由于 fread 可以一次整块读入,因此速度比 getchar 快多了。
char in[1<<20],*p1,*p2;
inline char gc(){  // 从缓冲区读1个字符,空则补充
	return p1==p2&&(p2=(p1=in)+fread(in,1,1<<20,stdin))==in?EOF:*p1++;
}
加上这段代码,然后用 gc() 替换掉所有 getchar() 就可以了。
0x04 进阶优化:快写
普通快写:基于 putchar()
通过 putchar() 函数逐字符输出。
void write(int x){  // 写整数(支持负数)
	if(x<0) putchar('-'),x=-x;
	x<10?putchar(x|48):(write(x/10),putchar(x%10|48));
}
void write(char s[],int len){  // 写字符串,指定长度
	for(int i=0;i<len;++i) putchar(s[i]);
}
void write(char s[]){  // 写字符串,直到'\0'为止
	for(int i=0;s[i];++i) putchar(s[i]);
}
缓冲区快写:基于 fwrite()
和缓冲区快读差不多,自定义一个缓冲区,每次写一个字符到缓冲区,满了就刷新缓冲区,通过 fwrite() 一次性将整个缓冲区里的内容输出。
加上如下代码,再用 pc() 替换掉所有 putchar() 就可以了。
char out[1<<20],*p3=out;
inline void pc(char c){  // 向缓冲区写1个字符,满则刷新
	if(p3-out==1<<20) fwrite(out,1,1<<20,stdout),p3=out;
	*p3++=c;
}
但是程序结束时,缓冲区里可能还有东西,因此我们必须在结束前清空缓冲区。这一步千万不要忘!
fwrite(out,1,p3-out,stdout);
0x05 工程化实现:I/O 类封装
封装的核心目的
- 自动管理缓冲区:析构函数自动调用 fwrite()刷新输出缓冲区,避免忘记刷新;
- 统一接口:将快读和快写整合到同一个类中,使用时直接调用 io.函数名(参数)即可,无需再关注底层实现,适合作为缺省源使用;
代码实现
class IO{
    #define SIZE 1<<20
    private:
        char in[SIZE],out[SIZE],*p1,*p2,*p3;
    public:
        IO():p1(in),p2(in),p3(out){}
        ~IO(){fwrite(out,1,p3-out,stdout);}
        inline char gc(){  // 从缓冲区读1个字符,空则补充
            return p1==p2&&(p2=(p1=in)+fread(in,1,SIZE,stdin))==in?EOF:*p1++;
        }
        inline void pc(char c){  // 向缓冲区写1个字符,满则刷新
            if(p3-out==SIZE) fwrite(out,1,SIZE,stdout),p3=out;
            *p3++=c;
        }
        void read(int &x){  // 读整数(支持负数)
            int c,f=1;
            while((c=gc())<'0'||c>'9') if(c=='-') f=-1;
            for(x=c^48;(c=gc())>='0'&&c<='9';x=(x<<3)+(x<<1)+(c^48));
            x*=f;
        }
        void read(char &c){  // 读一个非空字符
            while(isspace(c=gc()));
        }
        int read(char s[]){  // 读一个字符串,到空格/EOF为止,返回长度
            int len=0;
            char c;
            while(isspace(c=gc()));
            do s[len++]=c;
            while(!isspace(c=gc())&&c!=EOF);
            s[len]='\0';  // 补字符串结束符
            return len;
        }
        int getline(char s[]){  // 读一行字符串,返回长度
            int len=0;
            char c;
            while((c=gc())!='\n'&&c!=EOF) s[len++]=c;
            s[len]='\0';  // 补字符串结束符
            return len;
        }
        void write(int x){  // 写整数(支持负数)
            if(x<0) pc('-'),x=-x;
            x<10?pc(x|48):(write(x/10),pc(x%10|48));
        }
        void write(char s[],int len){  // 写字符串,指定长度
            for(int i=0;i<len;++i) pc(s[i]);
        }
        void write(char s[]){  // 写字符串,直到'\0'为止
            for(int i=0;s[i];++i) pc(s[i]);
        }
    #undef SIZE
}io;  // 全局实例化,无需重复创建对象
0x06 关键避坑指南
- 混用不同 I/O 方式:
- 关流后不能混用 cin和scanf,也不能混用cout和printf;
- 缓冲区快读与 getchar()不可混用,缓冲区快写和putchar()也不能混用;
 
- 关流后不能混用 
- 缓冲区快写忘记刷新:非封装版本需在程序结束前调用 fwrite(out,1,p3-out,stdout),否则缓冲区剩余数据不会输出;
- 整数快读快写处理边界值:处理 INT_MIN即 \(-2147483648\) 时会溢出,若需支持要开long long;
0x07 性能对比与选型建议
| I/O 方式 | 速度排序 | 优点 | 缺点 | 适用场景 | 
|---|---|---|---|---|
| 缓冲区快读快写 | 1 | 速度极致,适合大数据 | 代码长,需封装,不支持复杂类型(如浮点数) | 输入超大,高频 I/O 的题目 | 
| 普通快读快写 | 2 | 代码短,速度快 | 不支持复杂类型(如浮点数) | 输入较大,需要卡常的题目 | 
| 关流 cin/cout | 3 | 代码超短 | 兼容性差 | 一般题目 | 
| scanf/printf | 4 | 适合输出格式串 | 格式控制符复杂,速度较慢 | 一般题目 | 
| 不关流 cin/cout | 5 | 兼容性好 | 速度极慢 | 极小数据量的题目,调试输出 | 
注:若需支持浮点数(如
double),需扩展快读快写函数,手动解析小数点前后数字。因浮点数 I/O 场景较少,通常含有浮点数的题目数据量也不会太大,本文暂不展开,可根据需求自行扩展。
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号