2007年12月23日星期日

IP Multicasting

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

介绍IP组播基本概念的几个链接:
http://www.tcpipguide.com/free/t_IPMulticasting.htm
http://www.cisco.com/univercd/cc/td/doc/cisintwk/ito_doc/ipmulti.htm
http://www.cisco.com/univercd/cc/td/doc/product/software/ios121/121newft/121t/121t3/dtssm.htm#wp1026772

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

IP组播应用中的三个主要技术问题:
1. Multicast Addressing (组播编址方法)
2. Multicast Group Management (IGMP related) (有效的通知和交付机制)
3. Multicast Datagram Processing and Routing (有效的网络间转发工具)


Scope of IPv4 and IPv6 multicast addresses


* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

(转载)
组播报文基于组的概念,它的接收对象是特定组的主机。想接收组播数据的主机需要利用IGMP报文申明加入某个组播组。
组播报文采用D类地址,即开头为1110的IP地址,范围是224.0.0.0 - 239.255.255.255,映射到01 00 5e 00 00 00 - 01 00 5e 7f ff ff的MAC地址上面。组播地址只可用作目的地址。
一般情况下,网卡只接收MAC地址与自身匹配或者广播地址的数据包,但在802.3标准中,定义了MAC地址的第一个字节的最低位用来指示是否一个组播/广播数据包。标记为组播/广播数据包的仍然会被网卡接收处理。

那为什么我的主机没有申请加入任何组播组,却仍然截获了组播报文呢?原来二层交换机默认会把需要转发的组播包转发给子网内的所有机器,这使得组播机制有名无实。 在二层交换机中一般会采用CGMP或者IGMP Snooping来弥补这一点,前者是Cisco的专有技术,后者是IEEE标准。如果一个交换机启用IGMP Snooping,它会在每个组播包进入交换机之后,解析报文头,如果它是一帧IGMP报文,则按照报文的内容将连接该主机的端口保存到组播表或者从中删除。如果是普通的组播包,则根据设备里保存的组播表进行转发。因为二层交换机不能直接区分IGMP和普通组播包,所以对于每个广播/组播包都要执行这样的检查,在低端设备中通常在软件上实现,对交换速度有很大影响。高端设备采用专用的ASIC,在硬件里实现。
(转载)

CGMP和IGMP Snooping的区别(在IGMPv2中)
两者都是为了解决二层switch转发多播包时的效率问题。
在实现上两者的主要区别在于在何处解析IGMP包的内容。
在CGMP 的实现中,是由multicast router在收到某主机通过switch传过来的IGMP report时,发送一条CGMP join消息给switch, switch收到后会把该主机的相关信息加入到对应多播组的content addressable memory (CAM) table里。
在IGMP Snooping中,解析IGMP包的工作是在二层switch上做的。switch会检查所有通过它外发的多播包,分辨其中的IGMP report消息并在收到该类消息时把发送的主机的信息保存到对应组的table entry里。
由于IGMP Snooping的实现中,switch需要检查所有的多播包,所以在数据吞吐量很大时会对switch的性能有很大影响,所以IGMP Snooping技术一般只实现在高端的switch中。而CGMP技术可以被应用在低端的switch中。关于二者的更详尽的阐述可参考本文的第二个链接。

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

IGMP和组播编址方法指定了主机怎样和本地路由器交互,如何通过单个网络传送组播数据报,但没有指定路由器怎样交换群组成员信息,或者怎样确保每个数据报的副本能够到达所有群组成员。

常规转发/选路和组播转发/选路的区别
  • 在单播选路中,只有当拓扑结构改变或设备出故障时才会发生路由改变;而组播路由则不同,应用程序加入或退出一个组播群组就会发生组播路由的变化。
  • 组播转发需要路由器检查多个目的网络。
  • 组播数据报可以从非组播群组成员的计算机上发起,并且可以路由通过没有任何组播成员的网络。

基本组播选路:
因为组播目的代表了一个计算机集合,最佳转发系统将使数据报到达该集合中的所有成员,并且一个数据报不会两次通过同一个网络。
为了避免选路环路,组播路由器必须依靠数据报的源地址。

当进行转发决策时,组播路由器使用了数据报的源地址和目的地址。
基本转发机制称为截尾反向路径转发(TRPF - Truncated Reverse Path Forwarding)
组播选路的一种最早形式是从TRPF派生的,称为反向路径组播RPM(Reverse Path Multicasting)
最早的组播选路协议之一是矢量距离组播选路协议DVMRP(Distance Vector Multicast Routing Protocol). mrouted是在UNIX下实现DVMRP的著名程序。

DVMRP的局限性意味着他从规模上无法处理大量路由器、更大量的组播群组或者成员关系的迅速变化。因此其不适合作为Internet的通用组播选路协议。
为克服DVMRP的局限性,IETF研究了其他一些组播协议:
  • 核心基干树CBT(Core Based Trees)
  • 协议无关组播PIM(Protocol Independent Multicast)
  • OSPF组播扩展(MOSPF)

Multicast Distribution Trees:
Multicast router会创建multicast distribution trees来控制发送traffic的路径。
一般会有两种建立分布树的算法:Source Trees 和 Shared Trees。
详尽的阐述参考本文第二个链接。

组播转发树被定义为一系列通过组播路由器的路径,这些路径从源站到组播群组的所有成员。对于某组播群组,每个可能的数据报源都能确定一个不同的转发树。

组播选路的实质:
成员关系问题是选路的核心 - 所有组播选路方法都提供了一种传播成员信息的机制,还提供了转发数据报时使用该信息的方式。
一般来讲,因为成员关系会迅速改变,从给定路由器可得到的信息是不完整的,因此选路可能滞后于变化。所以,组播设计代表了选路通信量超载和低效数据传输之间的一种折衷。

可靠组播和ACK内爆(ACK implosion):
可靠组播(reliable multicast)指的是这样的系统:使用组播交付并能够保证所有群组成员收到按序到达、无丢失、无重复且未遭破坏的数据。
为了避免ACK内爆问题,可靠组播方法或者使用确认点层次结构,或者发送冗余信息。

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

2007年12月18日星期二

DMZ

DMZ是英文“demilitarized zone”的缩写。它是为了解决安装防火墙后外部网络不能访问内部网络服务器的问题,而设立的一个非安全系统与安全系统之间的缓冲区,这个缓冲区位于企业内部网络和外部网络之间的小网络区域内。DMZ可以理解为一个不同于外网或内网的特殊网络区域。DMZ内通常放置一些不含机密信息的公用服务器,比如Web、 Mail、FTP等。这样来自外网的访问者可以访问DMZ中的服务,但不可能接触到存放在内网中的公司机密或私人信息等。即使DMZ中服务器受到破坏,也 不会对内网中的机密信息造成影响。网络结构如下图所示:


DMZ防火墙方案为要保护的内部网络增加了一道安全防线,通常认为是非常安全的。同时它提供了一个区域放置公共服务器,从而又能有效地避免一些互联应用需要公开,而与内部安全策略相矛盾的情况发生。在DMZ区域中通常包括堡垒主机、Modem池,以及所有的公共服务器,但要注意的是电子商务服务器只能用作用户连接,真正的电子商务后台数据需要放在内部网络中
在这个防火墙方案中,包括两个防火墙,外部防火墙抵挡外部网络的攻击,并管理所有内部网络对DMZ的访问。内部防火墙管理DMZ对于内部网络的访问。内部防火墙是内部网络的第三道安全防线(前面有了外部防火墙和堡垒主机),当外部防火墙失效的时候,它还可以起到保护内部网络的功能。而局域网内部,对于 Internet的访问由内部防火墙和位于DMZ的堡垒主机控制。
  • DMZ网络访问控制策略
