解答昨天的题目
题目:
void strcpy(char *p1, char *p2)
{
for (int i = 0; i <= strlen(p1); i++) {
p1[i] = p2[i];
}
}
错误:
1 接口设计不合理
1.1 不支持字符串链接表达式
1.2 没有函数正确性判断
1.3 参数名不清晰,不能够从函数名看出该参数的用途
1.4 没有对拷贝源串进行const声明
2 没有对接收参数进行合法性校验
3 对于标准C不支持for内部声明int i
4 for内部的strlen会多次计算,效率低
5 数组有越界情况
6 源串和目的串混乱,达不到拷贝目的
1 接口:针对上述错误和不足,可以进行如下定义
/**
* @brief 用于进行字符串拷贝
* @param dest 目的地址
* @param src const属性(并不保证src串不可被修改,只是提示程序员不要进行修改),源串
* @param dest_buf_size目的缓冲区的大小
* @return 成功 目的串的指针
* 失败 NULL
**/
char * strlcpy(char *dest, const char *src, size_t dest_buf_size);
2 程序:针对上述提出的问题做出下实现
char * strlcpy(char *dest, const char *src, size_t dest_buf_size)
{
int i = 0;
int src_len = 0;
if (NULL == dest || NULL == src) {
return NULL;
}
src_len = strlen(src);
for (i = 0; i < src_len && i < dest_buf_size; i++) {
dest[i] = src[i];
}
if (i == dest_buf_size) {
dest[dest_buf_size - 1] = ”;
}
return dest;
}
其它相关知识可以参阅日志strcpy strncpy strlcpy比较
经网友指正,的确上述代码有误,没调试就发给大家,有点不负责任:)
以下是经过调试的,效率也应该是最好的吧。
char * strlcpy(char *dest, const char *src, int dest_buf_size)
{
int i = 0;
if (NULL == dest || NULL == src) {
return NULL;
}
while (src[i] != ‘\0′ && i < dest_buf_size - 1) {
dest[i] = src[i];
i++;
}
dest[i] = ‘\0′;
return dest;
}
thx
张久安
If you enjoyed this post, make sure you subscribe to my RSS feed!









