2006年07月06日

作者:zzroom 更新时间: 2005-05-14

在编程的过程中,文件的操作是一个经常用到的问题,在C++Builder中,可以使用多种方法对文件操作,下面我就按以下几个部分对此作详细介绍,就是:

 

1、基于C的文件操作;

2、基于C++的文件操作;

3、基于WINAPI的文件操作;

4、基于BCB库的文件操作;

5、特殊文件的操作。

 

壹、基于C的文件操作 
  在ANSI C中,对文件的操作分为两种方式,即流式文件操作和I/O文件操作,下面就分别介绍之。

一、流式文件操作
  这种方式的文件操作有一个重要的结构FILE,FILE在stdio.h中定义如下:

typedef struct {
int level; /* fill/empty level of buffer */
unsigned flags; /* File status flags */
char fd; /* File descriptor */
unsigned char hold; /* Ungetc char if no buffer */
int bsize; /* Buffer size */
unsigned char _FAR *buffer; /* Data transfer buffer */
unsigned char _FAR *curp; /* Current active pointer */
unsigned istemp; /* Temporary file indicator */
short token; /* Used for validity checking */
} FILE; /* This is the FILE object */

  FILE这个结构包含了文件操作的基本属性,对文件的操作都要通过这个结构的指针来进行,此种文件操作常用的函数见下表 函数 功能 
fopen() 打开流 
fclose() 关闭流 
fputc() 写一个字符到流中 
fgetc() 从流中读一个字符 
fseek() 在流中定位到指定的字符 
fputs() 写字符串到流 
fgets() 从流中读一行或指定个字符 
fprintf() 按格式输出到流 
fscanf() 从流中按格式读取 
feof() 到达文件尾时返回真值 
ferror() 发生错误时返回其值 
rewind() 复位文件定位器到文件开始处 
remove() 删除文件 
fread() 从流中读指定个数的字符 
fwrite() 向流中写指定个数的字符 
tmpfile() 生成一个临时文件流 
tmpnam() 生成一个唯一的文件名 

  下面就介绍一下这些函数

1.fopen()
  fopen的原型是:FILE *fopen(const char *filename,const char *mode),fopen实现三个功能

为使用而打开一个流 
把一个文件和此流相连接 
给此流返回一个FILR指针
参数filename指向要打开的文件名,mode表示打开状态的字符串,其取值如下表

字符串 含义 
"r" 以只读方式打开文件 
"w" 以只写方式打开文件 
"a" 以追加方式打开文件 
"r+" 以读/写方式打开文件,如无文件出错 
"w+" 以读/写方式打开文件,如无文件生成新文件 

  一个文件可以以文本模式或二进制模式打开,这两种的区别是:在文本模式中回车被当成一个字符’\n’,而二进制模式认为它是两个字符0×0D,0×0A;如果在文件中读到0×1B,文本模式会认为这是文件结束符,也就是二进制模型不会对文件进行处理,而文本方式会按一定的方式对数据作相应的转换。

  系统默认的是以文本模式打开,可以修改全部变量_fmode的值来修改这个设置,例如_fmode=O_TEXT;就设置默认打开方式为文本模式;而_fmode=O_BINARY;则设置默认打开方式是二进制模式。

  我们也可以在模式字符串中指定打开的模式,如"rb"表示以二进制模式打开只读文件,"w+t"或"wt+"表示以文本模式打开读/写文件。

  此函数返回一个FILE指针,所以申明一个FILE指针后不用初始化,而是用fopen()来返回一个指针并与一个特定的文件相连,如果成败,返回NULL。

例:

  FILE *fp; 
  if(fp=fopen("123.456","wb"))
    puts("打开文件成功");
  else 
    puts("打开文件成败"); 

2.fclose() 
  fclose()的功能就是关闭用fopen()打开的文件,其原型是:int fclose(FILE *fp);如果成功,返回0,失败返回EOF。

  在程序结束时一定要记得关闭打开的文件,不然可能会造成数据丢失的情况,我以前就经常犯这样的毛病。

例:fclose(fp);

3.fputc()
  向流写一个字符,原型是int fputc(int c, FILE *stream); 成功返回这个字符,失败返回EOF。

例:fputc(‘X’,fp);

4.fgetc()
  从流中读一个字符,原型是int fputc(FILE *stream); 成功返回这个字符,失败返回EOF。

例:char ch1=fgetc(fp);

5. fseek()
  此函数一般用于二进制模式打开的文件中,功能是定位到流中指定的位置,原型是int fseek(FILE *stream, long offset, int whence);如果成功返回0,参数offset是移动的字符数,whence是移动的基准,取值是

符号常量 值 基准位置 
SEEK_SET 0 文件开头 
SEEK_CUR 1 当前读写的位置 
SEEK_END 2 文件尾部 

例:fseek(fp,1234L,SEEK_CUR);//把读写位置从当前位置向后移动1234字节(L后缀表示长整数)

  fseek(fp,0L,2);//把读写位置移动到文件尾

6.fputs()
  写一个字符串到流中,原型int fputs(const char *s, FILE *stream); 

例:fputs("I Love You",fp);

7.fgets()
  从流中读一行或指定个字符,原型是char *fgets(char *s, int n, FILE *stream); 从流中读取n-1个字符,除非读完一行,参数s是来接收字符串,如果成功则返回s的指针,否则返回NULL。

例:如果一个文件的当前位置的文本如下

Love ,I Have

But ……..

如果用

  fgets(str1,4,file1);

则执行后str1="Lov",读取了4-1=3个字符,而如果用

  fgets(str1,23,file1);

则执行str="Love ,I Have",读取了一行(不包括行尾的’\n’)。

8.fprintf()
  按格式输入到流,其原型是int fprintf(FILE *stream, const char *format[, argument, …]);其用法和printf()相同,不过不是写到控制台,而是写到流罢了

例:fprintf(fp,"%2d%s",4,"Hahaha");

9.fscanf()
  从流中按格式读取,其原型是int fscanf(FILE *stream, const char *format[, address, …]);其用法和scanf()相同,不过不是从控制台读取,而是从流读取罢了。

例:fscanf(fp,"%d%d" ,&x,&y);

10.feof()
  检测是否已到文件尾,是返回真,否则返回0,其原型是int feof(FILE *stream);

例:if(feof(fp))printf("已到文件尾");

11.ferror()
  原型是int ferror(FILE *stream);返回流最近的错误代码,可用clearerr()来清除它,clearerr()的原型是void clearerr(FILE *stream);

例:printf("%d",ferror(fp));

12.rewind()
  把当前的读写位置回到文件开始,原型是void rewind(FILE *stream);其实本函数相当于fseek(fp,0L,SEEK_SET);

例:rewind(fp);

12.remove()
  删除文件,原型是int remove(const char *filename); 参数就是要删除的文件名,成功返回0。

例:remove("c:\\io.sys");

13.fread()
  从流中读指定个数的字符,原型是size_t fread(void *ptr, size_t size, size_t n, FILE *stream);参数ptr是保存读取的数据,void*的指针可用任何类型的指针来替换,如char*、int *等等来替换;size是每块的字节数;n是读取的块数,如果成功,返回实际读取的块数(不是字节数),本函数一般用于二进制模式打开的文件中。

例:

  char x[4230];
  FILE *file1=fopen("c:\\msdos.sys","r");
  fread(x,200,12 ,file1);//共读取200*12=2400个字节

14.fwrite()
  与fread对应,向流中写指定的数据,原型是size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);参数ptr是要写入的数据指针,void*的指针可用任何类型的指针来替换,如char*、int *等等来替换;size是每块的字节数;n是要写的块数,如果成功,返回实际写入的块数(不是字节数),本函数一般用于二进制模式打开的文件中。

例:

  char x[]="I Love You";
  fwire(x, 6,12,fp);//写入6*12=72字节

  将把"I Love"写到流fp中12次,共72字节

15.tmpfile()
  其原型是FILE *tmpfile(void); 生成一个临时文件,以"w+b"的模式打开,并返回这个临时流的指针,如果失败返回NULL。在程序结束时,这个文件会被自动删除。

例:FILE *fp=tmpfile();

16.tmpnam();
  其原型为char *tmpnam(char *s); 生成一个唯一的文件名,其实tmpfile()就调用了此函数,参数s用来保存得到的文件名,并返回这个指针,如果失败,返回NULL。

例:tmpnam(str1);

二、直接I/O文件操作
  这是C提供的另一种文件操作,它是通过直接存/取文件来完成对文件的处理,而上篇所说流式文件操作是通过缓冲区来进行;流式文件操作是围绕一个FILE指针来进行,而此类文件操作是围绕一个文件的“句柄”来进行,什么是句柄呢?它是一个整数,是系统用来标识一个文件(在WINDOWS中,句柄的概念扩展到所有设备资源的标识)的唯一的记号。此类文件操作常用的函数如下表,这些函数及其所用的一些符号在io.h和fcntl.h中定义,在使用时要加入相应的头文件。

函数 说明 
open() 打开一个文件并返回它的句柄 
close() 关闭一个句柄 
lseek() 定位到文件的指定位置 
read() 块读文件 
write() 块写文件 
eof() 测试文件是否结束 
filelength() 取得文件长度 
rename() 重命名文件 
chsize() 改变文件长度 

  下面就对这些函数一一说明:

1.open()
  打开一个文件并返回它的句柄,如果失败,将返回一个小于0的值,原型是int open(const char *path, int access [, unsigned mode]); 参数path是要打开的文件名,access是打开的模式,mode是可选项。表示文件的属性,主要用于UNIX系统中,在DOS/WINDOWS这个参数没有意义。其中文件的打开模式如下表。

符号 含义 符号 含义 符号 含义 
O_RDONLY 只读方式 O_WRONLY 只写方式 O_RDWR 读/写方式 
O_NDELAY 用于UNIX系统 O_APPEND 追加方式 O_CREAT 如果文件不存在就创建 
O_TRUNC 把文件长度截为0 O_EXCL 和O_CREAT连用,如果文件存在返回错误 O_BINARY 二进制方式 
O_TEXT 文本方式 

  对于多个要求,可以用"|"运算符来连接,如O_APPEND|O_TEXT表示以文本模式和追加方式打开文件。

例:int handle=open("c:\\msdos.sys",O_BINARY|O_CREAT|O_WRITE)

2.close()
  关闭一个句柄,原型是int close(int handle);如果成功返回0

例:close(handle)

3.lseek()
  定位到指定的位置,原型是:long lseek(int handle, long offset, int fromwhere);参数offset是移动的量,fromwhere是移动的基准位置,取值和前面讲的fseek()一样,SEEK_SET:文件首部;SEEK_CUR:文件当前位置;SEEK_END:文件尾。此函数返回执行后文件新的存取位置。

例:

  lseek(handle,-1234L,SEEK_CUR);//把存取位置从当前位置向前移动1234个字节。
  x=lseek(hnd1,0L,SEEK_END);//把存取位置移动到文件尾,x=文件尾的位置即文件长度

4.read()
  从文件读取一块,原型是int read(int handle, void *buf, unsigned len);参数buf保存读出的数据,len是读取的字节。函数返回实际读出的字节。

例:char x[200];read(hnd1,x,200);

5.write()
  写一块数据到文件中,原型是int write(int handle, void *buf, unsigned len);参数的含义同read(),返回实际写入的字节。

例:char x[]="I Love You";write(handle,x,strlen(x));

7.eof()
  类似feof(),测试文件是否结束,是返回1,否则返回0;原型是:int eof(int handle);

例:while(!eof(handle1)){……};

8.filelength()
  返回文件长度,原型是long filelength(int handle);相当于lseek(handle,0L,SEEK_END)

例:long x=filelength(handle);

9.rename()
  重命名文件,原型是int rename(const char *oldname, const char *newname); 参数oldname是旧文件名,newname是新文件名。成功返回0

例:rename("c:\\config.sys","c:\\config.w40");

10.chsize();
  改变文件长度,原型是int chsize(int handle, long size);参数size表示文件新的长度,成功返回0,否则返回-1,如果指定的长度小于文件长度,则文件被截短;如果指定的长度大于文件长度,则在文件后面补’\0′。

例:chsize(handle,0×12345);

——————————————————————————–

  如果熟悉汇编可能会发现这种方式和汇编语言的DOS功能调用句柄式文件操作很像,比如open()就像DOS服务的3CH号功能调用,其实这种操作还有两种类型的函数就是直接用DOS功能来完成的,如_open(),_dos_open()等等。有兴趣可自已查询BCB的帮助。

  同流式文件操作相同,这种也提供了Unicode字符操作的函数,如_wopen()等等,用于9X/NT下的宽字符编程,有兴趣可自已查询BCB的帮助。

  另外,此种操作还有lock(),unlock(),locking()等用于多用户操作的函数,但在BCB中用得并不多,我就不介绍了,但如果要用C来写CGI,这些就必要的常识了,如果你有这方面的要求,那就得自已好好看帮助了。 

  

  在C++中,有一个stream这个类,所有的I/O都以这个“流”类为基础的,包括我们要认识的文件I/O,stream这个类有两个重要的运算符:

1、插入器(<<)
  向流输出数据。比如说系统有一个默认的标准输出流(cout),一般情况下就是指的显示器,所以,cout<<"Write Stdout"<<’\n’;就表示把字符串"Write Stdout"和换行字符(‘\n’)输出到标准输出流。

2、析取器(>>)
  从流中输入数据。比如说系统有一个默认的标准输入流(cin),一般情况下就是指的键盘,所以,cin>>x;就表示从标准输入流中读取一个指定类型(即变量x的类型)的数据。

  在C++中,对文件的操作是通过stream的子类fstream(file stream)来实现的,所以,要用这种方式操作文件,就必须加入头文件fstream.h。下面就把此类的文件操作过程一一道来。

一、打开文件
  在fstream类中,有一个成员函数open(),就是用来打开文件的,其原型是:

void open(const char* filename,int mode,int access);

参数:

filename:  要打开的文件名 
mode:    要打开文件的方式 
access:   打开文件的属性
打开文件的方式在类ios(是所有流式I/O类的基类)中定义,常用的值如下: 

ios::app:   以追加的方式打开文件 
ios::ate:   文件打开后定位到文件尾,ios:app就包含有此属性 
ios::binary:  以二进制方式打开文件,缺省的方式是文本方式。两种方式的区别见前文 
ios::in:    文件以输入方式打开 
ios::out:   文件以输出方式打开 
ios::nocreate: 不建立文件,所以文件不存在时打开失败  
ios::noreplace:不覆盖文件,所以打开文件时如果文件存在失败 
ios::trunc:  如果文件存在,把文件长度设为0 
  可以用“或”把以上属性连接起来,如ios::out|ios::binary

  打开文件的属性取值是:

0:普通文件,打开访问 
1:只读文件 
2:隐含文件 
4:系统文件 
  可以用“或”或者“+”把以上属性连接起来 ,如3或1|2就是以只读和隐含属性打开文件。

  例如:以二进制输入方式打开文件c:\config.sys 

  fstream file1;
  file1.open("c:\\config.sys",ios::binary|ios::in,0);

  如果open函数只有文件名一个参数,则是以读/写普通文件打开,即:

  file1.open("c:\\config.sys");<=>file1.open("c:\\config.sys",ios::in|ios::out,0);

  另外,fstream还有和open()一样的构造函数,对于上例,在定义的时侯就可以打开文件了:

  fstream file1("c:\\config.sys");

  特别提出的是,fstream有两个子类:ifstream(input file stream)和ofstream(outpu file stream),ifstream默认以输入方式打开文件,而ofstream默认以输出方式打开文件。

  ifstream file2("c:\\pdos.def");//以输入方式打开文件
  ofstream file3("c:\\x.123");//以输出方式打开文件

  所以,在实际应用中,根据需要的不同,选择不同的类来定义:如果想以输入方式打开,就用ifstream来定义;如果想以输出方式打开,就用ofstream来定义;如果想以输入/输出方式来打开,就用fstream来定义。

二、关闭文件
  打开的文件使用完成后一定要关闭,fstream提供了成员函数close()来完成此操作,如:file1.close();就把file1相连的文件关闭。

三、读写文件
  读写文件分为文本文件和二进制文件的读取,对于文本文件的读取比较简单,用插入器和析取器就可以了;而对于二进制的读取就要复杂些,下要就详细的介绍这两种方式

  1、文本文件的读写
  文本文件的读写很简单:用插入器(<<)向文件输出;用析取器(>>)从文件输入。假设file1是以输入方式打开,file2以输出打开。示例如下:

  file2<<"I Love You";//向文件写入字符串"I Love You"
  int I;
  file1>>I;//从文件输入一个整数值。 

  这种方式还有一种简单的格式化能力,比如可以指定输出为16进制等等,具体的格式有以下一些

操纵符 功能 输入/输出 
dec 格式化为十进制数值数据 输入和输出 
endl 输出一个换行符并刷新此流 输出 
ends 输出一个空字符 输出 
hex 格式化为十六进制数值数据 输入和输出 
oct 格式化为八进制数值数据 输入和输出 
setpxecision(int p) 设置浮点数的精度位数 输出 

  比如要把123当作十六进制输出:file1<<hex<<123;要把3.1415926以5位精度输出:file1<<setpxecision(5)<<3.1415926。

  2、二进制文件的读写
①put()
  put()函数向流写入一个字符,其原型是ofstream &put(char ch),使用也比较简单,如file1.put(‘c’);就是向流写一个字符’c'。 