当规划一个拥有DMZ的网络时候,我们可以明确各个网络的以下六条访问控制策略:

1. 内网可以访问外网
内网的用户显然需要自由地访问外网。在这一策略中,防火墙需要进行源地址转换。

2. 内网可以访问DMZ
此策略是为了方便内网用户使用和管理DMZ中的服务器。

3. 外网不能访问内网
很显然,内网中存放的是公司内部数据,这些数据不允许外网的用户进行访问。

4. 外网可以访问DMZ
DMZ中的服务器本身就是要给外界提供服务的,所以外网必须可以访问DMZ。同时,外网访问DMZ需要由防火墙完成对外地址到服务器实际地址的转换。

5. DMZ不能访问内网
很明显,如果违背此策略,则当入侵者攻陷DMZ时,就可以进一步进攻到内网的重要数据。

6. DMZ不能访问外网
此条策略也有例外,比如DMZ中放置邮件服务器时,就需要访问外网,否则将不能正常工作。在网络中,非军事区(DMZ)是指为不信任系统提供服务的孤立网段,其目的是把敏感的内部网络和其他提供访问服务的网络分开,阻止内网和外网直接通信,以保证内网安全。
  • DMZ服务配置
DMZ提供的服务是经过了地址转换(NAT)和受安全规则限制的,以达到隐蔽真实地址、控制访问的功能。首先要根据将要提供的服务和安全策略建立一个清晰的网络拓扑,确定DMZ区应用服务器的IP和端口号以及数据流向。通常网络通信流向为禁止外网区与内网区直接通信,DMZ区既可与外网区进行通信,也可以 与内网区进行通信,受安全规则限制

1. 地址转换
DMZ区服务器与内网区、外网区的通信是经过网络地址转换(NAT)实现的。网络地址转换用于将一个地址域(如专用Intranet)映射到另一个地址域 (如Internet),以达到隐藏专用网络的目的。DMZ区服务器对内服务时映射成内网地址,对外服务时映射成外网地址。采用静态映射配置网络地址转换时,服务用IP和真实IP要一一映射,源地址转换和目的地址转换都必须要有。

2. DMZ安全规则制定
安全规则集是安全策略的技术实现,一个可靠、高效的安全规则集是实现一个成功、安全的防火墙的非常关键的一步。在建立规则集时必须注意规则次序,因为防火墙大多以顺序方式检查信息包,同样的规则,以不同的次序放置,可能会完全改变防火墙的运转情况。如果信息包经过每一条规则而没有发现匹配,这个信息包便会被拒绝。一般来说,通常的顺序是,较特殊的规则在前,较普通的规则在后,防止在找到一个特殊规则之前一个普通规则便被匹配,避免防火墙被配置错误。

DMZ安全规则指定了非军事区内的某一主机(IP地址)对应的安全策略。由于DMZ区内放置的服务器主机将提供公共服务,其地址是公开的,可以被外部网的用户访问,所以正确设置DMZ区安全规则对保证网络安全是十分重要的。

Firewall可以根据数据包的地址、协议和端口进行访问控制。它将每个连接作为一个数据流,通过规则表与连接表共同配合,对网络连接和会话的当前状态进行分析和监控。其用于过滤和监控的IP包信息主要有:源IP地址、目的IP地址、协议类型(IP、ICMP、TCP、UDP)、源TCP/UDP端口、 目的TCP/UDP端口、ICMP报文类型域和代码域、碎片包和其他标志位(如SYN、ACK位)等。

为了让DMZ区的应用服务器能与内网中DB服务器(服务端口4004、使用TCP协议)通信,需增加DMZ区安全规则, 这样一个基于DMZ的安全应用服务便配置好了。其他的应用服务可根据安全策略逐个配置。

DMZ无疑是网络安全防御体系中重要组成部分,再加上入侵检测和基于主机的其他安全措施,将极大地提高公共服务及整个系统的安全性。

2007年12月16日星期日

RTP/RTCP

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RFC3550 - RTP: A Transport Protocol for Real-Time Applications

http://www.ietf.org/rfc/rfc3550.txt?number=3550

RFC3551 - RTP Profile for Audio and Video Conferences with Minimal Control
http://www.ietf.org/rfc/rfc3551.txt?number=3551

RFC2198 - RTP Payload for Redundant Audio Data
http://www.ietf.org/rfc/rfc2198.txt?number=2198

RFC2205 - Resource ReSerVation Protocol (RSVP)
http://www.ietf.org/rfc/rfc2205.txt?number=2205

RFC2750 - RSVP Extensions for Policy Control
http://www.ietf.org/rfc/rfc2750.txt?number=2750

RFC3936 - Procedures for Modifying the Resource reSerVation Protocol (RSVP)
http://www.ietf.org/rfc/rfc3936.txt?number=3936

RFC4495 - A Resource Reservation Protocol (RSVP) Extension for the Reduction of Bandwidth of a Reservation Flow
http://www.ietf.org/rfc/rfc4495.txt?number=4495

RFC2748 - The COPS (Common Open Policy Service) Protocol
http://www.ietf.org/rfc/rfc2748.txt?number=2748

RFC2749 -
COPS usage for RSVP
http://www.ietf.org/rfc/rfc2749.txt?number=2749

RFC4261 - Common Open Policy Service (COPS) Over Transport Layer Security (TLS)
http://www.ietf.org/rfc/rfc4261.txt?number=4261

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - -

因为IP互联网不是等时系统,所以在发送实时数字数据时需要额外的协议支持。除了允许检测复制或重新排序的分组的基本次序信息之外,每个分组还必须传送单独的时间戳,告诉接收方应该播放分组中的数据的准确时间。
如果网络中有抖动,接收方需要实现回放缓冲区来准确地重建信号。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

在IP互联网上传输数字音频或视频信号所使用的协议是实时传输协议 (Real-Time Transport Protocol, RTP)。RTP不包含确保及时交付的机制,必须由底层系统来保证。RTP是

RTP提供两个关键的特性:每个分组中的序号以及时间戳。序号允许接收方检测不按顺序的交付或数据丢失,时间戳允许接收方控制回放。

设计RTP是为了传送包括音频和视频等广泛的实时数据,所以RTP不强制统一的语义解释,而是每个分组以固定的首部开头,首部中的字段指定如何解释其余的首部字段以及如何解释有效载荷。

***********
*********************************************************************
The RTP header has the following format:
    0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| payload (audio, video....) |
