首页 > 代码库 > 编写高质量代码,改善C++程序的150个建议:指针、初始化和运算符

编写高质量代码,改善C++程序的150个建议:指针、初始化和运算符

建议0:不要让main函数返回void

首先C++ 标准中从没有出现过void main(){}这样的函数定义。
标准的主函数定义有两种:
int main()
int main(int argc,char * argv[])
在main函数中,return 语句的作用在于离开main函数(析构掉所有具有动态生存时间的对象),并将其返回值作为参数来调用exit函数。如果函数执行到结尾儿没有遇到return 语句,其效果就等于执行了return 0。

建议1:区分0 的四种面孔

1)整形0。作为一个int整形,占据32位的空间。二进制表示为:00000000 0000000 0000000 00000000
2)空指针。指针与整形占据的空间是一样的。0在指针的可以替换为NULL,现在使用nullptr。
int *pValue = http://www.mamicode.com/0;//合法>3)字符串结束标志‘\0‘
 这里作为一个字符,占8位。二进制表示:00000000
char sHello[12] = {"Hello c/c++"};
if(sHello[11] == '\0')//比较作为结束符使用
4)逻辑FALSE/false
上面是有区别的,false/true是c++语言新增的关键字。FALSE/TRUE是通过#define定义的宏。
#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif

换言之,FALSE/TRUE是int类型,false/true是bool类型,两者是不一样的。

建议2:避免由运算符引发的混乱