②get()
  get()函数比较灵活,有3种常用的重载形式:

  一种就是和put()对应的形式:ifstream &get(char &ch);功能是从流中读取一个字符,结果保存在引用ch中,如果到文件尾,返回空字符。如file2.get(x);表示从文件中读取一个字符,并把读取的字符保存在x中。

  另一种重载形式的原型是: int get();这种形式是从流中返回一个字符,如果到达文件尾,返回EOF,如x=file2.get();和上例功能是一样的。

  还有一种形式的原型是:ifstream &get(char *buf,int num,char delim=’\n’);这种形式把字符读入由 buf 指向的数组,直到读入了 num 个字符或遇到了由 delim 指定的字符,如果没使用 delim 这个参数,将使用缺省值换行符’\n’。例如:

  file2.get(str1,127,’A');//从文件中读取字符到字符串str1,当遇到字符’A'或读取了127个字符时终止。

③读写数据块
  要读写二进制数据块,使用成员函数read()和write()成员函数,它们原型如下:

    read(unsigned char *buf,int num);
    write(const unsigned char *buf,int num);

  read()从文件中读取 num 个字符到 buf 指向的缓存中,如果在还未读入 num 个字符时就到了文件尾,可以用成员函数 int gcount();来取得实际读取的字符数;而 write() 从buf 指向的缓存写 num 个字符到文件中,值得注意的是缓存的类型是 unsigned char *,有时可能需要类型转换。

例:

    unsigned char str1[]="I Love You";
    int n[5];
    ifstream in("xxx.xxx");
    ofstream out("yyy.yyy");
    out.write(str1,strlen(str1));//把字符串str1全部写到yyy.yyy中
    in.read((unsigned char*)n,sizeof(n));//从xxx.xxx中读取指定个整数,注意类型转换
    in.close();out.close(); 

四、检测EOF
  成员函数eof()用来检测是否到达文件尾,如果到达文件尾返回非0值,否则返回0。原型是int eof();

例:  if(in.eof())ShowMessage("已经到达文件尾!");

五、文件定位
  和C的文件操作方式不同的是,C++ I/O系统管理两个与一个文件相联系的指针。一个是读指针,它说明输入操作在文件中的位置;另一个是写指针,它下次写操作的位置。每次执行输入或输出时,相应的指针自动变化。所以,C++的文件定位分为读位置和写位置的定位,对应的成员函数是 seekg()和 seekp(),seekg()是设置读位置,seekp是设置写位置。它们最通用的形式如下:

    istream &seekg(streamoff offset,seek_dir origin);
    ostream &seekp(streamoff offset,seek_dir origin); 

  streamoff定义于 iostream.h 中,定义有偏移量 offset 所能取得的最大值,seek_dir 表示移动的基准位置,是一个有以下值的枚举: 

ios::beg:  文件开头 
ios::cur:  文件当前位置 
ios::end:  文件结尾 
  这两个函数一般用于二进制文件,因为文本文件会因为系统对字符的解释而可能与预想的值不同。

例:

     file1.seekg(1234,ios::cur);//把文件的读指针从当前位置向后移1234个字节
     file2.seekp(1234,ios::beg);//把文件的写指针从文件开头向后移1234个字节 

——————————————————————————–

  有了这些知识,我们就可以完成对文件的操作了,当然,还有好多的成员函数我没介绍,但有这些我们已经能完成大多数的需要了,这种文件操作方式是我比较喜欢的一种方法,比C的方法灵活,又比BCB函数和WINAPI函数具有通用性。

  下一次,我将介绍BCB提供的文件操作的库函数。 

  

 在BCB中也提供了文件操作的函数,这些函数的功能和前面所介绍的大致相同,但这类函数和BCB关系紧密,能使用BCB中的AnsiString等数据类型,在BCB中用这种方式的文件操作是最方便的,下面我就把这种文件操作详细介绍。

  在BCB提供的这组文件操作函数中,可分为三种类型,就是:1、文件名函数,2、文件管理函数;3、文件I/O函数。

1、文件名函数
  文件名函数可以对文件的名称、所在子目录、驱动器和扩展名等进行操作。下表列出这些函数及其功能。

函数 说明 
ExpandFileName() 返回文件的全路径(含驱动器、路径) 
ExtractFileExt() 从文件名中抽取扩展名 
ExtractFileName() 从文件名中抽取不含路径的文件名 
ExtractFilePath() 从文件名中抽取路径名 
ExtractFileDir() 从文件名中抽取目录名 
ExtractFileDrive() 从文件名中抽取驱动器名 
ChangeFileExt() 改变文件的扩展名 
ExpandUNCFileName() 返回含有网络驱动器的文件全路径 
ExtractRelativePath() 从文件名中抽取相对路径信息 
ExtractShortPathName() 把文件名转化为DOS的8·3格式 
MatchesMask() 检查文件是否与指定的文件名格式匹配 

  下面就把这些函数作一一介绍:

⑴ExpandFileName() 
  原型:extern PACKAGE AnsiString __fastcall ExpandFileName(const AnsiString FileName);

  功能:返回文件的全路径(含驱动器、路径)

  参数:FileName:要处理的文件名

  例:ShowMessage(ExpandFileName(Application->ExeName));//显示你的程序文件名,如C:\MyBCB\Sample1.EXE

⑵ExtractFileExt()
  原型:extern PACKAGE AnsiString __fastcall ExtractFileExt(const AnsiString FileName);

  功能:从文件名中抽取扩展名

  参数:FileName:要处理的文件名(全路径)

  例:ShowMessage(ExtractFileExt(Application->ExeName));//显示".exe"

⑶ExtractFileName()
  原型:extern PACKAGE AnsiString __fastcall ExtractFileName(const AnsiString FileName); 

  功能:从文件名中抽取不含路径的文件名

  参数:FileName:要处理的文件名

  例:ShowMessage(ExtractFileExt("c:\\Winnt\\SOL.EXE"));//显示"SOL.EXE"

⑷ExtractFilePath()
  原型:extern PACKAGE AnsiString __fastcall ExtractFilePath(const AnsiString FileName); 

  功能:从文件名中抽取路径名

  参数:FileName:要处理的文件名

  例:ShowMessage(ExtractFilePath("Winnt\\SOL.EXE"));//显示"Winnt\"

⑸ExtractFileDir()
  原型:extern PACKAGE AnsiString __fastcall ExtractFileDir(const AnsiString FileName); 

  功能:从文件名中抽取目录名(和上个函数不同,不包括最后的"\")

  参数:FileName:要处理的文件名

  例:ShowMessage(ExtractFileDir("Winnt\\SOL.EXE"));//显示"Winnt",注意和上个函数的区别 

⑹ExtractFileDrive()
  原型:extern PACKAGE AnsiString __fastcall ExtractFileDrive(const AnsiString FileName); 

  功能:从文件名中抽取驱动器名

  参数:FileName:要处理的文件名

  例:ShowMessage(ExtractFileDrive("c:\\Winnt\\SOL.EXE"));//显示"c:"

⑺ChangeFileExt()
  原型:extern PACKAGE System::AnsiString __fastcall ChangeFileExt(const System::AnsiString FileName, const System::AnsiString Extension); 

  功能:更改文件名的扩展名,不是对真正的文件进行改名,只是对文件名这个字符串进行处理

  参数:FileName:要改名的文件名,Extension:新的扩展名

  例:ShowMessage(ChangeFileExt("c:\\Winnt\\SOL.EXE",".OOO"));//显示"c:\winnt\SOL.OOO"

⑻ExpandUNCFileName()
  原型:extern PACKAGE AnsiString __fastcall ExpandUNCFileName(const AnsiString FileName);

  功能:返回含有网络驱动器的文件全路径,格式为:\\机器名\共享名\文件名

  参数:FileName:要处理的文件名

  例:ShowMessage(ExpandUNCFileName("F:\\Winnt\\SOL.EXE"));/*如果F:是映射的网络驱动器\\NT40\WINNT,则显示"\\NT40\WINNT\SOL.EXE"*/

⑼ExtractRelativePath()
  原型:extern PACKAGE AnsiString __fastcall ExtractRelativePath(const AnsiString BaseName, const AnsiString DestName); 

  功能:从文件名中抽取相对路径信息,如"..\sss\ss.asd"这种形式

  参数:BaseName:基准文件名;DestName:目标文件名

  例:ShowMessage(ExtractRelativePath("D:\\Source\\c\\1.123","D:\\Source\\Asm\\dz.asm"));/*显示"..\asm\dz.asm"*/

⑽ExtractShortPathName()
  原型:extern PACKAGE AnsiString __fastcall ExtractShortPathName(const AnsiString FileName); 

  功能:把文件名转换为DOS的8、3格式

  参数:FileName:要处理的文件名

  例:ShowMessage(ExtractShortPathName("E:\\Program Files\\Dual Wheel Mouse\\4dmain.exe"));/*显示"E:\Progra~1\dualwh~1\4dmain.exe"*/

⑾MatchesMask()
  原型:extern PACKAGE bool __fastcall MatchesMask(const AnsiString Filename, const AnsiString Mask);

  功能:检查文件是否与指定的文件名格式匹配

  参数:FileName:要处理的文件名;Mask:文件名格式,支持通配符

  例:ShowMessage(MatchesMask("Lxf.exe","*.?x?));//显示"true"

——————————————————————————–

2、文件管理函数
  这类函数包括设置和读取驱动器、子目录和文件的有关的各种操作,下表列出这类操作常用的函数及其功能。

函数 功能 
CreateDir() 创建新的子目录 
DeleteFile() 删除文件 
DirectoryExists() 判断目录是否存在 
DiskFree() 获取磁盘剩余空间 
DiskSize() 获取磁盘容量 
FileExists() 判断文件是否存在 
FileGetAttr() 获取文件属性 
FileGetDate() 获取文件日期 
GetCurrentDir() 获取当前目录 
RemoveDir() 删除目录 
SetCurrentDir() 设置当前目录 

  下面就把这些函数作一一介绍:

⑴CreateDir()
  原型:extern PACKAGE bool __fastcall CreateDir(const System::AnsiString Dir);

  功能:建立子目录,如果成功返回true,否则返回false

  参数:Dir:要建立的子目录的名字

  例:Create("ASM");//在当前目录下建立一个名为ASM的子目录

⑵DeleteFile()
  原型:extern PACKAGE bool __fastcall DeleteFile(const System::AnsiString FileName);

  功能:删除文件,如果成功返回true,否则返回false

  参数:FileName:要删除的文件名

  例:if(OpenDialog1->Execute())DeleteFile(OpenDialog1->FileName);

⑶DirectoryExists()
  原型:extern PACKAGE bool __fastcall DirectoryExists(const System:: AnsiString Name);

  功能:检测目录是否存在,如果存在返回true,否则返回false

  参数:Name:要检测的目录名

  例:if(!DirectoryExists("ASM"))CreateDir("ASM");//如果ASM这个目录不存在则创建之

⑷DiskFree()
  原型:extern PACKAGE __int64 __fastcall DiskFree(Byte Drive);

  功能:检测磁盘剩余空间,返回值以字节为单位,如果指定的磁盘无效,返回-1

  参数:Drive:磁盘的代号,0表示当前盘, 1=A,2=B,3=C 以此类推

  例:ShowMessage(DiskFree(0));//显示当前盘的剩余空间

⑸DiskSize()
  原型:extern PACKAGE __int64 __fastcall DiskSize(Byte Drive);

  功能:检测磁盘容量,返回值以字节为单位,如果指定的磁盘无效,返回-1

  参数:Drive:磁盘的代号,0表示当前盘, 1=A,2=B,3=C 以此类推

  例:ShowMessage(DiskFree(0));//显示当前盘的容量

⑹FileExists()
  原型:extern PACKAGE bool __fastcall FileExists(const AnsiString FileName);

  功能:检测文件是否存在,如果存在返回true,否则返回false

  参数:FileName:要检测的文件名

  例:if(FileExists("AAA.ASM"))DeleteFile("AAA.ASM");

⑺FileGetAttr()
  原型:extern PACKAGE int __fastcall FileGetAttr(const AnsiString FileName);

  功能:取得文件属性,如果出错返回-1

返回值如下表,如果返回$00000006表示是一个具有隐含和系统属性的文件(4+2)

常量 值 含义 
faReadOnly $00000001 只读文件 
faHidden $00000002 隐含文件 
faSysFile $00000004 系统文件 
faVolumeID $00000008 卷标 
faDirectory $00000010 目录 
faArchive $00000020 归档文件 

  例:if(FileGetAttr("LLL.TXT")&0×2)ShowMessage("这是一个有隐含属性的文件");

  与此对应的有FileSetAttr() ,请自已查阅帮助系统

⑻FileGetDate()
  原型:extern PACKAGE int __fastcall FileGetDate(int Handle);

  功能:返回文件的建立时间到1970-1-1日0时的秒数

  参数:Handle:用FileOpen()打开的文件句柄。

  例:

    int i=FileOpen("C:\\autoexec.bat",fmOpenRead);
    ShowMessage(FileGetDate(i));
    FileClose(i);

  与此对应的有FileSetDate(),请自已查阅帮助系统

⑼GetCurrentDir()
  原型:extern PACKAGE AnsiString __fastcall GetCurrentDir();

  功能:取得当前的目录名

  例:ShowMessage(GetCurrentDir());

⑽RemoveDir()
  原型:extern PACKAGE bool __fastcall RemoveDir(const AnsiString Dir);

  功能:删除目录,如果成功返回true,否则返回false

  参数:Dir:要删除的目录名

  例:if(DiectoryExists("ASM"))RemoveDir("ASM");

⑾SetCurrentDir()
  原型:extern PACKAGE bool __fastcall SetCurrentDir(const AnsiString Dir);

  功能:设置当前目录,如果成功返回true,否则返回false

  参数:Dir:要切换到的目录名

  例:SetCurrentDir("C:\\WINDOWS");

——————————————————————————–

3、文件I/O函数
  这类函数完成对文件的读写相关的操作,这种类型的操作和C的基于I/O文件操作类似,下表列出这类操作常用的函数及其功能。

FileOpen() 打开文件 
FileClose() 关闭文件 
FileRead() 读文件 
FileSeek() 文件定位 
FileWrite() 写文件 
FileCreate() 创建文件 

  下面就对这些函数作详细介绍。

⑴FileOpen()
  原型:extern PACKAGE int __fastcall FileOpen(const AnsiString FileName, int Mode);

  功能:打开文件,如果成功返回其句柄,否则返回-1

  参数:FileName:要打开的文件名;Mode:打开的方式,取值如下表,可用"或"("|")运算符连接。

常量 值 说明
————————————————————-
fmOpenRead 0 以只读属性打开
fmOpenWrite 1 以只写属性打开
fmOpenReadWrite 2 以读/写属性打开
fmShareCompat 0 兼容FCB方式(汇编中有相应的DOS功能调用,感兴趣自已查阅相关资料)
fmShareExclusive 16 共享方式:以独占方式打开,在关闭以前,别人不能访问
fmShareDenyWrite 32 共享方式:拒绝写访问
fmShareDenyRead 48 共享方式:拒绝读访问
fmShareDenyNone 64 共享方式:无限制,允许读写
  例:int i=FileOpen("C:\\WINDOWS\\Win.ini",fmOpenReadWrite|fmShareExclusive);

⑵FileClose()
  原型:extern PACKAGE void __fastcall FileClose(int Handle);

  功能:关闭打开的句柄。

  参数:Handle:要关闭的句柄

  例:FileClose(i);

⑶FileRead()
  原型:extern PACKAGE int __fastcall FileRead(int Handle, void *Buffer, int Count);

  功能:读文件,返回实际读取的字节数,句柄必须首先由FileOpen或FileCreate创建。

  参数:Handle:要读取的句柄;Buffer:存放读取的数据的缓冲区;Count:想读取的字节数

  例:char str[400];FileRead(hnd1,str,400);

⑷FileSeek()
  原型:extern PACKAGE int __fastcall FileSeek(int Handle, int Offset, int Origin);

  功能:移动文件读取指针,成功返回文件指针的位置,失败返回-1

  参数:Handle:相关联的句柄;Offset:移动的量;Orgin:移动的基准,0=文件头,1=当前位置,2=文件尾。

  例:ShowMessage(FileSeek(hnd1,0,2));//取得文件的长度

⑸FileWrite()
  原型:extern PACKAGE int __fastcall FileWrite(int Handle, const void *Buffer, int Count);

  功能:写文件,返回实际写入的字节数,句柄必须首先由FileOpen或FileCreate创建。

  参数:Handle:要写入的句柄;Buffer:存放写入数据的缓冲区;Count:想写入的字节数

  例:char str[]="I Love You";FileWrite(hnd1,str,strlen(str));

⑹FileCreate()
  原型:extern PACKAGE int __fastcall FileCreate(const AnsiString FileName);

  功能:创建文件。成功返回其句柄,否则返回-1

  参数:FileName:要创建的文件名

  例:if(!FileExists("KC.C"))hnd1=FileCreate("KC.C");

 

    Dev-C++是一个在Windows下用使用gcc编译器的IDE,我目前使用的版本是4.9.8.0版,下载地址为http://www.c-view.org/soft/devcpp/downloads/devcpp4980.exe,可以在它自己的主页http://bloodshed.net/或http://sourceforge.net/projects/dev-cpp/中找到关于Dev-C++的更多信息.

1.在Dev-C++中调试程序
    用Dev-C++编写C++项目时,如果用菜单"Debug\Debug"调试时,系统会提示"Your project does not have debugging information, do you want to enable debugging and rebuild your project?",此时即使选"Yes"也还是不能调试.要在"Project/Class Brower"窗口中(通过View\Project/Class Brower调出该窗口),然后选中项目按右键,选"Project Options"在"Compiler"页中选"Linker"项,将其中的"Generate debugging information"设为"Yes".此时再重新链接一次程序,就可以调试了.

2.观察变量
    在调试中观察变量值时,string类型的变量如果先加入到了Debug窗口中,单步走时是不会自动刷新的,所以需要将已经加入的变量删除后再添加才能看到新的值,但对int这样的变量是可以自动刷新的.

3.恢复c,cpp等文件与VC的关联
    由于安装程序有bug,所以不管选不选于C,C++文件关联,安装完成后C,Cpp,H,rc等都会自动与Dev-C++关联,如果在"Tools\Emvironment Options"的"File Associ…"页中将与c,cpp,h,rc的关联去除,也不能恢复本来与VC的关联,而且在资源管理器中通过"工具\文件夹选项"的文件类型中,也看不到c,cpp,h,rc了.此时可以将下列内容放入一个扩展名为reg的文件中,导入到注册表中.注意,下面例子中,Visual Studio 6安装在F:\Program Files\Microsoft Visual Studio目录下,所以要根据具体情况修改路径.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile]
@="C Source file"
"AlwaysShowExt"=""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\DefaultIcon]
@="F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe,-20210"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell\&Open with MSDev]
@="&Open with MSDEV"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell\&Open with MSDev\command]
@="\"F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe\" \"%1\""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell\&Open with MSDev\ddeexec]
@="Open(\"%1\")"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell\&Open with MSDev\ddeexec\application]
@="msdev"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cfile\shell\&Open with MSDev\ddeexec\topic]
@="system"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile]
@="C++ Source file"
"AlwaysShowExt"=""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\DefaultIcon]
@="F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe,-20209"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell\&Open with MSDev]
@="&Open with MSDEV"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell\&Open with MSDev\command]
@="\"F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe\" \"%1\""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell\&Open with MSDev\ddeexec]
@="Open(\"%1\")"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell\&Open with MSDev\ddeexec\application]
@="msdev"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\cppfile\shell\&Open with MSDev\ddeexec\topic]
@="system"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile]
@="Resource Template"
"AlwaysShowExt"=""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\DefaultIcon]
@="F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe,-20212"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell\&Open with MSDev]
@="&Open with MSDEV"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell\&Open with MSDev\command]
@="\"F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe\" \"%1\""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell\&Open with MSDev\ddeexec]
@="Open(\"%1\")"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell\&Open with MSDev\ddeexec\application]
@="msdev"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\rcfile\shell\&Open with MSDev\ddeexec\topic]
@="system"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile]
@="C Header file"
"AlwaysShowExt"=""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\DefaultIcon]
@="F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe,-20211"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell]

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell\&Open with MSDev]
@="&Open with MSDEV"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell\&Open with MSDev\command]
@="\"F:\\Program Files\\Microsoft Visual Studio\\Common\\MSDev98\\Bin\\msdev.exe\" \"%1\""

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell\&Open with MSDev\ddeexec]
@="Open(\"%1\")"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell\&Open with MSDev\ddeexec\application]
@="msdev"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\hfile\shell\&Open with MSDev\ddeexec\topic]
@="system"

然后在"工具\文件夹选项"的文件类型中,删除c,cpp,h,rc类型,再重新建立,建立类型时按下"高级"按钮,选择"关联的文件类型"
c选C Source file
cpp选C++ Source file
h选C Header file
rc选Resource Template

作者Blog:http://blog.csdn.net/ninja/
2006年06月26日

C/C++中的日期和时间

撰文/周翔
摘要:
本文从介绍基础概念入手,探讨了在C/C++中对日期和时间操作所用到的数据结构和函数,并对计时、时间的获取、时间的计算和显示格式等方面进行了阐述。本文还通过大量的实例向你展示了time.h头文件中声明的各种函数和数据结构的详细使用方法。

关键字:UTC(世界标准时间),Calendar Time(日历时间),epoch(时间点),clock tick(时钟计时单元)

1.概念
在C/C++中,对字符串的操作有很多值得注意的问题,同样,C/C++对时间的操作也有许多值得大家注意的地方。最近,在技术群中有很多网友也多次问到过C++语言中对时间的操作、获取和显示等等的问题。下面,在这篇文章中,笔者将主要介绍在C/C++中时间和日期的使用方法.