| +-+-+-+-+-+-+-+-+-+-+-+-+
| ....| padding | count |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
开始12个八进制出现在每个RTP包中,而CSRC标识列表仅出现在混合器插入时。各段含义如下:
①版本(V
2位,标识RTP版本。
②填充标识(P
1位,如设置填充位,在包尾将包含附加填充字,它不属于有效载荷。填充的最后一个八进制包含应该忽略的八进制计数。某些加密算法需要固定大小的填充字,或为在底层协议数据单元中携带几个RTP包。
③扩展(X
1位,如设置扩展位,固定头后跟一个头扩展。
CSRC计数(CC
4位,CSRC计数包括紧接在固定头后CSRC标识符个数。
⑤标记(M
1位,标记解释由设置定义,目的在于允许重要事件在包流中标记出来。设置可定义其他标示位,或通过改变位数量来指定没有标记位。
⑥载荷类型(PT
7位,记录后面资料使用哪种 Codec , receiver 端找出相应的 decoder 解碼出來。
常用 types
Payload Type
Codec
0
PCM μ -Law
8
PCM-A Law
9
G..722 audio codec
4
G..723 audio codec
15
G..728 audio codec
18
G..729 audio codec
34
G..763 audio codec
31
G..761 audio codec
⑦系列号
16位,系列号随每个RTP数据包而增加1,由接收者用来探测包损失。系列号初值是随机的,使对加密的文本攻击更加困难。
⑧时标
32位,时标反映RTP数据包中第一个八进制数的采样时刻,采样时刻必须从单调、线性增加的时钟导出,以允许同步与抖动计算。时标可以让receiver端知道在正确的时间将资料播放出来。
由上图可知,如果只有系列号,并不能完整按照顺序的将data播放出来,因为如果data中间有一段是没有资料的,只有系列号的话会造成错误,需搭配上让它知道在哪个时间将data正确播放出来,如此我们才能播放出正确无误的信息。
SSRC
32位,SSRC段标识同步源。此标识不是随机选择的,目的在于使同一RTP包连接中没有两个同步源有相同的SSRC标识。尽管多个源选择同一个标识的概率很低,所有RTP实现都必须探测并解决冲突。如源改变源传输地址,也必须选择一个新SSRC标识以避免插入成环行源。
CSRC列表
0到15项,每项32位。CSRC列表表示包内的对载荷起作用的源。标识数量由CC段给出。如超出15个作用源,也仅标识15个。CSRC标识由混合器插入,采用作用源的SSRC标识。
********************************************************************************


RTP的关键部分是对变换 (也就是在中间位置改变数据流的编码)或混合 (也就是从多个源接收数据流,把它们组合成一个数据流,然后发送结果)的支持。混合和组播技术的结合能使交付给每个参与主机的数据报减少。

RTP首部中的字段标识发送方,指示是否发生混合。同步源标识符字段指定数据流的源站。每个源站必须选择一个惟一的32位标识符,如果发生冲突,则协议包括解决冲突的机制。当混合器组合多个数据流时,混合器就变成新的数据流的同步源。但是有关初始源的信息没有丢失,这是因为混合器使用大小可变的参与源ID字段提供正在混合的数据流的同步ID。4位CC字段给出参与源的数目,最多可以列出15个源。

传统的TCP 协议是一个面向连接的协议,它的重传机制和拥塞控制机制都是不适用于实时多媒体传输的。RTP 是一个应用型的传输层协议,它并不提供任何传输可靠性的保证和流量的拥塞控制机制。RTP不像传输协议那样发挥作用,在实际应用中不会在IP中直接封装,而是RTP在UDP上运行,这意味着把每条RTP报文封装到UDP数据报中。使用UDP的主要优点是并发性 -- 单个计算机可以有多个使用RTP的应用程序,而不会互相干扰。RTP不使用保留的UDP端口号,而是为每个会话分配一个端口,因此必须把端口号通知给远程的应用程序。通常情况下RTP选择偶数UDP端口号。



- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

RTP控制协议 (RTCP),是RTP的一个完整部分,提供需要的控制功能。RTCP允许发送方和接收方相互传输一系列报告,这些报告包含有关正在传输的数据以及网络性能的额外信息。
在RTP会话期间,各参与者周期性地传送RTCP包,包中含有已发送的数据包的数量、丢失的数据包的数量等统计资料。因此,服务器可以利用这些信息动态地改变传输速率,甚至改变有效载荷类型。RTP和RTCP配合使用,能以有效的反馈和最小的开销使传输效率最佳化,故特别适合传送网上的实时数据。

当应用程序开始一个rtp会话时将使用两个端口:一个给rtp,一个给rtcp。rtp本身并不能为按顺序传送数据包提供可靠的传送机制,也不提供流量控制或拥塞控制,它依靠rtcp提供这些服务。RTCP报文封装在UDP中,以便进行传输,发送时使用比它们所属的RTP流的端口号大1的协议号。

RTCP使用5个基本报文类型允许发送方和接收方交换有关会话的信息。

类型 含义
-------------------------------------------------------------
200 发送方报告 (SR)
201 接收方报告 (RR)
202 源描述报文 (SDES)
203 结束报文 (BYE)
204 应用程序特定报文 (APP)

1. 发送方在停止发送数据流时传输一条结束报文
2. 应用程序特定报文提供了基本功能的扩展,以允许应用程序定义报文类型。
3. 接收方周期性地传输接收方报告报文,向源通知接收的条件。
4. 发送方周期性地传输发送方报告报文,提供绝对的时间戳。而绝对时间戳为接收方提供了使多个数据流同步的机制。
5. 发送方还传输源站描述报文,提供有关拥有对源站控制权的用户的常规信息。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

H.323不是单个协议,而是制定如何把多个协议组合起来,形成一个实用的IP电话技术系统。

H.323为IP电话技术使用的协议:
协议 目的
-----------------------------------------------
H.225.0 建立通话所使用的信令
H.245 在通话中控制和返回
RTP 实时数据传输(序列和时限)
T.120 交换和通话有关的数据


SIP只涵盖信令,不推荐特殊的编码,也不要求对实时传输使用RTP。SIP使用C/S交互方式。为了提供有关通话的信息,SIP要依靠一个伴生的协议,即会话说明协议 (SDP)。

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IETF提供的在IP环境中的QoS方案:
资源预留协议 (Resource Reservation Protocol, RSVP)
通用开放策略服务 (Common Open Policy Services, COPS)

RSVP处理预留请求并进行应答,它不是路由协议,也不强制在建立数据流时就启用策略,而是在发送任何数据之前运转。每个RSVP流是单工的 (simplex),也就是单向传输的。
端点使用RSVP在指定了QoS界限的IP互联网上请求单工数据流。如果路径上的路由器同意请求,则认可数据流,否则拒绝数据流。如果应用程序需要两个方向上的QoS,则每个端点必须使用RSVP请求单独的数据流。

当RSVP请求抵达时,路由器必须对两个方面进行评估:可行性(也就是路由器是否具有满足请求的资源)和策略(也就是请求是否符合策略约束)。可行性是本地决策,而策略强制要求全局合作。

要实现全局策略,IETF使用两级模型,用C/S模式在两级之间交互作用。当路由器接收到RSVP请求时,它就变成客户,与服务器协商以确定请求是否符合策略约束,该服务器称为策略确定点(Policy Decision Point, PDP)。PDP不处理通信量,只对请求进行评估,检查它是否符合全局策略。如果PDP认可请求,则路由器必须作为策略执行点(Policy Enforcement Point, PEP)进行操作,确保通信量不会超过认可的策略。


COPS协议定义在路由器和PDP之间的C/S交互作用,或者如果组织有多级策略服务器,就定义路由器和本地PDP之间的C/S交互作用。虽然COPS定义它自己的报文首部,但是底层格式和RSVP共享许多具体字段。而且,对于请求报文中的单个数据项,COPS使用和RSVP一样的格式。这样,当路由器接收到RSVP请求时,它可以提取和策略相关的数据项,把它们放在COPS报文中,把结果发送给PDP。

2007年12月10日星期一

Understand inline

(转载)
inline 函数在调用时不仅仅可以避免函数调用的成本。在典型情况下,编译器的优化是为了一段连续的没有函数调用的代码设计的,所以当你 inline化一个函数,你可能就使得编译器能够对函数体实行上下文相关的特殊优化。大多数编译器都不会对"outlined"函数调用实行这样的优化。


然而inline 函数背后的思想是用函数本体代替每一处对这个函数的调用,这样可能会增加你的目标代码的大小。在有限内存的机器上,过分热衷于inline 化会使得程序对于可用空间来说过于庞大。即使使用了虚拟内存,inline 引起的代码膨胀也会导致附加的分页调度,减少指令缓存命中率,以及随之而来的性能损失。

在另一方面,如果一个inline 函数本体很短,为函数本体生成的代码可能比为一个函数调用生成的代码还要小。如果是这种情况,inline化这个函数可以导致更小的目标代码和更高的指令缓存命中率!

记住,inline是向编译器发出的一个请求,而不是一个命令。这个请求能够以显式的或隐式的方式提出:

隐式方法就是在一个类定义的内部定义一个函数:
class Person {

 public:
  ...
  int age() const { return theAge; } // an implicit inline request: age is
  ... // defined in a class definition  
private:

  int theAge;
};
这样的函数通常是成员函数,不过我们知道友元函数也能被定义在类的内部,如果它们在那里,它们也被隐式地声明为 inline。

显式声明一个inline函数的方法是在它的声明和定义之前加上inline 关键字。例如,以下就是标准 max 模板(来自 )经常用到的的实现方法:
template // an explicit inline

inline const T& std::max(const T& a, const T& b) // request: std::max is
{ return a < b ? b : a } //precede by "inline"
max 是一个模板的事实引出一个观察结论:inline 函数和模板一般都是定义在头文件中的。这就使得一些程序员得出结论断定函数模板必须是 inline。这个结论是非法的而且有潜在的危害,所以它值得我们考察一下。

inline 函数一般必须在头文件内,因为大多数构建环境在编译期间进行 inline 化。为了用被调用函数的函数本体替换一个函数调用,编译器必须知道函数看 起来像什么样子。有一些构建环境可以在连接期间进行inline 化,还有少数几个--比如,基于.NET Common Language Infrastructure (CLI)的控制环境--居然能在运行时inline化。然而,这些环境都是例外,并非规则。inline 化在大多数C++程序中是一个编译时行为。
模板一般在头文件内,因为编译器需要知道一个模板看起来像什么以便用到它时对它进行实例化。同样,也不是全部如此。一些构建环境可以在连接期间进行模板 实例化。然而,编译期实例化更为普遍。

模板实例化与inline化无关。如果你写了一个模板,而且你认为所有从这个模板实例化出来的函数都应该是inline的,那么就声明这个模板为inline,这就是上面的std::max的实现被做的事情。但是如果你为没有理由要inline化的函数写了一个模板,就要避免声明这个模板为inline(无论显式的还是隐式的)。inline化是有成本的,而且你不希望在毫无预见的情况下遭遇它们。我们已经说到inline化是如何引起代码膨胀的,但是它还有其它的成本
,过一会儿我们再讨论

在做这件事之前,我们先来完成对这个结论的考察:inline是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的 inline 函数(例如,那些包含循环或者递归的),而且,除了最细碎的以外的全部虚函数的调用都不会被inline化。不应该对后一个结论感到惊讶 -- 虚拟意味着“等待,直到运行时才能断定哪一个函数被调用”,而inline意味着“执行之前,用被调用函数取代调用的地方”。如果编译器不知道哪一个函数将被调用,你很难责备它们拒绝inline化这个函数本体。

所有这些加在一起得出:一个被指定的inline函数是否能真的被inline化,取决于你所使用的构建环境--主要是编译器。幸运的是,大多数编译器都有一个诊断层次,在它们不能inline化一个你提出的函数时,会导致一个警告。

有时候,即使编译器完全心甘情愿地inline化一个函数,它们还是会为这个inline函数生成outlined函数本体。例如,如果你的程序要持有一个inline函数的地址,编译器必须为它生成一个outlined 函数本体。它们怎么能生成一个指向根本不存在的函数的指针呢?再加上,编译器一般不会对通过函数指针的调用进行inline化,这就意味着,对一个inline函数的调用可能被也可能不被inline 化,依赖于这个调用是如何做成的

inline void f() {...} // assume compilers are willing to inline calls to f

void (*pf)() = f; // pf points to f
...

f(); // this call will be inlined, because it’s a "normal" call
pf(); // this call probably won’t be, because it’s through a function pointer

甚至在你从来没有使用函数指针的时候,未inline化的inline函数的幽灵也会时不时地拜访你,因为程序员并不必然是函数指针的唯一需求者。有时候编译器会生成构造函数和析构函数的 out-of-line 拷贝,以便它们能得到指向这些函数的指针,在对数组中的对象进行构造和析构时使用。

事实上,构造函数和析构函数的inline化经常会产生误解。例如,考虑下面这个类 Derived 的构造函数:

class Base {
 public:
  ...

 private:
  std::string bm1, bm2; // base members 1 and 2
};

class Derived: public Base {
 public:
  Derived() {} // Derived’s ctor is empty - or is it?
  ...

 private:
  std::string dm1, dm2, dm3; // derived members 1-3
};

这个构造函数看上去像一个inline化的极好的候选者,因为它不包含代码。但是视觉会被欺骗。

C++为对象被创建和被销毁时所发生的事情做出了各种保证。例如,当你使用new时,你动态创建对象会被它们的构造函数自动初始化,而当你使用delete时相应的析构函数会被调用。当你创建一个对象时,这个对象的每一个基类和每一个数据成员都会自动构造,而当一个对象被销毁时,则发生关于析构的反向过程。如果在一个对象构造期间有一个异常被抛出,这个对象已经完成构造的任何部分都被自动销毁。所有这些情节,C++只说什么必须发生,但没有说如何发生。那是编译器的实现者的事,但显然这些事情不会自己发生。在你的程序中必须有一些代码使这些事发生,而这些代码--由 编译器写出的代码和在编译期间插入你的程序的代码--必须位于某处。有时它们最终就位于构造函数和析构函数中,所以我们可以设想实现为上面那个声称为空的 Derived 的构造函数生成的代码就相当于下面这样:

Derived::Derived() // conceptual implementation of
{
 // "empty" Derived ctor

 Base::Base(); // initialize Base part

 try { dm1.std::string::string(); } // try to construct dm1
 catch (...) { // if it throws,
  Base::~Base(); // destroy base class part and
 throw; // propagate the exception
}

try { dm2.std::string::string(); } // try to construct dm2
catch(...) {
 // if it throws,
 dm1.std::string::~string(); // destroy dm1,
 Base::~Base(); // destroy base class part, and
throw; // propagate the exception
}

try { dm3.std::string::string(); } // construct dm3
catch(...) {
 // if it throws,
 dm2.std::string::~string(); // destroy dm2,
 dm1.std::string::~string(); // destroy dm1,
 Base::~Base(); // destroy base class part, and
throw; // propagate the exception
}
}

这些代码并不代表真正的编译器所生成的,因为真正的编译器会用更复杂的方法处理异常。尽管如此,它还是准确地反映了Derived的“空”构造函数必须提供的行为。不论一个编译器的异常多么复杂,Derived的构造函数至少必须调用它的数据成员和基类的构造函数,而这些调用(它们自己也可能是inline的)会影响它对于inline化的吸引力。

同样的原因也适用于Base的构造函数,所以如果它是inline的,插入它的全部代码也要插入 Derived的构造函数(通过 Derived 的构造函数对Base的构造函数的调用)。而且如果string的构造函数碰巧也是inline的,Derived的构造函数中将增加五个那个函数代码的拷贝,分别对应于 Derived 对象中的五个strings(两个继承的加上三个它自己声明的)。也许在现在,为什么说是否inline化Derived的构造函数并不确定就很清楚了。类似的考虑也适用于Derived的析构函数,用同样的或者不同的方法,必须保证所有被Derived的构造函数初始化的对象被完全销毁。

库设计者必须评估声明函数为inline的影响,因为为客户可见的inline函数提供二进制升级版本是不可能的。换句话说,如果f 是一个库中的一个inline函数,库的客户将函数f 的本体编译到他们的应用程序中。如果一个库的实现者后来决定修改f,所有使用了f 的客户都必须重新编译。这常常会令人厌烦。在另一方面,如果f 是一个非inline函数,对f 的改变只需要客户重新连接。这与重新编译相比显然减轻了很大的负担,而且,如果库中包含的函数是动态链接的,这就是一种对于用户来说完全透明的方法。
 
为了程序开发的目标,在头脑中牢记这些需要考虑的事项是很重要的,但是从编码期间的实用观点来看,占有支配地位的事实是:大多数调试器会与inline函数发生冲突。这不应该是什么重大的发现。你怎么能在一个不在那里的函数中设置断点呢?虽然一些构建环境设法支持 inline函数的调试,多数环境还是简单地为调试构建取消了inline化。

这就导出了一个用于决定哪些函数应该被声明为inline,哪些不应该的合乎逻辑的策略。最初,不要inline任何东西,或者至少要将你的inline化的范围限制在那些必须inline的和那些实在微不足道的函数上。通过慎重地使用inline,你可以使调试器的使用变得容易,但是你也将inline化放在了它本来应该在的地位:作为一种手动的优化。不要忘记由经验确定的80-20规则,它宣称一个典型的程序用80%的时间执行20%的代码。这是一个重要的规则,因为它提醒你作为一个软件开发者的目标是识别出能全面提升你的程序性能的20%的代码。你可以 inline 或者用其他方式无限期地调节你的函数,但除非你将精力集中在正确的函数上,否则就是白白浪费精力。

Things to Remember:

  • 将大部分inline限制在小的,调用频繁的函数上。这使得程序调试和二进制升级更加容易,最小化潜在的代码膨胀,并最大化提高程序速度的几率。
  • 不要仅仅因为函数模板出现在头文件中,就将它声明为 inline。

2007年12月9日星期日

Key point in IPTV

1. 视频编码技术

编码技术是多媒体通信中使用的基本技术之一。多媒体通信的一个显著特点就是要传输的信息量非常大,尤其是视频数据,其编码技术甚至会在较大程度上影响业务质量,因此视频编码技术在IPTV中的地位非常重要。(H.261,H.262,H.263,H.264以及MPEG-1,MPEG-2,MPEG-4等)

MPEG-2主要目的是提供标准数字电视和高清晰度电视的编码方案,目前人们所熟知的DVD就是采用的这种格式。MPEG-2在编码时对图像和声音 的处理是分别进行的,将图像看成是一个矩形像素阵列的序列来处理,将音频看成是一个多声道或单声道的声音来处理。这种处理方式压缩效率较低,而且不利于传输。

目前的趋势是使用更加适合于流媒体系统的H.264/MPEG-4。MPEG-4将一个场景的视频、音频对象综合考虑,对不同的主体采用不同的编码 方式,再在解码端进行重新组合。它综合了数字电视、交互图形学和Internet等领域的多种技术,在大大提高了编码压缩率的同时,亦提高了传输的灵活性 和交互性。H.264是MPEG-4的第10部分,它不仅能使MPEG-4节约50%的码率,而且引入了面向IP包的编码机制,更加有利于网络中的分组传输。

AVS是我国自行开发、具有自主知识产权的新一代编码方式。目前,我国正在进行AVS的规格制订工作,AVS视频主要面向高清晰度电视、高密度光存储媒体等应用中的视频压缩。


除了视频编码技术之外,目前互联网使用较多的流媒体格式主要是美国Real Networks公司的RealMedia,Apple公司的QuickTime,微软公司的Windows Media以及Macromedia的ShockWave Flash。


2. 数字版权管理(DRM)技术

数据数字版权管理(Digital Rights Management,简称DRM)技术为内容提供者保护他们的私有视频、音乐或其他数据的版权提供了一种技术手段。

DRM技术的工作原理是,媒体制作者在节目制作阶段将版权信息嵌入到媒体内容之中,并通过加密方式对其加以保护。当需要验证媒体合法性的信息时,再通过技术方法或手段从媒体之中将版权信息读取出来,这样就可以进行版权信息的比对了。

目前,使用最为普遍的数字版权技术是数字水印(Digital Watermark)。它使用一定的算法,在被保护的数字格式的音乐、歌曲、图片或影片中嵌入某些标志性信息(称为数字水印),来达到证明版权归属和跟踪 侵权行为的目的。数字水印信息一般包括作者的序列号、公司标志以及一些有特殊意义的文本信息。


数字多媒体内容是IPTV中最为关键的节目来源。有了DRM技术,可使各个平台(无论是因特网、流媒体还是交互数字电视)的内容提供商们放心地提供更多的内容,采取更灵活的节目销售方式,同时有效地保护知识产权。


3. 存储分发技术


IPTV系统中对存储网络的存储分发要求:
●让用户连接到最近的服务器,减少用户访问的延迟和响应时间,减少网络带宽的消耗
●全局负载平衡,提高存储资源的利用率,提高分发服务的性能与质量;
●提高资源存储和内容分发的管理控制能力,智能分配路由和进行流量管理;
●对用户提供鉴权、认证机制,发送的内容受到保护,未授权的用户不能分发;
●具有高可靠、可用性,具有良好伸缩性和兼容性,能容错且很容易扩展。

组网方式:
1)CDN模式
IPTV系统对于用户的服务质量有很高的要求,而且要保证播放的流畅。在广域网内,从用户的客户端到流媒体服务器之间经过了一个很复杂的路由以后,就很难保证播放的流畅了。为了克服网络复杂路线带来的制约,在IPTV中必须通过边缘服务来实现最终用户的点播服务,所以要把内容从中心服务器有效地分发到边缘服务器,实现把内容从中心存储服务器分发到边缘服务器的这样一个网络体系,称为内容分发网络(Content Delivery Network,CDN)。

CDN的核心思想是将内容从中心推到靠近用户的边缘,使用户可以就近取得所需的内容,这样,不但有效提高了用户访问内容的性能,而且有效减轻了中心设备和骨干网络的压力。通过CDN,可以将内容分发从原来的单一中心结构变为分布式的结构,提高用户访问网站的响应速度。因而,CDN可以提高网络中信息流动的效率,从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等问题,提高用户访问网站的响应速度。

在CDN网络中,包含内容缓存设备、内容交换机、内容路由器、CDN内容管理等重要元素:

内容缓存设备是CDN的业务提供点,是面向最终用户的内容提供设备,可缓存静态的Web内容和流媒体内容,实现内容的边缘传播和存储,以便最终用户的就近访问,因此一般部署于集中的用户接入点,完成内容边缘存储的设备通常都采用Cache技术来实现。
  
内容交换机处于用户接入集中点和POP点,可对内容进行缓存负载平衡及访问控制。在很多设备情况下,和内容缓存集中在一个设备上。内容交换机可以均衡单点多个内容缓存设备的负载,它基于最终用户会话和特定的内容请求提供内容服务。内容交换机是CDN的可选部件之一,对于较大型的用户接入点,内容交换机可以提高站点的可用性。


内容路由器负责将用户的请求调度到适当的设备上,内容路由通常通过负载均衡系统来实现,负载均衡系统是实现CDN的内容路由功能。它的作用是动态均衡各个内容缓存站点的负荷分配,为用户的请求选择最佳的访问站点,同时提高网站的可用性。内容路由根据网络拓扑结构、网络延时、服务器负荷与规则等策略设定,指定最优站点向特定的内容请求提供服务。内容路由器可根据多种因素制定路由,包括站点与用户的临近度、内容的可用性、网络负载、设备状况等。负载均衡系统是整个CDN的核心,负载均衡的准确性和效率直接决定了整个CDN的效率和性能。


内容管理系统主要负责整个CDN系统的管理,是CDN的可选部件,它的作用是进行内容管理,如内容的注入和发布、内容的分发、内容的审核、内容的服务等,并能够让网络内容分布和传输服务的用户或者服务供应商可以根据需要监视、管理或者控制网络内容的分布、设备状态等。

  
2)P2P模式
P2P技术是一种用于不同PC用户之间、不经过中继设备直接交换数据或服务的技术。它打破了传统的Client/Server模式,在对等网络中,每个节点的地位都是相同的,具备客户端和服务器双重特性,可以同时作为服务使用者和服务提供者。由于P2P技术的飞速发展,互联网的存储模式将由目前的“内容位于中心”模式转变为“内容位于边缘”模式,改变互联网现在的以大网站为中心的状态,重返“非中心化”,将权力交还给用户。

P2P模式的网络组成变化经历了集中式、分布式和混合式3个阶段:

集中式P2P模式由一个中心服务器来负责记录共享信息以及反馈对这些信息的查询;每一个对等实体要对它所需共享的信息以及进行的通信负责,根据需要下载它所需要的其他对等实体上的信息。集中式P2P模式则是所有网上提供的资料都存放在提供该资料的客户机上,服务器上只保留索引信息,此外服务器与对等实体以及对等实体之间都具有交互能力。集中式P2P有利于网络资源的快速检索,并且只要服务器能力足够强大就可以无限扩展,但是其中心化的模式容易遭到直接的攻击。

在分布式P2P中,对等机通过与相邻对等机之间的连接遍历整个网络体系。每个对等机在功能上都是相似的,并没有专门的服务器,而对等机必须依靠它们所在的分布网络来查找文件和定位其他对等机。分布式P2P解决了抗攻击问题,但是又缺乏快速搜索和可扩展性。


混合式P2P在分布式模式的基础上,将用户节点按能力进行分类,使某些节点担任特殊的任务。这些节点共分为3种:用户节点、搜索节点和索引节点。一个节点可以既是搜索节点又是索引节点。混合式P2P结合了集中式和分布式P2P的优点,在设计思想和处理能力上都得到了进一步的优化。



4. 光纤到户(FTTH)技术

FTTH是将光纤作为物理媒介实现用户和运营商之间网络连接的一种技术,因其利用了光网络和光纤通信所固有的大容量、高速率的特点而倍受青睐。

在诸多FTTH技术中,近年来兴起的无源光网络(PON)接入技术尤其引人注目。千兆无源光网络(GPON)的带宽下载速度将达到2.4Gbit/s,上 传速率达到1.2Gbit/s,可支持长达20km的传输距离,消除了各类铜缆接入的距离限制。应该说,GPON技术已经成为一种理想的IPTV宽带接入 技术。


5. 电子节目指南(EPG)技术


电子节目指南(EPG)也就是电视节目导航系统,主要用来描述提供给电视观众的所有节目的信息,它是构成交互电视的重要技术之一。在IPTV业务中,用户可通过EPG来了解电视节目的名称、播放时间和内容梗概等相关信息,并实现对节目的快速检索和访问,进行频道选择或视频点播等操 作。除了电视节目的导航之外,还可通过电子节目指南向用户提供由文字、图形和图像组成的人机交互界面来实现各种增值业务的导航。

一般来讲,一个EPG系统包含两个主要部分,即发生器子系统解码器子系统。发生器子系统负责产生IPTV业务信息(Service Information,SI),一般由平台运营商提供。解码器子系统负责对业务信息进行解析并生成电子节目菜单,一般在接收端机顶盒的综合接收解码器 (IRD)中实现。这就要求发生器子系统产生的信息,必须能保证被解码器子系统正确解析。

可将EPG信息分成基本EPG信息和扩展EPG信息两部分。对于基本EPG信息,在相同的EPG标准下,只要发生器子系统发送的是标准SI信息,那么不同 厂家生产的机顶盒可按照同样的标准对SI信息进行解析。对于扩展EPG信息,各平台运营商和机顶盒制造商可用来提供自己的特色功能。


6. 机顶盒STB技术


IPTV终端有三种基本类型,分别是计算机、手机和电视机。
计算机配备相应的软件,即可直接用作IPTV的终端;用于移动流媒体平台的多媒体手机也可以直接使用;但对于电视机来讲,情况就不同了。由于电视机本身并没有存储功能,不支持软件安装,也无法像手机那样加装流媒体支持功能,因而无法实现IP的支持 功能,必须加装一个将IP数据流转换成电视机可以接受的信号的机顶盒设备作为中介才能收看IPTV节目。这样,作为普及面最广的接收终端——电视机,为它制作机顶盒就成为开展IPTV业务的关键。

就目前的发展情况来看,作为用户接收端设备,STB需要具备包括数据转换、接入支持、协议支持、业务支持、解码支持等在内的多种功能。
数据转换是STB最 基本的功能,就是要将接收到的IP数据转换成电视屏幕可以显示的数据。
在接入支持方面,STB一般需要支持目前应用较多的LAN或xDSL或WLAN等多种宽带接入方式,未来还要提供FTTH接入支持。
在协议支持方面,STB要支持TCP/UDP/IP协议族来完成互操作信息的网络传输,以及IP数据和视频流媒体数据的接收和处理工作。
在业务支持方面,STB一般需要支持目前较为流行的视频点播、组播,Internet浏览、短消息、可视业务和
网络游戏等业务。
在解码支持方面,STB需要支持对多媒体码流的解码能力,一般要支持现行的国际标准格式(如MPEG-2,MPEG-4等)以及国产标准格式AVS。
除了上述这些功能之外,STB还要支持数字版权管理、内容缓存、交互控制、接入鉴权和业务及网络管理功能。



2007年12月6日星期四

Reference in C++

引用是C++引入的新语言特性,是C++常用的一个重要内容之一,正确、灵活地使用引用,可以使程序简洁、高效。

引用简介

引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
引用的声明方法:类型标识符 &引用名=目标变量名;

【例1】:int a; int &ra=a; //定义引用ra,它是变量a的引用,即别名
说明:
(1)&在此不是求地址运算,而是起标识作用。
(2)类型标识符是指目标变量的类型。
(3)声明引用时,必须同时对其进行初始化。
(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
ra=1; 等价于 a=1;
(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。


引用应用

1、引用作为参数
引用的一个重要作用就是作为函数的参数。以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为这样可以避免 将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择),就是引用。

【例2】:

void swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用
{ int p; p=p1; p1=p2; p2=p; }

为在程序中调用该函数,则相应的主调函数的调用点处,直接以变量作为实参进行调用即可,而不需要实参变量有任何的特殊要求。如:对应上面定义的swap函数,相应的主调函数可写为:
main( )
{
int a,b;
cin >> a >> b; //输入a,b两变量的值
swap(a,b); //直接以变量a和b作为实参调用swap函数
cout 《 a 《 ‘ ’ 《 b; //输出结果
}

上述程序运行时,如果输入数据10 20并回车后,则输出结果为20 10。
由【例2】可看出:
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2) 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单 元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都 好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名" 的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常量引用。

2、常量引用

常量引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。

【例3】:
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确

这不光是让代码更健壮,也有些其它方面的需要。

【例4】:假设有如下函数声明:
string foo( );
void bar(string & s);

那么下面的表达式将是非法的:
bar(foo( ));
bar("hello world");

原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const 。

3、引用作为返回值
要以引用返回函数值,则函数定义时要按以下格式:
类型标识符 &函数名(形参列表及类型说明)
{函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。(多用在返回值是一个对象)

【例5】以下程序中定义了一个普通的函数fn1(它用返回值的方法返回函数值),另外一个函数fn2,它以引用的方法返回函数值。

#include
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
void main() //主函数
{
 float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
 float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
 //不能从被调函数中返回一个临时变量或局部变量的引用
 float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 cout 《 a 《 c 《 d;

}


引用作为返回值,必须遵守以下规则:
(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2) 不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)可以返 回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)引用与一些操作符的重载:
流操作符<<和 >>,这两个操作符常常希望被连续使用,例如:cout << "hello" << x =" j" x="10)=">
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=10;
cout 《 vals[0];
cout 《 vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n]; else { cout《"subscript error"; return error;} }

(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一 个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

4、引用和多态
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。

【例7】:
class  A;
class  B:public A{……};
B  b;
A  &Ref = b; // 用派生类对象初始化基类对象的引用
Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。


引用总结
(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符 << 和 >> 、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。


(补充)

引用与指针的比较 -

1. 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)

2. 不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)

3. 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)

