编码:
A->B
,B->C
... Y->Z
,Z->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()
,循环条件内的变量声明)声明)。但是,我的编译器忽略了它们。我是个懒人。我喜欢少打字。尽管如此,我还是很清楚这一事实。#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个字符-二者之差令人讨厌)。可移植性
如果愿意,可以跳过本节,这对像我这样的人很重要,他们必须使他们编写的软件可以在许多不同的机器上工作。如果只使用一种机器类型,则可以忽略它。
原始代码使用
-= 1
和Z
映射字符。如评论中所述,这假定A
的字符代码比A
的字符代码大25。现在,实际上,这对于世界上使用的大多数字符集都是有效的,尤其是对于Unicode来说是准确的。但是,有些机器(例如IBM大型机)使用的代码集称为EBCDIC,其中I
的代码是193,J
的代码是201,R
的代码是209,S
的代码是217,Z
的代码是226,以及Z
为233。 A
和fgets()
之间的距离是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
的字符值。这意味着可以安全地放弃对isalpha
和if
的调用中的强制类型转换。我选择在
else
和if
子句之后的单行操作周围不使用大括号。这是另一个样式问题。有些人认为您应该始终在else
或else
之后使用大括号(并且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可能为负。
\ $ \ 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
评论
该代码在C99中有效。有人回答我的问题该怎么办?
请停止编辑代码。编辑会使答案无效。
过早的优化是万恶之源。学习算法,了解它们如何工作,运行速度如何以及为什么。学习数据结构。然后尝试将这些应用于您的问题,但是请注意,您只需要优化重要的地方即可。问题“效率是否足够”的答案是“您需要效率如何?”。您可以在游戏中使用O(n ^ 2)算法,但仍然可以完美运行,因为我们拥有快速的PC,并且算法可以在小数据上运行。您可能有O(n log(n)),而且速度太慢。分析,然后进行优化。并非如此。
请不要更新您问题中的代码以合并答案的反馈,否则会违反“代码审查”的“问题+答案”样式。这不是一个论坛,您应该在其中保留问题的最新版本。收到答案后,请查看您可能会做什么和可能不会做什么。