作者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 (11/03 01:24)
1F:推 yichengliu:学很多,非常谢谢 02/05 02:04
2F:推 gn00695737:阅~好文! 08/04 20:28
3F:推 aszs:推~~推~~ 08/14 20:12
4F:推 NTUSTKnight:好家伙 10/03 18:33
5F:推 ainigi:这篇超赞的 10/14 12:22
6F:推 locationc:借转 11/18 17:03
7F:推 funfox:这篇超赞.... 借转 ^^ 11/18 22:36
8F:推 withrain:推推推… 12/23 05:37
9F:推 lutaki2002:好的没话说... 12/27 16:49
10F:→ masterenfant:推推推,借转 01/17 22:04
11F:推 fancybubble:借转 02/09 12:07
12F:推 becomesonumb:好教学 03/16 15:07
13F:推 seedamen:good job :) 06/14 01:12
14F:推 s213895:借转 06/26 17:40
15F:推 aneyoko:推推 o.o 07/01 10:07
16F:推 rogerliu0916:这一篇真的是超赞的!! 07/27 13:06
17F:推 mcsrO:推 10/01 12:16
18F:推 lingerkptor:借转 10/04 15:10
19F:推 et282523:推啊! 10/09 16:30
20F:推 a032195:写得好啊 对新手来说很有用 借转一下 10/28 23:06
21F:推 ckcraig:好! 10/29 22:14
22F:推 jim221:借转 11/19 22:21
23F:推 Jyie:我是C++新鲜人,昨天觉得无聊读读看!这十诫我看的不是很懂! 11/25 19:20
24F:→ Jyie:是不是我没有天份阿= = 但真很想学,就请各位老鸟多多照顾! 11/25 19:22
25F:推 chyihuann:多写一点程式,就会遇到较多的问题 就有感觉了 12/01 13:40
26F:推 TTGSP:GOOD!!! 01/10 18:03
27F:推 redgoing:GOOD... 01/25 10:48
28F:推 saicouter:推推,借转 02/19 10:37
29F:推 lingin1204:推~ 04/10 00:58
30F:推 pd0320:借转,谢谢~ 04/10 15:33
31F:推 NNyozi:推~借转~ 感谢~ 140.114.27.122 04/15 08:57
32F:推 snoop750205:好文!!! 140.124.42.58 04/24 22:04
33F:推 franckstone:又学到一课 05/01 01:11
34F:推 chrisdar:using namespace std; 06/14 19:48
35F:→ netsphere:如果是 if (n==1) return 5; 1F胜 XD 07/02 22:50
36F:推 lctwolf:借转!!谢谢!! 08/24 20:47
37F:推 cch0927:借转 11/12 03:37
※ cch0927:转录至某隐形看板 11/12 03:38
38F:推 ricky155030:感谢 11/17 19:59
39F:推 cygnus122:推! 好文 12/20 00:57
40F:推 puph:原来是这个原因 感谢你 明天马上来试试看!! 03/05 21:20
41F:推 sandr1983:这真的写的太好啦.很多时候写程式都会忽略@@ 感谢在三 04/15 15:04
42F:推 GUAlexander:感谢 的确是好文 04/25 22:41
43F:推 sunkill:不推不行 05/17 09:39
44F:推 kw9812:很好的建议。谢谢,好文。 06/25 18:39
45F:推 xatier:借转 07/28 12:43
46F:推 PhySeraph:顶高高 07/29 14:06
48F:推 VictorTom:唉~~问完问题不用砍文吧??以後其他人也许可以参考啊Orz 10/16 13:55
49F:推 grc0621:写得很好, 一些常犯错误都注意了 10/18 17:53
50F:→ POSIX:........................................................ 10/27 21:40
51F:→ POSIX:推 @@" 上面推错 10/27 21:41
52F:推 u9211025:真的学很多!推一下罗~ 11/16 09:56
53F:推 noahleft:借转..推 11/24 01:31
54F:推 erase2004:借转...必推 12/08 22:50
55F:推 rabird:好文 12/23 16:46
56F:推 danielsig727:借转~ 01/09 11:10
57F:推 flash5408:借转~~ 01/29 20:32
58F:推 a60301:借印当教学讲义,谢谢。 02/09 18:59
59F:推 prettyfaces:就算不是初学一不小心还是会犯 02/18 13:41
60F:推 kilink:里面都是我的错误,超推! 03/05 15:41