2007年12月4日星期二

How to use (data_member | member function) pointers in class

(转载)

1. 首先普通函数指针不能被赋值为成员函数的地址,即使返回类型和参数完全匹配

例如:下面是的pfi是一个普通函数指针,它没有参数,返回类型为int:
int (*pfi)();
若有两个全局函数,HeightIs()和WidthIs():
int HeightIs();
int WidthIs();

则下面的的赋值操作是合法的:
pfi = HeightIs;
pfi = WidthIs;

但类Screen也定义了两个访问函数 - height()和width(),它们也没有参数,返回类型也为int:
inline int Screen::height() { return _height; }
inline int Screan::width() { return _width; }
但是下面的赋值是非法的,会导致编译错误产生。
pfi = &Screen::height;

因为,成员函数有一个非成员函数不具有的属性 - 它的类(class)。
指向类成员函数的指针必须与其赋值的类成员函数类型匹配,不是两个方面而是三个方面:
(1)参数类型和个数(2)返回类型 (3) 它所属的类类型

类成员函数指针和普通函数指针之间的不匹配是由于这两种指针在表示上的区别。
普通函数指针存储函数的地址,可以被用来直接调用那个函数。
类成员函数指针首先必须被绑定在一个对象或者一个指针上,才能得到被调用对象的this指针,然后才调用指针所指的成员函数。


