改善C++程序的建议:语法篇1<从C继承而来的特性>

参考自李健的《编写高质量代码--改善C++程序的150个建议》


0 不要让main函数返回void

      操作系统将main作为程序入口,main函数执行程序代码,最后返回程序的退出状态。标准的C/C++中并不支持void main(),VC支持,gcc则不支持;

      但是C++中有一个好坏难定的规定:

      在main函数中,return语句用于离开main函数(析构掉所有的具有动态生存时间的对象),并将其返回值作为参数来调用exit函数。如果函数执行到结尾而没有遇到return语句,其效果等同于执行了return 0;。

      也就是说编译器会协助完成返回值的问题。

      然而,这样的坏处就是,当编译器不支持这个规定的时候,程序就会出错,比如

      int main()

      {

      }

      在VC下编译是成功的,在g++下编译,就会提示错误:‘main’必须返回’int‘



1 区分0的4个面孔

      1 整型0

      2 空指针NULL

      3 字符串结束标记'\0'

'            \0'是一个字符,占8位,其二进制位表示是 00000000,在C/C++中,'\0'作为字符串的结束标记,是唯一的结束标记

      4 逻辑FALSE/false

            false/true是标准C++语言里新增的关键字,而FALSE/TRUE是通过#define定义的宏:

            #ifndef FALSE

            #define FALSE 0

            #endif

            #ifndef TRUE

            #defineTRUE 1

            #endif

            也就是说FALSE/TRUE是int类型,而false/true是bool型,两者是不一样的,bool在C++里是占用1个字节


2 避免那些有运算符引起的混乱


         =和==  :在if语句中 比较两个表达式是否相等,

            if( nValue == 0)

            {  ...  }

        如果程序员出现疲劳或精神不集中,把’==‘写成’=‘,这样if(nValue = 0),那么这个if条件是恒成立,并且nValue被赋值为0,并且不会提示错误

       这恐怕不是程序员想要的。。。

       但如果把常量写在前面,如if(0 == nValue){  },这样如果程序员写成if(0 = nValue){  },编译器会直接的提示错误

       但是对于&和&& 、|和|| 这类运算符,就需要平时养成谨慎的习惯了。



3 对表达式计算顺序不要想当然

      让我们来看一个例子

      if(nGrade & MASK == GRAND_ONE)

      {...}

      我们的本意是,通过nGrade和MASK取与,然后在比较是否等于GRAND_ONE,

      可是,实际上上面的代码的真实效果是if( nGrade & (MASK == GRAND_ONE)){}

      这个建议的核心是,不要吝啬括号,让语义表达的更准确,这样可以减少出错

      如: a = p() + q() * r();

      三个函数p() 、q() 、r()的执行顺序可能是6种组合中的一个,所以a的值是不确定的,在这种情况下,就需要明确一种执行顺序,

             int x=p();

            int y = q();

            a = x+y*r();


            类似的: expr1 ? expr2 : expr3



4 小心宏#define使用中的陷阱

      定义宏时,要使用完备的括号:

      比如定义两个参数相加, #define ADD(a,b) ((a)+(b))是一个安全的方式

      使用宏时,不允许参数发生变化,比如,ADD( a++, --b )

      定义宏时,用大括号将宏所定义的多条表达式括起来,比如 #define INIT { int a=1;int b=2;int c=3; }



5 不要忘记指针变量的初始化

      局部变量的指针未初始化,可能导致程序崩溃

      全局变量的指针,编译器会悄悄完成变量的初始化(0)。


6 明晰逗号分隔表达式的奇怪之处

      逗号表达式是从C继承来的,其中每个表达式都会被执行,不过,整个表达式的值仅是最右边表达式的结果

      如:if(++x,--y,x<20 && y >0),该语句返回的是“ x<20 && y>0”与0比较的结果

      另外,逗号表达式既可以用作左值,也可以用作右值。


7 时刻提防内存溢出

      C语言中的字符串库没有响应的安全保护措施,strcpy、strcat等函数操作时没有检查缓冲区大小,容易引起安全问题

      好的方法是,定义一个函数,并传递确定的长度,如:

      const int DATA_LENGTH = 16;

      void DataCopy(char* src ,int len)

      {

 char dst[DATA_LENGTH];

for(int i=0;src[i] != 0 && i<DATA_LENGTH; i++)

cout<<src[i]<<endl;

if(len < DATA_LENGTH)

strcpy(dst,src);

}

      这里如果,src的长度是10,当i=10时,src[10]就已经越界了。。。



8 拒绝晦涩难懂的函数指针

      比如 void (*p[10])  (void  (*)() );

      1 声明一个无参数、返回空的函数指针的typedef,如:typedef void (*pfv)();

      2 声明一个指向参数为pfv且返回空的函数指针, 如 : typedef  void (*pFun_taking_pfv) (pfv));

      3 定义数组,pFun_taking_pfv p[10];


