C语言函数

来源:互联网 发布:淘宝分销平台下载 编辑:程序博客网 时间:2024/05/23 07:26

C语言函数递归

一、一个简单但易出错的递归例子

几乎每一本C 语言基础的书都讲到了函数递归的问题,但是初学者仍然容易在这个地方犯错误。先看看下面的例子:
void fun(int i)
{
   if (i>0)
   {
      fun(i/2);
   }
   printf("%d\n",i);
}
intmain()
{
   fun(10);
   return 0;
}
问:输出结果是什么?

这是我上课时,一个学生问我的问题。他不明白为什么输出的结果会是这样:
0
1
2
5
10
他认为应该输出0。因为当i 小于或等于0 时递归调用结束,然后执行printf 函数打印i 的值。

这就是典型的没明白什么是递归。其实很简单,printf("%d\n",i);语句是fun 函数的一部分,肯定执行一次fun 函数,就要打印一行。怎么可能只打印一次呢?关键就是不明白怎么展开递归函数。展开过程如下:
void fun(int i)
{
   if (i>0)
   {
      //fun(i/2);
      if(i/2>0)
      {
         if(i/4>0)
         {
            …
         }
         printf("%d\n",i/4);
      }
      printf("%d\n",i/2);
   }
   printf("%d\n",i);
}
这样一展开,是不是清晰多了?其实递归本身并没有什么难处,关键是其展开过程别弄错了。

二、不使用任何变量编写strlen 函数

看到这里,也许有人会说,strlen 函数这么简单,有什么好讨论的。是的,我相信你能熟练应用这个函数,也相信你能轻易的写出这个函数。但是如果我把要求提高一些呢:

不允许调用库函数,也不允许使用任何全局或局部变量编写intmy_strlen (char *strDest);似乎问题就没有那么简单了吧?这个问题曾经在网络上讨论的比较热烈,我几乎是全程“观战”,差点也忍不住手痒了。不过因为我的解决办法在我看到帖子时已经有人提出了,所以作罢。

解决这个问题的办法由好几种,比如嵌套有编语言。因为嵌套汇编一般只在嵌入式底层开发中用到,所以本书就不打算讨论C 语言嵌套汇编的知识了。有兴趣的读者,可以查找相关资料。

也许有的读者想到了用递归函数来解决这个问题。是的,你应该想得到,因为我把这个问题放在讲解函数递归的时候讨论。既然已经有了思路,这个问题就很简单了。代码如下:
intmy_strlen( const char* strDest )
{
   assert(NULL != strDest);
   if ('\0' == *strDest)
   {
      return 0;
   }
   else
   {
      return (1 + my_strlen(++strDest));
   }
}
第一步:用assert 宏做入口校验。
第二步:确定参数传递过来的地址上的内存存储的是否为'\0'。如果是,表明这是一个空字符串,或者是字符串的结束标志。
第三步:如果参数传递过来的地址上的内存不为'\0',则说明这个地址上的内存上存储的是一个字符。既然这个地址上存储了一个字符,那就计数为1,然后将地址加1 个char类型元素的大小,然后再调用函数本身。如此循环,当地址加到字符串的结束标志符'\0'时,递归停止。


当然,同样是利用递归,还有人写出了更加简洁的代码:
intmy_strlen( const char* strDest )
{
   return *strDest?1+strlen(strDest+1):0;
}
这里很巧妙的利用了问号表达式,但是没有做参数入口校验,同时用*strDest 来代替('\0'== *strDest)也不是很好。所以,这种写法虽然很简洁,但不符合我们前面所讲的编码规范。可以改写一下:
intmy_strlen( const char* strDest )
{
   assert(NULL != strDest);
   return ('\0' != *strDest)?(1+my_strlen(strDest+1)):0;
}

