【问题标题】:C - strlen() seems to return a minumum of 6C - strlen() 似乎至少返回 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,已经试过了 - 没有任何改变。
  • 如果你不明白它是如何工作的,你需要回到你的教科书/教程并重新阅读关于字符串的章节。

标签: arrays c string cs50 strlen


【解决方案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 分配、归零和释放的内存。由于内存是一种全局资源,因此程序某一部分的更改可以揭示或隐藏程序另一部分的内存错误。

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

【讨论】:

  • 您可能还会接触到 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 只是数组开头的内存地址(指针)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-07
  • 2015-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-22
相关资源
最近更新 更多