通过学习许多C/C++库,你可以有很多操作、使用时间的方法。但在这之前你需要了解一些“时间”和“日期”的概念,主要有以下几个:

Coordinated Universal Time(UTC):协调世界时,又称为世界标准时间,也就是大家所熟知的格林威治标准时间(Greenwich Mean Time,GMT)。比如,中国内地的时间与UTC的时差为+8,也就是UTC+8。美国是UTC-5。

Calendar Time:日历时间,是用“从一个标准时间点到此时的时间经过的秒数”来表示的时间。这个标准时间点对不同的编译器来说会有所不同,但对一个编译系统来说,这个标准时间点是不变的,该编译系统中的时间对应的日历时间都通过该标准时间点来衡量,所以可以说日历时间是“相对时间”,但是无论你在哪一个时区,在同一时刻对同一个标准时间点来说,日历时间都是一样的。

epoch:时间点。时间点在标准C/C++中是一个整数,它用此时的时间和标准时间点相差的秒数(即日历时间)来表示。

clock tick:时钟计时单元(而不把它叫做时钟滴答次数),一个时钟计时单元的时间长短是由CPU控制的。一个clock tick不是CPU的一个时钟周期,而是C/C++的一个基本计时单位。

我们可以使用ANSI标准库中的time.h头文件。这个头文件中定义的时间和日期所使用的方法,无论是在结构定义,还是命名,都具有明显的C语言风格。下面,我将说明在C/C++中怎样使用日期的时间功能。

2. 计时

C/C++中的计时函数是clock(),而与其相关的数据类型是clock_t。在MSDN中,查得对clock函数定义如下:

clock_t clock( void );

这个函数返回从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元(clock tick)数,在MSDN中称之为挂钟时间(wal-clock)。其中clock_t是用来保存时间的数据类型,在time.h文件中,我们可以找到对它的定义:

#ifndef _CLOCK_T_DEFINED
typedef long clock_t;
#define _CLOCK_T_DEFINED
#endif

很明显,clock_t是一个长整形数。在time.h文件中,还定义了一个常量CLOCKS_PER_SEC,它用来表示一秒钟会有多少个时钟计时单元,其定义如下:

#define CLOCKS_PER_SEC ((clock_t)1000)

可以看到每过千分之一秒(1毫秒),调用clock()函数返回的值就加1。下面举个例子,你可以使用公式clock()/CLOCKS_PER_SEC来计算一个进程自身的运行时间:

void elapsed_time()
{
printf(”Elapsed time:%u secs.\n”,clock()/CLOCKS_PER_SEC);
}

当然,你也可以用clock函数来计算你的机器运行一个循环或者处理其它事件到底花了多少时间:

#include “stdio.h”
#include “stdlib.h”
#include “time.h”

int main( void )
{
   long    i = 10000000L;
   clock_t start, finish;
   double  duration;
   /* 测量一个事件持续的时间*/
   printf( “Time to do %ld empty loops is “, i );
   start = clock();
   while( i– )      ;
   finish = clock();
   duration = (double)(finish – start) / CLOCKS_PER_SEC;
   printf( “%f seconds\n”, duration );
   system(”pause”);
}

在笔者的机器上,运行结果如下:

Time to do 10000000 empty loops is 0.03000 seconds

上面我们看到时钟计时单元的长度为1毫秒,那么计时的精度也为1毫秒,那么我们可不可以通过改变CLOCKS_PER_SEC的定义,通过把它定义的大一些,从而使计时精度更高呢?通过尝试,你会发现这样是不行的。在标准C/C++中,最小的计时单位是一毫秒。

3.与日期和时间相关的数据结构

在标准C/C++中,我们可通过tm结构来获得日期和时间,tm结构在time.h中的定义如下:

#ifndef _TM_DEFINED
struct tm {
        int tm_sec;     /* 秒 – 取值区间为[0,59] */
        int tm_min;     /* 分 – 取值区间为[0,59] */
        int tm_hour;    /* 时 – 取值区间为[0,23] */
        int tm_mday;    /* 一个月中的日期 – 取值区间为[1,31] */
        int tm_mon;     /* 月份(从一月开始,0代表一月) – 取值区间为[0,11] */
        int tm_year;    /* 年份,其值等于实际年份减去1900 */
        int tm_wday;    /* 星期 – 取值区间为[0,6],其中0代表星期天,1代表星期一,以此类推 */
        int tm_yday;    /* 从每年的1月1日开始的天数 – 取值区间为[0,365],其中0代表1月1日,1代表1月2日,以此类推 */
        int tm_isdst;   /* 夏令时标识符,实行夏令时的时候,tm_isdst为正。不实行夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。*/
        };
#define _TM_DEFINED
#endif

ANSI C标准称使用tm结构的这种时间表示为分解时间(broken-down time)。

而日历时间(Calendar Time)是通过time_t数据类型来表示的,用time_t表示的时间(日历时间)是从一个时间点(例如:1970年1月1日0时0分0秒)到此时的秒数。在time.h中,我们也可以看到time_t是一个长整型数:

#ifndef _TIME_T_DEFINED
typedef long time_t;         /* 时间值 */
#define _TIME_T_DEFINED      /* 避免重复定义 time_t */
#endif

大家可能会产生疑问:既然time_t实际上是长整型,到未来的某一天,从一个时间点(一般是1970年1月1日0时0分0秒)到那时的秒数(即日历时间)超出了长整形所能表示的数的范围怎么办?对time_t数据类型的值来说,它所表示的时间不能晚于2038年1月18日19时14分07秒。为了能够表示更久远的时间,一些编译器厂商引入了64位甚至更长的整形数来保存日历时间。比如微软在Visual C++中采用了__time64_t数据类型来保存日历时间,并通过_time64()函数来获得日历时间(而不是通过使用32位字的time()函数),这样就可以通过该数据类型保存3001年1月1日0时0分0秒(不包括该时间点)之前的时间。

在time.h头文件中,我们还可以看到一些函数,它们都是以time_t为参数类型或返回值类型的函数:

double difftime(time_t time1, time_t time0);
time_t mktime(struct tm * timeptr);
time_t time(time_t * timer);
char * asctime(const struct tm * timeptr);
char * ctime(const time_t *timer);

此外,time.h还提供了两种不同的函数将日历时间(一个用time_t表示的整数)转换为我们平时看到的把年月日时分秒分开显示的时间格式tm:

struct tm * gmtime(const time_t *timer);                                        
struct tm * localtime(const time_t * timer);

通过查阅MSDN,我们可以知道Microsoft C/C++ 7.0中时间点的值(time_t对象的值)是从1899年12月31日0时0分0秒到该时间点所经过的秒数,而其它各种版本的Microsoft C/C++和所有不同版本的Visual C++都是计算的从1970年1月1日0时0分0秒到该时间点所经过的秒数。

4.与日期和时间相关的函数及应用
在本节,我将向大家展示怎样利用time.h中声明的函数对时间进行操作。这些操作包括取当前时间、计算时间间隔、以不同的形式显示时间等内容。

4.1 获得日历时间

我们可以通过time()函数来获得日历时间(Calendar Time),其原型为:

time_t time(time_t * timer);

如果你已经声明了参数timer,你可以从参数timer返回现在的日历时间,同时也可以通过返回值返回现在的日历时间,即从一个时间点(例如:1970年1月1日0时0分0秒)到现在此时的秒数。如果参数为空(NUL),函数将只通过返回值返回现在的日历时间,比如下面这个例子用来显示当前的日历时间:

#include “time.h”
#include “stdio.h”
int main(void)
{
struct tm *ptr;
time_t lt;
lt =time(NUL);
printf(”The Calendar Time now is %d\n”,lt);
return 0;
}

运行的结果与当时的时间有关,我当时运行的结果是:

The Calendar Time now is 1122707619

其中1122707619就是我运行程序时的日历时间。即从1970年1月1日0时0分0秒到此时的秒数。

4.2 获得日期和时间

这里说的日期和时间就是我们平时所说的年、月、日、时、分、秒等信息。从第2节我们已经知道这些信息都保存在一个名为tm的结构体中,那么如何将一个日历时间保存为一个tm结构的对象呢?

其中可以使用的函数是gmtime()和localtime(),这两个函数的原型为:

struct tm * gmtime(const time_t *timer);                                        
struct tm * localtime(const time_t * timer);

其中gmtime()函数是将日历时间转化为世界标准时间(即格林尼治时间),并返回一个tm结构体来保存这个时间,而localtime()函数是将日历时间转化为本地时间。比如现在用gmtime()函数获得的世界标准时间是2005年7月30日7点18分20秒,那么我用localtime()函数在中国地区获得的本地时间会比世界标准时间晚8个小时,即2005年7月30日15点18分20秒。下面是个例子:

#include “time.h”
#include “stdio.h”
int main(void)
{
struct tm *local;
time_t t;
t=time(NUL);
local=localtime(&t);
printf(”Local hour is: %d\n”,local->tm_hour);
local=gmtime(&t);
printf(”UTC hour is: %d\n”,local->tm_hour);
return 0;
}

运行结果是:

Local hour is: 15
UTC hour is: 7

4.3 固定的时间格式

我们可以通过asctime()函数和ctime()函数将时间以固定的格式显示出来,两者的返回值都是char*型的字符串。返回的时间格式为:

星期几 月份 日期 时:分:秒 年\n
例如:Wed Jan 02 02:03:55 1980\n

其中\n是一个换行符,是一个空字符,表示字符串结束。下面是两个函数的原型:

char * asctime(const struct tm * timeptr);
char * ctime(const time_t *timer);

其中asctime()函数是通过tm结构来生成具有固定格式的保存时间信息的字符串,而ctime()是通过日历时间来生成时间字符串。这样的话,asctime()函数只是把tm结构对象中的各个域填到时间字符串的相应位置就行了,而ctime()函数需要先参照本地的时间设置,把日历时间转化为本地时间,然后再生成格式化后的字符串。在下面,如果t是一个非空的time_t变量的话,那么:

printf(ctime(&t));

等价于:

struct tm *ptr;
ptr=localtime(&t);
printf(asctime(ptr));

那么,下面这个程序的两条printf语句输出的结果就是不同的了(除非你将本地时区设为世界标准时间所在的时区):