9 防止头文件重发包含

      方式一:

            #ifndef __TEST_H__

            #define __TEST_H__

            ......

            #endif

      方式二:

            #pragma once

      优缺点:方式一的缺点是宏导致的编译时间长

            方式二的缺点是只是针对物理路径相同的文件,而不是文件的内容


10 优化结构体中元素的布局

   了解结构体中元素的对齐规则,合理地对结构体元素进行布局,可以有效的结余空间,还可以提高元素的存取效率 

   首先看两个成员相同的结构体:

struct A {

int a;

char b;

short c;

};

struct B {

char b;

int a;

short c;

};

在内存中的排列是

struct A   

struct B 

注释:A中前4个字节存放a,第5个字节存放b,最后两个字节存放c,白色的表示系统填充的字节

B中第一个字节存放b,紧接着3个字节为编译器填充字节,第5到8个字节存放a,9到10存放c,最后两个字节为编译器填充。

结构体的对齐规则:

1. 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2. 结构体的每个成员相对于结构体首地址的偏移量都是该成员自身大小的整数倍,比如上面的结构体B中的3个变量的相对于首地址的偏移量都是自身大小的整数倍;

3. 结构体的总大小应为结构体中最宽基本类型大小的整数倍,如果需要,编译器会在最后一个成员后填充字节,如上面的struct B的末尾。


11 将强制转型减到最少

C风格的强制转换语法:

( Type ) exp 或者 Type ( exp )

标准C的强制类型转换可能导致内存的扩张和截断

扩张:把char型的变量强制转换成int

   截断:把int型转换成char型,

特殊的情况:

unsigned int i = 65535;

int j = (int) i;

这样j就变成了-1

在C中,void类型的指针可以与其他类型的指针互相转换

在C++中,不允许不同类型的指针转换,而提供了四个转换方法:

const_cast<T*>(a)

它用于从一个类中去除:const、volatile和__unaligned属性

dynamic_cast<T*>(a)

它将a值转换成类型为T的对象指针,主要用来实现类层次结构的提升,也被称做"安全向下转型",即基类转向派生类是严格和安全的

reiniterpret_cast<T*>(a)

它用于转换不相干的类型,多用于指针类型的转换,类似C的强制类型转换,是不安全的

static_cast<T*>(a)

它多用于基本类型的转换,在转换过程中不进行类型检查,不能确保转换是安全的


12 优先使用前缀操作符

对于整型和长整形,前缀和后缀操作的性能区别可以忽略

对于自定义的类型,前缀操作省去了临时对象的构造,效率上优于后缀


建议13 变量定义的位置和时机

尽可能的推迟变量的定义,直到不得不需要定义时为止;

尽可能的缩小变量的作用域,减少变量名污染,提高程序可读性


14 小心使用typedef

typedef与define的不同:typedef不是简单的字符替换,是对类型的整体封装,就好像类型的别名一样;define只是简单的字符串替换,没有任何的其他操作

typedef在语法上是一个存储类的关键字,类似auto、extern、mutable、static、register;虽然它不会是真正影响对象的存储特性,但:

typedef staticint iInt;这行代码编译器会提示“指定了一个以上的存储类型”

下面是来自http://www.cnblogs.com/kerwinshaw/archive/2009/02/02/1382428.html

陷阱: 
记住,typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。比如: 
先定义: 
typedef   char*   PSTR; 
然后: 
int   mystrcmp(const   PSTR,   const   PSTR); 
const   PSTR
实际上相当于const   char*吗?不是的,它实际上相当于char*   const
 
原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char*   const 
简单来说,记住当consttypedef一起出现时,typedef不会是简单的字符串替换就行。 



15 避免使用可变参数的函数

编译器对可变参数函数的原型检查不够严格,所以容易引起问题,难于排错:

    

尽量使用C++中的函数重载代替可变参数函数



16 慎用goto

 在程序中的一组嵌套循环中,满足某种退出循环的情况时,会用到goto

goto语句破坏了程序的结构性,影响了程序的可读性,因此应尽可能少的使用goto语句


17 提防隐式转换带来的麻烦

在C中void*可以与其他类型的相互转换,C++中void* 与其他类型的转换是单向的,即只允许其他类型转换成void*

尽量控制隐式转换的发生:通常采用的方式包括:

(1) 使用非C/C++关键字的具名函数,用operateor as_T()替换operator_T()(T为C++的数据类型)

(2) 为单参数的构造函数减少explicit 关键字


18 正确区分void与void*

void是无类型的意思,它不是一种数据结构

 void*表示无类型指针,即它可以指向任何类型的数据

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章