2. 成员函数指针的声明

拿下面这个类来说明:
class Screen {
public:
// 成员函数
void home() { _cursor = 0; }
void move( int, int );
char get() { return _screen[_cursor]; }
char get( int, int );
bool checkRange( int, int );
int height() { return _height; }
int width() { return _width; }
//....
private:
string _screen;
string::size_type _cursor;
short _height;
short _width;
};

成员函数指针的声明要求扩展的语法,它要考虑类的类型。
指向类数据成员的指针也是这样。
考虑Screen 类的成员_height 的类型,它的完整类型是“short 型的Screen 类的成员”,
指向_height 的指针的完整类型是“指向short 型的Screen 类的成员的指针”,这可以写为:
short Screen::*
指向short型的Screen类的成员的指针定义如下:
short Screen::* ps_Screen;
ps_Screen 可以用_height 的地址初始化如下
short Screen::*ps_Screen = &Screen::_height;

数据成员指针和普通指针之间的不匹配也是由于这两种指针的表示上的区别。普通指针含有引用一个对象所需的全部信息。数据成员指针在被用来访问数据成员之前,必须先被绑定到一个对象或指针上。

定义一个成员函数指针需要指定函数返回类型,参数表和类。
例如指向Screen 成员函数并且能够引用成员函数height()和width()的指针类型如下:
int (Screen::*) ( );
这种类型指定了一个指向类Screen的成员函数的指针,它没有参数,返回值类型为int。
指向成员函数的指针可被声明,初始化及赋值如下:
// 所有指向类成员的指针都可以用0赋值
int (Screen::*pmf1)( ) = 0;
int (Screen::*pmf2)( ) = &Screen::height;
pmf1 = pmf2;
pmf2 = &Screen::width;

