当前位置:   article > 正文

编写高质量代码:改善C++程序的150个建议(八)_c++ 150>>8

c++ 150>>8

建议15:尽量不要使用可变参数

  在某些情况下我们希望函数参数的个数可以根据实际需要来确定,所以C语言中就提供了一种长度不确定的参数,形如:“...”,C++语言也继承了这一语言特性。在采用ANSI标准形式时,参数个数可变的函数的原型是:

type funcname(type para1, type para2, ...);

  这种形式至少需要一个普通的形式参数,后面的省略号(...)不能省去,它是函数原型必不可少的一部分。典型的例子有大家熟悉的printf()、scanf()函数,如下所示的就是printf()的原型:

int printf( const char *format ,  ... );

  除了参数format固定以外,其他参数的个数和类型是不确定的。在实际调用时可以有以下形式:

  1. int year = 2011;  
  2. char str[] = "Hello 2011";  
  3. printf("This year is %d", year);  
  4. printf("The greeting words are %s", str);  
  5. printf("This year is %d ,and the greeting words are:%s", year, str);

  也许这些已经为大家所熟知,但是可变参数的实现原理却是C语言中比较难理解的一部分。在标准C语言中定义了一个头文件,专门用来对付可变参数列表,其中,包含了一个va_list的typedef声明和一组宏定义va_start、va_arg、va_end,如下所示:

  1. // File: VC++2010中的stdarg.h  
  2. #include <vadefs.h> 
  3. #define va_start _crt_va_start  
  4. #define va_arg _crt_va_arg  
  5. #define va_end _crt_va_end  
  6. // File: VC++2010中的vadefs.h  
  7. #ifndef _VA_LIST_DEFINED  
  8. typedef char *  va_list;  
  9. #define _VA_LIST_DEFINED  
  10. #endif  
  11. #ifdef  __cplusplus  
  12. #define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )  
  13. #else  
  14. #define _ADDRESSOF(v)   ( &(v) )  
  15. #endif  
  16. #ifdefined(_M_IX86)  
  17. #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  
  18. #define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )  
  19. #define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )  
  20. #define _crt_va_end(ap)      ( ap = (va_list)0 )

  定义_INTSIZEOF(n)是为了使系统内存对齐;va_start(ap, v)使ap指向第一个可变参数在堆栈中的地址,va_arg(ap,t)使ap指向下一个可变参数的堆栈地址,并用*取得该地址的内容;最后变参获取完毕,通过va_end(ap)让ap不再指向堆栈,如图1-3所示。

图1-3 可变参数存储示意图

  由于将va_start、va_arg、va_end定义成了宏,可变参数的类型和个数在该函数中完全由程序代码控制,并不能智能地进行识别,所以导致编译器对可变参数的函数原型检查不够严格,难于查错,不利于写出高质量的代码。

 参数个数可变具有很多的优点,为程序员带来了很多的方便,但是上面C风格的可变参数却存在着如下的缺点:

  (1)缺乏类型检查,类型安全性无从谈起。“省略号的本质是告诉编译器‘关闭所有检查,从此由我接管,启动reinterpret_cast’”,强制将某个类型对象的内存表示重新解释成另外一种对象类型,这是违反“类型安全性”的,是大忌。

  例如,自定义的打印函数。

  1. void UserDefinedPrintFun(char* format, int i, ...)  
  2. {  
  3.      va_list arg_ptr;  
  4.      char *s = NULL;  
  5.      int *i = NULL;  
  6.      float *f = NULL;  
  7.  
  8.      va_start(arg_ptr, i);  
  9.      while(*format!='\0')  
  10.      {  
  11.          format++;  
  12.          if(*(format-1)=='%' && *format=='s')  
  13.         {  
  14.           s = va_arg(arg_ptr, char*);  
  15.             …… // 输出至屏幕  
  16.         }  
  17.         else if(*(format-1)=='%' && *format=='d')  
  18.         {  
  19.           i = va_arg(arg_ptr, int*);  
  20.             …… // 输出至屏幕  
  21.         }  
  22.         else if(*(format-1)=='%' && *format=='f')  
  23.         {  
  24.           f = va_arg(arg_ptr, float*);  
  25.             …… // 输出至屏幕  
  26.         }  
  27.     }  
  28.  
  29.     va_end(arg_ptr);  
  30.     return;  
  31. }

  如果采用下面三种方法调用,合法合理:

  1. UserDefinedPrintFun ("%d", 2010);  // 结果2010  
  2. UserDefinedPrintFun ("%d%d", 2010,2011);  // 结果20102011  
  3. UserDefinedPrintFun ("%s%d", "Hello", 2012); // 结果Hello2012

  但是,当给定的格式字符串与参数类型不对应时,强制转型这个“怪兽”就会被唤醒,悄悄地毁坏程序的安全性,这可不是什么高质量的程序,如下所示:

  1. UserDefinedPrintFun ("%d", 2010.80f);  
  2. // 结果2010  
  3. UserDefinedPrintFun ("%d%d", "Hello", 2012);  
  4. // 结果150958722015(这是什么结果???)

  (2)因为禁用了语言类型检查功能,所以在调用时必须通过其他方式告诉函数所传递参数的类型,以及参数个数,就像很多人熟知的printf()函数中的格式字符串char* format。这种方式需要手动协调,既易出错,又不安全,上面的代码片段已经充分说明了这一点。

  (3)不支持自定义数据类型。自定义数据类型在C++中占有较重的地位,但是长参数只能传递基本的内置类型。还是以printf()为例,如果要打印出一个Student类型对象的内容,对于这样的自定义类型,该用什么格式的字符串去传递参数类型呢?如下所示:

  1. class Student  
  2. {  
  3. public:  
  4.     Student();  
  5.     ~ Student();  
  6. private:  
  7.   string m_name;  
  8.     char   m_age;  
  9.     int    m_scoer;  
  10. };  
  11.  
  12. Student XiaoLi;  
  13. printf(format, XiaoLi);  // format应该是什么呢