比较容易出错的是=和==,一个是赋值,一个是判断是否相等。
一个比较容易出错的地方就是
if(nValue = http://www.mamicode.com/0)

这样条件下的语句永远不会执行,这里可以写成:
if(0 == nValue)

如果==写成=,那么编译器会直接给出错误,因为0不允许作为左值来使用。还有,&和&&,|和||之间的差别。用细心和良好的代码习惯避免由于运算符混乱带来的麻烦。

建议3:对表达式的计算不要想当然

下面的代码:
if(nGrade & MASK == GRADE_ONE)
  ...//prossing codes

本意是grades等于GRADE_ONE,可是因为优先级的关系,后者判等运算将首先被计算。这样就不是我们期望的了,所以,要对前两项添加括号,明确表示我们的意图。
接下来是更重要的:函数参数也好,某个操作符的操作数也好,表达式求值次序是不一定的,每个特定机器,操作系统和编译器都不同。求值顺序主要包括两个方面:函数参数的评估求职顺序和操作数的评估求值顺序。
int i = 2010;
cout<<i<<i = i +1<<endl;

对两者的计算顺序是没有定义的,所以输出可能是2010、2011和2011、2011。我们不能在这上面对求值顺序有依赖。
a = p() + q() * r();

这三个函数可能会以六种顺序被计算,对求值顺序是不确定的。但是可以通过添加中间变量的方式来确定求值顺序。
但是,下面两种方式的求值顺序是确定的:
(a < b) && (c < d);
expr1 ? expr2 : expr3;

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

(1)用宏定义表达式时,要使用完备的括号。
因为宏只是简单的替换,如果没有括号保护会因为运算符的优先级产生意想不到的情况。
#define ADD(a,b) a+b
//当计算ADD(a,b) * ADD(c,d)时本意是(a+b)*(c+d),但是展开后是a + b* c + d
#define ADD(a + b) ((a)+(b))
(2)使用宏时,不允许参数发生变化
(3)用大括号将宏定义的多条表达式括起来。

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

程序员应该保证初始化指针变量。当然对于全局变量而言,编译器会进行零初始化。但是对于局部变量,尤其是局部指针应该在声明的时候就进行初始化。


建议6:明确都好分隔符的奇怪之处

都好表达式的形式一般如下:
表达式1,表达式2,···,表达式n
if(++x,--y,x < 20 && y > 0)

会确保每个表达式都会被执行,整个表达式的值仅是最右边表达式的结果。逗号表达式即可以用作左值也可以用作右值。

建议7:时刻提防内存溢出

C语言中的字符串库没有采用相应的安全防护措施,在使用的时候要特别小心。例如,在使用strcpy,strcat的时候如果没有检查缓冲区的大小就会很容易引起安全问题。
#include<string.h>
char* strcpy(char* s1,const char* s2);

把s2指向的字符串(包括终止的空字符)复制到s1指向的数组中,如果复制发生在两个重叠的数组中,则行为时未定义的。函数返回s1的值。
#include<string.h>
char* strncpy(char* s1.const char* s2.size_t n);

从s2指向的数组中复制n个字符(不复制空字符后面的字符)到s1指向的数组中,如果复制发生在两个重叠的对象中,则行为时未定义的。函数返回s1的值。
#include<string.h>
char* strcat(char* s1,const char* s2);

把s2指向的串(包括终止的空字符)的副本添加到s1指向的串的末尾,s2的第一个字符覆盖s1的末尾的空字符,若行为发生在两个重叠的对象中,则行为时未定义的。函数返回s1的值。
#include<string.h>
char* strncat(char* s1,const char* s2,size_t n);

把s2指向的数组中的最多n个字符(空字符及后面的字符不添加)添加到s1指向的串的末尾。s2的第一个字符覆盖s1的末尾的空字符,通常在最后的结果后面添加一个空字符。
如果对象重叠则行为是未定义的。
 那么如何发生缓冲区溢出导致攻击的发生呢?如果填满预设的空间后,溢出的字符就会取代缓冲区后面的数据,如果这些溢出的数据恰好覆盖了后面的函数返回地址,函数调用完毕后,程序就会跳转到攻击者设定的“返回地址”,执行攻击者的代码。因此使用这些函数时必须检查是否可能发生越界。只有在不越界的情况下进行操作。

同样,访问边界数据也会发生溢出。如下面的函数:
void printData()
{
  for(int i = 0;a[i]!=0 && i<DATA_LENGTH;i++)
     cout<<data[i]<<endl;
}

这个问题并不容易被发现,当执行到最后的时候,也就是刚刚超越边界的第一个数据时,在判断i<DATA_LENGTH的同时也会访问这个位置的数据,这时就已经发生了越界。可以修改如下:
void printData()
{
  for(int i = 0; i<DATA_LENGTH;i++)
    if(a[i]!=0)     
        cout<<data[i]<<endl;
}

还有需要注意的就是在指针失效的时候的情况,在未初始化的时候使用或者两个指针指向同一个对象,但是其中一个指针释放了这个对象,但是另一个指针还是可能会访问这个对象。推荐使用智能指针,在后面会有专门的介绍。


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

函数指针在运行时的动态调用(例如函数回调,现在可以使用lambda替代)中使用广泛。直接定义复杂的函数指针由于太多的括号导致可读性降低,使用typedef可以让函数指针更直观和易于维护。

建议9:防止重复包含头文件

#ifndef _PROJECT_PATH_FILE_H
#define _PROJECT_PATH_FILE_H
``` ```
``` ```
#include "```.cpp"
#endif

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

struct A
{
  int a;
  char b;
  short c;
};
struct B
{
  char b;
  int a;
  short c;
};

int,short,char三种类型的大小分别是4,2,1.直觉而言以上两个结构体的大小都是7.但是实际上,sizeof(struct A) =8 , sizeof(struct B) = 12.
这是因为字节对齐导致的。
12345678
这是结构体A的布局空间。前四个字节是a的位置,第五个是b的位置,第六个位置会空着,最后两个位置是c的位置。
123456789101112
第一个是b的空间,2,到4空着,5到8是a的位置,9到10是c的位置,最后两个位置空着。
优化布局的原则是:把结构体中的变量按照类型大小依次声明,尽量减少中间的填充字节。提高存取效率。

建议11:将强制转型较少到最少

强制转型是一个“你必须全神关注才能正确使用”的特性。
在C++总必须转型的时候要使用static_cast<T*>(a),const_cast<T*>(a),dynamic_cast<T*>(a)和reinterpret_cast<T*>(a).
两个优点,一是标准库实现的更加安全的转型,二是调试时候更容易发现因为转型发生的异常。

建议12:优先使用前缀操作符

对内置类型而言前缀后缀没有区别,但是对自定义或者类对象而言会有很大的效率问题。在实现中后缀操作会先构造一个临时对象用于返回,然后完成自增操作,最后将保存的临时对象返回。正如80-20规则告诉我们的一样,如果在一个很大的程序里,程序的数据结构和算法不够优秀,它所带来的效率提升也是微不足道的。

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

关于变量定义的位置,越"local"越好,尽量避免变量作用域的膨胀。这样做不仅可以有效减少变量名污染,还有利于代码阅读者尽快找到变量定义,熟悉变量类型与初始值,使代码阅读更容易。
string changeToUpper(const string & str)
{
    string upperStr;
    if(str.length()<=0)
        throw errer("string is null");
    //do something
    return upperStr;

}

在上面的定义中,upperStr的定义有点早,因为如果函数抛出异常,那么变量将不会被使用。因此可以延缓变量的定义时机。
下面有两个定义:
for(int i = 0;i<10000;++i)
{
    ClassName obj;
    obj.dosomething();
}
//写成下面的形式会更加高效
ClassName obj;
for(int i = 0;i<10000;++i)
{
    obj.dosomething();
}

建议14:小心typedef使用中的陷阱

定义多个指针对象,形式直观,简单方便:
char *pa,*pb,*pc,*pd;//方式一
typedef char* PTR_CHAR;
PTR_CHAR pa,pb,pc,pd;//方式二
下面是其他用途:
typedef struct tarRect
{
    /* data */
}RECT;//用途一:格式声明
//用途二:声明一些与平台无关的类型
#ifndef _SIZE_T_DEFINED
#ifdef _WIN64
typedef unsigned _int64 size_t;
#else
typedef _W64 unsigned int size_t;
#endif
#define _SIZE_T_DEFINED
#endif

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

type function(type para1,type para2,```)

编译器对于可变参数函数的原型检查不够严格,容易引发问题,难于查错。不利于写出高质量的代码。所以应该尽量避免使用C风格的可变参数设置,使用更加安全的方式。其中C语言中的printf()就使用了可变长参数列表。

建议16:慎用goto

goto会破坏程序的局部性,且不易维护,难以阅读。过度使用goto会使代码流程错综复杂,难以理清头绪,所以,古国不熟悉尽量不去使用,如果已经习惯使用,试着不去使用。

建议17:堤防隐式转换带来的麻烦

建议18:正确区分void和void*

如果函数没有返回值,那么应该将其声明为void
如果函数没有参数,那么声明函数参数为void*
如果存在两个相同类型的指针,那么可以直接在两者之间赋值,如果是两个指向不同数据类型的两个指针,直接赋值会编译出错,必须使用强制类型转换才可以。而void*不同,任何类型的指针都可以直接赋值给它,无须强制转换。
int *pInt;
float *pFloat;
pInt = pFloat;//编译出错
pInt = (float *)pFloat;//强制转换
void *pVoid;
pVoid = pFloat;//正确

如果函数的参数是任意类型的指针,那么应该声明他的参数是void *,最典型的例子就是内存操作函数。
void * memcpy(void *dest,const void * src,size_t len);
void * memset(void *buffer,int c,size_t num);

仔细品味,就会发现这样的函数设计是多么富有哲学,任何类型的指针都可以传入函数中,传出的是一块没有具体类型规定的内存,这也体现了内存操作函数的意义。

建议19:明白在C++中如何使用C

建议20:使用 memcpy()函数时格外小心


使用memcpy(),menset(),memcmp()函数的时候我们对内存模型是可知的透明的,我们可以对底层的字节序列一一操作,简单而高效。C中所有的数据结构都是POD(Plain Old Data),一种古老的纯数据,满足以下条件:其二进制内容是可以随意赋值的,无论在什么地方,只要其二进制存在就可以准确无误的还原。但是在C++中,对象可能不再是一种纯数据,不能简单地通过基地址和偏移量来获得对象内存模型。是因为多态和虚函数的存在。

建议21:尽量用new/delete替换malloc/free

new是C++运算符,而malloc是C标准库函数。
通过new创建的对象是有类型的,而malloc创建的返回void*类型,需要进行强制转换。
new会自动调用对象的构造函数,malloc不会。
new失败会调用new_handler处理函数,而malloc失败直接返回NULL。
free和delete的区别相同于上述的1,3两点。

建议22:灵活的使用不同风格的注释

建议23:尽量使用C++标准的iostream

建议24:尽量采用C++风格的强制类型

建议25:尽量用const、enum、inline替换#define

#define PI 3.1415926535
在预处理阶段会使用数字把PI替换掉,编译器根本接触不到PI这个符号。因此不会进入到符号列表中,若代码中因为这个常量引发异常,会难以发现,出错信息只涉及数字,不涉及符号。使用下面的替换:
const double PI = 3.1415926535
当出现问题的时候通过PI标识,有章可循。另外使用常量会减少代码的多份复制,生成的目标代码会更小。这是因为预处理器对代码中的所有宏PI复制出一份3.1415926535,而使用常量只会为其分配一块内存。

建议26:用引用代替指针

首先说明一下区别:
第一:引用是别名,指针是一个实体,引用在声明的时候必须初始化,不存在引用的引用,因为引用没有地址,不占内存,只存在符号表中。
第二:引用的使用无需解引用,引用只在定义的时候初始化一次,指针可变。
第三:引用没有const,指针有。引用不能为空,指针可以为空。
第四:sizeof(引用)是指变量的大小。sizeof(指针)是指指针本身的大小。
第五:++意义不一样。
如果函数返回值是引用类型,那么意味着可以对该函数的返回值重新赋值。
template<typename T,int n>
class Array
{
public:
    T &operator []()
    {
        return a_[i];
    }
private:
    T a_[n];
};

Array<int,10> iArray;
for(int i = 0;i<10;i++)
    iArray[i] = i*2;