L
O
A
D
I
N
G

奇怪的循环(C++)分析


一、问题发现

最近写代码遇到个很神奇的循环,然后就研究了一下。下面是具体的代码:

出现奇怪循环的代码
#include <bits/stdc++.h> using namespace std; int main(int argc, char* argv[]) { char s[255]; cin >> s; cout << "s=" << s << cin.rdbuf(); return 0; }

这段程序运行后的具体表现为:当用户输入字符串“第一次输入”并回车时,控制台会立即输出字符串“s=第一次输入”。然后,程序会要求用户再次进行输入。当用户输入字符串“第二次输入”并回车时,控制台会立即输出字符串“第二次输入”。接着,程序依然会要求用户进行输入。后面就是以此类推,不断重复这个过程,从而陷入无限循环的输入输出过程

图1.1 程序运行截图

二、问题研究

为了研究这个奇怪的现象,于是我打算从这段代码对应的汇编代码开始研究,于是我使用了 Complier Explorer 网站对这段代码基于 GCC 4.9.2 进行了汇编,得到的结果如下:

Complier Explorer 汇编结果
.LC0: .string "s=" main: push rbp mov rbp, rsp push rbx sub rsp, 280 mov DWORD PTR [rbp-276], edi mov QWORD PTR [rbp-288], rsi lea rax, [rbp-272] mov rsi, rax mov edi, OFFSET FLAT:_ZSt3cin call std::basic_istream<char, std::char_traits<char> >& std::operator>><char, std::char_traits<char> >(std::basic_istream<char, std::char_traits<char> >&, char*) mov edi, OFFSET FLAT:_ZSt3cin+16 call std::basic_ios<char, std::char_traits<char> >::rdbuf() const mov rbx, rax mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:_ZSt4cout call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) mov rdx, rax lea rax, [rbp-272] mov rsi, rax mov rdi, rdx call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) mov rsi, rbx mov rdi, rax call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_streambuf<char, std::char_traits<char> >*) mov eax, 0 add rsp, 280 pop rbx pop rbp ret __static_initialization_and_destruction_0(int, int): push rbp mov rbp, rsp sub rsp, 16 mov DWORD PTR [rbp-4], edi mov DWORD PTR [rbp-8], esi cmp DWORD PTR [rbp-4], 1 jne .L3 cmp DWORD PTR [rbp-8], 65535 jne .L3 mov edi, OFFSET FLAT:_ZStL8__ioinit call std::ios_base::Init::Init() [complete object constructor] mov edx, OFFSET FLAT:__dso_handle mov esi, OFFSET FLAT:_ZStL8__ioinit mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev call __cxa_atexit .L3: leave ret _GLOBAL__sub_I_main: push rbp mov rbp, rsp mov esi, 65535 mov edi, 1 call __static_initialization_and_destruction_0(int, int) pop rbp ret

对于我这样汇编水平一般般的来说,看懂汇编可不是一件轻松的事情。为加快研究速度(真的很想知道为什么),所以这里借用了一下 chatGPT 3.5 对这段汇编代码进行了注释。下面是 chatGPT 注释后的内容:

chatGPT 注释后的代码
.LC0: .string "s=" main: push rbp ; 保存栈帧指针 mov rbp, rsp ; 设置栈帧指针 push rbx ; 保存通用寄存器 rbx sub rsp, 280 ; 在栈上分配 280 字节的空间 mov DWORD PTR [rbp-276], edi ; 存储命令行参数 argc mov QWORD PTR [rbp-288], rsi ; 存储命令行参数 argv lea rax, [rbp-272] ; 计算数组 s 的地址 mov rsi, rax ; 将数组 s 的地址存入 rsi 寄存器 mov edi, OFFSET FLAT:_ZSt3cin ; 将输入流对象 cin 的地址存入 edi 寄存器 call std::basic_istream<char, std::char_traits<char> >& std::operator>><char, std::char_traits<char> >(std::basic_istream<char, std::char_traits<char> >&, char*) ; 调用输入运算符重载函数,将输入流中的内容读入数组 s mov edi, OFFSET FLAT:_ZSt3cin+16 ; 获取输入流对象 cin 的 rdbuf 地址 call std::basic_ios<char, std::char_traits<char> >::rdbuf() const ; 调用 rdbuf 函数,获取输入流对象 cin 的缓冲区地址,并将其保存在 rbx 寄存器 mov rbx, rax ; 将输入流对象 cin 的缓冲区地址保存在 rbx 寄存器中 mov esi, OFFSET FLAT:.LC0 ; 将字符串 "s=" 的地址存入 esi 寄存器 mov edi, OFFSET FLAT:_ZSt4cout ; 将输出流对象 cout 的地址存入 edi 寄存器 call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) ; 调用输出运算符重载函数,将字符串 "s=" 输出到标准输出流 mov rdx, rax ; 将输出流对象 cout 的地址保存在 rdx 寄存器 lea rax, [rbp-272] ; 计算数组 s 的地址 mov rsi, rax ; 将数组 s 的地址存入 rsi 寄存器 mov rdi, rdx ; 将输出流对象 cout 的地址存入 rdi 寄存器 call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) ; 调用输出运算符重载函数,将数组 s 的内容输出到标准输出流 mov rsi, rbx ; 将输入流对象 cin 的缓冲区地址存入 rsi 寄存器 mov rdi, rax ; 将输出流对象 cout 的地址存入 rdi 寄存器 call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_streambuf<char, std::char_traits<char> >*) ; 调用输出运算符重载函数,将输入流对象 cin 的缓冲区内容输出到标准输出流 mov eax, 0 ; 设置返回值为 0 add rsp, 280 ; 释放栈上的空间 pop rbx ; 恢复通用寄存器 rbx pop rbp ; 恢复栈帧指针 ret ; 返回 __static_initialization_and_destruction_0(int, int): push rbp ; 保存栈帧指针 mov rbp, rsp ; 设置栈帧指针 sub rsp, 16 ; 在栈上分配 16 字节的空间 mov DWORD PTR [rbp-4], edi ; 存储静态初始化函数的参数 mov DWORD PTR [rbp-8], esi ; 存储静态初始化函数的参数 cmp DWORD PTR [rbp-4], 1 ; 检查参数 argc 是否为 1 jne .L3 ; 如果不是,则跳转到 .L3 cmp DWORD PTR [rbp-8], 65535 ; 检查参数 argv 是否为 65535 jne .L3 ; 如果不是,则跳转到 .L3 mov edi, OFFSET FLAT:_ZStL8__ioinit ; 将输入输出库初始化函数地址存入 edi 寄存器 call std::ios_base::Init::Init() [complete object constructor] ; 调用输入输出库初始化函数 mov edx, OFFSET FLAT:__dso_handle mov esi, OFFSET FLAT:_ZStL8__ioinit mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev call __cxa_atexit ; 注册析构函数,确保在程序退出时调用 .L3: leave ; 恢复栈帧并返回 ret _GLOBAL__sub_I_main: push rbp ; 保存栈帧指针 mov rbp, rsp ; 设置栈帧指针 mov esi, 65535 ; 设置参数值 mov edi, 1 ; 设置参数值 call __static_initialization_and_destruction_0(int, int) ; 调用静态初始化函数 pop rbp ; 恢复栈帧指针 ret ; 返回

可以看出很多都是不相关的代码,实际上只需要关注的代码只有很小一部分。所以,我提取了关键的部分给 chatGPT 进行注释。下面是重新注释后的结果:

关键代码注释结果
;关键代码: ; cin >> s; ; cout << "s=" << s << cin.rdbuf(); ;下面是这段代码对应的汇编及 chatGPT 的详细注释 ; 以下是对上述代码中每一行汇编指令的详细注释: ; 将数组 s 的地址加载到寄存器 rax 中 lea rax, [rbp-272] ; 将数组 s 的地址存储到寄存器 rsi 中,准备作为输入参数传递给输入运算符重载函数 mov rsi, rax ; 将输入流对象 cin 的地址存储到寄存器 edi 中 mov edi, OFFSET FLAT:_ZSt3cin ; 调用输入运算符重载函数,将输入流中的内容读入数组 s call std::basic_istream<char, std::char_traits<char> >& std::operator>><char, std::char_traits<char> >(std::basic_istream<char, std::char_traits<char> >&, char*) ; 获取输入流对象 cin 的 rdbuf 地址并存储到寄存器 rax 中 mov edi, OFFSET FLAT:_ZSt3cin+16 call std::basic_ios<char, std::char_traits<char> >::rdbuf() const ; 将输入流对象 cin 的 rdbuf 地址存储到寄存器 rbx 中 mov rbx, rax ; 将字符串 "s=" 的地址存储到寄存器 esi 中 mov esi, OFFSET FLAT:.LC0 ; 将输出流对象 cout 的地址存储到寄存器 edi 中 mov edi, OFFSET FLAT:_ZSt4cout ; 调用输出运算符重载函数,将字符串 "s=" 输出到标准输出流 call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) ; 将输出流对象 cout 的地址存储到寄存器 rdx 中 mov rdx, rax ; 将数组 s 的地址存储到寄存器 rsi 中 lea rax, [rbp-272] mov rsi, rax ; 将输出流对象 cout 的地址存储到寄存器 rdi 中 mov rdi, rdx ; 调用输出运算符重载函数,将数组 s 的内容输出到标准输出流 call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) ; 将输入流对象 cin 的 rdbuf 地址存储到寄存器 rsi 中 mov rsi, rbx ; 将输出流对象 cout 的地址存储到寄存器 rdi 中 mov rdi, rax ; 调用输出运算符重载函数,将输入流对象 cin 的 rdbuf 地址的内容输出到标准输出流 call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_streambuf<char, std::char_traits<char> >*)

三、幡然醒悟

不知道咋回事,突然顿悟…回头想想又觉得很可笑,这么简单的问题我整的如此复杂。不得不说,在这次分析过程中做了很多“奇怪的分析行为”,也就是无用功。而且没有准确地抓住问题的痛点,稀里糊涂地乱分析一通最后把自己绕进去了。

最后,突然醒悟的时候恨不得抽自己几个大嘴巴子…哈哈哈😂

四、原因分析

下面说一下产生这个现象的具体原因:

其实产生循环的代码精炼出来就是这段:
cout << cin.rdbuf();

我们知道 cin.rdbuf() 函数会返回标准输入流缓冲区对象的指针。也就是说,这段代码的作用相当于把输入流缓冲区的指针直接重定向到输出流,并且还请求输出“输入缓冲区的内容”。然后,输入缓冲区就会等待输入内容。但是,当我们按下回车将内容送到输入缓冲区时,由于输入缓冲区已经被重定向到输出流,cin 还没来得及判断输入过程是否结束(cin 对象读取到回车才会认为一次输入过程结束),输入缓冲区的内容就已经被输出流拿走并输出了(也就是 cin 没拿到数据,一直在等待,实际上它等待的内容已经被拿走了)。

下面对运行结果进行逐步解释:

注意,以下均为我个人理解,不保证绝对正确!

  • 第一次输入:执行 cin >> s;,用户第一次输入字符串“第一次输入”
  • s=第一次输入:执行 cout << "s=" << s,将字符串“第一次输入”保存到变量 s 中,并输出。
  • 第二次输入:执行 << cin.rdbuf();(实际上是 cout << cin.rdbuf();),将输入流缓冲区重定向到输出流。同时请求输出“输入缓冲区”的内容。因为向“输入缓冲区”发送了读取请求,而“输入缓冲区”中此时为空,故而导致了此次的输入请求
  • 第二次输入:当按下回车时,内容直接从“输入缓冲区”被送入“输出缓冲区”,然后交给“输出处理模块”(这里我并不清楚到底谁在处理,就先这么称呼吧!)进行处理并展示到控制台上。
  • 第三次输入:但是,输入并没有被判定结束!因为“输入处理模块”(这里我也并不清楚到底谁在处理,就先这么称呼吧!)并未收到任何内容,也就并不认为输入结束了,所以还在等待输入内容(就是在等“输入缓冲区”的内容被送过来,但是实际上已经被送给“输出缓冲区”了)。
  • 第三次输入:继续输出。
  • :继续等待输入。
  • :继续输出。
  • 第N次输入:继续等待输入。
  • 第N次输入:继续输出。

从某种意义上说,从第二次输入到后面的每一次输入都是同一次输入,因为这次输入并没有结束


文章作者: SeaYJ
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 SeaYJ !
评论
  目录