Zhang Jiuan’ Notes

解答昨天的题目

题目:

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

  1. 你的程序写错了,而且效率还非常低 -_-

  2. 呵呵,多谢指正,现已修改。
    看起来没有经过调试的代码毕竟没谱:)

  3. 我刚才写了一个:
    [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]

  4. 呵呵,程序写的不错,多谢光临本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′,但不做要求。

    不妥之处请指正。

  5. 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。而我上面的代码风格是我几年来说确定下来,是出于我自己的理解和看别人大师的代码。另外,我的代码都是在编译器上面跑过,并且用数据测试过的。

  6. 最后说一句:

    我始终不理解 为什么 a[len] = 0; 会招质疑。 ” 和 NULL 还有 0 本来就是同一个东西,完全一模一样。 ” 要打的字符多出3个。

    我认为很多人不写 a[len] = 0, 不是因为 ” 更好,而是他们根本不知道 a[len]=0 可以这么来写。

  7. Anonymous

    这里应按类型赋值比较好。
    比如NULL和0,
    再比如a[len]是一个char类型,因此和”类型相匹配。
    当然NULL如果查一下,应该是(void *)0,值虽说都和0相等,
    但平时对代码规范了,有好处。
    个从见解

  8. 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];
    而且这么写,明显方便和高效很多,可以更加合理的开辟内存空间。

  9. 此篇文章引来了大家的争论,这是不错的,本人十分开心。因为对于一些东西只有有了争论,对于问题和本质都会有更加深刻的认识。

    为此我也专门查了些相关的资料,转了篇相对比较客观的文章: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的写法,因此这些并不是一些硬性规定,只是一个建议罢了。

    记得康神说过,一个优质的工程师不在于程序写的多么精巧,技艺多么高超,而在于逻辑和表达足够的清晰,效率足够高,就算是让一个新人来接手你的负责的模块,很容易就能上手。如果一个工程师写的代码的确精细,漂亮,但是新人接手要付出很大代价的话,仍旧称不上是是优质工程师。

  10. 和大家讨论了这么多, 难道就没人发现我的程序里面有一个功能bug?

  11. 1. 康神是谁? 引用他的话来辩驳一个事物的时候,先应该论证他的话的正确性。

    2. “如果一个工程师写的代码的确精细,漂亮,但是新人接手要付出很大代价的话,仍旧称不上是是优质工程师。”
    上面的话我完全不同意。

    首先. 新人的水平毕竟和大师有差距,那么肯定要花一番功夫来学习大师的代码。就比如我们看ACRush, Petr的代码,或者看google总部那些superguru的代码,90%我们不能很快上手,那么我们就能说别人的代码90%都是不好的? 再者,如果一个新手很快就能看懂大师的代码,那请问,还有什么必要去花时间学习大师的代码? 我认为别人既然是大师,那么他们的代码是他们多年智慧的结晶,我们上手起来自然需要花费不少时间,但是在这段学习代码的时间里面,我们的进步是非常非常快的。

    另外,我不喜欢国内IT的气氛,很多时候就是人们只背教条,不去自己实践或者体会,更别说对自己的代码做一些测试。就比如我开始的代码,讨论了那么多,提出 规范和代码风格 之类的问题一堆。 只差最后说我 return NULL; 应该另起一行,并且最好用 {} 括起来 (我知道有人想说,但是最后觉得太过 trivial而没启口)。 但是,不幸的是,最关键的功能上的bug却没有一个人发现。 很多时候,这是国内IT的一种悲哀。

  12. 我想bug不是没人能发现吧,if (!dest && !src)就是一个很简单的bug吧。

  13. #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,一样不是自已想要的结果。

  14. 另外我想这些不是到此讨论的重点了。我想Hacker_QC曲解了几个词吧。
    接手和实际的开发;看懂和精通。
    比如你离开了GOOGLE,你的代码存在的价值还有多大呢,是不是能在一个很短的时间内让别人知道如何入手你的程序呢?这是需要思考的。看得懂不代表就精通,甚至是概念有些方面是相背的。如果觉得“看懂”了别人的代码就没必要再下功能去理解更深层次的东西的话,我觉得太过短浅了。代码只是实现系统的工具,更重要的是系统设计的清晰,只有这些清晰了,才能从宏观上让别人了解自已哪些东西是做什么的。如果只以一些晦涩难懂的程序就认为是高手的话,这有些可笑了。
    国外的一些不良之风就是这样,往往喜好这些,拿这些来设置门槛,使许多新人望而生畏。恰恰拿着这些说什么知识产权,严重阻碍发展中国家的IT发展。这是不好的。
    对编程深思是好事,理解的深入更是好事,但我想讲的不是深入深出,更高的境界应该在深入浅出;不再为IT蒙上更多神密面纱而已。只有这样,才会有更多的人参与IT,才能使其发展的更迅速。

  15. 我说的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;
    }

  16. 1.“如果只以一些晦涩难懂的程序就认为是高手的话,这有些可笑了。”

    我的程序也算晦涩? !src , =0 我见过无数次,在一些开源的代码里面也是比比皆是,几乎很少用到 == NULL. 就好象,如果初中生看 New York Times 发现里面有很多GRE词汇,总不可能就说 它专门搞晦涩难懂的词来装高手吧。

    2. 另外,试想一下,如果不是我回复,请问之前您的代码是不是现在还保持原样? 是不是bug没有被发现,或者程序没有去测试?

    实话实说:通过我的代码,很多人看了后一下子知道:
    memcpy 原来还有这么一个函数来做 连续内存拷贝 的,可以在这里使用。
    还有 a[i] = 0 也可以这么来写的。
    另外对 NULL 和 0 经过讨论也有了深入的了解。

    3. 国内IT就是转来转去,你copy我的,我copy你的,在网上一搜什么,中文的论坛里面的帖子,内容几乎都一样。 说句实话罢了。没人静下来来写几个程序,体会体会里面的优劣。而回复内容大多都是: 顶!要不就是 说几句课本上的套话。 唉,这样下去,和别人的差距只会越来越大。

  17. 上面的第三点我不是说您,只是对我之前的”另外,我不喜欢国内IT的气氛“的补充,国外虽然也有不好的地方,但是我们自己国内的氛围大家都明白,我觉得我们应该正视和改进。

    最后,您自己写程序,然后发出来让大家来论坛就是件很不错的事情。

  18. 嗯,上面的这些是挺有道理的。是的,国内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不良之风是需要慢慢进步的,至少希望通过自已可以影响一部分人吧。

  19. 问题就是 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

    结果错误.

  20. 多谢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

  21. 哦,谢谢你的分析。看来,微软在VS上面还是写得挺用心的!

    我觉得 char* 始终在处理字符串上面有先天不足。 比如这个函数,我们就无法判断 用户给的dest的长度是不是真的有他输入的size那么大。

    我还是强烈建议用 std::string. 这样我们的strcpy就可以简单地写:
    std::string strcpy(char* src) {
    if (!src) return “”;
    return std::string(src);
    }

    然后在函数外面用 str.c_str() 来去 char* ,这样既简单又安全。

  22. 的确是这样子,使的时候使std::string可以免去很多的困扰,方便安全。这里的讨论也是一个相互学习的过程,有空可以多来我的blog交流。

Reply to “解答昨天的题目”

You must be logged in to post a comment.

返回顶部