也可以用typedef 定义,这样语法更容易读。如:
typedef int (Screen::*Action)( );
Action default = &Screen::home;
Action next = &Screen::forward;


3. 怎样使用指向类成员的指针

类成员的指针必须总是通过特定的对象或指向该类型的对象的指针来访问。是通过使用两个指向成员操作符的指针(针对类对象和引用的.* ,以及针对指向类对象的指针的->*)

(操作符.*和->*的说明如下:
pm-expression :
cast-expression
pm-expression .* cast-expression
pm-expression ->* cast-expression

The binary operator .* combines its first operand, which must be an object of class type,
with its second operand, which must be a pointer-to-member type.

The binary operator ->* combines its first operand, which must be a pointer to an object
of class type, with its second operand, which must be a pointer-to-member type.

In an expression containing the .* operator, the first operand must be of the class type
of the pointer to member specified in the second operand or of a type unambiguously derived
from that class.

In an expression containing the ->* operator, the first operand must be of the type "pointer
to the class type" of the type specified in the second operand, or it must be of a type
unambiguously derived from that class.)

如下面例子:

int (Screen::*pmfi)() = &Screen::height;
Screen& (Screen::*pmfS)( const Screen& ) = &Screen::copy;
Screen myScreen, *bufScreen;

// 直接调用成员函数
if ( myScreen.height() == bufScreen->height() )
bufScreen->copy( myScreen );

