C - strlen() 似乎 return 至少 6

C - strlen() seems to return a minumum of 6

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

简而言之:在命令行上,您提供了一个 26 长字母数组作为参数,这些字母将用于 'encrypt' 在运行时被提示输入的字符串,称为明文。

然后遍历明文数组,它们的 ascii 整数值(稍微简化)用于索引作为命令行参数提供的“26 个字母键”,因此 'encrypting' 初始明文字符串 (ptxt) 并将其存储在新的密文字符串 (ctxt) 中。

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

以下示例:

$ ./substitution YTNSHKVEFXRBAUQZCLWDMIPGJO

plaintext: a

ciphertext: y.G[

ciphertext is 6 long

预期的输出只是 'y' ,但显然有些东西超出了范围 - 长度不应该是 6,而应该是 1。 让我发疯的是——如果你在初始化 'n' 后 取消注释 printf 语句,那么代码突然工作并且你得到以下内容:

$ ./substitution YTNSHKVEFXRBAUQZCLWDMIPGJO

plaintext: a

plaintext is 1 long

ciphertext: y

ciphertext is 1 long

我在这里错过了什么? 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;
    }
}

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

strlen 读取字节直到它看到一个空字节。如果没有空字节,strlen 将愉快地从数组末尾进入垃圾内存,直到它碰巧看到一个空字节或 OS 阻止程序超出其内存边界,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] != '[=10=]'; len++ );
    
    return len;
}

(IMO CS50 通过在您学习 C 时试图隐藏这一点而造成损害。长期以来一直试图将 C 视为不是 C。裸机,热棒,没有防护 rails 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] = '[=11=]';

And how is it that a printf call somehow remedies this issue?

当你像char ctxt[n+1]一样分配内存时,它未初始化。它不会自动归零。它包含该内存中的所有垃圾。你可能会走运并得到全零。它可以包含其他字符串。它可能包含看起来像随机垃圾的东西。

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

valgrind and AddressSanitizer 等工具可以帮助发现这些细微的错误。