C 语言新手十三诫(The Thirteen Commandments for Newbie C Programmers)
by Khoguan Phuann
请注意:
(1) 本篇旨在提醒新手,避免初学常犯的错误(其实老手也常犯:-Q)。
但不能取代完整的学习,请自己好好研读一两本 C 语言的好书,
并多多实作练习。
(2) 强烈建议新手先看过此文再发问,你的问题极可能此文已经提出并解答了。
(3) 以下所举的错误例子如果在你的电脑上印出和正确例子相同的结果,
那只是不足为恃的一时侥幸。
(4) 不守十三诫者,轻则执行结果的输出数据错误,或是程式当掉,重则
引爆核弹、毁灭地球(如果你的 C 程式是用来控制核弹发射器的话)。
============================================================================
目录: 2
01. 不可以使用尚未给予适当初值的变量 3
02. 不能存取超过阵列既定范围的空间 4
03. 不可以提取不知指向何方的指标 5
04. 不要试图用 char* 去更改一个"字串常数" 8
05. 不能在函式中回传一个指向区域性自动变量的指标 10
06. 不可以只做 malloc(), 而不做相应的 free() 13
07. 在数值运算、赋值或比较中不可以随意混用不同型别的数值 14
08. 在一个运算式中,不能对一个基本型态的变量修改其值超过一次以上 16
09. 在 Macro 定义中, 务必为它的参数个别加上括号 19
10. 不可以在 stack 设置过大的变量 21
11. 使用浮点数精确度造成的误差问题 22
12. 不要猜想二维阵列可以用 pointer to pointer 来传递 23
13. 函式内 new 出来的空间记得要让主程式的指标接住 27
直接输入数字可跳至该页码
01. 你不可以使用尚未给予适当初值的变量
错误例子:
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;
}
02. 你不可以存取超过阵列既定范围的空间
错误例子:
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++ 为了执行效率,并不会自动检查阵列索引值是否超过阵列边界,
我们要自己来确保不会越界。一旦越界,操作的不再是合法的空间,
将导致无法预期的后果。
03. 你不可以提取(dereference)不知指向何方的指标(包含 null 指标)。
错误例子:
char *pc1; /* 未给予初值,不知指向何方 */
char *pc2 = NULL; /* pc2 起始化为 null pointer */
*pc1 = 'a'; /* 将 'a' 写到不知何方,错误 */
*pc2 = 'b'; /* 将 'b' 写到“位址0”,错误 */
正确例子:
char c; /* c 的内容尚未起始化 */
char *pc1 = &c; /* pc1 指向字符变量 c */
*pc1 = 'a'; /* c 的内容变为 'a' */
/* 动态分配 10 个 char(其值未定),并将第一个char的位址赋值给 pc2 */
char *pc2 = (char *) malloc(10);
pc2[0] = 'b'; /* 动态配置来的第 0 个字符,内容变为 'b'
free(pc2);
说明:指标变量必需先指向某个可以合法操作的空间,才能进行操作。
( 使用者记得要检查 malloc 回传是否为 NULL,
碍于篇幅本文假定使用上皆合法,也有正确归还内存 )
错误例子:
char *name; /* name 尚未指向有效的空间 */
printf("Your name, please: ");
fgets(name,20,stdin); /* 您确定要写入的那块空间合法吗??? */
printf("Hello, %s\n", name);
正确例子:
/* 如果编译期就能决定字串的最大空间,那就不要宣告成 char* 改用 char[] */
char name[21]; /* 可读入字串最长 20 个字符,保留一格空间放 '\0' */
printf("Your name, please: ");
fgets(name,20,stdin);
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);
name = NULL;
04. 你不可以试图用 char* 去更改一个"字串常数"
错误例子:
char* pc = "john"; /* pc 现在指著一个字串常数 */
*pc = 'J'; /* 但是 pc 没有权利去更改这个常数! */
正确例子:
char pc[] = "john"; /* pc 现在是个合法的阵列,里面住着字串 john */
/* 也就是 pc[0]='j', pc[1]='o', pc[2]='h',
pc[3]='n', pc[4]='\0' */
*pc = 'J';
pc[2] = 'H';
说明:字串常数的内容是"唯读"的。您有使用权,但是没有更改的权利。
若您希望使用可以更改的字串,那您应该将其放在合法空间
错误例子:
char *s1 = "Hello, ";
char *s2 = "world!";
/* strcat() 不会另行配置空间,只会将资料附加到 s1 所指唯读字串的后面,
造成写入到程式无权碰触的内存空间 */
strcat(s1, s2);
正确例子(2):
/* s1 宣告成阵列,并保留足够空间存放后续要附加的内容 */
char s1[20] = "Hello, ";
char *s2 = "world!";
/* 因为 strcat() 的返回值等于第一个参数值,所以 s3 就不需要了 */
strcat(s1, s2);
05. 你不可以在函式中回传一个指向区域性自动变量的指标。否则,会得到垃圾值
[感谢 gocpp 网友提供程式例子]
错误例子:
char *getstr(char *name)
{
char buf[30] = "hello, "; /*将字串常数"hello, "的内容复制到buf阵列*/
strcat(buf, name);
return buf;
}
说明:区域性自动变量,将会在离开该区域时(本例中就是从getstr函式返回时)
被消灭,因此呼叫端得到的指标所指的字串内容就失效了。
正确例子:
void getstr(char buf[], int buflen, char const *name)
{
char const s[] = "hello, ";
strcpy(buf, s);
strcat(buf, name);
}
正确例子:
int* foo()
{
int* pInteger = (int*) malloc( 10*sizeof(int) );
return pInteger;
}
int main()
{
int* pFromfoo = foo();
}
说明:上例虽然回传了函式中的指标,但由于指标内容所指的位址并非区域变量,
而是用动态的方式抓取而得,换句话说这块空间是长在 heap 而非 stack,
又因 heap 空间并不会自动回收,因此这块空间在离开函式后,依然有效
(但是这个例子可能会因为 programmer 的疏忽,忘记 free 而造成
memory leak)
[针对字串操作,C++提供了更方便安全更直观的 string class, 能用就尽量用]
正确例子:
#include <string> /* 并非 #include <cstring> */
using std::string;
string getstr(string const &name)
{
return string("hello, ") += name;
}
06. 你不可以只做 malloc(), 而不做相应的 free(). 否则会造成内存漏失
但若不是用 malloc() 所得到的内存,则不可以 free()。已经 free()了
所指内存的指标,在它指向另一块有效的动态分配得来的空间之前,不可
以再被 free(),也不可以提取(dereference)这个指标。
[C++] 你不可以只做 new, 而不做相应的 delete
注:new 与 delete 对应,new[] 与 delete[] 对应,不可混用
切记,做了几次 new,就必须做几次 delete
小技巧: 可在 delete 之后将指标指到 NULL,由于 delete 本身会先做检查,
因此可以避免掉多次 delete 的错误
正确例子:
int *ptr = new int(99);
delete ptr;
ptr = NULL;
delete ptr; /* delete 只会处理指向非 NULL 的指标 */
07. 你不可以在数值运算、赋值或比较中随意混用不同型别的数值,而不谨慎考
虑数值型别转换可能带来的“意外惊喜”(错愕)。必须随时注意数值运算
的结果,其范围是否会超出变量的型别
错误例子:
unsigned int sum = 2000000000 + 2000000000; /* 超出 int 存放范围 */
unsigned int sum = (unsigned int) (2000000000 + 2000000000);
double f = 10 / 3;
正确例子:
/* 全部都用 unsigned int, 注意数字后面的 u, 大写 U 也成 */
unsigned int sum = 2000000000u + 2000000000u;
/* 或是用显式的转型 */
unsigned int sum = (unsigned int) 2000000000 + 2000000000;
double f = 10.0 / 3.0;
错误例子:
unsigned int a = 0;
int b[10];
for(int i = 9 ; i >= a ; i