// 通过成员指针的等价调用
if ( (myScreen.*pmfi)() == (bufScreen->*pmfi)() )
(bufScreen->*pmfS)( myScreen );

类似地指向数据成员的指针可以按下列方式被访问:
typedef short Screen::*ps_Screen;
Screen myScreen, *tmpScreen = new Screen( 10, 10 );
ps_Screen pH = &Screen::_height;
ps_Screen pW = &Screen::_width;
tmpScreen->*pH = myScreen.*pH;
tmpScreen->*pW = myScreen.*pW;


4. 静态类成员的指针

在非静态类成员的指针和静态类成员的指引之间有一个区别:指向类成员的指针语法不能被用来引用类的静态成员,静态类成员是属于该类的全局对象和函数,它们的指针是普通指针

如:
class A{
public :
static void f( );
private:
static int m_data;
};

指向m_data指针的定义如下:
int *p = &A::m_data;
int A::* p = &A::m_data;
//错误
指向f 函数指针可以这样定义,它是一个普通的函数指针:
void (*ptrf)( ) = &A::f;

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

C++指针直接调用类成员函数

在编程工作中常会遇到在一个“类”中通过函数指针调用成员函数的要求,如,当在一个类中使用了C++标准库中的排序函数qsort时,因qsort参数需要一个“比较函数”指针,如果这个“类”使用某个成员函数作“比较函数”,就需要将这个成员函数的指针传给qsort供其调用。本文所讨论的用指针调用 “类”的成员函数包括以下三种情况:

例1:将 “类”的成员函数指针赋予同类型非成员函数指针

typedef void (*Function1)( ); //定义一个函数指针类型。
Function1 f1;

class Test1
{
 public:
  //…被调用的成员函数。
  void Memberfun1( ){ printf("%s \n","Calling Test3::Memberfun2 OK");};
  void Memberfun2()
  {
   f1=reinterpret_cast
(Memberfun1); //将成员函数指针赋予f1,编译出错
   f1();
  }
  //…
};

int main()
{
 Test1 t1;
 t1.Memberfun2();
 return 0;
}

例2:在一个“类”内有标准库函数,如qsort 或其他全局函数,用函数指针调用类的成员函数

class Test2
{
private:
int data[2];
//…
public:
//…
int __cdecl Compare(const void* elem1, const void* elem2) //成员函数。
{
printf("%s \n","Calling Test2::Memberfun OK");
return *((int*)elem1)- *((int*)elem2) ;
}

void Memberfun()
{
data[0]=2; data[1]=5;
qsort( data, 2, sizeof(int), Compare);
//标准库函数调用成员函数。编译出错
}
//…
};

