C - strlen() 似乎至少返回 6

Posted

技术标签:

【中文标题】C - strlen() 似乎至少返回 6【英文标题】:C - strlen() seems to return a minumum of 6 【发布时间】:2021-07-14 22:34:28 【问题描述】:

以下是 CS50 课程练习的一部分。此处描述了完整的问题: https://cs50.harvard.edu/x/2021/psets/2/substitution/

简而言之:在命令行中,您提供一个 26 长的字母数组作为参数,这些字母将用于“加密”在运行时提示输入的字符串,称为纯文本。

然后循环遍历明文数组,并使用它们的 ascii 整数值(稍微简化)来索引作为命令行参数提供的“26 字母密钥”,从而“加密”初始明文字符串 (ptxt)并将其存储在一个新的密文字符串(ctxt)中。

问题我遇到的输入是纯文本 比 6 - 我用来将 ptxt 的长度存储在 'n 中的 strlen() 函数' 似乎返回 6。因此,如果我在纯文本提示符下仅键入字母 'a' - n 似乎设置为 6。

以下示例:

$ ./substitution YTNSHKVEFXRBAUQZCLWDMIPGJO

明文:a

密文:y.G[

密文长度为6

预期的输出只是 'y' ,但显然有些东西超出了界限 - 长度不应该是 6,而应该是 1。 让我抓狂的是 - 如果您在初始化 'n' 后取消注释 printf 语句,那么代码会突然起作用,您会得到以下信息:

$ ./substitution YTNSHKVEFXRBAUQZCLWDMIPGJO

明文:a

明文长度为 1

密文:是的

密文长度为 1

我在这里缺少什么? printf 调用如何以某种方式解决此问题?

让我发疯:)

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

bool is_letter(string array);
char encrypt(string key, char c);

//Command Line input a key to 'encrypt' some plaintext inputted at runtime
int main(int argc, string argv[])

    // if there are NOT 2 arguments OR the first argument is NOT just letters OR is not 26 letters
    if (argc != 2 || !is_letter(argv[1]) || strlen(argv[1]) != 26)
    
        printf("Usage: ./caesar key (where key must be 26 letters)\n");
        return 1;
    

    // prompt user for a plaintext string, store the length in n and initialize a ciphertext string with same length
    string ptxt = get_string("plaintext: ");
    int n = strlen(ptxt);
    //printf("plaintext is %i long\n", n); //this is here to correct n (try commenting out this line and see what happens for ptxt < 6)
    char ctxt[n];
    for (int i = 0; i < n; i++)
    
        ctxt[i] = encrypt(argv[1], ptxt[i]);
    
    printf("ciphertext: %s\n", ctxt);
    printf("ciphertext is %i long\n", (int) strlen(ctxt));
    return 0;



// function that checks whether command line argument is all letters
bool is_letter(string array)

    int n = strlen(array);
    for (int i = 0; i < n; i++)
    
        if (!isalpha(array[i])) //loop over string - if any char is not a letter - return false
        
            return false;
        
    
    return true; //reaching this means all chars in the string are a letter - return true


//function that takes a key and a char and returns the "encrypted" char
char encrypt(string key, char c)

    if (isalpha(c))
    
        int n = 0;
        char letter = 0;
        if (isupper(c))
        
            n = c - 65;
            letter = key[n];
            return toupper(letter);
        
        else
        
            n = c - 97;
            letter = key[n];
            return tolower(letter);
        
    
    else
    
        return c;
    

【问题讨论】:

您需要用'\0' 终止ctxt,仅此而已。与strlen无关。 您还需要char ctxt[n+1]; 为空字节留出空间。 您希望printf 知道要打印多少个字符?您如何期望strlen 知道数组的长度?当事情没有按照您的预期进行时,首先要看的是为什么您预期会有不同的行为以及您的预期是否合理。 @Barmar,已经试过了 - 没有任何改变。 如果你不明白它是如何工作的,你需要回到你的教科书/教程并重新阅读关于字符串的章节。 【参考方案1】:

C 中没有“字符串”这样的东西。C 中的“字符串”实际上是字节数组,char *。 C中的数组不知道它们有多长,没有内置bounds checks。您要么需要知道它们的大小,要么有一个终结器。 “字符串”以称为“空字节”的 0 结尾,通常表示为 \0