上面的问题利用函数递归的特性就轻易的搞定了,也就是说每调用一遍my_strlen 函数,其实只判断了一个字节上的内容。但是,如果传入的字符串很长的话,就需要连续多次函数调用,而函数调用的开销比循环来说要大得多,所以,递归的效率很低,递归的深度太大甚至可能出现错误(比如栈溢出)。所以,平时写代码,不到万不得已,尽量不要用递归。即便是要用递归,也要注意递归的层次不要太深,防止出现栈溢出的错误;同时递归的停止条件一定要正确,否则,递归可能没完没了。


C语言函数设计的一般原则和技巧

1、原则上尽量少使用全局变量,因为全局变量的生命周期太长,容易出错,也会长时间占用空间.各个源文件负责本身文件的全局变量,同时提供一对对外函数,方便其它函数使用该函数来访问变量。
比如:niSet_ValueName(…);niGet_ValueName(…);不要直接读写全局变量,尤其是在多线程编程时,必须使用这种方式,并且对读写操作加锁。

2、参数命名要恰当,顺序要合理。
例如编写字符串拷贝函数str_copy,它有两个参数。如果把参数名字起为str1 和str2,例如
   void str_copy (char *str1, char *str2);
那么我们很难搞清楚究竟是把str1 拷贝到str2 中,还是刚好倒过来。

可以把参数名字起得更有意义,如叫strSource 和strDestination。这样从名字上就可以看出应该把strSource 拷贝到strDestination。

还有一个问题,这两个参数那一个该在前那一个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。如果将函数声明为:
   void str_copy (char *strSource, char *strDestination);
别人在使用时可能会不假思索地写成如下形式:
   char str[20];
   str_copy (str, “Hello World”); 参数顺序颠倒

3、如果参数是指针,且仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被意外修改。例如:
   void str_copy (char *strDestination,const char *strSource);

4、不要省略返回值的类型,如果函数没有返回值,那么应声明为void 类型。
如果没有返回值,编译器则默认为函数的返回值是int类型的。

5、在函数体的“入口处”,对参数的有效性进行检查。尤其是指针参数,尽量使用assert宏做入口校验,而不使用if语句校验。(关于此问题讨论,详见指针与数组那章。)

6、return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。例如:
char * Func(void)
{
   char str[30];
   …
   return str;
}
str 属于局部变量,位于栈内存中,在Func 结束的时候被释放,所以返回str 将导致错误。

7、函数的功能要单一,不要设计多用途的函数。微软的Win32 API就是违反本规则的典型,其函数往往因为参数不一样而功能不一,导致很多初学者迷惑。

8、函数体的规模要小,尽量控制在80 行代码之内。

9、相同的输入应当产生相同的输出。尽量避免函数带有“记忆”功能。

带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态“。这样的函数既不易理解又不利于测试和维护。在C 语言中,函数的static局部变量是函数的“记忆”存储器。建议尽量少用static 局部变量,除非必需。

10、避免函数有太多的参数,参数个数尽量控制在4个或4个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。微软的Win32 API就是违反本规则的典型,其函数的参数往往七八个甚至十余个。比如一个CreateWindow函数的参数就达11个之多。

11、尽量不要使用类型和数目不确定的参数。
C 标准库函数printf 是采用不确定参数的典型代表,其原型为:
   int printf(const chat *format[, argument]…);
这种风格的函数在编译时丧失了严格的类型安全检查。

12、有时候函数不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。例如字符串拷贝函数strcpy 的原型:
   char *strcpy(char *strDest,const char *strSrc);
strcpy 函数将strSrc 拷贝至输出参数strDest 中,同时函数的返回值又是strDest。这样做并非多此一举,可以获得如下灵活性:
   char str[20];
   int length = strlen(strcpy(str, “Hello World”) );

13、不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。

