作者dieft ( )
看板NCTU-STAT94G
标题[转录] C 语言新手十诫(增修一版)
时间Thu Oct 20 17:41:03 2005
※ [本文转录自 C_and_CPP 看板]
作者: khoguan (Khoguan Phuann) 看板: C_and_CPP
标题: C 语言新手十诫(增修一版)
时间: Sat Sep 3 01:07:31 2005
C 语言新手十诫(The Ten Commandments for Newbie C Programmers)
by Khoguan Phuann
请注意:
(1) 本篇旨在提醒新手,避免初学常犯的错误(其实老手也常犯:-Q)。
但不能取代完整的学习,请自己好好研读一两本 C 语言的好书,
并多多实作练习。
(2) 强烈建议新手先看过此文再发问,你的问题极可能此文已经提出并
解答了。
(3) 以下所举的错误例子如果在你的电脑上印出和正确例子相同的结果,
那只是不足为恃的一时侥幸。
(4) 不守十诫者,轻则执行结果的输出数据错误,或是程式当掉,重则
引爆核弹、毁灭地球(如果你的 C 程式是用来控制核弹发射器的话)。
一、你不可以使用尚未给予适当初值的变数。
错误例子:
int accumulate(int max) /* 从 1 累加到 max,传回结果 */
{
int sum; /* 未给予初值的区域变数,其内容值是垃圾 */
int num;
for (num = 1; num <= max; num++) {
sum += num;
}
return sum;
}
正确例子:
int accumulate(int max)
{
int sum = 0; /* 正确的赋予适当的初值 */
int num;
for (num = 1; num <= max; num++) {
sum += num;
}
return sum;
}
二、你不可以存取超过阵列既定范围的空间。
错误例子:
int str[5];
int i;
for (i = 0; i <= 5; i++) str[i] = i;
正确例子:
int str[5];
int i;
for (i = 0; i < 5; i++) str[i] = i;
说明:宣告阵列时,所给的阵列元素个数值如果是 N, 那麽我们在後面
透过 [索引值] 存取其元素时,所能使用的索引值范围是从 0 到 N-1,
也就是 C 和 C++ 的阵列元素是从第 0 个开始算起,最後一个元素的
索引值是 N-1, 不是 N。
C/C++ 为了执行效率,并不会自动检查阵列索引值是否超过阵列边界,
我们要自己写程式来确保不会越界。一旦越界,将导致无法预期的後果。
三、你不可以提取(dereference)不知指向何方的指标(包含 null 指标)。
错误例子:
char *pc1; /* 未给予初值,不知指向何方 */
char *pc2 = 0; /* pc2 起始化为 null pointer */
*pc1 = 'a'; /* 将 'a' 写到不知何方,错误 */
*pc2 = 'b'; /* 将 'b' 写到「位址0」,错误 */
正确例子:
char c; /* c 的内容尚未起始化 */
char *pc1 = &c; /* pc1 指向字元变数 c */
/* 动态分配 10 个 char(其值未定),并将第一个char的位址赋值给 pc2 */
char *pc2 = (char *)malloc(10);
*pc1 = 'a'; /* c 的内容变为 'a' */
pc2[0] = 'b'; /* 动态配置来的第 0 个字元,内容变为 'b'
/* 最後记得 free() 掉 malloc() 所分配的空间 */
free(pc2);
说明:指标变数必需先指向某个明确的东西(object),才能进行操作。
四、你不可以将字串常数赋值(assign)给 char* 变数,然後透过该变数
改写字串的内容(只能读不能写)。
错误例子:
char* pc = "john";
*pc = 'J';
printf("Hello, %s\n", pc);
正确例子:
char pc[] = "john";
*pc = 'J'; /* 或 pc[0] = 'J'; */
printf("Hello, %s\n", pc);
说明:字串常数的内容是唯读的。上面的错误例子,是将其内容所在的位址赋
值给字元指标 pc, 我们透过指标只可以去读该字串常数的内容,而不应该做
写入的动作。而正确例子,则是另外宣告一个独立的字元阵列,它的大小我们
未明文指定([]),编译器会自动将其设为刚好可以容纳後面的字串常数起始
值的大小,包括字串後面隐含的 '\0' 字元,并将字串常数的内容复制到字元
阵列中,因此可以自由的对该字元阵列的内容进行读和写。
错误例子(2):
char *s1 = "Hello, ";
char *s2 = "world!";
/* strcat() 不会另行配置空间,只会将资料附加到 s1 所指唯读字串的後面,
造成写入到程式无权碰触的记忆体空间 */
char *s3 = strcat(s1, s2);
正确例子(2):
/* s1 宣告成阵列,并保留足够空间存放後续要附加的内容 */
char s1[20] = "Hello, ";
char *s2 = "world!";
/* 因为 strcat() 的返回值等於第一个参数值,所以 s3 就不需要了 */
strcat(s1, s2);
五、你不可以对尚未分配所指空间的 char* 变数,进行(字串)阵列的相关操作。
其他型别的指标亦然。
错误例子:
char *name; /* name 尚未指向有效的空间 */
printf("Your name, please: ");
gets(name);
printf("Hello, %s\n", name);
正确例子(1):
/* 如果编译期就能决定字串的最大空间,那就不要宣告成 char* 改用 char[] */
char name[21]; /* 字串最长 20 个字元,另加一个 '\0' */
printf("Your name, please: ");
gets(name);
printf("Hello, %s\n", name);
正确例子(2):
/* 若是在执行时期才能决定字串的最大空间,则需利用 malloc() 函式来动态
分配空间 */
size_t length;
char *name;
printf("请输入字串的最大长度(含null字元): ");
scanf("%u", &length);
name = (char *)malloc(length);
printf("Your name, please: ");
scanf("%s", name);
printf("Hello, %s\n", name);
/* 最後记得 free() 掉 malloc() 所分配的空间 */
free(name);
注意:上例用 gets() 或 scanf() 来读入字串,是不安全的。 因为这些函式
不会帮我们检查使用者所输入的字串长度是否超过我们所分配的 buffer 空间,
很可能会发生 buffer overflow。比较安全的做法是用 fgets() 来取代。如:
char *p;
char name[21];
printf("Your name, please: ");
fgets(name, sizeof(name), stdin);
/* fgets()会连行末的'\n'也读进字串中,所以要找出存入'\n'的位置,填入 '\0'
if ((p = strchr(name, '\n')) != NULL)
*p = '\0';
printf("Hello, %s\n", name);
六、你不可以在函式中回传一个指向区域性自动变数的指标。否则,会得到垃圾值。
[感谢 gocpp 网友提供程式例子]
错误例子:
char *getstr(char *name)
{
char buf[30] = "hello, "; /*将字串常数"hello, "的内容复制到buf阵列*/
strcat(buf, name);
return buf;
}
说明:区域性自动变数,将会在离开该区域时(本例中就是从getstr函式返回时)
被消灭,因此呼叫端得到的指标所指的字串内容就失效了。【不过,倒是可以从
函式中直接传回字串常数,赋值给呼叫端的一个 const char * 变数,它既是唯
读的(参见第四诫),同时也具有恒常的储存期(static storage duration),其
内容将一直有效。】
正确例子:
void getstr(char buf[], int buflen, char const *name)
{
char const s[] = "hello, ";
assert(strlen(s) + strlen(name) < buflen);
strcpy(buf, s);
strcat(buf, name);
}
[针对字串操作,C++提供了更方便安全的 string class, 能用就尽量用]
#include <string>
using std::string;
string getstr(string const &name)
{
return string("hello, ") += name;
}
七、你不可以只做 malloc(), 而不做相应的 free(). 否则会造成记忆体漏失。
但若不是用 malloc() 所得到的记忆体,则不可以 free()。已经 free()了
所指记忆体的指标,在它指向另一块有效的动态分配得来的空间之前,不可
以再被 free(),也不可以提取(dereference)这个指标。
[C++] 你不可以只做 new, 而不做相应的 delete.
八、你不可以在数值运算、赋值或比较中随意混用不同型别的数值,而不谨慎考
虑数值型别转换可能带来的「意外惊喜」(错愕)。必须随时注意数值运算
的结果,其范围是否会超出变数的型别。
错误例子(1):
unsigned int sum = 2000000000 + 2000000000; /* 20 亿 */
double f = 10 / 3;
正确例子(1):
/* 全部都用 unsigned int, 注意数字後面的 u, 大写 U 也成 */
unsigned int sum = 2000000000u + 2000000000u;
/* 或是用显式的转型 */
unsigned int sum = (unsigned int)2000000000 + 2000000000;
double f = 10.0 / 3.0;
说明:在目前最普遍的32位元PC作业平台上,整数常数2000000000的型别为
signed int(简写为 int),相加後,其结果仍为 int, 但是 signed int
放不下 4000000000, 造成算术溢位(arithmetic overflow),很可能无法
将正确的值指派给 unsigned int sum,纵使 unsigned int 放得下4000000000
的数值。注意:写成
unsigned int sum = (unsigned int)(2000000000 + 2000000000);
也是不对的。
例子(2):(感谢 sekya 网友提供)
unsigned char a = 0x80;
char b = 0x80; /* implementation-defined result */
if( a == 0x80 ) { /* 恒真 */
printf( "a ok\n" );
if( b == 0x80 ) { /* 不一定恒真 */
printf( "b ok\n" );
}
说明:在将 char 型别定义为范围从 -128 至 +127 的系统上,int 0x80
(其值等於 +128)要转成 char 会放不下,会产生编译器自行定义的值。
这样的程式就不具可移植性了。
九、你不可以在一个运算式(expression)中,对一个基本型态的变数修改其值
超过一次以上。否则,将导致未定义的行为(undefined behavior)。
错误例子:
int i = 7;
int j = ++i + i++;
正确例子:
int i = 7;
int j = ++i;
j += i++;
你也不可以在一个运算式(expression)中,对一个基本型态的变数修改其值,
而且还在同一个式子的其他地方为了其他目的而存取该变数的值。(其他目的,
是指不是为了计算这个变数的新值的目的)。否则,将导致未定义的行为。
错误例子:
int arr[5];
int i = 0;
arr[i] = i++;
正确例子:
int arr[5];
int i = 0;
arr[i] = i;
i++;
[C++程式]
错误例子:
int i = 10;
cout << i << "==" << i++;
正确例子:
int i = 10;
cout << i << "==";
cout << i++;
十、你不可以在macro的定义中,不为它的参数个别加上括号。
错误例子:
#include <stdio.h>
#define SQUARE(x) (x * x)
int main()
{
printf("%d\n", SQUARE(10-5));
return 0;
}
正确例子:
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main()
{
printf("%d\n", SQUARE(10-5));
return 0;
}
说明:如果是用 C++, 请多多利用 inline function 来取代上述的 macro,
以免除 macro 定义的种种危险性。如:
inline int square(int x) { return x * x; }
macro 定义出的「伪函式」至少缺乏下列数项函式本有的能力:
(1) 无法进行参数型别的检查。
(2) 无法递回呼叫。
(3) 无法用 & 加在 macro name 之前,取得函式位址。
(4) 呼叫时往往不能使用具有 side effect 的引数。例如:
错误例子:(感谢 yaca 网友提供)
#define MACRO(x) (((x) * (x)) - ((x) * (x)))
int main()
{
int x = 3;
printf("%d\n", MACRO(++x));
return 0;
}
MACRO(++x) 展开来後变成 (((++x) * (++x)) - ((++x) * (++x)))
违反了第九诫。在 gcc 4.3.3 下的结果是 -24, 在 vc++ 下是 0.
後记:从「古时候」流传下来一篇文章
"The Ten Commandments for C Programmers"(Annotated Edition)
by Henry Spencer
http://www.lysator.liu.se/c/ten-commandments.html
一方面它不是针对 C 的初学者,一方面它特意模仿中古英文
圣经的用语,写得文诌诌。所以我现在另外写了这篇,希望
能涵盖最重要的观念以及初学甚至老手最易犯的错误。
作者:潘科元(Khoguan Phuann) (c)2005. 感谢 ptt.cc BBS 的 C_and_CPP
看板众多网友提供宝贵意见及程式实例。
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 220.130.208.167
※ 编辑: khoguan 来自: 220.130.208.168 (10/16 17:23)
--
●ˍ▁▂▄▃▂▃▂ˍ_▁▂▄▃▃▄▅▄▃▃▂ˍ●
满 █ ╔╦╦╦╮ ╠══╮ ╰╬═╦╮ ╭╝ ╠═╮█ 第
所 足 █ ╰╩╬╩╝ ╯═╦╩╮╰╬ ║║ ╰╦╯ ║█ 八
有 你 █ ═╬═ ╠═╩═╯╰╬ ╯╝ ╭╝ ╰╮║█ 号
愿 █ ╭╦╩╦╮ ║ ═╬══ ╰╦╯ ║█ 当
望 █ ╰╰ ╰╰ ╰═══╯ ═╯╰═ ╰╝╯ ╰╝█ 舖
--
※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.113.128.41
1F:→ dieft:虽然有些是字串变数的例子 但是数值变数也是要注意的 10/20 17:41