友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
[免费下载 c语言深度解剖[1]-第22部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
{
dosomething();
}
【规则6…12】如果函数中的参数较长,则要进行适当的划分。
例如:
voidfunction(float
very_longer_var1;
float
very_longer_var2;
float
very_longer_var3)
【规则
6…13】用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
例如:
int
aiMinValue;
intaiMaxValue;
intniSet_Value(。);
intniGet_Value(。);
【规则6…14】如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用
默认的优先级。
例如:
leap_year
=((year
%
4
0)
&&(year
%
100!=0))
||
(year%
400
0);
【规则6…15】不要编写太复杂的复合表达式。
例如:
i=
a》=b
&&
c《
d&&c
+f
=
10))
应改为:
if
(num
《
10)
【规则6…18】参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果
函数没有参数,则用void填充。
例如:
提倡的风格不提倡的风格
void
set_value(int
width;
int
height);
float
get_value(void);
void
set_value
(int;
int);
float
get_value
();
6。2;函数设计的一般原则和技巧
【规则6…19】原则上尽量少使用全局变量,因为全局变量的生命周期太长,容易出错,
也会长时间占用空间。各个源文件负责本身文件的全局变量;同时提供一对对外函数;方
便其它函数使用该函数来访问变量。比如:
niSet_ValueName(。);niGet_ValueName(。);
不要直接读写全局变量,尤其是在多线程编程时,必须使用这种方式,并且对读写操作
加锁。
【规则6…20】参数命名要恰当,顺序要合理。
例如编写字符串拷贝函数str_copy,它有两个参数。如果把参数名字起为
str1和str2,例
如
voidstr_copy
(char
*str1;char
*str2);
那么我们很难搞清楚究竟是把str1拷贝到str2中,还是刚好倒过来。
可以把参数名字起得更有意义,如叫
strSource和strDestination。这样从名字上就可
以看出应该把strSource拷贝到strDestination。
还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员
的习惯。一般地,应将目的参数放在前面,源参数放在后面。
如果将函数声明为:
void
str_copy
(char
*strSource;char
*strDestination);
别人在使用时可能会不假思索地写成如下形式:
charstr'20';
str_copy
(str;
“HelloWorld”);参数顺序颠倒
【规则6…21】如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该
指针在函数体内被意外修改。
例如:
voidstr_copy(char
*strDestination,const
char
*strSource);
【规则6…22】不要省略返回值的类型,如果函数没有返回值,那么应声明为
void类型。
如果没有返回值,编译器则默认为函数的返回值是int类型的。
【规则6…23】在函数体的“入口处”,对参数的有效性进行检查。尤其是指针参数,尽
量使用assert宏做入口校验,而不使用
if语句校验。(关于此问题讨论,详见指针与数组那章。)
【规则6…24】return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结
束时被自动销毁。例如:
char
*
Func(void)
{
charstr'30';
…
return
str;
}
str属于局部变量,位于栈内存中,在
Func结束的时候被释放,所以返回str将导致错误。
【规则6…25】函数的功能要单一,不要设计多用途的函数。微软的Win32API就是违反
本规则的典型,其函数往往因为参数不一样而功能不一,导致很多初学者迷惑。
【规则6…26】函数体的规模要小,尽量控制在80行代码之内。
【建议6…27】相同的输入应当产生相同的输出。尽量避免函数带有“记忆”功能。
带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种
“记忆状态“。这样的函数既不易理解又不利于测试和维护。在
C语言中,函数的
static
局部变量是函数的“记忆”存储器。建议尽量少用static局部变量,除非必需。
【建议6…28】避免函数有太多的参数,参数个数尽量控制在
4个或4个以内。如果参数太
多,在使用时容易将参数类型或顺序搞错。微软的Win32API就是违反本规则的典型,
其函数的参数往往七八个甚至十余个。比如一个CreateWindow函数的参数就达11个之
多。
【建议6…29】尽量不要使用类型和数目不确定的参数。
C标准库函数printf是采用不确定参数的典型代表,其原型为:
int
printf(const
chat
*format';
argument'。);
这种风格的函数在编译时丧失了严格的类型安全检查。
【建议6…30】有时候函数不需要返回值,但为了增加灵活性如支持链式表达,可以附加
返回值。例如字符串拷贝函数strcpy的原型:
char
*strcpy(char*strDest,const
char
*strSrc);
strcpy函数将strSrc拷贝至输出参数strDest中,同时函数的返回值又是
strDest。这样做
并非多此一举,可以获得如下灵活性:
charstr'20';
int
length
=
strlen(strcpy(str;
“Hello
World”)
);
【建议6…31】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变
量的有效性,例如全局变量、文件句柄等。
【规则6…32】函数名与返回值类型在语义上不可冲突。
违反这条规则的典型代表就是C语言标准库函数getchar。几乎没有一部名著没有提到
getchar函数,因为它实在太经典,太容易让人犯错误了。所以,每一个有经验的作者都
会拿这个例子来警示他的读者,我这里也是如此:
charc;
c=
getchar();
if(EOF
c)
{
…
}
按照
getchar名字的意思,应该将变量
c定义为
char类型。但是很不幸,
getchar函数的
返回值却是
int类型,其原型为:
intgetchar(void);
由于
c是
char类型的,取值范围是
'…128;127';如果宏
EOF的值在
char的取值范围之外,
EOF的值将无法全部保存到
c内,会发生截断,将
EOF值的低
8位保存到
c里。这样
if语
句有可能总是失败。这种潜在的危险,如果不是犯过一次错,肯怕很难发现。
6。4,函数递归
6。4。1,一个简单但易出错的递归例子
几乎每一本
C语言基础的书都讲到了函数递归的问题,但是初学者仍然容易在这个地
方犯错误。先看看下面的例子:
voidfun(inti)
{
if
(i》0)
{
fun(i/2);
}
printf(〃%dn〃;i);
}
intmain()
{
fun(10);
return0;
}
问:输出结果是什么?
这是我上课时,一个学生问我的问题。他不明白为什么输出的结果会是这样:
0
1
2
5
10
他认为应该输出
0。因为当
i小于或等于
0时递归调用结束,然后执行
printf函数打印
i的值。
这就是典型的没明白什么是递归。其实很简单,printf(〃%dn〃;i);语句是
fun函数的一部
分,肯定执行一次
fun函数,就要打印一行。怎么可能只打印一次呢?关键就是不明白怎么
展开递归函数。展开过程如下:
voidfun(inti)
{
if
(i》0)
{
//fun(i/2);
if(i/2》0)
{
if(i/4》0)
{
…
}
printf(〃%dn〃;i/4);
}
printf(〃%dn〃;i/2);
}
printf(〃%dn〃;i);
}
这样一展开,是不是清晰多了?其实递归本身并没有什么难处,关键是其展开过程别弄错了。
6。4。2,不使用任何变量编写
strlen函数
看到这里,也许有人会说,strlen函数这么简单,有什么好讨论的。是的,我相信你能
熟练应用这个函数,也相信你能轻易的写出这个函数。但是如果我把要求提高一些呢:
不允许调用库函数,也不允许使用任何全局或局部变量编写
intmy_strlen(char*strDest);
似乎问题就没有那么简单了吧?这个问题曾经在网络上讨论的比较热烈,我几乎是全
程“观战”,差点也忍不住手痒了。不过因为我的解决办法在我看到帖子时已经有人提出了,
所以作罢。
解决这个问题的办法由好几种,比如嵌套有编语言。因为嵌套汇编一般只在嵌入式底
层开发中用到,所以本书就不打算讨论
C语言嵌套汇编的知识了。有兴趣的读者,可以查
找相关资料。
也许有的读者想到了用递归函数来解决这个问题。是的,你应该想得到,因为我把这
个问题放在讲解函数递归的时候讨论。既然已经有了思路,这个问题就很简单了。代码如下:
intmy_strlen(constchar*strDest)
{
assert(NULL!=
strDest);
if
('0'
*strDest)
{
return0;
}
else
{
return(1+my_strlen(++strDest));
}
}
第一步:用
assert宏做入口校验。
第二步:确定参数传递过来的地址上的内存存储的是否为
'0'。如果是,表明这是一个
空字符串,或者是字符串的结束标志。
第三步:如果参数传递过来的地址上的内存不为
'0',则说明这个地址上的内存上存储
的是一个字符。既然这个地址上存储了一个字符,那就计数为
1,然后将地址加
1个
char
类型元素的大小,然后再调用函数本身。如此循环,当地址加到字符串的结束标志符
'0'时,
递归停止。
当然,同样是利用递归,还有人写出了更加简洁的代码:
intmy_strlen(constchar*strDest)
{
return*strDest?1+strlen(strDest+1):0;
}
这里很巧妙的利用了问号表达式,但是没有做参数入口校验,同时用
*strDest来代替('0'
*strDest)也不是很好。所以,这种写法虽然很简洁,但不符合我们前面所讲的编码规范。
可以改写一下:
intmy_strlen(constchar*strDest)
{
assert(NULL!=
strDest);
return('0'
!=
*strDest)?(1+my_strlen(strDest+1)):0;
}
上面的问题利用函数递归的特性就轻易的搞定了,也就是说每调用一遍
my_strlen函数,
其实只判断了一个字节上的内容。但是,如果传入的字符串很长的话,就需要连续多次函数
调用,而函数调用的开销比循环来说要大得多,所以,递归的效率很低,递归的深度太大甚
至可能出现错误(比如栈溢出)。所以,平时写代码,不到万不得已,尽量不要用递归。即
便是要用递归,也要注意递归的层次不要太深,防止出现栈溢出的错误;同时递归的停止条
件一定要正确,否则,递归可能没完没了。
第七章文件结构
一个工程是往往由多个文件组成。这些文件怎么管理、怎么命名都是非常重要的。下面
给出一些基本的方法,比较好的管理这些文件,避免错误的发生。
7。1,文件内容的一般规则
【规则
7…1】每个头文件和源文件的头部必须包含文件头部说明和修改记录。
源文件和头文件的头部说明必须包含的内容和次序如下:
/************************************************************************
*
FileName
:
FN_FileName。c/FN_FileName。h
*
Copyright
:
2003…2008XXXXCorporation;AllRightsReserved。
*
ModuleName
:
DrawEngine/Display
*
*
CPU
:
ARM7
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!