一、cin,cout解锁
1.1:cin,cout解锁以及why
首先cin和cout是在c++中为了提供类型安全和易用性设计的,它兼容了c语言的输入和输出,以上几点导致它在性能行(读取和输出速度)远不如传统c语言的输入和输出。
在看到一些代码里面,会在main
函数开头加上这两行代码,叫做对cin
和cout
解锁,使用之后确实能对性能有一定的提升:
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
🚨注意:cin cout解锁使用时,不能与 scanf,getchar, printf,cin.getline()混用,一定要注意,会出错。
我们来看看这两行代码其实是在做什么,但在这之前,让我们先学习一下底层的一些知识:输入输出缓冲区。
二:C++输入输出缓冲区
ios::sync_with_stdio(false);
首先,上文提到过,由于cin、cout
是兼容了C语言的输入和输出,这意味着C++的IO标准库和C的IO标准库是同步的,这能保证可以在程序中混用cin、cout
和scanf、printf
。但是这种同步带来的代价是,在用户进行输入和输出时,这两个库之间会协调同一片共享缓冲区,保证程序能按顺序的执行,这种协调会导致资源的消耗。这个代码相当于关闭了这种协调机制,减少了这部分的资源消耗,但是同时你也就不能混用两种输入输出方式!
cin.tie(0),cout.tie(0)
: 首先我们需要明白,在C++的输入和输出过程中有两个独立的缓冲区:输入缓冲区、输出缓冲区。
- 输入缓冲区:当我们用标准输入从外部设备(如键盘or硬盘)向程序(也就是内存)输入数据时,数据不会立马跑到程序里被程序的变量所接收从而处理,而是先到内存的输入缓冲区,等到
cin
函数进行缓冲区数据读取时,数据才会从缓冲区这个中间站流向内存。 - 输出缓冲区: 它和输入缓冲区的性质是一样的,区别在于它数据的流向:从内存流向设备(硬盘、显示屏);和
cin
输入数据一样,当我们用cout
将数据从程序(内存)往外部设备输出时,它不会直接输出,而是先将数据送到缓冲区(打住,cout
的作用就是这个),当某些条件触发(如:缓冲区满了、缓冲区被刷新),才会将在缓冲区的数据送到外部设备。
而C++默认cin
和cout
是绑定的,也就是说,当我们用cin
输入数据时,它会自动提前刷新输出缓冲区,以确保将之前就被拿出来的数据能输出。而这种绑定机制可能会频繁无意义的多次刷新缓冲区,造成资源浪费。而cin.tie(0),cout.tie(0)
的作用就是解除这种绑定机制,谁也不用等谁。但是这样带来的问题是:在进行输入之前,必须手动刷新缓冲区,否则前面的提示信息还没显示,后面的输入先来了。
关于上面的手动刷新缓冲区,意思就是说,你需要手动把cout
从内存拿到缓冲区的数据输出到屏幕上,否则它就一直在在缓冲区,除非手动(<<flush或<<endl
)刷新才能显示。这里举个例子:
int main(){ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);int n = 0;cout << "请输入棋盘(nxn)的大小:n = ";cin >> n;return 0;
}
这个代码,按理来说,应该是先出现提示信息,再进行输入n
,但是实际运行结果是:
我们看到,是先输入,再输出了本该提前输出的提示信息。这是因为我们对cin
和cout
进行解绑后,cout
不再具有在cin
运行前自动刷新输出缓冲区(意味着将缓冲区的数据输出到屏幕)的功能,所以其实在输入2
之前,cout
已经将提升信息从程序(内存)拿到了缓冲区,但是没有将其刷新送到屏幕,而是在程序结束后不得不刷新缓冲区。
所以,在对cin
和cout
进行解绑后,一定要记得手动刷新缓冲区(flush
或endl
):
cout << "请输入棋盘(nxn)的大小:n = " << flush;
cin >> n;
cout << "请输入棋盘(nxn)的大小:n = " << endl;
cin >> n;
三、C的标准输入
虽然在某些情况下,使用cin
和cout
解绑后能提升性能,但是肯定还是没有用传统的c语言的输入输出效率和性能高。所以我打算法竞赛的朋友也建议我用传统的输入输出方法,下面也在这里做一个简单的总结:
3.1:scanf和printf
-
定义:按照特定格式从stdin读取输入。
-
用法示例:
char str[100]; int a; scanf("%s %d", str, &a); // 注意,传入的一定是变量的地址
-
从缓冲区读取数据流程:
- 缓冲区开头:读取并丢弃空白字符(包括空格、Tab、换行符),直到第一个非空白字符才认为是第一个数据的开始。
- 缓冲区中间:开始读取第一个数据后,一旦遇到空白字符(非换行符), 就认为读取完毕一次。遇到的空白字符不读取残留在缓冲区,直到下一次被读取或刷新。例如输入字符串hello yyz,则会被认为是2个字符串。
- 缓冲区末尾:按下回车键时,换行符\n残留在缓冲区。换行符之前的空格可以认为是中间的空白字符,处理同上。
char c[100]; // 分配长度为100的字符数组scanf("%s", c); // 不需要使用&,因为数组名已经是地址printf("%s", c);
-
格式控制符说明:
注意,%c是一个比较特殊的格式符号,它将会读取所有空白字符,包括缓冲区开头的空格、Tab、换行符,使用时要特别注意。
3.2:字符和字符串(char [])
3.2.1:fgetc() & getc()读取字符
-
定义:从指定输入流读取一个字符,输入可以是
stdin
,也可以是文件流 ,使用时需要显式指定。这两个函数完全等效,getc()由fgetc()宏定义而来。不同的是,gets()和fgets()相互之间没有关系。
fgetc()和getc()对应的输出函数是
fputc()
和putc()
-
用法示例:
char a, b; a = fgetc(stdin); b = getc(stdin);
在下面代码中,如果输入一个字符按下回车,那么
a
读取到的是第一个字符,而b
读取到的是紧跟着的回车char a, b; a = fgetc(stdin); // 从标准输入读取一个字符 b = getc(stdin); // 再从标准输入读取另一个字符fputc(a, stdout); // 输出字符a到标准输出 putc(b, stdout); // 输出字符b到标准输出
-
读取数据流程:
所有空格、Tab、换行等空白字符,无论在缓冲区开头、中间还是结尾,均会被读取,不丢弃也不会残留在缓冲区。
如果输入一个字符如’a’,然后按下回车键,则读取到的是字符’a’,同时换行符\n
残留在缓冲区。
因为只读取一个字符,所以如果输入多于1个字符(包括换行符),则它们均会残留在缓冲区。具体地说,如果什么字符都不输入,直接按下回车键,则读取到的是换行符\n,缓冲区无任何残留;
3.2.2:getchar()读取字符
-
定义:从stdin读取一个字符。
getchar()实际上也由fgetc()宏定义而来,只是默认输入流为stdin。( 它和前面两个唯一不同的就是不需要显示指定输入流)
getchar()对应的输出函数是
putchar()
。 -
用法示例:
char a; a = getchar();
getchar()常常用于清理缓冲区开头残留的换行符。当知道缓冲区开头有
\n
残留时,可以调用getchar()但不赋值给任何变量,即可实现冲刷掉\n的效果。
3.2.3:字符串读入——fgets()
-
定义:
char *fgets(char *str, int n, FILE *stream)
从指定输入流读取一行,输入可以是stdin,也可以是文件流,使用时需要显式指定, 并把它存储在 str 所指向的字符串内。当读取(n-1)
个字符时,或者读取到换行符\n
时,或者到达文件末尾时,它会停止读取,并在末尾补上\0
。str
– 这是指向一个字符数组的指针,该数组存储了要读取的字符串。n
– 这是要读取的最大字符数(包括最后的空字符\0
)。通常是使用以 str 传递的数组长度。(当输入的字符小于最大个数n-1
时,会读取到\n
并停止,\n
会被读取到,不会丢弃,和普通字符地位一样)stream
– 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
-
读取文件流示例:
char str[100];
memset(str, 0, sizeof(str));
int i = 1;FILE *fp = fopen("...test.txt", "r");
if (fp == NULL) {printf("File open Error!\n");exit(1);
}while (fgets(str, sizeof(str), fp) != NULL)printf("line%d [len %d]: %s", i++, strlen(str), str);fclose(fp);
- 读取stdin示例:
char str[100];
memset(str, 0, sizeof(str));
int i = 1;
while (fgets(str, sizeof(str), stdin) != NULL)printf("line%d [len %d]: %s", i++, strlen(str), str);
- 读取数据流程:
- 所有空格、Tab等空白字符均被读取,不忽略。
- 按下回车键时,缓冲区末尾的换行符也被读取,字符串末尾将有一个换行符\n作为字符串的一部分
- fgets()函数会自动在字符串末尾加上
\0
结束符。所以当输入字符串hello,再按下回车,则读到的字符串长度为6(hello + \n),字符数组大小为7(因为包括了\0) - 第 2 个参数n指定了读取的最大长度(算上\0的)。函数读到
n-1
个字符(包括换行符\n)就会停止,并在末尾加上\0结束符。剩余字符将残留在缓冲区。
四、C++标准输入
4.1:cin和cout
-
定义:cin是 C++ 的标准输入流对象,即istream类的一个对象实例。cin有自己的缓冲区,但默认情况下是与stdin同步的,因此在 C++ 中可以混用 C++ 和 C 风格的输入输出(在不手动取消同步的情况下)。
cin与stdin一样是行缓冲,即遇到换行符时才会将数据同步到输入缓冲区。
-
读取数据流程:cin对空白字符的处理与scanf一致。即:跳过开头空白字符,遇到空白字符停止读取,且空白字符(包括换行符)残留在缓冲区。
🚨注意:所以
cin
用来读取字符串是读取不到空白字符的!!!字符串string
的读取需要用到下面专门读取字符串的函数!!!!
4.2:字符串读取
4.2.1:给C类型字符串(字符数组 char [])赋值
1:cin.get()
- 定义:
- 读取单个或指定长度的字符,包括空白字符。
- 当使用无参数时,它读取并返回下一个字符
- 也可以与一个参数一起使用(字符的引用),用来读取一个字符,包括换行符
\n
。 - 当与两个参数一起使用时(字符数组和长度)
std::cin.get(buffer, SIZE);
- 它会读取至多
SIZE - 1
个字符到 buffer 中(SIZE 是数组大小的参数)。这是因为最后一个位置需要留给空字符(null character,'\0'
),这是 C 风格字符串的结束标志。 - 如果在读取
SIZE - 1
个字符之前遇到了文件结束符(EOF,通常由用户输入 Ctrl+D 或 Ctrl+Z 产生),或者遇到了换行符'\n'
,std::cin.get() 就会停止读取。 - 如果在读取
SIZE - 1
个字符之前遇到了换行符'\n'
,遇到换行符\n
,随之停止读取;换行符\n
留在输入缓冲区
- 它会读取至多
- 用法示例:
#include <iostream>int main() {const int SIZE = 10;char buffer[SIZE];std::cout << "Enter some text: ";std::cin.get(buffer, SIZE);// 注意:手动添加 null 字符来确保字符串正确终止buffer[SIZE - 1] = '\0';std::cout << "You entered: " << buffer << std::endl;// 如果需要清理输入流if (std::cin.peek() == '\n') {// 如果下一个字符是换行符,只需忽略它std::cin.ignore();} else {// 清除剩余的输入直到下一个换行符std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');}return 0;
}
cin.get()
读取单个字符时,类似于 C 中的fgetc()
,对空白字符的处理也与其一致,可以读取回车\n
。cin.get()
读取的字符也可以赋值给整型变量。cin.get()
读取指定长度个字符时,类似于 C 中的fgets()
,但在换行符的处理上不同:fgets()
会将缓冲区末尾的换行符\n
也写入字符串,而cin.get()
遇到缓冲区末尾的\n
,立即停止读入,\n
会被残留在缓冲区。即:当输入test
时,用fgets()
读取得到的字符串长度为5(把\n读
入),用cin.get()读取得到的字符串长度为4。
2.cin.getline()
- 定义:读取指定长度的字符,包括空白字符。
- 用法示例:
char str[20]; cin.getline(str, sizeof(str)); // 第3个参数也可以指定终止字符
cin.getline()
与cin.get()
指定读取长度时的用法几乎一样。
唯一区别在于,cin.getline()
读取到\n
会停止读取,但不会将\0
追加到字符串末尾而是直接将其丢弃,所以不同于cin.get()
,cin.getline()
不会把\n
残留在输入缓冲区。- 如果输入的字符个数大于指定的最大长度
n-1
(不含终止符),cin.get()
会使余下字符残留在缓冲区,等待下次读取;而cin.getline()
会给输入流设为Fail
状态,在主动恢复之前,无法再进行正常输入。
4.2.2:给string类型赋值:getline()
-
定义:
getline()
并不是标准输入流istream
的函数,而是字符串流sstream
的函数,只能用于读取数据给string类对象,使用时也需要包含头文件<string>
。如果使用
getline()
读取标准输入流的数据,需要显式指定输入流。 -
用法示例:
int n; string s; cin >> n; getchar(); //cin.get() 清空前面cin留下的换行符,避免下面读到空字符串 getline(cin, s);//可正确读入下一行的输入
-
和前面
cin.get()
函数不同的是,它是 遇到换行符\n
读取并丢弃,随之停止读取;换行符\n
既不加到字符串末尾,也不会留在输入缓冲区(这点和cin.getline()
是一样的) -
需要注意的是,假如缓冲区开头就是换行符(比如可能是上一次cin残留的),则getline()会直接读取到空字符串并结束,不会给键盘输入的机会。所以这种情况下要注意先清除开头的换行符。
五、C&C++的输入方法总结
首先在C语言中
scanf
用来格式化读取各种基本数据(但是遇到空比字符则视为读入的终止标志而停止,所以不可用来读取带有空格的字符串)getchar()
和fgets()
用来读取字符(可读白字符:如空格、换行),区别仅在于getchar()
默认是stdin
输入流,不用显示传参- 在读取字符串时(C语言格式的字符数组
char []
),使用fgets
读入,其会读取到换行符\0
便终止读取,并把\0
算入字符串的一个字符加入到字符串末尾,并且自动在字符串末尾加上\0
。
在C++中
cin
可以用来读取各种基本数据,和scanf
一样即:跳过开头空白字符,遇到空白字符停止读取,且空白字符(包括换行符)残留在缓冲区。所以不可用来直接读取带有空格的字符串cin.get()
、cin.getline()
用法方面类似,都是对传统C形式字符串即字符数组char []
的读取(cin.get()在读取字符时和putchar()用法一致)。二者而和fgets()
不同的是,这两者都不会将\0
作为字符串的一部分追加到字符串末尾;但cin.get()
的做法是直接将\0
残留在了缓冲区,而cin.getline()
是将其读取并且丢弃,输入缓冲区不会残留。同时它们也会自动给字符数组末尾追加\0
,即仅从输入流中读取n-1
个字符,字符数组末尾会用'\0'
补充- getline()是专门对
string
这种字符串类型变量的读取,它对换行符的处理和上面的cin.getline()
一致。