Thursday, April 26, 2007

基于linux的中文TTS 简单实现

前言
      今年,我参加学校举办的的一个嵌入式比赛,我的作品中需要用到中文文本语音转换的功能,由于是使用linux系统,所以不能使用MS提供的语音开发包,虽然linux上也有很多TTS功能的软件,但都只支持英语文本的转换,不支持中文的转换,当然支持中文语音转换的也有,但都需要money的,而且我们对语音的要求不高,所以就由我自己来实现。
我实现的中文TTS现有功能如下:

  1. 基于中文二级字库,语音库的语音元素使用的是MS的语音库语音元素。
  2. 合成语音文件的格式为 WAV,  所以合成语音可以直接在声卡上播放。
  3. 提供简单的函数调用接口,支持程序开发

附:
我实现的中文TTS 需改进的N点:

  1. 没有自己的语音播放功能(我是FORK 播放器进程来读语音文件的)需要在TTS中增加播放功能模块,才能算是完整的TTS系统,网上有很多LINUX上播放器的代码,由于时间关系我暂时没做,但以后我一定会加上去的,我也希望能有朋友能帮我完成这功能
  2. 语音库文件太大(没压缩前有40M左右,使用普通的压缩程序压缩后只有2M多点)我的发音元素是采用WAV格式的,所以有点大, 解决方法我暂时还没想到,我打算对压缩文件进行操作,但不知道行不行,自己没把握。
  3. 提供的函数功能太简单
    3.1): 现在只提供了将GBK/GB2312编码 的中文文本转换, 如果程序使用的编码是其它格式的话,还需先自己转换成GBK/GB2312的编码 才能使用我提供的函数,我想在中文TTS中封装编码转换函数,这样能使应用程序更方便的使用。
    3.2): 其他很多功能的实现:
      太多功能需要来实现,写都写不来了。。比如说读数字的处理等等,太多了,所以暂时空着
  4. 读音效果
         我想这点应该是评价一个TTS系统的最关键的因素,也是我自己认为最难的一点,我想这可能超出了我的知识范围,我的头脑中只存在着编程逻辑,对其他的知道得甚少,我想我自己得突破这点。了解其它学科的知识。 现在我只能说一句, 现在的读音效果超差,一听就知道是机器在读,没有一点人情味。
  5. 不支持中英文混读
    现在只能读中文,英文是一个一个字母的读,虽然有很多关于英语转换的TTS软件是开源的,但要完成这功能还是需要一定的工作量的,想想如果让我一个人来做,就头晕,天哪,要看N多的代码.......
  6. 多音字的处理
    我想这可能要用到中文分词了....

实现原理
        采用波形拼接的语音生成方法来实现本系统。这种方法预先建立一个庞大的语音数据库,在合成时采用恰当的技术手段挑选出所需的语音基元进行拼接,从而形成语音输出。因此,语音拼接的重点是基本语音单元的拼接,我们选取字作为基本的语音拼接单位,语音数据库由若干以字为单位的WAV样本文件组成。[1]

所以实现本汉语TTS系统主要完成两项工作:
第一:建立一个语音库,语音库中记录了普通话中所有汉字的读音;
第二:建立汉字机内码到汉字读音的访问方法,实现由输人的汉字机内码得到该汉字的读音。并且合成WAV声音文件。[2]

语音库的实现
  语音库保存着常用汉字的发音(多音的汉字只记录其一种发音,这也是本系统的一个缺陷,需要以后完善),所以先要得到一汉字集,这个汉字集包含了大部分常用的汉字,然后在根据这个汉字集,来一个个的取得汉字的发音,并且按一定的规则保存到语音库中。

所以实现语音库可以分为三步:

  1. 取得常用汉字的集合
  2. 根据汉字集,使用一些朗读软件生成该汉字集的语音文件
  3. 处理汉字集语音文件的格式,使它能符合我们的要求

1.1根据汉字编码规则获取汉字字符集的文本文件
1.1.1编码知识:
         所谓编码,是以固定的顺序排列字符,并以此作为记录、存贮、传递、交换的统一内部特征。一个汉字有ASC II码、区位码等与之对应。ASC II码中对应于码值161到254的字符用于表示汉字,每个汉字用两个ASC1I码值对应的字符表示。区位码用4位数字表示,前两位从01到94称区码,后两位从01到94称位码。一个汉字的前一半是 ASC II码为"160+区码"的字符,后一半是ASCII码为"160+位码"的字符。例如:"刘"的区位码是3385,其意为区码33和位码85,它是由 ASCII码为160+33=193和160+85=245的两个字符组成。

    该文所说的汉字字符集一般是指ISO 10646.1 即GB13000.1。在Windows 95/98/2000中,微软提供了"汉字扩展内码规范(GBK)"以解决汉字的收字不足、简繁共存、简化代码体系间转换等汉字信息交换的瓶颈问题,利用 GBK可以方便解决"�F"、"薯"等大量汉字的交换问题而不必自行造字了。GBK字库共分为5部分,其中GBK/1和GBK/5为符号部分,GBK/2为国标汉字部分,GBK/3和GBK/4为扩展汉字部分。其中,第16区至55区为一级汉字,以拼音排序,共计3755字,56―87区为二级汉字,按偏旁部首排序,共计3008字。

1.1.2程序代码:
   用C++实现的获取GBK中一级汉字字符的代码段如下:

 //getgbk.cpp一获取GBK汉字码文件
#include <strstream.h>  //字符串I/O操作
#include <fstream.h>   //文件I/O操作
 unsigned char oneline[4];
 ofstream ofs("gbhz.txt",ios::binary );
 oneline[2]=163;
 oneline[3]=172;
 int qm;
 int wm;
 for( qm=176;qm<=247;qm++)  //区码0XB0―0XD7 87
  for(wm=161;wm<=254;wm++) //位码0XA1―0XFE
   if(!((qm==247)&&(wm=250)))
    //剔除GBK中没有编码的字位
   {
    oneline[0]=qm; //汉字区码
    oneline[1]=wm; //汉字位码
    ofs.write((char *)&oneline,4); //写一行至gbhz txt
   
   }//if end

1.2:根据取得的汉字集,使用一些报读软件生成该汉字集的语音文件,一般为WAV格式的。          这里取名为gbhz.wav
1.3:处理汉字集读音文件的格式
      1.3.1 理解WAV文件格式
WAVE文件作为多媒体中使用的声波文件格式之一.它是以RlFF格式为标准的 每个wAVE文件的头4个字节便是"RIFF" WAVE文件由文件头和数据体两大部分组成。其中文件头又分为RIFE/WAV文件标识段和声音数据格式说明段两部分 。WAVE文件格式说明见下表,

内容 数据类型 字节数
"RIFF"标志 Char 4Byte
文件大小 Long int 4Byte
"wave"标志 Char 4Byte
"fmt"标志
Char 4Byte
PCMWAVFORMAT数据结构大小 Long int 4Byte
PCMWAVFORMAT数据结构    
"data"标志 Char 4Byte
语音数据大小 Long int 4Byte

     可以以时域-幅度的方式显示出原始声音的波形,这是最简单同时也是最直接的信息处理方式。在时域范国内,可以观察该信号波形是否连续,中间是否有祧变等。据发现,经语音引擎处理后,每个汉字所对应的语音数据长度不尽相同.这给以后截取每个汉字的语音数据造成了困难。因此.为了区分每个汉宇的语音数据.在生成汉字字符集时.在每个汉字后添加了一个逗号作为闻隔符 这样生成的语音文件的波形图(部分)如图1所示
 
1.3.2生成新的汉字字符集的话音文件的算法
(1)打开gbhz.wav.从gbhz.wav中读人文件头CHAR HEAD[46]。46为文件头长度。
(2)从gbhz.wav 中读入第一个汉字的语音波形数据放入CHAR BUFFER[3200]。3200为一个汉字的语音波形数据长度.选取决于形成语音的速率、音质等因素
(3)读后面的数据,如果是0x80则不做处理.继续往后读。ox80是逗号语音数据,既没有发音的数据。
(4)直到读到第一个不是逗号的语音数据,开始记录第二个汉字的语音波形数据到CHAR BUFFER[3200]
(5)重复3.4步直到读完全部的语音波形数据。
合成新的汉字字符集的语音文件后的波形图(部分)如图2所示
 

1.3.3 处理代码如下:
#define  MAXLEN  32000
int HandingWav()
{
 FILE * fpf,*fpt; //文件操作指针
 if((fpf=fopen("gbhz.wav","rb+"))==NULL)   //gbhz.wav为处理前的语音文件
     return -1;
 if((fpt=fopen("ddd.wav","rb+"))==NULL)  //ddd.wav为合成的新的语音文件
     return -1;
 
 char head[46];     //wav文件的文件头长度
 char data[100];    //用来加速文件处理的数组
 char buffer[MAXLEN];
 memset(buffer,0,MAXLEN);
 fread(head,sizeof(head),1,fpf);    //head of wav
 fwrite(head,sizeof(head),1,fpt);
 while(!feof(fpf))
 {
        fread(buffer,MAXLEN,1,fpf);  //读一个字的发音
        fwrite(buffer,MAXLEN,1,fpt);  //写一个字
        memset(buffer,0,MAXLEN);
        fread(data,1,1,fpf); //读一个字节
        while(data[0]==char(0x80)) //判断是否为0x80
         {
           if(fread(data,100,1,fpf)==-1) //每次取100个字节,但只判断第一个字节,这样可以加速文件处理
                {
                   fclose(fpf);
                   fclose(fpt);
                   return -1;
                 }  //end if
           }  //end while  判断是否为0x80
 }  // end while(!feof(fpf))

 fclose(fpf);  //关闭文件
 fclose(fpt);
 return 0;
}

经过以上的操作后,就形成了我们所要的语音库了。

实现语音合成
合成语音归根到底是根据汉字在字符集的定位来取语音库中的数据

定位方法:
根据救字的两个字节中的值.从高字节算出汉字的位wm.从低字节算出汉字的区qm,(qm一176)*94+wm一160就是该况字在汉字集里的位置position,而该汉字所对应的语音数据的偏移量就是(position一1) 3200+46。

根据定位方法取得汉字在语音库中的发音数据后,根据WAV格式合成语音文件。

定位和合成代码如下:
#define MAXLEN  32000