上述缺点足以让我们有了拒绝使用C风格可变参数的念头,何况C++的多态性已经为我们提供了实现可变参数的安全可靠的有效途径呢!如下所示:

  1. class PrintFunction  
  2. {  
  3. public:  
  4.     void UserDefinedPrintFun(int i);  
  5.     void UserDefinedPrintFun(float f);  
  6.     void UserDefinedPrintFun(int i, char* s);  
  7.     void UserDefinedPrintFun(float f, char* s);  
  8. private:  
  9.      ……  
  10. };

  虽然上述设计不能像printf()函数那样灵活地满足各种各样的需求,但是可以根据需求适度扩充函数定义,这样不仅能满足需求,其安全性也是毋庸置疑的。舍安全而求危险,这可不是明白人所为。如果还对printf()的灵活性念念不忘,我告诉大家,有些C++库已经使用C++高级特性将类型安全、速度与使用方便很好地结合在一起了,比如Boost中的format库,大家可以尝试使用。

  请记住:

  编译器对可变参数函数的原型检查不够严格,所以容易引起问题,难于查错,不利于写出高质量的代码。所以应当尽量避免使用C语言方式的可变参数设计,而用C++中更为安全的方式来完美代替之。

  建议16:慎用goto

  如果说有一个关键字在C/C++语言程序中备受争议,那么非程序跳转关键字goto莫属。在早期的BASIC和FORTRAN语言中,goto备受依赖,为了照顾部分程序员的设计习惯,在C语言中goto关键字依然得到了保留。然而,与前面两种语言有所不同,goto在C/C++语言中就像一个多余的外来户,有没有它几乎不影响C语言程序的设计与运行,它没有带来太大的正面作用,相反却容易破坏程序的结构性。所以,Kernighan和Ritchie认为goto语句“非常容易被滥用”,并且建议“一定要谨慎使用,或者干脆不用”。

  之所以建议避免使用goto,是因为C/C++语言中提供了更好的方式去实现goto的功能。为了帮助部分程序员克服goto依赖症,接下来我会分别介绍以下情形下goto语句的替换方式:

  if控制内的多条语句

  如果熟悉旧风格的BASIC和FORTRAN,会知道只有紧跟在if条件后的那一条语句才属于该if的控制域,所以就出现了以下goto的使用形式:

  1. Size = 20;  
  2. Flag = 1;  
  3. if( price > 15)  
  4.    goto A_PLAN;  
  5. goto B_PLAN;  
  6. A_PLAN:  
  7.    Size /=2;  
  8.    Flag = 3;  
  9. B_PLAN:  
  10.    Money = price * Size * Flag;

  而在C/C++语言中,复合语句或代码块很容易实现上述目的,而且使代码更加清晰可读,如下所示:

  1. Size = 20;  
  2. Flag = 1;  
  3. if( price > 15)  
  4. {  
  5.   Size /=2;  
  6.   Flag = 3;  
  7. }  
  8. Money = price * Size * Flag;

不确定循环

  首先请看如下代码:

  1. ReadScore:  
  2. scanf("%d", &Score);  
  3. if(Score < 0 )  
  4.      goto ErrorStage;  
  5. ... // Processing codes  
  6. goto ReadScore;  
  7. ErrorStage:  
  8. ... // Error Processing

  这种情形可以用我们熟知的while循环来完美代替:

  1. scanf("%d", &Score);  
  2. while(Score >= 0)  
  3. {  
  4.      ... // Processing codes  
  5.      scanf("%d", &Score);  
  6. }

  此外,如果跳转到循环结尾会开始新一轮循环,可以使用continue代替;如果要跳出循环,那就使用break。

  上述goto语句破坏了程序的结构性,影响了程序的可读性,这在C/C++程序员看来是难以容忍的。然而,有一种goto的使用情形为许多C/C++程序员所接受,那就是程序在一组嵌套循环中出现错误,无路可走时的跳转处理,如下所示:

  1. while(... )  
  2. {  
  3.      for(...)  
  4.      {  
  5.         for(...)  
  6.         {  
  7.             Processing statement;  
  8.             if(error)  
  9.                 goto ERROR;  
  10.         }  
  11.         More processing statement;  
  12.     }  
  13.     Yet more processing statement;  
  14. }  
  15. And more processing statement;  
  16. ERROR:  
  17.      Deal_With_Error Statement;

  请记住:

  过度使用goto会使代码流程错综复杂,难以理清头绪。所以,如果不熟悉goto,不要使用它;如果已经习惯使用它,试着不去使用。


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/512202
推荐阅读
相关标签
  

闽ICP备14008679号