你好,我是雨乐!
相信很多人跟我一样,在学了C++基本语法之后,对于需要有换行的需求,无脑使用std::endl
,这个习惯一直沿用至今,大概有十几年了吧,虽然知道这个有性能上的缺陷,但因为习惯使然或者本着存在即合理
的原则,还是有意无意中使用。
std::endl
std::endl
是 C++ 标准库中的一个 manipulator(操作符),用于向输出流插入一个换行符,并刷新输出流。它的定义位于 <iostream>
头文件中。
具体来说,std::endl
的作用是将当前缓冲区中的内容输出到设备(例如终端、文件等),然后在输出流中插入一个换行符,并强制刷新缓冲区,确保输出内容立即显示在设备上。这个换行符的具体形式取决于操作系统,通常是 '\n'
。
使用 std::endl
的优点是它确保输出的立即可见,并且可以在输出流中插入一个换行符,使得代码更加清晰易读。
上面说过,使用std::endl来输出换行并刷新缓冲区,即:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
相当于:
#include <iostream>
int main() {
std::cout << "Hello, World!" << '\n' << std::flush;
return 0;
}
其实,源码实现远比我们上述这个复杂的多。
gcc中对std::endl的实现如下:
template<typename _CharT, typename _Traits>
basic_ostream<_CharT, _Traits>&
endl(basic_ostream<_CharT, _Traits>& __os)
{ return flush(__os.put(__os.widen('\n'))); }
flush()
是刷新缓冲区,如果缓冲区中有数据的话,则显示在终端或者其他外接设备上。
除了上面的flush()操作,endl的具体实现依赖于编译:
•扩展换行符(如上的widen
操作),改代码:•获取与流关联的当前区域设置(std::ios_base::getloc
)•使用此区域设置的“facet”(通过调用 std::use_facet)来执行可能的扩展•检查 std::has_facet
widen声明如下:
template<typename CharT, typename Traits>
CharT std::basic_ios< CharT, Traits >::widen ( char c ) const;
在上面,提到了locale和facet
两个概念,这个是关于本地化,而facet则是对本地化提供的一种支持,这块内容较多,理解起来比较抽象,可以查看相关资料。
从以上可以看出,std::endl不仅仅是刷新缓冲区这么简单,里面的操作涉及到其它很多方面,所以有时候为了我们直观上的显示,使用std::endl做了很多我们用不到的需求,所以,如果仅仅只是刷新缓冲区的话,可以考虑另一种方案。
备用
可以使用\n
来替代std::endl
:
void fun(std::ostream &os) {
os << "Hello world!\n";
}
如果需要刷新缓冲区,则可以:
void fun(std::ostream &os) {
os << "Hello world!\n" << std::endl;
}
对比
既然本文的主题是使用\n来替代std::endl
,那么就得给出详实的理由,测试用例如下:
#include <iostream>
#include <chrono>
int main() {
int num = 1000;
auto start_endl = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num; ++i) {
std::cout << "Hello" << std::endl;
}
auto fin_endl = std::chrono::high_resolution_clock::now();
auto duration_endl = std::chrono::duration_cast<std::chrono::microseconds>(fin_endl - start_endl);
auto start_newline = std::chrono::high_resolution_clock::now();
for (int i = 0; i < num; ++i) {
std::cout << "Hello \n";
}
auto fin_newline = std::chrono::high_resolution_clock::now();
auto duration_newline = std::chrono::duration_cast<std::chrono::microseconds>(fin_newline - start_newline);
std::cout << "Time with 'endl': " << duration_endl.count() << " microseconds\n";
std::cout << "Time with '\\n': " << duration_newline.count() << " microseconds\n";
return 0;
}
输出如下:
// ...
Time with 'endl': 1977 microseconds
Time with '\n': 44 microseconds
从上述输出可以看出,使用endl的耗时是使用\n的40多倍。
如果从汇编的角度分析下面这块代码:
void UseEndl() {
std::cout << "Hello world!" << std::endl;
}
void UseNewline() {
std::cout << "Hello world!\n";
}
其汇编输出如下:
UseEndl():
push rbx
mov edx, 12
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
mov rax, QWORD PTR std::cout[rip]
mov rax, QWORD PTR [rax-24]
mov rbx, QWORD PTR std::cout[rax+240]
test rbx, rbx
je .L10
cmp BYTE PTR [rbx+56], 0
je .L5
movsx esi, BYTE PTR [rbx+67]
.L6:
mov edi, OFFSET FLAT:std::cout
call std::basic_ostream<char, std::char_traits<char> >::put(char)
pop rbx
mov rdi, rax
jmp std::basic_ostream<char, std::char_traits<char> >::flush()
.L5:
mov rdi, rbx
call std::ctype<char>::_M_widen_init() const
mov rax, QWORD PTR [rbx]
mov esi, 10
mov rax, QWORD PTR [rax+48]
cmp rax, OFFSET FLAT:_ZNKSt5ctypeIcE8do_widenEc
je .L6
mov rdi, rbx
call rax
movsx esi, al
jmp .L6
.L10:
call std::__throw_bad_cast()
.LC1:
.string "Hello world!\n"
UseNewline():
mov edx, 13
mov esi, OFFSET FLAT:.LC1
mov edi, OFFSET FLAT:std::cout
jmp std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
_GLOBAL__sub_I_UseEndl():
sub rsp, 8
mov edi, OFFSET FLAT:std::__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:std::__ioinit
mov edi, OFFSET FLAT:std::ios_base::Init::~Init() [complete object destructor]
add rsp, 8
jmp __cxa_atexit
我们不必去挨个分析汇编语句,单单从汇编行数来看,UseEndl生成了35行汇编语句,而UseNewline生成了13行汇编语句
以上~~
如果对本文有异议或者有其他技术问题,可以加微信交流: