高中物理公式大全求解 第一问mg+N=m2v²/r怎么列的? 为什么不是N-mg=mv²/r

优化方案2014高中物理必修二第七章章末过关检测(Word有详解答案)-学路网-学习路上 有我相伴
优化方案2014高中物理必修二第七章章末过关检测(Word有详解答案)
来源:gkstk &责任编辑:小易 &时间: 4:51:18
=================相关文字版=================(时间:60分钟,满分:100分)
一、选择题(本题共8小题,每小题6分,共48分.在每小题给出的四个选项中,1~6小题只有一个选项正确,7~8小题有多个选项正确.全部选对的得6分,选对但不全的得3分,有错选或不答的得0分)
1.如图所示,一辆玩具小车静止在光滑的水平导轨上,一个小球用细绳挂在车上,由图中位置无初速度释放,则小球在下摆的过程中,下列说法正确的是(  )
A.绳的拉力对小球不做功
B.绳的拉力对小球做正功
C.小球的合力不做功
D.绳的拉力对小球做负功
解析:选D.从能量转化的角度判断.在小球向下摆动的过程中,小车的动能增加;小球和小车组成的系统机械能守恒,小车的机械能增加,小球的机械能一定减少,所以绳的拉力对小球做负功.
2.运动员跳伞将经历加速下降和减速下降两个过程.将人和伞看成一个系统,在这两个过程中,下列说法正确的是(  )
A.阻力对系统始终做负功
B.系统受到的合外力始终向下
C.重力做功使系统的重力势能增加
D.任意相等的时间内重力做的功相等
解析:选A.无论什么情况下,阻力一定做负功,A正确;加速下降时,合力向下,减速下降时,合力向上,B错误;系统下降,重力做正功,所以重力势能减少,C错误;由于系统做变速运动,系统在相等的时间内下落的高度不同,所以在任意相等时间内重力做的功不同,D错误.
3.如图所示,两个完全相同的小球A、B,在同一高度处以相同大小的初速度v0分别水平抛出和竖直向上抛出,下列说法正确的是(  )
A.两小球落地时的速度相同
B.两小球落地时,重力的瞬时功率相同
C.从开始运动至落地,重力对两小球做功相同
D.从开始运动至落地,重力对两小球做功的平均功率相同
解析:选C.根据机械能守恒定律或动能定理,可以判断出它们落地时的速度大小相等,但是A球落地时的速度在水平方向和竖直方向上存在分速度,即速度方向与竖直方向存在夹角,而B球落地时的速度方向竖直向下,可见,它们落地时的速度方向不同,A错误;它们质量相等,而B球落地时沿竖直方向的速度大小大于A球落地时沿竖直方向上的分速度的大小,所以两小球落地时,重力的瞬时功率不同,B错误;重力做功与路径无关,只与初末位置的高度有关,所以,从开始运动至落地,重力对两小球做功相同,C正确;从开始运动至落地,重力对两小球做功相同,但做功的时间不同,所以重力做功的平均功率不同,D错误.
4.如图所示,均匀长直木板长l=40 cm,放在水平桌面上,它的右端与桌边相齐,木板质量m=2 kg,与桌面间动摩擦因数μ=0.2,今用水平推力F将其推下桌子,则水平推力至少做功为(g取10 m/s2)(  )
解析:选A.将木板推下桌子时木块的重心要通过桌子边缘,水平推力至少等于滑动摩擦力,所以W=Fs=μmg=0.2×20× J=0.8 J.
(2012?江苏卷)如图所示,细线的一端固定于O点,另一端系一小球.在水平拉力作用下,小球以恒定速率在竖直平面内由A点运动到B点.在此过程中拉力的瞬时功率变化情况是(  )
A.逐渐增大
B.逐渐减小
C.先增大,后减小
D.先减小,后增大
解析:选A.因小球速率不变,所以小球以O点为圆心做匀速圆周运动.受力如图所示,因此在切线方向上应有:mgsin θ=Fcos θ,F=mgtan θ.则拉力F的瞬时功率P=F?vcos θ=mgv?sin θ.从A运动到B的过程中,拉力的瞬时功率随θ的增大而增大.A项正确.
6.质量为2 t的汽车,发动机的牵引功率为30 kW,在水平公路上,能达到的最大速度为15 m/s,当汽车的速度为10 m/s 时的加速度为(  )
A.0.5 m/s2
C.1.5 m/s2
解析:选A.当汽车达到最大速度时,即为汽车牵引力等于阻力时,则有
P=Fv=Ffvm,Ff== N=2×103 N,
当v=10 m/s,F== N=3×103 N,
所以a== m/s2=0.5 m/s2.
7.(2013?上饶高一检测)质量为m1、m2的两物体,静止在光滑的水平面上,质量为m的人站在m1上用恒力F拉绳子,经过一段时间后,两物体的速度大小分别为v1和v2,位移分别为s1和s2,如图所示.则这段时间内此人所做的功的大小等于(  )
B.F(s1+s2)
C.m2v+(m+m1)v
解析:选BC.人做的功等于绳子对人和m2做的功之和,即W=Fs1+Fs2=F(s1+s2),A错误,B正确.根据动能定理知,人做的功等于人、m1和m2动能的增加量,所以W=(m1+m)v+m2v,C正确,D错误.
8.如图所示,用竖直向下的恒力F通过跨过光滑定滑轮的细线拉动光滑水平面上的物体,物体沿水平面移动过程中经过A、B、C三点,设AB=BC,物体经过A、B、C三点时的动能分别为EkA、EkB、EkC,则它们间的关系一定是(  )
A.EkB-EkA=EkC-EkB
B.EkB-EkA<EkC-EkB
C.EkB-EkA>EkC-EkB
D.EkC<2EkB
解析:选CD.绳对物体做的功W=F?cos α?l,由于AB=BC,Fcos α逐渐减小,故WAB>WBC,由动能定理得:EkB-EkA>EkC-EkB,故A、B错误,C正确,由上式得EkC<2EkB-EkA<2EkB,故D正确.
二、实验题(本题共2小题,共14分.按题目要求作答)
9.(4分)如图所示,在“探究动能定理”的实验中,关于橡皮筋做的功,下列说法中正确的是________.
A.橡皮筋做的功可以直接测量
B.通过增加橡皮筋的条数可以使橡皮筋对小车做的功成整数倍增加
C.橡皮筋在小车运动的全过程中始终做功
D.把橡皮筋拉伸为原来的两倍,橡皮筋做功也增加为原来的两倍
解析:橡皮筋的功等于橡皮筋所释放的弹性势能,但无法直接测量,橡皮筋的条数成倍增加,弹性势能也会成倍增加,即做功成整数倍增加,但橡皮筋只是在释放弹性势能的一段时间内才做功,故A、C错误,B正确;橡皮筋的弹性势能与形变量的平方成正比,当拉伸为原来的两倍时,功变为原来的4倍,故D错误.
10.(10分)在用打点计时器验证机械能守恒定律的实验中,使质量为m=1 kg的重物自由下落,打点计时器在纸带上打出一系列的点,选取一条符合实验要求的纸带如图所示.O为第一个点,A、B、C为从合适位置开始选取的三个连续点(其他点未画出).已知打点计时器每隔0.02 s打一个点,当地的重力加速度为g=9.80 m/s2.那么:
(1)纸带的________(填“左”或“右”)端与重物相连;
(2)根据图中所得的数据,应取图中O点到________点来验证机械能守恒定律;
(3)从O点到(2)问中所取的点,重物重力势能的减少量ΔEp=________ J,动能增加量ΔEk=________ J.(结果保留三位有效数字)
解析:因O点为打出的第一个点,所以O端(即左端)与重物相连,用OB段验证机械能守恒定律,则重力势能的减少量为ΔEp=mghOB=1.88 J
动能的增加量为
ΔEk=mv-0
由以上两式得ΔEk=1.87 J.
答案:(1)左 (2)B (3)1.88 1.87
三、计算题(本题共2小题,共38分.解答应写出必要的文字说明、方程式和重要的演算步骤,只写出最后答案的不能得分.有数值计算的题,答案中必须明确写出数值和单位)
11.(9分)如图所示为半径R=0.50 m 的四分之一圆弧轨道,底端距水平地面的高度h=0.45 m.一质量m=1.0 kg的小滑块从圆弧轨道顶端A由静止释放,到达轨道底端B点的速度v=2.0 m/s.忽略空气阻力.取g=10 m/s2.求:
(1)小滑块在圆弧轨道底端B点受到的支持力大小FN;
(2)小滑块由A到B的过程中,克服摩擦力所做的功W;
(3)小滑块落地点与B点的水平距离x.
解析:(1)滑块在B点时,根据牛顿第二定律得:
FN-mg=m(2分)
解得:FN=18 N.(1分)
(2)根据动能定理,mgR-W=mv2(2分)
解得:W=3.0 J.(1分)
(3)小滑块从B点开始做平抛运动
水平方向:x=vt(1分)
竖直方向:h=gt2(1分)
解得:x=v?=0.60 m.(1分)
答案:(1)18 N (2)3.0 J (3)0.6 m
12.(13分)如图甲所示,在水平路段AB上有一质量为2×103 kg的汽车,正以10 m/s的速度向右匀速行驶,汽车前方的水平路段BC较粗糙,汽车通过整个ABC路段的v-t图象如图乙所示,在t=20 s时汽车到达C点,运动过程中汽车发动机的输出功率保持不变.假设汽车在AB路段上运动时所受的恒定阻力(含地面摩擦力和空气阻力等)Ff1=2 000 N.求:
(1)汽车运动过程中发动机的输出功率P;
(2)汽车速度减至8 m/s时加速度a的大小;
(3)BC路段的长度.(解题时将汽车看成质点)
解析:(1)汽车在AB路段时,牵引力和阻力相等
F1=Ff1,P=F1v1
联立解得:P=20 kW.(3分)
(2)t=15 s后汽车处于匀速运动状态,有
F2=Ff2,P=F2v2,Ff2=P/v2
联立解得:Ff2=4 000 N(3分)
v=8 m/s时汽车在做减速运动,有
Ff2-F=ma,F=P/v
解得a=0.75 m/s2.(2分)
(3)Pt-Ff2s=mv-mv(3分)
解得s=93.75 m.(2分)
答案:(1)20 kW (2)0.75 m/s2 (3)93.75 m
13.(16分)(2013?高考浙江卷)山谷中有三块石头和一根不可伸长的轻质青藤,其示意图如下.图中A、B、C、D均为石头的边缘点,O为青藤的固定点,h1=1.8 m,h2=4.0 m,x1=4.8 m,x2=8.0 m.开始时,质量分别为M=10 kg和m=2 kg的大、小两只滇金丝猴分别位于左边和中间的石
头上,当大猴发现小猴将受到伤害时,迅速从左边石头的A点水平跳至中间石头.大猴抱起小猴跑到C点,抓住青藤下端,荡到右边石头上的D点,此时速度恰好为零.运动过程中猴子均可看成质点,空气阻力不计,重力加速度g=10 m/s2.求:
(1)大猴从A点水平跳离时速度的最小值;
(2)猴子抓住青藤荡起时的速度大小;
(3)猴子荡起时,青藤对猴子的拉力大小.
解析:猴子先做平抛运动,后做圆周运动,两运动过程机械能均守恒.寻求力的关系时要考虑牛顿第二定律.
(1)设猴子从A点水平跳离时速度的最小值为vmin,根据平抛运动规律,有
h1=gt2(2分)
x1=vmint(2分)
联立、式,得
vmin=8 m/s.(1分)
(2)猴子抓住青藤后的运动过程中机械能守恒,设荡起时速度为vC,有
(M+m)gh2=(M+m)v(3分)
vC== m/s≈9 m/s.(2分)
(3)设拉力为F,青藤的长度为L.在最低点,由牛顿第二定律得
F-(M+m)g=(M+m)(2分)
由几何关系
(L-h2)2+x=L2(1分)
得:L=10 m(1分)
综合、、式并代入数据解得:
F=(M+m)g+(M+m)=216 N.(2分)
答案:(1)8http://www.xue63.com/===================相关推荐===================
本文相关:
- Copyright & 2018 www.xue63.com All Rights Reserved陈硕 (giantchen_AT_gmail)& 陈硕关于 C++ 工程实践的系列文章: 排版正常的版本: 陈硕博客文章合集下载: 本作品采用&#8220;Creative Commons 署名-非商业性使用-禁止演绎 3.0 Unported 许可协议(cc by-nc-nd)&#8221;进行许可。 前一篇文章谈了值语义,这篇文章谈一谈与之密切相关的数据抽象(data abstraction)。 文章结构:
什么是数据抽象?它与面向对象有何区别?
数据抽象所需的语言设施
数据抽象的例子
什么是数据抽象 数据抽象(data abstraction)是与面向对象(object-oriented)并列的一种编程范式(programming paradigm)。说&#8220;数据抽象&#8221;或许显得陌生,它的另外一个名字&#8220;抽象数据类型/abstract data type/ADT&#8221;想必如雷贯耳。 &#8220;支持数据抽象&#8221;一直是C++语言的设计目标,Bjarne Stroustrup 在他的《The C++ Programming Language》第二版(1991年出版)中写道[2nd]: The C++ programming language is designed to
be a better C
support data abstraction
support object-oriented programming 这本书第三版(1997年出版)[3rd] 增加了一条: C++ is a general-purpose programming language with a bias towards systems programming that
is a better C,
supports data abstraction,
supports object-oriented programming, and
supports generic programming. 在
可以找到 C++ 的早期文献,其中有一篇 Bjarne Stroustrup 在 1984 年写的 《Data Abstraction in C++》
。在这个页面还能找到 Bjarne 写的关于 C++ 操作符重载和复数运算的文章,作为数据抽象的详解与范例。可见 C++ 早期是以数据抽象为卖点的,支持数据抽象是C++相对于C的一大优势。 作为语言的设计者,Bjarne 把数据抽象作为C++的四个子语言之一。这个观点不是普遍接受的,比如作为语言的使用者,Scott Meyers 在《Effective C++ 第三版》中把 C++ 分为四个子语言:C、Object-Oriented C++、Template C++、STL。在 Scott Meyers 的分类法中,就没有出现数据抽象,而是归入了 object-oriented C++。 & 那么到底什么是数据抽象? 简单的说,数据抽象是用来描述数据结构的。数据抽象就是 ADT。一个 ADT 主要表现为它支持的一些操作,比方说 stack.push、stack.pop,这些操作应该具有明确的时间和空间复杂度。另外,一个 ADT 可以隐藏其实现细节,比方说 stack 既可以用动态数组实现,又可以用链表实现。 按照这个定义,数据抽象和基于对象(object-based)很像,那么它们的区别在哪里?语义不同。ADT 通常是值语义,而 object-based 是对象语言。(这两种语义的定义见前文《C++ 工程实践(8):值语义》)。ADT class 是可以拷贝的,拷贝之后的 instance 与原 instance 脱离关系。 比方说 a.push(10); stack b = b.pop(); 这时候 a 里仍然有元素 10。 & C++ 标准库中的数据抽象 C++ 标准库里& complex&& 、pair&&、vector&&、list&&、map&&、set&&、string、stack、queue 都是数据抽象的例子。vector 是动态数组,它的主要操作有 push_back()、size()、begin()、end() 等等,这些操作不仅含义清晰,而且计算复杂度都是常数。类似的,list 是链表,map 是有序关联数组,set 是有序集合、stack 是 FILO 栈、queue是 FIFO 队列。&#8220;动态数组&#8221;、&#8220;链表&#8221;、&#8220;有序集合&#8221;、&#8220;关联数组&#8221;、&#8220;栈&#8221;、&#8220;队列&#8221;都是定义明确(操作、复杂度)的抽象数据类型。 & 数据抽象与面向对象的区别 本文把 data abstraction、object-based、object-oriented 视为三个编程范式。这种细致的分类或许有助于理解区分它们之间的差别。 庸俗地讲,面向对象(object-oriented)有三大特征:封装、继承、多态。而基于对象(object-based)则只有封装,没有继承和多态,即只有具体类,没有抽象接口。它们两个都是对象语义。 面向对象真正核心的思想是消息传递(messaging),&#8220;封装继承多态&#8221;只是表象。这一点孟岩 和王益
都有精彩的论述,陈硕不再赘言。 数据抽象与它们两个的界限在于&#8220;语义&#8221;,数据抽象不是对象语义,而是值语义。比方说 muduo 里的 TcpConnection 和 Buffer 都是具体类,但前者是基于对象的(object-based),而后者是数据抽象。 类似的,muduo::Date、muduo::Timestamp 都是数据抽象。尽管这两个 classes 简单到只有一个 int/long 数据成员,但是它们各自定义了一套操作(operation),并隐藏了内部数据,从而让它从 data aggregation 变成了 data abstraction。 数据抽象是针对&#8220;数据&#8221;的,这意味着 ADT class 应该可以拷贝,只要把数据复制一份就行了。如果一个 class 代表了其他资源(文件、员工、打印机、账号),那么它就是 object-based 或 object-oriented,而不是数据抽象。 ADT class 可以作为 Object-based/object-oriented class 的成员,但反过来不成立,因为这样一来 ADS class 的拷贝就失去意义了。 & 数据抽象所需的语言设施 不是每个语言都支持数据抽象,下面简要列出&#8220;数据抽象&#8221;所需的语言设施。 支持数据聚合 数据聚合 data aggregation,或者 value aggregates。即定义 C-style struct,把有关数据放到同一个 struct 里。FORTRAN77没有这个能力,FORTRAN77 无法实现 ADT。这种数据聚合 struct 是 ADT 的基础,struct List、struct HashTable 等能把链表和哈希表结构的数据放到一起,而不是用几个零散的变量来表示它。 全局函数与重载 例如我定义了 complex,那么我可以同时定义 complex sin(const complex& x); 和 complex exp(const complex& x); 等等全局函数来实现复数的三角函数和指数运算。sin 和 exp 不是 complex 的成员,而是全局函数 double sin(double) 和 double exp(double) 的重载。这样能让 double a = sin(b); 和 complex a = sin(b); 具有相同的代码形式,而不必写成 complex a = b.sin();。 C 语言可以定义全局函数,但是不能与已有的函数重名,也就没有重载。Java 没有全局函数,而且 Math class 是封闭的,并不能往其中添加 sin(Complex)。 成员函数与 private 数据 数据也可以声明为 private,防止外界意外修改。不是每个 ADT 都适合把数据声明为 private,例如 complex、point、pair&& 这样的 ADT 使用 public data 更加合理。 要能够在 struct 里定义操作,而不是只能用全局函数来操作 struct。比方说 vector 有 push_back() 操作,push_back 是 vector 的一部分,它必须直接修改 vector 的 private data members,因此无法定义为全局函数。 这两点其实就是定义 class,现在的语言都能直接支持,C 语言除外。 拷贝控制(copy control)
copy control 是拷贝
stack b = 和赋值
b = 的合称。 当拷贝一个 ADT 时会发生什么?比方说拷贝一个 stack,是不是应该把它的每个元素按值拷贝到新 stack? 如果语言支持显示控制对象的生命期(比方说C++的确定性析构),而 ADT 用到了动态分配的内存,那么 copy control 更为重要,不然如何防止访问已经失效的对象? 由于 C++ class 是值语义,copy control 是实现深拷贝的必要手段。而且 ADT 用到的资源只涉及动态分配的内存,所以深拷贝是可行的。相反,object-based 编程风格中的 class 往往代表某样真实的事物(Employee、Account、File 等等),深拷贝无意义。 C 语言没有 copy control,也没有办法防止拷贝,一切要靠程序员自己小心在意。FILE* 可以随意拷贝,但是只要关闭其中一个 copy,其他 copies 也都失效了,跟空悬指针一般。整个 C 语言对待资源(malloc 得到的内存,open() 打开的文件,socket() 打开的连接)都是这样,用整数或指针来代表(即&#8220;句柄&#8221;)。而整数和指针类型的&#8220;句柄&#8221;是可以随意拷贝的,很容易就造成重复释放、遗漏释放、使用已经释放的资源等等常见错误。这方面 C++ 是一个显著的进步,boost::noncopyable 是 boost 里最值得推广的库。 操作符重载 如果要写动态数组,我们希望能像使用内置数组一样使用它,比如支持下标操作。C++可以重载 operator[] 来做到这一点。 如果要写复数,我们系统能像使用内置的 double 一样使用它,比如支持加减乘除。C++ 可以重载 operator+ 等操作符来做到这一点。 如果要写日期时间,我们希望它能直接用大于小于号来比较先后,用 == 来判断是否相等。C++ 可以重载 operator& 等操作符来做到这一点。 这要求语言能重载成员与全局操作符。操作符重载是 C++ 与生俱来的特性,1984 年的 CFront E 就支持操作符重载,并且提供了一个 complex class,这个 class 与目前标准库的 complex&& 在使用上无区别。 如果没有操作符重载,那么用户定义的ADT与内置类型用起来就不一样(想想有的语言要区分 == 和 equals,代码写起来实在很累赘)。Java 里有 BigInteger,但是 BigInteger 用起来和普通 int/long 大不相同:
public static BigInteger mean(BigInteger x, BigInteger y) {
BigInteger two = BigInteger.valueOf(2);
return x.add(y).divide(two);
public static long mean(long x, long y) {
return (x + y) / 2;
当然,操作符重载容易被滥用,因为这样显得很酷。我认为只在 ADT 表示一个&#8220;数值&#8221;的时候才适合重载加减乘除,其他情况下用具名函数为好,因此 muduo::Timestamp 只重载了关系操作符,没有重载加减操作符。另外一个理由见《C++ 工程实践(3):采用有利于版本管理的代码格式》。
&#8220;抽象&#8221;不代表低效。在 C++ 中,提高抽象的层次并不会降低效率。不然的话,人们宁可在低层次上编程,而不愿使用更便利的抽象,数据抽象也就失去了市场。后面我们将看到一个具体的例子。
模板与泛型
如果我写了一个 int vector,那么我不想为 doule 和 string 再实现一遍同样的代码。我应该把 vector 写成 template,然后用不同的类型来具现化它,从而得到 vector&int&、vector&double&、vector&complex&、vector&string& 等等具体类型。
不是每个 ADT 都需要这种泛型能力,一个 Date class 就没必要让用户指定该用哪种类型的整数,int32_t 足够了。
根据上面的要求,不是每个面向对象语言都能原生支持数据抽象,也说明数据抽象不是面向对象的子集。
数据抽象的例子
下面我们看看数值模拟 N-body 问题的两个程序,前一个用 C 语言,后一个是 C++ 的。这个例子来自编程语言的性能对比网站 。
两个程序使用了相同的算法。
C 语言版,完整代码见 ,下面是代码骨干。planet 保存与行星位置、速度、质量,位置和速度各有三个分量,程序模拟几大行星在三维空间中受引力支配的运动。struct planet
double x, y,
double vx, vy,
void advance(int nbodies, struct planet *bodies, double dt)
for (int i = 0; i & i++)
struct planet *p1 = &(bodies[i]);
for (int j = i + 1; j & j++)
struct planet *p2 = &(bodies[j]);
double dx = p1-&x - p2-&x;
double dy = p1-&y - p2-&y;
double dz = p1-&z - p2-&z;
double distance_squared = dx * dx + dy * dy + dz *
double distance = sqrt(distance_squared);
double mag = dt / (distance * distance_squared);
p1-&vx -= dx * p2-&mass *
p1-&vy -= dy * p2-&mass *
p1-&vz -= dz * p2-&mass *
p2-&vx += dx * p1-&mass *
p2-&vy += dy * p1-&mass *
p2-&vz += dz * p1-&mass *
for (int i = 0; i & i++)
struct planet * p = &(bodies[i]);
p-&x += dt * p-&
p-&y += dt * p-&
p-&z += dt * p-&
其中最核心的算法是 advance() 函数实现的数值积分,它根据各个星球之间的距离和引力,算出加速度,再修正速度,然后更新星球的位置。这个 naive 算法的复杂度是 O(N^2)。
C++ 数据抽象版,完整代码见 ,下面是代码骨架。
首先定义 Vector3 这个抽象,代表三维向量,它既可以是位置,有可以是速度。本处略去了 Vector3 的操作符重载,Vector3 支持常见的向量加减乘除运算。
然后定义 Planet 这个抽象,代表一个行星,它有两个 Vector3 成员:位置和速度。
需要说明的是,按照语义,Vector3 是数据抽象,而 Planet 是 object-based.struct Vector3
Vector3(double x, double y, double z)
: x(x), y(y), z(z)
struct Planet
Planet(const Vector3& position, const Vector3& velocity, double mass)
: position(position), velocity(velocity), mass(mass)
相同功能的 advance() 代码简短得多,而且更容易验证其正确性。(想想如果把 C 语言版的 advance() 中的 vx、vy、vz、dx、dy、dz 写错位了,这种错误较难发现。)void advance(int nbodies, Planet* bodies, double delta_time)
for (Planet* p1 = p1 != bodies + ++p1)
for (Planet* p2 = p1 + 1; p2 != bodies + ++p2)
Vector3 difference = p1-&position - p2-&
double distance_squared = magnitude_squared(difference);
double distance = std::sqrt(distance_squared);
double magnitude = delta_time / (distance * distance_squared);
p1-&velocity -= difference * p2-&mass *
p2-&velocity += difference * p1-&mass *
for (Planet* p = p != bodies + ++p)
p-&position += delta_time * p-&
性能上,尽管 C++ 使用了更高层的抽象 Vector3,但它的性能和 C 语言一样快。看看 memory layout 就会明白:
C struct 的成员是连续存储的,struct 数组也是连续的。
C++ 尽管定义了了 Vector3 这个抽象,它的内存布局并没有改变,Planet 的布局和 C planet 一模一样,Planet[] 的布局也和 C 数组一样。
另一方面,C++ 的 inline 函数在这里也起了巨大作用,我们可以放心地调用 Vector3::operator+=() 等操作符,编译器会生成和 C 一样高效的代码。
不是每个编程语言都能做到在提升抽象的时候不影响性能,来看看 Java 的内存布局。
如果我们用 class Vector3、class Planet、Planet[] 的方式写一个 Java 版的 N-body 程序,内存布局将会是:
这样大大降低了 memory locality,有兴趣的读者可以对比 Java 和 C++ 的实现效率。
注:这里的 N-body 算法只为比较语言之间的性能与编程的便利性,真正科研中用到的 N-body 算法会使用更高级和底层的优化,复杂度是O(N log N),在大规模模拟时其运行速度也比本 naive 算法快得多。
更多的例子
Date 与 Timestamp,这两个 class 的&#8220;数据&#8221;都是整数,各定义了一套操作,用于表达日期与时间这两个概念。
BigInteger,它本身就是一个&#8220;数&#8221;。如果用 C++ 实现 BigInteger,那么阶乘函数写出来十分自然,下面第二个函数是 Java 语言的版本。BigInteger factorial(int n)
BigInteger result(1);
for (int i = 1; i &= ++i) {
public static BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 1; i &= ++i) {
result = result.multiply(BigInteger.valueOf(i));
高精度运算库 gmp 有一套高质量的 C++ 封装
图形学中的三维齐次坐标 Vector4 和对应的 4x4 变换矩阵 Matrix4,例如
金融领域中经常成对出现的&#8220;买入价/卖出价&#8221;,可以封装为 BidOffer struct,这个 struct 的成员可以有 mid() &#8220;中间价&#8221;,spread() &#8220;买卖差价&#8221;,加减操作符,等等。
数据抽象是C++的重要抽象手段,适合封装&#8220;数据&#8221;,它的语义简单,容易使用。数据抽象能简化代码书写,减少偶然错误。
陈硕 (giantchen_AT_gmail)& 陈硕关于 C++ 工程实践的系列文章: 排版正常的版本: 陈硕博客文章合集下载: 本作品采用&#8220;Creative Commons 署名-非商业性使用-禁止演绎 3.0 Unported 许可协议(cc by-nc-nd)&#8221;进行许可。 本文是前一篇《C++ 工程实践(7):iostream 的用途与局限》的后续,在这篇文章的&#8220;iostream 与标准库其他组件的交互&#8221;一节,我简单地提到iostream的对象和C++标准库中的其他对象(主要是容器和string)具有不同的语义,主要体现在iostream不能拷贝或赋值。今天全面谈一谈我对这个问题的理解。 本文的&#8220;对象&#8221;定义较为宽泛,a region of memory that has a type,在这个定义下,int、double、bool 变量都是对象。 什么是值语义 值语义(value sematics)指的是对象的拷贝与原对象无关,就像拷贝 int 一样。C++ 的内置类型(bool/int/double/char)都是值语义,标准库里的 complex&& 、pair&&、vector&&、map&&、string 等等类型也都是值语意,拷贝之后就与原对象脱离关系。Java 语言的 primitive types 也是值语义。 与值语义对应的是&#8220;对象语义/object sematics&#8221;,或者叫做引用语义(reference sematics),由于&#8220;引用&#8221;一词在 C++ 里有特殊含义,所以我在本文中使用&#8220;对象语义&#8221;这个术语。对象语义指的是面向对象意义下的对象,对象拷贝是禁止的。例如 muduo 里的 Thread 是对象语义,拷贝 Thread 是无意义的,也是被禁止的:因为 Thread 代表线程,拷贝一个 Thread 对象并不能让系统增加一个一模一样的线程。 同样的道理,拷贝一个 Employee 对象是没有意义的,一个雇员不会变成两个雇员,他也不会领两份薪水。拷贝 TcpConnection 对象也没有意义,系统里边只有一个 TCP 连接,拷贝 TcpConnection& 对象不会让我们拥有两个连接。Printer 也是不能拷贝的,系统只连接了一个打印机,拷贝 Printer 并不能凭空增加打印机。凡此总总,面向对象意义下的&#8220;对象&#8221;是 non-copyable。 Java 里边的 class 对象都是对象语义/引用语义。ArrayList&Integer& a = new ArrayList&Integer&(); ArrayList&Integer& b = 那么 a 和 b 指向的是同一个ArrayList 对象,修改 a 同时也会影响 b。 值语义与 immutable 无关。Java 有 value object 一说,按(PoEAA 486)的定义,它实际上是 immutable object,例如 String、Integer、BigInteger、joda.time.DateTime 等等(因为 Java 没有办法实现真正的值语义 class,只好用 immutable object 来模拟)。尽管 immutable object 有其自身的用处,但不是本文的主题。muduo 中的 Date、Timestamp 也都是 immutable 的。 C++中的值语义对象也可以是 mutable,比如 complex&&、pair&&、vector&&、map&&、string 都是可以修改的。muduo 的 InetAddress 和 Buffer 都具有值语义,它们都是可以修改的。 值语义的对象不一定是 POD,例如 string 就不是 POD,但它是值语义的。 值语义的对象不一定小,例如 vector&int& 的元素可多可少,但它始终是值语义的。当然,很多值语义的对象都是小的,例如complex&&、muduo::Date、muduo::Timestamp。 值语义与生命期 值语义的一个巨大好处是生命期管理很简单,就跟 int 一样&#8212;&#8212;你不需要操心 int 的生命期。值语义的对象要么是 stack object,或者直接作为其他 object 的成员,因此我们不用担心它的生命期(一个函数使用自己stack上的对象,一个成员函数使用自己的数据成员对象)。相反,对象语义的 object 由于不能拷贝,我们只能通过指针或引用来使用它。 一旦使用指针和引用来操作对象,那么就要担心所指的对象是否已被释放,这一度是 C++ 程序 bug 的一大来源。此外,由于 C++ 只能通过指针或引用来获得多态性,那么在C++里从事基于继承和多态的面向对象编程有其本质的困难&#8212;&#8212;资源管理。 考虑一个简单的对象建模&#8212;&#8212;家长与子女:a Parent has a Child, a Child knows his/her Parent。在 Java 里边很好写,不用担心内存泄漏,也不用担心空悬指针:public class Parent
private Child myC
public class Child
private Parent myP
只要正确初始化 myChild 和 myParent,那么 Java 程序员就不用担心出现访问错误。一个 handle 是否有效,只需要判断其是否 non null。
在 C++ 里边就要为资源管理费一番脑筋:Parent 和 Child 都代表的是真人,肯定是不能拷贝的,因此具有对象语义。Parent 是直接持有 Child 吗?抑或 Parent 和 Child 通过指针互指?Child 的生命期由 Parent 控制吗?如果还有 ParentClub 和 School 两个 class,分别代表家长俱乐部和学校:ParentClub has many Parent(s),School has many Child(ren),那么如何保证它们始终持有有效的 Parent 对象和 Child 对象?何时才能安全地释放 Parent 和 Child ?
直接但是易错的写法:class C
class Parent : boost::noncopyable
Child* myC
class Child : boost::noncopyable
Parent* myP
如果直接使用指针作为成员,那么如何确保指针的有效性?如何防止出现空悬指针?Child 和 Parent 由谁负责释放?在释放某个 Parent 对象的时候,如何确保程序中没有指向它的指针?在释放某个 Child 对象的时候,如何确保程序中没有指向它的指针?
这一系列问题一度是C++面向对象编程头疼的问题,不过现在有了 smart pointer,我们可以借助 smart pointer 把对象语义转换为值语义,从而轻松解决对象生命期:让 Parent 持有 Child 的 smart pointer,同时让 Child 持有 Parent 的 smart pointer,这样始终引用对方的时候就不用担心出现空悬指针。当然,其中一个 smart pointer 应该是 weak reference,否则会出现循环引用,导致内存泄漏。到底哪一个是 weak reference,则取决于具体应用场景。
如果 Parent 拥有 Child,Child 的生命期由其 Parent 控制,Child 的生命期小于 Parent,那么代码就比较简单:class P
class Child : boost::noncopyable
explicit Child(Parent* myParent_)
: myParent(myParent_)
Parent* myP
class Parent : boost::noncopyable
: myChild(new Child(this))
boost::scoped_ptr&Child& myC
在上面这个设计中,Child 的指针不能泄露给外界,否则仍然有可能出现空悬指针。
如果 Parent 与 Child 的生命期相互独立,就要麻烦一些:class P
typedef boost::shared_ptr&Parent& ParentP
class Child : boost::noncopyable
explicit Child(const ParentPtr& myParent_)
: myParent(myParent_)
boost::weak_ptr&Parent& myP
typedef boost::shared_ptr&Child& ChildP
class Parent : public boost::enable_shared_from_this&Parent&,
private boost::noncopyable
void addChild()
myChild.reset(new Child(shared_from_this()));
ChildPtr myC
int main()
ParentPtr p(new Parent);
p-&addChild();
上面这个 shared_ptr+weak_ptr 的做法似乎有点小题大做。
考虑一个稍微复杂一点的对象模型:a Child has parents: a Parent has one or more Child(ren); a Parent knows his/her spouser. 这个对象模型用 Java 表述一点都不复杂,垃圾收集会帮我们搞定对象生命期。public class Parent
private Parent myS
private ArrayList&Child& myC
public class Child
private Parent myM
private Parent myD
如果用 C++ 来实现,如何才能避免出现空悬指针,同时避免出现内存泄漏呢?借助 shared_ptr 把裸指针转换为值语义,我们就不用担心这两个问题了:class P
typedef boost::shared_ptr&Parent& ParentP
class Child : boost::noncopyable
explicit Child(const ParentPtr& myMom_,
const ParentPtr& myDad_)
: myMom(myMom_),
myDad(myDad_)
boost::weak_ptr&Parent& myM
boost::weak_ptr&Parent& myD
typedef boost::shared_ptr&Child& ChildP
class Parent : boost::noncopyable
void setSpouser(const ParentPtr& spouser)
mySpouser =
void addChild(const ChildPtr& child)
myChildren.push_back(child);
boost::weak_ptr&Parent& myS
std::vector&ChildPtr& myC
int main()
ParentPtr mom(new Parent);
ParentPtr dad(new Parent);
mom-&setSpouser(dad);
dad-&setSpouser(mom);
ChildPtr child(new Child(mom, dad));
mom-&addChild(child);
dad-&addChild(child);
ChildPtr child(new Child(mom, dad));
mom-&addChild(child);
dad-&addChild(child);
如果不使用 smart pointer,用 C++ 做面向对象编程将会困难重重。
值语义与标准库
C++ 要求凡是能放入标准容器的类型必须具有值语义。准确地说:type 必须是 SGIAssignable concept 的 model。但是,由 于C++ 编译器会为 class 默认提供 copy constructor 和 assignment operator,因此除非明确禁止,否则 class 总是可以作为标准库的元素类型&#8212;&#8212;尽管程序可以编译通过,但是隐藏了资源管理方面的 bug。
因此,在写一个 class 的时候,先让它继承 boost::noncopyable,几乎总是正确的。
在现代 C++ 中,一般不需要自己编写 copy constructor 或 assignment operator,因为只要每个数据成员都具有值语义的话,编译器自动生成的 member-wise copying&assigning 就能正常工作;如果以 smart ptr 为成员来持有其他对象,那么就能自动启用或禁用 copying&assigning。例外:编写 HashMap 这类底层库时还是需要自己实现 copy control。
值语义与C++语言
C++ 的 class 本质上是值语义的,这才会出现 object slicing 这种语言独有的问题,也才会需要程序员注意 pass-by-value 和 pass-by-const-reference 的取舍。在其他面向对象编程语言中,这都不需要费脑筋。
值语义是C++语言的三大约束之一,C++ 的设计初衷是让用户定义的类型(class)能像内置类型(int)一样工作,具有同等的地位。为此C++做了以下设计(妥协):
class 的 layout 与 C struct 一样,没有额外的开销。定义一个&#8220;只包含一个 int 成员的 class &#8221;的对象开销和定义一个 int 一样。
甚至 class data member 都默认是 uninitialized,因为函数局部的 int 是 uninitialized。
class 可以在 stack 上创建,也可以在 heap 上创建。因为 int 可以是 stack variable。
class 的数组就是一个个 class 对象挨着,没有额外的 indirection。因为 int 数组就是这样。
编译器会为 class 默认生成 copy constructor 和 assignment operator。其他语言没有 copy constructor 一说,也不允许重载 assignment operator。C++ 的对象默认是可以拷贝的,这是一个尴尬的特性。
当 class type 传入函数时,默认是 make a copy (除非参数声明为 reference)。因为把 int 传入函数时是 make a copy。
当函数返回一个 class type 时,只能通过 make a copy(C++ 不得不定义 RVO 来解决性能问题)。因为函数返回 int 时是 make a copy。
以 class type 为成员时,数据成员是嵌入的。例如 pair&complex&double&, size_t& 的 layout 就是 complex&double& 挨着 size_t。
这些设计带来了性能上的好处,原因是 memory locality。比方说我们在 C++ 里定义 complex&double& class,array of complex&double&, vector&complex&double& &,它们的 layout 分别是:(re 和 im 分别是复数的实部和虚部。)
而如果我们在 Java 里干同样的事情,layout 大不一样,memory locality 也差很多:
Java 里边每个 object 都有 header,至少有两个 word 的开销。对比 Java 和 C++,可见 C++ 的对象模型要紧凑得多。
下一篇文章我会谈与值语义紧密相关的数据抽象(data abstraction),解释为什么它是与面向对象并列的一种编程范式,为什么支持面向对象的编程语言不一定支持数据抽象。C++在最初的时候是以 data abstraction 为卖点,不过随着时间的流逝,现在似乎很多人只知 Object-Oriented,不知 data abstraction 了。C++ 的强大之处在于&#8220;抽象&#8221;不以性能损失为代价,下一篇文章我们将看到具体例子。
陈硕 (giantchen_AT_gmail) &
陈硕关于 C++ 工程实践的系列文章:
陈硕博客文章合集下载:
本作品采用&#8220;Creative Commons 署名-非商业性使用-禁止演绎 3.0 Unported 许可协议(cc by-nc-nd)&#8221;进行许可。 本文主要考虑 x86 Linux 平台,不考虑跨平台的可移植性,也不考虑国际化(i18n),但是要考虑 32-bit 和 64-bit 的兼容性。本文以 stdio 指代 C 语言的 scanf/printf 系列格式化输入输出函数。本文注意区分&#8220;编程初学者&#8221;和&#8220;C++初学者&#8221;,二者含义不同。 摘要:C++ iostream 的主要作用是让初学者有一个方便的命令行输入输出试验环境,在真实的项目中很少用到 iostream,因此不必把精力花在深究 iostream 的格式化与 manipulator。iostream 的设计初衷是提供一个可扩展的类型安全的 IO 机制,但是后来莫名其妙地加入了 locale 和 facet 等累赘。其整个设计复杂不堪,多重+虚拟继承的结构也很巴洛克,性能方面几无亮点。iostream 在实际项目中的用处非常有限,为此投入过多学习精力实在不值。 stdio 格式化输入输出的缺点 1. 对编程初学者不友好 看看下面这段简单的输入输出代码。#include &stdio.h&
int main()
char name[80];
scanf("%d %hd %f %lf %s", &i, &s, &f, &d, name);
printf("%d %d %f %f %s", i, s, f, d, name);
注意到其中
输入和输出用的格式字符串不一样。输入 short 要用 %hd,输出用 %d;输入 double 要用 %lf,输出用 %f。
输入的参数不统一。对于 i、s、f、d 等变量,在传入 scanf() 的时候要取地址(&),而对于 name,则不用取地址。
读者可以试一试如何用几句话向刚开始学编程的初学者解释上面两条背后原因(涉及到传递函数不定参数时的类型转换,函数调用栈的内存布局,指针的意义,字符数组退化为字符指针等等),如果一开始解释不清,只好告诉学生&#8220;这是规定&#8221;。
缓冲区溢出的危险。上面的例子在读入 name 的时候没有指定大小,这是用 C 语言编程的安全漏洞的主要来源。应该在一开始就强调正确的做法,避免养成错误的习惯。正确而安全的做法如 Bjarne Stroustrup 在《Learning Standard C++ as a New Language》所示:#include &stdio.h&
int main()
const int max = 80;
char name[max];
char fmt[10];
sprintf(fmt, "%%%ds", max - 1);
scanf(fmt, name);
printf("%s\n", name);
这个动态构造格式化字符串的做法恐怕更难向初学者解释。
2. 安全性(security)
C 语言的安全性问题近十几年来引起了广泛的注意,C99 增加了 snprintf() 等能够指定输出缓冲区大小的函数,输出方面的安全性问题已经得到解决;输入方面似乎没有太大进展,还要靠程序员自己动手。
考虑一个简单的编程任务:从文件或标准输入读入一行字符串,行的长度不确定。我发现没有哪个 C 语言标准库函数能完成这个任务,除非 roll your own。
首先,gets() 是错误的,因为不能指定缓冲区的长度。
其次,fgets() 也有问题。它能指定缓冲区的长度,所以是安全的。但是程序必须预设一个长度的最大值,这不满足题目要求&#8220;行的长度不确定&#8221;。另外,程序无法判断 fgets() 到底读了多少个字节。为什么?考虑一个文件的内容是 9 个字节的字符串 "Chen\000Shuo",注意中间出现了 '\0' 字符,如果用 fgets() 来读取,客户端如何知道 "\000Shuo" 也是输入的一部分?毕竟 strlen() 只返回 4,而且整个字符串里没有 '\n' 字符。
最后,可以用 glibc 定义的 getline(3) 函数来读取不定长的&#8220;行&#8221;。这个函数能正确处理各种情况,不过它返回的是 malloc() 分配的内存,要求调用端自己 free()。
3. 类型安全(type-safe)
如果 printf() 的整数参数类型是 int、long 等标准类型, 那么 printf() 的格式化字符串很容易写。但是如果参数类型是 typedef 的类型呢?
如果你想在程序中用 printf 来打印日志,你能一眼看出下面这些类型该用 "%d" "%ld" "%lld" 中的哪一个来输出?你的选择是否同时兼容 32-bit 和 64-bit 平台?
clock_t。这是 clock(3) 的返回类型
dev_t。这是 mknod(3) 的参数类型
in_addr_t、in_port_t。这是 struct sockaddr_in 的成员类型
nfds_t。这是 poll(2) 的参数类型
off_t。这是 lseek(2) 的参数类型,麻烦的是,这个类型与宏定义 _FILE_OFFSET_BITS 有关。
pid_t、uid_t、gid_t。这是 getpid(2) getuid(2) getgid(2) 的返回类型
ptrdiff_t。printf() 专门定义了 "t" 前缀来支持这一类型(即使用 "%td" 来打印)。
size_t、ssize_t。这两个类型到处都在用。printf() 为此专门定义了 "z" 前缀来支持这两个类型(即使用 "%zu" 或 "%zd" 来打印)。
socklen_t。这是 bind(2) 和 connect(2) 的参数类型
time_t。这是 time(2) 的返回类型,也是 gettimeofday(2) 和 clock_gettime(2) 的输出结构体的成员类型
如果在 C 程序里要正确打印以上类型的整数,恐怕要费一番脑筋。《The Linux Programming Interface》的作者建议(3.6.2节)先统一转换为 long 类型再用 "%ld" 来打印;对于某些类型仍然需要特殊处理,比如 off_t 的类型可能是 long long。
还有,int64_t 在 32-bit 和 64-bit 平台上是不同的类型,为此,如果程序要打印 int64_t 变量,需要包含 &inttypes.h& 头文件,并且使用 PRId64 宏:#include &stdio.h&
#define __STDC_FORMAT_MACROS
#include &inttypes.h&
int main()
int64_t x = 100;
printf("%" PRId64 "\n", x);
printf("%06" PRId64 "\n", x);
muduo 的 Timestamp 使用了 PRId64
Google C++ 编码规范也提到了 64-bit 兼容性:
这些问题在 C++ 里都不存在,在这方面 iostream 是个进步。
C stdio 在类型安全方面原本还有一个缺点,即格式化字符串与参数类型不匹配会造成难以发现的 bug,不过现在的编译器已经能够检测很多这种错误:int main()
double d = 100.0;
// warning: format '%d' expects type 'int', but argument 2 has type 'double'
printf("%d\n", d);
// warning: format '%d' expects type 'int*', but argument 2 has type 'short int*'
scanf("%d", &s);
size_t sz = 1;
// no warning
printf("%zd\n", sz);
4. 不可扩展?
C stdio 的另外一个缺点是无法支持自定义的类型,比如我写了一个 Date class,我无法像打印 int 那样用 printf 来直接打印 Date 对象。struct Date
int year, month,
printf("%D", &date);
Glibc 放宽了这个限制,允许用户调用 register_printf_function(3) 注册自己的类型,当然,前提是与现有的格式字符不冲突(这其实大大限制了这个功能的用处,现实中也几乎没有人真的去用它)。&
C stdio 的性能方面有两个弱点。
使用一种 little language (现在流行叫 DSL)来配置格式。固然有利于紧凑性和灵活性,但损失了一点点效率。每次打印一个整数都要先解析 "%d" 字符串,大多数情况下不是问题,某些场合需要自己写整数到字符串的转换。
C locale 的负担。locale 指的是不同语种对&#8220;什么是空白&#8221;、&#8220;什么是字母&#8221;,&#8220;什么是小数点&#8221;有不同的定义(德语里边小数点是逗号,不是句点)。C 语言的 printf()、scanf()、isspace()、isalpha()、ispunct()、strtod() 等等函数都和 locale 有关,而且可以在运行时动态更改。就算是程序只使用默认的 "C" locale,任然要为这个灵活性付出代价。
iostream 的设计初衷
iostream 的设计初衷包括克服 C stdio 的缺点,提供一个高效的可扩展的类型安全的 IO 机制。&#8220;可扩展&#8221;有两层意思,一是可以扩展到用户自定义类型,而是通过继承 iostream 来定义自己的 stream,本文把前一种称为&#8220;类型可扩展&#8221;后一种称为&#8220;功能可扩展&#8221;。
&#8220;类型可扩展&#8221;和&#8220;类型安全&#8221;都是通过函数重载来实现的。
iostream 对初学者很友好,用 iostream 重写与前面同样功能的代码:#include &iostream&
#include &string&
int main()
cin && i && s && f && d &&
cout && i && " " && s && " " && f && " " && d && " " && name &&
这段代码恐怕比 scanf/printf 版本容易解释得多,而且没有安全性(security)方面的问题。
我们自己的类型也可以融入 iostream,使用起来与 built-in 类型没有区别。这主要得力于 C++ 可以定义 non-member functions/operators。#include &ostream&
// 是不是太重量级了?
class Date
Date(int year, int month, int day)
: year_(year), month_(month), day_(day)
void writeTo(std::ostream& os) const
os && year_ && '-' && month_ && '-' && day_;
int year_, month_, day_;
std::ostream& operator&&(std::ostream& os, const Date& date)
date.writeTo(os);
int main()
Date date();
std::cout && date && std::
iostream 凭借这两点(类型安全和类型可扩展),基本克服了 stdio 在使用上的不便与不安全。如果 iostream 止步于此,那它将是一个非常便利的库,可惜它前进了另外一步。
iostream 与标准库其他组件的交互
不同于标准库其他 class 的&#8220;值语意&#8221;,iostream 是&#8220;对象语意&#8221;,即 iostream 是 non-copyable。这是正确的,因为如果 fstream 代表一个文件的话,拷贝一个 fstream 对象意味着什么呢?表示打开了两个文件吗?如果销毁一个 fstream 对象,它会关闭文件句柄,那么另一个 fstream copy 对象会因此受影响吗?
C++ 同时支持&#8220;数据抽象&#8221;和&#8220;面向对象编程&#8221;,其实主要就是&#8220;值语意&#8221;与&#8220;对象语意&#8221;的区别,我发现不是每个人都清楚这一点,这里多说几句。标准库里的 complex&& 、pair&&、vector&&、 string 等等都是值语意,拷贝之后就与原对象脱离关系,就跟拷贝一个 int 一样。而我们自己写的 Employee class、TcpConnection class 通常是对象语意,拷贝一个 Employee 对象是没有意义的,一个雇员不会变成两个雇员,他也不会领两份薪水。拷贝 TcpConnection 对象也没有意义,系统里边只有一个 TCP 连接,拷贝 TcpConnection& 对象不会让我们拥有两个连接。因此如果在 C++ 里做面向对象编程,写的 class 通常应该禁用 copy constructor 和 assignment operator,比如可以继承 boost::noncopyable。对象语意的类型不能直接作为标准容器库的成员。另一方面,如果要写一个图形程序,其中用到三维空间的向量,那么我们可以写 Vector3D class,它应该是值语意的,允许拷贝,并且可以用作标准容器库的成员,例如 vector&Vector3D& 表示一条三维的折线。
C stdio 的另外一个缺点是 FILE* 可以随意拷贝,但是只要关闭其中一个 copy,其他 copies 也都失效了,跟空悬指针一般。这其实不光是 C stdio 的缺点,整个 C 语言对待资源(malloc 得到的内存,open() 打开的文件,socket() 打开的连接)都是这样,用整数或指针来代表(即&#8220;句柄&#8221;)。而整数和指针类型的&#8220;句柄&#8221;是可以随意拷贝的,很容易就造成重复释放、遗漏释放、使用已经释放的资源等等常见错误。这是因为 C 语言错误地让&#8220;对象语言&#8221;的东西变成了值语意。
iostream 禁止拷贝,利用对象的生命期来明确管理资源(如文件),很自然地就避免了 C 语言易犯的错误。这就是 RAII,一种重要且独特的 C++ 编程手法。
std::string
iostream 可以与 string 配合得很好。但是有一个问题:谁依赖谁?
std::string 的 operator && 和 operator && 是如何声明的?"string" 头文件在声明这两个 operators 的时候要不要 include "iostream" ?
iostream 和 string 都可以单独 include 来使用,显然 iostream 头文件里不会定义 string 的 && 和 && 操作。但是,如果"string"要include "iostream",岂不是让 string 的用户被迫也用了 iostream?编译 iostream 头文件可是相当的慢啊(因为 iostream 是 template,其实现代码都放到了头文件中)。
标准库的解决办法是定义 iosfwd 头文件,其中包含 istream 和 ostream 等的前向声明 (forward declarations),这样 "string" 头文件在定义输入输出操作符时就可以不必包含 "iostream",只需要包含简短得多的 "iosfwd"。我们自己写程序也可借此学习如何支持可选的功能。
值得注意的是,istream::getline() 成员函数的参数类型是 char*,因为 "istream" 没有包含 "string",而我们常用的 std::getline() 函数是个 non-member function,定义在 "string" 里边。
std::complex
标准库的复数类 complex 的情况比较复杂。使用 complex 会自动包含 sstream,后者会包含 istream 和 ostream,这是个不小的负担。问题是,为什么?
它的 operator && 操作比 string 复杂得多,如何应对格式不正确的情况?输入字符串不会遇到格式不正确,但是输入一个复数可能遇到各种问题,比如数字的格式不对等。我怀疑有谁会真的在产品项目里用 operator && 来读入字符方式表示的复数,这样的代码的健壮性如何保证。基于同样的理由,我认为产品代码中应该避免用 istream 来读取带格式的内容,后面也不再谈 istream 的缺点,它已经被秒杀。
它的 operator && 也很奇怪,它不是直接使用参数 ostream& os 对象来输出,而是先构造 ostringstream,输出到该 string stream,再把结果字符串输出到 ostream。简化后的代码如下:template&typename T&
std::ostream& operator&&(std::ostream& os, const std::complex&T&& x)
s && '(' && x.real() && ',' && x.imag() && ')';
return os && s.str();
注意到 ostringstream 会用到动态分配内存,也就是说,每输出一个 complex 对象就会分配释放一次内存,效率堪忧。
根据以上分析,我认为 iostream 和 complex 配合得不好,但是它们耦合得更紧密(与 string/iostream 相比),这可能是个不得已的技术限制吧(complex 是 template,其 operator&& 必须在头文件中定义,而这个定义又用到了 ostringstream,不得已包含了 iostream 的实现)。
如果程序要对 complex 做 IO,从效率和健壮性方面考虑,建议不要使用 iostream。
iostream 在使用方面的缺点
在简单使用 iostream 的时候,它确实比 stdio 方便,但是深入一点就会发现,二者可说各擅胜场。下面谈一谈 iostream 在使用方面的缺点。
1. 格式化输出很繁琐
iostream 采用 manipulator 来格式化,如果我想按照
的格式输出前面定义的 Date class,那么代码要改成:--- 02-02.cc
16:40:05. +0800
+++ 04-01.cc
17:10:27. +0800
@@ -1,4 +1,5 @@
#include &iostream&
+#include &iomanip&
class Date
@@ -10,7 +11,9 @@
void writeTo(std::ostream& os) const
os && year_ && '-' && month_ && '-' && day_;
os && year_ && '-'
&& std::setw(2) && std::setfill('0') && month_ && '-'
&& std::setw(2) && std::setfill('0') && day_;
假如用 stdio,会简短得多,因为 printf 采用了一种表达能力较强的小语言来描述输出格式。--- 04-01.cc
17:03:22. +0800
+++ 04-02.cc
17:04:21. +0800
@@ -1,5 +1,5 @@
#include &iostream&
-#include &iomanip&
+#include &stdio.h&
class Date
@@ -11,9 +11,9 @@
void writeTo(std::ostream& os) const
os && year_ && '-' && month_ && '-' && day_;
char buf[32];
snprintf(buf, sizeof buf, "%d-%02d-%02d", year_, month_, day_);
使用小语言来描述格式还带来另外一个好处:外部可配置。
2. 外部可配置性
比方说,我想用一个外部的配置文件来定义日期的格式。C stdio 很好办,把格式字符串 "%d-%02d-%02d" 保存到配置里就行。但是 iostream 呢?它的格式是写死在代码里的,灵活性大打折扣。
再举一个例子,程序的 message 的多语言化。
const char* name = "Shuo Chen";
int age = 29;
printf("My name is %1$s, I am %2$d years old.\n", name, age);
cout && "My name is " && name && ", I am " && age && " years old." &&
对于 stdio,要让这段程序支持中文的话,把代码中的"My name is %1$s, I am %2$d years old.\n",
替换为 "我叫%1$s,今年%2$d岁。\n" 即可。也可以把这段提示语做成资源文件,在运行时读入。而对于 iostream,恐怕没有这么方便,因为代码是支离破碎的。
C stdio 的格式化字符串体现了重要的&#8220;数据就是代码&#8221;的思想,这种&#8220;数据&#8221;与&#8220;代码&#8221;之间的相互转换是程序灵活性的根源,远比 OO 更为灵活。
3. stream 的状态
如果我想用 16 进制方式输出一个整数 x,那么可以用 hex 操控符,但是这会改变 ostream 的状态。比如说
int x = 8888;
cout && hex && showbase && x &&
// forgot to reset state
cout && 123 &&
这这段代码会把 123 也按照 16 进制方式输出,这恐怕不是我们想要的。
再举一个例子,setprecision() 也会造成持续影响:
double d = 123.45;
printf("%8.3f\n", d);
cout && d &&
cout && setw(8) && fixed && setprecision(3) && d &&
cout && d &&
输出是:$ ./a.out
# default cout format
# our format
# side effects
可见代码中的 setprecision() 影响了后续输出的精度。注意 setw() 不会造成影响,它只对下一个输出有效。
这说明,如果使用 manipulator 来控制格式,需要时刻小心防止影响了后续代码。而使用 C stdio 就没有这个问题,它是&#8220;上下文无关的&#8221;。
4. 知识的通用性
在 C 语言之外,有其他很多语言也支持 printf() 风格的格式化,例如 Java、Perl、Ruby 等等 ()。学会 printf() 的格式化方法,这个知识还可以用到其他语言中。但是 C++ iostream 只此一家别无分店,反正都是格式化输出,stdio 的投资回报率更高。
基于这点考虑,我认为不必深究 iostream 的格式化方法,只需要用好它最基本的类型安全输出即可。在真的需要格式化的场合,可以考虑 snprintf() 打印到栈上缓冲,再用 ostream 输出。
5. 线程安全与原子性
iostream 的另外一个问题是线程安全性。stdio 的函数是线程安全的,而且 C 语言还提供了 flockfile(3)/funlockfile(3) 之类的函数来明确控制 FILE* 的加锁与解锁。
iostream 在线程安全方面没有保证,就算单个 operator&& 是线程安全的,也不能保证原子性。因为 cout && a && 是两次函数调用,相当于 cout.operator&&(a).operator&&(b)。两次调用中间可能会被打断进行上下文切换,造成输出内容不连续,插入了其他线程打印的字符。
而 fprintf(stdout, "%s %d", a, b); 是一次函数调用,而且是线程安全的,打印的内容不会受其他线程影响。
因此,iostream 并不适合在多线程程序中做 logging。
iostream 的局限
根据以上分析,我们可以归纳 iostream 的局限:
输入方面,istream 不适合输入带格式的数据,因为&#8220;纠错&#8221;能力不强,进一步的分析请见孟岩写的《》,孟岩说&#8220;复杂的设计必然带来复杂的使用规则,而面对复杂的使用规则,用户是可以投票的,那就是你做你的,我不用!&#8221;可谓鞭辟入里。如果要用 istream,我推荐的做法是用 getline() 读入一行数据,然后用正则表达式来判断内容正误,并做分组,然后用 strtod/strtol 之类的函数做类型转换。这样似乎更容易写出健壮的程序。
输出方面,ostream 的格式化输出非常繁琐,而且写死在代码里,不如 stdio 的小语言那么灵活通用。建议只用作简单的无格式输出。
log 方面,由于 ostream 没有办法在多线程程序中保证一行输出的完整性,建议不要直接用它来写 log。如果是简单的单线程程序,输出数据量较少的情况下可以酌情使用。当然,产品代码应该用成熟的 logging 库,而不要用其它东西来凑合。
in-memory 格式化方面,由于 ostringstream 会动态分配内存,它不适合性能要求较高的场合。
文件 IO 方面,如果用作文本文件的输入或输出,(i|o)fstream 有上述的缺点;如果用作二进制数据输入输出,那么自己简单封装一个 File class 似乎更好用,也不必为用不到的功能付出代价(后文还有具体例子)。ifstream 的一个用处是在程序启动时读入简单的文本配置文件。如果配置文件是其他文本格式(XML 或 JSON),那么用相应的库来读,也用不到 ifstream。
性能方面,iostream 没有兑现&#8220;高效性&#8221;诺言。iostream 在某些场合比 stdio 快,在某些场合比 stdio 慢,对于性能要求较高的场合,我们应该自己实现字符串转换(见后文的代码与测试)。iostream 性能方面的一个注脚:在线 ACM/ICPC 判题网站上,如果一个简单的题目发生超时错误,那么把其中 iostream 的输入输出换成 stdio,有时就能过关。
既然有这么多局限,iostream 在实际项目中的应用就大为受限了,在这上面投入太多的精力实在不值得。说实话,我没有见过哪个 C++ 产品代码使用 iostream 来作为输入输出设施。 &
iostream 在设计方面的缺点
iostream 的设计有相当多的 WTFs,stackoverflow 有人吐槽说&#8220;If you had to judge by today's software engineering standards, would C++'s IOStreams still be considered well-designed?&#8221;
面向对象的设计
iostream 是个面向对象的 IO 类库,本节简单介绍它的继承体系。
对 iostream 略有了解的人会知道它用了多重继承和虚拟继承,简单地画个类图如下,是典型的菱形继承:
如果加深一点了解,会发现 iostream 现在是模板化的,同时支持窄字符和宽字符。下图是现在的继承体系,同时画出了 fstreams 和 stringstreams。图中方框的第二行是模板的具现化类型,也就是我们代码里常用的具体类型(通过 typedef 定义)。
这个继承体系糅合了面向对象与泛型编程,但可惜它两方面都不讨好。
再进一步加深了解,发现还有一个平行的 streambuf 继承体系,fstream 和 stringstream 的不同之处主要就在于它们使用了不同的 streambuf 具体类型。
再把这两个继承体系画到一幅图里:
注意到 basic_ios 持有了 streambuf 的指针;而 fstreams 和 stringstreams 则分别包含 filebuf 和 stringbuf 的对象。看上去有点像 Bridge 模式。
看了这样巴洛克的设计,有没有人还打算在自己的项目中想通过继承 iostream 来实现自己的 stream,以实现功能扩展么?
面向对象方面的设计缺陷
本节我们分析一下 iostream 的设计违反了哪些 OO 准则。
我们知道,面向对象中的 public 继承需要满足 Liskov 替换原则。(见《Effective C++ 第3版》条款32:确保你的 public 继承模塑出 is-a 关系。《C++ 编程规范》条款 37:public 继承意味可替换性。继承非为复用,乃为被复用。)
在程序里需要用到 ostream 的地方(例如 operator&& ),我传入 ofstream 或 ostringstream 都应该能按预期工作,这就是 OO 继承强调的&#8220;可替换性&#8221;,派生类的对象可以替换基类对象,从而被 operator&& 复用。
iostream 的继承体系多次违反了 Liskov 原则,这些地方继承的目的是为了复用基类的代码,下图中我把违规的继承关系用红线标出。
在现有的继承体系中,合理的有:
ifstream is-a istream
istringstream is-a istream
ofstream is-a ostream
ostringstream is-a ostream
fstream is-a iostream
stringstream is-a iostream
我认为不怎么合理的有:
ios 继承 ios_base,有没有哪种情况下程序代码期待 ios_base 对象,但是客户可以传入一个 ios 对象替代之?如果没有,这里用 public 继承是不是违反 OO 原则?
istream 继承 ios,有没有哪种情况下程序代码期待 ios 对象,但是客户可以传入一个 istream 对象替代之?如果没有,这里用 public 继承是不是违反 OO 原则?
ostream 继承 ios,有没有哪种情况下程序代码期待 ios 对象,但是客户可以传入一个 ostream 对象替代之?如果没有,这里用 public 继承是不是违反 OO 原则?
iostream 多重继承 istream 和 ostream。为什么 iostream 要同时继承两个 non-interface class?这是接口继承还是实现继承?是不是可以用组合(composition)来替代?(见《Effective C++ 第3版》条款38:通过组合模塑出 has-a 或&#8220;以某物实现&#8221;。《C++ 编程规范》条款 34:尽可能以组合代替继承。)
用组合替换继承之后的体系:
注意到在新的设计中,只有真正的 is-a 关系采用了 public 继承,其他均以组合来代替,组合关系以红线表示。新的设计没有用的虚拟继承或多重继承。
其中 iostream 的新实现值得一提,代码结构如下:
class iostream
istream& get_istream();
ostream& get_ostream();
virtual ~iostream();
这样一来,在需要 iostream 对象表现得像 istream 的地方,调用 get_istream() 函数返回一个 istream 的引用;在需要 iostream 对象表现得像 ostream 的地方,调用 get_ostream() 函数返回一个 ostream 的引用。功能不受影响,而且代码更清晰。(虽然我非常怀疑 iostream 的真正价值,一个东西既可读又可写,说明是个 sophisticated IO 对象,为什么还用这么厚的 OO 封装?)
阳春的 locale
iostream 的故事还不止这些,它还包含一套阳春的 locale/facet 实现,这套实践中没人用的东西进一步增加了 iostream 的复杂度,而且不可避免地影响其性能。Nathan Myers 正是始作俑者
ostream 自身定义的针对整数和浮点数的 operator&& 成员函数的函数体是:bool failed =
use_facet&num_put&(getloc()).put(
ostreambuf_iterator(*this), *this, fill(), val).failed();
它会转而调用 num_put::put(),后者会调用 num_put::do_put(),而 do_put() 是个虚函数,没办法 inline。iostream 在性能方面的不足恐怕部分来自于此。这个虚函数白白浪费了把 template 的实现放到头文件应得的好处,编译和运行速度都快不起来。
我没有深入挖掘其中的细节,感兴趣的同学可以移步观看 facet 的继承体系:
据此分析,我不认为以 iostream 为基础的上层程序库(比方说那些克服 iostream 格式化方面的缺点的库)有多大的实用价值。
孟岩评价 &#8220; iostream 最大的缺点是臆造抽象&#8221;,我非常赞同他老人家的观点。
这个评价同样适用于 Java 那一套叠床架屋的 InputStream/OutputStream/Reader/Writer 继承体系,.NET 也搞了这么一套繁文缛节。
乍看之下,用 input stream 表示一个可以&#8220;读&#8221;的数据流,用 output stream 表示一个可以&#8220;写&#8221;的数据流,屏蔽底层细节,面向接口编程,&#8220;符合面向对象原则&#8221;,似乎是一件美妙的事情。但是,真实的世界要残酷得多。
IO 是个极度复杂的东西,就拿最常见的 memory stream、file stream、socket stream 来说,它们之间的差异极大:
是单向 IO 还是双向 IO。只读或者只写?还是既可读又可写?
顺序访问还是随机访问。可不可以 seek?可不可以退回 n 字节?
文本数据还是二进制数据。格式有误怎么办?如何编写健壮的处理输入的代码?
有无缓冲。write 500 字节是否能保证完全写入?有没有可能只写入了 300 字节?余下 200 字节怎么办?
是否阻塞。会不会返回 EWOULDBLOCK 错误?
有哪些出错的情况。这是最难的,memory stream 几乎不可能出错,file stream 和 socket stream 的出错情况完全不同。socket stream 可能遇到对方断开连接,file stream 可能遇到超出磁盘配额。
根据以上列举的初步分析,我不认为有办法设计一个公共的基类把各方面的情况都考虑周全。各种 IO 设施之间共性太小,差异太大,例外太多。如果硬要用面向对象来建模,基类要么太瘦(只放共性,这个基类包含的 interface functions 没多大用),要么太肥(把各种 IO 设施的特性都包含进来,这个基类包含的 interface functions 很多,但是不是每一个都能调用)。
C 语言对此的解决办法是用一个 int 表示 IO 对象(file 或 PIPE 或 socket),然后配以 read()/write()/lseek()/fcntl() 等一系列全局函数,程序员自己搭配组合。这个做法我认为比面向对象的方案要简洁高效。
iostream 在性能方面没有比 stdio 高多少,在健壮性方面多半不如 stdio,在灵活性方面受制于本身的复杂设计而难以让使用者自行扩展。目前看起来只适合一些简单的要求不高的应用,但是又不得不为它的复杂设计付出运行时代价,总之其定位有点不上不下。
在实际的项目中,我们可以提炼出一些简单高效的 strip-down 版本,在获得便利性的同时避免付出不必要的代价。
一个 300 行的 memory buffer output stream
我认为以 operator&& 来输出数据非常适合 logging,因此写了一个简单的 LogStream。代码不到 300行,完全独立于 iostream。
这个 LogStream 做到了类型安全和类型可扩展。它不支持定制格式化、不支持 locale/facet、没有继承、buffer 也没有继承与虚函数、没有动态分配内存、buffer 大小固定。简单地说,适合 logging 以及简单的字符串转换。
LogStream 的接口定义是class LogStream : boost::noncopyable
typedef LogS
typedef detail::FixedBuffer B
LogStream();
self& operator&&(bool);
self& operator&&(short);
self& operator&&(unsigned short);
self& operator&&(int);
self& operator&&(unsigned int);
self& operator&&(long);
self& operator&&(unsigned long);
self& operator&&(long long);
self& operator&&(unsigned long long);
self& operator&&(const void*);
self& operator&&(float);
self& operator&&(double);
// self& operator&&(long double);
self& operator&&(char);
// self& operator&&(signed char);
// self& operator&&(unsigned char);
self& operator&&(const char*);
self& operator&&(const string&);
const Buffer& buffer() const { return buffer_; }
void resetBuffer() { buffer_.reset(); }
Buffer buffer_;
LogStream 本身不是线程安全的,它不适合做全局对象。正确的使用方式是每条 log 消息构造一个 LogStream,用完就扔。LogStream 的成本极低,这么做不会有什么性能损失。
目前这个 logging 库还在开发之中,只完成了 LogStream 这一部分。将来可能改用动态分配的 buffer,这样方便在线程之间传递数据。
整数到字符串的高效转换
muduo::LogStream 的整数转换是自己写的,用的是 Matthew Wilson 的算法,见
。这个算法比 stdio 和 iostream 都要快。
浮点数到字符串的高效转换
目前 muduo::LogStream 的浮点数格式化采用的是 snprintf() 所以从性能上与 stdio 持平,比 ostream 快一些。
浮点数到字符串的转换是个复杂的话题,这个领域 20 年以来没有什么进展(目前的实现大都基于 David M. Gay 在 1990 年的工作《Correctly Rounded Binary-Decimal and Decimal-Binary Conversions》,代码 ),直到 2010 年才有突破。
Florian Loitsch 发明了新的更快的算法 Grisu3,他的论文《Printing floating-point numbers quickly and accurately with integers》发表在 PLDI 2010,代码见 ,还有这里
。有兴趣的同学可以阅读这篇博客
将来 muduo::LogStream 可能会改用 Grisu3 算法实现浮点数转换。
由于 muduo::LogStream 抛掉了很多负担,可以预见它的性能好于 ostringstream 和 stdio。我做了一个简单的性能测试,结果如下。
从上表看出,ostreamstream 有时候比 snprintf 快,有时候比它慢,muduo::LogStream 比它们两个都快得多(double 类型除外)。
其他程序库如何使用 LogStream 作为输出呢?办法很简单,用模板。
前面我们定义了 Date class 针对 std::ostream 的 operator&&,只要稍作修改就能同时适用于 std::ostream 和 LogStream。而且 Date 的头文件不再需要 include &ostream&,降低了耦合。 class Date
Date(int year, int month, int day)
: year_(year), month_(month), day_(day)
void writeTo(std::ostream& os) const
template&typename OStream&
void writeTo(OStream& os) const
char buf[32];
snprintf(buf, sizeof buf, "%d-%02d-%02d", year_, month_, day_);
int year_, month_, day_;
-std::ostream& operator&&(std::ostream& os, const Date& date)
+template&typename OStream&
+OStream& operator&&(OStream& os, const Date& date)
date.writeTo(os);
现实的 C++ 程序如何做文件 IO
举两个例子,
Google leveldb
Google leveldb 是一个高效的持久化 key-value db。
它定义了三个精简的 interface:
SequentialFile
RandomAccessFile
WritableFile
接口函数如下struct Slice {
const char* data_;
size_t size_;
// A file abstraction for reading sequentially through a file
class SequentialFile {
SequentialFile() { }
virtual ~SequentialFile();
virtual Status Read(size_t n, Slice* result, char* scratch) = 0;
virtual Status Skip(uint64_t n) = 0;
// A file abstraction for randomly reading the contents of a file.
class RandomAccessFile {
RandomAccessFile() { }
virtual ~RandomAccessFile();
virtual Status Read(uint64_t offset, size_t n, Slice* result,
char* scratch) const = 0;
// A file abstraction for sequential writing.
The implementation
// must provide buffering since callers may append small fragments
// at a time to the file.
class WritableFile {
WritableFile() { }
virtual ~WritableFile();
virtual Status Append(const Slice& data) = 0;
virtual Status Close() = 0;
virtual Status Flush() = 0;
virtual Status Sync() = 0;
leveldb 明确区分 input 和 output,进一步它又把 input 分为 sequential 和 random access,然后提炼出了三个简单的接口,每个接口只有屈指可数的几个函数。这几个接口在各个平台下的实现也非常简单明了(& ),一看就懂。
注意这三个接口使用了虚函数,我认为这是正当的,因为一次 IO 往往伴随着 context switch,虚函数的开销比起 context switch 来可以忽略不计。相反,iostream 每次 operator&&() 就调用虚函数,我认为不太明智。
Kyoto Cabinet
Kyoto Cabinet 也是一个 key-value db,是前几年流行的 Tokyo Cabinet 的升级版。它采用了与 leveldb 不同的文件抽象。
KC 定义了一个 File class,同时包含了读写操作,这是个 fat interface。
在具体实现方面,它没有使用虚函数,而是采用 #ifdef 来区分不同的平台(见 ),等于把两份独立的代码写到了同一个文件里边。
相比之下,Google leveldb 的做法更高明一些。
在 C++ 项目里边自己写个 File class,把项目用到的文件 IO 功能简单封装一下(以 RAII 手法封装 FILE* 或者 file descriptor 都可以,视情况而定),通常就能满足需要。记得把拷贝构造和赋值操作符禁用,在析构函数里释放资源,避免泄露内部的 handle,这样就能自动避免很多 C 语言文件操作的常见错误。
如果要用 stream 方式做 logging,可以抛开繁重的 iostream 自己写一个简单的 LogStream,重载几个 operator&&,用起来一样方便;而且可以用 stack buffer,轻松做到线程安全。
陈硕 (giantchen AT gmail) blog.csdn.net/Solstice 前几天我在新浪微博上出了两道有关 TCP 的思考题,引发了一场讨论
。 第一道初级题目是: 有一台机器,它有一个 IP,上面运行了一个 TCP 服务程序,程序只侦听一个端口,问:从理论上讲(只考虑 TCP/IP 这一层面,不考虑IPv6)这个服务程序可以支持多少并发 TCP 连接?答 65536 上下的直接刷掉。 具体来说,这个问题等价于:有一个 TCP 服务程序的地址是 1.2.3.4:8765,问它从理论上能接受多少个并发连接? 第二道进阶题目是: 一台被测机器 A,功能同上,同一交换机上还接有一台机器 B,如果允许 B 的程序直接收发以太网 frame,问:让 A 承担 10 万个并发 TCP 连接需要用多少 B 的资源?100万个呢? 从讨论的结果看,很多人做出了第一道题,而第二道题几乎无人问津。 & 这里先不公布答案(第一题答案见文末),让我们继续思考一个本质的问题:一个 TCP 连接要占用多少系统资源。 在现在的 Linux 操作系统上,如果用 socket()/connect() 或 accept() 来创建 TCP 连接,那么每个连接至少要占用一个文件描述符(file descriptor)。为什么说“至少”?因为文件描述符可以复制,比如 dup();也可以被继承,比如 fork();这样可能出现系统里边同一个 TCP 连接有多个文件描述符与之对应。据此,很多人给出的第一题答案是:并发连接数受限于系统能同时打开的文件数目的最大值。这个答案在实践中是正确的,却不符合原题意。 & 如果抛开操作系统层面,只考虑 TCP/IP 层面,建立一个 TCP 连接有哪些开销?理论上最小的开销是多少?考虑两个场景: 1. 假设有一个 TCP 服务程序,向这个程序成功发起连接需要做哪些事情?换句话说,如何才能让这个 TCP 服务程序认为有客户连接到了它(让它的 accept() 调用正常返回)? 2. 假设有一个 TCP 客户端程序,让这个程序成功建立到服务器的连接需要做哪些事情?换句话说,如何才能让这个 TCP 客户端程序认为它自己已经连接到服务器了(让它的 connect() 调用正常返回)? 以上这两个问题问的不是如何编程,如何调用 Sockets API,而是问如何让操作系统的 TCP/IP 协议栈认为任务已经成功完成,连接已经成功建立。 & 学过 TCP/IP 协议,理解三路握手的同学明白,TCP 连接是虚拟的连接,不是电路连接,维持 TCP 连接理论上不占用网络资源(会占用两头程序的系统资源)。只要连接的双方认为 TCP 连接存在,并且可以互相发送 IP packet,那么 TCP 连接就一直存在。 对于问题 1,向一个 TCP 服务程序发起一个连接,客户端(为明白起见,以下称为 faketcp 客户端)只需要做三件事情(三路握手): 1a. 向 TCP 服务程序发一个 IP packet,包含 SYN 的 TCP segment
1b. 等待对方返回一个包含 SYN 和 ACK 的 TCP segment 1c. 向对方发送一个包含 ACK 的 segment 在做完这三件事情之后,TCP 服务器程序会认为连接已建立。而做这三件事情并不占用客户端的资源(?),如果faketcp 客户端程序可以绕开操作系统的 TCP/IP 协议栈,自己直接发送并接收 IP packet 或 Ethernet frame 的话。换句话说,faketcp 客户端可以一直重复做这三件事件,每次用一个不同的 IP:PORT,在服务端创建不计其数的 TCP 连接,而 faketcp 客户端自己毫发无损。很快我们将看到如何用程序来实现这一点。 对于问题 2,为了让一个 TCP 客户端程序认为连接已建立,faketcp 服务端只需要做两件事情: 2a. 等待客户端发来的 SYN TCP segment 2b. 发送一个包含 SYN 和 ACK 的 TCP segment 2c. 忽视对方发来的包含 ACK 的 segment 在做完这两件事情(收一个 SYN、发一个 SYN+ACK)之后,TCP 客户端程序会认为连接已建立。而做这三件事情并不占用 faketcp 服务端的资源(?)换句话说,faketcp 服务端可以一直重复做这两件事件,接受不计其数的 TCP 连接,而 faketcp 服务端自己毫发无损。很快我们将看到如何用程序来实现这一点。 & 基于对以上两个问题的分析,说明单独谈论“TCP 并发连接数”是没有意义的,因为连接数基本上是要多少有多少。更有意义的性能指标或许是:“每秒钟收发多少条消息”、“每秒钟收发多少字节的数据”、“支持多少个活动的并发客户”等等。 faketcp 的程序实现 代码见:
可以直接用 make 编译 为了验证我上面的说法,我写了几个小程序来实现 faketcp,这几个程序可以发起或接受不计其数的 TCP 并发连接,并且不消耗操作系统资源,连动态内存分配都不会用到。 我家里有一台运行 Ubuntu Linux 10.04 的 PC 机,hostname 是 atom,所有的试验都在这上面进行。 家里试验环境的网络配置是:
陈硕在《》中曾提到“可以用 TUN/TAP 设备在用户态实现一个能与本机点对点通信的 TCP/IP 协议栈”,这次的试验正好可以用上这个办法。 试验的网络配置是:
具体做法是:在 atom 上通过打开 /dev/net/tun 设备来创建一个 tun0 虚拟网卡,然后把这个网卡的地址设为 192.168.0.1/24,这样 faketcp 程序就扮演了 192.168.0.0/24 这个网段上的所有机器。atom 发给 192.168.0.2~192.168.0.254 的 IP packet 都会发给 faketcp 程序,faketcp 程序可以模拟其中任何一个 IP 给 atom 发 IP packet。 程序分成几步来实现。 第一步:实现 icmp echo 协议,这样就能 ping 通 faketcp 了。 代码见
其中响应 icmp echo request 的函数在
这个函数在后面的程序中也会用到。 运行方法,打开 3 个命令行窗口: 1. 在第 1 个窗口运行 sudo ./icmpecho ,程序显示 allocted tunnel interface tun0 2. 在第 2 个窗口运行
$ sudo ifconfig tun0 192.168.0.1/24 $ sudo tcpdump -i tun0 3. 在第 3 个窗口运行 $ ping 192.168.0.2 $ ping 192.168.0.3 $ ping 192.168.0.234 发现每个 192.168.0.X 的 IP 都能 ping 通。 & 第二步:实现拒绝 TCP 连接的功能,即在收到 SYN TCP segment 的时候发送 RST segment。 代码见
运行方法,打开 3 个命令行窗口,头两个窗口的操作与前面相同,运行的 faketcp 程序是 ./rejectall 3. 在第 3 个窗口运行 $ nc 192.168.0.2 2000 $ nc 192.168.0.2 3333 $ nc 192.168.0.7 5555 发现向其中任意一个 IP 发起的 TCP 连接都被拒接了。 & 第三步:实现接受 TCP 连接的功能,即在收到SYN TCP segment 的时候发回 SYN+ACK。这个程序同时处理了连接断开的情况,即在收到 FIN segment 的时候发回 FIN+ACK。 代码见
运行方法,打开 3 个命令行窗口,步骤与前面相同,运行的 faketcp 程序是 ./acceptall。这次会发现 nc 能和 192.168.0.X 中的每一个 IP 每一个 PORT 都能连通。还可以在第 4 个窗口中运行 netstat –tpn ,以确认连接确实建立起来了。如果在 nc 中输入数据,数据会堆积在操作系统中,表现为 netstat 显示的发送队列(Send-Q)的长度增加。 & 第四步:在第三步接受 TCP 连接的基础上,实现接收数据,即在收到包含 payload 数据 的 TCP segment 时发回 ACK。 代码见
运行方法,打开 3 个命令行窗口,步骤与前面相同,运行的 faketcp 程序是 ./acceptall。这次会发现 nc 能和 192.168.0.X 中的每一个 IP 每一个 PORT 都能连通,数据也能发出去。还可以在第 4 个窗口中运行 netstat –tpn ,以确认连接确实建立起来了,并且发送队列的长度为 0。 这一步已经解决了前面的问题 2,扮演任意 TCP 服务端。 & 第五步:解决前面的问题 1,扮演客户端向 atom 发起任意多的连接。 代码见
这一步的运行方法与前面不同,打开 4 个命令行窗口。 1. 在第 1 个窗口运行 sudo ./connectmany 192.168.0.1
,表示将向 192.168.0.1:2007 发起 1000 个并发连接。 程序显示 allocted tunnel interface tun0press enter key to start connecting 192.168.0.1:2007 & 2. 在第 2 个窗口运行
$ sudo ifconfig tun0 192.168.0.1/24 $ sudo tcpdump -i tun0 3. 在第 3 个窗口运行一个能接收并发 TCP 连接的服务程序,可以是 httpd,也可以是 muduo 的 echo 或 discard 示例,程序应 listen 2007 端口。 4. 回到第 1 个窗口中敲回车,然后在第 4 个窗口中用 netstat -tpn 来观察并发连接。 & 有兴趣的话,还可以继续扩展,做更多的有关 TCP 的试验,以进一步加深理解,验证操作系统 TCP/IP 协议栈面对不同输入的行为。甚至可以按我在《》中提议的那样,实现完整的 TCP 状态机,做出一个简单的 mini tcp stack。 & 第一道题的答案: 在只考虑 IPv4 的情况下,并发数的理论上限是 2**48。考虑某些 IP 段被保留了,这个上界可适当缩小,但数量级不变。实际的限制是操作系统全局文件描述符的数量,以及内存大小。 一个 TCP 连接有两个 end points,每个 end point 是 {ip, port},题目说其中一个 end point 已经固定,那么留下一个 end point 的自由度,即 2 ** 48。客户端 IP 的上限是 2**32 个,每个客户端IP发起连接的上限是 2**16,乘到一起得理论上限。 即便客户端使用 NAT,也不影响这个理论上限。(为什么?) & 在真实的 Linux 系统中,可以通过调整内核参数来支持上百万并发连接,具体做法见:
陈硕 (giantchen AT gmail) blog.csdn.net/Solstice Muduo 全系列文章列表:
本文以一个 Sudoku Solver 为例,回顾了并发网络服务程序的多种设计方案,并介绍了使用 muduo 网络库编写多线程服务器的两种最常用手法。以往的例子展现了 Muduo 在编写单线程并发网络服务程序方面的能力与便捷性,今天我们看一看它在多线程方面的表现。 本文代码见: Sudoku Solver 假设有这么一个网络编程任务:写一个求解数独的程序 (Sudoku Solver),并把它做成一个网络服务。 Sudoku Solve

我要回帖

更多关于 高中物理选修3 1 的文章

 

随机推荐