/*
参数str:为纯汉字的字符串,且编码格式为GBK
返回值:
-1:表示语音库文件打开错误
-2:表示合成语音文件 打开/生成错误
其它:函数执行成功
*/
int   wav(char *str)
{
 FILE * fpf,*fpt;   //文件指针
 int qm,wm;      //汉字区、位码
 int re;               //函数返回值
 long fileleng=0;   //文件长度 后面修改WAV格式时有用 
 if((fpf=fopen("ddd.wav","rb+"))==NULL)    //打开语音库文件
  return -1;
 
 if((fpt=fopen("china.wav","wb+"))==NULL)  //打开或生成合成后的语音文件,用来播放的
  return -2;

 char head[46];                       //WAV 文件头
 char buffer[MAXLEN];           //发音数据BUFF
 memset(buffer,0,MAXLEN);  //置0
 
 fread(head,sizeof(head),1,fpf);     //读语音库文件头
 fwrite(head,sizeof(head),1,fpt);    //写入合成语音文件
 
 int l=strlen(str);
 char *s=str;
 for(int i=0;i<=l;i=i+2)
 {
  qm=(unsigned char)*(s+i);      //取汉字的区码
  wm=(unsigned char)*(s+1+i);      //取汉字的位码

  if (qm<176||qm>215)   //判断是否在汉字字符集中        
   continue;
   
  if (wm<161||wm>254)  //判断是否在汉字字符集中
    continue; 

  int position =(qm-176)*94+wm-160;       
  int offset=(position-1)*MAXLEN+46;     //定位
  fseek(fpf,offset,0);
  fread(buffer,sizeof(buffer),1,fpf);     //取发音数据
  fwrite(buffer,sizeof(buffer),1,fpt);     //写入合成文件
  fileleng++;                                          //合成文件长度增加
 
 }   //end for
 
 re =fileleng;
 fileleng=fileleng*MAXLEN;
 fseek(fpt,42,SEEK_SET);
 fwrite(&fileleng,sizeof(long),1,fpt);    //修改合成文件的WAV格式,主要是修改文件大小,具体请看WAV格式表
 
 fileleng+=44;
 
 fseek(fpt,4,SEEK_SET);
 fwrite(&fileleng,sizeof(long),1,fpt);  //修改合成文件的WAV格式,主要是修改文件大小,具体请看WAV格式表
 
 fclose(fpf);     //关闭文件
 fclose(fpt);
 return re;

其它:
由函数WAV可以看出,我们接收用户的输入字符的编码必须为GBK编码,所以如果系统使用的不是 GBK编码的话,我们还应当进行编码转换。如果编码正确的话,还得从把用户的输入中把中文字符给提取出来。为此,我写了小段代码,用来过滤非中文字符的。
void trans(char *str)
{
 int i = 0, j = 0;
 while( str[i] != '\0' )
 {
  if ( str[i] < 0 )
      {
   str[j++] = str[i++];
   str[j++] = str[i++];
     }
  else
     i++;
 
 }   //end while
 str[j] = '\0';
}

后记:
     采用波形拼接的方法有个很大的缺点,就是使用的语音库文件太大,而且多音节字根本无法解决。还有一种实现中文TTS方法就是记录全部的发音,因为在普通话中,实际存在的发音只有1333种。所以我们的语音库只要保存这1333个语音就行,而不需要保存每个汉字的读音。如果语音库只有1333个读音的话,我们还得建立索引表来记录每个汉字在语音库中的位置,因为有了索引表,就可以解决多音汉字的问题,由于涉及到多音汉字发音的识别,还得用到分词技术。关于分词技术朋友们可以看下 http://www.nlp.org.cn/

关于takaya朋友问的实现自然连续的发音问题:
   说实话,这是我最想解决和实现的问题,我自己想了下,如果以词组为单位来做语音库,那声音听起来就比较连续,因为我们平时说话 也是以词组为单位说的,但如果要实现这个的话,就得用到分词技术,而且语音库的建立也要一定的功夫。

白: 我一直是以程序员的角度去做这个简单的TTS的,所以很多东西我自己也有误解或不懂的地方,希望朋友们不要笑话我,并且能帮我提出问题和指导我。
这个TTS还有很多很多如前言列出的需要改进的地方,我希望能有这方面爱好的朋友能我和一起来完善它。
期待ing...

参考文献:
1郭兰英. 汉语语音拼接技术的研究. 计算机应用软件第22卷第11期
2袁嵩. 一个TTS系统的实现方案. 计算机工程与应用2004.21 229

C/C++程序员应聘常见面试题

WAVE文件格式说明表
 
 偏移地址 字节数 数据类型 内 容
文件头
 00H 4  Char "RIFF"标志
04H 4 int32 文件长度
08H 4 Char "WAVE"标志
0CH 4 Char "fmt"标志
10H 4   过渡字节(不定)
14H 2 int16 格式类别
16H 2 int16 通道数
18H 2 int16  采样率(每秒样本数),表示每个通道的播放速度
1CH 4 int32 波形音频数据传送速率
20H 2 int16 数据块的调整数(按字节算的)
22H 2   每样本的数据位数
24H 4 Char 数据标记符"data"
28H 4 int32 语音数据的长度
 

  解答:
 
  将WAV文件格式定义为结构体WAVEFORMAT:
typedef struct tagWaveFormat
{
 char cRiffFlag[4];
 UIN32 nFileLen;
 char cWaveFlag[4];
 char cFmtFlag[4];
 char cTransition[4];
 UIN16 nFormatTag ;
 UIN16 nChannels;
 UIN16 nSamplesPerSec;
 UIN32 nAvgBytesperSec;
 UIN16 nBlockAlign;
 UIN16 nBitNumPerSample;
 char cDataFlag[4];
 UIN16 nAudioLength;
 
} WAVEFORMAT;
 

  假设WAV文件内容读出后存放在指针buffer开始的内存单元内,则分析文件格式的代码很简单,为:
WAVEFORMAT waveFormat;
memcpy( &waveFormat, buffer,sizeof( WAVEFORMAT ) );
 

  直接通过访问waveFormat的成员,就可以获得特定WAV文件的各项格式信息。
 
 
 
 
 
 
 
 
 
 
1.引言

  本文的写作目的并不在于提供C/C++程序员求职面试指导,而旨在从技术上分析面试题的内涵。文中的大多数面试题来自各大论坛,部分试题解答也参考了网友的意见。

  许多面试题看似简单,却需要深厚的基本功才能给出完美的解答。企业要求面试者写一个最简单的strcpy函数都可看出面试者在技术上究竟达到了怎样的程度,我们能真正写好一个strcpy函数吗?我们都觉得自己能,可是我们写出的strcpy很可能只能拿到10分中的2分。读者可从本文看到strcpy函数从2分到10分解答的例子,看看自己属于什么样的层次。此外,还有一些面试题考查面试者敏捷的思维能力。

  分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。

  2.找错题

  试题1:

void test1()
{
 char string[10];
 char* str1 = "0123456789";
 strcpy( string, str1 );
}

  试题2:

void test2()
{
 char string[10], str1[10];
 int i;
 for(i=0; i<10; i++)
 {
  str1[i] = 'a';
 }
 strcpy( string, str1 );
}

  试题3:

void test3(char* str1)
{
 char string[10];
 if( strlen( str1 ) <= 10 )
 {
  strcpy( string, str1 );
 }
}

  解答:

  试题1字符串str1需要11个字节才能存放下(包括末尾的'\0'),而string只有10个字节的空间,strcpy会导致数组越界;

  对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;

  对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计'\0'所占用的1个字节。

  剖析:

  考查对基本功的掌握:

  (1)字符串以'\0'结尾;

  (2)对数组越界把握的敏感度;

  (3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:

  2分

void strcpy( char *strDest, char *strSrc )
{
  while( (*strDest++ = * strSrc++) != '\0' );
}

  4分

void strcpy( char *strDest, const char *strSrc )
//将源字符串加const,表明其为输入参数,加2分
{
  while( (*strDest++ = * strSrc++) != '\0' );
}

  7分

void strcpy(char *strDest, const char *strSrc)
{
 //对源地址和目的地址加非0断言,加3分
 assert( (strDest != NULL) && (strSrc != NULL) );
 while( (*strDest++ = * strSrc++) != '\0' );
}

  10分

//为了实现链式操作,将目的地址返回,加3分!

char * strcpy( char *strDest, const char *strSrc )
{
 assert( (strDest != NULL) && (strSrc != NULL) );
 char *address = strDest;
 while( (*strDest++ = * strSrc++) != '\0' );
  return address;
}

  从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!

  (4)对strlen的掌握,它没有包括字符串末尾的'\0'。

  读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const

{
 assert( strt != NULL ); //断言字符串地址非0
 int len;
 while( (*str++) != '\0' )
 {
  len++;
 }
 return len;
}

  试题4:

void GetMemory( char *p )
{
 p = (char *) malloc( 100 );
}

void Test( void )
{
 char *str = NULL;
 GetMemory( str );
 strcpy( str, "hello world" );
 printf( str );
}

  试题5:

char *GetMemory( void )
{
 char p[] = "hello world";
 return p;
}

void Test( void )
{
 char *str = NULL;
 str = GetMemory();
 printf( str );
}

  试题6:

void GetMemory( char **p, int num )
{
 *p = (char *) malloc( num );
}

void Test( void )
{
 char *str = NULL;
 GetMemory( &str, 100 );
 strcpy( str, "hello" );
 printf( str );
}

  试题7:

void Test( void )
{
 char *str = (char *) malloc( 100 );
 strcpy( str, "hello" );
 free( str );
 ... //省略的其它语句
}

  解答:

  试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完

char *str = NULL;
GetMemory( str );

  后的str仍然为NULL;

  试题5中

char p[] = "hello world";
return p;

  的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。

  试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句

*p = (char *) malloc( num );

  后未判断内存是否申请成功,应加上:

if ( *p == NULL )
{
 ...//进行申请内存失败处理
}

  试题7存在与试题6同样的问题,在执行

char *str = (char *) malloc(100);

  后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个"野"指针,应加上:

str = NULL;

  试题6的Test函数中也未对malloc的内存进行释放。

  剖析:

  试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。

  对内存操作的考查主要集中在:

  (1)指针的理解;

  (2)变量的生存期及作用范围;

  (3)良好的动态内存申请和释放习惯。

  再看看下面的一段程序有什么错误:

swap( int* p1,int* p2 )
{
 int *p;
 *p = *p1;
 *p1 = *p2;
 *p2 = *p;
}

  在swap函数中,p是一个"野"指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误"Access Violation"。该程序应该改为:

swap( int* p1,int* p2 )
{
 int p;
 p = *p1;
 *p1 = *p2;
 *p2 = p;
}

  3.内功题

  试题1:分别给出BOOL,int,float,指针变量 与"零值"比较的 if 语句(假设变量名为var)

  解答:

   BOOL型变量:if(!var)

   int型变量: if(var==0)

   float型变量:

   const float EPSINON = 0.00001;

   if ((x >= - EPSINON) && (x <= EPSINON)

   指针变量:  if(var==NULL)

  剖析:

  考查对0值判断的"内功",BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。

  一般的,如果想让if判断一个变量的"真"、"假",应直接使用if(var)、if(!var),表明其为"逻辑"判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行"数值"上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。

  浮点型变量并不精确,所以不可将float变量用"=="或"!="与数字比较,应该设法转化成">="或"<="形式。如果写成if (x == 0.0),则判为错,得0分。

  试题2:以下为Windows NT下的32位C++程序,请计算sizeof的值

void Func ( char str[100] )
{
 sizeof( str ) = ?
}

void *p = malloc( 100 );
sizeof ( p ) = ?

  解答:

sizeof( str ) = 4
sizeof ( p ) = 4

  剖析:

  Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。

  数组名的本质如下:

  (1)数组名指代一种数据结构,这种数据结构就是数组;

  例如:

char str[10];
cout << sizeof(str) << endl;

  输出结果为10,str指代数据结构char[10]。

  (2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改;

char str[10];
str++; //编译出错,提示str不是左值 

  (3)数组名作为函数形参时,沦为普通指针。

  Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。

  试题3:写一个"标准"宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事?

least = MIN(*p++, b);

  解答:

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

  MIN(*p++, b)会产生宏的副作用

  剖析:

  这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的"参数"也不是真的参数,在宏展开的时候对"参数"进行的是一对一的替换。

  程序员对宏定义的使用要非常小心,特别要注意两个问题:

  (1)谨慎地将宏定义中的"参数"和整个宏用用括弧括起来。所以,严格地讲,下述解答:

#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )

  都应判0分;

  (2)防止宏的副作用。

  宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:

((*p++) <= (b) ? (*p++) : (*p++))

  这个表达式会产生副作用,指针p会作三次++自增操作。

  除此之外,另一个应该判0分的解答是:

#define MIN(A,B) ((A) <= (B) ? (A) : (B));

  这个解答在宏定义的后面加";",显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。

  试题4:为什么标准头文件都有类似以下的结构?

#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus

extern "C" {

Wednesday, April 25, 2007

xargs的简单应用

 
 
 
linux shell 参数有关
 

Linux Shell真的是一个比较好玩的东西,以前也会经常写一些,不过都是些简单的东西,或者称之为批处理,估计也只能叫批处理,只是一些简单的命令的堆积。:)。

今天稍微看了一下xargs,一个简单的示例应用如下:
[root@TestServer haha]#ls
a.class b.class c.class
a.java b.java c.java
[root@TestServer haha]#ls |xargs rm
[root@TestServer haha]#ls
[root@TestServer haha]#

呵呵,虽然有点避简就烦,但是能说明了xargs的功能:它将输入输出给xargs后面的命令,作为那个命令的参数

也就是说,上面的命令执行相当于:
rm a.class b.class c.class a.java b.java c.java

xargs将前面的ls的结果交给了xargs后面的rm命令,作为rm命令的参数。它真正的含义可以用xargs自己来解释:

[root@TestServer haha]# cat a
first line
second line
[root@TestServer haha]# cat a |xargs
first line second line
[root@TestServer haha]# cat a |xargs --verbose
/bin/echo first line second line
first line second line third line
[root@TestServer haha]# cat a |xargs --verbose --max-args=1
/bin/echo first
first
/bin/echo line
line
/bin/echo second
second
/bin/echo line
line

还有人这样解释xargs:xargs的作用是用来回避对命令行长度的限制,它通过使用一个参数多次调用一个命令实现这一功能,而不是一次使用多个参数。这句话的意思是:xargs永远一次只传给他后面的命令一个参数,然后不停的将所有参数一一传完,然而,这个说法是错误的!下面的试验将证明这个说法是不正确的:

[root@TestServer haha]# ll
total 12
-rw-r--r-- 1 root root 34 Aug 19 20:43 a
-rw-r--r-- 1 root root 34 Aug 19 20:45 b
drwxr-xr-x 2 root root 4096 Aug 19 20:54 c_folder
[root@TestServer haha]# ll c_folder/
total 0
[root@TestServer haha]# ls| xargs cp
[root@TestServer haha]# ll
total 12
-rw-r--r-- 1 root root 34 Aug 19 20:43 a
-rw-r--r-- 1 root root 34 Aug 19 20:45 b
drwxr-xr-x 2 root root 4096 Aug 19 20:54 c_folder
[root@TestServer haha]# ll c_folder/
total 8
-rw-r--r-- 1 root root 34 Aug 19 20:54 a
-rw-r--r-- 1 root root 34 Aug 19 20:54 b
[root@TestServer haha]#

从这个试验可以看到,xargs确实是将a b c_folder三个参数同时传给了cp命令,不然不可能出现这样的结果。

看看周末有没有时间好好看看awk是怎么玩的,好像很强大的样子!:)

 

 

xargs 命令 
用途
构造参数列表并运行命令。
语法
xargs [  -p ] [  -t ] [  -e [ EOFString ] ] [  -E EOFString ] [  -i [ 
ReplaceString ] ] [  -I ReplaceString ] [  -l [ Number ] ] [  -L Number ] [  -n 
Number  [  -x ] ] [  -s Size ] [ Command [ Argument ... ] ]
  注: 不要在小写的标志和参数之间放置空格。
描述
生成的命令行长度是 Command 和每个作为字符串对待的 Argument,包括用于每个字符串的空字节结束符号,大小(以字节计算)的总和。xargs 
命令限制命令行的长度。当构造的命令行运行时,组合的 Argument 和环境列表不能超过 ARG_MAX 字节。在这一约束里,如果不指定 -n 或 -s 
标志,缺省命令行长度至少是 LINE_MAX 指定的值。
标志
      -e[EOFString]废弃的标志。请使用 -E 标志。 
      将 EOFString 参数用作逻辑 EOF 字符串。如果不指定 -e 或 -E 标志,则采用下划线(_)为逻辑 EOF 字符串。如果不指定 
      EOFString 参数,逻辑 EOF 字符串能力被禁用且下划线被照字面含义使用。xargs 命令读取标准输入直到达到 EOF 或指定的字符串。
      -E EOFString指定逻辑 EOF 字符串以替换缺省的下划线(_)。 xargs 命令读取标准输入直到达到 EOF 或指定的字符串。
      -i[ReplaceString]废弃的标志。请使用 -I(大写 i)标志。 
      如果没有指定 ReplaceString 参数,使用字符串 "{}"。
        注:-I(大写 i)和 -i 标志是互相排斥的;最后指定的标志生效。
      -I ReplaceString(大写 i)。插入标准输入的每一行用 Command 参数的自变量,把它插入出现的每个 ReplaceString 
      的 Argument 中。ReplaceStrings 不能在超过 5 个自变量中使用。在每个标准输入行开始的空字符被忽略。每个 Argument 
      能包含一个或多个 ReplaceStrings,但不能大于 255 字节。-I 标志同样打开 -x 标志。 
        注:-I(大写 i)和 -i 标志是互相排斥的;最后指定的标志生效。
      -l[Number](小写的 L)。废弃的标志。请使用 -L 标志。 
      如果没有指定 Number 参数,使用缺省值 1。-l 标志同样打开 -x 标志。
        注: -L、-I(小写的 L)和 -n 标志是互相排斥的;最后指定的标志生效。
      -L Number用从标准输入读取的非空参数行的指定的数量运行 Command 命令。如果保留少于指定的 Number,Command 
      参数的最后调用可以有少数几个参数行。一行以第一个换行字符结束,除非行的最后一个字符是一个空格或制表符。后续的空格表示延续至下一个非空行。 
        注: -L、-I(小写的 L)和 -n 标志是互相排斥的;最后指定的标志生效。
      -n Number运行 Command 参数,且使用尽可能多的标准输入自变量,直到 Number 参数指定的最大值。xargs 
      命令使用很少的自变量,如果: 
        如果被积累的命令行长度超过了由 -s Size 标志指定的字节。 
        最后的迭代有少于 Number(但是非零)的自变量保留。 
          注: -L、-I(小写的 L)和 -n 标志是互相排斥的;最后指定的标志生效。
      -p询问是否运行 Command 参数。它显示构造的命令行,后跟一个 ?...(问号和省略号)提示。输入肯定的、特定于语言环境的响应以运行 
      Command 参数。任何其它响应都会引起 xargs 命令跳过那个特定的参数调用。每个调用都将询问您。 -p 标志同样打开 -t 标志。
      -s Size设置构造的 Command 行的最大总大小。Size 参数必须是正整数。如果满足以下条件,则使用很少的自变量: 
        自变量的总数超出 -n 标志指定的自变量数。 
        总行数超出 -L 或 -I(小写 L)标志指定的行数。 
        累加至在 Size 参数指定的字节数之前达到 EOF。

      -t启用跟踪方式并在运行之前将构造的 Command 行回送到标准错误。
      -x如果有任何 Command 行大于 -s Size 标志指定的字节数,停止运行 xargs 命令。如果指定 -I(大写 i)或 -l(小写 
      L)标志,则打开 -x 标志。如果没有指定 -i、-I(大写 i)、-l(小写 L)、-L 或 -n 标志,则 Command 行的总长度必须在 
      -s Size 标志指定的限制内。

出口状态
该命令返回下列出口值:
      0所有 Command 参数的调用都返回出口状态 0。
      1-125不能组装满足指定要求的命令行,一个或多个 Command 参数的调用返回一个非零出口状态,或发生一些其它的错误。
      126Command 已找到但不能被调用。
      127找不到 Command。

如果不能组装满足指定要求的命令行,则不能调用这个命令,命令的调用被一个信号终止,或一个命令调用以出口状态 255 退出。xargs 
命令将写一条诊断消息并退出而不处理任何保留的输入。
示例
  要对名称在一个文件中列出的文件使用命令,输入: 
xargs lint -a <cfiles如果 cfiles 文件包含下面的文本:
main.c readit.c
gettoken.c
putobj.cxargs 命令就构造并运行下面的命令:
lint -a main.c readit.c gettoken.c putobj.c如果 cfiles 文件包含比列出在单一外壳程序命令行上的文件名更多的文件名(最多 LINE_MAX),xargs 命令会用列出的文件名运行 lint 
  命令。然后它使用余下的文件名构造并运行另一个 lint 命令。根据在 cfiles 文件中列出的文件名,命令看起来可能类似于如下所示的内容:
lint -a main.c readit.c gettoken.c . . .
lint -a getisx.c getprp.c getpid.c . . .
lint -a fltadd.c fltmult.c fltdiv.c . . .这一命令序列同用所有的文件名运行 lint 命令一次不完全一样。lint 命令检查文件之间的交叉引用。然而,在这个示例中,它不能在 main.c 和 
  fltadd.c 文件之间,或任意两个在分开的命令行上列出的两个文件之间进行检查。
  由于这个原因,仅当所有的文件名都在一行上列出时,才可能运行命令。要将这个指定到 xargs命令,通过输入以下命令使用 -x 标志:

  xargs  -x lint -a <cfiles
  如果在文件 cfiles 中的所有文件名没有在一个命令行上列出,xargs 命令显示一条错误消息。 
  要构造包含一定数量文件名的命令,输入: 
  xargs  -t  -n 2 diff <<EOF
  starting chap1 concepts chap2 writing
  chap3
  EOF
  这一命令序列构造并运行每个包含两个文件名的 diff 命令(-n 2): 
diff starting chap1
diff concepts chap2
diff writing chap3-t 标志使 xargs 命令在运行每个命令之前显示该命令,所以能看到正在发生的事件。<<EOF 和 EOF 模式匹配字符定义一个 here 
  document,它把在结尾行之前输入的文本用作对 xargs 命令的标准输入。
  要把文件名插入命令行的中间,输入: 
  ls | xargs  -t  -I {} mv {} {}.old
  这一命令序列通过在每个名字结尾添加 .old 来重命名在当前目录里的所有文件。-I 标志告诉 xargs 命令插入有{}(花括号)出现的 ls 
  目录列表的每一行。如果当前目录包含文件 chap1、chap2 和 chap3,这会构造下面的命令: 
mv chap1 chap1.old
mv chap2 chap2.old
mv chap3 chap3.old要对独立选择的文件运行命令,输入: 
  ls | xargs  -p  -n 1 ar r lib.a
  这一命令序列允许选择文件以添加到 lib.a 库。-p 标志告诉 xargs 命令去显示每一个它构造的 ar命令并询问是否想运行它。输入 y 
  来运行命令。如果不想运行这个命令按任意其它键。 
  会显示一些类似于下面的内容:
ar r lib.a chap1 ?...
ar r lib.a chap2 ?...
ar r lib.a chap3 ?... 要构造包含特定数量自变量的命令并将那些自变量插入一个命令行的中间,输入: 
ls | xargs -n6 | xargs -I{} echo {} - some files in the directory如果当前目录包含从 chap1 到 chap10 的文件,构造的输出将会是下列内容:
chap1 chap2 chap3 chap4 chap5 chap6 - some files in the directory
chap7 chap8 chap9 chap10 - some file in the directory文件
      /usr/bin/xargs包含 xargs 命令。

C++风格与技巧

C++风格与技巧
 
 
 
C++风格与技巧
作者:Bjarne Stroustrup

目录:

我如何写这个非常简单的程序?

为什么编译要花这么长的时间?

为什么一个空类的大小不为0?

我必须在类声明处赋予数据吗?

为什么成员函数默认不是virtual的?

为什么析构函数默认不是virtual的?

为什么不能有虚拟构造函数?

为什么重载在继承类中不工作?

我能够在构造函数中调用一个虚拟函数吗?

有没有"指定位置删除"(placement delete)?

我能防止别人继承我自己的类吗?

为什么不能为模板参数定义约束(constraints)?

既然已经有了优秀的qsort()函数,为什么还需要一个sort()?

什么是函数对象(function object)?

我应该如何对付内存泄漏?

我为什么在捕获一个异常之后就不能继续?

为什么C++中没有相当于realloc()的函数?

如何使用异常?

怎样从输入中读取一个字符串?

为什么C++不提供"finally"的构造?

什么是自动指针(auto_ptr),为什么没有自动数组(auto_array)?

可以混合使用C风格与C++风格的内存分派与重新分配吗?

我为什么必须使用一个造型来转换*void?

我如何定义一个类内部(in-class)的常量?

为什么delete不会将操作数置0?

我能够写"void main()"吗?

为什么我不能重载点符号,::,sizeof,等等?

怎样将一个整型值转换为一个字符串?

"int* p"正确还是"int *p"正确?

对于我的代码,哪一种布局风格(layout style)是最好的?

我应该将"const"放在类型之前还是之后?

使用宏有什么问题?

我如何写这个非常简单的程序?

特别是在一个学期的开始,我常常收到许多关于编写一个非常简单的程序的询问。这个问题有一个很具代表性的解决方法,那就是(在你的程序中)读入几个数字,对它们做一些处理,再把结果输出。下面是一个这样做的例子:

#include<iostream>

#include<vector>

#include<algorithm>

using namespace std;

int main()

{

vector<double> v;

double d;

while(cin>>d) v.push_back(d); // 读入元素

if (!cin.eof()) { // 检查输入是否出错

cerr << "format error\n";

return 1; // 返回一个错误

}

cout << "read " << v.size() << " elements\n";

reverse(v.begin(),v.end());

cout << "elements in reverse order:\n";

for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';

return 0; // 成功返回

}

对这段程序的观察:

这是一段标准的ISO C++程序,使用了标准库(standard library)。标准库工具在命名空间std中声明,封装在没有.h后缀的头文件中。

如果你要在Windows下编译它,你需要将它编译成一个"控制台程序"(console application)。记得将源文件加上.cpp后缀,否则编译器可能会以为它是一段C代码而不是C++。

是的,main()函数返回一个int值。

读到一个标准的向量(vector)中,可以避免在随意确定大小的缓冲中溢出的错误。读到一个数组(array)中,而不产生"简单错误"(silly error),这已经超出了一个新手的能力――如果你做到了,那你已经不是一个新手了。如果你对此表示怀疑,我建议你阅读我的文章"将标准C++作为一种新的语言来学习"("Learning Standard C++ as a New Language"),你可以在本人著作列表(my publications list)中下载到它。

!cin.eof()是对流的格式的检查。事实上,它检查循环是否终结于发现一个end-of-file(如果不是这样,那么意味着输入没有按照给定的格式)。更多的说明,请参见你的C++教科书中的"流状态"(stream state)部分。

vector知道它自己的大小,因此我不需要计算元素的数量。

这段程序没有包含显式的内存管理。Vector维护一个内存中的栈,以存放它的元素。当一个vector需要更多的内存时,它会分配一些;当它不再生存时,它会释放内存。于是,使用者不需要再关心vector中元素的内存分配和释放问题。

程序在遇到输入一个"end-of-file"时结束。如果你在UNIX平台下运行它,"end-of-file"等于键盘上的Ctrl+D。如果你在Windows平台下,那么由于一个BUG它无法辨别"end-of-file"字符,你可能倾向于使用下面这个稍稍复杂些的版本,它使用一个词"end"来表示输入已经结束。

#include<iostream>

#include<vector>

#include<algorithm>

#include<string>

using namespace std;

int main()

{

vector<double> v;

double d;

while(cin>>d) v.push_back(d); // 读入一个元素

if (!cin.eof()) { // 检查输入是否失败

cin.clear(); // 清除错误状态

string s;

cin >> s; // 查找结束字符

if (s != "end") {

cerr << "format error\n";

return 1; // 返回错误

}

}

cout << "read " << v.size() << " elements\n";

reverse(v.begin(),v.end());

cout << "elements in reverse order:\n";

for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';

return 0; // 成功返回

}

更多的关于使用标准库将事情简化的例子,请参见《C++程序设计语言》中的"漫游标准库"("Tour of the Standard Library")一章。

为什么编译要花这么长的时间?

你的编译器可能有问题。也许它太老了,也许你安装它的时候出了错,也许你用的计算机已经是个古董。在诸如此类的问题上,我无法帮助你。

但是,这也是很可能的:你要编译的程序设计得非常糟糕,以至于编译器不得不检查数以百计的头文件和数万行代码。理论上来说,这是可以避免的。如果这是你购买的库的设计问题,你对它无计可施(除了换一个更好的库),但你可以将你自己的代码组织得更好一些,以求得将修改代码后的重新编译工作降到最少。这样的设计会更好,更有可维护性,因为它们展示了更好的概念上的分离。

看看这个典型的面向对象的程序例子:

class Shape {

public: // 使用Shapes的用户的接口

virtual void draw() const;

virtual void rotate(int degrees);

// ...

protected: // common data (for implementers of Shapes)

Point center;

Color col;

// ...

};

class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

// ...

protected:

int radius;

// ...

};

class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

// ...

protected:

Point a, b, c;

// ...

};

设计思想是,用户通过Shape的public接口来操纵它们,而派生类(例如Circle和Triangle)的实现部分则共享由protected成员表现的那部分实现(implementation)。

这不是一件容易的事情:确定哪些实现部分是对所有的派生类都有用的,并将之共享出来。因此,与public接口相比,protected成员往往要做多得多的改动。举例来说,虽然理论上"中心"(center)对所有的图形都是一个有效的概念,但当你要维护一个三角形的"中心"的时候,是一件非常麻烦的事情――对于三角形,当且仅当它确实被需要的时候,计算这个中心才是有意义的。

protected成员很可能要依赖于实现部分的细节,而Shape的用户(译注:user此处译为用户,指使用Shape类的代码,下同)却不见得必须依赖它们。举例来说,很多(大多数?)使用Shape的代码在逻辑上是与"颜色"无关的,但是由于Shape中"颜色"这个定义的存在,却可能需要一堆复杂的头文件,来结合操作系统的颜色概念。

当protected部分发生了改变时,使用Shape的代码必须重新编译――即使只有派生类的实现部分才能够访问protected成员。

于是,基类中的"实现相关的信息"(information helpful to implementers)对用户来说变成了象接口一样敏感的东西,它的存在导致了实现部分的不稳定,用户代码的无谓的重编译(当实现部分发生改变时),以及将头文件无节制地包含进用户代码中(因为"实现相关的信息"需要它们)。有时这被称为"脆弱的基类问题"(brittle base class problem)。

一个很明显的解决方案就是,忽略基类中那些象接口一样被使用的"实现相关的信息"。换句话说,使用接口,纯粹的接口。也就是说,用抽象基类的方式来表示接口:

class Shape {

public: //使用Shapes的用户的接口

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...

// 没有数据

};

class Circle : public Shape {

public:

void draw() const;

void rotate(int) { }

Point center() const { return center; }

// ...

protected:

Point cent;

Color col;

int radius;

// ...

};

class Triangle : public Shape {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Color col;

Point a, b, c;

// ...

};

现在,用户代码与派生类的实现部分的变化之间的关系被隔离了。我曾经见过这种技术使得编译的时间减少了几个数量级。

但是,如果确实存在着对所有派生类(或仅仅对某些派生类)都有用的公共信息时怎么办呢?可以简单把这些信息封装成类,然后从它派生出实现部分的类:

class Shape {

public: //使用Shapes的用户的接口

virtual void draw() const = 0;

virtual void rotate(int degrees) = 0;

virtual Point center() const = 0;

// ...

// no data

};

struct Common {

Color col;

// ...

};

class Circle : public Shape, protected Common {

public:

void draw() const;

void rotate(int) { }

Point center() const { return center; }

// ...

protected:

Point cent;

int radius;

};

class Triangle : public Shape, protected Common {

public:

void draw() const;

void rotate(int);

Point center() const;

// ...

protected:

Point a, b, c;

};

为什么一个空类的大小不为0?

要清楚,两个不同的对象的地址也是不同的。基于同样的理由,new总是返回指向不同对象的指针。

看看:

class Empty { };

void f()

{

Empty a, b;

if (&a == &b) cout << "impossible: report error to compiler supplier";

Empty* p1 = new Empty;

Empty* p2 = new Empty;

if (p1 == p2) cout << "impossible: report error to compiler supplier";

}

有一条有趣的规则:一个空的基类并不一定有分隔字节。

struct X : Empty {

int a;

// ...

};

void f(X* p)

{

void* p1 = p;

void* p2 = &p->a;

if (p1 == p2) cout << "nice: good optimizer";

}

这种优化是允许的,可以被广泛使用。它允许程序员使用空类以表现一些简单的概念。现在有些编译器提供这种"空基类优化"(empty base class optimization)。

我必须在类声明处赋予数据吗?

不必须。如果一个接口不需要数据时,无须在作为接口定义的类中赋予数据。代之以在派生类中给出它们。参见"为什么编译要花这么长的时间?"。

有时候,你必须在一个类中赋予数据。考虑一下复数类的情况:

template<class Scalar> class complex {

public:

complex() : re(0), im(0) { }

complex(Scalar r) : re(r), im(0) { }

complex(Scalar r, Scalar i) : re(r), im(i) { }

// ...

complex& operator+=(const complex& a)

{ re+=a.re; im+=a.im; return *this; }

// ...

private:

Scalar re, im;

};

设计这种类型的目的是将它当做一个内建(built-in)类型一样被使用。在声明处赋值是必须的,以保证如下可能:建立真正的本地对象(genuinely local objects)(比如那些在栈中而不是在堆中分配的对象),或者使某些简单操作被适当地inline化。对于那些支持内建的复合类型的语言来说,要获得它们提供的效率,真正的本地对象和inline化都是必要的。

为什么成员函数默认不是virtual的?

因为很多类并不是被设计作为基类的。例如复数类。

而且,一个包含虚拟函数的类的对象,要占用更多的空间以实现虚拟函数调用机制――往往是每个对象占用一个字(word)。这个额外的字是非常可观的,而且在涉及和其它语言的数据的兼容性时,可能导致麻烦(例如C或Fortran语言)。

要了解更多的设计原理,请参见《C++语言的设计和演变》(The Design and Evolution of C++)。

为什么析构函数默认不是virtual的?

因为很多类并不是被设计作为基类的。只有类在行为上是它的派生类的接口时(这些派生类往往在堆中分配,通过指针或引用来访问),虚拟函数才有意义。

那么什么时候才应该将析构函数定义为虚拟呢?当类至少拥有一个虚拟函数时。拥有虚拟函数意味着一个类是派生类的接口,在这种情况下,一个派生类的对象可能通过一个基类指针来销毁。例如:

class Base {

// ...

virtual ~Base();

};

class Derived : public Base {

// ...

~Derived();

};

void f()

{

Base* p = new Derived;

delete p; // 虚拟析构函数保证~Derived函数被调用

}

如果基类的析构函数不是虚拟的,那么派生类的析构函数将不会被调用――这可能产生糟糕的结果,例如派生类的资源不会被释放。

为什么不能有虚拟构造函数?

虚拟调用是一种能够在给定信息不完全(given partial information)的情况下工作的机制。特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知道具体的对象类型。但是要建立一个对象,你必须拥有完全的信息。特别地,你需要知道要建立的对象的具体类型。因此,对构造函数的调用不可能是虚拟的。

当要求建立一个对象时,一种间接的技术常常被当作"虚拟构造函数"来使用。有关例子,请参见《C++程序设计语言》第三版15.6.2.节。

下面这个例子展示一种机制:如何使用一个抽象类来建立一个适当类型的对象。

struct F { // 对象建立函数的接口

virtual A* make_an_A() const = 0;

virtual B* make_a_B() const = 0;

};

void user(const F& fac)

{

A* p = fac.make_an_A(); // 将A作为合适的类型

B* q = fac.make_a_B(); // 将B作为合适的类型

// ...

}

struct FX : F {

A* make_an_A() const { return new AX(); } // AX是A的派生

B* make_a_B() const { return new BX(); } // AX是B的派生

};

struct FY : F {

A* make_an_A() const { return new AY(); } // AY是A的派生

B* make_a_B() const { return new BY(); } // BY是B的派生

};

int main()

{

user(FX()); // 此用户建立AX与BX

user(FY()); // 此用户建立AY与BY

// ...

}

这是所谓的"工厂模式"(the factory pattern)的一个变形。关键在于,user函数与AX或AY这样的类的信息被完全分离开来了。

为什么重载在继承类中不工作?

这个问题(非常常见)往往出现于这样的例子中:

#include<iostream>

using namespace std;

class B {

public:

int f(int i) { cout << "f(int): "; return i+1; }

// ...

};

class D : public B {

public:

double f(double d) { cout << "f(double): "; return d+1.3; }

// ...

};

int main()

{

D* pd = new D;

cout << pd->f(2) << '\n';

cout << pd->f(2.3) << '\n';

}

它输出的结果是:

f(double): 3.3

f(double): 3.6

而不是象有些人猜想的那样:

f(int): 3

f(double): 3.6

换句话说,在B和D之间并没有发生重载的解析。编译器在D的区域内寻找,找到了一个函数double f(double),并执行了它。它永远不会涉及(被封装的)B的区域。在C++中,没有跨越区域的重载――对于这条规则,继承类也不例外。更多的细节,参见《C++语言的设计和演变》和《C++程序设计语言》。

但是,如果我需要在基类和继承类之间建立一组重载的f()函数呢?很简单,使用using声明:

class D : public B {

public:

using B::f; // make every f from B available

double f(double d) { cout << "f(double): "; return d+1.3; }

// ...

};

进行这个修改之后,输出结果将是:

f(int): 3

f(double): 3.6

这样,在B的f()和D的f()之间,重载确实实现了,并且选择了一个最合适的f()进行调用。

我能够在构造函数中调用一个虚拟函数吗?

可以,但是要小心。它可能不象你期望的那样工作。在构造函数中,虚拟调用机制不起作用,因为继承类的重载还没有发生。对象先从基类被创建,"基类先于继承类(base before derived)"。

看看这个:

#include<string>

#include<iostream>

using namespace std;

class B {

public:

B(const string& ss) { cout << "B constructor\n"; f(ss); }

virtual void f(const string&) { cout << "B::f\n";}

};

class D : public B {

public:

D(const string & ss) :B(ss) { cout << "D constructor\n";}

void f(const string& ss) { cout << "D::f\n"; s = ss; }

private:

string s;

};

int main()

{

D d("Hello");

}

程序编译以后会输出:

B constructor

B::f

D constructor

注意不是D::f。设想一下,如果出于不同的规则,B::B()可以调用D::f()的话,会产生什么样的后果:因为构造函数D::D()还没有运行,D::f()将会试图将一个还没有初始化的字符串s赋予它的参数。结果很可能是导致立即崩溃。

析构函数在"继承类先于基类"的机制下运行,因此虚拟机制的行为和构造函数一样:只有本地定义(local definitions)被使用――不会调用虚拟函数,以免触及对象中的(现在已经被销毁的)继承类的部分。

更多的细节,参见《C++语言的设计和演变》13.2.4.2和《C++程序设计语言》15.4.3。

有人暗示,这只是一条实现时的人为制造的规则。不是这样的。事实上,要实现这种不安全的方法倒是非常容易的:在构造函数中直接调用虚拟函数,就象调用其它函数一样。但是,这样就意味着,任何虚拟函数都无法编写了,因为它们需要依靠基类的固定的创建(invariants established by base classes)。这将会导致一片混乱。

有没有"指定位置删除"(placement delete)?

没有,不过如果你需要的话,可以自己写一个。

看看这个指定位置创建(placement new),它将对象放进了一系列Arena中;

class Arena {

public:

void* allocate(size_t);

void deallocate(void*);

// ...

};

void* operator new(size_t sz, Arena& a)

{

return a.allocate(sz);

}

Arena a1(some arguments);

Arena a2(some arguments);

这样实现了之后,我们就可以这么写:

X* p1 = new(a1) X;

Y* p2 = new(a1) Y;

Z* p3 = new(a2) Z;

// ...

但是,以后怎样正确地销毁这些对象呢?没有对应于这种"placement new"的内建的"placement delete",原因是,没有一种通用的方法可以保证它被正确地使用。在C++的类型系统中,没有什么东西可以让我们确认,p1一定指向一个由Arena类型的a1分派的对象。p1可能指向任何东西分派的任何一块地方。

然而,有时候程序员是知道的,所以这是一种方法:

template<class T> void destroy(T* p, Arena& a)

{

if (p) {

p->~T(); // explicit destructor call

a.deallocate(p);

}

}

现在我们可以这么写:

destroy(p1,a1);

destroy(p2,a2);

destroy(p3,a3);

如果Arena维护了它保存着的对象的线索,你甚至可以自己写一个析构函数,以避免它发生错误。

这也是可能的:定义一对相互匹配的操作符new()和delete(),以维护《C++程序设计语言》15.6中的类继承体系。参见《C++语言的设计和演变》10.4和《C++程序设计语言》19.4.5。

我能防止别人继承我自己的类吗?

可以,但你为什么要那么做呢?这是两个常见的回答:

效率:避免我的函数被虚拟调用

安全:保证我的类不被用作一个基类(例如,保证我能够复制对象而不用担心出事)

根据我的经验,效率原因往往是不必要的担心。在C++中,虚拟函数调用是如此之快,以致于它们在一个包含虚拟函数的类中被实际使用时,相比普通的函数调用,根本不会产生值得考虑的运行期开支。注意,仅仅通过指针或引用时,才会使用虚拟调用机制。当直接通过对象名字调用一个函数时,虚拟函数调用的开支可以被很容易地优化掉。

如果确实有真正的需要,要将一个类封闭起来以防止虚拟调用,那么可能首先应该问问为什么它们是虚拟的。我看见过一些例子,那些性能表现不佳的函数被设置为虚拟,没有其他原因,仅仅是因为"我们习惯这么干"。

这个问题的另一个部分,由于逻辑上的原因如何防止类被继承,有一个解决方案。不幸的是,这个方案并不完美。它建立在这样一个事实的基础之上,那就是:大多数的继承类必须建立一个虚拟的基类。这是一个例子:

class Usable;

class Usable_lock {

friend class Usable;

private:

Usable_lock() {}

Usable_lock(const Usable_lock&) {}

};

class Usable : public virtual Usable_lock {

// ...

public:

Usable();

Usable(char*);

// ...

};

Usable a;

class DD : public Usable { };

DD dd; // 错误: DD::DD() 不能访问

// Usable_lock::Usable_lock()是一个私有成员

(来自《C++语言的设计和演变》11.4.3)

为什么不能为模板参数定义约束(constraints)?

可以的,而且方法非常简单和通用。

看看这个:

template<class Container>

void draw_all(Container& c)

{

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}

如果出现类型错误,可能是发生在相当复杂的for_each()调用时。例如,如果容器的元素类型是int,我们将得到一个和for_each()相关的含义模糊的错误(因为不能够对对一个int值调用Shape::draw的方法)。

为了提前捕捉这个错误,我这样写:

template<class Container>

void draw_all(Container& c)

{

Shape* p = c.front(); // accept only containers of Shape*s

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}

对于现在的大多数编译器,中间变量p的初始化将会触发一个易于了解的错误。这个窍门在很多语言中都是通用的,而且在所有的标准创建中都必须这样做。在成品的代码中,我也许可以这样写:

template<class Container>

void draw_all(Container& c)

{

typedef typename Container::value_type T;

Can_copy<T,Shape*>(); // accept containers of only Shape*s

for_each(c.begin(),c.end(),mem_fun(&Shape::draw));

}

这样就很清楚了,我在建立一个断言(assertion)。Can_copy模板可以这样定义:

template<class T1, class T2> struct Can_copy {

static void constraints(T1 a, T2 b) { T2 c = a; b = a; }

Can_copy() { void(*p)(T1,T2) = constraints; }

};

Can_copy(在运行时)检查T1是否可以被赋值给T2。Can_copy<T,Shape*>检查T是否是Shape*类型,或者是一个指向由Shape类公共继承而来的类的对象的指针,或者是被用户转换到Shape*类型的某个类型。注意这个定义被精简到了最小:

一行命名要检查的约束,和要检查的类型

一行列出指定的要检查的约束(constraints()函数)

一行提供触发检查的方法(通过构造函数)

注意这个定义有相当合理的性质:

你可以表达一个约束,而不用声明或复制变量,因此约束的编写者可以用不着去设想变量如何被初始化,对象是否能够被复制,被销毁,以及诸如此类的事情。(当然,约束要检查这些属性的情况时例外。)

使用现在的编译器,不需要为约束产生代码

定义和使用约束,不需要使用宏

当约束失败时,编译器会给出可接受的错误信息,包括"constraints"这个词(给用户一个线索),约束的名字,以及导致约束失败的详细错误(例如"无法用double*初始化Shape*")。

那么,在C++语言中,有没有类似于Can_copy――或者更好――的东西呢?在《C++语言的设计和演变》中,对于在C++中实现这种通用约束的困难进行了分析。从那以来,出现了很多方法,来让约束类变得更加容易编写,同时仍然能触发良好的错误信息。例如,我信任我在Can_copy中使用的函数指针的方式,它源自Alex Stepanov和Jeremy Siek。我并不认为Can_copy()已经可以标准化了――它需要更多的使用。同样,在C++社区中,各种不同的约束方式被使用;到底是哪一种约束模板在广泛的使用中被证明是最有效的,还没有达成一致的意见。

但是,这种方式非常普遍,比语言提供的专门用于约束检查的机制更加普遍。无论如何,当我们编写一个模板时,我们拥有了C++提供的最丰富的表达力量。看看这个:

template<class T, class B> struct Derived_from {

static void constraints(T* p) { B* pb = p; }

Derived_from() { void(*p)(T*) = constraints; }

};

template<class T1, class T2> struct Can_copy {

static void constraints(T1 a, T2 b) { T2 c = a; b = a; }

Can_copy() { void(*p)(T1,T2) = constraints; }

};

template<class T1, class T2 = T1> struct Can_compare {

static void constraints(T1 a, T2 b) { a==b; a!=b; a<b; }

Can_compare() { void(*p)(T1,T2) = constraints; }

};

template<class T1, class T2, class T3 = T1> struct Can_multiply {

static void constraints(T1 a, T2 b, T3 c) { c = a*b; }

Can_multiply() { void(*p)(T1,T2,T3) = constraints; }

};

struct B { };

struct D : B { };

struct DD : D { };

struct X { };

int main()

{

Derived_from<D,B>();

Derived_from<DD,B>();

Derived_from<X,B>();

Derived_from<int,B>();

Derived_from<X,int>();

Can_compare<int,float>();

Can_compare<X,B>();

Can_multiply<int,float>();

Can_multiply<int,float,double>();

Can_multiply<B,X>();

Can_copy<D*,B*>();

Can_copy<D,B*>();

Can_copy<int,B*>();

}

// 典型的"元素必须继承自Mybase*"约束:

template<class T> class Container : Derived_from<T,Mybase> {

// ...

};

事实上,Derived_from并不检查来源(derivation),而仅仅检查转换(conversion),不过这往往是一个更好的约束。为约束想一个好名字是很难的。

既然已经有了优秀的qsort()函数,为什么还需要一个sort()?

对于初学者来说,

qsort(array,asize,sizeof(elem),elem_compare);

看上去太古怪了,而且比这个更难理解:

sort(vec.begin(),vec.end());

对于专家来说,在元素与比较方式(comparison criteria)都相同的情况下,sort()比qsort()更快,这是很重要的。而且,qsort()是通用的,所以它可以用于不同容器类型、元素类型、比较方式的任意有意义的组合。举例来说:

struct Record {

string name;

// ...

};

struct name_compare { // 使用"name"作为键比较Record

bool operator()(const Record& a, const Record& b) const

{ return a.name<b.name; }

};

void f(vector<Record>& vs)

{

sort(vs.begin(), vs.end(), name_compare());

// ...

}

而且,很多人欣赏sort()是因为它是类型安全的,使用它不需要进行造型(cast),没有人必须去为基本类型写一个compare()函数。

更多的细节,参见我的文章《将标准C++作为一种新的语言来学习》(Learning C++ as a New language),可以从我的文章列表中找到。

sort()胜过qsort()的主要原因是,比较操作在内联(inlines)上做得更好。

什么是函数对象(function object)?

顾名思义,就是在某种方式上表现得象一个函数的对象。典型地,它是指一个类的实例,这个类定义了应用操作符operator()。

函数对象是比函数更加通用的概念,因为函数对象可以定义跨越多次调用的可持久的部分(类似静态局部变量),同时又能够从对象的外面进行初始化和检查(和静态局部变量不同)。例如:

class Sum {

int val;

public:

Sum(int i) :val(i) { }

operator int() const { return val; } // 取得值

int operator()(int i) { return val+=i; } // 应用

};

void f(vector v)

{

Sum s = 0; // initial value 0

s = for_each(v.begin(), v.end(), s); // 求所有元素的和

cout << "the sum is " << s << "\n";

//或者甚至:

cout << "the sum is " << for_each(v.begin(), v.end(), Sum(0)) << "\n";

}

注意一个拥有应用操作符的函数对象可以被完美地内联化(inline),因为它没有涉及到任何指针,后者可能导致拒绝优化。与之形成对比的是,现有的优化器几乎不能(或者完全不能?)将一个通过函数指针的调用内联化。

在标准库中,函数对象被广泛地使用以获得弹性。

我应该如何对付内存泄漏?

写出那些不会导致任何内存泄漏的代码。很明显,当你的代码中到处充满了new 操作、delete操作和指针运算的话,你将会在某个地方搞晕了头,导致内存泄漏,指针引用错误,以及诸如此类的问题。这和你如何小心地对待内存分配工作其实完全没有关系:代码的复杂性最终总是会超过你能够付出的时间和努力。于是随后产生了一些成功的技巧,它们依赖于将内存分配(allocations)与重新分配(deallocation)工作隐藏在易于管理的类型之后。标准容器(standard containers)是一个优秀的例子。它们不是通过你而是自己为元素管理内存,从而避免了产生糟糕的结果。想象一下,没有string和vector的帮助,写出这个:

#include<vector>

#include<string>

#include<iostream>

#include<algorithm>

using namespace std;

int main() // small program messing around with strings

{

cout << "enter some whitespace-separated words:\n";

vector<string> v;

string s;

while (cin>>s) v.push_back(s);

sort(v.begin(),v.end());

string cat;

typedef vector<string>::const_iterator Iter;

for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";

cout << cat << '\n';

}

你有多少机会在第一次就得到正确的结果?你又怎么知道你没有导致内存泄漏呢?

注意,没有出现显式的内存管理,宏,造型,溢出检查,显式的长度限制,以及指针。通过使用函数对象和标准算法(standard algorithm),我可以避免使用指针――例如使用迭代子(iterator),不过对于一个这么小的程序来说有点小题大作了。

这些技巧并不完美,要系统化地使用它们也并不总是那么容易。但是,应用它们产生了惊人的差异,而且通过减少显式的内存分配与重新分配的次数,你甚至可以使余下的例子更加容易被跟踪。早在1981年,我就指出,通过将我必须显式地跟踪的对象的数量从几万个减少到几打,为了使程序正确运行而付出的努力从可怕的苦工,变成了应付一些可管理的对象,甚至更加简单了。

如果你的程序还没有包含将显式内存管理减少到最小限度的库,那么要让你程序完成和正确运行的话,最快的途径也许就是先建立一个这样的库。

模板和标准库实现了容器、资源句柄以及诸如此类的东西,更早的使用甚至在多年以前。异常的使用使之更加完善。

如果你实在不能将内存分配/重新分配的操作隐藏到你需要的对象中时,你可以使用资源句柄(resource handle),以将内存泄漏的可能性降至最低。这里有个例子:我需要通过一个函数,在空闲内存中建立一个对象并返回它。这时候可能忘记释放这个对象。毕竟,我们不能说,仅仅关注当这个指针要被释放的时候,谁将负责去做。使用资源句柄,这里用了标准库中的auto_ptr,使需要为之负责的地方变得明确了。

#include<memory>

#include<iostream>

using namespace std;

struct S {

S() { cout << "make an S\n"; }

~S() { cout << "destroy an S\n"; }

S(const S&) { cout << "copy initialize an S\n"; }

S& operator=(const S&) { cout << "copy assign an S\n"; }

};

S* f()

{

return new S; // 谁该负责释放这个S?

};

auto_ptr<S> g()

{

return auto_ptr<S>(new S); // 显式传递负责释放这个S

}

int main()

{

cout << "start main\n";

S* p = f();

cout << "after f() before g()\n";

// S* q = g(); // 将被编译器捕捉

auto_ptr<S> q = g();

cout << "exit main\n";

// *p产生了内存泄漏

// *q被自动释放

}

在更一般的意义上考虑资源,而不仅仅是内存。

如果在你的环境中不能系统地应用这些技巧(例如,你必须使用别的地方的代码,或者你的程序的另一部分简直是原始人类(译注:原文是Neanderthals,尼安德特人,旧石器时代广泛分布在欧洲的猿人)写的,如此等等),那么注意使用一个内存泄漏检测器作为开发过程的一部分,或者插入一个垃圾收集器(garbage collector)。

我为什么在捕获一个异常之后就不能继续?

换句话说,C++为什么不提供一种简单的方式,让程序能够回到异常抛出点之后,并继续执行?

主要的原因是,如果从异常处理之后继续,那么无法预知掷出点之后的代码如何对待异常处理,是否仅仅继续执行,就象什么也没有发生一样。异常处理者无法知道,在继续之前,有关的上下文环境(context)是否是"正确"的。要让这样的代码正确执行,抛出异常的编写者与捕获异常的编写者必须对彼此的代码与上下文环境都非常熟悉才行。这样会产生非常复杂的依赖性,因此无论在什么情况下,都会导致一系列严重的维护问题。

当我设计C++的异常处理机制时,我曾经认真地考虑过允许这种继续的可能性,而且在标准化的过程中,这个问题被非常详细地讨论过。请参见《C++语言的设计和演变》中的异常处理章节。

在一次新闻组的讨论中,我曾经以一种稍微不同的方式回答过这个问题。

为什么C++中没有相当于realloc()的函数?

如果你需要,你当然可以使用realloc()。但是,realloc()仅仅保证能工作于这样的数组之上:它们被malloc()(或者类似的函数)分配,包含一些没有用户定义的复制构造函数(copy constructors)的对象。而且,要记住,与通常的期望相反,realloc()有时也必须复制它的参数数组。

在C++中,处理内存重新分配的更好的方法是,使用标准库中的容器,例如vector,并让它自我增长。

如何使用异常?

参见《C++程序设计语言》第4章,第8.3节,以及附录E。这个附录针对的是如何在要求苛刻的程序中写出异常安全的代码的技巧,而不是针对初学者的。一个关键的技术是"资源获得即初始化"(resource acquisiton is initialization),它使用一些有析构函数的类,来实现强制的资源管理。

怎样从输入中读取一个字符串?

你可以用这种方式读取一个单独的以空格结束的词:

#include<iostream>

#include<string>

using namespace std;

int main()

{

cout << "Please enter a word:\n";

string s;

cin>>s;

cout << "You entered " << s << '\n';

}

注意,这里没有显式的内存管理,也没有可能导致溢出的固定大小的缓冲区。

如果你确实想得到一行而不是一个单独的词,可以这样做:

#include<iostream>

#include<string>

using namespace std;

int main()

{

cout << "Please enter a line:\n";

string s;

getline(cin,s);

cout << "You entered " << s << '\n';

}

在《C++程序设计语言》(可在线获得)的第3章,可以找到一个对诸如字符串与流这样的标准库工具的简介。对于使用C与C++进行简单输入输出的详细比较,参见我的文章《将标准C++作为一种新的语言来学习》(Learning Standard C++ as a New Language),你可以在本人著作列表(my publications list)中下载到它。

为什么C++不提供"finally"的构造?

因为C++提供了另外一种方法,它几乎总是更好的:"资源获得即初始化"(resource acquisiton is initialization)技术。基本的思路是,通过一个局部对象来表现资源,于是局部对象的析构函数将会释放资源。这样,程序员就不会忘记释放资源了。举例来说:

class File_handle {

FILE* p;

public:

File_handle(const char* n, const char* a)

{ p = fopen(n,a); if (p==0) throw Open_error(errno); }

File_handle(FILE* pp)

{ p = pp; if (p==0) throw Open_error(errno); }

~File_handle() { fclose(p); }

operator FILE*() { return p; }

// ...

};

void f(const char* fn)

{

File_handle f(fn,"rw"); //打开fn进行读写

// 通过f使用文件

}

在一个系统中,需要为每一个资源都使用一个"资源句柄"类。无论如何,我们不需要为每一个资源获得都写出"finally"语句。在实时系统中,资源获得要远远多于资源的种类,因此和使用"finally"构造相比,"资源获得即初始化"技术会产生少得多的代码。

什么是自动指针(auto_ptr),为什么没有自动数组(auto_array)?

auto_ptr是一个非常简单的句柄类的例子,在<memory>中定义,通过"资源获得即初始化"技术支持异常安全。auto_ptr保存着一个指针,能够象指针一样被使用,并在生存期结束时释放指向的对象。举例:

#include<memory>

using namespace std;

struct X {

int m;

// ..

};

void f()

{

auto_ptr<X> p(new X);

X* q = new X;

p->m++; // 象一个指针一样使用p

q->m++;

// ...

delete q;

}

如果在...部分抛出了一个异常,p持有的对象将被auto_ptr的析构函数正确地释放,而q指向的X对象则产生了内存泄漏。更多的细节,参见《C++程序设计语言》14.4.2节。

auto_ptr是一个非常简单的类。特别地,它不是一个引用计数(reference counted)的指针。如果你将一个auto_ptr赋值给另一个,那么被赋值的auto_ptr将持有指针,而原来的auto_ptr将持有0。举例:

#include<memory>

#include<iostream>

using namespace std;

struct X {

int m;

// ..

};

int main()

{

auto_ptr<X> p(new X);

auto_ptr<X> q(p);

cout << "p " << p.get() << " q " << q.get() << "\n";

}

将会打印出一个指向0的指针和一个指向非0的指针。例如:

p 0x0 q 0x378d0

auto_ptr::get()返回那个辅助的指针。

这种"转移"语义不同于通常的"复制"语义,这是令人惊讶的。特别地,永远不要使用auto_ptr作为一个标准容器的成员。标准容器需要通常的"复制"语义。例如:

std::vector<auto_ptr<X> >v; // 错误

auto_ptr只持有指向一个单独元素的指针,而不是指向一个数组的指针:

void f(int n)

{

auto_ptr<X> p(new X[n]); //错误

// ...

}

这是错误的,因为析构函数会调用delete而不是delete[]来释放指针,这样就不会调用余下的n-1个X的析构函数。

那么我们需要一个auto_array来持有数组吗?不。没有auto_array。原因是根本没有这种需要。更好的解决方案是使用vector:

void f(int n)

{

vector<X> v(n);

// ...

}

当...部分发生异常时,v的析构函数会被正确地调用。

可以混合使用C风格与C++风格的内存分派与重新分配吗?

在这种意义上是可以的:你可以在同一个程序中使用malloc()和new。

在这种意义上是不行的:你不能使用malloc()来建立一个对象,又通过delete来释放它。你也不能用new建立一个新的对象,然后通过free()来释放它,或者通过realloc()在数组中再建立一个新的。

C++中的new和delete操作可以保证正确的构造和析构:构造函数和析构函数在需要它们的时候被调用。C风格的函数alloc(), calloc(), free(), 和realloc()却不能保证这一点。此外,用new和delete来获得和释放的原始内存,并不一定能保证与malloc()和free()兼容。如果这种混合的风格在你的系统中能够运用,只能说是你走运――暂时的。

如果你觉得需要使用realloc()――或者要做更多――考虑使用标准库中的vector。例如:

// 从输入中将词读取到一个字符串vector中

vector<string> words;

string s;

while (cin>>s && s!=".") words.push_back(s);

vector会视需要自动增长。

更多的例子与讨论,参见我的文章《将标准C++作为一种新的语言来学习》(Learning Standard C++ as a New Language),你可以在本人著作列表(my publications list)中下载到它。

我为什么必须使用一个造型来转换*void?

在C语言中,你可以隐式地将*void转换为*T。这是不安全的。考虑一下:

#include<stdio.h>

int main()

{

char i = 0;

char j = 0;

char* p = &i;

void* q = p;

int* pp = q; /* 不安全的,在C中可以,C++不行 */

printf("%d %d\n",i,j);

*pp = -1; /* 覆盖了从i开始的内存 */

printf("%d %d\n",i,j);

}

使用一个并不指向T类型的T*将是一场灾难。因此,在C++中,如果从一个void*得到一个T*,你必须进行显式转换。举例来说,要得到上列程序的这个令人别扭的效果,你可以这样写:

int* pp = (int*)q;

或者使用一个新的类型造型,以使这种没有检查的类型转换操作变得更加清晰:

int* pp = static_cast<int*>(q);

造型被最好地避免了。

在C语言中,这种不安全的转换最常见的应用之一,是将malloc()的结果赋予一个合适的指针。例如:

int* p = malloc(sizeof(int));

在C++中,使用类型安全的new操作符:

int* p = new int;

附带地,new操作符还提供了胜过malloc()的新特性:

new不会偶然分配错误的内存数量;

new会隐式地检查内存耗尽情况,而且

new提供了初始化。

举例:

typedef std::complex<double> cmplx;

/* C风格: */

cmplx* p = (cmplx*)malloc(sizeof(int)); /* 错误:类型不正确 */

/* 忘记测试p==0 */

if (*p == 7) { /* ... */ } /* 糟糕,忘记了初始化*p */

// C++风格:

cmplx* q = new cmplx(1,2); // 如果内存耗尽,将抛出一个bad_alloc异常

if (*q == 7) { /* ... */ }

我如何定义一个类内部(in-class)的常量?

如果你需要一个通过常量表达式来定义的常量,例如数组的范围,你有两种选择:

class X {

static const int c1 = 7;

enum { c2 = 19 };

char v1[c1];

char v2[c2];

// ...

};

乍看起来,c1的声明要更加清晰,但是要注意的是,使用这种类内部的初始化语法的时候,常量必须是被一个常量表达式初始化的整型或枚举类型,而且必须是static和const形式。这是很严重的限制:

class Y {

const int c3 = 7; // 错误:不是static

static int c4 = 7; // 错误:不是const

static const float c5 = 7; // 错误:不是整型

};

我倾向使用枚举的方式,因为它更加方便,而且不会诱使我去使用不规范的类内初始化语法。

那么,为什么会存在这种不方便的限制呢?一般来说,类在一个头文件中被声明,而头文件被包含到许多互相调用的单元去。但是,为了避免复杂的编译器规则,C++要求每一个对象只有一个单独的定义。如果C++允许在类内部定义一个和对象一样占据内存的实体的话,这种规则就被破坏了。对于C++在这个设计上的权衡,请参见《C++语言的设计和演变》。

如果你不需要用常量表达式来初始化它,那么可以获得更大的弹性:

class Z {

static char* p; // 在定义中初始化

const int i; // 在构造函数中初始化

public:

Z(int ii) :i(ii) { }

};

char* Z::p = "hello, there";

你可以获取一个static成员的地址,当且仅当它有一个类外部的定义的时候:

class AE {

// ...

public:

static const int c6 = 7;

static const int c7 = 31;

};

const int AE::c7; // 定义

int f()

{

const int* p1 = &AE::c6; // 错误:c6没有左值

const int* p2 = &AE::c7; // ok

// ...

}

为什么delete不会将操作数置0?

考虑一下:

delete p;

// ...

delete p;

如果在...部分没有涉及到p的话,那么第二个"delete p;"将是一个严重的错误,因为C++的实现(译注:原文为a C++ implementation,当指VC++这样的实现了C++标准的具体工具)不能有效地防止这一点(除非通过非正式的预防手段)。既然delete 0从定义上来说是无害的,那么一个简单的解决方案就是,不管在什么地方执行了"delete p;",随后都执行"p=0;"。但是,C++并不能保证这一点。

一个原因是,delete的操作数并不需要一个左值(lvalue)。考虑一下:

delete p+1;

delete f(x);

在这里,被执行的delete并没有拥有一个可以被赋予0的指针。这些例子可能很少见,但它们的确指出了,为什么保证"任何指向被删除对象的指针都为0"是不可能的。绕过这条"规则"的一个简单的方法是,有两个指针指向同一个对象:

T* p = new T;

T* q = p;

delete p;

delete q; // 糟糕!

C++显式地允许delete操作将操作数左值置0,而且我曾经希望C++的实现能够做到这一点,但这种思想看来并没有在C++的实现中变得流行。

如果你认为指针置0很重要,考虑使用一个销毁的函数:

template<class T> inline void destroy(T*& p) { delete p; p = 0; }

考虑一下,这也是为什么需要依靠标准库的容器、句柄等等,来将对new和delete的显式调用降到最低限度的另一个原因。

注意,通过引用来传递指针(以允许指针被置0)有一个额外的好处,能防止destroy()在右值上(rvalue)被调用:

int* f();

int* p;

// ...

destroy(f()); // 错误:应该使用一个非常量(non-const)的引用传递右值

destroy(p+1); // 错误:应该使用一个非常量(non-const)的引用传递右值

我能够写"void main()"吗?

这种定义:

void main() { /* ... */ }

在C++中从未被允许,在C语言中也是一样。参见ISO C++标准3.6.1[2]或者ISO C标准5.1.2.2.1。规范的实现接受这种方式:

int main() { /* ... */ }

int main(int argc, char* argv[]) { /* ... */ }

一个规范的实现可能提供许多版本的main(),但它们都必须返回int类型。main()返回的int值,是程序返回一个值给调用它的系统的方式。在那些不具备这种方式的系统中,返回值被忽略了,但这并不使"void main()"在C++或C中成为合法的。即使你的编译器接受了"void main()",也要避免使用它,否则你将冒着被C和C++程序员视为无知的风险。

在C++中,main()并不需要包含显式的return语句。在这种情况下,返回值是0,表示执行成功。例如:

#include<iostream>

int main()

{

std::cout << "This program returns the integer value 0\n";

}

注意,无论是ISO C++还是C99,都不允许在声明中漏掉类型。那就是说,与C89和ARM C++形成对照,当声明中缺少类型时,并不会保证是"int"。于是:

#include<iostream>

main() { /* ... */ }

是错误的,因为缺少main()的返回类型。

为什么我不能重载点符号,::,sizeof,等等?

大多数的运算符能够被程序员重载。例外的是:

. (点符号) :: ?: sizeof

并没有什么根本的原因要禁止重载?:。仅仅是因为,我没有发现有哪种特殊的情况需要重载一个三元运算符。注意一个重载了 表达式1?表达式2:表达式3 的函数,不能够保证表达式2:表达式3中只有一个会被执行。

Sizeof不能够被重载是因为内建的操作(built-in operations),诸如对一个指向数组的指针进行增量操作,必须依靠它。考虑一下:

X a[10];

X* p = &a[3];

X* q = &a[3];

p++; // p指向a[4]

// 那么p的整型值必须比q的整型值大出一个sizeof(X)

所以,sizeof(X)不能由程序员来赋予一个不同的新意义,以免违反基本的语法。

在N::m中,无论N还是m都不是值的表达式;N和m是编译器知道的名字,::执行一个(编译期的)范围解析,而不是表达式求值。你可以想象一下,允许重载x::y的话,x可能是一个对象而不是一个名字空间(namespace)或者一个类,这样就会导致――与原来的表现相反――产生新的语法(允许 表达式1::表达式2)。很明显,这种复杂性不会带来任何好处。

理论上来说,.(点运算符)可以通过使用和->一样的技术来进行重载。但是,这样做会导致一个问题,那就是无法确定操作的是重载了.的对象呢,还是通过.引用的一个对象。例如:

class Y {

public:

void f();

// ...

};

class X { // 假设你能重载.

Y* p;

Y& operator.() { return *p; }

void f();

// ...

};

void g(X& x)

{

x.f(); // X::f还是Y::f还是错误?

}

这个问题能够用几种不同的方法解决。在标准化的时候,哪种方法最好还没有定论。更多的细节,请参见《C++语言的设计和演变》。

怎样将一个整型值转换为一个字符串?

最简单的方法是使用一个字符串流(stringstream):

#include<iostream>

#include<string>

#include<sstream>

using namespace std;

string itos(int i) // 将int转换成string

{

stringstream s;

s << i;

return s.str();

}

int main()

{

int i = 127;

string ss = itos(i);

const char* p = ss.c_str();

cout << ss << " " << p << "\n";

}

自然地,这种技术能够将任何使用<<输出的类型转换为字符串。对于字符串流的更多说明,参见《C++程序设计语言》21.5.3节。

"int* p"正确还是"int *p"正确?

二者都是正确的,因为二者在C和C++中都是有效的,而且意义完全一样。就语言的定义与相关的编译器来说,我们还可以说"int*p"或者"int * p"。

在"int* p"和"int *p"之间的选择与正确或错误无关,而只关乎风格与侧重点。C侧重表达式;对声明往往比可能带来的问题考虑得更多。另一方面,C++则非常重视类型。

一个"典型的C程序员"写成"int *p",并且解释说"*p表示一个什么样的int"以强调语法,而且可能指出C(与C++)的语法来证明这种风格的正确性。是的,在语法上*被绑定到名字p上。

一个"典型的C++程序员"写成"int* p",并且解释说"p是一个指向int的指针类型"以强调类型。是的,p是一个指向int的指针类型。我明确地倾向于这种侧重方向,而且认为对于学好更多的高级C++这是很重要的。

严重的混乱(仅仅)发生在当人们试图在一条声明中声明几个指针的时候:

int* p, p1; // 也许是错的:p1不是一个int*

把*放到名字这一边,看来也不能有效地减少这种错误:

int *p, p1; // 也许是错的?

为每一个名字写一条声明最大程度地解决了问题――特别是当我们初始化变量的时候。人们几乎不会这样写:

int* p = &i;

int p1 = p; // 错误:int用一个int*初始化了

如果他们真的这么干了,编译器也会指出。

每当事情可以有两种方法完成,有人就会迷惑。每当事情仅仅是一个风格的问题,争论就会没完没了。为每一个指针写一条声明,而且永远都要初始化变量,这样,混乱之源就消失了。更多的关于C的声明语法的讨论,参见《C++语言的设计和演变》。

对于我的代码,哪一种布局风格(layout style)是最好的?

这种风格问题属于个人的爱好。人们往往对布局风格的问题持有强烈的意见,不过,也许一贯性比某种特定的风格更加重要。象大多数人一样,我花了很长的时间,来为我的偏好作出一个固定的结论。

我个人使用通常称为"K&R"的风格。当使用C语言没有的构造函数时,需要增加新的习惯,这样就变成了一种有时被称为"Stroustrup"的风格。例如:

class C : public B {

public:

// ...

};

void f(int* p, int max)

{

if (p) {

// ...

}

for (int i = 0; i<max; ++i) {

// ...

}

}

比大多数布局风格更好,这种风格保留了垂直的空格,我喜欢尽可能地在合理的情况下对齐屏幕。对函数开头的大括弧的放置,有助于我第一眼就分别出类的定义和函数的定义。

缩进是非常重要的。

设计问题,诸如作为主要接口的抽象基类的使用,使用模板以表现有弹性的类型安全的抽象,以及正确地使用异常以表现错误,比布局风格的选择要重要得多。

我应该将"const"放在类型之前还是之后?

我把它放在前面,但那仅仅是个人爱好问题。"const T"和"T const"总是都被允许的,而且是等效的。例如:

const int a = 1; // ok

int const b = 2; // also ok

我猜想第一种版本可能会让少数(更加固守语法规范)的程序员感到迷惑。

为什么?当我发明"const"(最初的名称叫做"readonly",并且有一个对应的"writeonly")的时候,我就允许它出现在类型之前或之后,因为这样做不会带来任何不明确。标准之前的C和C++规定了很少的(如果有的话)特定的顺序规范。

我不记得当时有过任何有关顺序问题的深入思考或讨论。那时,早期的一些使用者――特别是我――仅仅喜欢这种样子:

const int c = 10;

看起来比这种更好:

int const c = 10;

也许我也受了这种影响:在我最早的一些使用"readonly"的例子中

readonly int c = 10;

比这个更具有可读性:

int readonly c = 10;

我创造的那些最早的使用"const"的(C或C++)代码,看来已经在全球范围内取代了"readonly"。

我记得这个语法的选择在几个人――例如Dennis Ritchie――当中讨论过,但我不记得当时我倾向于哪种语言了。

注意在固定指针(const pointer)中,"const"永远出现在"*"之后。例如:

int *const p1 = q; // 指向int变量的固定指针

int const* p2 = q; //指向int常量的指针

const int* p3 = q; //指向int常量的指针

使用宏有什么问题?

宏不遵循C++中关于范围和类型的规则。这经常导致一些微妙的或不那么微妙的问题。因此,C++提供更适合其他的C++(译注:原文为the rest of C++,当指C++除了兼容C以外的部分)的替代品,例如内联函数、模板与名字空间。

考虑一下:

#include "someheader.h"

struct S {

int alpha;

int beta;

};

如果某人(不明智地)地写了一个叫"alpha"或"beta"的宏,那么它将不会被编译,或者被错误地编译,产生不可预知的结果。例如,"someheader.h"可能包含:

#define alpha 'a'

#define beta b[2]

将宏(而且仅仅是宏)全部大写的习惯,会有所帮助,但是对于宏并没有语言层次上的保护机制。例如,虽然成员的名字包含在结构体的内部,但这无济于事:在编译器能够正确地辨别这一点之前,宏已经将程序作为一个字符流进行了处理。顺便说一句,这是C和C++程序开发环境和工具能够被简化的一个主要原因:人与编译器看到的是不同的东西。

不幸的是,你不能假设别的程序员总是能够避免这种你认为"相当白痴"的事情。例如,最近有人报告我,他们遇到了一个包含goto的宏。我也见过这种情况,而且听到过一些――在很脆弱的时候――看起来确实有理的意见。例如:

#define prefix get_ready(); int ret__

#define Return(i) ret__=i; do_something(); goto exit

#define suffix exit: cleanup(); return ret__

void f()

{

prefix;

// ...

Return(10);

// ...

Return(x++);

//...

suffix;

}

作为一个维护的程序员,就会产生这种印象;将宏"隐藏"到一个头文件中――这并不罕见――使得这种"魔法"更难以被辨别。

一个常见的微妙问题是,一个函数风格的宏并不遵守函数参数传递的规则。例如:

#define square(x) (x*x)

void f(double d, int i)

{

square(d); // 好

square(i++); // 糟糕:这表示 (i++*i++)

square(d+1); //糟糕:这表示(d+1*d+1); 也就是 (d+d+1)

// ...

}

"d+1"的问题,可以通过在"调用"时或宏定义时添加一对圆括号来解决:

#define square(x) ((x)*(x)) /*这样更好 */

但是, i++被执行了两次(可能并不是有意要这么做)的问题仍然存在。

是的,我确实知道有些特殊的宏并不会导致C/C++预处理宏这样的问题。但是,我无心去发展C++中的宏。作为替代,我推荐使用C++语言中合适的工具,例如内联函数,模板,构造函数(用来初始化),析构函数(用来清除),异常(用来退出上下文环境),等等。

 

 

 

 

 

原文的地址为:http://www.research.att.com/~bs/bs_faq.html

 
 

Monday, April 23, 2007

ECMAScript Reference

ECMAScript Reference


All of the ECMAScript operators, statements, and core objects are supported by VoiceGenie. In v7.0+, VoiceGenie uses the SpiderMonkey 1.5 RC 6A engine to perform JavaScript/ECMAScript processing; v6.1+ use SpiderMonkey 1.5 RC 5A; previous versions use SpiderMonkey 1.5 RC 4A.

The following is a quick reference of many commonly-used features. It is not intended to be a comprehensive JavaScript/ECMAScript language specification.

Here are some other sites that have ECMAScript/JavaScript reference documentation:
http://www.mozilla.org/js/
http://www.mozilla.org/js/language/
http://www.mozilla.org/js/scripting/
http://www.devguru.com/Technologies/ecmascript/quickref/javascript_index.html


Functions

Function

Description

_VGGetInfo

(VoiceGenie-defined)

A global function that allows applications to access information about the VoiceGenie platform and the running page, as well as any other custom information that is defined.

The _VGGetInfo() function takes in a string argument and returns a string value, or an empty string if the specified parameter name is not recognized. The 3 currently defined parameters can be used as follows:

  • _VGGetInfo('host_ip') - returns the platform IP address
  • _VGGetInfo('host_name') - returns the platform hostname
  • _VGGetInfo('running_uri') - returns the URI of the current VoiceXML page

Additional parameters can be defined for access to custom information with this function. This is done through the 'getinfo_pairs' parameter in the VoiceXML Interpreter Configuration (see the System Reference Guide for details).

Supported in v7.0+.

Operators

Category

Operator

Description

Arithmetic

+

Adds 2 numbers.

++

Increments a number.

-

As a unary operator, negates the value of its argument. As a binary operator, subtracts 2 numbers.

--

Decrements a number.

*

Multiplies 2 numbers.

/

Divides 2 numbers.

%

Computes the integer remainder of dividing 2 numbers.

String

+

Concatenates 2 strings.

+=

Concatenates 2 strings and assigns the result to the first operand.

Logical Operators

&&

(Logical AND) Returns true if both logical operands are true. Otherwise, returns false.

||

(Logical OR) Returns true if either logical expression is true. If both are false, returns false.

!

(Logical negation) If its single operand is true, returns false; otherwise, returns true.

Bitwise Operators

&

(Bitwise AND) Returns a one in each bit position if bits of both operands are ones.

^

(Bitwise XOR) Returns a one in a bit position if bits of one but not both operands are one.

|

(Bitwise OR) Returns a one in a bit if bits of either operand is one.

~

(Bitwise NOT) Flips the bits of its operand.

<<

(Left shift) Shifts its first operand in binary representation the number of bits to the left specified in the second operand, shifting in zeros from the right.

>>

(Sign-propagating right shift) Shifts the first operand in binary representation the number of bits to the right specified in the second operand, discarding bits shifted off.

>>>

(Zero-fill right shift) Shifts the first operand in binary representation the number of bits to the right specified in the second operand, discarding bits shifted off, and shifting in zeros from the left.

Assignment

=

Assigns the value of the second operand to the first operand.

+=

Adds 2 numbers and assigns the result to the first.

-=

Subtracts 2 numbers and assigns the result to the first.

*=

Multiplies 2 numbers and assigns the result to the first.

/=

Divides 2 numbers and assigns the result to the first.

%=

Computes the modulus of 2 numbers and assigns the result to the first.

&=

Performs a bitwise AND and assigns the result to the first operand.

^=

Performs a bitwise XOR and assigns the result to the first operand.

|=

Performs a bitwise OR and assigns the result to the first operand.

<<=

Performs a left shift and assigns the result to the first operand.

>>=

Performs a sign-propagating right shift and assigns the result to the first operand.

>>>=

Performs a zero-fill right shift and assigns the result to the first operand.

Comparison

==

Returns true if the operands are equal.

!=

Returns true if the operands are not equal.

>

Returns true if left operand is greater than right operand.

>=

Returns true if left operand is greater than or equal to right operand.

<

Returns true if left operand is less than right operand.

<=

Returns true if left operand is less than or equal to right operand.

Special

?:

Performs simple "if ? then : else"

,

Evaluates two expressions and returns the result of the second expression.

delete

Deletes an object property or an element at a specified index in an array.

new

Creates an instance of an object.

this

Refers to the current object.

typeof

Returns a string indicating the type of the unevaluated operand.

void

Specifies an expression to be evaluated without returning a value.

Statements

Statement

Description

break

Terminates the current while or for loop and transfers program control to the statement following the terminated loop.

continue

Terminates execution of the block of statements in a while or for loop, and continues execution of the loop with the next iteration.

delete

Deletes an object's property or an element of an array.

do

...

while(condition)

Executes its statements until the test condition evaluates to false. Statement is executed at least once.

for (init; condition; increment)

A loop that consists of three optional expressions, enclosed in parentheses and separated by semicolons, followed by a block of statements executed in the loop.

for (var in object)

Iterates a specified variable over all the properties of an object. For each distinct property, JavaScript executes the specified statements.

function

Declares a JavaScript function name with the specified parameters. Acceptable parameters include strings, numbers, and objects.

if (condition) ... else

Executes a set of statements if a specified condition is true. If the condition is false, another set of statements can be executed.

labeled

Provides an identifier that can be used with break or continue to indicate where the program should continue execution.

return

Statement that specifies the value to be returned by a function.

switch (expression)

case label:

Evaluates an expression and attempt to match the expression's value to a case label.

var

Declares a variable, optionally initializing it to a value.

while (condition) ...

Creates a loop that evaluates an expression, and if it is true, executes a block of statements.

with (object) ...

Establishes the default object for a set of statements.

//

Defines comment until end of the line.

/* ... */

Defines comment within the operators.

Core Objects

Array Object

Property

Description

length

Size of the array.

index

Position of matched substring (from RegExp object)

input

Original string for matching (from RegExp object)

Method

Description

concat(array1)

Joins two arrays into one array.

join(separator)

Joins array element into a string, separated by separator (Defaults to ",")

pop

Removes last element from an array and returns that element.

push(e1, e2 ...)

Adds one or more elements to the end of the array and returns the last element.

reverse

Reverses the elements in the array.

shift

Removes the first element from an array and returns that element.

slice(begin, end)

Extracts elements from index begin to end and returns a new array.

sort

Sorts the elements of an array

splice

Change content of array by adding and removing elements.

toString

Returns string representation of array

unshift(e1, e2 ...)

Adds one or more elements to the beginning of the array and returns new array length.

Boolean Object

Method

Description

toString

Returns string representation of Boolean.

Date Object

Method

Description

getDate

Returns the day of the month.

getDay

Returns the day of the week.

getHours

Returns the hour.

getMinutes

Returns the minutes.

getMonth

Returns the month.

getSeconds

Returns the seconds.

getTime

Returns the numeric value corresponding to the time.

getTimezoneOffset

Returns the time-zone offset in minutes for the current locale.

getYear

Returns the year.

parse

Returns the number of milliseconds in a date string since January 1, 1970, 00:00:00, local time.

setDate

Sets the day of the month.

setHours

Sets the hours.

setMinutes

Sets the minutes.

setMonth

Sets the month.

setSeconds

Sets the seconds.

setTime

Sets the value of a Date object.

setYear

Sets the year.

toGMTString

Converts a date to a string, using the Internet GMT conventions.

toLocaleString

Converts a date to a string, using the current locale's conventions.

UTC

Returns the number of milliseconds in a Date object since January 1, 1970, 00:00:00, Universal Coordinated Time (GMT).

Math Object

Property

Description

E

Euler's constant, approximately 2.718.

LN10

Natural logarithm of 10, approximately 2.302.

LN2

Natural logarithm of 2, approximately 0.693.

LOG10E

Base 10 logarithm of E, approximately 0.434.

PI

Pi, approximately 3.14159.

SQRT1_2

Square root of 1/2, approximately 0.707.

SQRT2

Square root of 2, approximately 1.414.

Method

Description

abs

Returns the absolute value of a number.

acos

Returns the arccosine (in radians) of a number.

asin

Returns the arcsine (in radians) of a number.

atan

Returns the arctangent (in radians) of a number.

atan2

Returns the arctangent of the quotient of its arguments.

ceil

Returns the smallest integer greater than or equal to a number.

cos

Returns the cosine of a number.

exp

Returns Enumber, where number is the argument, and E is Euler's constant, the base of the natural logarithms.

floor

Returns the largest integer less than or equal to a number.

log

Returns the natural logarithm (base E) of a number.

max

Returns the greater of two numbers.

min

Returns the lesser of two numbers.

pow

Returns base to the exponent power, that is, baseexponent.

random

Returns a pseudo-random number between 0 and 1.

round

Returns the value of a number rounded to the nearest integer.

sin

Returns the sine of a number.

sqrt

Returns the square root of a number.

tan

Returns the tangent of a number.

Number Object

Property

Description

MAX_VALUE

The largest representable number.

MIN_VALUE

The smaller representable number.

NaN

Not a number value.

NEGATIVE_INFINITY

Negative infinite value for overflow.

POSITIVE_INFINITY

Infinite value for overflow.

Method

Description

toString

Returns string representation of a number.

Object Object

Method

Description

eval

Evaluates a string of ECMAScript in the context of this object.

toString

Returns the string representation of this object.

valueOf

Returns the primitive value of the specified object.

String Object

Property

Description

length

Returns length of the string.

Method

Description

charAt

Returns the character at the specified index.

charCodeAt

Returns a number indicating the ISO-Latin-1 codeset value of the character at the given index.

concat

Combines the text of two strings and returns a new string.

fromCharCode

Returns a string from the specified sequence of numbers that are

ISO-Latin-1 codeset values.

indexOf

Returns the index within the calling String object of the first occurrence of the specified value.

lastIndexOf

Returns the index within the calling String object of the last occurrence of the specified value.

match

Matches a regular expression against a string.

replace

Finds a match between a regular expression and a string, and to replace the matched substring with a new substring.

search

Executes the search for a match between a regular expression and a specified string.

slice

Extracts a section of a string and returns a new string.

split

Splits a string into an array of strings by separating the string into substrings.

substr

Returns the characters in a string beginning at the specified location through the specified number of characters.

substring

Returns the characters in a string between two indexes into the string.

toLowerCase

Returns the calling string value converted to lowercase.

toUpperCase

Returns the calling string value converted to uppercase.

RegExp Object

Property

Description

$1...$9

Parenthesized substring matches, if any.

global

Whether or not to test the regular expression against all possible matches in a string, or only against the first.

ignoreCase

Whether or not to ignore case while attempting a match in a string.

input or $_

The string against which a regular expression is matched.

lastIndex

The index at which to start the next match.

lastMatch or $&

The last matched characters.

lastParen

The last parenthesized substring match, if any.

leftContext or $`

The substring preceding the most recent match.

multiline or $*

Whether or not to search in strings across multiple lines.

right Context or $'

The substring following the most recent match.

source

The text of the pattern.

Method

Description

compile

Compiles a regular expression object.

exec

Executes a search for a match in its string parameter.

test

Tests for a match in its string parameter.

Notes

1. Numeric literals starting with 0 are treated as octal numbers

The JavaScript engine treats numeric literals starting with 0 as octal numbers (base 8). If such numeric literals are specified within scripts or as an ECMAScript expression in any attribute, the numbers will be converted from an octal number to a decimal number when the expressions are evaluated by the JavaScript engine. This may cause unexpected results.

For example, if you are creating an object to send an ANI with an outbound call, you may get unexpected results for ANIs that start with 0. Here are three different cases:

  1. <assign name="callvars.ani" expr="4161234444"/>
  2. <assign name="callvars.ani" expr="0146504262"/>
  3. <assign name="callvars.ani" expr="0153080631"/>

In case (1), the numeric literal does not start with 0. So it will be treated as a decimal number when the expression is evaluated, and the same string of digits will be sent as the ANI with the outbound call.

In case (2), the numeric literal starts with 0, so it will be treated as an octal number. It is a valid octal number (all the digits are less than 8), so when the expression is evaluated, it will be converted to its decimal equivalent, 26904754. Therefore, the string of digits that are sent as the ANI with the outbound call will be different from what was originally specified.

In case (c), the numeric literal starts with 0, so it will be treated as an octal number. However, it is not a valid octal number (one of the digits is 8), so it cannot be converted to its decimal equivalent. In this case, the result of evaluating the expression is the decimal value 153080631, and the same string of digits (minus the leading 0) will be sent as the ANI with the outbound call.

In any case, wrapping the number in single quotes will ensure that it will be treated as a string. For numbers starting with 0, this means that there is no danger of it being converted. For example, the following variable setting ensures that the expected string of digits will be sent as the ANI with the outbound call:

<assign name="callvars.ani" expr="'0146504262'"/>



Reserved Words

Avoid using the reserved words in this list as variables, functions, methods, or object names. Some of these words are keywords; others are reserved for future use. Use of some of these reserved words will cause an error event to be thrown.

The words in the list that do not currently cause an error should still be avoided, as they might cause an error in the future.

  • abstract
  • boolean
  • break
  • byte
  • case
  • catch
  • char
  • class
  • comment
  • const
  • continue
  • debugger
  • default
  • delete
  • do
  • double
  • else
  • enum
  • export
  • extends
  • false
  • final
  • finally
  • float
  • for
  • function
  • goto
  • if
  • implements
  • import
  • in
  • instanceof
  • int
  • interface
  • label
  • long
  • native
  • new
  • null
  • package
  • private
  • protected
  • public
  • return
  • short
  • static
  • super
  • switch
  • synchronized
  • this
  • throw
  • throws
  • transient
  • true
  • try
  • typeof
  • var
  • void
  • volatile
  • while
  • with