NCTU-STAT94G 板


LINE

※ [本文转录自 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







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Boy-Girl站内搜寻

TOP