14、函数名与返回值类型在语义上不可冲突。
违反这条规则的典型代表就是C语言标准库函数getchar。几乎没有一部名著没有提到getchar函数,因为它实在太经典,太容易让人犯错误了。所以,每一个有经验的作者都会拿这个例子来警示他的读者,我这里也是如此:
char c;
c = getchar();
if(EOF == c)
{
   …
}
按照getchar 名字的意思,应该将变量c 定义为char 类型。但是很不幸,getchar 函数的返回值却是int 类型,其原型为:
   int getchar(void);
由于c 是char 类型的,取值范围是[-128,127],如果宏EOF 的值在char 的取值范围之外,EOF 的值将无法全部保存到c 内,会发生截断,将EOF 值的低8 位保存到c 里。这样if 语句有可能总是失败。这种潜在的危险,如果不是犯过一次错,肯怕很难发现。


C语言编码风格

很多人不重视这点,认为无所谓,甚至国内的绝大多数教材也不讨论这个话题,导致学生入公司后仍要进行编码风格的教育。我接触过很多学生,发现他们由于平时缺乏这种意识,养成了不好的习惯,导致很难改正过来。代码没有注释,变量、函数等命名混乱,过两天自己都看不懂自己的代码。下面是一些我见过的比较好的做法,希望读者能有所收获。

1、每一个函数都必须有注释,即使函数短到可能只有几行。头部说明需要包含包含的内容和次序如下:
/************************************************************************
* Function Name : nucFindThread
* Create Date : 2000/01/07
* Author/Corporation : your name/your company name
**
Description : Find a proper thread in thread array.
* If it’s a new then search an empty.
*
* Param : ThreadNo: someParam description
* ThreadStatus: someParam description
**
Return Code : Return Code description,eg:
ERROR_Fail: not find a thread
ERROR_SUCCEED: found
*
* Global Variable : DISP_wuiSegmentAppID
* File Static Variable : naucThreadNo
* Function Static Variable : None
*
*------------------------------------------------------------------------
* Revision History
* No. Date Revised by Item Description
* V0.5 2008/01/07 your name … …
************************************************************************/
static unsigned char nucFindThread(unsigned char ThreadNo,unsigned char ThreadStatus)
{

   }

2、每个函数定义结束之后以及每个文件结束之后都要加一个或若干个空行。例如:
/************************************************************************
* ………
* Function1 Description
* ………
************************************************************************/
void Function1(……)
{
   …
}
//Blank Line
/************************************************************************
* ………
* Function2 Description
* ………
************************************************************************/
void Function2(……)
{
   …
}
//Blank Line
/************************************************************************
* ………
* Function3 Description
* ………
************************************************************************/
void Function3(……)
{
   …
}
//Blank Line

3、在一个函数体内,变量定义与函数语句之间要加空行。例如:
/************************************************************************
* ………
* Function Description
*………
************************************************************************/
void Function1()
{
   int n;
   //Blank Line
   statement1
   …….
}

4、逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。例如:
//Blank Line
while (condition)
{
   statement1;
   //Blank Line
   if (condition)
   {
      statement2;
   }
   else
   {
      statement3;
   }
   //Blank Line
   statement4
}

5、复杂的函数中,在分支语句,循环语句结束之后需要适当的注释,方便区分各分支或循环体。
while (condition)
{
   statement1;
   if (condition)
   {
      for(condition)
      {
         Statement2;
      }//end “for(condition)”
   }
   else
   {
      statement3;
   }//”end if (condition)”
   statement4
}//end “while (condition)”

6、修改别人代码的时候不要轻易删除别人的代码,应该用适当的注释方式。例如:
while (condition)
{
   statement1;
   //////////////////////////////////////
   //your name , 2008/01/07 delete
   //if (condition)
   //{
   // for(condition)
   // {
   // Statement2;
   // }
   //}
   //else
   //{
   // statement3;
   //}
   ////////////////////////////////////////
   ///////////////////////////////////////
   // your name , 2000/01/07 add
   …
   new code
   …
   ///////////////////////////////////////
   statement4
}