int main( )
{
Test2 t2;
t2.Memberfun(); //调用成员函数。
return 0;
}

例3:同一个“类”内,一个成员函数调用另一个成员函数

#include "stdlib.h"
class Test3
{
public:
//…
void Memberfun1( void (* f2)( ) ) { f2( ) ;} //成员函数1调用成员函数2
void Memberfun2( ) { printf("%s \n","Calling Test3::Memberfun2 OK");} //成员函数2
void Memberfun3( ) { Memberfun1( Memberfun2);}
// 编译出错
//…
};

int main( )
{
Test3 t3;
t3.Memberfun3(); //调用成员函数。
return 0;
}

以上三种情况的代码语法上没有显著的错误,在一些较早的编译环境中,如VC++ 4.0 通常可以编译通过或至多给出Warning。后来的编译工具,如VC++6.0和其他一些常用的C++编译软件不能通过以上代码的编译, 并指出错误如下(以第三种情况用VC++ 6.0编译为例):
error C2664: 'Memberfun1' : cannot convert parameter 1 from 'void (void)' to 'void (__cdecl *)(void)'
None of the functions with this name in scope match the target type
即:Memberfun1参数中所调用的函数类型不对。

按照以上提示,仅通过改变函数的类型无法消除错误。但是如果单将这几个函数从类的定义中拿出来不作任何改变就可以消除错误通过编译。仍以第三种情况为例,以下代码可通过编译:

#include


void Memberfun1( void (* f2)( ) ) { f2( ) ;} //原成员函数1调用成员函数//2。
void Memberfun2( ) { printf("%s \n","Calling Test3::Memberfun2 OK");} //原成员函数2。
void Memberfun3( ) { Memberfun1( Memberfun2);}

int main( )
{
Memberfun3 ();
return 0;
}

由此可以得出结论,以上三种情况编译不能通过的原因并不在于函数类型调用不对,而是与 “类”有关。没通过编译的情况是用函数指针调用了 “类”的成员函数,通过编译的是用函数指针调用了非成员函数,而函数的类型完全相同。那么 “类”的成员函数指针和非成员函数指针有什么不同吗?

在下面的程序中,用sizeof()函数可以查看各种“类”的成员函数指针和非成员函数指针的长度(size)并输出到屏幕上。

#include "stdafx.h"
#include

#include


class Test; //一个未定义的类。

class Test2 //一个空类。
{
};

class Test3 //一个有定义的类。
{
 public:
  //...
  void (* memberfun)();
  void Memberfun1( void (* f2)( ) ) { f2( ) ;} //成员函数1调用成员函数2
  void Memberfun2( );//成员函数2。
  //…
};

class Test4: virtual Test3 ,Test2 //一个有virtual继承的类(derivative class)
{
 public:
  void Memberfun1( void (* f2)( ) ) { f2( ) ;}
};

class Test5: Test3,Test2 //一个继承类(derivative class)
{
 public:
  void Memberfun1( void (* f2)( ) ) { f2( ) ;}
};

int main()
{
 std::cout 《 "一般函数指针长度= "《 sizeof(void(*)()) 《 '\n';
 std::cout 《"-类的成员函数指针长度-"《'\n'《'\n';
 std::cout 《"Test3类成员函数指针长度="《 sizeof(void(Test3::*)())《'\n'《'\n';
 std::cout 《"Test5类成员函数指针长度="《sizeof(void (Test5:: *)())《'\n';
 std::cout 《"Test4类成员函数指针长度="《sizeof(void (Test4:: *)())《'\n';
 std::cout 《"Test类成员函数指针长度="《sizeof(void(Test::*)()) 《'\n';
 return 0;

}
输出结果为(VC++6.0编译,运行于Win98操作系统,其他操作系统可能有所不同):
一般非成员函数指针长度= 4
-类的成员函数指针长度-
Test3类成员函数指针长度=4
Test5类成员函数指针长度=8
Test4类成员函数指针长度=12
Test类成员函数指针长度=16

以上结果表明,在32位Win98操作系统中,一般函数指针的长度为4个字节(32位),而类的成员函数指针的长度随类的定义与否、类的继承种类和关系而变,从无继承关系类(Test3)的4字节(32位)到有虚继承关系类(Virtual Inheritance)(Test4)的12字节(96位),仅有说明(declaration)没有定义的类(Test)因为与其有关的一些信息不明确成员函数指针最长为16字节(128位)。显然, 与一般函数指针不同,指向“类”的成员函数的指针不仅包含成员函数地址的信息,而且包含与类的属性有关的信息,因此,一般函数指针和类的成员函数指针是根本不同的两种类型。当然也就不能用一般函数指针直接调用类的成员函数,这就是为什么本文开始提到的三种情况编译出错的原因。尽管使用较早版本的编译软件编译仍然可以通过,但这会给程序留下严重的隐患。

至于为什么同样是指向类的成员函数的指针,其长度竟然不同,从32位到128位,差别很大,由于没有看到微软官方的资料只能推测VC++6.0在编译时对类的成员函数指针进行了优化,以尽量缩短指针长度,毕竟使用128位或96位指针在 32位操作系统上对程序性能会有影响。但是,无论如何优化,类的成员函数指针包含一定量的对象(Objects)信息是确定的。

那么如何用指针调用类的成员函数?可以考虑以下方法:

(1) 将需要调用的成员函数设为static 类型。如在前述例子2中,将class Test2 成员函数Compare 定义前加上static:

class Test2
{
//….
int static __cdecl Compare(const void* elem1, const void* elem2) //成员函数
//其他不变
}
改变后的代码编译顺利通过。原因是static 类型的成员函数与类是分开的,其函数指针也不包含对象信息,与一般函数指针一致。这种方法虽然简便,但有两个缺点:1、被调用的函数成员定义内不能出现任何类的成员(包括变量和函数)2、由于使用了static 成员,类在被继承时受到了限制。

(2) 使用一个函数参数含有对象信息的static 类型的成员函数为中转间接地调用其他成员函数。以例3为例,将类Test3作如下修改,main( )函数不变,则可顺利通过编译:

class Test3
{
 public:
  //…
  void static __cdecl Helper(Test3* test3)
  {
   test3->Memberfun2();
  }
  void Memberfun1( void (* f2)(Test3*)) { f2(this) ;} //将对象信息传给Helper函数
  void Memberfun2( ) {printf("%s \n","Calling Test3::Memberfun2 OK"); } //成员函数2
  void Memberfun3( ) { Memberfun1( Helper);}
  //…
};
这种间接方式对成员函数没有任何限制,克服了第一种方法成员函数不能使用任何类的成员的缺点,但由于有static 成员,类的继承仍受到制约。

(3) 使用一个全程函数(global function)为中转间接调用类的成员函数。仍以例3为例,将代码作如下修改(VC++6.0编译通过):

class Test3;
void __cdecl Helper(Test3* test3);

class Test3
{
 public:
  //…
  void Memberfun1( void (* f2)(Test3*)) { f2(this) ;} //成员函数1调用成员函数2
  void Memberfun2( ) {printf("%s \n","Calling Test3::Memberfun2 OK"); } //成员函数2
  void Memberfun3( ) { Memberfun1(Helper);}
  //…
};

void __cdecl Helper(Test3* test3)
{
 test3->Memberfun2();
};
这个方法对成员函数没有任何要求,但是需要较多的代码。

除上述三种方法外还有其他方法,如可以在汇编层面上修改代码解决上述问题等,不属于本文范围。

结论:函数指针不能直接调用类的成员函数,需采取间接的方法。原因是成员函数指针与一般函数指针有根本的不同,成员函数指针除包含地址信息外,同时携带其所属对象信息。

World Clocks

Endless Space Headline Animator

Mobile Ads