How Does it End()?

How Does It End()?

 

By 刘未鹏

C++的罗浮宫(http://blog.csdn.net/pongba)

 

The End() of the Story

STL里的end()看似是个简单得不能再简单的函数,遵从内建数组的遍历手法,end()返回指向最后一个元素之后一位的迭代器,从而使得使用end()的循环遍历变得更容易和直观。然而,由于end()的特殊性(即它从逻辑上和物理上都不属于指向其容器内元素的迭代器,故而无法对其解引用),从而会导致一些微妙的问题,使得看上去完全无辜的代码也会出现莫明其妙的错误。

 

这里要说的便是一个这样的例子,是在教MM C++的时候无意间发现的。代码是要实现一个字符串的split()算法,一个经典的课后作业,呵呵:) Anyway,片刻之后写出来的代码像这个样子:

 

namespace MyPrj

{

  namespace detail

  {

    template<typename InputIter>

    InputIter skip(InputIter iter, InputIter end, typename InputIter::value_type c = ' ')

    {

      while(iter != end && *iter == c)++iter;

      return iter;

    }

  } // namespace detail

 

template<typename SequenceT, typename StringT>

  inline  SequenceT&

    split(SequenceT& seq, StringT s, typename StringT::value_type delim)

  {

    typename StringT::iterator end   = s.end(),

                            start  = detail::skip(s.begin(),end,delim),

                            over  = start;

 

    for(;start != end;++over)

    {

      if(*over == delim || over == end)

      {

        seq.push_back(StringT(start,over));

        start = over = detail::skip(over,end,delim);

      }

}

    return seq;

  }

} // namespace MyProj

 

上面这个小小的函数看上去很简单:首先我们跳过所有可能存在的前导分隔符(分隔符一般为空格),然后进入一个循环,over迭代器每次后移一格,一旦发现等于分隔符的字符或者发现到了串的结尾(显然,结尾总是算作分隔符的),就把卡在[start,over)区间内的串拷贝到目标容器内(注意,start用于记住上次拷贝出的区间往后的第一个非分隔符的地方),并同时把startover后移(skip)到下一个非分隔符处以便进行下一轮的卡位-拷贝。循环结束条件为start == end(注意,决不能是over == end,想想为什么),也就是说如果在某一次拷贝完毕,将startover往后skip掉所有分隔符之后,我们发现已经到了串末尾,这时就代表不用接着循环了。

 

很直观的逻辑是不是?然而编程跟数学的区别之一就在于编程随所用语言的不同会需要程序员去注意各种各样的细节。

 

所以以上这寥寥数行代码中存在着两处非常隐诲的错误,你能直接看出来吗?(至少我没有:(

 

We End() Up with a big Assertion!

使用”C++ Rocks!”为测试串来编译运行上述代码会发现得到的是一个大大的Assertion!(如果你用的是Debug模式并且你的STL实现支持迭代器的RangeCheck的话)。当然,在不支持RangeCheckSTL实现上也许你会得到一个无声崩溃,甚至更恐怖的——顺利运行。后一种情况等于埋下了一颗不定时炸弹。代码完全不可移植,换一个STL实现可能就挂了。

 

在使用STL的时候,遇到莫明其妙的崩溃或是明确的Assertion,第一时间想到的是越界/非法访问(似乎这句话不光对STL适用吧,呵呵:))。而上面这个函数则是熊掌兼得了,一个越界,一个非法访问。所以

 

To That End(), …

实际上直接引发我们的测试崩溃的那个bug是非法访问,更直接地说就是对end()解引用。但,怎么会呢?前面不是说过,会对start == end的情况作检查吗?事实上,这个对end()解引用的行为发生在over迭代器递增到达串结尾的时候,此时我们进入循环体,执行if判断,我们发现if判断里面的逻辑或的两个分式是*over == delim在前,over == end在后,而此时over == end()! 然而,虽说over == end成立从而足够使得该逻辑或判断成立,然而由于这一分式在后面,所以先被求值的表达式是*over == delim,而*over就是对end()解引用,于是还没等到看一眼后一个逻辑分式(over ==end)是否成立,就悲壮的挂了!出师未捷身先死。(你看,数学上并列可交换的逻辑或,到了程序里面就变成不可交换的了:))这个问题有一个很简单的解决方案——把这两个逻辑分式互换位置。只不过这种做法非常晦涩和危险,跟利用&&的短路性质一样,可读性和可维护性都不佳,但眼下也管不了这么多了,先通过测试再说,完了大不了再重构就是了。

 

