C++字符串格式化 sprintf、printf

【区别】由于sprintf 跟printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。

sprintf 是个变参函数,定义如下:
int sprintf( char *buffer, const char *format [, argument] ... );除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:格式化字符串上。

printf 和sprintf 都使用格式化字符串来指定串的格式,在格式串内部使用一些以"%"开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要 的字符串。
【格式化数字字符串】
sprintf 最常见的应用之一莫过于把整数打印到字符串中,所以,spritnf 在大多数场合可以替代itoa
【示例】:
//把整数123 打印成一个字符串保存在s 中。
sprintf(s, "%d", 123); //产生"123"可以指定宽度,不足的左边补空格
sprintf(s, "%8d%8d", 123, 4567); //产生:" 123 4567"当然也可以左对齐:
sprintf(s, "%-8d%8d", 123, 4567); //产生:"123 4567"
也可以按照16 进制打印:
sprintf(s, "%8x", 4567); //小写16 进制,宽度占8 个位置,右对齐
sprintf(s, "%-8X", 4568); //大写16 进制,宽度占8 个位置,左对齐
这样,一个整数的16 进制字符串就很容易得到,但我们在打印16 进制内容时,通常想要一种左边补0 的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0 就可以了。
sprintf(s, "%08X", 4567); //产生:"000011D7"
上面以"%d"进行的10 进制打印同样也可以使用这种左边补0 的方式。

这里要注意一个符号扩展的问题:比如,假如想打印短整数(short)-1 的内存16 进制表示形式,在Win32 平台上,一个short 型占2 个字节,所以我们自然希望用4 个16 进制数字来打印它:
short si = -1;
sprintf(s, "%04X", si);
产生"FFFFFFFF",怎么回事?因为 sprintf 是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个"%X"就能得知当初函数调用前参数压栈时被压进来的到底 是个4 字节的整数还是个2 字节的短整数,所以采取了统一4 字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32 位的整数-1,打印时4 个位置不够了,就把32 位整数-1 的8 位16 进制都打印出来了。
如果你想看si 的本来面目,那么就应该让编译器做0 扩展而不是符号扩展(扩展时二进制左边补0 而不是补符号位)
sprintf(s, "%04X", (unsigned short)si);
就可以了。或者:
unsigned short si = -1;
sprintf(s, "%04X", si);
sprintf 和 printf 还可以按8 进制打印整数字符串,使用"%o"。注意8 进制和16 进制都不会打印出负数,都是无符号的,实际上也就是变量的内部编码的直接的16 进制或8 进制表示。

【控制浮点数打印格式】
浮点数的打印和格式控制是sprintf 的又一大常用功能,浮点数使用格式符"%f"控制,默认保 留小数点后6 位数字,比如:
sprintf(s, "%f", 3.1415926); //产生"3.141593"
但有时我们希望自己控制打印的宽度和小数位数,这时就应该使用:"%m.nf"格式,其中m 表 打印的宽度,n 表示小数点后的位数。比如:
sprintf(s, "%10.3f", 3.1415626); //产生:" 3.142"
sprintf(s, "%-10.3f", 3.1415626); //产生:"3.142 "
sprintf(s, "%.3f", 3.1415626); //不指定总宽度,产生:"3.142" 

注意一个问题,你猜
int i = 100;
sprintf(s, "%.2f", i);

会打出什么东东来?"100.00"?对吗?自己试试就知道了,同时也试试下面这个:
sprintf(s, "%.2f", (double)i);
第 一个打出来的肯定不是正确结果,原因跟前面提到的一样,参数压栈时调用者并不知道跟i相对应的格式控制符是个"%f"。而函数执行时函数本身则并不知道当 年被压入栈里的是个整数,于是可怜的保存整数i 的那4 个字节就被不由分说地强行作为浮点数格式来解释了,整个乱套了。不过,如果有人有兴趣使用手工编码一个浮点数,那么倒可以使用这种方法来检验一下你手工编 排的结果是否正确。