22 Comments, Comment or Ping
Hacker_QC
你的程序写错了,而且效率还非常低 -_-
May 15th, 2009
zja601
呵呵,多谢指正,现已修改。
看起来没有经过调试的代码毕竟没谱:)
May 15th, 2009
Hacker_QC
我刚才写了一个:
[code]
char* QCstrcpy(char *dest, const char *src, int size)
{
if (!dest && !src) return NULL;
int len = size-1 < strlen(src) ? size-1 : strlen(src);
memcpy(dest, src, len);
dest[len] = 0;
return dest;
}
[/code]
May 15th, 2009
zja601
呵呵,程序写的不错,多谢光临本Blog
不过我说三个小点吧。
1 零值比较问题:
<1 对于bool类型的判断,近量使用if (var) or if (!var)比较好,相对规范。浮点,就要看精度范围比较了
<2 整型判断if (0 == var) or if (0 != var)
<3 判断浮点if (PRECISE > |var|)
<4 对于指针if (NULL == var)
这些不做硬性要求,但写规范写程序好看。
2 int len = size-1 < strlen(src) ? size-1 : strlen(src);
<1 这句存在效率问题,走了两趟strlen,因此相当于对src扫描了两
遍。
<2 这句上面有if判断,有语句。因此这不符合标准C编程,c++没有这个限制
3 memcpy是标准clib库的实现,它的效率也是o(n)的。
字符赋零值近量使’\0′,但不做要求。
不妥之处请指正。
May 16th, 2009
Hacker_QC
1. 我开始是用len来接strlen,后来改成上面的样子。我知道 strlen 走了2便,但是我只是想程序短一点。
2. 看算法的速度,只看O级别在这里远远不够。memcpy的N前面的常数系数非常低,比用i一个一个来循环要高效的多。或者,你可以看编译出来的汇编,你就知道差距了。
3. 刚开始写代码的时候,我是写 var == NULL, 后来又看了一些中文的c语言方面所谓的经典著作,我改用 NULL == var 了,后来3年前,我不再这么写了,我觉得这么不专业,有冗余,代码非常啰嗦。而这种 NULL == var 的建议很多时候都是一家之见,还有人说 NULL == var 比 var == NULL 好,简直就是好笑,两者一模一样,如果偏爱用第一种的话,只能说明对自己没有信心,害怕自己少写一个 =。 !var 的用法我是3年前看stroustrup的FAQ然后决定采用了,这几乎是写c代码的约定俗成了。 别给我提教科书或者一些所谓的中文著作里面的建议,我不觉得那些作者自己写过多少代码,有多少自己的理解,大多只是copy。而我上面的代码风格是我几年来说确定下来,是出于我自己的理解和看别人大师的代码。另外,我的代码都是在编译器上面跑过,并且用数据测试过的。
May 16th, 2009
Hacker_QC
最后说一句:
我始终不理解 为什么 a[len] = 0; 会招质疑。 ” 和 NULL 还有 0 本来就是同一个东西,完全一模一样。 ” 要打的字符多出3个。
我认为很多人不写 a[len] = 0, 不是因为 ” 更好,而是他们根本不知道 a[len]=0 可以这么来写。
May 16th, 2009
Anonymous
这里应按类型赋值比较好。
比如NULL和0,
再比如a[len]是一个char类型,因此和”类型相匹配。
当然NULL如果查一下,应该是(void *)0,值虽说都和0相等,
但平时对代码规范了,有好处。
个从见解
May 16th, 2009
Hacker_QC
1. 关于NULL还是0,我参考的是 stroustrup 的FAQ。 关于NULL和0的比较如下:
http://www.research.att.com/~bs/bs_faq2.html#null
2. char的本质就是一个无符号数,说白了就是一个字节所表示的数在ASCII码里面的对应值。 a[i] = ” 就是将 a[i] 的那个byte赋予全0,这和 a[i] = 0; 没有任何区别。 而且编译器也是这么去做。 所以我觉得没必要去多打’\’ 这3个字符。 而且这3个字符全部在小指上,影响速度。
3. 另外,您说:“当然NULL如果查一下,应该是(void *)0”。
如果当真查过的话,会发现它的定义如下:
the stddef.h header supplied with MSVC++ 8
/* Define NULL pointer value */
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
也就是说,在C中,的确是 (void*) 0, 而如果在 C++ 库中 (比如我们现在用的 GCC 或者 Visual Studio)早已经换成了 0, 而且 (void *) 已经不再使用。 我们大学用的教程,和事实往往有出入,而且编书的人在一些问题上面没有自己实践过,没有自己的理解。再者,教程毕竟在时效性上面有滞后。
就比如:
课程里面说定义数组的时候,数组的长度比如是常量: int a[100] (可以)。 int a[n] (不行)
但是新的ansi标准已经支持了变量。所以现在我们可以这么写:
int n;
scanf(”%s”, &n);
int a[n];
而且这么写,明显方便和高效很多,可以更加合理的开辟内存空间。
May 17th, 2009
zja601
此篇文章引来了大家的争论,这是不错的,本人十分开心。因为对于一些东西只有有了争论,对于问题和本质都会有更加深刻的认识。
为此我也专门查了些相关的资料,转了篇相对比较客观的文章:http://www.jiuanblog.cn/2009/05/17/%e5%85%b3%e4%ba%8e0%e5%92%8cnull%e7%9a%84%e6%80%9d%e8%80%83%ef%bc%88%e8%bd%ac%e8%bd%bd%ef%bc%89/
Hacker_QC对于NULL,0和’\0′的认识是正确的,一些方面的理解也是挺深刻的。
在我说建议使NULL和’\0′,依个人经验,应该有些自已的道理。
是的,对于C++而言NULL, 0, ‘\0′本质是一样的,而且有许多C++程序员也习惯于统一使用0而非其它。但是,这样理解应该是不全面的。因为NULL和’\0′的存在是有它各自的道理的。作为一商业化的代码,代码要尽量规范清晰。下面个人简单说明一下NULL和’\0′的优点吧。
对于一个变量应具备一些必须属性,比如值,类型和空间。程序员在编程的时候应该对这些做道心中有数,也近量做到让读者读到这里心中有数。
比如对于变量的比较:
如果统一使 if (var),那么在别人阅读自已代码的时候,就不会立刻反应到这个变量的类型是什么。
但如果统一了规范,if (var)bool; if (NULL == p)指针; if (0 ==var)整型就量; if (PRECISE > |var|),那么对于代码的阅读者是十分有益的,在读到这一行代码的时候,可以有意识的加强读者对该变量类型的认识。对于编程者,写到这些代码的时候也是一样。
代码量小了,一般都能做到对自已定义的变量类型值心中有数,但如果服务代码量达到一定的量级,这些规范就会显出一定的作用了。
但是,对于许多忠城的C++程序员,早就习惯了全0的写法,因此这些并不是一些硬性规定,只是一个建议罢了。
记得康神说过,一个优质的工程师不在于程序写的多么精巧,技艺多么高超,而在于逻辑和表达足够的清晰,效率足够高,就算是让一个新人来接手你的负责的模块,很容易就能上手。如果一个工程师写的代码的确精细,漂亮,但是新人接手要付出很大代价的话,仍旧称不上是是优质工程师。
May 17th, 2009
Hacker_QC
和大家讨论了这么多, 难道就没人发现我的程序里面有一个功能bug?
May 17th, 2009
Hacker_QC
1. 康神是谁? 引用他的话来辩驳一个事物的时候,先应该论证他的话的正确性。
2. “如果一个工程师写的代码的确精细,漂亮,但是新人接手要付出很大代价的话,仍旧称不上是是优质工程师。”
上面的话我完全不同意。
首先. 新人的水平毕竟和大师有差距,那么肯定要花一番功夫来学习大师的代码。就比如我们看ACRush, Petr的代码,或者看google总部那些superguru的代码,90%我们不能很快上手,那么我们就能说别人的代码90%都是不好的? 再者,如果一个新手很快就能看懂大师的代码,那请问,还有什么必要去花时间学习大师的代码? 我认为别人既然是大师,那么他们的代码是他们多年智慧的结晶,我们上手起来自然需要花费不少时间,但是在这段学习代码的时间里面,我们的进步是非常非常快的。
另外,我不喜欢国内IT的气氛,很多时候就是人们只背教条,不去自己实践或者体会,更别说对自己的代码做一些测试。就比如我开始的代码,讨论了那么多,提出 规范和代码风格 之类的问题一堆。 只差最后说我 return NULL; 应该另起一行,并且最好用 {} 括起来 (我知道有人想说,但是最后觉得太过 trivial而没启口)。 但是,不幸的是,最关键的功能上的bug却没有一个人发现。 很多时候,这是国内IT的一种悲哀。
May 18th, 2009
zja601
我想bug不是没人能发现吧,if (!dest && !src)就是一个很简单的bug吧。
May 18th, 2009
zja601
#ifndef _SIZE_T_DEFINED
#ifdef _WIN64
typedef unsigned __int64 size_t;
#else
typedef _W64 unsigned int size_t;
#endif
#define _SIZE_T_DEFINED
#endif
标准clib库里面buffer长度参数是size_t, 贵程序长度使的应该是int类型,我想传个负值,应该就挂了吧。就算不是负数,src的buffer长度为0,一样不是自已想要的结果。
May 18th, 2009
zja601
另外我想这些不是到此讨论的重点了。我想Hacker_QC曲解了几个词吧。
接手和实际的开发;看懂和精通。
比如你离开了GOOGLE,你的代码存在的价值还有多大呢,是不是能在一个很短的时间内让别人知道如何入手你的程序呢?这是需要思考的。看得懂不代表就精通,甚至是概念有些方面是相背的。如果觉得“看懂”了别人的代码就没必要再下功能去理解更深层次的东西的话,我觉得太过短浅了。代码只是实现系统的工具,更重要的是系统设计的清晰,只有这些清晰了,才能从宏观上让别人了解自已哪些东西是做什么的。如果只以一些晦涩难懂的程序就认为是高手的话,这有些可笑了。
国外的一些不良之风就是这样,往往喜好这些,拿这些来设置门槛,使许多新人望而生畏。恰恰拿着这些说什么知识产权,严重阻碍发展中国家的IT发展。这是不好的。
对编程深思是好事,理解的深入更是好事,但我想讲的不是深入深出,更高的境界应该在深入浅出;不再为IT蒙上更多神密面纱而已。只有这样,才会有更多的人参与IT,才能使其发展的更迅速。
May 18th, 2009
Hacker_QC
我说的bug不是指的那些。
下面的程序,还有你修改过的程序,都有功能上的bug:
char* QCstrcpy(char *dest, const char *src, int size)
{
if (NULL == dest || NULL == src || size <= 0) return NULL;
int len = size-1 < strlen(src) ? size-1 : strlen(src);
memcpy(dest, src, len);
dest[len] = 0;
return dest;
}
May 18th, 2009
Hacker_QC
1.“如果只以一些晦涩难懂的程序就认为是高手的话,这有些可笑了。”
我的程序也算晦涩? !src , =0 我见过无数次,在一些开源的代码里面也是比比皆是,几乎很少用到 == NULL. 就好象,如果初中生看 New York Times 发现里面有很多GRE词汇,总不可能就说 它专门搞晦涩难懂的词来装高手吧。
2. 另外,试想一下,如果不是我回复,请问之前您的代码是不是现在还保持原样? 是不是bug没有被发现,或者程序没有去测试?
实话实说:通过我的代码,很多人看了后一下子知道:
memcpy 原来还有这么一个函数来做 连续内存拷贝 的,可以在这里使用。
还有 a[i] = 0 也可以这么来写的。
另外对 NULL 和 0 经过讨论也有了深入的了解。
3. 国内IT就是转来转去,你copy我的,我copy你的,在网上一搜什么,中文的论坛里面的帖子,内容几乎都一样。 说句实话罢了。没人静下来来写几个程序,体会体会里面的优劣。而回复内容大多都是: 顶!要不就是 说几句课本上的套话。 唉,这样下去,和别人的差距只会越来越大。
May 18th, 2009
Hacker_QC
上面的第三点我不是说您,只是对我之前的”另外,我不喜欢国内IT的气氛“的补充,国外虽然也有不好的地方,但是我们自己国内的氛围大家都明白,我觉得我们应该正视和改进。
最后,您自己写程序,然后发出来让大家来论坛就是件很不错的事情。
May 18th, 2009
zja601
嗯,上面的这些是挺有道理的。是的,国内IT是存在很多需要深思的地方,我写这个Blog也是想把自已对编程和一些结构设计上的想法和思考记录下来。当然,如果能对别人有些帮助的话,我是十分开心的。
对于你说的第二点,的确,当时想到顺手写了下来,没有调试,应该是对读者和自已的不负责任了,这一点我做检讨。
有些东西自已也在不断思考,当然,大家经过讨论,许多东西会更加明晰,这对大家的编程是十分有益的。我当初工作,也认为自已写的程序是最优美的,可实际参加工作后,经过许多高手指点,还是发现自已的许多不良习惯,这也是一个很大的进步吧。
说实话,!src, = 0 我的确见过不少,先前也写过。比如=0和={0}等等。不过最后还是形成了NULL==的习惯。因为这些习些对编程有时候还是有些帮助的,特别是型的值的概念上。
至于你说的如下程序的功能上的bug, 我喜耳恭听,也想想自已还有哪些没有考虑到。
char* strlcpy(char *dest, const char *src, int size)
{
if (NULL == dest || NULL == src || size <= 0) return NULL;
int len = size-1 < strlen(src) ? size-1 : strlen(src);
memcpy(dest, src, len);
dest[len] = 0;
return dest;
}
(注:malloc(0)是成立的,因此,size为零也是有可能的)
最后,你说的客气了,国内IT不良之风是需要慢慢进步的,至少希望通过自已可以影响一部分人吧。
May 18th, 2009
Hacker_QC
问题就是 memcpy 在copy的时候,要求dest和src不能重叠,不然就会出来问题. 当然,如果有重叠,用for一个一个字符赋值也是不行的.
测试的代码:
void main() {
char a[100] = “123456″;
char* b = a + 3;
printf(”%s\n”, a);
printf(”%s\n”, b);
char* x = QCstrcpy(b, a, 7);
printf(”%s\n”, a);
printf(”%s\n”, x);
}
结果:
在VS的编译器上:
123456
456
123123456
123456
结果是对的,估计是它的memcpy有cache,先一次性读进来一批数据,然后再copy.
在GCC上:
123456
456
123123123
123123
结果错误.
May 20th, 2009
zja601
多谢Hacker_QC,问题分析的挺深处。
跟了一把cl的汇编,msvc做了特殊处理。
;
; Check for overlapping buffers:
; If (dst < = src) Or (dst >= src + Count) Then
; Do normal (Upwards) Copy
; Else
; Do Downwards Copy to avoid propagation
;
mov eax,ecx ;V - eax = byte count…
mov edx,ecx ;U - edx = byte count…
add eax,esi ;V - eax = point past source end
cmp edi,esi ;U - dst <= src ?
jbe short CopyUp ;V - yes, copy toward higher addresses
cmp edi,eax ;U - dst < (src + count) ?
jb CopyDown ;V - yes, copy toward lower addresses
;
当时考虑到if (src == dest) return dest;有可能提高效率,但没有想到mem重叠。
再分析一下接口:
更合理一些呢?
char * strlcpy(char *dest, const char *src, size_t buf_size);
实际上接口里的const并不能做到一个限制作用,只能提供一个接口说明。因此这里原意是不想做对src的任何修改。
由此,我想buf重合应该属于不合法的情况,应认为调用不合法而返回NULL更加合理一些。
所以头部是否改成:
if (dest == src) return dest;
if (NULL == dest || NULL == src || size == 0 || |dest - src|
希望能够有更深入的讨论。
最后多谢Hacker_QC
May 20th, 2009
Hacker_QC
哦,谢谢你的分析。看来,微软在VS上面还是写得挺用心的!
我觉得 char* 始终在处理字符串上面有先天不足。 比如这个函数,我们就无法判断 用户给的dest的长度是不是真的有他输入的size那么大。
我还是强烈建议用 std::string. 这样我们的strcpy就可以简单地写:
std::string strcpy(char* src) {
if (!src) return “”;
return std::string(src);
}
然后在函数外面用 str.c_str() 来去 char* ,这样既简单又安全。
May 20th, 2009
zja601
的确是这样子,使的时候使std::string可以免去很多的困扰,方便安全。这里的讨论也是一个相互学习的过程,有空可以多来我的blog交流。
May 20th, 2009
Reply to “解答昨天的题目”
You must be logged in to post a comment.