strlen 读取字节,直到它看到一个空字节。如果没有空字节,strlen 将愉快地离开数组的末尾进入垃圾内存,直到它碰巧看到空字节或操作系统阻止程序超出其内存边界,segmentation fault。

// A basic strlen() implementation.
size_t my_strlen(const char *string) 
    size_t len;

    // no body, just counting until it sees a null byte.
    for( len = 0; string[len] != '\0'; len++ );
    
    return len;

(IMO CS50 在你学习 C 时试图隐藏这一点是有害的。长期以来,人们试图将 C 视为不是 C。C 的裸金属、热棒、无护栏性质不能零碎隐藏。你要么得到一团糟,要么得到一种新语言。如果你想要字符串,请使用 C++ 或像 GLib 这样的完全实现的库。)

当一个字节一个字节地创建一个新字符串时,你必须终止它。并且它必须有一个额外的字节来存储 0。

    // Allocate an extra byte for the terminating null.
    // At this point ctxt contains garbage.
    char ctxt[n+1];
    for (int i = 0; i < n; i++)
    
        ctxt[i] = encrypt(argv[1], ptxt[i]);
    

    // Terminate the string.
    ctxt[n] = '\0';

printf 调用如何以某种方式解决这个问题?

当您像char ctxt[n+1] 这样分配内存时,它未初始化。它不会自动归零。它包含那个记忆中的任何垃圾。您可能会很幸运并得到全零。它可以包含其他字符串。它可能包含看起来像随机垃圾的东西。

在分配ctxt 之前添加printf 会稍微改变分配给ctxt 的内存块。 printf 还必须分配内存,因此ctxt 可能会得到一个稍微不同的内存块,它恰好以零开头。 ctxt 可能会得到一块被 printf 分配、归零和释放的内存。由于内存是一种全局资源,因此程序某一部分的更改可以揭示或隐藏程序另一部分的内存错误。

valgrind 和AddressSanitizer 等工具可以帮助找出这些细微的错误。

【讨论】:

您可能还会接触到 C 中的数组。与其他语言不同,数组不跟踪自己的长度,它只是一块内存,程序员必须确保没有超出它的末尾。 @GarrGodfrey 这应该是 CS50 的工作。 ;) 好建议,我已经添加进去了。 感谢 Schwern,这确实很有帮助。肯定没有明确提到/教导我们必须自己终止字符串,但它是有道理的并且有效。不过,我仍然不确定以下几点:您能解释一下为什么对于超过 6 个字符的字符串,这似乎可以正常工作(无需事先手动终止 ctxt),但对于较短的字符串却不行吗? @Peter 可能是因为 ctxt 的未分配垃圾以一些垃圾字符开头,然后是一个零块。考虑ctxt = 'a', 'b', 'c', 'd', 'e', 'f', '\0', '\0', '\0', 'z', 'y', 'x', '\0'如果你只用123 ctxt覆盖前几个字符是'1', '2', '3', 'd', 'e', 'f', '\0', '\0', '\0', 'z', 'y', 'x', '\0'或“123def”。如果你用 123456 覆盖 ctxt 是'1', '2', '3', '4', '5', '6', '\0', '\0', '\0', 'z', 'y', 'x', '\0' 或“123456”。如果你用 123456789 覆盖 ctxt 是 '1', '2', '3', '4', '5', '6', '7', '8', '9', 'z', 'y', 'x', '\0' 或 "123456789zyx"。 @Peter 是的,你很幸运。操作系统会阻止程序跳出程序分配的内存,但 C 不会阻止程序跳出变量分配的内存。没有运行时边界检查。如果您偏离行外,对 ctxt 的更改可能会覆盖另一个变量的内存。或者额外的内存可能被分配给另一个变量,然后可以覆盖 ctxt 的。我最后提到的工具可以捕捉到这些错误。 ctxt 只是数组开头的内存地址(指针)。

以上是关于C - strlen() 似乎至少返回 6的主要内容,如果未能解决你的问题,请参考以下文章

c语言中strlen()怎么用

C语言怎样判断用户输入的是中文?

Makefile没有针对目标的规则

C++Sizeof与Strlen的区别与联系

strcpy,strlen函数和string类原型

C语言----string函数和memory函数