7、用缩行显示程序结构,使排版整齐,缩进量统一使用4个字符(不使用TAB缩进)。
每个编辑器的TAB键定义的空格数不一致,可能导致在别的编辑器打开你的代码乱成一团糟。

8、在函数体的开始、结构/联合的定义、枚举的定义以及循环、判断等语句中的代码都要采用缩行。

9、同层次的代码在同层次的缩进层上。
例如:

10、代码行最大长度宜控制在80 个字符以内,较长的语句、表达式等要分成多行书写。

11、长表达式要在低优先级操作符处划分新行,操作符放在新行之首(以便突出操作符)。
拆分出的新行要进行适当的缩进,使排版整齐,语句可读。例如:
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
{
   dosomething();
}
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
{
   dosomething();
}

12、如果函数中的参数较长,则要进行适当的划分。例如:
void function(float very_longer_var1,
float very_longer_var2,
float very_longer_var3)

13、用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。例如:
int aiMinValue;
int aiMaxValue;
int niSet_Value(…);
int niGet_Value(…);

14、如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。例如:
   leap_year = ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);

15、不要编写太复杂的复合表达式。例如:
   i = a >= b&&c < d && c + f <= g + h; 复合表达式过于复杂

16、不要有多用途的复合表达式。例如:
   d = (a = b + c) + r;
该表达式既求a 值又求d 值。应该拆分为两个独立的语句:
   a = b + c;
   d = a + r;

17、尽量避免含有否定运算的条件表达式。例如:
   if (!(num >= 10))
应改为:
   if (num < 10)

18、参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void 填充。例如:


C语言函数的由来与好处

其实在汇编语言阶段,函数这个概念还是比较模糊的。汇编语言的代码往往就是从入口开始一条一条执行,直到遇到跳转指令(比如ARM 指令B、BL、BX、BLX 之类)然后才跳转到目的指令处执行。这个时候所有的代码仅仅是按其将要执行的顺序排列而已。后来人们发现这样写代码非常费劲,容易出错,也不方便。于是想出一个办法,把一些功能相对来说能成为一个整体的代码放到一起打包,通过一些数据接口和外界通信。这就是函数的由来。

那函数能给我们带来什么好处呢?简单来说可以概括成以下几点:
1、降低复杂性:使用函数的最首要原因是为了降低程序的复杂性,可以使用函数来隐含信息,从而使你不必再考虑这些信息。

2、避免重复代码段:如果在两个不同函数中的代码很相似,这往往意味着分解工作有误。这时,应该把两个函数中重复的代码都取出来,把公共代码放入一个新的通用函数中,然后再让这两个函数调用新的通用函数。通过使公共代码只出现一次,可以节约许多空间。

因为只要在一个地方改动代码就可以了。这时代码也更可靠了。

3、限制改动带来的影响:由于在独立区域进行改动,因此,由此带来的影响也只限于一个或最多几个区域中。

4、隐含顺序:如果程序通常先从用户那里读取数据,然后再从一个文件中读取辅助数据,在设计系统时编写一个函数,隐含哪一个首先执行的信息。

5、改进性能:把代码段放入函数也使得用更快的算法或执行更快的语言(如汇编)来改进这段代码的工作变得容易些。

6、进行集中控制:专门化的函数去读取和改变内部数据内容,也是一种集中的控制形式。

7、隐含数据结构:可以把数据结构的实现细节隐含起来。

8、隐含指针操作:指针操作可读性很差,而且很容易引发错误。通过把它们独立在函数中,可以把注意力集中到操作意图而不是集中到的指针操作本身。

9、隐含全局变量:参数传递。
C 语言中,函数其实就是一些语句的的集合,而语句又是由关键字和符号等元素组成,如果我们把关键字、符号等基本元素弄明白了,函数不就没有问题了么?我看未必。真正要编写出高质量的函数来,是非常不容易的。

原创粉丝点击