于是交换两者顺序,编译,执行。哗啦~再次牺牲。为什么呢?稍稍跟踪一下便会发现,问题出在循环结束条件的摆放位置上。看起来放在循环继续条件中的“start!=end”,跟在循环体中放置“if(start == end) break;”没什么区别,如:

 

    for(;;++over)

    {

      if(over == end || *over == delim)

      {

        seq.push_back(StringT(start,over));

        start = over = detail::skip(over,end,delim);

      }

      if(start == end)

        break;

}

 

然而这两者实际上却是有着很微妙但实质性的区别的,看一下它们的逻辑图示便清楚了:

原先的逻辑的示意图

 

修改后的逻辑示意图

 

这两者的拓扑结构是不一样的。前者的over!=end检查位于区间提取操作之前,完了之后进行可能的区间提取,start/over后移,并接着直接++over。这种安排下,如果中间进行了区间提取并且将start/over后移到了串末尾,接下来的++over便会使over超过end(),越界!而第二种拓扑就不一样了,over!=end位于区间提取、start/over后移之后,这样就能及时发现start/over后移到串末尾的情况,从而阻止下面的++over操作越界。

 

此外,实际上”if(start == end) break;”也可以放在前面那个if的里面,像这样:

 

    for(;;++over)

    {

      if(over == end || *over == delim)

      {

        seq.push_back(StringT(start,over));

        start = over = detail::skip(over,end,delim);

        if(start == end)

          break;

      }

}

 

这是因为导致start==end的只可能是由于start被后移了,而导致start后移的只可能是由于外围的if成立。这样修改了之后,就免除了每重循环都去检查if(start == end)的额外开销。虽然对于这个小函数来说根本无关痛痒,然而节俭是美德:)说不定遇到什么场合下这类节省可以省下可观开销呢。

 

最后,由于循环结束检查被放到了循环体中部,所以一开始就是直接进入循环的了。然而,如果一开始循环就不该进入呢?比如说,一个空串,这时候start/over一开始就等于end,而按照上面的逻辑,循环仍然进入,if(over == end || *over == delim)仍然满足,结果往seqpush了一个空串。然后,才是if(start == end)check,从而退出循环。那个pushseq的空串虽然无伤大雅,然而并不符合算法向外界提供的保证,所以最好还是在进入循环之前检查一下start是否等于end,是就立即让函数返回吧。

 

The End()

说实话这种看似简单直观的基础编程题其实还是挺锻炼基本编程素养的,毕竟,墙也是一块块砖垒起来的。

 

不过,实际编程中这类情况还真罕见,即便有也可以用其它方案绕过去。如:

 

    while(start!=end)

    {

      over = std::find(over,end,delim);

      seq.push_back(StringT(start,over));

      start = over = detail::skip(over,end,delim);

}

 

以上做法由于将寻找下一个分隔符“skip连续的分隔符这两个逻辑分离了开来(前面的实现是把“skip连续的分隔符套在了寻找分隔符逻辑的内部了,耦合在一起,所以弄得逻辑异常复杂。所以还是谨遵爱因斯坦的名言吧:)别乱耍花枪。

 

不过话说回来(类)STL容器的迭代器越界/失效一直都是令STL使用者们头大的问题之一,所以才诞生了如safe_STL之类的为STL安全防护网的库,VC2005STL实现(by P.J Plauger)也带了安全性检查。不过,这些安全性检查能够带来的也只是令错误提前或显式表现出来(如,给出一个assertion失败消息而不是悄无声息地崩溃甚至不崩溃而访问随机数据),真正查错还是要自己动手的。不过毕竟还是帮了很大忙的,俗话说隐患险于明火嘛。然而别忘了还有一句防患胜于救灾,所以平素编程时多留个心眼可以免得事后亡羊补牢。

 

End()-less

P.S. 近日将放出一组关于C++0x核心进展的简讯,由于事情多,尽管有好几个特性想好好单独来个全景回顾的(如ConceptsVariadic Templates,甚至Opaque Type),可惜没时间去细细理一理那些proposal,然而为了最近的几个令我开心的进展,还是忍不住做一个近期的介绍,顺便也让感兴趣的朋友对即将到来的0x标准热一热身:)

