我已经建立了一个C解码器程序。将在下一行中给出编码消息的长度以及消息本身。消息的所有字符都将使用大写字母。任务是打印出解码后的消息。

编码:A->BB->C ... Y->ZZ->A(好了,您就明白了)

#include<stdio.h>

main()
{
    int size;
    scanf("%d", &size);

    char str[size];
    scanf("%s", str);

    for (int i=0; i<size; i++)
    {
        if (str[i]!='Z')
        {
            str[i] -= 1;
        }

        else
        {
            str[i] -= 25;
        }

        printf("%c", str[i]);
    }

    return 0;
}


这足够有效吗?能否以更简单,更有效的方式完成此工作?另外,我应该使用gets()而不是scanf()将该字符串作为输入吗?

请注意,我代码中的某些内容不是有效的C语言(例如,在int()之前未使用main(),循环条件内的变量声明)声明)。但是,我的编译器忽略了它们。我是个懒人。我喜欢少打字。尽管如此,我还是很清楚这一事实。

评论

该代码在C99中有效。

有人回答我的问题该怎么办?

请停止编辑代码。编辑会使答案无效。

过早的优化是万恶之源。学习算法,了解它们如何工作,运行速度如何以及为什么。学习数据结构。然后尝试将这些应用于您的问题,但是请注意,您只需要优化重要的地方即可。问题“效率是否足够”的答案是“您需要效率如何?”。您可以在游戏中使用O(n ^ 2)算法,但仍然可以完美运行,因为我们拥有快速的PC,并且算法可以在小数据上运行。您可能有O(n log(n)),而且速度太慢。分析,然后进行优化。并非如此。

请不要更新您问题中的代码以合并答案的反馈,否则会违反“代码审查”的“问题+答案”样式。这不是一个论坛,您应该在其中保留问题的最新版本。收到答案后,请查看您可能会做什么和可能不会做什么。

#1 楼

尽管我要提出很多建议,但这并不是一个糟糕的程序。我确实认识到您是一个学习者,并且是一个年轻的学习者。

我们可以在几个层次上分析此程序。一种是检查当前的实现。另一个正在考虑它是否便携。我们可以质疑界面是否良好–应该算人吗?还有其他可以使用的算法。

我要提出的一些观点与风格有关。尽管您可以用各种方式用C编写程序,但其中一些比其他方法更易于阅读。您显示的内容还不错,但是并不完全一致(一致性非常重要,而且很难!)。

逐行分析当前实现。

#include<stdio.h>


通常,在#include和标头名称之间放置一个空格,无论是标准标头(例如<stdio.h>)还是您自己的标头(例如"caesar.h")。

main()


您应始终指定每个函数的返回类型,包括main()。您正在使用C99或C11,因此需要指定返回类型。 (标准的最旧版本C90并不是那么挑剔。)此外,main()的返回类型应为int,尽管如果使用Microsoft编译器,它们也允许void。标准很明确;预期为int。编写int main(void)来明确地说“无命令行参数”通常是一个好主意,但实际上,int main()几乎在所有时间都可以正常工作。 (必须要做一些奇怪的事情。)

