建议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 000000002)空指针。指针与整形占据的空间是一样的。0在指针的可以替换为NULL,现在使用nullptr。
int *pValue = 0;//合法 int *pValue = 1;//不合法,不可以表示地址 作为指针类型使用0时,推荐下面的使用方法: float *pNum = NULL;//赋值 if(pNum == NULL);//比较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 = 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);
#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.
这是因为字节对齐导致的。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
优化布局的原则是:把结构体中的变量按照类型大小依次声明,尽量减少中间的填充字节。提高存取效率。
建议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*不同,任何类型的指针都可以直接赋值给它,无须强制转换。
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
在预处理阶段会使用数字把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;