【问题标题】:working with strings in c在 c 中处理字符串
【发布时间】:2012-04-16 05:36:31
【问题描述】:

请有人帮我理解下面程序中的这些代码行 这个程序根据作者写了一串hello world然后里面有一个函数也将字符串反转为world hello,我的任务是这段代码做什么?

char * p_divs = divs; //what does divs do
    char tmp;
    while(tmp = *p_divs++)
        if (tmp == c) return 1

;

还有这段代码在 void 函数中

*dest = '\0';//what does this pointer do?
    int source_len = strlen(source); //what is source
    if (source_len == 0) return;
    char * p_source = source + source_len - 1;
    char * p_dest = dest;
    while(p_source >= source){
        while((p_source >= source) && (inDiv(*p_source, divs))) p_source--;

这是主程序

#include <stdio.h>
#include <string.h>

int inDiv(char c, char * divs){
    char * p_divs = divs;
    char tmp;
    while(tmp = *p_divs++)
        if (tmp == c) return 1;
    return 0;
}

void reverse(char * source, char * dest, char * divs){
    *dest = '\0';
    int source_len = strlen(source);
    if (source_len == 0) return;
    char * p_source = source + source_len - 1;
    char * p_dest = dest;
    while(p_source >= source){
        while((p_source >= source) && (inDiv(*p_source, divs))) p_source--;
        if (p_source < source) break;
        char * w_end = p_source;
        while((p_source >= source) && (!inDiv(*p_source, divs))) p_source--;
        char * w_beg = p_source + 1;
        for(char * p = w_beg; p <= w_end; p++) *p_dest++ = *p;
        *p_dest++ = ' ';
    }
    *p_dest = '\0';
}

#define MAS_SIZE 100

int main(){
    char source[MAS_SIZE], dest[MAS_SIZE], divs[MAS_SIZE];
    printf("String          : "); gets(source);
    printf("Dividers        : "); gets(divs);
    reverse(source, dest, divs);
    printf("Reversed string : %s", dest);
    return 0;  
}

【问题讨论】:

  • "我的任务是这段代码有什么作用?"鉴于它使用gets,答案是:它的存在会危及您的系统。至少在 IMO 中,让某人分析这段代码就像给某人一桶 5 加仑的污水,然后问它是从哪里来的。代码是一个丑陋的混乱。如果你想知道如何做它应该做的任务,你应该直接问,所以有人可以解释一个体面的方法来做。这段代码应该被忽略,除非(也许)作为要避免的事情的例子。
  • @JerryCoffin:哇,这太苛刻了。我见过很多更糟糕的代码。
  • @MichaelBurr:我也见过更糟糕的情况,但是要阅读单词并反向打印,这仍然是一个糟糕的起点。从头开始会更容易理解任务。
  • @JerryCoffin:我不知道——它基本上从源字符串的末尾开始,找到单词并将它们复制到 dest——非常简单,总的来说,大多数 C 程序可能如何接近问题。代码肯定存在问题,但除了在缓冲区开始之外递减指针的技术 UB(很多生产代码也可以避免),乍一看,它似乎确实有效并且是相对错误的-免费。
  • @Jerry:这个特殊的疏忽可以相当容易地纠正——为什么不建议修复而不是把婴儿和洗澡水一起扔掉呢?该代码与 C 课程的其他代码相同。如果您要批评,请提供替代方案,我们可以看到您碰巧喜欢的简洁/优雅/性能折衷......会有一些。

标签: c


【解决方案1】:

这里可以调用inDiv在字符串divs中搜索字符c,例如:

inDiv('x', "is there an x character in here somewhere?') will return 1
inDiv('x', "ahhh... not this time') will return 0

解决它:

int inDiv(char c, char * divs)
{
    char * p_divs = divs;    // remember which character we're considering
    char tmp;
    while(tmp = *p_divs++)   // copy that character into tmp, and move p_divs to the next character
                             // but if tmp is then 0/false, break out of the while loop
         if (tmp == c) return 1;  // if tmp is the character we're searching for, return "1" meaning found
    return 0;   // must be here because tmp == 0 indicating end-of-string - return "0" meaning not-found
}

我们可以通过查看调用站点来推断reverse

int main()
{
    char source[MAS_SIZE], dest[MAS_SIZE], divs[MAS_SIZE];
    printf("String          : ");
    gets(source);
    printf("Dividers        : ");
    gets(divs);
    reverse(source, dest, divs);
    printf("Reversed string : %s", dest);

我们可以看到调用gets() 从标准输入读取字符数组sourcedivs -> 然后将这些输入提供给reverse()。打印dest 的方式,显然是要作为source 中字符串反转的目的地。在这个阶段,还没有洞察到divs 的相关性。

让我们看看源码...

void reverse(char * source, char * dest, char * divs)
{
    *dest = '\0'; //what does this pointer do?
    int source_len = strlen(source); //what is source
    if (source_len == 0) return;
    char* p_source = source + source_len - 1;
    char* p_dest = dest;
    while(p_source >= source)
    {
        while((p_source >= source) && (inDiv(*p_source, divs))) p_source--;

这里,*dest = '\0' 将 NUL 字符写入字符数组 dest - 这是编码字符串结尾位置的正常标记值 - 将其放在第一个字符 *dest 表示我们希望目的地被清除。我们知道 source 是我们将要反转的文本输入 - strlen() 将设置 source_len 为其中的字符数。如果没有字符,则 return 因为没有工作要做,并且输出已经以 NUL 终止。否则,将创建一个新指针 p_source 并将其初始化为 source + source_len - 1 -> 这意味着它指向源中的 last 非 NUL 字符。 p_dest 指向目标缓冲区开头的 NUL 字符。

然后循环说:while (p_source &gt;= source) - 为此,p_source 必须最初是 &gt;= source - 这是有道理的,因为 p_source 指向最后一个字符,source 是第一个字符地址缓冲;比较意味着我们将一个或两个移向另一个,直到它们交叉 - 每次都做一些工作。这让我们:

while((p_source >= source) && (inDiv(*p_source, divs))) p_source--;

这是我们刚刚看到的相同测试 - 但这次我们只是将 p_source 向后移动到字符串的开头,而 inDiv(*p_source, divs) 也是如此......这意味着 @987654353 处的字符@ 是divs 字符串中的字符之一。它的意思基本上是:向后移动,直到你超过了字符串的开头(尽管这个测试有未定义的行为,正如 Michael Burr 在 cmets 中指出的那样,如果字符串恰好被分配在地址 0 上,那么真的可能不起作用 -即使相对于某些特定的数据段,因为指针可以从 0 变为类似于 FFFFFFFF 十六进制的值,而不会看起来小于 0),或者直到您找到一个不是“分隔符”字符之一的字符。

在这里,我们对代码的作用有了一些真正的了解......将输入分成由divs 输入中的任何一组字符分隔的“单词”,然后用空格分隔符以相反的顺序将它们写入目标缓冲区。这有点超前了 - 但让我们跟踪一下:

下一行是...

if (p_source < source) break;

...这意味着如果循环退出已经超过了源字符串的前面,则退出所有循环(向前看,我们看到代码只是在已经生成的末尾放置了一个新的 NUL输出和返回 - 但这是我们所期望的吗? - 如果我们一直在“hello world”中支持“hello”,那么我们会点击字符串的开头并终止循环而不复制最后一个“hello” " 输出的单词!输出将始终是输入中的所有单词 - 除了第一个单词 - 反转 - 这不是作者描述的行为。

否则:

char* w_end = p_source;  // remember where the non-divider character "word" ends

// move backwards until there are no more characters (p_source < source) or you find a non-divider character
while((p_source >= source) && (!inDiv(*p_source, divs))) p_source--;

// either way that loop exited, the "word" begins at p_source + 1
char * w_beg = p_source + 1;

// append the word between w_beg and w_end to the destination buffer
for(char* p = w_beg; p <= w_end; p++) *p_dest++ = *p;

// also add a space...
*p_dest++ = ' ';

对于输入中的每个“单词”都会发生这种情况,然后最后一行将 NUL 终止符添加到目标。

*p_dest = '\0';

现在,你说:

根据 [to] 作者,它写了一个 hello world 字符串,然后其中有一个函数也将字符串反转为 world hello

好吧,给定输入“hello world”和包含空格的分隔符(但输入中没有其他字符),那么输出将是“hello world”(注意末尾的空格)。

对于它的价值——这段代码并没有那么糟糕......对于 ASCIIZ 缓冲区的 C 处理来说是很正常的,尽管关于输入长度的假设是危险的并且它丢失了第一个单词......

** 如何修复未定义的行为 **

关于未定义的行为 - 对地址的最小更改是更改循环,以便它们在缓冲区开始时终止,并让下一行明确检查它终止的原因并确定需要什么行为。那会有点难看,但不是火箭科学....

【讨论】:

  • 因此,如果取决于未定义的行为 (if (p_source &lt; source) break;) 不是“那么糟糕”,那么究竟是什么“那么糟糕”?
  • 那个特定的位是完全错误的——毫无疑问......我的意思是——如果它被修复了——整个代码是相当可读的、简洁的而不是过于神秘、可维护和高效的。
【解决方案2】:
char * p_divs = divs; //what does divs do
char tmp;
while(tmp = *p_divs++)
    if (tmp == c) return 1

divs 是一个指向 char 数组(当然是字符串)的指针。 p_divs 只是指向同一个字符串,并且在 while 循环中提取单个字符并将其写入tmp,然后指针递增,这意味着下一个字符将在下一个迭代器中提取。如果tmpc 匹配,则函数返回。

编辑:你应该了解更多关于指针的信息,看看Pointer Arithmetic

【讨论】:

    【解决方案3】:

    正如我在 cmets 中指出的,我不认为 C 真的是完成这项任务的理想工具(如果可以选择,我会毫不犹豫地使用 C++)。

    但是,我想如果我要谈论代码有多糟糕,反驳意见确实是正确的:我应该发布更好的东西。然而,与有问题的评论相反,我认为这并不代表优雅、简洁或性能方面的妥协。

    可能对真正的争论开放的唯一部分是优雅,但认为这足够简单和直接,在这方面几乎没有真正的问题。它显然更简洁——使用与原始格式大致相同的格式约定,我的 rev_words 是 14 行而不是 17 行。大多数人会格式化它们,我的是 17 行,他是 21 行。

    就性能而言,我希望两者在大多数情况下大约相当。我的避免了从数组的开头跑掉,这节省了一点时间。原版包含一个提前退出,这将节省一点点反转空字符串的时间。不过我认为两者都微不足道。

    我认为还有一点更重要:我有理由确定我不会像原来那样使用/调用/依赖未定义的行为。我想有些人可能会认为如果它在另一个领域提供了巨大优势是合理的,但鉴于它在其他领域大致并列或劣势,我无法想象谁会考虑它(甚至close to) 在这种情况下是合理的。

    #include <string.h>
    #include <stdlib.h>
    
    #include <stdio.h>
    
    int contains(char const *input, char val) {
        while (*input != val && *input != '\0')
            ++input;
        return *input == val;
    }
    
    void rev_words(char *dest, size_t max_len, char const *input, char const *delims) {
        char const *end = input + strlen(input);
        char const *start;
        char const *pos;
    
        do {
            for (; end>input && contains(delims, end[-1]); --end);
            for (start=end; start>input && !contains(delims,start[-1]); --start);
            for (pos=start; pos<end && max_len>1; --max_len) 
                *dest++=*pos++;
            if (max_len > 1) { --max_len; *dest++ = ' '; }
            end=start;
        } while (max_len > 1 && start > input);
        *dest++ = '\0';
    }
    
    int main(){ 
        char reversed[100];
    
        rev_words(reversed, sizeof(reversed), "This is an\tinput\nstring with\tseveral words in\n     it.", " \t\n.");
        printf("%s\n", reversed);
        return 0;
    }
    

    编辑:该:

    if (max_len > 1) { --max_len; *dest++ = ' '; }
    

    应该是:

    if (max_len > 1 && end-start > 0) { --max_len; *dest++ = ' '; }
    

    如果你想允许 max_len

    *dest++ = '\0';
    

    到:

    if (max_len > 0) *dest++ = '\0';
    

    如果缓冲区长度可以通过来自(可能是敌对的)用户的输入以某种方式设置,那可能是值得的。对于许多目的,只需要一个正的缓冲区大小就足够了。

    【讨论】:

    • 我希望您对原始代码中的 UB 的讨论是“合理的”并没有考虑到我的 cmets。我的 cmets 并不是要说原版中的 UB 没有问题 - 只是它没有使发布的代码值得被描述为“一桶污水”。原版中的 UB 行为可以通过对几行进行少量编辑来修复,而无需进行重大重组。我只是认为这是对发布代码的苛刻描述;我没有说代码是不需要改进的模型。
    • 他们主要是对 Tony 声称原始代码“还不错”的说法做出反应。虽然可以对代码进行黑客攻击以防止 UB,但它可能会进一步降低代码的可读性,这些代码一开始就不太好。最好解决根本问题。
    • @Jerry:你知道 - 从我的角度来看,你的解决方案与原始解决方案非常相似,如果有任何东西验证了一般风格和方法:-)。魔鬼在细节中。当输入以“delims”字符开头并且至少有一个标记时,您的反转会产生两个尾随空格。当 max_len 为 0 时,你写一个 NUL! ;-) 但是,让我们回到 C++ 并称之为休战 :-)。 (另外,我很想建议单次 delims 填充由字符代码索引的数组 - 允许 O(1) 分隔符检测 - 但这对于> 8位字符的可扩展性较差)。无论如何 +1!
    • 我故意尽可能地遵循原件,包括总是写 NUL(要求 size>0 似乎至少是相当现实的)。不过,双倍的尾随空格是一个疏忽。我认为足够多的分隔符使 O(1) 检测值得进行是非常不寻常的。
    猜你喜欢
    • 1970-01-01
    • 2012-10-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-04
    • 2010-11-14
    • 2010-09-24
    • 1970-01-01
    相关资源
    最近更新 更多