{
    int size;
    scanf("%d", &size);


您将学习编码C的痛苦课程之一是,您的很多工作都花在了错误检查上。特别是,检查输入函数很重要,因为它们是可能出错并对程序其余部分造成严重影响的地方之一。 scanf()函数返回成功的转换次数;它还可以返回0表示没有成功进行转换,并且返回EOF表示没有任何数据可读取。您还应该检查输入值是否合理:负数,零,甚至一个是可疑的,以及巨大的数字(例如,大于1024)。

    char str[size];


这是一个VLA —可变长度数组。自C99标准以来,它们非常有用,并且已成为C的一部分(尽管从技术上讲,它们是C11中的可选功能-在C99中是强制性的)。您已经为size字符分配了足够的空间,但是字符串以空字节'char str[size+1];'终止,并且您需要为此留出空间。您可能应该使用"%s"

    scanf("%s", str);


再次,应该检查是否已读取一些数据。请注意,使用scanf()时,"%1023s"首先跳过任何空白(空白,制表符,换行符),然后读取一个单词-一系列非空白。您没有限制输入的大小。如果用户说了10个字符,但在换行符或第一个空白或制表符之前键入了20个字符,则可能会遇到问题。如果将数组大小固定为总共1024个字节,则可以使用{将输入限制为1023个非空白和终止的空字节。使用可变长度的数组,难度更大。 (这是程序中的常见疏忽,即使是那些有很多经验的人也是如此。)

    for (int i=0; i<size; i++)


此循环很好,但是很多人更喜欢更多的空间周围的经营者。你写的是自洽的;很好。

    {
        if (str[i]!='Z'){
            str[i] -= 1;
        }


您假设用户是听话的并且完全按照您的意愿进行操作。不幸的是,用户很少听话,很少按照自己的意愿去做。如果用户按预期输入'abracadabra'而不是'ABRACADABRA',或者如果用户输入'@ Wonderful2CU',则不会获得预期的结果。您可以通过多种方式处理此问题。您当前选择的最简单的方法是忽略该问题-有时称为GIGO:垃圾进场,垃圾进出。您可以决定将小写字母转换为大写字母,然后对其进行解码。您可以决定不触摸非字母。您可以决定抱怨非字母。总体而言,最好像大写字母一样处理小写字母,而不更改非字母,但您可能会做出不同的决定。

许多人会在{之前加一个空格。其他人(包括我自己)会将else放在下一行,就像您在下面if之后所做的一样。这是一个不一致的情况。您应该使用1TBS(一种True Brace样式),或多或少在else中使用,或者应该使用Allman样式,或多或少在'Z'中使用。有关更多信息(以及更多其他样式),请参见Wikipedia中的缩进样式。

        else
        {
            str[i] -= 25;
        }


您可以简单地将'A'转换为str[i] = 'A';,而不是减去一个幻数-putchar('\n');会起作用很好。

        printf("%c", str[i]);
    }


这些行很好。但是,优良作法是用换行符结束输出行。您可以明智地添加return 0;来添加换行符。

    return 0;
}


我喜欢在main()的末尾看到显式的return 0;;还有其他人不相信。在C99和更高版本中,如果省略显式的main(),则如果使用main()函数-仅使用return函数;它不适用于任何其他功能-没有任何return 0;的“掉落”,等效于@

糟糕!

另一个问题,指出NowIGetToLearnWhatAHead指出,您的解码步骤与编码步骤并不完全相反。

Original: ABCDEFGHIJKLMNOPQRSTUVWXYZ
Encoded:  BCDEFGHIJKLMNOPQRSTUVWXYZA
Decoded:  ABCDEFGHIJKLMNOPQRSTUVWXA@


这里有小小的作弊;由于字符串长度的问题,在Z之后还存在一个杂散字符。

显然,解码后的信息与原始信息不同。代替A特殊,而是1特别。我们需要从其他所有内容中减去Z,并在字母为A时加25或映射到#include <ctype.h>

修改当前实现

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

int main(void)
{
    int size;
    if (scanf("%d", &size) != 1)
    {
        fprintf(stderr, "Failed to read an integer\n");
        return 1;
    }
    if (size < 2 || size > 1024)
    {
        fprintf(stderr, "Size %d is not in the range 2..1024\n", size);
        return 1;
    }

    char str[size + 1];
    char fmt[10];
    snprintf(fmt, sizeof(fmt), "%%%ds", size);

    if (scanf(fmt, str) != 1)
    {
        fprintf(stderr, "Failed to read a string\n");
        return 1;
    }

    for (int i = 0; i < size; i++)
    {
        if (toupper((unsigned char)str[i]) == 'A')
        {
            str[i] = 'Z';
        }
        else if (isalpha((unsigned char)str[i]))
        {
            str[i] -= 1;
        }
        printf("%c", str[i]);
    }
    putchar('\n');

    return 0;
}


我使用isalpha()提供宏toupper()char。一个稍微令人讨厌的问题是,普通<ctype.h>可以是有符号或无符号类型,并且来自unsigned char的宏期望将int转换为(unsigned char)。将à强制转换为这些调用可保护您免受那些在程序中键入ÿAa的虐待狂用户的攻击。 (要处理带重音符号的字符还不止于此,但是目前这是足够的安全预防措施。)

请注意,这会将ZZ解码为snprintf()

我还使用%100s创建适当的格式字符串,以确保缓冲区不会溢出。如果给定的大小为100,则会生成-= 25(这是要使用的正确大小,因为该数组的长度为101个字符-二者之差令人讨厌)。

可移植性

如果愿意,可以跳过本节,这对像我这样的人很重要,他们必须使他们编写的软件可以在许多不同的机器上工作。如果只使用一种机器类型,则可以忽略它。

原始代码使用-= 1Z映射字符。如评论中所述,这假定A的字符代码比A的字符代码大25。现在,实际上,这对于世界上使用的大多数字符集都是有效的,尤其是对于Unicode来说是准确的。但是,有些机器(例如IBM大型机)使用的代码集称为EBCDIC,其中I的代码是193,J的代码是201,R的代码是209,S的代码是217,Z的代码是226,以及Z为233。 Afgets()之间的距离是40,而不是您所期望的25。

如果您担心的话,您应该设计一种替代方法来映射字符。

在某些时候,您会注意到原始的Caesar密码使用的是3而不是1的移位,并且您将升级代码以处理1到25之间的任何移位(将0或26移位并不多;没有什么改变)。这使在字母末尾的环绕检查变得复杂。同样,这不是眼前的问题。

接口设计

您当前需要人员知道消息有多长时间。我不认识你,但是我不知道本段第一句中有多少个字符。当然,我可以指望,但是计算机在计算方面要好得多。您可以通过多种方式解决此问题。一种简单的方法是提供固定大小但较大的缓冲区(例如1024字节),然后将数据简单地读入其中。您可能会使用scanf()函数代替./decode-caesar < encoded.txt来完成工作。然后,您可能还会使用循环在程序运行中读取多行输入。修改后的程序看起来会更简单:

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

int main(void)
{
    char str[1024];

    while (fgets(str, sizeof(str), stdin) != 0)
    {
        int size = strlen(str);    
        for (int i = 0; i < size; i++)
        {
            if (toupper((unsigned char)str[i]) == 'A')
            {
                str[i] = 'Z';
            }
            else if (isalpha((unsigned char)str[i]))
            {
                str[i] -= 1;
            }
            printf("%c", str[i]);
        }
    }

    return 0;
}


它只是读取标准输入,直到遇到EOF。您可以将程序运行为fgets(),也可以手动键入编码后的消息,在这种情况下,您可以通过在行首键入控制字符来表示EOF,在Unix系统上为Control-D或在Control-在Windows系统上为Z(除非您已在Unix上更改了EOF的默认设置)。

请注意,int c;保留换行符,并且经过修改的算法不会更改换行符,因此无需打印多余的换行符在输出的末尾。

替代设计

到目前为止,这些程序一次读取了整个单词或一行,然后依次处理了每个字符。做到这一点的另一种方法是简单地依次读取和处理每个字符。这仍然更简单:

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

int main(void)
{
    int c;

    while ((c = getchar()) != EOF)
    {
        if (toupper(c) == 'A')
            c = 'Z';
        else if (isalpha(c))
            c -= 1;
        putchar(c);
    }

    return 0;
}


请注意,我使用了getchar()-这是因为int返回的是char,而不仅仅是char。它必须返回每个可能的EOF值和一个单独的char值,这意味着它不能仅返回int;它必须返回一个c。这样做的副作用是getchar()中的所有字符都将在0..255(正)范围内,因为unsigned char返回转换为toupper的字符值。这意味着可以安全地放弃对isalphaif的调用中的强制类型转换。

我选择在elseif子句之后的单行操作周围不使用大括号。这是另一个样式问题。有些人认为您应该始终在elseelse之后使用大括号(并且Perl等语言在其中坚持使用大括号),部分原因是,如果您在printf("%c", str[i])子句中添加了另一条语句,您可能会忘记添加必要的花括号。我不相信这种说法,但也许有些程序员很粗心,因此“总是使用花括号”确实可以帮助防止错误。

总结

有很多东西可以解释和讨论的细节。还有一些警告需要讨论。但是,我认为它们太小了,您无需担心它们。

您询问效率。您编写的代码相当有效。没有任何低效率。它很干净,通常比效率更重要。在担心效率之前,请先测量是否存在性能问题。有了这段代码,对于合理的输入大小,就不会出现性能问题-所有程序都不是效率低下的事情。

如果要提高效率,可以一次打印整个单词或一行,而不用一次使用printf("%c", str[i])一次打印每个字符。您可以将putchar(str[i])替换为gets(),这样会“更高效”,但是否能够对其进行测量更值得商<。

Michael Jackson(不,不是流行歌手,但也许他早于您的时间)有两个优化规则(提高了代码的效率):



第一个优化规则:不要这样做。

第二个优化规则(仅适用于专家):暂时不要做。

您还询问有关使用gets()的问题。您永远不要使用gets()!没有安全的方法来使用fgets(),因为这样做无法防止缓冲区溢出。默认情况下,应使用getline(),如果在基于POSIX的系统上工作,则应使用gets()。请注意,它们都保留了换行符,而fgets(buffer, sizeof(buffer), stdin)则将其​​删除。从buffer[strcspn(buffer, "\n")] = 'getline()';删除尾随换行符的一个好习惯是'a',无论缓冲区中是否有换行符,它都能正常工作。使用'Z',它可以告诉您该字符串有多长,因此您可以使用它来删除换行符(尽管有一个外部的机会,该文件不是以换行符结尾,所以最后读取的行将没有换行符-这令人讨厌)。

测试

哦,测试代码很重要。直到@NowIGetToLearnWhatAHeadIs指出解码中的错误,我才对任何数据运行您的代码(或我的代码)。那真让我懒惰-将您的错误复制到我的代码中。测试非常重要!

将'a'映射为'Z'

我决定不热衷于将我的代码将q4312079q映射到q4312079q。修复起来很容易。第三个程序的此变体处理它。它包含一个断言,以确保未在EBCDIC机器上成功使用该代码。

#include <assert.h>
#include <ctype.h>
#include <stdio.h>

int main(void)
{
    int c;

    assert('Z' - 'A' == 25 && 'z' - 'a' == 25);

    while ((c = getchar()) != EOF)
    {
        if (toupper(c) == 'A')
            c += 25;
        else if (isalpha(c))
            c -= 1;
        putchar(c);
    }

    return 0;
}


样本输入:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
09@?


示例输出:

ZABCDEFGHIJKLMNOPQRSTUVWXY
zabcdefghijklmnopqrstuvwxy
09@?


更干净!

使用C11,您可以使用“静态断言”代替运行时断言。这将阻止程序编译:

static_assert('Z' - 'A' == 25 && 'z' - 'a' == 25,
              "Alphabet should be contiguous but isn't");


评论


\ $ \ begingroup \ $
您的实现将“ BZ”解码为“ AA”。例如,ideone.com / Oh93oE。这似乎是错误的。
\ $ \ endgroup \ $
–布莱恩·飞蛾(Brian Moths)
17年1月2日在1:38



\ $ \ begingroup \ $
@NowIGetToLearnWhatAHeadIs:解码应该与问题中的相同。我将测试编译和检查,但我认为与原始代码相同。感谢您的注意。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月2日,下午1:42

\ $ \ begingroup \ $
我认为那是Soha Farhin Pine代码中的错误。我认为解码算法应该反转问题中描述的编码算法。具体来说,我认为B应该解码为A,A应该解码为Z,Z应该解码为Y(因为A编码为B,Y编码为Z,Z编码为A)。
\ $ \ endgroup \ $
–布莱恩·飞蛾(Brian Moths)
17年1月2日,下午1:46

\ $ \ begingroup \ $
@JonathanLeffler真的,真的很有帮助!谢谢你万千!您指出的某些内容(例如格式或无效的C)要么是拼写错误,要么是因为我的编译器忽略了它们而编写的。我还没结束-快要结束了-我不得不说,这是一个了不起的答案!键入所有这些花了您很多时间和精力。非常感谢您的帮助。
\ $ \ endgroup \ $
– Soha Farhin Pine
17年1月2日在10:17



\ $ \ begingroup \ $
也许应该是“…引起混乱”(对那些认为字母总是连续的人来说意味着混乱)。范围与打孔卡上的9列有关。这并不是要暗示IBM竭尽所能造成混乱。确实,可以说ASCII是“新手”,并造成了混乱。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月3日在16:45

#2 楼

这是当前算法的实现,带有适当的错误检查。

但是,此算法似乎与问题的内容不匹配,因为当问题表明需要递减值时,它将增加char值。

#include<stdio.h> // scanf(), perror(), fprintf() sprintf()
// need following header for error handling
#include <stdlib.h>  // exit(), EXIT_FAILRUE
#include <string.h>  // strlen()
#include <ctype.h>   // toupper(), isalpha()

// not a valid function signature: main()
int main( void )
{
//poor choice for a number that will never be <0    int size;
    size_t size;
//always check the returned value to assure the operation was successful    scanf("%d", &size);
    if( 1 != scanf( "%lu", &size ) )
    {
        perror( "scanf for string length failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, scanf successful

// need to allow for NUL termination character    char str[size];
    char str[ size+1];
// always include a MAX CHARACTERS modifier to avoid buffer overflow    scanf("%s", str);
// note: this input will stop early if a space or newline is encountered
    char format[20] = {'q4312078q'};
    sprintf( format, "%c%lu%c", '%', size, 's' );
    if( 1 != scanf( format, str ) )
    {
        perror( "scanf for string failed" );
        exit( EXIT_FAILURE );
    }

    // implied else, scanf successful

// never trust the user
    if( strlen( str ) != size )
    { // then wrong number of chars entered
        fprintf( stderr, "Num chars entered %lu does not match expected %lu chars\n",
                strlen(str),
                size );
        exit( EXIT_FAILURE );
    }

    // implied else, expected number of characters entered

//    for (int i=0; i<size; i++)
    for( size_t i=0; i<size; i++ )
    {
        // validate the input from the user
        if( !(isalpha(str[i])) || (toupper(str[i]) != str[i]) )
        { // invalid char entered
            fprintf( stderr, "invalid char %c entered, must all be upper case alpha\n", str[i] );
            exit(EXIT_FAILURE );
        }

        // implied else, valid character

        if (str[i]!='Z')
        { // any upper case char except 'Z'
// avoid implicit conversions str[i] -= 1;
            str[i]--;
        }

        else
        { // wrap around any 'Z'
            //str[i] -= 25;
            str[i] = 'A';
        }

        printf("%c", str[i]);
    } // end for

    return 0;
} // end function: main


但是,对scanf()printf()的调用非常昂贵。在循环中调用getchar()会更快地对数据行进行“编码”,而在该循环中调用putc()会更快。

评论


\ $ \ begingroup \ $
我观察到使用“%lu”代替“%zu”并不严格正确; z修饰符对于size_t是正确的。我还认为,有必要检查读取的非负值是否合理。如果用户键入-1作为大小(堆栈空间不足,是的,可以为无符号整数键入-1),代码将崩溃。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月3日在15:07



\ $ \ begingroup \ $
请注意,如果您的编译器提供带符号的纯字符型(标准允许,无符号),并且如果用户键入带重音符号的字符,则存储在str [i]中的值将转换为int可能为负。 函数/宏期望将普通字符转换为无符号字符(因此,正数在0..UCHAR_MAX范围内)或EOF(负;通常,但不一定是-1)。在这样的平台上调用isalpha(str [i])会导致不确定的行为。即使不愉快,强制转换也是安全的(isalpha((unsigned char)str [i]))。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月3日在15:40

\ $ \ begingroup \ $
@JonathanLeffler,您可能没有注意到此答案开头的语句,表明该答案只是对当前代码的改进。注意:zu修饰符将允许输入负值,而lu始终将输入的值视为正值。
\ $ \ endgroup \ $
–user3629249
17年5月5日在9:08

#3 楼

这使我想起了过去的时光-与同学和同事进行编程练习和比赛。

首先,main()必须返回一个int值。有些编译器也会接受void

int main()


那么您就不能使用可变大小的数组,因为它将花费更多的时间来编译程序。对于您自己的情况,应该执行以下操作:

#include <memory>

....
//char str[size];
char* str = malloc(size + 1);  // also make sure size > 0, and +1 for NULL terminates a string


for循环中,如果您确定所有输入字符的范围都从A到Z ...

str[i] = (str[i] - 'A' + 25) % 26 + 'A';


最后,别忘了释放字符串:

free(str);


我没有尝试编译所有这些,因此可能会有任何错误。

评论


\ $ \ begingroup \ $
如果可变长度数组不可用,您如何使用C语言的过时版本?
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月1日在21:04

\ $ \ begingroup \ $
[…继续…]。我不认为malloc()解决方案是最佳选择—我会使用固定的大尺寸(1024甚至4096)数组,而不是使用malloc(),特别是在OP是自识别的情况下青少年。是的,他们会足够快地学习malloc()(可能是下周,如果不是本周的话),但这会使事情不必要地复杂化,尤其是如果您错误地检查分配时。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月1日23:18



\ $ \ begingroup \ $
那是我的错,在切换到C ++之前,我没有学习C99,而是在CPP和C混合环境中工作,大部分是在Windows中工作。顺便说一句,在堆栈中使用大尺寸数组不是一个好主意。
\ $ \ endgroup \ $
–黄虎
17年1月1日在23:33

\ $ \ begingroup \ $
如今,在任何台式机上,1 KiB的数组已不是很大的堆栈分配。如果您正在嵌入式环境中工作,或者可能正在使用线程,则需要谨慎行事,但是对于单线程程序,即使Windows默认情况下也会提供1 MiB的堆栈空间;对于单线程程序,默认情况下Windows会提供1 MiB的堆栈空间。在基于Unix的系统上通常是8 MiB。仅使用1 KiB的空间对这两个都不是一个严重的内存压力源。
\ $ \ endgroup \ $
–乔纳森·莱弗勒(Jonathan Leffler)
17年1月2日在2:20



\ $ \ begingroup \ $
我也阅读了此规范,我认为最好不要使用它。也许对于初学者来说比较容易,但是如果您习惯了这一点,它可能会在更大的项目中引起隐藏的问题,并且难以发现。我曾经遇到过这样的情况,有人在某些函数中声明了一个缓冲区,每个函数都不是很大,但是如果它们按顺序调用,则在堆栈中写入一个变量会修改另一个变量。这一定是由于堆栈溢出引起的,我花了将近一个星期的时间才弄清楚并修复它。
\ $ \ endgroup \ $
–黄虎
17年1月2日,9:25