#include “time.h”
#include “stdio.h”
int main(void)
{
struct tm *ptr;
time_t lt;
lt =time(NUL);
ptr=gmtime(

运行结果:

Sat Jul 30 08:43:03 2005
Sat Jul 30 16:43:03 2005

4.4 自定义时间格式

我们可以使用strftime()函数将时间格式化为我们想要的格式。它的原型如下:

size_t strftime(
   char *strDest,
   size_t maxsize,
   const char *format,
   const struct tm *timeptr
);

我们可以根据format指向字符串中格式命令把timeptr中保存的时间信息放在strDest指向的字符串中,最多向strDest中存放maxsize个字符。该函数返回向strDest指向的字符串中放置的字符数。

函数strftime()的操作有些类似于sprintf():识别以百分号(%)开始的格式命令集合,格式化输出结果放在一个字符串中。格式化命令说明串strDest中各种日期和时间信息的确切表示方法。格式串中的其他字符原样放进串中。格式命令列在下面,它们是区分大小写的。

%a 星期几的简写
%A 星期几的全称
%b 月分的简写
%B 月份的全称
%c 标准的日期的时间串
%C 年份的后两位数字
%d 十进制表示的每月的第几天
%D 月/天/年
%e 在两字符域中,十进制表示的每月的第几天
%F 年-月-日
%g 年份的后两位数字,使用基于周的年
%G 年分,使用基于周的年
%h 简写的月份名
%H 24小时制的小时
%I 12小时制的小时
%j 十进制表示的每年的第几天
%m 十进制表示的月份
%M 十时制表示的分钟数
%n 新行符
%p 本地的AM或PM的等价显示
%r 12小时的时间
%R 显示小时和分钟:hh:mm
%S 十进制的秒数
%t 水平制表符
%T 显示时分秒:hh:mm:ss
%u 每周的第几天,星期一为第一天 (值从0到6,星期一为0)
%U 第年的第几周,把星期日做为第一天(值从0到53)
%V 每年的第几周,使用基于周的年
%w 十进制表示的星期几(值从0到6,星期天为0)
%W 每年的第几周,把星期一做为第一天(值从0到53)
%x 标准的日期串
%X 标准的时间串
%y 不带世纪的十进制年份(值从0到99)
%Y 带世纪部分的十进制年份
%z,%Z 时区名称,如果不能得到时区名称则返回空字符。
%% 百分号

如果想显示现在是几点了,并以12小时制显示,就象下面这段程序:

#include “time.h”
#include “stdio.h”
int main(void)
{
struct tm *ptr;
time_t lt;
char str[80];
lt=time(NUL);
ptr=localtime(

其运行结果为:
It is now 4PM

而下面的程序则显示当前的完整日期:

#include
#include

void main( void )
{
        struct tm *newtime;
        char tmpbuf[128];
        time_t lt1;
        time(

运行结果:

Today is Saturday, day 30 of July in the year 2005.

4.5 计算持续时间的长度

有时候在实际应用中要计算一个事件持续的时间长度,比如计算打字速度。在第1节计时部分中,我已经用clock函数举了一个例子。Clock()函数可以精确到毫秒级。同时,我们也可以使用difftime()函数,但它只能精确到秒。该函数的定义如下:

double difftime(time_t time1, time_t time0);

虽然该函数返回的以秒计算的时间间隔是double类型的,但这并不说明该时间具有同double一样的精确度,这是由它的参数觉得的(time_t是以秒为单位计算的)。比如下面一段程序:

#include “time.h”
#include “stdio.h”
#include “stdlib.h”
int main(void)
{
time_t start,end;
start = time(NUL);
system(”pause”);
end = time(NUL);
printf(”The pause used %f seconds.\n”,difftime(end,start));//

运行结果为:
请按任意键继续. . .
The pause used 2.000000 seconds.
请按任意键继续. . .

可以想像,暂停的时间并不那么巧是整整2秒钟。其实,你将上面程序的带有“//printf(”The pause used %f seconds.\n”,end-start);

其运行结果是一样的。

4.6 分解时间转化为日历时间

这里说的分解时间就是以年、月、日、时、分、秒等分量保存的时间结构,在C/C++中是tm结构。我们可以使用mktime()函数将用tm结构表示的时间转化为日历时间。其函数原型如下:

time_t mktime(struct tm * timeptr);

其返回值就是转化后的日历时间。这样我们就可以先制定一个分解时间,然后对这个时间进行操作了,下面的例子可以计算出1997年7月1日是星期几:

#include “time.h”
#include “stdio.h”
#include “stdlib.h”
int main(void)
{
struct tm t;
time_t t_of_day;
t.tm_year=1997-1900;
t.tm_mon=6;
t.tm_mday=1;
t.tm_hour=0;
t.tm_min=0;
t.tm_sec=1;
t.tm_isdst=0;
t_of_day=mktime(&t);
printf(ctime(&t_of_day));
return 0;
}

运行结果:

Tue Jul 01 00:00:01 1997

现在注意了,有了mktime()函数,是不是我们可以操作现在之前的任何时间呢?你可以通过这种办法算出1945年8月15号是星期几吗?答案是否定的。因为这个时间在1970年1月1日之前,所以在大多数编译器中,这样的程序虽然可以编译通过,但运行时会异常终止。

这是在我毕业设计中使用到的一个函数,用来将当前日期时间转换成字符串并返回的代码:

char* URL::GetTime()
{

 time_t t;
 int nLen;
 if(pszTime==NULL)
  pszTime=new char[22];
 t=time(NULL);
 ptime=localtime(&t);
 ptime->tm_year+=1900;  //日期是从1900开始计算的
 ptime->tm_mon+=1;  //月份是从0开始的
 sprintf(pszTime+1,”%04d-%02d-%02d %02d:%02d:%02d”,ptime->tm_year,ptime->tm_mon,ptime->tm_mday
             ,ptime->tm_hour,ptime->tm_min,ptime->tm_sec);
 pszTime[0]=’\’’;
 nLen=strlen(pszTime);
 pszTime[nLen]=’\’’;
 pszTime[nLen+1]=’’;
 return pszTime;
}

5.总结

本文介绍了标准C/C++中的有关日期和时间的概念,并通过各种实例讲述了这些函数和数据结构的使用方法。笔者认为,和时间相关的一些概念是相当重要的,理解这些概念是理解各种时间格式的转换的基础,更是应用这些函数和数据结构的基础。

const修饰指针和引用的用法,对于初学C++的人直是讳莫如深,不知所云.一旦你了解了其用法,一切便不值一哂了.下面我为读者一一释疑:
   大致说来其可分为三种情况: const修饰指针,const修饰引用,const修饰指针的引用.
1.const修饰指针
   const修饰指针又可分为三种情况:
     const修饰指针本身
     const修饰指针所指的变量(或对象)
     const修饰指针本身和指针所指的变量(或对象)
(1).const修饰指针本身
    这种情形下,指针本身为常量,不可改变,任何修改指针本身的行为都是非法的.例如:
const int a = 1;
const int b = 2;
int i = 3;
int j = 4;
int* const pi = &i; //ok, pi的类型为int* const , &i的类型为int* const
int* const pi = &a; //error, pi的类型为int* const, &a的类型为const int* const
pi = &j; //error, 指针是常量,不可变
*pi = a; //ok, *pi并没有限定是常量 ,可变
    由此看出,pi是常量,常量在初始化和赋值时,类型必须严格一致。也就是
const修饰指针本身时,=号两边的变量类型必须严格一致,否则不能匹配。
(2).const修饰指针指向的变量(或对象)
    此种情形下,通过间接引用指针不可改变变量的值,假设指针为p,则*p不可变,下面以例子说明:
const int *pi = &a;
//or int const *pi = &a;
//两者毫无二致,不过BS喜欢前者,这也没什么技术上的优劣之分,也就是说const int与int const可以互换.建议大家熟
//悉这两种形式,为简洁便,以后统统用前者.
//ok, const并不修饰指针本身,pi对赋值类型
//没要求 ,但pi是int*型指针,所以所赋的必须是个地址值。
const int *pi = &i; //ok ,pi可赋值常量的地址,又可赋变量的地址
const int *pi1 = &a;
const int *pi = pi1; //ok
*pi = j; //error,*pi 不可变,不能更改指针的间接引用形式
pi = &j; //ok,pi可变
pi = &b; //ok,pi可变
pi++; //ok
–pi; //ok
     由此可见,pi是变量,可以赋值常量和变量的值,正如一个整型变量可赋整型数和整型变量一样.const修饰的不是指针本身,而是其间接引用,=号两边的类型不必严格匹配,如:const int* pi = &a;中,pi的类型为int*,而&a的类型为const int* const,只要其中含有int* 就可以。又如:const int *pi = &j;中,pi的类型为int*,而&j的类型为int* const,它向pi赋值并无大碍。
(3)const修饰指针本身和指针所指的变量(或对象)
     设有指针p,此种情形下,p和*p都不可变.举例如下:
const int* const pi = &a;
//or int const* const pi = &a;
//将const pi看作一体,就与(2)所述相同,只是要求pi必须为const,正如上所说,=号两边的类型不必严格匹配,但必须含有int*, &a的类型为const int* const,含有int*, 所以可以赋值。
const int* const pi = &i; //ok, &i类型为int* const,含有int*, 可赋值。
const int *pi1 = &j;
const int *const pi = pi1; //ok,  pi1类型为int*
pi = &b; //error, pi不可变
pi = &j; //error, pi不可变
*pi = b; //error, *pi不可变
*pi = j; //error, *pi不可变
pi++; //error ,pi不可变
++i; //ok, =号右边的变量(或对象)与所修饰的变量无关
a–; //error, a为const
     这种情况,跟以上两种情形有联系。对const int* const pi = &a;我们可以这样看:const int*( const pi )= &a;(仅仅是表达需要),将const pi看作一体,就与上述分类(2)符合。只要含有int*便可.
 
2.const修饰引用
    这种情况比较简单,没有象修饰指针那样繁复,因为引用和引用对象是一体的,所以引用被const修饰只有一种类型。
const修饰引用,引用本身不可变,但引用的变量(或对象)可以改变.例如:
const int& ri = a; //or int const & ri = a; ok, ri 本身是常量,引用不区分类型
const int& ri = i; //ok,引用不区分类型
ri++; //error, ri为常量,不可变
i++; //ok,=右边的变量与引用无关
ri=b; //error, ri为常量
i=j; //ok,=右边的变量与引用无关
int & const ri = i; //error,不存在这种形式,没有意义
 
3.const修饰指针的引用
    引用只是个别名,这里与修饰指针类似,又分为三种情况:
(1)
     先给个例子:
const int *pi = &a;
const int *&ri = pi;
//or int const *&ri = pi;
    引用是引用对象的别名,正因为如此,ri是pi的别名,所以ri的类型必须与pi完全一致才行。这里pi的类型为int*,ri的类型也为int*,赋值可行。若const int *&ri = &a;正不正确?分析一下就知晓。ri类型为int*,&a的类型则为const int* const不匹配。
const int *&ri = &i; //error,类型不匹配,一为int*,一为int* const
ri = &a; //ok
ri = &i; //ok
const int *pi1=&a;
const int *pi2=&i;
ri = pi1; //ok
ri = pi2; //ok
*ri = i; //error
*ri = a; //error
     注意这与1-(2)的区别.
(2)
     用例子说明:
int *const &ri = &i;
    去掉ri左边的&号,则为int *const ri,因为ri是别名,故ri的类型应与赋值的数类型一致,ri类型为int *const,&i为int *const,可以这么做.
int *const &ri = pi; //error,类型不合,一为int *const ,一为int *
int *const &ri = &a; //error,类型不合,一为int *const,一为const int* const
(*ri)++; //ok
i++; //ok
ri = &i; //error
   这种情况下,ri为常量,不可更改.
(3)
     用例子说明:
const int* pi = &j;
const int* const &ri = pi; //or int const * const &ri = pi;ok
const int* const &ri = &i; //ok
     ri是pi的别名,pi的类型应与ri一致。拿掉&,得const int* const ri ,把const  ri看作一体,很容易得出ri的类型信息,就象前面2-(3)所讨论的一样,可以得到赋给ri的只要含有类型int* 即可。pi的类型为int*,&i的类型为int* const ,可以这么做.
const int * const &ri = &a; //ok
ri++;  //error
*ri = 6;  //error
    言尽于此,希望对初学者有所助益!

1、什么是sizeof

    首先看一下sizeof在msdn上的定义:

    The sizeof keyword gives the amount of storage, in bytes, associated with a variable or a type (including aggregate types). This keyword returns a value of type size_t.

    看到return这个字眼,是不是想到了函数?错了,sizeof不是一个函数,你见过给一个函数传参数,而不加括号的吗?sizeof可以,所以sizeof不是函数。网上有人说sizeof是一元操作符,但是我并不这么认为,因为sizeof更像一个特殊的宏,它是在编译阶段求值的。举个例子:
 
 cout<<sizeof(int)<<endl; // 32位机上int长度为4
 cout<<sizeof(1==2)<<endl; // == 操作符返回bool类型,相当于 cout<<sizeof(bool)<<endl;

    在编译阶段已经被翻译为:

 cout<<4<<endl;
 cout<<1<<endl;

    这里有个陷阱,看下面的程序:

 int a = 0;
 cout<<sizeof(a=3)<<endl;
 cout<<a<<endl;

    输出为什么是4,0而不是期望中的4,3???就在于sizeof在编译阶段处理的特性。由于sizeof不能被编译成机器码,所以sizeof作用范围内,也就是()里面的内容也不能被编译,而是被替换成类型。=操作符返回左操作数的类型,所以a=3相当于int,而代码也被替换为:

 int a = 0;
 cout<<4<<endl;
 cout<<a<<endl;

    所以,sizeof是不可能支持链式表达式的,这也是和一元操作符不一样的地方。

    结论:不要把sizeof当成函数,也不要看作一元操作符,把他当成一个特殊的编译预处理。

2、sizeof的用法

    sizeof有两种用法:
 
    (1)sizeof(object)
    也就是对对象使用sizeof,也可以写成sizeof object 的形式。例如:

    (2)sizeof(typename)
    也就是对类型使用sizeof,注意这种情况下写成sizeof typename是非法的。下面举几个例子说明一下:

 
 int i = 2;
 cout<<sizeof(i)<<endl; // sizeof(object)的用法,合理
 cout<<sizeof i<<endl; // sizeof object的用法,合理
 cout<<sizeof 2<<endl; // 2被解析成int类型的object, sizeof object的用法,合理
 cout<<sizeof(2)<<endl; // 2被解析成int类型的object, sizeof(object)的用法,合理
 cout<<sizeof(int)<<endl;// sizeof(typename)的用法,合理
 cout<<sizeof int<<endl; // 错误!对于操作符,一定要加()

    可以看出,加()是永远正确的选择。

    结论:不论sizeof要对谁取值,最好都加上()。

3、数据类型的sizeof

(1)C++固有数据类型

    32位C++中的基本数据类型,也就char,short int(short),int,long int(long),float,double, long double
大小分别是:1,2,4,4,4,8, 10。

    考虑下面的代码:

 cout<<sizeof(unsigned int) == sizeof(int)<<endl; // 相等,输出 1

    unsigned影响的只是最高位bit的意义,数据长度不会被改变的。

    结论:unsigned不能影响sizeof的取值。

(2)自定义数据类型

    typedef可以用来定义C++自定义类型。考虑下面的问题:

 typedef short WORD;
 typedef long DWORD;
 cout<<(sizeof(short) == sizeof(WORD))<<endl; // 相等,输出1
 cout<<(sizeof(long) == sizeof(DWORD))<<endl; // 相等,输出1

    结论:自定义类型的sizeof取值等同于它的类型原形。

(3)函数类型

    考虑下面的问题:

 int f1(){return 0;};
 double f2(){return 0.0;}
 void f3(){}

 cout<<sizeof(f1())<<endl; // f1()返回值为int,因此被认为是int
 cout<<sizeof(f2())<<endl; // f2()返回值为double,因此被认为是double
 cout<<sizeof(f3())<<endl; // 错误!无法对void类型使用sizeof
 cout<<sizeof(f1)<<endl;  // 错误!无法对函数指针使用sizeof   
 cout<<sizeof*f2<<endl;  // *f2,和f2()等价,因为可以看作object,所以括号不是必要的。被认为是double

    结论:对函数使用sizeof,在编译阶段会被函数返回值的类型取代,

4、指针问题

    考虑下面问题:
 
 cout<<sizeof(string*)<<endl; // 4
 cout<<sizeof(int*)<<endl; // 4
 cout<<sizof(char****)<<endl; // 4

    可以看到,不管是什么类型的指针,大小都是4的,因为指针就是32位的物理地址。

    结论:只要是指针,大小就是4。(64位机上要变成8也不一定)。

    顺便唧唧歪歪几句,C++中的指针表示实际内存的地址。和C不一样的是,C++中取消了模式之分,也就是不再有small,middle,big,取而代之的是统一的flat。flat模式采用32位实地址寻址,而不再是c中的 segment:offset模式。举个例子,假如有一个指向地址 f000:8888的指针,如果是C类型则是8888(16位, 只存储位移,省略段),far类型的C指针是f0008888(32位,高位保留段地址,地位保留位移),C++类型的指针是f8888(32位,相当于段地址*16 + 位移,但寻址范围要更大)。

5、数组问题

    考虑下面问题:

 char a[] = "abcdef";
 int b[20] = {3, 4};
 char c[2][3] = {"aa", "bb"};
 

 cout<<sizeof(a)<<endl; // 7
 cout<<sizeof(b)<<endl; // 20
 cout<<sizeof(c)<<endl; // 6
 

    数组a的大小在定义时未指定,编译时给它分配的空间是按照初始化的值确定的,也就是7。c是多维数组,占用的空间大小是各维数的乘积,也就是6。可以看出,数组的大小就是他在编译时被分配的空间,也就是各维数的乘积*数组元素的大小。

    结论:数组的大小是各维数的乘积*数组元素的大小。

    这里有一个陷阱:

 int *d = new int[10];

 cout<<sizeof(d)<<endl; // 4

    d是我们常说的动态数组,但是他实质上还是一个指针,所以sizeof(d)的值是4。

    再考虑下面的问题:

 double* (*a)[3][6];
 
 cout<<sizeof(a)<<endl;  // 4
 cout<<sizeof(*a)<<endl;  // 72
 cout<<sizeof(**a)<<endl; // 24
 cout<<sizeof(***a)<<endl; // 4
 cout<<sizeof(****a)<<endl; // 8

    a是一个很奇怪的定义,他表示一个指向 double*[3][6]类型数组的指针。既然是指针,所以sizeof(a)就是4。

    既然a是执行double*[3][6]类型的指针,*a就表示一个double*[3][6]的多维数组类型,因此sizeof(*a)=3*6*sizeof(double*)=72。同样的,**a表示一个double*[6]类型的数组,所以sizeof(**a)=6*sizeof(double*)=24。***a就表示其中的一个元素,也就是double*了,所以sizeof(***a)=4。至于****a,就是一个double了,所以sizeof(****a)=sizeof(double)=8。

6、向函数传递数组的问题。

    考虑下面的问题:
#include <iostream>
using namespace std;

int Sum(int i[])
{
 int sumofi = 0;
 for (int j = 0; j < sizeof(i)/sizeof(int); j++) //实际上,sizeof(i) = 4
 {
  sumofi += i[j];
 }
 return sumofi;
}

int main()
{
 int allAges[6] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(allAges)<<endl;
 system("pause");
 return 0;
}

    Sum的本意是用sizeof得到数组的大小,然后求和。但是实际上,传入自函数Sum的,只是一个int 类型的指针,所以sizeof(i)=4,而不是24,所以会产生错误的结果。解决这个问题的方法使是用指针或者引用。

    使用指针的情况:
int Sum(int (*i)[6])
{
 int sumofi = 0;
 for (int j = 0; j < sizeof(*i)/sizeof(int); j++) //sizeof(*i) = 24
 {
  sumofi += (*i)[j];
 }
 return sumofi;
}

int main()
{
 int allAges[] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(&allAges)<<endl;
 system("pause");
 return 0;
}
    在这个Sum里,i是一个指向i[6]类型的指针,注意,这里不能用int Sum(int (*i)[])声明函数,而是必须指明要传入的数组的大小,不然sizeof(*i)无法计算。但是在这种情况下,再通过sizeof来计算数组大小已经没有意义了,因为此时大小是指定为6的。
使用引用的情况和指针相似:

int Sum(int (&i)[6])
{
 int sumofi = 0;
 for (int j = 0; j < sizeof(i)/sizeof(int); j++)
 {
  sumofi += i[j];
 }
 return sumofi;
}

int main()
{
 int allAges[] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(allAges)<<endl;
 system("pause");
 return 0;
}
    这种情况下sizeof的计算同样无意义,所以用数组做参数,而且需要遍历的时候,函数应该有一个参数来说明数组的大小,而数组的大小在数组定义的作用域内通过sizeof求值。因此上面的函数正确形式应该是:
#include <iostream>
using namespace std;

int Sum(int *i, unsigned int n)
{
 int sumofi = 0;
 for (int j = 0; j < n; j++)
 {
  sumofi += i[j];
 }
 return sumofi;
}

int main()
{
 int allAges[] = {21, 22, 22, 19, 34, 12};
 cout<<Sum(i, sizeof(allAges)/sizeof(int))<<endl;
 system("pause");
 return 0;
}

7、字符串的sizeof和strlen

    考虑下面的问题:

 char a[] = "abcdef";
 char b[20] = "abcdef";
 string s = "abcdef";

 cout<<strlen(a)<<endl;  // 6,字符串长度
 cout<<sizeof(a)<<endl;  // 7,字符串容量
 cout<<strlen(b)<<endl;  // 6,字符串长度
 cout<<strlen(b)<<endl;  // 20,字符串容量
 cout<<sizeof(s)<<endl;  // 12, 这里不代表字符串的长度,而是string类的大小
 cout<<strlen(s)<<endl;  // 错误!s不是一个字符指针。

 a[1] = ‘\0′;
 cout<<strlen(a)<<endl;  // 1
 cout<<sizeof(a)<<endl;  // 7,sizeof是恒定的

    strlen是寻找从指定地址开始,到出现的第一个0之间的字符个数,他是在运行阶段执行的,而sizeof是得到数据的大小,在这里是得到字符串的容量。所以对同一个对象而言,sizeof的值是恒定的。string是C++类型的字符串,他是一个类,所以sizeof(s)表示的并不是字符串的长度,而是类string的大小。strlen(s)根本就是错误的,因为strlen的参数是一个字符指针,如果想用strlen得到s字符串的长度,应该使用sizeof(s.c_str()),因为string的成员函数c_str()返回的是字符串的首地址。实际上,string类提供了自己的成员函数来得到字符串的容量和长度,分别是Capacity()和Length()。string封装了常用了字符串操作,所以在C++开发过程中,最好使用string代替C类型的字符串。

8、从union的sizeof问题看cpu的对界

    考虑下面问题:(默认对齐方式)

 union u
 {
  double a;
  int b;
 };

 union u2
 {
  char a[13];
  int b;
 };

 union u3
 {
  char a[13];
  char b;
 };

 cout<<sizeof(u)<<endl;  // 8
 cout<<sizeof(u2)<<endl;  // 16
 cout<<sizeof(u3)<<endl;  // 13

    都知道union的大小取决于它所有的成员中,占用空间最大的一个成员的大小。所以对于u来说,大小就是最大的double类型成员a了,所以sizeof(u)=sizeof(double)=8。但是对于u2和u3,最大的空间都是char[13]类型的数组,为什么u3的大小是13,而u2是16呢?关键在于u2中的成员int b。由于int类型成员的存在,使u2的对齐方式变成4,也就是说,u2的大小必须在4的对界上,所以占用的空间变成了16(最接近13的对界)。

    结论:复合数据类型,如union,struct,class的对齐方式为成员中对齐方式最大的成员的对齐方式。

    顺便提一下CPU对界问题,32的C++采用8位对界来提高运行速度,所以编译器会尽量把数据放在它的对界上以提高内存命中率。对界是可以更改的,使用#pragma pack(x)宏可以改变编译器的对界方式,默认是8。C++固有类型的对界取编译器对界方式与自身大小中较小的一个。例如,指定编译器按2对界,int类型的大小是4,则int的对界为2和4中较小的2。在默认的对界方式下,因为几乎所有的数据类型都不大于默认的对界方式8(除了long double),所以所有的固有类型的对界方式可以认为就是类型自身的大小。更改一下上面的程序:

 #pragma pack(2)
 union u2
 {
  char a[13];
  int b;
 };

 union u3
 {
  char a[13];
  char b;
 };
 #pragma pack(8)

 cout<<sizeof(u2)<<endl;  // 14
 cout<<sizeof(u3)<<endl;  // 13

    由于手动更改对界方式为2,所以int的对界也变成了2,u2的对界取成员中最大的对界,也是2了,所以此时sizeof(u2)=14。

    结论:C++固有类型的对界取编译器对界方式与自身大小中较小的一个。

9、struct的sizeof问题

    因为对齐问题使结构体的sizeof变得比较复杂,看下面的例子:(默认对齐方式下)

 struct s1
 {
  char a;
  double b;
  int c;
  char d; 
 };

 struct s2
 {
  char a;
  char b;
  int c;
  double d;
 };

 cout<<sizeof(s1)<<endl; // 24
 cout<<sizeof(s2)<<endl; // 16

    同样是两个char类型,一个int类型,一个double类型,但是因为对界问题,导致他们的大小不同。计算结构体大小可以采用元素摆放法,我举例子说明一下:首先,CPU判断结构体的对界,根据上一节的结论,s1和s2的对界都取最大的元素类型,也就是double类型的对界8。然后开始摆放每个元素。
    对于s1,首先把a放到8的对界,假定是0,此时下一个空闲的地址是1,但是下一个元素d是double类型,要放到8的对界上,离1最接近的地址是8了,所以d被放在了8,此时下一个空闲地址变成了16,下一个元素c的对界是4,16可以满足,所以c放在了16,此时下一个空闲地址变成了20,下一个元素d需要对界1,也正好落在对界上,所以d放在了20,结构体在地址21处结束。由于s1的大小需要是8的倍数,所以21-23的空间被保留,s1的大小变成了24。
    对于s2,首先把a放到8的对界,假定是0,此时下一个空闲地址是1,下一个元素的对界也是1,所以b摆放在1,下一个空闲地址变成了2;下一个元素c的对界是4,所以取离2最近的地址4摆放c,下一个空闲地址变成了8,下一个元素d的对界是8,所以d摆放在8,所有元素摆放完毕,结构体在15处结束,占用总空间为16,正好是8的倍数。

    这里有个陷阱,对于结构体中的结构体成员,不要认为它的对齐方式就是他的大小,看下面的例子:

 struct s1
 {
  char a[8];
 };

 struct s2
 {
  double d;
 };

 struct s3
 {
  s1 s;
  char a;
 };

 struct s4
 {
  s2 s;
  char a; 
 };

 cout<<sizeof(s1)<<endl; // 8
 cout<<sizeof(s2)<<endl; // 8
 cout<<sizeof(s3)<<endl; // 9
 cout<<sizeof(s4)<<endl; // 16;

    s1和s2大小虽然都是8,但是s1的对齐方式是1,s2是8(double),所以在s3和s4中才有这样的差异。

    所以,在自己定义结构体的时候,如果空间紧张的话,最好考虑对齐因素来排列结构体里的元素。

10、不要让double干扰你的位域

    在结构体和类中,可以使用位域来规定某个成员所能占用的空间,所以使用位域能在一定程度上节省结构体占用的空间。不过考虑下面的代码:

 struct s1
 {
  int i: 8;
  int j: 4;
  double b;
  int a:3;
 };

 struct s2
 {
  int i;
  int j;
  double b;
  int a;
 };

 struct s3
 {
  int i;
  int j;
  int a;
  double b;
 };

 struct s4
 {
  int i: 8;
  int j: 4;
  int a:3;
  double b;
 };

 cout<<sizeof(s1)<<endl;  // 24
 cout<<sizeof(s2)<<endl;  // 24
 cout<<sizeof(s3)<<endl;  // 24
 cout<<sizeof(s4)<<endl;  // 16

    可以看到,有double存在会干涉到位域(sizeof的算法参考上一节),所以使用位域的的时候,最好把float类型和double类型放在程序的开始或者最后。

引言

  毫无疑问,我们都看到过像 TCHAR, std::string, BSTR 等各种各样的字符串类型,还有那些以 _tcs 开头的奇怪的宏。你也许正在盯着显示器发愁。本指引将总结引进各种字符类型的目的,展示一些简单的用法,并告诉您在必要时,如何实现各种字符串类型之间的转换。
  在第一部分,我们将介绍3种字符编码类型。了解各种编码模式的工作方式是很重要的事情。即使你已经知道一个字符串是一个字符数组,你也应该阅读本部分。一旦你了解了这些,你将对各种字符串类型之间的关系有一个清楚地了解。
  在第二部分,我们将单独讲述string类,怎样使用它及实现他们相互之间的转换。

字符基础 — ASCII, DBCS, Unicode

  所有的 string 类都是以C-style字符串为基础的。C-style 字符串是字符数组。所以我们先介绍字符类型。这里有3种编码模式对应3种字符类型。第一种编码类型是单子节字符集(single-byte character set or SBCS)。在这种编码模式下,所有的字符都只用一个字节表示。ASCII是SBCS。一个字节表示的0用来标志SBCS字符串的结束。
  第二种编码模式是多字节字符集(multi-byte character set or MBCS)。一个MBCS编码包含一些一个字节长的字符,而另一些字符大于一个字节的长度。用在Windows里的MBCS包含两种字符类型,单字节字符(single-byte characters)和双字节字符(double-byte characters)。由于Windows里使用的多字节字符绝大部分是两个字节长,所以MBCS常被用DBCS代替。
  在DBCS编码模式中,一些特定的值被保留用来表明他们是双字节字符的一部分。例如,在Shift-JIS编码中(一个常用的日文编码模式),0×81-0×9f之间和 0xe0-oxfc之间的值表示"这是一个双字节字符,下一个子节是这个字符的一部分。"这样的值被称作"leading bytes",他们都大于0×7f。跟随在一个leading byte子节后面的字节被称作"trail byte"。在DBCS中,trail byte可以是任意非0值。像SBCS一样,DBCS字符串的结束标志也是一个单字节表示的0。
  第三种编码模式是Unicode。Unicode是一种所有的字符都使用两个字节编码的编码模式。Unicode字符有时也被称作宽字符,因为它比单子节字符宽(使用了更多的存储空间)。注意,Unicode不能被看作MBCS。MBCS的独特之处在于它的字符使用不同长度的字节编码。Unicode字符串使用两个字节表示的0作为它的结束标志。
  单字节字符包含拉丁文字母表,accented characters及ASCII标准和DOS操作系统定义的图形字符。双字节字符被用来表示东亚及中东的语言。Unicode被用在COM及Windows NT操作系统内部。
  你一定已经很熟悉单字节字符。当你使用char时,你处理的是单字节字符。双字节字符也用char类型来进行操作(这是我们将会看到的关于双子节字符的很多奇怪的地方之一)。Unicode字符用wchar_t来表示。Unicode字符和字符串常量用前缀L来表示。例如:

wchar_t wch = L”1”; // 2 bytes, 0×0031
wchar_t* wsz = L"Hello"; // 12 bytes, 6 wide characters
字符在内存中是怎样存储的

  单字节字符串:每个字符占一个字节按顺序依次存储,最后以单字节表示的0结束。例如。"Bob"的存贮形式如下:

42 6F 62 00
B o b BOS

Unicode的存储形式,L"Bob"

42 00  6F 00 62 00 00 00
B o b BOS

使用两个字节表示的0来做结束标志。

  一眼看上去,DBCS 字符串很像 SBCS 字符串,但是我们一会儿将看到 DBCS 字符串的微妙之处,它使得使用字符串操作函数和永字符指针遍历一个字符串时会产生预料之外的结果。字符串" " ("nihongo")在内存中的存储形式如下(LB和TB分别用来表示 leading byte 和 trail byte)

93 FA 96 7B 8C EA 00
LB TB LB TB LB TB EOS
  EOS

值得注意的是,"ni"的值不能被解释成WORD型值0xfa93,而应该看作两个值93和fa以这种顺序被作为"ni"的编码。

使用字符串处理函数

  我们都已经见过C语言中的字符串函数,strcpy(), sprintf(), atoll()等。这些字符串只应该用来处理单字节字符字符串。标准库也提供了仅适用于Unicode类型字符串的函数,比如wcscpy(), swprintf(), wtol()等。
  微软还在它的CRT(C runtime library)中增加了操作DBCS字符串的版本。Str***()函数都有对应名字的DBCS版本_mbs***()。如果你料到可能会遇到DBCS字符串(如果你的软件会被安装在使用DBCS编码的国家,如中国,日本等,你就可能会),你应该使用_mbs***()函数,因为他们也可以处理SBCS字符串。(一个DBCS字符串也可能含有单字节字符,这就是为什么_mbs***()函数也能处理SBCS字符串的原因)
  让我们来看一个典型的字符串来阐明为什么需要不同版本的字符串处理函数。我们还是使用前面的Unicode字符串 L"Bob":

42 00  6F 00 62 00 00 00
B o b BOS

  因为x86CPU是little-endian,值0×0042在内存中的存储形式是42 00。你能看出如果这个字符串被传给strlen()函数会出现什么问题吗?它将先看到第一个字节42,然后是00,而00是字符串结束的标志,于是strlen()将会返回1。如果把"Bob"传给wcslen(),将会得出更坏的结果。wcslen()将会先看到0×6f42,然后是0×0062,然后一直读到你的缓冲区的末尾,直到发现00 00结束标志或者引起了GPF。
  到目前为止,我们已经讨论了str***()和wcs***()的用法及它们之间的区别。Str***()和_mbs**()之间的有区别区别呢?明白他们之间的区别,对于采用正确的方法来遍历DBCS字符串是很重要的。下面,我们将先介绍字符串的遍历,然后回到str***()与_mbs***()之间的区别这个问题上来。

正确的遍历和索引字符串

  因为我们中大多数人都是用着SBCS字符串成长的,所以我们在遍历字符串时,常常使用指针的++-和-操作。我们也使用数组下标的表示形式来操作字符串中的字符。这两种方式是用于SBCS和Unicode字符串,因为它们中的字符有着相同的宽度,编译器能正确的返回我们需要的字符。
  然而,当碰到DBCS字符串时,我们必须抛弃这些习惯。这里有使用指针遍历DBCS字符串时的两条规则。违背了这两条规则,你的程序就会存在DBCS有关的bugs。

1.在前向遍历时,不要使用++操作,除非你每次都检查lead byte;
2.永远不要使用-操作进行后向遍历。
  我们先来阐述规则2,因为找到一个违背它的真实的实例代码是很容易的。假设你有一个程序在你自己的目录里保存了一个设置文件,你把安装目录保存在注册表中。在运行时,你从注册表中读取安装目录,然后合成配置文件名,接着读取该文件。假设,你的安装目录是C:\Program Files\MyCoolApp,那么你合成的文件名应该是C:\Program Files\MyCoolApp\config.bin。当你进行测试时,你发现程序运行正常。
  现在,想象你合成文件名的代码可能是这样的:

bool GetConfigFileName ( char* pszName, size_t nBuffSize )
{
   char szConfigFilename[MAX_PATH];

   // Read install dir from registry… we”ll assume it succeeds.

   // Add on a backslash if it wasn”t present in the registry value.
   // First, get a pointer to the terminating zero.
   char* pLastChar = strchr ( szConfigFilename, ”{post.abstract}” );

   // Now move it back one character.
   pLastChar–;  

   if ( *pLastChar != ”\” )
       strcat ( szConfigFilename, "\" );

   // Add on the name of the config file.
   strcat ( szConfigFilename, "config.bin" );

   // If the caller’’s buffer is big enough, return the filename.
   if ( strlen ( szConfigFilename ) >= nBuffSize )
       return false;
   else
       {
       strcpy ( pszName, szConfigFilename );
       return true;
       }
}      
  这是一段很健壮的代码,然而在遇到 DBCS 字符时它将会出错。让我们来看看为什么。假设一个日本用户使用了你的程序,把它安装在 C:\。下面是这个名字在内存中的存储形式:
 43 3A 5C 83 88 83 45 83 52 83 5C 00
      LB TB  LB TB  LB TB  LB TB   
C : \     EOS

  当使用 GetConfigFileName() 检查尾部的”\”时,它寻找安装目录名中最后的非0字节,看它是等于”\”的,所以没有重新增加一个”\”。结果是代码返回了错误的文件名。
  哪里出错了呢?看看上面两个被用蓝色高量显示的字节。斜杠”\”的值是0×5c。” ”的值是83 5c。上面的代码错误的读取了一个 trail byte,把它当作了一个字符。
  正确的后向遍历方法是使用能够识别DBCS字符的函数,使指针移动正确的字节数。下面是正确的代码。(指针移动的地方用红色标明)

bool FixedGetConfigFileName ( char* pszName, size_t nBuffSize )
{
   char szConfigFilename[MAX_PATH];

   // Read install dir from registry… we”ll assume it succeeds.

   // Add on a backslash if it wasn”t present in the registry value.
   // First, get a pointer to the terminating zero.
   char* pLastChar = _mbschr ( szConfigFilename, ”{post.abstract}” );

   // Now move it back one double-byte character.
   pLastChar = CharPrev ( szConfigFilename, pLastChar );

   if ( *pLastChar != ”\” )
       _mbscat ( szConfigFilename, "\" );

   // Add on the name of the config file.
   _mbscat ( szConfigFilename, "config.bin" );

    // If the caller’’s buffer is big enough, return the filename.
   if ( _mbslen ( szInstallDir ) >= nBuffSize )
       return false;
   else
       {
       _mbscpy ( pszName, szConfigFilename );
       return true;
       }
}

  上面的函数使用CharPrev() API使pLastChar向后移动一个字符,这个字符可能是两个字节长。在这个版本里,if条件正常工作,因为lead byte永远不会等于0×5c。
  让我们来想象一个违背规则1的场合。例如,你可能要检测一个用户输入的文件名是否多次出现了”:”。如果,你使用++操作来遍历字符串,而不是使用CharNext(),你可能会发出不正确的错误警告如果恰巧有一个trail byte它的值的等于”:”的值。
与规则2相关的关于字符串索引的规则:2a. 永远不要使用减法去得到一个字符串的索引。
违背这条规则的代码和违背规则2的代码很相似。例如,

char* pLastChar = &szConfigFilename [strlen(szConfigFilename) - 1];
这和向后移动一个指针是同样的效果。

回到关于str***()和_mbs***()的区别

  现在,我们应该很清楚为什么_mbs***()函数是必需的。Str***()函数根本不考虑DBCS字符,而_mbs***()考虑。如果,你调用strrchr("C:\ ", ”\”),返回结果可能是错误的,然而_mbsrchr()将会认出最后的双字节字符,返回一个指向真的”\”的指针。
  关于字符串函数的最后一点:str***()和_mbs***()函数认为字符串的长度都是以char来计算的。所以,如果一个字符串包含3个双字节字符,_mbslen()将会返回6。Unicode函数返回的长度是按wchar_t来计算的。例如,wcslen(L"Bob")返回3。

Win32 API中的MBCS和Unicode

两组 APIs:
  尽管你也许从来没有注意过,Win32中的每个与字符串相关的API和message都有两个版本。一个版本接受MBCS字符串,另一个接受Unicode字符串。例如,根本没有SetWindowText()这个API,相反,有SetWindowTextA()和SetWindowTextW()。后缀A表明这是MBCS函数,后缀W表示这是Unicode版本的函数。
  当你 build 一个 Windows 程序,你可以选择是用 MBCS 或者 Unicode APIs。如果,你曾经用过VC向导并且没有改过预处理的设置,那表明你用的是MBCS版本。那么,既然没有 SetWindowText() API,我们为什么可以使用它呢?winuser.h头文件包含了一些宏,例如:

BOOL WINAPI SetWindowTextA ( HWND hWnd, LPCSTR lpString );
BOOL WINAPI SetWindowTextW ( HWND hWnd, LPCWSTR lpString );

#ifdef UNICODE
#define SetWindowText  SetWindowTextW
#else
#define SetWindowText  SetWindowTextA
#endif      
当使用MBCS APIs来build程序时,UNICODE没有被定义,所以预处理器看到:#define SetWindowText SetWindowTextA
  这个宏定义把所有对SetWindowText的调用都转换成真正的API函数SetWindowTextA。(当然,你可以直接调用SetWindowTextA() 或者 SetWindowTextW(),虽然你不必那么做。)
  所以,如果你想把默认使用的API函数变成Unicode版的,你可以在预处理器设置中,把_MBCS从预定义的宏列表中删除,然后添加UNICODE和_UNICODE。(你需要两个都定义,因为不同的头文件可能使用不同的宏。) 然而,如果你用char来定义你的字符串,你将会陷入一个尴尬的境地。考虑下面的代码:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowText ( hwnd, szNewText );
在预处理器把SetWindowText用SetWindowTextW来替换后,代码变成:

HWND hwnd = GetSomeWindowHandle();
char szNewText[] = "we love Bob!";
SetWindowTextW ( hwnd, szNewText );
  看到问题了吗?我们把单字节字符串传给了一个以Unicode字符串做参数的函数。解决这个问题的第一个方案是使用 #ifdef 来包含字符串变量的定义:

HWND hwnd = GetSomeWindowHandle();
#ifdef UNICODE
wchar_t szNewText[] = L"we love Bob!";
#else
char szNewText[] = "we love Bob!";
#endif
SetWindowText ( hwnd, szNewText );
你可能已经感受到了这样做将会使你多么的头疼。完美的解决方案是使用TCHAR.

使用TCHAR

  TCHAR是一种字符串类型,它让你在以MBCS和UNNICODE来build程序时可以使用同样的代码,不需要使用繁琐的宏定义来包含你的代码。TCHAR的定义如下:

#ifdef UNICODE
typedef wchar_t TCHAR;
#else
typedef char TCHAR;
#endif
所以用MBCS来build时,TCHAR是char,使用UNICODE时,TCHAR是wchar_t。还有一个宏来处理定义Unicode字符串常量时所需的L前缀。

#ifdef UNICODE
#define _T(x) L##x
#else
#define _T(x) x
#endif
  ##是一个预处理操作符,它可以把两个参数连在一起。如果你的代码中需要字符串常量,在它前面加上_T宏。如果你使用Unicode来build,它会在字符串常量前加上L前缀。

TCHAR szNewText[] = _T("we love Bob!");
  像是用宏来隐藏SetWindowTextA/W的细节一样,还有很多可以供你使用的宏来实现str***()和_mbs***()等字符串函数。例如,你可以使用_tcsrchr宏来替换strrchr()、_mbsrchr()和wcsrchr()。_tcsrchr根据你预定义的宏是_MBCS还是UNICODE来扩展成正确的函数,就像SetWindowText所作的一样。
  不仅str***()函数有TCHAR宏。其他的函数如, _stprintf(代替sprinft()和swprintf()),_tfopen(代替fopen()和_wfopen())。 MSDN中"Generic-Text Routine Mappings."标题下有完整的宏列表。

字符串和TCHAR typedefs

  由于Win32 API文档的函数列表使用函数的常用名字(例如,"SetWindowText"),所有的字符串都是用TCHAR来定义的。(除了XP中引入的只适用于Unicode的API)。下面列出一些常用的typedefs,你可以在msdn中看到他们。

type  Meaning in MBCS builds  Meaning in Unicode builds
WCHAR wchar_t wchar_t
LPSTR  zero-terminated string of char (char*) zero-terminated string of char (char*)
LPCSTR  constant zero-terminated string of char (const char*) constant zero-terminated string of char (const char*)
LPWSTR zero-terminated Unicode string (wchar_t*)  zero-terminated Unicode string (wchar_t*)
LPCWSTR constant zero-terminated Unicode string (const wchar_t*) constant zero-terminated Unicode string (const wchar_t*)  
TCHAR char wchar_t
LPTSTR zero-terminated string of TCHAR (TCHAR*)  zero-terminated string of TCHAR (TCHAR*)
LPCTSTR  constant zero-terminated string of TCHAR (const TCHAR*) constant zero-terminated string of TCHAR (const TCHAR*)

何时使用 TCHAR 和 Unicode

  到现在,你可能会问,我们为什么要使用Unicode。我已经用了很多年的char。下列3种情况下,使用Unicode将会使你受益:

1.你的程序只运行在Windows NT系统中。
2. 你的程序需要处理超过MAX_PATH个字符长的文件名。
3. 你的程序需要使用XP中引入的只有Unicode版本的API.
  Windows 9x 中大多数的 API 没有实现 Unicode 版本。所以,如果你的程序要在windows 9x中运行,你必须使用MBCS APIs。然而,由于NT系统内部都使用Unicode,所以使用Unicode APIs将会加快你的程序的运行速度。每次,你传递一个字符串调用MBCS API,操作系统会把这个字符串转换成Unicode字符串,然后调用对应的Unicode API。如果一个字符串被返回,操作系统还要把它转变回去。尽管这个转换过程被高度优化了,但它对速度造成的损失是无法避免的。
  只要你使用Unicode API,NT系统允许使用非常长的文件名(突破了MAX_PATH的限制,MAX_PATH=260)。使用Unicode API的另一个优点是你的程序会自动处理用户输入的各种语言。所以一个用户可以输入英文,中文或者日文,而你不需要额外编写代码去处理它们。
  最后,随着windows 9x产品的淡出,微软似乎正在抛弃MBCS APIs。例如,包含两个字符串参数的SetWindowTheme() API只有Unicode版本的。使用Unicode来build你的程序将会简化字符串的处理,你不必在MBCS和Unicdoe之间相互转换。
  即使你现在不使用Unicode来build你的程序,你也应该使用TCHAR及其相关的宏。这样做不仅可以的代码可以很好地处理DBCS,而且如果将来你想用Unicode来build你的程序,你只需要改变一下预处理器中的设置就可以实现了。

 

 

作者简介
  Michael Dunn:居住在阳光城市洛杉矶。他是如此的喜欢这里的天气以致于想一生都住在这里。他在4年级时开始编程,那时用的电脑是Apple //e。1995年,在 UCLA 获得数学学士学位,随后在Symantec 公司做 QA 工程师,在 Norton AntiVirus 组工作。他自学了 Windows 和 MFC 编程。1999-2000年,他设计并实现了 Norton AntiVirus 的新界面。 
  Michael 现在在 Napster(一个提供在线订阅音乐服务的公司)做开发工作,他还开发了UltraBar,一个IE工具栏插件,它可以使网络搜索更加容易,给了 googlebar 以沉重打击;他还开发了 CodeProject SearchBar;与人共同创建了 Zabersoft 公司,该公司在洛杉矶和丹麦的 Odense 都设有办事处。
  他喜欢玩游戏。爱玩的游戏有 pinball, bike riding,偶尔还玩 PS, Dreamcasth 和 MAME 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。
 

 

 

C++字符串完全指引之二 —— 字符串封装类

原著:Michael Dunn

作者:Chengjie Sun

原文出处:CodeProject:The Complete Guide to C++ Strings, Part II

引言

  因为C语言风格的字符串容易出错且不易管理,黑客们甚至利用可能存在的缓冲区溢出bug把C语言风格的字符串作为攻击目标,所以出现了很多字符串封装类。不幸的是,在某些场合下我们不知道该使用哪个字符串类,也不知道怎样把一个C风格的字符串转换成一个字符串封装类。
  这篇文章将介绍所有在Win32 API, MFC, STL, WTL 和 Visual C++ 运行库中出现的字符串类型。我将描述每一个类的用法,告诉大家怎样创建每一个类的对象以及怎样把一个类转换成其他类。受控字符串和Visual C++ 7中的类两部分是Nish完成的。
  为了更好的从这篇文章中受益,你必须要明白不同的字符类型和编码,这些内容我在第一部分中介绍过。

Rule #1 of string classes
  使用cast来实现类型转换是不好的做法,除非有文档明确指出这种转换可以使用。
促使我写这两篇文章的原因是字符串类型转换中经常遇到的一些问题。当我们使用cast把字符串从类型X转换到类型Z的时候,我们不知道为什么代码不能正常工作。各种各样的字符串类型,尤其是BSTR,几乎没有在任何一个地方的文档中被明确的指出可以用cast来实现类型转换。所以我想一些人可能会使用cast来实现类型转换并希望这种转换能够正常工作。
  除非源字符串是一个被明确指明支持转换操作符的字符串包装类,否则cast不对字符串做任何转换。对常量字符串使用cast不会起到任何作用,所以下面的代码:

void SomeFunc ( LPCWSTR widestr );
main()
{
 SomeFunc ( (LPCWSTR) "C:\foo.txt" );  // WRONG!
}      
  肯定会失败。它可以被编译,因为cast操作会撤消编译器的类型检查。但是,编译可以通过并不能说明代码是正确的。
  在下面的例子中,我将会指明cast在什么时候使用是合法的。C-style strings and typedefs
  正如我在第一部分中提到的,windows APIs 是用TCHARs来定义的,在编译时,它可以根据你是否定义_MBCS或者_UNICODE被编译成MBCS或者Unicode字符。你可以参看第一部分中对TCHAR的完整描述,这里为了方便,我列出了字符的typedefs

Type Meaning
WCHAR Unicode character (wchar_t)
TCHAR MBCS or Unicode character, depending on preprocessor settings
LPSTR  string of char (char*)
LPCSTR constant string of char (const char*)
LPWSTR  string of WCHAR (WCHAR*)
LPCWSTR  constant string of WCHAR (const WCHAR*)
LPTSTR  string of TCHAR (TCHAR*)
LPCTSTR  constant string of TCHAR (const TCHAR*)

  一个增加的字符类型是OLETYPE。它表示自动化接口(如word提供的可以使你操作文档的接口)中使用的字符类型。这种类型一般被定义成wchar_t,然而如果你定义了OLE2ANSI预处理标记,OLECHAR将会被定义成char类型。我知道现在已经没有理由定义OLE2ANSI(从MFC3以后,微软已经不使用它了),所以从现在起我将把OLECHAR当作Unicode字符。
这里给出你将会看到的一些OLECHAR相关的typedefs:

Type Meaning
OLECHAR  Unicode character (wchar_t)
LPOLESTR  string of OLECHAR (OLECHAR*)
LPCOLESTR  constant string of OLECHAR (const OLECHAR*)

  还有两个用于包围字符串和字符常量的宏定义,它们可以使同样的代码被用于MBCS和Unicode builds :

Type  Meaning
_T(x) Prepends L to the literal in Unicode builds.
OLESTR(x) Prepends L to the literal to make it an LPCOLESTR.  

  在文档或例程中,你还会看到好多_T的变体。有四个等价的宏定义,它们是TEXT, _TEXT, __TEXT和__T,它们都起同样的做用。

COM 中的字符串 —— BSTR 和 VARIANT

  很多自动化和COM接口使用BSTR来定义字符串。BSTRs中有几个"陷阱",所以这里我用单独的部分来说明它。
  BSTR 是 Pascal-style 字符串(字符串长度被明确指出)和C-style字符串(字符串的长度要通过寻找结束符来计算)的混合产物。一个BSTR是一个Unicode字符串,它的长度是预先考虑的,并且它还有一个0字符作为结束标记。下面是一个BSTR的示例:

 

06 00 00 00 42 00 6F 00 62 00 00 00
–length– B o b EOS

  注意字符串的长度是如何被加到字符串数据中的。长度是DWORD类型的,保存了字符串中包含的字节数,但不包括结束标记。在这个例子中,"Bob"包含3个Unicode字符(不包括结束符),总共6个字节。字符串的长度被预先存储好,以便当一个BSTR在进程或者计算机之间被传递时,COM库知道多少数据需要传送。(另一方面,一个BSTR能够存储任意数据块,而不仅仅是字符,它还可以包含嵌入在数据中的0字符。然而,由于这篇文章的目的,我将不考虑那些情况)。
  在 C++ 中,一个 BSTR 实际上就是一个指向字符串中第一个字符的指针。它的定义如下:

BSTR bstr = NULL;
 bstr = SysAllocString ( L"Hi Bob!" );
 if ( NULL == bstr )
   // out of memory error
 // Use bstr here…
SysFreeString ( bstr );      
自然的,各种各样的BSTR封装类为你实现内存管理。
  另外一个用在自动化接口中的变量类型是VARIANT。它被用来在无类型(typeless)语言,如Jscript和VBScript,来传递数据。一个VARIANT可能含有很多不同类型的数据,例如long和IDispatch*。当一个VARIANT包含一个字符串,字符串被存成一个BSTR。当我后面讲到VARIANT封装类时,我会对VARIANT多些介绍。

字符串封装类

  到目前为止,我已经介绍了各种各样的字符串。下面,我将说明封装类。对于每个封装类,我将展示怎样创建一个对象及怎样把它转换成一个C语言风格的字符串指针。C语言风格的字符串指针对于API的调用,或者创建一个不同的字符串类对象经常是必需的。我不会介绍字符串类提供的其他操作,比如排序和比较。
  重复一遍,除非你确切的明白结果代码将会做什么,否则不要盲目地使用cast来实现类型转换。

CRT提供的类

_bstr_t
  _bstr_t是一个对BSTR的完整封装类,实际上它隐藏了底层的BSTR。它提供各种构造函数和操作符来访问底层的C语言风格的字符串。然而,_bstr_t却没有访问BSTR本身的操作符,所以一个_bstr_t类型的字符串不能被作为输出参数传给一个COM方法。如果你需要一个BSTR*参数,使用ATL类CComBSTR是比较容易的方式。
  一个_bstr_t字符串能够传给一个接收参数类型为BSTR的函数,只是因为下列3个条件同时满足。首先,_bstr_t有一个向wchar_t*转换的转换函数;其次,对编译器而言,因为BSTR的定义,wchar_t*和BSTR有同样的含义;第三,_bstr_t内部含有的wchar_t*指向一片按BSTR的形式存储数据的内存。所以,即使没有文档说明,_bstr_t可以转换成BSTR,这种转换仍然可以正常进行。 // Constructing
_bstr_t bs1 = "char string";       // construct from a LPCSTR
_bstr_t bs2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs3 = bs1;                 // copy from another _bstr_t
_variant_t v = "Bob";
_bstr_t bs4 = v;                   // construct from a _variant_t that has a string

// Extracting data
LPCSTR psz1 = bs1;              // automatically converts to MBCS string
LPCSTR psz2 = (LPCSTR) bs1;     // cast OK, same as previous line
LPCWSTR pwsz1 = bs1;            // returns the internal Unicode string
LPCWSTR pwsz2 = (LPCWSTR) bs1;  // cast OK, same as previous line
BSTR    bstr = bs1.copy();      // copies bs1, returns it as a BSTR

 // …
SysFreeString ( bstr );      
  注意_bstr_t也提供char*和wchar_t*之间的转换操作符。这是一个值得怀疑的设计,因为即使它们是非常量字符串指针,你也一定不能使用这些指针去修改它们指向的缓冲区的内容,因为那将破坏内部的BSTR结构。

_variant_t
  _variant_t是一个对VARIANT的完整封装,它提供很多构造函数和转换函数来操作一个VARIANT可能包含的大量的数据类型。这里,我将只介绍与字符串有关的操作。 // Constructing
_variant_t v1 = "char string";       // construct from a LPCSTR
_variant_t v2 = L"wide char string"; // construct from a LPCWSTR
_bstr_t bs1 = "Bob";
_variant_t v3 = bs1;                 // copy from a _bstr_t object

// Extracting data
_bstr_t bs2 = v1;           // extract BSTR from the VARIANT
_bstr_t bs3 = (_bstr_t) v1; // cast OK, same as previous line      
注意:
  如果类型转换不能被执行,_variant_t方法能够抛出异常,所以应该准备捕获_com_error异常。

还需要注意的是:
  没有从一个_variant_t变量到一个MBCS字符串的直接转换。你需要创建一个临时的_bstr_t变量,使用提供Unicode到MBCS转换的另一个字符串类或者使用一个ATL转换宏。
  不像_bstr_t,一个_variant_t变量可以被直接作为参数传递给一个COM方法。_variant_t
  继承自VARIANT类型,所以传递一个_variant_t来代替VARIANT变量是C++语言所允许的。

STL 类
  STL只有一个字符串类,basic_string。一个basic_string管理一个以0做结束符的字符串数组。字符的类型是basic_string模般的参数。总的来说,一个basic_string类型的变量应该被当作不透明的对象。你可以得到一个指向内部缓冲区的只读指针,但是任何写操作必须使用basic_string的操作符和方法。
  basic_string有两个预定义的类型:包含char的string类型和包含wchar_t的wstring类型。这里没有内置的包含TCHAR的类型,但是你可以使用下面列出的代码来实现。 // Specializations
typedef basic_string tstring; // string of TCHARs

// Constructing
string str = "char string";         // construct from a LPCSTR
wstring wstr = L"wide char string"; // construct from a LPCWSTR
tstring tstr = _T("TCHAR string");  // construct from a LPCTSTR

// Extracting data
LPCSTR psz = str.c_str();    // read-only pointer to str’’s buffer
LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr’’s buffer
LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr’’s buffer

  不像_bstr_t,一个basic_string变量不能在字符集之间直接转换。然而,你可以传递由c_str()返回的指针给另外一个类的构造函数(如果这个类的构造函数接受这种字符类型)。例如: // Example, construct _bstr_t from basic_string
_bstr_t bs1 = str.c_str();  // construct a _bstr_t from a LPCSTR
_bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTR      
ATL 类

CComBSTR
  CComBSTR 是 ATL 中的 BSTR 封装类,它在某些情况下比_bstr_t有用的多。最引人注意的是CComBSTR允许访问底层的BSTR,这意味着你可以传递一个CComBSTR对象给COM的方法。CComBSTR对象能够替你自动的管理BSTR的内存。例如,假设你想调用下面这个接口的方法: // Sample interface:
struct IStuff : public IUnknown
{
 // Boilerplate COM stuff omitted…
 STDMETHOD(SetText)(BSTR bsText);
 STDMETHOD(GetText)(BSTR* pbsText);
};      
  CComBSTR有一个操作符–BSTR方法,所以它能直接被传给SetText()函数。还有另外一个操作–&,这个操作符返回一个BSTR*。所以,你可以对一个CComBSTR对象使用&操作符,然后把它传给需要BSTR*参数的函数。 CComBSTR bs1;
CComBSTR bs2 = "new text";

 pStuff->GetText ( &bs1 );       // ok, takes address of internal BSTR
 pStuff->SetText ( bs2 );        // ok, calls BSTR converter
 pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous line      
  CComBSTR有和_bstr_t相似的构造函数,然而却没有内置的向MBCS字符串转换的函数。因此,你需要使用一个ATL转换宏。 // Constructing
CComBSTR bs1 = "char string";       // construct from a LPCSTR
CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs3 = bs1;                 // copy from another CComBSTR
CComBSTR bs4;

 bs4.LoadString ( IDS_SOME_STR );  // load string from string table
// Extracting data
BSTR bstr1 = bs1;        // returns internal BSTR, but don”t modify it!
BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line
BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR
BSTR bstr4;
 bstr4 = bs1.Detach();  // bs1 no longer manages its BSTR
 // …
 SysFreeString ( bstr3 );
 SysFreeString ( bstr4 );      
  注意在上个例子中使用了Detach()方法。调用这个方法后,CComBSTR对象不再管理它的BSTR字符串或者说它对应的内存。这就是bstr4需要调用SysFreeString()的原因。
  做一个补充说明:重载的&操作符意味着在一些STL容器中你不能直接使用CComBSTR变量,比如list。容器要求&操作符返回一个指向容器包含的类的指针,但是对CComBSTR变量使用&操作符返回的是BSTR*,而不是CComBSTR*。然而,有一个ATL类可以解决这个问题,这个类是CAdapt。例如,你可以这样声明一个CComBSTR的list:std::list< CAdapt<CComBSTR> > bstr_list;
  CAdapt提供容器所需要的操作符,但这些操作符对你的代码是透明的。你可以把一个bstr_list当作一个CComBSTR的list来使用。

CComVariant
  CComVariant是VARIANT的封装类。然而,不像_variant_t,在CComVariant中VARIANT没有被隐藏。事实上你需要直接访问VARIANT的成员。CComVariant提供了很多构造函数来对VARIANT能够包含的多种类型进行处理。这里,我将只介绍和字符串相关的操作。

// Constructing
CComVariant v1 = "char string";       // construct from a LPCSTR
CComVariant v2 = L"wide char string"; // construct from a LPCWSTR
CComBSTR bs1 = "BSTR bob";
CComVariant v3 = (BSTR) bs1;          // copy from a BSTR

// Extracting data
CComBSTR bs2 = v1.bstrVal;            // extract BSTR from the VARIANT      
  不像_variant_t,这里没有提供针对VARIANT包含的各种类型的转换操作符。正如上面介绍的,你必须直接访问VARIANT的成员并且确保这个VARIANT变量保存着你期望的类型。如果你需要把一个CComVariant类型的数据转换成一个BSTR类型的数据,你可以调用ChangeType()方法。 CComVariant v4 = … // Init v4 from somewhere
CComBSTR bs3;

 if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) ))
   bs3 = v4.bstrVal;      
  像_variant_t一样,CComVariant也没有提供向MBCS字符串转换的转换操作。你需要创建一个_bstr_t类型的中间变量,使用提供从Unicode到MBCS转换的另一个字符串类,或者使用一个ATL的转换宏。

ATL转换宏

  ATL:转换宏是各种字符编码之间进行转换的一种很方便的方式,在函数调用时,它们显得非常有用。ATL转换宏的名称是根据下面的模式来命名的[源类型]2[新类型]或者[源类型]2C[新类型]。据有第二种形式的名字的宏的转换结果是常量指针(对应名字中的"C")。各种类型的简称如下:A: MBCS string, char* (A for ANSI)
W: Unicode string, wchar_t* (W for wide)
T: TCHAR string, TCHAR*
OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W)
BSTR: BSTR (used as the destination type only)
  所以,W2A()宏把一个Unicode字符串转换成一个MBCS字符串。T2CW()宏把一个TCHAR字符串转转成一个Unicode字符串常量。
  为了使用这些宏,需要先包含atlconv.h头文件。你甚至可以在非ATL工程中包含这个头文件来使用其中定义的宏,因为这个头文件独立于ATL中的其他部分,不需要一个_Module全局变量。当你在一个函数中使用转换宏时,需要把USES_CONVERSION宏放在函数的开头。它定义了转换宏所需的一些局部变量。
  当转换的目的类型是除了BSTR以外的其他类型时,被转换的字符串是存在栈中的。所以,如果你想让字符串的生命周期比当前的函数长,你需要把这个字符串拷贝到其他的字符串类中。当目的类型是BSTR时,内存不会自动被释放,你必须把返回值赋给一个BSTR变量或者一个BSTR封装类以避免内存泄漏。
  下面是一些各种转换宏的使用例子:

// Functions taking various strings:
void Foo ( LPCWSTR wstr );
void Bar ( BSTR bstr );
// Functions returning strings:
void Baz ( BSTR* pbstr );
#include <atlconv.h>
main()
{
using std::string;
USES_CONVERSION;    // declare locals used by the ATL macros
// Example 1: Send an MBCS string to Foo()
LPCSTR psz1 = "Bob";
string str1 = "Bob";

 Foo ( A2CW(psz1) );
 Foo ( A2CW(str1.c_str()) );

// Example 2: Send a MBCS and Unicode string to Bar()
LPCSTR psz2 = "Bob";
LPCWSTR wsz = L"Bob";
BSTR bs1;
CComBSTR bs2;

 bs1 = A2BSTR(psz2);         // create a BSTR
 bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR
 Bar ( bs1 );
 Bar ( bs2 );

 SysFreeString ( bs1 );      // free bs1 memory
 // No need to free bs2 since CComBSTR will do it for us.

// Example 3: Convert the BSTR returned by Baz()
BSTR bs3 = NULL;
string str2;
 Baz ( &bs3 );          // Baz() fills in bs3
 str2 = W2CA(bs3);      // convert to an MBCS string
 SysFreeString ( bs3 ); // free bs3 memory
}      
  正如你所看见的,当你有一个和函数所需的参数类型不同的字符串时,使用这些转换宏是非常方便的。

MFC类

CString
  因为一个MFC CString类的对象包含TCHAR类型的字符,所以确切的字符类型取决于你所定义的预处理符号。大体来说,CString 很像STL string,这意味着你必须把它当成不透明的对象,只能使用CString提供的方法来修改CString对象。CString有一个string所不具备的优点:CString具有接收MBCS和Unicode两种字符串的构造函数,它还有一个LPCTSTR转换符,所以你可以把CString对象直接传给一个接收LPCTSTR的函数而不需要调用c_str()函数。 // Constructing
CString s1 = "char string";  // construct from a LPCSTR
CString s2 = L"wide char string";  // construct from a LPCWSTR
CString s3 ( ” ”, 100 );  // pre-allocate a 100-byte buffer, fill with spaces
CString s4 = "New window text";

 // You can pass a CString in place of an LPCTSTR:
 SetWindowText ( hwndSomeWindow, s4 );

 // Or, equivalently, explicitly cast the CString:
 SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );        
  你可以从你的字符串表中装载一个字符串,CString的一个构造函数和LoadString()函数可以完成它。Format()方法能够从字符串表中随意的读取一个具有一定格式的字符串。      // Constructing/loading from string table
CString s5 ( (LPCTSTR) IDS_SOME_STR );  // load from string table
CString s6, s7;
 // Load from string table.
 s6.LoadString ( IDS_SOME_STR );

 // Load printf-style format string from the string table:
 s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, … );  
  第一个构造函数看起来有点奇怪,但是这实际上是文档说明的装入一个字符串的方法。 注意,对一个CString变量,你可以使用的唯一合法转换符是LPCTSTR。转换成LPTSTR(非常量指针)是错误的。养成把一个CString变量转换成LPTSTR的习惯将会给你带来伤害,因为当你的程序后来崩溃时,你可能不知道为什么,因为你到处都使用同样的代码而那时它们都恰巧正常工作。正确的得到一个指向缓冲区的非常量指针的方法是调用GetBuffer()方法。下面是正确的用法的一个例子,这段代码是给一个列表控件中的项设定文字: CString str = _T("new text");
LVITEM item = {0};
 item.mask = LVIF_TEXT;
 item.iItem = 1;
 item.pszText = (LPTSTR)(LPCTSTR) str; // WRONG!
 item.pszText = str.GetBuffer(0);      // correct

 ListView_SetItem ( &item );
str.ReleaseBuffer();  // return control of the buffer to str      
  pszText成员是一个LPTSTR变量,一个非常量指针,因此你需要对str调用GetBuffer()。GetBuffer()的参数是你需要CString为缓冲区分配的最小长度。如果因为某些原因,你需要一个可修改的缓冲区来存放1K TCHARs,你需要调用GetBuffer(1024)。把0作为参数时,GetBuffer()返回的是指向字符串当前内容的指针。
  上面划线的语句可以被编译,在这种情况下,甚至可以正常起作用。但这并不意味着这行代码是正确的。通过使用非常量转换,你已经破坏了面向对象的封装,并对CString的内部实现作了某些假定。如果你有这样的转换习惯,你终将会陷入代码崩溃的境地。你会想代码为什么不能正常工作了,因为你到处都使用同样的代码而那些代码看起来是正确的。
  你知道人们总是抱怨现在的软件的bug是多么的多吗?软件中的bug是因为程序员写了不正确的代码。难道你真的想写一些你知道是错误的代码来为所有的软件都满是bug这种认识做贡献吗?花些时间来学习使用CString的正确方法让你的代码在任何时间都正常工作把。
  CString 有两个函数来从一个 CString 创建一个 BSTR。它们是 AllocSysString() 和SetSysString()。 // Converting to BSTR
CString s5 = "Bob!";
BSTR bs1 = NULL, bs2 = NULL;
 bs1 = s5.AllocSysString();
 s5.SetSysString ( &bs2 );
 SysFreeString ( bs1 );
 SysFreeString ( bs2 );      
COleVariant
  COleVariant和CComVariant.很相似。COleVariant继承自VARIANT,所以它可以传给接收VARIANT的函数。然而,不像CComVariant,COleVariant只有一个LPCTSTR构造函数。没有对LPCSTR 和LPCWSTR的构造函数。在大多数情况下这不是一个问题,因为不管怎样你的字符串很可能是LPCTSTRs,但这是一个需要意识到的问题。COleVariant还有一个接收CString参数的构造函数。 // Constructing
CString s1 = _T("tchar string");
COleVariant v1 = _T("Bob"); // construct from an LPCTSTR
COleVariant v2 = s1; // copy from a CString      
  像CComVariant一样,你必须直接访问VARIANT的成员。如果需要把VARIANT转换成一个字符串,你应该使用ChangeType()方法。然而,COleVariant::ChangeType()如果失败会抛出异常,而不是返回一个表示失败的HRESULT代码。 // Extracting data
COleVariant v3 = …; // fill in v3 from somewhere
BSTR bs = NULL;
 try
   {
   v3.ChangeType ( VT_BSTR );
   bs = v3.bstrVal;
   }
 catch ( COleException* e )
   {
   // error, couldn”t convert
   }
 SysFreeString ( bs );      

WTL 类

CString
  WTL的CString的行为和MFC的 CString完全一样,所以你可以参考上面关于MFC的 CString的介绍。

CLR 和 VC 7 类

  System::String是用来处理字符串的.NET类。在内部,一个String对象包含一个不可改变的字符串序列。任何对String对象的操作实际上都是返回了一个新的String对象,因为原始的对象是不可改变的。String的一个特性是如果你有不止一个String对象包含相同的字符序列,它们实际上是指向相同的对象的。相对于C++的使用扩展是增加了一个新的字符串常量前缀S,S用来代表一个受控的字符串常量(a managed string literal)。 // Constructing
String* ms = S"This is a nice managed string";      
  你可以传递一个非受控的字符串来创建一个String对象,但是样会比使用受控字符串来创建String对象造成效率的微小损失。这是因为所有以S作为前缀的相同的字符串实例都代表同样的对象,但这对非受控对象是不适用的。下面的代码清楚地阐明了这一点: String* ms1 = S"this is nice";
String* ms2 = S"this is nice";
String* ms3 = L"this is nice";
 Console::WriteLine ( ms1 == ms2 ); // prints true
 Console::WriteLine ( ms1 == ms3);  // prints false      
正确的比较可能没有使用S前缀的字符串的方法是使用String::CompareTo()   Console::WriteLine ( ms1->CompareTo(ms2) );
 Console::WriteLine ( ms1->CompareTo(ms3) );      
  上面的两行代码都会打印0,0表示两个字符串相等。 String和MFC 7 CString之间的转换是很容易的。CString有一个向LPCTSTR的转换操作,而String有两个接收char* 和 wchar_t*的构造函数,因此你可以把一个CString变量直接传给一个String的构造函数。 CString s1 ( "hello world" );
String* s2 ( s1 );  // copy from a CString      
反方向的转换也很类似 String* s1 = S"Three cats";
CString s2 ( s1 );      
  这也许会使你感到一点迷惑,但是它确实是起作用的。因为从VS.NET 开始,CString 有了一个接收String 对象的构造函数。   CStringT ( System::String* pString );      
对于一些快速操作,你可能想访问底层的字符串: String* s1 = S"Three cats";
 Console::WriteLine ( s1 );
const __wchar_t __pin* pstr = PtrToStringChars(s1);
 for ( int i = 0; i < wcslen(pstr); i++ )
   (*const_cast<__wchar_t*>(pstr+i))++;
 Console::WriteLine ( s1 );      
  PtrToStringChars()返回一个指向底层字符串的const __wchar_t* ,我们需要固定它,否则垃圾收集器或许会在我们正在管理它的内容的时候移动了它。

在 printf-style 格式函数中使用字符串类

  当你在printf()或者类似的函数中使用字符串封装类时你必须十分小心。这些函数包括sprintf()和它的变体,还有TRACE和ATLTRACE宏。因为这些函数没有对添加的参数的类型检查,你必须小心,只能传给它们C语言风格的字符串指针,而不是一个完整的字符串类。
  例如,要把一个_bstr_t 字符串传给ATLTRACE(),你必须使用显式转换(LPCSTR) 或者(LPCWSTR):_bstr_t bs = L"Bob!";
ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);
  如果你忘了使用转换符而把整个_bstr_t对象传给了函数,将会显示一些毫无意义的输出,因为_bstr_t保存的内部数据会全部被输出。

所有类的总结

  两个字符串类之间进行转换的常用方式是:先把源字符串转换成一个C语言风格的字符串指针,然后把这个指针传递给目的类型的构造函数。下面这张表显示了怎样把一个字符串转换成一个C语言风格的字符串指针以及哪些类具有接收C语言风格的字符串指针的构造函数。

Class   string type  convert to char*?  convert to const char*?  convert to wchar_t*?  convert to const wchar_t*?  convert to BSTR?  construct from char*?  construct from wchar_t*?
_bstr_t BSTR yes cast1 yes cast yes cast1 yes cast yes2 yes yes
_variant_t BSTR no no no cast to
_bstr_t3 cast to
_bstr_t3 yes yes
string MBCS no yes c_str() method no no no yes no
wstring Unicode no no no yes c_str() method no no yes
CComBSTR BSTR no no no yes cast to BSTR yes cast yes yes
CComVariant BSTR no no no yes4 yes4 yes yes
CString  TCHAR no6 in MBCS
builds, cast no6 in Unicode
builds, cast no5 yes yes
COleVariant BSTR no no no yes4 yes4 in MBCS
builds in Unicode
builds
1、即使 _bstr_t 提供了向非常量指针的转换操作符,修改底层的缓冲区也会已引起GPF如果你溢出了缓冲区或者造成内存泄漏。
2、_bstr_t 在内部用一个 wchar_t* 来保存 BSTR,所以你可以使用 const wchar_t* 来访问BSTR。这是一个实现细节,你可以小心的使用它,将来这个细节也许会改变。
3、如果数据不能转换成BSTR会抛出一个异常。
4、使用 ChangeType(),然后访问 VARIANT 的 bstrVal 成员。在MFC中,如果数据转换不成功将会抛出异常。
5、这里没有转换 BSTR 函数,然而 AllocSysString() 返回一个新的BSTR。
6、使用 GetBuffer() 方法,你可以暂时地得到一个非常量的TCHAR指针。

 

 

作者简介

Michael Dunn:
  Michael Dunn居住在阳光城市洛杉矶。他是如此的喜欢这里的天气以致于想一生都住在这里。他在4年级时开始编程,那时用的电脑是Apple //e。1995年,在UCLA获得数学学士学位,随后在Symantec公司做QA工程师,在 Norton AntiVirus 组工作。他自学了 Windows 和 MFC 编程。1999-2000年,他设计并实现了 Norton AntiVirus的新界面。
  Michael 现在在 Napster(一个提供在线订阅音乐服务的公司)做开发工作,他还开发了UltraBar,一个IE工具栏插件,它可以使网络搜索更加容易,给了 googlebar 以沉重打击;他还开发了 CodeProject SearchBar;与人共同创建了 Zabersoft 公司,该公司在洛杉矶和丹麦的 Odense 都设有办事处。
  他喜欢玩游戏。爱玩的游戏有 pinball, bike riding,偶尔还玩 PS, Dreamcasth 和 MAME 游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。

Nishant S(Nish):
  Nish是来自印度 Trivandrum,的 Microsoft Visual C++ MVP。他从1990年开始编码。现在,Nish为作为合同雇员在家里为 CodeProject 工作。   
  他还写了一部浪漫戏剧《Summer Love and Some more Cricket》和一本编程书籍《Extending MFC applications with the .NET Framework》。他还管理者MVP的一个网站http://www.voidnish.com/ 。在这个网站上,你可以看到他的很多关于编程方面的思想和文章。
Nish 还计划好了旅游,他希望自一生中能够到达地球上尽可能多的地方。

2006年06月16日

设计的思想主要是通过双链表来实现的.利用双链表的节点存储每位的数字,利用前驱进行对上一个位数的使用.用后继来连接后面的节点.通过对"+"号和"*"号的重载实现对大整数的加和乘.理论上可以实现N多位的运算,只要你的机器内存够大…程序的重点在于运算符的重载.

本程序主要由三个文件构成:

BigInteger.h 包涵了对节点的结构定义,以及类BigInteger的定义.

BigInteger.cpp 包涵了BigInteger类里面成员函数的具体内容.

main.cpp 主函数…

//BigInteger.h

struct Node        //定义了节点的结构
{
 char Num;
 Node *Prev,*Next;
};

class BigInteger      //定义BigInteger 类
{
 Node *Head,*End,*TempNode;
 void AddHead(char Num);
 void AddEnd(char Num);
 public:
  BigInteger();
  BigInteger(const BigInteger &BigNum);
  void GetNumber();
  void disp();
  BigInteger operator + (const BigInteger &BigNum);
  BigInteger operator * (const BigInteger &BigNum);
  BigInteger operator = (const BigInteger &BigNum);
  ~BigInteger();
};

//BigInteger.cpp

#include <iostream.h>
#include <stdio.h>
#include "BigInteger.h"

BigInteger::BigInteger()        //构造函数,将每个节点置空.
{
 Head=End=TempNode=NULL;
}

BigInteger::BigInteger(const BigInteger &BigNum)     //拷贝构造
{
 Node *p;
 Head=End=TempNode=NULL;
 p=BigNum.Head;
 while(p)
 {
  AddEnd(p->Num);
  p=p->Next;
 }
}

BigInteger::~BigInteger()        //析构
{
 Node *NextNode;
 if(Head==NULL)
  return;
 TempNode=Head;
 while(TempNode)
 {
  NextNode=TempNode->Next;
  delete TempNode;
  TempNode=NextNode;
 }
 Head=NULL;
 End=NULL;
 TempNode=NULL;
}

void BigInteger::AddHead(char Num)        //在链表头插入节点的操作
{
 TempNode=new Node;
 TempNode->Num=Num;
 TempNode->Prev=NULL;
 if(!Head)
 {
  Head=End=TempNode;
  TempNode->Next=NULL;
 }
 else
 {
  TempNode->Next=Head;
  Head->Prev=TempNode;
  Head=TempNode;
 }
}

void BigInteger::AddEnd(char Num)       //在链表尾插入节点的操作
{
 TempNode=new Node;
 TempNode->Num=Num;
 TempNode->Next=NULL;
 if(!Head)
 {
  Head=End=TempNode;
  TempNode->Prev=NULL;
 }
 else
 {
  TempNode->Prev=End;
  End->Next=TempNode;
  End=TempNode;
 }
}

void BigInteger::GetNumber()            //输入部分
{
 char key;
 int count=0,num=0;
 while((key=getchar())!=10)            //判断输入的是否是回车,不是的话将内容从后到前放到链表中.
 {
  if(key>=’0′ && key<=’9′)
  {
   num=key-’0′;
   AddEnd(num);
   num=0;
  }
 }
}

BigInteger BigInteger::operator + (const BigInteger &BigNum2)    //重载"+"
{
 BigInteger &BigNum1=*this,result;
 Node *temp1,*temp2;
 int TempNum,rest=0;
 temp1=BigNum1.End;            //将临时链表首地址放置到输入链表的尾部
 temp2=BigNum2.End;
 while(temp1 && temp2)
 {
  TempNum=int(temp1->Num)+int(temp2->Num)+rest;         //节点内元素相加并加上进位rest
  if(TempNum>9)                  //判断相加结果是否会产生进位.
  {
   TempNum=TempNum-10;
   rest=1;
  }
  else
   rest=0;
  result.AddHead(char(TempNum));          //将结果放置到最终结果链表里
  temp1=temp1->Prev;
  temp2=temp2->Prev;
 }
 if(temp2)temp1=temp2;
 while(temp1)
 {
  int(TempNum)=int(temp1->Num)+rest;           //节点内元素加上进位rest
  if(TempNum>9)
  {
   TempNum=TempNum-10;
   rest=1;
  }
  else
   rest=0;
  result.AddHead(char(TempNum));             //将结果放置到最终结果链表里
  temp1=temp1->Prev;
 }
 if(rest)
  result.AddHead(char(rest));                //考虑最后的进位是否存在,如果存在则存入链表的首部.
 return result;
}

BigInteger BigInteger::operator * (const BigInteger &BigNum2)     //对*进行重载
{
 BigInteger &BigNum1=*this,temp,result;
 Node *temp1,*temp2,*tempa,*tempb;
 int TempNum,rest,i=0,rest2;
 temp1=BigNum1.End;
 temp2=BigNum2.End;
 while(temp2)        //由乘数的存在与否判断是否去乘被乘数的每个位
 {
  rest=0;
  while(temp1!=NULL)
  {
   TempNum=int(temp1->Num)*int(temp2->Num)+rest;
   if(TempNum>9)
   { 
    rest=TempNum/10;                 //进位由相乘结果与10做商求得
    TempNum=TempNum%10;                 //由相乘结果与10求模取个位
   }
   else
    rest=0;
   temp.AddHead(char(TempNum));        //存入临时链表
   temp1=temp1->Prev;
  }
  if(rest!=0)temp.AddHead(char(rest));
  for(int k=i;k>=1;k–)temp.AddEnd(char(0));       //判断应该在链表后面补几个0
  i++;            //每次乘完后计数,用来下一次的补0
  temp1=BigNum1.End;             //把被乘数重新置到尾,用来让乘数下一次去乘每个元素
  temp2=temp2->Prev;              //将乘数取出链表的前驱
  tempa=result.End;                  //下面进行的是将每次乘数与被乘数的相乘结果累加放到最终链表里等待输出
  if(result.Head!=NULL)           //下面过程与"+"重载基本一样,只是多了对临时链表的置空,所以不在做详细的注释.
  {
   result.End=temp.Head;
   result.Head=NULL;
  }
  tempb=temp.End;
  rest2=0;
  while(tempa!=NULL && tempb!=NULL)
  {
   TempNum=int(tempa->Num)+int(tempb->Num)+rest2;
   if(TempNum>9)
   {
    TempNum=TempNum-10;
    rest2=1;
   }
   else
    rest2=0;
   result.AddHead(char(TempNum));
   tempa=tempa->Prev;
   tempb=tempb->Prev;
  }
  if(tempb)tempa=tempb;
  while(tempa)
  {
   int(TempNum)=int(tempa->Num)+rest2;
   if(TempNum>9)
   {
    TempNum=TempNum-10;
    rest2=1;
   }
   else
    rest2=0;
   result.AddHead(char(TempNum));
   tempa=tempa->Prev;
  }
  if(rest2)
   result.AddHead(char(rest2));
  if(temp.Head!=NULL)
  {
   temp.End=temp.Head;
   temp.Head=NULL;
  }
  tempb=NULL;
 }
 return result;
}

BigInteger BigInteger::operator = (const BigInteger &BigNum)          //对=号进行重载
{
 if(this==&BigNum)
  return *this;
 Node *p;
 TempNode=Head=End=NULL;
 p=BigNum.Head;
 while(p)
 {
  AddEnd(p->Num);
  p=p->Next;
 }
 return *this;
}

void BigInteger::disp()                    //输出链表
{
 if(Head)
 {
  cout<<int(Head->Num);
  TempNode=Head->Next;
 }
 else return;
 while(TempNode)
 {
  cout<<int(TempNode->Num);
  TempNode=TempNode->Next;
 }
 cout<<endl;
}

//main.cpp

#include <iostream.h>
#include "BigInteger.h"

void main()
{
 BigInteger BigNum1,BigNum2,BigNum3;
 int c;
 cout<<"选择你要进行的操作:"<<endl;
 cout<<"1.大整数加法运算"<<endl;
 cout<<"2.大整数乘法运算"<<endl;
 cout<<"选择你需要进行的运算:"<<endl;
 cin>>c;
 switch(c)
 {
  case 1:
   { 
    cout<<"A:"<<endl;
    BigNum1.GetNumber();
    cout<<"B:"<<endl;
    BigNum2.GetNumber();
    BigNum3=BigNum1+BigNum2;
    cout<<"相加的结果是:"<<endl;
    BigNum3.disp();
   }break;
  case 2:
   {
    cout<<"A:"<<endl;
    BigNum1.GetNumber();
    cout<<"B:"<<endl;
    BigNum2.GetNumber();
    BigNum3=BigNum1*BigNum2;
    cout<<"相乘的结果是:"<<endl;
    BigNum3.disp();
   }break;
  default:break;
 }
}

1、分治法的基本思想
任何一个可以用计算机求解的问题所需的计算时间都与其规模N有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算;n=2时,只要作一次比较即可排好序;n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。
分治法的设计思想是,将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
如果原问题可分割成k个子问题(1<k≤n),且这些子问题都可解,并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
2、分治法的适用条件
分治法所能解决的问题一般具有以下几个特征:
(1)该问题的规模缩小到一定的程度就可以容易地解决;
(2)该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质;
(3)利用该问题分解出的子问题的解可以合并为该问题的解;
(4)该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提,它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
3、分治法的基本步骤
分治法在每一层递归上都有三个步骤:
(1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
(2)解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题;
(3)合并:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide_and_Conquer(P)
if |P|≤n0
then return(ADHOC(P))
将P分解为较小的子问题P1、P2、…、Pk
for i←1 to k
do
yi ← Divide-and-Conquer(Pi)         △ 递归解决Pi
T ← MERGE(y1,y2,…,yk)            △ 合并子问题
Return(T)
其中 |P| 表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时,直接用算法ADHOC(P)求解。
算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1、P2、…、Pk的相应的解y1、y2、…、yk合并为P的解。
根据分治法的分割原则,原问题应该分为多少个子问题才较适宜?各个子问题的规模应该怎样才为适当?这些问题很难予以肯定的回答。但人们从大量实践中发现,在用分治法设计算法时,最好使子问题的规模大致相同。换句话说,将一个问题分成大小相等的k个子问题的处理方法是行之有效的。许多问题可以取k=2。这种使子问题规模大致相等的做法是出自一种平衡子问题的思想,它几乎总是比子问题规模不等的做法要好。
分治法的合并步骤是算法的关键所在。有些问题的合并方法比较明显,有些问题合并方法比较复杂,或者是有多种合并方案;或者是合并方案不明显。究竟应该怎样合并,没有统一的模式,需要具体问题具体分析。
【问题】   大整数乘法
问题描述:
通常,在分析一个算法的计算复杂性时,都将加法和乘法运算当作是基本运算来处理,即将执行一次加法或乘法运算所需的计算时间当作一个仅取决于计算机硬件处理速度的常数。
这个假定仅在计算机硬件能对参加运算的整数直接表示和处理时才是合理的。然而,在某些情况下,我们要处理很大的整数,它无法在计算机硬件能直接表示的范围内进行处理。若用浮点数来表示它,则只能近似地表示它的大小,计算结果中的有效数字也受到限制。若要精确地表示大整数并在计算结果中要求精确地得到所有位数上的数字,就必须用软件的方法来实现大整数的算术运算。
请设计一个有效的算法,可以进行两个n位大整数的乘法运算。
设X和Y都是n位的二进制整数,现在要计算它们的乘积XY。我们可以用小学所学的方法来设计一个计算乘积XY的算法,但是这样做计算步骤太多,显得效率较低。如果将每2个1位数的乘法或加法看作一步运算,那么这种方法要作O(n2)步运算才能求出乘积XY。下面我们用分治法来设计一个更有效的大整数乘积算法。

图6-3 大整数X和Y的分段
我们将n位的二进制整数X和Y各分为2段,每段的长为n/2位(为简单起见,假设n是2的幂),如图6-3所示。
由此,X=A2n/2+B,Y=C2n/2+D。这样,X和Y的乘积为:
XY=(A2n/2+B)(C2n/2+D)=AC2n+(AD+CB)2n/2+BD     (1)
如果按式(1)计算XY,则我们必须进行4次n/2位整数的乘法(AC,AD,BC和BD),以及3次不超过n位的整数加法(分别对应于式(1)中的加号),此外还要做2次移位(分别对应于式(1)中乘2n和乘2n/2)。所有这些加法和移位共用O(n)步运算。设T(n)是2个n位整数相乘所需的运算总数,则由式(1),我们有:
                   (2)
由此可得T(n)=O(n2)。因此,用(1)式来计算X和Y的乘积并不比小学生的方法更有效。要想改进算法的计算复杂性,必须减少乘法次数。为此我们把XY写成另一种形式:
XY=AC2n+[(A-B)(D-C)+AC+BD]2n/2+BD          (3)
虽然,式(3)看起来比式(1)复杂些,但它仅需做3次n/2位整数的乘法(AC,BD和(A-B)(D-C)),6次加、减法和2次移位。由此可得:
                       (4)
用解递归方程的套用公式法马上可得其解为T(n)=O(nlog3)=O(n1.59)。利用式(3),并考虑到X和Y的符号对结果的影响,我们给出大整数相乘的完整算法MULT如下:
function MULT(X,Y,n); {X和Y为2个小于2n的整数,返回结果为X和Y的乘积XY}
begin
S=SIGN(X)*SIGN(Y); {S为X和Y的符号乘积}
X=ABS(X);
Y=ABS(Y); {X和Y分别取绝对值}
if n=1 then
if (X=1)and(Y=1) then return(S)
else return(0)
else begin
A=X的左边n/2位;
B=X的右边n/2位;
C=Y的左边n/2位;
D=Y的右边n/2位;
ml=MULT(A,C,n/2);
m2=MULT(A-B,D-C,n/2);
m3=MULT(B,D,n/2);
S=S*(m1*2n+(m1+m2+m3)*2n/2+m3);
return(S);
end;
end;
上述二进制大整数乘法同样可应用于十进制大整数的乘法以提高乘法的效率减少乘法次数。
【问题】   最接近点对问题
问题描述:
在应用中,常用诸如点、圆等简单的几何对象代表现实世界中的实体。在涉及这些几何对象的问题中,常需要了解其邻域中其他几何对象的信息。例如,在空中交通控制问题中,若将飞机作为空间中移动的一个点来看待,则具有最大碰撞危险的2架飞机,就是这个空间中最接近的一对点。这类问题是计算几何学中研究的基本问题之一。下面我们着重考虑平面上的最接近点对问题。
最接近点对问题的提法是:给定平面上n个点,找其中的一对点,使得在n个点的所有点对中,该点对的距离最小。
严格地说,最接近点对可能多于1对。为了简单起见,这里只限于找其中的一对。
这个问题很容易理解,似乎也不难解决。我们只要将每一点与其他n-1个点的距离算出,找出达到最小距离的两个点即可。然而,这样做效率太低,需要O(n2)的计算时间。我们能否找到问题的一个O (nlogn)算法。
这个问题显然满足分治法的第一个和第二个适用条件,我们考虑将所给的平面上n个点的集合S分成2个子集S1和S2,每个子集中约有n/2个点,然后在每个子集中递归地求其最接近的点对。在这里,一个关键的问题是如何实现分治法中的合并步骤,即由S1和S2的最接近点对,如何求得原集合S中的最接近点对,因为S1和S2的最接近点对未必就是S的最接近点对。如果组成S的最接近点对的2个点都在S1中或都在S2中,则问题很容易解决。但是,如果这2个点分别在S1和S2中,则对于S1中任一点p,S2中最多只有n/2个点与它构成最接近点对的候选者,仍需做n2/4次计算和比较才能确定S的最接近点对。因此,依此思路,合并步骤耗时为O(n2)。整个算法所需计算时间T(n)应满足:
T(n)=2T(n/2)+O(n2)
它的解为T(n)=O(n2),即与合并步骤的耗时同阶,显示不出比用穷举的方法好。从解递归方程的套用公式法,我们看到问题出在合并步骤耗时太多。这启发我们把注意力放在合并步骤上。
为了使问题易于理解和分析,我们先来考虑一维的情形。此时S中的n个点退化为x轴上的n个实数x1、x2、…、xn。最接近点对即为这n个实数中相差最小的2个实数。我们显然可以先将x1、x2、…、xn排好序,然后,用一次线性扫描就可以找出最接近点对。这种方法主要计算时间花在排序上,因此如在排序算法中所证明的,耗时为O(nlogn)。然而这种方法无法直接推广到二维的情形。因此,对这种一维的简单情形,我们还是尝试用分治法来求解,并希望能推广到二维的情形。
假设我们用x轴上某个点m将S划分为2个子集S1和S2,使得S1={x∈S | x≤m};S2={x∈S | x>m}。这样一来,对于所有p∈S1和q∈S2有p<q。
递归地在S1和S2上找出其最接近点对{p1,p2}和{q1,q2},并设δ=min{|p1-p2|,|q1-q2|},S中的最接近点对或者是{p1,p2},或者是{q1,q2},或者是某个{p3,q3},其中p3∈S1且q3∈S2。如图1所示。

图1 一维情形的分治法
我们注意到,如果S的最接近点对是{p3,q3},即 | p3-q3 | < δ,则p3和q3两者与m的距离不超过δ,即 | p3-m | < δ,| q3-m | < δ,也就是说,p3∈(m-δ,m),q3∈(m,m+δ)。由于在S1中,每个长度为δ的半闭区间至多包含一个点(否则必有两点距离小于δ),并且m是S1和S2的分割点,因此(m-δ,m)中至多包含S中的一个点。同理,(m,m+δ)中也至多包含S中的一个点。由图1可以看出,如果(m-δ,m)中有S中的点,则此点就是S1中最大点。同理,如果(m,m+δ)中有S中的点,则此点就是S2中最小点。因此,我们用线性时间就能找到区间(m-δ,m)和(m,m+δ)中所有点,即p3和q3。从而我们用线性时间就可以将S1的解和S2的解合并成为S的解。也就是说,按这种分治策略,合并步可在O(n)时间内完成。这样是否就可以得到一个有效的算法了呢?
还有一个问题需要认真考虑,即分割点m的选取,及S1和S2的划分。选取分割点m的一个基本要求是由此导出集合S的一个线性分割,即S=S1∪S2 ,S1∩S2=Φ,且S1 {x | x≤m};S2 {x | x>m}。容易看出,如果选取m=[max(S)+min(S)]/2,可以满足线性分割的要求。选取分割点后,再用O(n)时间即可将S划分成S1={x∈S | x≤m}和S2={x∈S | x>m}。然而,这样选取分割点m,有可能造成划分出的子集S1和S2的不平衡。例如在最坏情况下,|S1|=1,|S2|=n-1,由此产生的分治法在最坏情况下所需的计算时间T(n)应满足递归方程:
T(n)=T(n-1)+O(n)
它的解是T(n)=O(n2)。这种效率降低的现象可以通过分治法中“平衡子问题”的方法加以解决。也就是说,我们可以通过适当选择分割点m,使S1和S2中有大致相等个数的点。自然地,我们会想到用S的n个点的坐标的中位数来作分割点。在选择算法中介绍的选取中位数的线性时间算法使我们可以在O(n)时间内确定一个平衡的分割点m。
至此,我们可以设计出一个求一维点集S中最接近点对的距离的算法pair如下。
Float pair(S);
{   if | S | =2   δ= | x[2]-x[1] |       /*x[1..n]存放的是S中n个点的坐标*/
else 
{   if ( | S | =1)   δ=∞
       else
{   m=S中各点的坐标值的中位数;
           构造S1和S2,使S1={x∈S | x≤m},S2={x∈S | x>m};
δ1=pair(S1);
             δ2=pair(S2);
            p=max(S1);
            q=min(S2);
            δ=min(δ1,δ2,q-p);
}
return(δ);
}
由以上的分析可知,该算法的分割步骤和合并步骤总共耗时O(n)。因此,算法耗费的计算时间T(n)满足递归方程:

解此递归方程可得T(n)=O(nlogn)。

【问题】循环赛日程表
问题描述:设有n=2k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能参赛一次;
(3)循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。
按分治策略,我们可以将所有的选手分为两半,则n个选手的比赛日程表可以通过n/2个选手的比赛日程表来决定。递归地用这种一分为二的策略对选手进行划分,直到只剩下两个选手时,比赛日程表的制定就变得很简单。这时只要让这两个选手进行比赛就可以了。

                                       1   2   3   4   5   6   7
                                    1   2   3   4   5   6   7   8
                                    2   1   4   3   6   7   8   5
                                    3   4   1   2   7   8   5   6
                  1   2   3            4   3   2   1   8   5   6   7
               1   2   3   4            5   6   7   8   1   4   3   2
   1            2   1   4   3            6   5   8   7   2   1   4   3
1   2            3   4   1   2            7   8   5   6   3   2   1   4
2   1            4   3   2   1            8   7   6   5   4   3   2   1
(1)                (2)                           (3)
图1  2个、4个和8个选手的比赛日程表
图1所列出的正方形表(3)是8个选手的比赛日程表。其中左上角与左下角的两小块分别为选手1至选手4和选手5至选手8前3天的比赛日程。据此,将左上角小块中的所有数字按其相对位置抄到右下角,又将左下角小块中的所有数字按其相对位置抄到右上角,这样我们就分别安排好了选手1至选手4和选手5至选手8在后4天的比赛日程。依此思想容易将这个比赛日程表推广到具有任意多个选手的情形。

(以下文中有来自对互联网内容的引用,在此作者对原作者表示感谢!--bigLeo)
看到const 关键字,很多程序员想到的可能是const 常量,这可有点象踩到陷井上还不知道自己危险了。读读以下文字会使你对c++中的const有一个全面的认识。

const 是C++中常用的类型修饰符,有某些微妙的应用场合,如果没有搞清本源,则错误在所难免。本篇中将对const进行辨析。溯其本源,究其实质,希望能对大家理解const有所帮助,根据思维的承接关系,分为如下几个部分进行阐述。

C++中为什么会引入const

  C++的提出者当初是基于什么样的目的引入(或者说保留)const关键字呢?,这是一个有趣又有益的话题,对理解const很有帮助。

1. 大家知道,C++有一个类型严格的编译系统,这使得C++程序的错误在编译阶段即可发现许多,从而使得出错率大为减少,因此,也成为了C++与C相比,有着突出优点的一个方面。

2. C中很常见的预处理指令 #define VariableName VariableValue 可以很方便地进行值替代,这种值替代至少在三个方面优点突出:

  一是避免了意义模糊的数字出现,使得程序语义流畅清晰,如下例:
  #define USER_NUM_MAX 107 这样就避免了直接使用107带来的困惑。

  二是可以很方便地进行参数的调整与修改,如上例,当人数由107变为201时,进改动此处即可,

  三是提高了程序的执行效率,由于使用了预编译器进行值替代,并不需要为这些常量分配存储空间,所以执行的效率较高。

  鉴于以上的优点,这种预定义指令的使用在程序中随处可见。

3. 说到这里,大家可能会迷惑上述的1点、2点与const有什么关系呢?,好,请接着向下看来:

  预处理语句虽然有以上的许多优点,但它有个比较致命的缺点,即,预处理语句仅仅只是简单值替代,缺乏类型的检测机制。这样预处理语句就不能享受C++严格类型检查的好处,从而可能成为引发一系列错误的隐患。

4.好了,第一阶段结论出来了:
结论: Const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。

现在它的形式变成了:

Const DataType VariableName = VariableValue ;
为什么const能很好地取代预定义语句?
const 到底有什么大神通,使它可以振臂一挥取代预定义语句呢?

1. 首先,以const 修饰的常量值,具有不可变性,这是它能取代预定义语句的基础。

2. 第二,很明显,它也同样可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。

3. 第三,C++的编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高,同时,这也是它取代预定义语句的重要基础。

这里,我要提一下,为什么说这一点是也是它能取代预定义语句的基础,这是因为,编译器不会去读存储的内容,如果编译器为const分配了存储空间,它就不能够成为一个编译期间的常量了。

4. 最后,const定义也像一个普通的变量定义一样,它会由编译器对它进行类型的检测,消除了预定义语句的隐患。

const 使用情况分类详析

1.const 用于指针的两种情况分析:
 int const *A;  //A可变,*A不可变
 int *const A;  //A不可变,*A可变

  分析:const 是一个左结合的类型修饰符,它与其左侧的类型修饰符和为一个类型修饰符,所以,int const 限定 *A,不限定A。int *const 限定A,不限定*A。

2.const 限定函数的传递值参数:

 void Fun(const int Var);

  分析:上述写法限定参数在函数体中不可被改变。由值传递的特点可知,Var在函数体中的改变不会影响到函数外部。所以,此限定与函数的使用者无关,仅与函数的编写者有关。
结论:最好在函数的内部进行限定,对外部调用者屏蔽,以免引起困惑。如可改写如下:

void Fun(int Var){
const int & VarAlias = Var;

VarAlias ….

…..

}

3.const 限定函数的值型返回值:

const int Fun1();

const MyClass Fun2();

 分析:上述写法限定函数的返回值不可被更新,当函数返回内部的类型时(如Fun1),已经是一个数值,当然不可被赋值更新,所以,此时const无意义,最好去掉,以免困惑。当函数返回自定义的类型时(如Fun2),这个类型仍然包含可以被赋值的变量成员,所以,此时有意义。

4. 传递与返回地址: 此种情况最为常见,由地址变量的特点可知,适当使用const,意义昭然。

5. const 限定类的成员函数:

class ClassName {

 public:

  int Fun() const;

 …..

}

 
注意:采用此种const 后置的形式是一种规定,是因为const修饰函数的位置都已经被占据了,不得以,才采取了这么别扭的方式。在此函数的声明中和定义中均要使用const,因为const已经成为类型信息的一部分。

获得能力:可以操作常量对象。

失去能力:不能修改类的数据成员,不能在函数中调用其他不是const的函数。

  这里不是C++的教科书,只是想详细地阐述它的实质和用处。 我会尽量说的很详细,因为我希望在一种很轻松随意的气氛中说出自己的某些想法,毕竟,编程也是轻松,快乐人生的一部分。有时候,你会惊叹这其中的世界原来是如此的精美。

由于阶乘运算的增长速度特别快(比2^n的增长速度快),对于较小整数的阶乘运算采用简单的递规算法可以实现,但是对于大整数的乘法(比如1000!),则传统的递规算法就失去了作用。
定义一个很长的数组,用数组的每一项表示计算结果的每一位。例如,7!=5040,a[1000],则a[0]=0,a[1]=4,a[2]=0,a[3]=5。

程序源代码:

/**
*计算大数的阶乘,算法的主要思想就是将计算结果的每一位用数组的一位来表示:如要计算5!,那么首先将
*(1) a[0]=1,然后a[0]=a[0]*2,a[0]=2,
*(2) a[0]=a[0]*3,a[0]=6
*(3) a[0]=a[0]*4,a[0]=24,此时a[1]=2,a[0]=4
*/
public class Factorial
{
static int a[] = new int [10000];
static void factorial(int n)
{
for(int i=2; i< a.length; i++)
a[i] = 0; //将数组元素初始化
a[0] = 1; //用数组的一项存放计算结果的位数
a[1] = 1; //将第一项赋值为一
for(int j= 2; j <= n; j++)
{
int i=1;
int c = 0; //c表示向高位的进位
for(; i <= a[0]; i++)
{
a[i] = a[i] * j + c;//将来自低位的计算结果和本位的结果相加
c = a[i] / 10;
a[i] = a[i] % 10;
}
for(; c != 0; i++)
{
a[i] = c%10;
c = c / 10;
}
a[0] = i – 1;
}
}
public static void main(String[] args)
{
String num = args[0];

int count = 0;
int n = Integer.parseInt(num);
f(n);
for(int i= a[0]; i>0; i–)
{

count++;
System.out.print(/*"a[" + i + "]=" + */a[i]/* + " "*/);
}
System.out.println("\n"+count);
}
}

阶乘算法(0—10000)

#include<stdlib.h>
#include<iostream.h>
#include<iomanip.h>

const int N=1000;

int compute(unsigned int *s,int n)//s用来存储一次的计算结果,n为本次计算的乘数,函数返回结果中有效数据的节数
{
unsigned long p; //暂时存放一节的结果
unsigned long more=0;//一次乘法的进位
int i; //循环变量

static int m=1;//存放结果中的节数,三位一节
static int q=0;//存放结果中最后为零的节数

//计算本次的结果
for(i=q;i<m;i++)
{
p=(long)s[i]*(long)n+more;
more=p/N;
s[i]=p-more*N;
}

//计算结果中最后为零的节数
while(s[q]==0) q++;

//处理最高位
for(more=p/N;more;)
{
p=more;
more=p/N;
s[i++]=p-more*N;
m++; //有效节数增一
}

return m; //返回有效节数
}

void main()
{
unsigned int *s; //存放结果
int i; //循环变量
int m; //存放节数
int n; //求n的阶乘

cout<<"请输入一个正整数<0—-10000>:";
cin>>n;

if(n<0)
{
cout<<"输入数据错误!"<<endl;
return;
}
else if(n==0)
{
cout<<"0!==1"<<endl;
return;
}

//初始化s
s=(unsigned int *)malloc(n*sizeof(s)*10);
for(i=1;i<n;i++) s[i]=0;
s[0]=1;

//计算结果
for(i=2;i<=n;i++) m=compute(s,i);

while(s[m]==0) m–;//过滤掉前面的零

//输出结果
cout<<n<<"!=="<<s[m--];//最高一节

int num=1;
for(i=m;i>=0;i–)
{
num++;
cout<<’,'<<setw(3)<<setfill(‘0′)<<s[i];
if((num%10)==0) cout<<endl;
}
cout<<endl;

//释放空间
free(s);
}
“阶乘”即从1开始的连续自然数相乘的积,即n!=1*2*3…*n,这就是它的计算公式。
由于阶乘运算的增长速度特别快(比2^n的增长速度快),对于较小的正整数的阶乘运算可采用简单的笔算,但是对于较大的正整数可用计算器或计算机。注意:当正整数过大时,计算器和计算机也算不出来!
当然,有一个近似的公式,斯特林公式:n!≈[√(2πn)[*(n/e)^n ,π和e是无理数,π≈3.14159 ,e≈2.71828
斯特林公式的最大好处就是把阶乘化成了指数的形式,在精度不高的情况下,可以使用。

0!=1(规定)
1!=1
2!=1*2=2
3!=1*2*3=6
4!=1*2*3*4=24
5!=1*2*3*4*5=120
6!=1*2*3*4*5*6=720