Task:
编写一个程序,将某文件名,如F:/testsue/input.txt,更改为F:/testuse/input_cutted.txt
这是我在编写一个分词程序时候遇到的问题。初看起来,该问题非常之简单,首先想到的是使用string.h中的几个库函数来实现,可能你会编码如下(不使用临时数组):
先看下strlen的实现(参考自P. J. Plauger 《C标准库》):
可以看出strlen是通过对字符串进行遍历来计算长度的;在以上算法中,strlen使用了两次,因此程序分别对tmp数组和原path数组进行了一次遍历;此后程序又使用while_1从后向前对path进行遍历并在此过程中进行赋值操作;在while_1结束后,程序继续使用while_2同时对path和tmp进行遍历。在这两次while遍历中,对tmp的遍历是完全遍历的而对path的遍历至少进行了一半;连同之前两次strlen所进行的遍历,程序一共对path进行了1.5次遍历,对tmp进行了2遍历;
如果该字符串有很长,虽然时间复杂度仍然是O(n),但其效率不是可被接受的。如果可以在前两次strlen的同时做点什么,使后来对path和tmp进行的while遍历不发生或少发生,那么效率无疑将得到大步提升。请看以下代码:
在该代码中,手工实现了strlen等的功能;总的看来,程序对path只进行了1次遍历,对tmp只进行了1次遍历;比较第一个算法,该算法的效率无疑更好。
库函数的存在是为了封装某些操作使程序员不必拘泥于细节的实现。但在使用库函数时,应适当了解库函数的实现,正确的使用库函数,否则,轻则影响程序的效率,重则将导致程序崩溃。
可以看出,在strcpy中并没有NULL检查,并没有数组空间检查;因此如果错误的使用,将造成难以预料的结果。
在string.h中,很多库函数都通过遍历字符串来实现。如上面的strlen,strcpy。又如strcat
因此,当我们使用string.h中的库函数时,我们应该考虑下,是否有使用的必要,使用库函数是否会导致多次遍历同一字符串从而导致效率的低下?是否有更好的办法来实现这些目的?
有些库函数只有知道了其实现,才能更好的理解其用法。例如下面的memset
memset内部使用1个字节的char,因此在使用memset时,应根据其类型来选择不同的调用方式
例如,int buff[10];调用方式是memset(buff,0,sizeof(buff)); 而不能是memset(buff,’0’,10);
又如:char buff[10];调用方式是memset(buff,'0',sizeof(buff)); 而不能是memset(buff,0,10);
特别要注意的是,若声明int buff[],则memset的使用目的只会是设置该数组的所有单元均为0,不可能将buff中所有单元设为其他数……(因为在memset中,使用了8位的char,因此,若想设置buff的每一个单元为1,则实际结果是,buff[]每一个单元的int的4个字节,每个字节均为1,因此该int数据实际上是1 00000001 00000001 00000001(32位机),即是16843009)
另要注意,对char的使用一定要传字符,如’1’是将char[]的每个单元初始化为1而1,则无法;
又如,strncat,我的实现为:
我觉得MARK处用得相当好,相当有创意,原来都不知道。就像上面的strcat中的for(;(*s=*s2)!='/0';++s,++s2)并不会影响性能,且括起来相当简洁,我认为相当不错。(用优先级规则怎么分析?)
如下面的strncpy:
strncpy将char *src中之多n个元素复制至des中,如果src中元素个数少于n,则des中相应的部分应全填充为’/0’
两点注意:
首先,如果这样写代码,那么在MARK处必须写成while(*src!='/0'&&0<n--)而不能是while(0<n--&&*src!=’/0’)。也就是说,while中的判断顺序不能反。形如while(*src!='/0'&&0<n--)时候,当src中字符数少于n时,while会在n>0之前遇到’/0’,此时进行while判断时,*src!=’/0’必为假,因此while会直接break而不会进行0<n—的判断,也就是说,n不会变化。这样就能保证在第二个while中能复制足够的’/0’;而如果使用while(0<n--&&*src!=’/0’)时,当判断出*src==’/0’时,n已经--,这样在while break时,n值比实际的值小1。在某些实现中,这可能造成相当严重的后果;
第二,在使用strncpy时,程序员同样应该保证des中有足够多的空间存放来自src的n个字符。即使src中实际上并没有n个字符要复制。当src中实际上没有n个字符要复制时,des同样会被“复制进”n个字符,同样会覆盖des后的其他数据。虽然如果仅仅复制src中有的数据不会出现这样的情况。
strcspn:用于返回char *s1中从起始位置起,不包含char *s2中任意字符的字符段的最后一个字符的位置(索引);即是,s1中从起始位置起,开头的n个字符均不出现于s2,于是返回该n(作为索引)
我的算法是:
但《C标准库》中的代码却是:
很明显能看出,《C标准库》的算法没有我的算法的效率高,我的算法所使用的空间较多。为什么会这样?我认为是作者考虑到,如果该函数运行在内存有限的平台上,那么开辟256个空间单元的愿望不免奢侈了一些,或者说不可能完成。于是宁愿采用稍微保守一点但移植性很好的算法。不过我认为,我的算法在支持ANSI C的开发环境中,且内存限制不大的环境中,不会出现运行问题。总觉得我所使用的参考数组int pre[256]={0};有问题……或者,可以只使用127个单元而不使用256个单元吗?
与之同理的还有strpbrk.其形式为:
char *strpbrk (cosnt char*,const char*);
值得注意的是,strpbrk中,return是:return ((char*)sc1);其函数形式如下:
sc1声明为const char*,如果直接return sc1,虽然在strpbrk中,sc1为const,但调用strpbrk的函数仍然可以使用strpbrk所得到的指针,例如改变该地址的值等。这是因为cosnt所修饰的sc1的作用范围只是strpbrk。但如果直接返回,则编译器会给出警告warning C4090: 'return' : different 'const' qualifiers。于是作者在这里使用了return ((char*)sc1)以消除该警告。但如果strpbrk的返回值定义为const char*,则在调用strpbrk的函数中,该指针不能进行赋值等操作,因为return的是一个const,那么接收的必然也是const……
注意strcspn和strspn的区别:
strspn返回的是:s1中第一个不出现在s2中的字符的位置;这与strcspn是不同的,strcspn返回的是从s1起始位置开始,最后一个不出现在s2字符集的字符的位置。这两者是不同的……不过两者的算法实在太像了……
另,在《C标准库》中,strspn的实现中,使用的第二个for是:
不知为何?为什么要将if(*sc2==*sc1)单独写出来?仅仅是为了直观?
strrchr:函数声明为char* strrchr (const char*,int);我的实现为:
这与《C标准库》的代码有点不同,在《C标准库》中,strrchr实现为:
为什么要将*s==’/0’的判断放在for中的if里?为什么不像我一样,将之放在for体中?我的算法不是有更好的可读性和效率吗?
想了半天才弄明白,这是因为:strrchr准许用户用于在一个字符串中搜索’/0’。如果按照我的算法,是无法搜索’/0’,于是有了《C标准库》中的strrchr
以下是测试用例:
我的算法的打印结果是:"不可以搜索’/0’"……
不得不佩服写标准库这些人的思虑,太周到了……
还有一点,注意在传递来的参数中的int ch;在strrchr中要使用一个unsigned char转写,否则可能得不到正确的结果。
strstr:char* strstr(const char*,const char*);
该函数查找s2第一次出现在s1中的位置。若没有,则返回NULL
我的实现代码是:
一如既往的,没有进行NULL检查。但很不幸的发现,《C标准库》中进行了检查……(知道原因了,见最后……)从本质上,该算法没有任何困难之处。但《C标准库》的算法和我有很大不同……我的算法中,for(sc2=s2;(*sc1==*sc2)!='/0';sc1++,sc2++)是能正常运行的,开始我怀疑了好久……
《C标准库》中的算法如下:
这也是我根据《C标准库》上的算法自己写的,和《C标准库》中的方法也还是有不一样的,最大的不同在于:《C标准库》中的for和我的不太一样。for如下:
想了半天,想不通为何他特地那样写?有什么用途吗?我没有看出来……
我将自己的for更改了一下,使用了while而不是for,如下:
我觉得我自己很喜欢使用while,而不太喜欢使用for。但作者使用了很多for,却很少使用while,是否是for的效率要高于while?还是有别的什么原因?我总觉得while看起来更紧凑。
对于strstr,我更喜欢这样的版本:
我喜欢while多于for……
另,注意,strstr将空串看做是任意串的字串,于是当s2==NULL时,返回的是s1,因此在算法的开始,程序才进行s2的NULL检查,如果不进行,势必会return NULL.这样就违背题意了。
strtok:
最后一个函数,strtok。strtok用于按所给分隔符打印字符串。可能这是string.h中最难的字符串函数了。
以下是我最开始编写的函数。总体思想是正确的,但在个别地方出错,导致整个算法失败……
后来参考了《C标准库》中的实现,写出了正确的函数,如下:
最开始用的是(s==NULL)?(sstart=ssave):(sstart=s);后来才发现可以使用更简单的方式:sstart=s?s:ssave;真的是一个最简单的表达式就可以看出一个人的水平如何。
总结:
1. 如果在函数内定义了const,而该const又要返回,则在return的时候,宜将 之强制类型转换
2. 若函数形参在函数运行中不会改变,则最好能加const修饰……
3. while和for,谁的效率更高?
4. 总体上来讲,我还需要更加努力才是……