[字符//Ascii码对照]
在C/C++语言中,char 也是一种普通的scalable 类型,除了字长之外,它与short,int,long 这些类型没有本质区别,只不过被大家习惯用来表示字符和字符串而已。

于是,使用"%d"或者"%x"打印一个字符,便能得出它的10 进制或16 进制的ASCII 码;反过来,使用"%c"打印一个整数,便可以看到它所对应的ASCII 字符。以下程序段把所有可见字符的ASCII 码对照表打印到屏幕上(这里采用printf,注意"#"与"%X"合用时自动为16 进制数增加"0X"前缀):
for(int i = 32; i < 127; i++) {
   printf("[ %c ]: %3d 0x%#04X\n", i, i, i);
}


【连接字符串】
sprintf 的格式控制串中既然可以插入各种东西,并最终把它们"连成一串",自然也就能够连接字符串,从而在许多场合可以替代strcat,但sprintf 能够一次连接多个字符串(自然也可以同时在它们中间插入别的内容,总之非常灵活)。比如:
char* who = "I";
char* whom = "CSDN";
sprintf(s, "%s love %s.", who, whom); //产生:"I love CSDN. "
strcat 只能连接字符串(一段以''结尾的字符数组或叫做字符缓冲,null-terminated-string),但有时我们有两段字符缓冲区,他们并不是以 '/0'结尾。比如许多从第三方库函数中返回的字符数组,从硬件或者网络传输中读进来的字符流,它们未必每一段字符序列后面都有个相应的'/0'来结尾。如果直接 连接,不管是sprintf 还是strcat 肯定会导致非法内存操作,而strncat 也至少要求第一个参数是个null-terminated-string,那该怎么办呢?我们自然会想起前面介绍打印整数和浮点数时可以指定宽度,字符串 也一样的。比如:
char a1[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
char a2[] = {'H', 'I', 'J', 'K', 'L', 'M', 'N'};
如果:
sprintf(s, "%s%s", a1, a2); //Don't do that!
十有八九要出问题了。是否可以改成:
sprintf(s, "%7s%7s", a1, a2);
也没好到哪儿去,正确的应该是:
sprintf(s, "%.7s%.7s", a1, a2);//产生:"ABCDEFGHIJKLMN"
这 可以类比打印浮点数的"%m.nf",在"%m.ns"中,m 表示占用宽度(字符串长度不足时补空格,超出了则按照实际宽度打印),n 才表示从相应的字符串中最多取用的字符数。通常在打印字符串时m 没什么大用,还是点号后面的n 用的多。自然,也可以前后都只取部分字符:
sprintf(s, "%.6s%.5s", a1, a2);//产生:"ABCDEFHIJKL"
在许多时候,我们或许还希望这些格式控制符中用以指定长度信息的数字是动态的,而不是静态指定的,因为许多时候,程序要到运行时才会清楚到底需要取字符数组中的几个字符,这种动态的宽度/精度设置功能在sprintf 的实现中也被考虑到了,sprintf 采用"*"来占用一个本来需要一个指定宽度或精度的常数数字的位置,同样,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来,于是,上面的例子 可以变成:
sprintf(s, "%.*s%.*s", 7, a1, 7, a2);
或者:
sprintf(s, "%.*s%.*s", sizeof(a1), a1, sizeof(a2), a2);
实际上,前面介绍的打印字符、整数、浮点数等都可以动态指定那些常量值,比如:
sprintf(s, "%-*d", 4, 'A'); //产生"65 "
sprintf(s, "%#0*X", 8, 128); //产生"0X000080","#"产生0X
sprintf(s, "%*.*f", 10, 2, 3.1415926); //产生" 3.14"
【打印地址信息】
有时调试程序时,我们可能想查看某些变量或者成员的地址,由于地址或者指针也不过是个32 位的数,你完全可以使用打印无符号整数的"%u"把他们打印出来:
sprintf(s, "%u", &i);
不过通常人们还是喜欢使用16 进制而不是10 进制来显示一个地址:
sprintf(s, "%08X", &i);
然而,这些都是间接的方法,对于地址打印,sprintf 提供了专门的"%p":
sprintf(s, "%p", &i);
我觉得它实际上就相当于:
sprintf(s, "%0*x", 2 * sizeof(void *), &i);
利用sprintf 的返回值
较少有人注意printf/sprintf 函数的返回值,但有时它却是有用的, sprintf 返回了本次函数调用最终打印到字符缓冲区中的字符数目。也就是说每当一次 sprintf 调用结束以后,你无须再调用一次strlen 便已经知道了结果字符串的长度。如:int len = sprintf(s, "%d", i);对于正整数来说,len 便等于整数i 的10 进制位数。下面的是个完整的例子,产生10 个[0, 100)之间的随机数,并将他们打印到一个字符数组s 中,以逗号分隔开。

int main() {
     srand(time(0));
     char s[64];
     int offset = 0;
     for(int i = 0; i < 10; i++) {
          offset += sprintf(s + offset, "%d,", rand() % 100);
     }
     s[offset - 1] = '\n';//将最后一个逗号换成换行符。
     printf(s);
     return 0;
}
设想当你从数据库中取出一条记录,然后希望把他们的各个字段按照某种规则连接成一个字符串时,就可以使用这种方法,从理论上讲,他应该比不断的 strcat 效率高,因为strcat 每次调用都需要先找到最后的那个'/0'的位置,而在上面给出的例子中,我们每次都利用sprintf 返回值把这个位置直接记下来了。
常见问题
sprintf 是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访问错误,但好在由sprintf 误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通常用眼睛再把出错的代码多看几眼就看出来了。 
  1. 缓冲区溢出
    第一个参数的长度太短了,没的说,给个大点的地方吧。当然也可能是后面的参数的问题.
    建议变参对应一定要细心,而打印字符串时,尽量使用"%.ns"的形式指定最大字符数
  2. 忘记了第一个参数低级得不能再低级问题,用printf 用得太惯了。
  3. 变参对应出问题
    通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位,检查检查吧。尤其是对应"*"的那些参数,都提供了吗?不要把一个整数对应一个"%s",编译器会觉得你欺她太甚了(编译器是obj 和exe 的妈妈)。

其他


strftime   专门用于格式化时间字符串的,还要调用者指定缓冲区的最大长度。
这里举个例子:
time_t t = time(0);
// 产生"YYYY-MM-DD hh:mm:ss"格式的字符串。
char s[32];
strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", localtime(&t));



printf大家都耳熟能详,但是能真正将其用法弄透的估计很少见。

1.转换说明符
%a(%A)     
浮点数、十六进制数字和p-(P-)记数法(C99)
%c         
字符
%d         
有符号十进制整数
%f         
浮点数(包括floatdoulbe)
%e(%E)     
浮点数指数输出[e-(E-)记数法]
%g(%G)     
浮点数不显无意义的零"0"
%i         
有符号十进制整数(%d相同)
%u         
无符号十进制整数
%o         
八进制整数
%x(%X)     
十六进制整数0f(0F)   e.g.   0x1234
%p         
指针
%s         
字符串
%%         
输出字符%

2.标志
左对齐:"-"   比如:"%-20s"
右对齐:"+"   比如:"%+20s"
空格:若符号为正,则显示空格,负则显示"-"  比如:"% 6.2f"      
#
:对c,s,d,u类无影响;对o类,在输出时加前缀o;对x类,在输出时加前缀0x;对e,g,f 类当结果有小数时才给出小数点。

printf的格式控制的完整格式:
% - 0 m.n l
格式字符

下面对组成格式说明的各项加以说明:

①%:表示格式说明的起始符号,不可缺少。
②-
:有-表示左对齐输出,如省略表示右对齐输出。
③0
:有0表示指定空位填0,如省略表示指定空位不填。
④m.n
m指域宽,即对应的输出项在输出设备上所占的字符数。N指精度。用于说明输出的实型数的小数位数。未指定n时,隐含的精度为n=6位。
⑤l
h:l对整型指long型,对实型指double型。h用于将整型的格式字符修正为short型。

格式字符 
格式字符用以指定输出项的数据类型和输出格式。
①d
格式:用来输出十进制整数。有以下几种用法:
%d
:按整型数据的实际长度输出。
%md
m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。
%ld
:输出长整型数据。
②o
格式:以无符号八进制形式输出整数。对长整型可以用"%lo"格式输出。同样也可以指定字段宽度用“%mo”格式输出。
例:
main()
{ int a = -1;
printf("%d, %o", a, a);
}
运行结果:-1,177777
程序解析:-1在内存单元中(以补码形式存放)为(1111111111111111)2,转换为八进制数为(177777)8
③x
格式:以无符号十六进制形式输出整数。对长整型可以用"%lx"格式输出。同样也可以指定字段宽度用"%mx"格式输出。
④u
格式:以无符号十进制形式输出整数。对长整型可以用"%lu"格式输出。同样也可以指定字段宽度用“%mu”格式输出。
⑤c
格式:输出一个字符。
⑥s
格式:用来输出一个串。有几中用法
%s
:例如:printf("%s", "CHINA")输出"CHINA"字符串(不包括双引号)。
%ms
:输出的字符串占m列,如字符串本身长度大于m,则突破获m的限制,将字符串全部输出。若串长小于m,则左补空格。
%-ms
:如果串长小于m,则在m列范围内,字符串向左靠,右补空格。
%m.ns
:输出占m列,但只取字符串中左端n个字符。这n个字符输出在m列的右侧,左补空格。
%-m.ns
:其中mn含义同上,n个字符输出在m列范围的左侧,右补空格。如果n>m,则自动取n值,即保证n个字符正常输出。
⑦f
格式:用来输出实数(包括单、双精度),以小数形式输出。有以下几种用法:
%f
:不指定宽度,整数部分全部输出并输出6位小数。
%m.nf
:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。 
%-m.nf
:输出共占n列,其中有n位小数,如数值宽度小于m右端补空格。
⑧e
格式:以指数形式输出实数。可用以下形式:
%e
:数字部分(又称尾数)输出6位小数,指数部分占5位或4位。
%m.ne
%-m.nemn”-”字符含义与前相同。此处n指数据的数字部分的小数位数,m表示整个输出数据所占的宽度。
⑨g
格式:自动选f格式或e格式中较短的一种输出,且不输出无意义的零。

---------------------------------------
关于printf函数的进一步说明:
如果想输出字符"%",则应该在格式控制字符串中用连续两个%表示,如:
printf("%f%%", 1.0/3);
输出0.333333%

对于单精度数,使用%f格式符输出时,仅前7位是有效数字,小数6位.
对于双精度数,使用%lf格式符输出时,前16位是有效数字,小数6位.

对于m.n的格式还可以用如下方法表示
char ch[20];
printf("%*.*s\n",m,n,ch);
前边的*定义的是总的宽度,后边的*定义的是输出的个数。分别对应外面的参数m。我想这种方法的好处是可以在语句之外对参数mn赋值,从而控制输出格式。


一种输出格式 %n 可以将所输出字符串的长度值赋绐一个变量见下例:

int slen;

printf("hello world%n", &slen);

执行后变量被赋值为11



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
看看格式,再看看大小,应该看出它的含金量,200多页吧,不多。<br>部分:<br>1.多态类中的虚函数表是Compile-Time,还是Run-Time时建立的?<br>答案:虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组.而对象的隐藏成员--虚拟函数表指针是在运行期--也就是构造函数被调用时进行初始化的,这是实现多态的关键.<br><br>2.将一个 1M -10M 的文件,逆序存储到另一个文件,就是前一个文件的最后一个 <br>字符存到新文件的第一个字符,以此类推。<br><br>3.main主函数执行完毕后,是否可能会再执行一段代码?(朗讯的一道笔试题)<br>答案:可以,可以用_onexit 注册一个函数,它会在main 之后执行;<br>如果你需要加入一段在main退出后执行的代码,可以使用atexit()函数,注册一个函数。 <br> 语法: <br> #include <stdlib.h> <br> int atexit(void (*function")(void)); <br> #include <stdlib.h> <br> #include <stdio.h> <br> void fn1( void ), fn2( void ), fn3( void ), fn4( void ); <br> int main( void ) <br> { <br> atexit( fn1 ); <br> atexit( fn2 ); <br> atexit( fn3 ); <br> atexit( fn4 ); <br> printf( "This is executed first.\n" ); <br> } <br> void fn1() <br> { <br> printf( "next.\n" ); <br> } <br> void fn2() <br> { <br> printf( "executed " ); <br> } <br> void fn3() <br> { <br> printf( "is " ); <br> } <br> void fn4() <br> { <br> printf( "This " ); <br> } <br>结果:<br>This is executed first. <br> This is executed next. <br><br>4.一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态? <br>在子类的空间里,有没有父类的这个函数,或者父类的私有变量? (华为笔试题)<br>答案:只要基类在定义成员函数时已经声明了virtue关键字,在派生类实现的时候覆盖该函数时,virtue关键字可加可不加,不影响多态的实现。子类的空间里有父类的所有变量(static除外)。<br><br>5.给一个字符串、例如 “ababc”要求返回“ab”. 因为“ab”连续重复出现且最长。<br> 用C/C++语言写一函数完成该算法,给出复杂度<br><br>6.对序列1、1、2、3、5、8、13。。。。 是Fab..数列<br> 2、3、5、13...是Fab..质数数列,因为他们与自己前面的Fab...数列都互质<br>给出k,返回第k小的Fab..质数<br><br>7.101个硬币100真、1假,真假区别在于重量。请用无砝码天平称两次给出真币重还是假币<br>重的结论。<br><br>8.完成字符串拷贝可以使用 sprintf、strcpy 及 memcpy 函数,请问这些函数有什么区别<br>,你喜欢使用哪个,为什么?<br>答案:这些函数的区别在于 实现功能 以及 操作对象 不同。<br>1.strcpy 函数操作的对象是 字符串,完成 从 源字符串 到 目的字符串 的 拷贝 功能。<br>2.snprintf 函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。<br>3.memcpy 函数顾名思义就是 内存拷贝,实现 将一个 内存块 的内容复制到另一个 内存块 这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy 的操作对象不局限于某一类数据类型,或者说可 适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于 memcpy 函数等长拷贝的特点以及数据类型代表的物理意义,memcpy 函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。<br><br>对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:<br>• strcpy 无疑是最合适的选择:效率高且调用方便。<br>• snprintf 要额外指定格式符并且进行格式转化,麻烦且效率不高。<br>• memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 + 1),还会带来性能的下降。其实 strcpy 函数一般是在内部调用 memcpy 函数或者用汇编直接实现的,以达到高效的目的。因此,使用 memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。<br>对于非字符串类型的数据的复制来说,strcpy 和 snprintf 一般就无能为力了,可是对 memcpy 却没有什么影响。但是,对于基本数据类型来说,尽管可以用 memcpy 进行拷贝,由于有赋值运算符可以方便且高效地进行同种或兼容类型的数据之间的拷贝,所以这种情况下 memcpy 几乎不被使用。memcpy 的长处是用来实现(通常是内部实现居多)对结构或者数组的拷贝,其目的是或者高效,或者使用方便,甚或两者兼有。<br>9.变量的声明和定义有什么区别?<br>10.请写出下面代码在 32 位平台上的运行结果,并说明 sizeof 的性质:<br>#include <stdio.h><br>#include <stdlib.h><br><br>int main(void)<br>{<br> char a[30];<br> char *b = (char *)malloc(20 * sizeof(char));<br> printf("%d\n", sizeof(a));<br> printf("%d\n", sizeof(b));<br> printf("%d\n", sizeof(a[3]));<br> printf("%d\n", sizeof(b+3));<br> printf("%d\n", sizeof(*(b+4)));<br> return 0 ;<br>}<br><br>12.请完成以下题目。注意,请勿直接调用 ANSI C 函数库中的函数实现。<br> a)请编写一个 C 函数,该函数给出一个字节中被置 1 的位的个数,并请给出该题的至少一个不同解法。<br>第一种unsigned int TestAsOne0(char log) <br> { <br> int i; <br> unsigned int num=0, val; <br> for(i=0; i<8; i++) <br> { <br> val = log >> i; //移位<br> val &= 0x01; //与1相与<br> if(val) <br> num++; <br> } <br> return num; <br> } <br>第二种unsigned int TestAsOne1(char log) <br> { <br> int i; <br> unsigned int num=0, val; <br> for(i=0; i<8; i++) <br> { <br> val = (~log) >> i; //反码? <br> val &= 0x00; //与0相与<br> if(!val) <br> num++; <br> } <br> return num; <br> } <br> b)请编写一个 C 函数,该函数将给定的一个字符串转换成整数。<br>int Invert(char *str) <br>{ <br>int num=0; <br>while(*str!='\0') <br>{ <br>int digital=*str-48; <br>num=num*10+digital; <br>str=str+1; <br>} <br>return num; <br>} <br> c)请编写一个 C 函数,该函数将给定的一个整数转换成字符串。<br>void IntToCharChange(int num, char* pval) <br> { <br> char strval[100]; <br> int i, j; <br> int val0 = 0; <br> int val1 = 0; <br> <br> val0 = num; <br> for(i=0; i<100; i++) <br> { <br> val1 = val0 % 10; //取余<br> val0 = val0 / 10; //取整<br> strval[i] = val1 + 48; //数字—字符 <br> if(val0 < 10) <br> { <br> i++; <br> strval[i] = val0 + 48; <br> break; <br> } <br> } <br> for(j=0; j<=i; j++) //倒置<br> { <br> pval[j] = strval[i-j]; <br> } <br> pval[j] = '\0'; <br> }<br> d)请编写一个 C 函数,该函数将一个字符串逆序。<br>void AntitoneValue(char* father, char* child) <br> { <br> int i; <br> char source[100]; <br> int j = 0; <br> <br> while(father[j]) //放入source,[j]为长度<br> { <br> source[j] = father[j]; <br> j++; <br> if(j > 99) <br> { <br> return; <br> } <br> } <br> source[j] = '\0'; <br> <br> for(i=0; i<j; i++) <br> { <br> child[i] = source[j-i-1]; //反序<br> } <br> child[i] = '\0'; <br> } <br><br>e)请编写一个 C 函数,该函数在给定的内存区域搜索给定的字符,并返回该字符所在位置索引值。<br>int search(char *cpSource, int n, char ch) //起始地址,搜索长度,目标字符<br>{<br>int i;<br>for(i=0; i<n && *(cpSource+i) != ch; ++i);<br>return i;<br>}<br> f)请编写一个 C 函数,该函数在一个字符串中找到可能的最长的子字符串,该字符串是由同一字符组成的。<br>int ChildString(char*p) //自己写<br> { <br> Char *q=p;<br>int stringlen=0, i=0,j=1,len=0,maxlen=1; <br>while(*q!=’\0’) //不能用strlen,求得长度stringlen<br>{<br> Stringlen++;<br>q++;<br>}<br> while( i< Stringlen ) <br> { <br> if(*(p+i)==*(p+j)&&j< Stringlen) <br> { <br> len++; //统计子串长度<br> i++;<br> j++; <br> } <br> else <br> { <br> if(len>maxlen) //统计最大子串长度<br> { <br> maxlen=len+1; <br> len=0;<br> } <br> else {<br> len=0;<br> } <br> i++;<br> j++;<br> <br> } <br> } <br> return maxlen; <br>} <br>给出演示上述函数功能的一个简单程序,并请编写对应的 Makefile 文件<br><br>……<br>……<br>……<br>……
主体:(一) 一、C++概述 (一) 发展历史 1980年,Bjarne Stroustrup博士开始着手创建一种模拟语言,能够具有面向对象的程序设计特色。在当时,面向对象编程还是一个比较新的理念,Stroustrup博士并不是从头开始设计新语言,而是在C语言的基础上进行创建。这就是C++语言。 1985年,C++开始在外面慢慢流行。经过多年的发展,C++已经有了多个版本。为次,ANSI和ISO的联合委员会于1989年着手为C++制定标准。1994年2月,该委员会出版了第一份非正式草案,1998年正式推出了C++的国际标准。 (二) C和C++ C++是C的超集,也可以说C是C++的子集,因为C先出现。按常理说,C++编译器能够编译任何C程序,但是C和C++还是有一些小差别。 例如C++增加了C不具有的关键字。这些关键字能作为函数和变量的标识符在C程序中使用,尽管C++包含了所有的C,但显然没有任何C++编译器能编译这样的C程序。 C程序员可以省略函数原型,而C++不可以,一个不带参数的C函数原型必须把void写出来。而C++可以使用空参数列表。 C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。 标准C++中的字符串类取代了C标准C函数库头文件中的字符数组处理函数。 C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。 C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。 二、关键字和变量 C++相对与C增加了一些关键字,如下: typename bool dynamic_cast mutable namespace static_cast using catch explicit new virtual operator false private template volatile const protected this wchar_t const_cast public throw friend true reinterpret_cast try bitor xor_e and_eq compl or_eq not_eq bitand 在C++中还增加了bool型变量和wchar_t型变量: 布尔型变量是有两种逻辑状态的变量,它包含两个值:真和假。如果在表达式中使用了布尔型变量,那么将根据变量值的真假而赋予整型值1或0。要把一个整型变量转换成布尔型变量,如果整型值为0,则其布尔型值为假;反之如果整型值为非0,则其布尔型值为真。布儿型变量在运行时通常用做标志,比如进行逻辑测试以改变程序流程。 #include iostream.h int main() { bool flag; flag=true; if(flag) cout<<true<<endl; return 0; } C++中还包括wchar_t数据类型,wchar_t也是字符类型,但是是那些宽度超过8位的数据类型。许多外文字符集所含的数目超过256个,char字符类型无法完全囊括。wchar_t数据类型一般为16位。 标准C++的iostream类库中包括了可以支持宽字符的类和对象。用wout替代cout即可。 #include iostream.h int main() { wchar_t wc; wc='b'; wout<<wc; wc='y'; wout<<wc; wc='e'; wout<<wc<<endl; return 0; } 说明一下:某些编译器无法编译该程序(不支持该数据类型)。 三、强制类型转换 有时候,根据表达式的需要,某个数据需要被当成另外的数据类型来处理,这时,就需要强制编译器把变量或常数由声明时的类型转换成需要的类型。为此,就要使用强制类型转换说明,格式如下: int* iptr=(int*) &table; 表达式的前缀(int*)就是传统C风格的强制类型转换说明(typecast),又可称为强制转换说明(cast)。强制转换说明告诉编译器把表达式转换成指定的类型。有些情况下强制转换是禁用的,例如不能把一个结构类型转换成其他任何类型。数字类型和数字类型、指针和指针之间可以相互转换。当然,数字类型和指针类型也可以相互转换,但通常认为这样做是不安全而且也是没必要的。强制类型转换可以避免编译器的警告。 long int el=123; short i=(int) el; float m=34.56; int i=(int) m; 上面两个都是C风格的强制类型转换,C++还增加了一种转换方式,比较一下上面和下面这个书写方式的不同: long int el=123; short i=int (el); float m=34.56; int i=int (m); 使用强制类型转换的最大好处就是:禁止编译器对你故意去做的事发出警告。但是,利用强制类型转换说明使得编译器的类型检查机制失效,这不是明智的选择。通常,是不提倡进行强制类型转换的。除非不可避免,如要调用malloc()函数时要用的void型指针转换成指定类型指针。 四、标准输入输出流 在C语言中,输入输出是使用语句scanf()和printf()来实现的,而C++中是使用类来实现的。 #include iostream.h main() //C++中main()函数默认为int型,而C语言中默认为void型。 { int a; cout<>a; /*输入一个数值*/ cout<<a<<endl; //输出并回车换行 return 0; } cin,cout,endl对象,他们本身并不是C++语言的组成部分。虽然他们已经是ANSI标准C++中被定义,但是他们不是语言的内在组成部分。在C++中不提供内在的输入输出运算符,这与其他语言是不同的。输入和输出是通过C++类来实现的,cin和cout是这些类的实例,他们是在C++语言的外部实现。 在C++语言中,有了一种新的注释方法,就是‘//’,在该行//后的所有说明都被编译器认为是注释,这种注释不能换行。C++中仍然保留了传统C语言的注释风格/*……*/。 C++也可采用格式化输出的方法: #include iostream.h int main() { int a; cout<>a; cout<<dec<<a<<' ' //输出十进制数 <<oct<<a<<' ' //输出八进制数 <<hex<<a<<endl; //输出十六进制数 return 0; } 从上面也可以看出,dec,oct,hex也不可作为变量的标识符在程序中出现。 五、函数参数问题 (一) 无名的函数形参 声明函数时可以包含一个或多个用不到的形式参数。这种情况多出现在用一个通用的函数指针调用多个函数的场合,其中有些函数不需要函数指针声明中的所有参数。看下面的例子: int fun(int x,int y) { return x*2; } 尽管这样的用法是正确的,但大多数C和C++的编译器都会给出一个警告,说参数y在程序中没有被用到。为了避免这样的警告,C++允许声明一个无名形参,以告诉编译器存在该参数,且调用者需要为其传递一个实际参数,但是函数不会用到这个参数。下面给出使用了无名参数的C++函数代码: int fun(int x,int) //注意不同点 { return x*2; } (二) 函数的默认参数 C++函数的原型中可以声明一个或多个带有默认值的参数。如果调用函数时,省略了相应的实际参数,那么编译器就会把默认值作为实际参数。可以这样来声明具有默认参数的C++函数原型: #include iostream.h void show(int=1,float=2.3,long=6); int main() { show(); show(2); show(4,5.6); show(8,12.34,50L); return 0; } void show(int first,float second,long third) { cout<<first=<<first <<second=<<second <<third=<<third<<endl; } 上面例子中,第一次调用show()函数时,让编译器自动提供函数原型中指定的所有默认参数,第二次调用提供了第一个参数,而让编译器提供剩下的两个,第三次调用则提供了前面两个参数,编译器只需提供最后一个,最后一个调用则给出了所有三个参数,没有用到默认参数。 六、函数重载 在C++中,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言中是不允许的。 1.参数个数不同 #include iostream.h void a(int,int); void a(int); int main() { a(5); a(6,7); return 0; } void a(int i) { cout<<i<<endl; //输出5 } void a(int i,int j) { cout<<i<<j<<endl; //输出67 } 2.参数格式不同 #include iostream.h void a(int,int); void a(int,float); int main() { a(5,6); a(6,7.0); return 0; } void a(int i,int j) { cout<<i<<j<<endl; //输出56 } void a(int i,float j) { cout<<i<<j<>a; for(int i=1;i<=10;i++) //C语言中,不允许在这里定义变量 { static int a=0; //C语言中,同一函数块,不允许有同名变量 a+=i; cout<<::a<< <<a<<endl; } return 0; } 八、new和delete运算符 在C++语言中,仍然支持malloc()和free()来分配和释放内存,同时增加了new和delete来管理内存。 1.为固定大小的数组分配内存 #include iostream.h int main() { int *birthday=new int[3]; birthday[0]=6; birthday[1]=24; birthday[2]=1940; cout<<I was born on <<birthday[0]<<'/'<<birthday[1]<<'/'<<birthday[2]<>size; int *array=new int[size]; for(int i=0;i<size;i++) array[i]=rand(); for(i=0;i<size;i++) cout<<'\n'<<array[i]; delete [] array; return 0; } 九、引用型变量 在C++中,引用是一个经常使用的概念。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。 1.引用是一个别名 C++中的引用是其他变量的别名。声明一个引用型变量,需要给他一个初始化值,在变量的生存周期内,该值不会改变。& 运算符定义了一个引用型变量: int a; int& b=a; 先声明一个名为a的变量,它还有一个别名b。我们可以认为是一个人,有一个真名,一个外号,以后不管是喊他a还是b,都是叫他这个人。同样,作为变量,以后对这两个标识符操作都会产生相同的效果。 #include iostream.h int main() { int a=123; int& b=a; cout<<a<<','<<b<<endl; //输出123,123 a++; cout<<a<<','<<b<<endl; //输出124,124 b++; cout<<a<<','<<b<等运算符 #include iostream.h void func1(s p); void func2(s& p); struct s { int n; char text[10]; }; int main() { static s str={123,China}; func1(str); func2(str); return 0; } void func1(s p) { cout<<p.n<<endl; cout<<p.text<<endl; } void func2(s& p) { cout<<p.n<<endl; cout<<p.text<<endl; } 从表面上看,这两个函数没有明显区别,不过他们所花的时间却有很大差异,func2()函数所用的时间开销会比func2()函数少很多。它们还有一个差别,如果程序递归func1(),随着递归的深入,会因为栈的耗尽而崩溃,但func2()没有这样的担忧。 4.以引用方式调用 当函数把引用作为参数传递给另一个函数时,被调用函数将直接对参数在调用者中的拷贝进行操作,而不是产生一个局部的拷贝(传递变量本身是这样的)。这就称为以引用方式调用。把参数的值传递到被调用函数内部的拷贝中则称为以传值方式调用。 #include iostream.h void display(const Date&,const char*); void swapper(Date&,Date&); struct Date { int month,day,year; }; int main() { static Date now={2,23,90}; static Date then={9,10,60}; display(now,Now: ); display(then,Then: ); swapper(now,then); display(now,Now: ); display(then,Then: ); return 0; } void swapper(Date& dt1,Date& dt2) { Date save; save=dt1; dt1=dt2; dt2=save; } void display(const Date& dt,const char *s) { cout<<s; cout<<dt.month<<'/'<<dt.day<<'/'<<dt.year<<endl; } 5.以引用作为返回值 #include iostream.h struct Date { int month,day,year; }; Date birthdays[]= { {12,12,60}; {10,25,85}; {5,20,73}; }; const Date& getdate(int n) { return birthdays[n-1]; } int main() { int dt=1; while(dt!=0) { cout<<Enter date # (1-3,0 to quit)<>dt; if(dt>0 && dt<4) { const Date& bd=getdate(dt); cout<<bd.month<<'/'<<bd.day<<'/'<<bd.year<<endl; } } return 0; } 程序都很简单,就不讲解了。 主体:(二)类的设计,构造函数和析构函数 类是编程人员表达自定义数据类型的C++机制。它和C语言中的结构类似,C++类支持数据抽象和面向对象的程序设计,从某种意义上说,也就是数据类型的设计和实现。 一、类的设计 1.类的声明 class 类名 { private: //私有 ... public: //公有 ... }; 2.类的成员 一般在C++类中,所有定义的变量和函数都是类的成员。如果是变量,我们就叫它数据成员如果是函数,我们就叫它成员函数。 3.类成员的可见性 private和public访问控制符决定了成员的可见性。由一个访问控制符设定的可访问状态将一直持续到下一个访问控制符出现,或者类声明的结束。私有成员仅能被同一个类中的成员函数访问,公有成员既可以被同一类中的成员函数访问,也可以被其他已经实例化的类中函数访问。当然,这也有例外的情况,这是以后要讨论的友元函数。 类中默认的数据类型是private,结构中的默认类型是public。一般情况下,变量都作为私有成员出现,函数都作为公有成员出现。 类中还有一种访问控制符protected,叫保护成员,以后再说明。 4.初始化 在声明一个类的对象时,可以用圆括号()包含一个初始化表。 看下面一个例子: #include iostream.h class Box { private: int height,width,depth; //3个私有数据成员 public: Box(int,int,int); ~Box(); int volume(); //成员函数 }; Box::Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } Box::~Box() { //nothing } int Box::volume() { return height*width*depth; } int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 当一个类中没有private成员和protected成员时,也没有虚函数,并且不是从其他类中派生出来的,可以用{}来初始化。(以后再讲解) 5.内联函数 内联函数和普通函数的区别是:内联函数是在编译过程中展开的。通常内联函数必须简短。定义类的内联函数有两种方法:一种和C语言一样,在定义函数时使用关键字inline。如: inline int Box::volume() { return height*width*depth; } 还有一种方法就是直接在类声明的内部定义函数体,而不是仅仅给出一个函数原型。我们把上面的函数简化一下: #include iostream.h class Box { private: int height,width,depth; public: Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 这样,两个函数都默认为内联函数了。 二、构造函数 什么是构造函数?通俗的讲,在类中,函数名和类名相同的函数称为构造函数。上面的Box()函数就是构造函数。C++允许同名函数,也就允许在一个类中有多个构造函数。如果一个都没有,编译器将为该类产生一个默认的构造函数,这个构造函数可能会完成一些工作,也可能什么都不做。 绝对不能指定构造函数的类型,即使是void型都不可以。实际上构造函数默认为void型。 当一个类的对象进入作用域时,系统会为其数据成员分配足够的内存,但是系统不一定将其初始化。和内部数据类型对象一样,外部对象的数据成员总是初始化为0。局部对象不会被初始化。构造函数就是被用来进行初始化工作的。当自动类型的类对象离开其作用域时,所站用的内存将释放回系统。 看上面的例子,构造函数Box()函数接受三个整型擦黑素,并把他们赋值给立方体对象的数据成员。 如果构造函数没有参数,那么声明对象时也不需要括号。 1.使用默认参数的构造函数 当在声明类对象时,如果没有指定参数,则使用默认参数来初始化对象。 #include iostream.h class Box { private: int height,width,depth; public: Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box defaulbox; //使用默认参数 cout<<thisbox.volume()<<endl; //输出60 cout<<defaulbox.volume()<<endl; //输出24 return 0; } 2.默认构造函数 没有参数或者参数都是默认值的构造函数称为默认构造函数。如果你不提供构造函数,编译器会自动产生一个公共的默认构造函数,这个构造函数什么都不做。如果至少提供一个构造函数,则编译器就不会产生默认构造函数。 3.重载构造函数 一个类中可以有多个构造函数。这些构造函数必须具有不同的参数表。在一个类中需要接受不同初始化值时,就需要编写多个构造函数,但有时候只需要一个不带初始值的空的Box对象。 #include iostream.h class Box { private: int height,width,depth; public: Box() { //nothing } Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box otherbox; otherbox=thisbox; cout<<otherbox.volume();<<endl; return 0; } 这两个构造函数一个没有初始化值,一个有。当没有初始化值时,程序使用默认值,即2,3,4。 但是这样的程序是不好的。它允许使用初始化过的和没有初始化过的Box对象,但它没有考虑当thisbox给otherbox赋值失败后,volume()该返回什么。较好的方法是,没有参数表的构造函数也把默认值赋值给对象。 class Box { int height,width,depth; public: Box() { height=0;width=0;depth=0; } Box(int ht,int wd,int dp) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 这还不是最好的方法,更好的方法是使用默认参数,根本不需要不带参数的构造函数。 class Box { int height,width,depth; public: Box(int ht=0,int wd=0,int dp=0) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 三、析构函数 当一个类的对象离开作用域时,析构函数将被调用(系统自动调用)。析构函数的名字和类名一样,不过要在前面加上 ~ 。对一个类来说,只能允许一个析构函数,析构函数不能有参数,并且也没有返回值。析构函数的作用是完成一个清理工作,如释放从堆中分配的内存。 我们也可以只给出析构函数的形式,而不给出起具体函数体,其效果是一样的,如上面的例子。但在有些情况下,析构函数又是必需的。如在类中从堆中分配了内存,则必须在析构函数中释放 主体:(三)类的转换 C++的内部数据类型遵循隐式类型转换规则。假设某个表达市中使用了一个短整型变量,而编译器根据上下文认为这儿需要是的长整型,则编译器就会根据类型转换规则自动把它转换成长整型,这种隐式转换出现在赋值、参数传递、返回值、初始化和表达式中。我们也可以为类提供相应的转换规则。 对一个类建立隐式转换规则需要构造一个转换函数,该函数作为类的成员,可以把该类的对象和其他数据类型的对象进行相互转换。声明了转换函数,就告诉了编译器,当根据句法判定需要类型转换时,就调用函数。 有两种转换函数。一种是转换构造函数;另一种是成员转换函数。需要采用哪种转换函数取决于转换的方向。 一、转换构造函数 当一个构造函数仅有一个参数,且该参数是不同于该类的一个数据类型,这样的构造函数就叫转换构造函数。转换构造函数把别的数据类型的对象转换为该类的一个对象。和其他构造函数一样,如果声明类的对象的初始化表同转换构造函数的参数表相匹配,该函数就会被调用。当在需要使用该类的地方使用了别的数据类型,便宜器就会调用转换构造函数进行转换。 #include iostream.h #include time.h #include stdio.h class Date { int mo, da, yr; public: Date(time_t); void display(); }; void Date::display() { char year[5]; if(yr<10) sprintf(year,0%d,yr); else sprintf(year,%d,yr); cout<<mo<<'/'<<da<<'/'<tm_mday; mo=tim->tm_mon+1; yr=tim->tm_year; if(yr>=100) yr-=100; } int main() { time_t now=time(0); Date dt(now); dt.display(); return 0; } 本程序先调用time()函数来获取当前时间,并把它赋给time_t对象;然后程序通过调用Date类的转换构造函数来创建一个Date对象,该对象由time_t对象转换而来。time_t对象先传递给localtime()函数,然后返回一个指向tm结构(time.h文件中声明)的指针,然后构造函数把结构中的日月年的数值拷贝给Date对象的数据成员,这就完成了从time_t对象到Date对象的转换。 二、成员转换函数 成员转换函数把该类的对象转换为其他数据类型的对象。在成员转换函数的声明中要用到关键字operator。这样声明一个成员转换函数: operator aaa(); 在这个例子中,aaa就是要转换成的数据类型的说明符。这里的类型说明符可以是任何合法的C++类型,包括其他的类。如下来定义成员转换函数; Classname::operator aaa() 类名标识符是声明了该函数的类的类型说明符。上面定义的Date类并不能把该类的对象转换回time_t型变量,但可以把它转换成一个长整型值,计算从2000年1月1日到现在的天数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m,int d,int y) {mo=m; da=d; yr=y;} operator int(); //声明 }; Date::operator int() //定义 { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; int days=yr-2000; days*=365; days+=(yr-2000)/4; for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date now(12,24,2003); int since=now; cout<<since<<endl; return 0; } 三、类的转换 上面两个例子都是C++类对象和内部数据对象之间的相互转换。也可以定义转换函数来实现两个类对象之间的相互转换。 #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m=0,int d=0,int y=0) {mo=m; da=d; yr=y;} Date(const CustomDate&); //转换构造函数 operator CustomDate(); //成员转换函数 void display() { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; Date::Date(const CustomDate& jd) { yr=jd.yr; da=jd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } Date::operator CustomDate() { CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; //调用成员转换函数 cd.display(); dt = cd; //调用转换构造函数 dt.display(); return 0; } 这个例子中有两个类CustomDate和Date,CustomDate型日期包含年份和天数。 这个例子没有考虑闰年情况。但是在实际构造一个类时,应该考虑到所有问题的可能性。 在Date里中具有两种转换函数,这样,当需要从Date型变为CustomDate型十,可以调用成员转换函数;反之可以调用转换构造函数。 不能既在Date类中定义成员转换函数,又在CustomDate类里定义转换构造函数。那样编译器在进行转换时就不知道该调用哪一个函数,从而出错。 四、转换函数的调用 C++里调用转换函数有三种形式:第一种是隐式转换,例如编译器需要一个Date对象,而程序提供的是CustomDate对象,编译器会自动调用合适的转换函数。另外两种都是需要在程序代码中明确给出的显式转换。C++强制类型转换是一种,还有一种是显式调用转换构造函数和成员转换函数。下面的程序给出了三中转换形式: #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd = dt; cd.display(); cd = (CustomDate) dt; cd.display(); cd = CustomDate(dt); cd.display(); return 0; } 五、转换发生的情形 上面的几个例子都是通过不能类型对象之间的相互赋值来调用转换函数,还有几种调用的可能: 参数传递 初始化 返回值 表达式语句 这些情况下,都有可能调用转换函数。 下面的程序不难理解,就不分析了。 #include iostream.h class CustomDate { public: int da, yr; CustomDate() {} CustomDate(int d,int y) { da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for (int i=0;i<mo-1;i++) cd.da += dys[i]; cd.da+=da; return cd; } class Tester { CustomDate cd; public: explicit Tester(CustomDate c) { cd=c; } void display() { cd.display(); } }; void dispdate(CustomDate cd) { cd.display(); } CustomDate rtndate() { Date dt(9,11,1); return dt; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; cd.display(); dispdate(dt); Tester ts(dt); ts.display(); cd = rtndate(); cd.display(); return 0; } 六、显式构造函数 注意上面Tester类的构造函数前面有一个explicit修饰符。如果不加上这个关键字,那么在需要把CustomDate对象转换成Tester对象时,编译器会把该函数当作转换构造函数来调用。但是有时候,并不想把这种只有一个参数的构造函数用于转换目的,而仅仅希望用它来显式地初始化对象,此时,就需要在构造函数前加explicit。如果在声明了Tester对象以后使用了下面的语句将导致一个错误: ts=jd; //error 这个错误说明,虽然Tester类中有一个以Date型变量为参数的构造函数,编译器却不会把它看作是从Date到Tester的转换构造函数,因为它的声明中包含了explicit修饰符。 七、表达式内部的转换 在表达式内部,如果发现某个类型和需要的不一致,就会发生错误。数字类型的转换是很简单,这里就不举例了。下面的程序是把Date对象转换成长整型值。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator long(); }; Date::operator long() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; long days=yr; days*=365; days+=(yr-1900)/4; //从1900年1月1日开始计算 for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date today(12,24,2003); const long ott=123; long sum=ott+today; cout<<ott<< + <<(long) today<< = <<sum; return 0; } 在表达式中,当需要转换的对象可以转换成某个数字类型,或者表达式调用了作用于某个类的重载运算符时,就会发生隐式转换。运算符重载以后再学习。 主体:(四)私有数据成员和友元 一、私有数据成员的使用 1.取值和赋值成员函数 面向对象的约定就是保证所有数据成员的私有性。一般我们都是通过公有成员函数来作为公共接口来读取私有数据成员的。某些时候,我们称这样的函数为取值和赋值函数。 取值函数的返回值和传递给赋值函数的参数不必一一匹配所有数据成员的类型。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } int getyear() const { return yr; } void setyear(int y) { yr = y; } }; int main() { Date dt(4,1,89); cout<<The year is: <<dt.getyear()<<endl; dt.setyear(97); cout<<The new year is: <<dt.getyear(); return 0; } 上面的例子很简单,不分析了。要养成这样的习惯,通过成员函数来访问和改变类中的数据。这样有利于软件的设计和维护。比如,改变Date类内部数据的形式,但仍然用修改过的getyear()和setyear()来提供访问接口,那么使用该类就不必修改他们的代码,仅需要重新编译程序即可。 2.常量成员函数 注意上面的程序中getyear()被声明为常量型,这样可以保证该成员函数不会修改调用他的对象。通过加上const修饰符,可以使访问对象数据的成员函数仅仅完成不会引起数据变动的那些操作。 如果程序声明某个Date对象为常量的话,那么该对象不得调用任何非常量型成员函数,不论这些函数是否真的试图修改对象的数据。只有把那些不会引起数据改变的函数都声明为常量型,才可以让常量对象来调用。 3.改进的成员转换函数 下面的程序改进了从Date对象到CustomDate对象的成员转换函数,用取值和赋值函数取代了使用公有数据成员的做法。(以前的程序代码在上一帖中) #include iostream.h class CustomDate { int da,yr; public: CustomDate() {} CustomDate(int d,int y) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} int getday() const { return da; } void setday(int d) { da=d; } }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate() const; }; Date::operator CustomDate() const { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); int day=da; for(int i=0;i<mo-1;i++) day+=dys[i]; cd.setday(day); return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd=dt; cd.display(); return 0; } 注意上面的程序中Date::operator CustomDate()声明为常量型,因为这个函数没有改变调用它对象的数据,尽管它修改了一个临时CustomDate对象并将其作为函数返回值。 二、友元 前面已经说过了,私有数据成员不能被类外的其他函数读取,但是有时候类会允许一些特殊的函数直接读写其私有数据成员。 关键字friend可以让特定的函数或者别的类的所有成员函数对私有数据成员进行读写。这既可以维护数据的私有性,有可以保证让特定的类或函数能够直接访问私有数据。 1.友元类 一个类可以声明另一个类为其友元,这个友元的所有成员函数都可以读写它的私有数据。 #include iostream.h class Date; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend Date; //这儿 }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0, yr); for (int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd(dt); cd.display(); return 0; } 在上面的程序中,有这样一句 friend Date; 该语句告诉编译器,Date类的所有成员函数有权访问CustomDate类的私有成员。因为Date类的转换函数需要知道CustomDate类的每个数据成员,所以真个Date类都被声明为CustomDate类的友元。 2.隐式构造函数 上面程序对CustomDate的构造函数的调用私有显示该类需要如下的一个转换构造函数: CustomDate(Date& dt); 但是唯一的一个构造函数是:CustomDate(int d=0;int y=0); 这就出现了问题,编译器要从Date对象构造一个CustomDate对象,但是CustomDate类中并没有定义这样的转换构造函数。不过Date类中定义了一个成员转换函数,它可以把Date对象转换成CustomDate对象。于是编译器开始搜索CustomDate类,看其是否有一个构造函数,能从一个已存在的CustomDate的对象创建新的CustomDate对象。这种构造函数叫拷贝构造函数。拷贝构造函数也只有一个参数,该参数是它所属的类的一个对象,由于CustomDate类中没有拷贝构造函数,于是编译器就会产生一个默认的拷贝构造函数,该函数简单地把已存在的对象的每个成员拷贝给新对象。现在我们已经知道,编译器可以把Date对象转换成CustomDate对象,也可以从已存在的CustomDate对象生成一个新的CustomDate对象。那么上面提出的问题,编译器就是这样做的:它首先调用转换函数,从Date对象创建一个隐藏的、临时的、匿名的CustomDate对象,然后用该临时对象作为参数调用默认拷贝构造函数,这就生成了一个新的CustomDate对象。 3.预引用 上面的例子中还有这样一句 class Date; 这个语句叫做预引用。它告诉编译器,类Date将在后面定义。编译器必须知道这个信号,因为CustomDate类中引用了Date类,而Date里也引用了CustomDate类,必须首先声明其中之一。 使用了预引用后,就可以声明未定义的类的友元、指针和引用。但是不可以使用那些需要知道预引用的类的定义细节的语句,如声明该类的一个实例或者任何对该类成员的引用。 4.显式友元预引用 也可以不使用预引用,这只要在声明友元的时候加上关键自class就行了。 #include iostream.h class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend class Date; //这儿,去掉前面的预引用 }; class Date { ... ... }; Date::operator CustomDate() { ... ... } int main() { ... ... } 5.友元函数 通常,除非真的需要,否则并不需要把整个类都设为另一个类的友元,只需挑出需要访问当前类私有数据成员的成员函数,将它们设置为该类的友元即可。这样的函数称为友元函数。 下面的程序限制了CustomDate类数据成员的访问,Date类中只有需要这些数据的成员函数才有权读写它们。 #include iostream.h class CustomDate; class Date { int mo,da,yr; public: Date(const CustomDate&); void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl;} }; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } friend Date::Date(const CustomDate&); }; Date::Date(const CustomDate& cd) { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; yr=cd.yr; da=cd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } int main() { Date dt(CustomDate(123, 89)); dt.display(); return 0; } 6.匿名对象 上面main()函数中Date对象调用CustomDate类的构造函数创建了一个匿名CustomDate对象,然后用该对象创建了一个Date对象。这种用法在C++中是经常出现的。 7.非类成员的友元函数 有时候友元函数未必是某个类的成员。这样的函数拥有类对象私有数据成员的读写权,但它并不是任何类的成员函数。这个特性在重载运算符时特别有用。 非类成员的友元函数通常被用来做为类之间的纽带。一个函数如果被两个类同时声明为友元,它就可以访问这两个类的私有成员。下面的程序说明了一个可以访问两个类私有数据成员的友元函数是如何将在两个类之间架起桥梁的。 #include iostream.h class Time; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y;} friend void display(const Date&, const Time&); }; class Time { int hr,min,sec; public: Time(int h,int m,int s) { hr=h; min=m; sec=s;} friend void display(const Date&, const Time&); }; void display(const Date& dt, const Time& tm) { cout << dt.mo << '/' << dt.da << '/' << dt.yr; cout << ' '; cout << tm.hr << ':' << tm.min << ':' << tm.sec; } int main() { Date dt(2,16,97); Time tm(10,55,0); display(dt, tm); return 0; } 主体:(五)析构函数和this指针 一、析构函数 前面的一些例子都没有说明析构函数,这是因为所用到的类在结束时不需要做特别的清理工作。下面的程序给出了一新的Date类,其中包括一个字符串指针,用来表示月份。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void display() const; }; Date::Date(int m,int d,int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo=m; da=d; yr=y; if(m!=0) { month=new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if(month!=0) cout<<month<<' '<<da<<','<<yr; } int main() { Date birthday(8,11,1979); birthday.display(); return 0; } 在Date对象的构造函数中,首先用new运算符为字符串month动态分配了内存,然后从内部数组中把月份的名字拷贝给字符串指针month。 析构函数在删除month指针时,可能会出现一些问题。当然从这个程序本身来看,没什么麻烦;但是从设计一个类的角度来看,当Date类用于赋值时,就会出现问题。假设上面的main()修改为“ int main() { Date birthday(8,11,1979); Date today; today=birthday; birthday.display(); return 0; } 这会生成一个名为today的空的Date型变量,并且把birthday值赋给它。如果不特别通知编译器,它会简单的认为类的赋值就是成员对成员的拷贝。在上面的程序中,变量birthday有一个字符型指针month,并且在构造函数里用new运算符初始化过了。当birthday离开其作用域时,析构函数会调用delete运算符来释放内存。但同时,当today离开它的作用域时,析构函数同样会对它进行释放操作,而today里的month指针是birthday里的month指针的一个拷贝。析构函数对同一指针进行了两次删除操作,这会带来不可预知的后果。 如果假设today是一个外部变量,而birthday是一个自变量。当birthday离开其作用域时,就已经把对象today里的month指针删除了。显然这也是不正确的。 再假设有两个初始化的Date变量,把其中一个的值赋值给另一个: Date birthday(8,11,1979); Date today(12,29,2003); today=birthday; 问题就更复杂了,当这两个变量离开作用域时,birthday中的month的值已经通过赋值传递给了today。而today中构造函数用new运算符给month的值却因为赋值被覆盖了。这样,birthday中的month被删除了两次,而today中month却没有被删除掉。 二、重载赋值运算符 为了解决上面的问题,我们应该写一个特殊的赋值运算符函数来处理这类问题。当需要为同一个类的两个对象相互赋值时,就可以重载运算符函数。这个方法可以解决类的赋值和指针的释放。 下面的程序中,类中的赋值函数用new运算符从堆中分配了一个不同的指针,该指针获取赋值对象中相应的值,然后拷贝给接受赋值的对象。 在类中重载赋值运算符的格式如下: void operator = (const Date&) 后面我们回加以改进。目前,重载的运算符函数的返回类型为void。它是类总的成员函数,在本程序红,是Date类的成员函数。它的函数名始终是operator =,参数也始终是同一个类的对象的引用。参数表示的是源对象,即赋值数据的提供者。重载函数的运算符作为目标对象的成员函数来使用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } } int main() { Date birthday(8,11,1979); birthday.display(); Date newday(12,29,2003); newday.display(); newday = birthday; newday.display(); return 0; } 除了为Date类加入了一个重载运算符函数,这个程序和上面的一个程序是相同的。赋值运算符函数首先取得所需的数据,然后用delete把原来的month指针所占用的内存返还给堆。接着,如果源对象的month指针已经初始化过,就用new运算符为对象重新分配内存,并把源对象的month字符串拷贝给接受方。 重载的Date类赋值运算符函数的第一个语句比较了源对象的地址和this指针。这个操作取保对象不会自己给自己赋值。 三、this指针 this指针是一个特殊的指针,当类的某个非静态的成员函数在执行时,就会存在this指针。它指向类的一个对象,且这个对象的某个成员函数正在被调用。 this指针的名字始终是this,而且总是作为隐含参数传递给每一个被声明的成员函数,例如: void Date::myFunc(Date* this); 实际编程时函数的声明不需要包含这个参数。 当程序中调用某个对象的成员函数时,编译器会把该对象的地址加入到参数列表中,感觉上就好象函数采用了上面所示的声明,并且是用如下方式来调用的: dt.myFunc(& dt); 静态成员函数不存在this指针。 当调用某个对象的成员函数时,编译器把对象的地址传递给this指针,然后再调用该函数。因此,成员函数你对任何成员的调用实际上都隐式地使用了this指针。 1.以this指针作为返回值 使用this指针可以允许成员函数返回调用对象给调用者。前面的程序中重载赋值运算符没有返回值,因此不能用如下的形式对字符串进行赋值: a=b=c; 为了使重载的类赋值机制也能这样方便,必须让赋值函数返回赋值的结果,在这里就是目标对象。当赋值函数执行时,其返回值也恰好是this指针所指的内容。 下面的程序对前面那个程序进行了修改,让重载赋值运算符返回了一个Date对象的引用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } return *this; } int main() { Date birthday(8,11,1979); Date oldday,newday; oldday=newday=birthday; birthday.display(); oldday.display(); newday.display(); return 0; } 2.在链表中使用this指针 在应用程序中,如果数据结构里有指向自身类型的成员,那么使用this指针会提供更多的方便。下面的程序中建立了一个类ListEntry的链表。 #include iostream.h #include string.h class ListEntry { char* listvalue; ListEntry* preventry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue; } ListEntry* PrevEntry() const { return preventry; }; void display() const { cout<<endl<<listvalue; } void AddEntry(ListEntry& le) { le.preventry = this; } }; ListEntry::ListEntry(char* s) { listvalue = new char[strlen(s)+1]; strcpy(listvalue, s); preventry = 0; } int main() { ListEntry* prev = 0; while (1) { cout <<endl<> name; if (strncmp(name, end, 3) == 0) break; ListEntry* list = new ListEntry(name); if (prev != 0) prev->AddEntry(*list); prev = list; } while (prev != 0) { prev->display(); ListEntry* hold = prev; prev = prev->PrevEntry(); delete hold; } return 0; } 程序运行时,会提示输入一串姓名,当输入完毕后,键入end,然后程序会逆序显示刚才输入的所有姓名。 程序中ListEntry类含有一个字符串和一个指向前一个表项的指针。构造函数从对中获取内存分配给字符串,并把字符串的内容拷贝到内存,然后置链接指针为NULL。析构函数将释放字符串所占用的内存。 成员函数PrevEntry()返回指向链表前一个表项的指针。另一个成员函数显示当前的表项内容。 成员函数AddEntry(),它把this指针拷贝给参数的preventry指针,即把当前表项的地址赋值给下一个表项的链接指针,从而构造了一个链表。它并没有改变调用它的listEntry对象的内容,只是把该对象的地址赋给函数的参数所引用的那个ListEntry对象的preventry指针,尽管该函数不会修改对象的数据,但它并不是常量型。这是因为,它拷贝对象的地址this指针的内容给一个非长常量对象,而编译器回认为这个非常量对象就有可能通过拷贝得到的地址去修改当前对象的数据,因此AddEntry()函数在声明时不需要用const。 主体:(六)类对象数组和静态成员 一、类对象数组 类的对象和C++其他数据类型一样,也可以为其建立数组,数组的表示方法和结构一样。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0, int y=0) { mo=m; da=d; yr=y;} void display() const { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 1.类对象数组和默认构造函数 在前面已经说过,不带参数或者所有参数都有默认值的构造函数叫做默认构造函数。如果类中没有构造函数,编译器会自动提供一个什么都不做的公共默认构造函数 。如果类当中至少有一个构造函数,编译器就不会提供默认构造函数。 如果类当中不含默认构造函数,则无法实例化其对象数组。因为实例花类对象数组的格式不允许用初始化值来匹配某个构造函数的参数表。 上面的程序中,main()函数声明了一个长度为2的Date对象数组,还有一个包含初始化值的单个Date对象。接着把这个初始化的Date对象赋值给数组中第一个对象,然后显示两个数组元素中包含的日期。从输出中可以看到,第一个日期是有效日期,而第二个显示的都是0。 当声明了某个类的对象数组时,编译器会为每个元素都调用默认构造函数。 下面的程序去掉了构造函数的默认参数值,并且增加了一个默认构造函数。 #include class Date { int mo, da, yr; public: Date(); Date(int m,int d,int y) { mo=m; da=d; yr=y;} void display() const { cout <<mo<<'/'<<da<<'/'<<yr<<endl; } }; Date::Date() { cout <<Date constructor running<<endl; mo=0; da=0; yr=0; } int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: Date constructor running Date constructor running 12/31/2003 0/0/0 从输出中可以看出,Date()这个默认构造函数被调用了两次。 2.类对象数组和析构函数 当类对象离开作用域时,编译器会为每个对象数组元素调用析构函数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} ~Date() {cout<<Date destructor running<<endl;} void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: 12/31/2003 0/0/0 Date destructor running Date destructor running Date destructor running 表明析构函数被调用了三次,也就是dates[0],dates[1],today这三个对象离开作用域时调用的。 二、静态成员 可以把类的成员声明为静态的。静态成员只能存在唯一的实例。所有的成员函数都可以访问这个静态成员。即使没有声明类的任何实例,静态成员也已经是存在的。不过类当中声明静态成员时并不能自动定义这个变量,必须在类定义之外来定义该成员。 1.静态数据成员 静态数据成员相当于一个全局变量,类的所有实例都可以使用它。成员函数能访问并且修改这个值。如果这个静态成员是公有的,那么类的作用域之内的所有代码(不论是在类的内部还是外部)都可以访问这个成员。下面的程序通过静态数据成员来记录链表首项和末项的地址。 #include iostream.h #include string.h class ListEntry { public: static ListEntry* firstentry; private: static ListEntry* lastentry; char* listvalue; ListEntry* nextentry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue;} ListEntry* NextEntry() const { return nextentry; }; void display() const { cout<<listvalue<nextentry=this; lastentry=this; listvalue=new char[strlen(s)+1]; strcpy(listvalue,s); nextentry=0; } int main() { while (1) { cout<>name

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值