<think>好的,我现在需要回答用户关于SMTP和HTTP如何标记消息体结束,以及HTTP是否可以使用SMTP的方法的问题。首先,我得回忆一下这两个协议的相关知识,并确保我的理解正确。 首先,SMTP的消息结束标记。我记得SMTP使用一个特定的序列来表示消息体的结束,即CRLF.CRLF。当客户端发送完邮件内容后,会发送一个单独的行,包含一个点号(.),后面跟着回车换行(CRLF)。服务器看到这个点号就会知道消息体结束了。例如,用户发送完数据后,发送“\r\n.\r\n”,服务器就会停止读取。这个机制在SMTP中很常见,但需要确保内容中的点号不会误触发,所以实际应用中会对数据进行点填充(dot-stuffing),即在内容中的每个行首点号前再加一个点号,服务器处理时会去掉多余的。 然后是HTTP的消息结束标记。HTTP有几种方法来确定消息体的结束。首先是Content-Length头部,指定了消息体的确切字节数。服务器读取到指定长度的数据后就会停止。其次是分块传输编码(Transfer-Encoding: chunked),这种情况下,消息体被分成多个块,每个块前面有十六进制的大小,最后有一个大小为0的块表示结束。另外,对于HTTP/1.0,有时也依靠服务器在发送完数据后关闭连接来标记结束,但这种方法不够可靠,现在主要用前两种方法。 接下来,用户问HTTP是否可以使用SMTP的方法,也就是用CRLF.CRLF来标记消息体结束。我需要分析两者的差异和兼容性。SMTP的方法基于文本协议,而HTTP支持二进制数据,使用分块或Content-Length更灵活。如果HTTP采用类似SMTP的点号结束,可能会遇到问题,比如消息体中如果包含同样的点号序列,就会导致提前终止,而HTTP需要处理各种类型的数据,包括二进制,这样就会有冲突。此外,HTTP已经有成熟的机制来处理消息体结束,没有必要改用SMTP的方法,而且改动协议本身会影响现有的应用和兼容性。 还需要考虑协议设计的上下文。SMTP主要用于传输文本邮件,而HTTP需要处理更多类型的数据,包括文件上传、图片等,所以需要更可靠的方法。分块编码允许流式传输,而Content-Length适用于已知大小的内容,这两种方法都比SMTP的结束符更灵活和可靠。 最后,可能用户想知道是否有其他协议采用类似SMTP的方法,或者在HTTP中是否有特殊情况可以使用。但根据我的知识,HTTP标准不支持这样的结束符,而是依赖现有的头部字段。所以结论是HTTP不能也不需要使用SMTP的方法,两者设计目标和需求不同,导致采用不同的机制。</think>### SMTP和HTTP消息体结束标记机制解析 #### 一、SMTP协议的消息体结束标记 SMTP协议使用**点号终结符**(Dot-stuffing)标记消息体结束。具体表现为: 1. 发送方在消息正文后单独发送一行`CRLF.CRLF`(即`\r\n.\r\n`) 2. 接收方检测到该模式时,停止读取消息体 3. 为防止正文中的自然换行符`.`被误判,SMTP要求发送方对正文中的行首`.`进行转义(添加额外`.`),接收方自动删除转义符[^1] ```text 示例: From: alice@example.com To: bob@example.com Subject: Test This is a message. . // 实际正文中的行首点会被转义为两个点 . // 结束标记 ``` #### 二、HTTP协议的消息体结束标记 HTTP协议使用三种方式确定消息体边界: 1. **Content-Length头部**(精确指定字节数) ```http HTTP/1.1 200 OK Content-Length: 1234 ``` 2. **分块传输编码**(Chunked Transfer Encoding) ```http HTTP/1.1 200 OK Transfer-Encoding: chunked 4\r\n Wiki\r\n 5\r\n pedia\r\n 0\r\n\r\n // 结束标记 ``` 3. **连接关闭**(HTTP/1.0遗留方法,非可靠方式) #### 三、HTTP能否使用SMTP的结束标记方法? **不能直接使用**,原因包括: 1. **协议设计差异**: - SMTP是文本协议,HTTP需要支持二进制数据 - SMTP的点号转义机制会增加HTTP处理二进制流的复杂度 2. **冲突风险**: - HTTP消息体中可能天然包含`\r\n.\r\n`序列 - 图像、压缩文件等二进制数据可能随机生成该模式 3. **现有标准兼容性**: - HTTP/1.1规范明确要求使用Content-Length或分块编码(RFC 7230 Section 3.3.3)[^1] - 引入新结束标记机制会破坏现有客户端/服务器实现 4. **性能考量**: - SMTP需要逐字节扫描结束符,影响HTTP大文件传输效率 - HTTP的分块编码支持流式传输,更适应现代Web需求 #### 四、协议设计启示 两种协议的不同选择反映了其应用场景的特性: - SMTP面向文本消息,采用简单终结符 - HTTP面向异构数据,采用精确长度控制 - SMTP需要消息完整性,HTTP优先考虑传输效率
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值