编写高质量代码,改善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 = 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);

把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.

这是因为字节对齐导致的。

1 2 3 4 5 6 7 8

这是结构体A的布局空间。前四个字节是a的位置,第五个是b的位置,第六个位置会空着,最后两个位置是c的位置。

1 2 3 4 5 6 7 8 9 10 11 12

第一个是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;

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

时间: 2024-08-17 13:32:36

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

编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predicate<in T> 常用委托有: IComparer<in T> 下面例子演示了泛型类型参数指定逆变所带来的好处: class Program { static void Main() { Programmer p = new Programmer { Name = "Mi

编写高质量代码改善C#程序的157个建议——建议27:在查询中使用Lambda表达式

建议27:在查询中使用Lambda表达式 LINQ实际上是基于扩展方法和Lambda表达式的.任何LINQ查询都能通过扩展方法的方式来代替. var personWithCompanyList = from person in personList select new { PersonName = person.Name, CompanyName = person.CompanyID==0?"Micro":"Sun" }; foreach (var item in

编写高质量代码改善C#程序的157个建议——建议26:使用匿名类型存储LINQ查询结果

建议26:使用匿名类型存储LINQ查询结果 从.NET3.0开始,C#开始支持一个新特性:匿名类型.匿名类型有var.赋值运算符和一个非空初始值(或以new开头的初始化项)组成.匿名类型有如下基本特性: 即支持简单类型也指出复杂类型.简单类型必须是一个非空初始值,复杂类型则是一个以new开头的初始化项. 匿名类型的属性是只读的,没有属性设置器,它一旦被初始化就不可更改. 如果两个匿名类型的属性值相同,那么就认为这两个匿名类型相等. 匿名类型可以再循环中用作初始化器. 匿名类型支持智能感知. 匿名

编写高质量代码改善C#程序的157个建议——建议20:使用泛型集合代替非泛型集合

建议20:使用泛型集合代替非泛型集合 在建议1中我们知道,如果要让代码高效运行,应该尽量避免装箱和拆箱,以及尽量减少转型.很遗憾,在微软提供给我们的第一代集合类型中没有做到这一点,下面我们看ArrayList这个类的使用情况: ArrayList al=new ArrayList(); al.Add(0); al.Add(1); al.Add("mike"); foreach (var item in al) { Console.WriteLine(item); } 上面这段代码充分演

编写高质量代码改善C#程序的157个建议——建议12: 重写Equals时也要重写GetHashCode

建议12: 重写Equals时也要重写GetHashCode 除非考虑到自定义类型会被用作基于散列的集合的键值:否则,不建议重写Equals方法,因为这会带来一系列的问题. 如果编译上一个建议中的Person这个类型,编译器会提示这样一个信息: “重写 Object.Equals(object o)但不重写 Object.GetHashCode()” 如果重写Equals方法的时候不重写GetHashCode方法,在使用如FCL中的Dictionary类时,可能隐含一些潜在的Bug.还是针对上一

编写高质量代码改善C#程序的157个建议——建议13: 为类型输出格式化字符串

建议13: 为类型输出格式化字符串 有两种方法可以为类型提供格式化的字符串输出.一种是意识到类型会产生格式化字符串输出,于是让类型继承接口IFormattable.这对类型来 说,是一种主动实现的方式,要求开发者可以预见类型在格式化方面的要求.更多的时候,类型的使用者需为类型自定义格式化器,这就是第二种方法,也是最灵活 多变的方法,可以根据需求的变化为类型提供多个格式化器.下面就来详细介绍这两种方法. 最简单的字符串输出是为类型重写ToString方法,如果没有为类型重写该方法,默认会调用Obj

编写高质量代码改善java程序的151个建议——导航开篇

2014-05-16 09:08 by Jeff Li 前言 系列文章:[传送门] 下个星期度过这几天的奋战,会抓紧java的进阶学习.听过一句话,大哥说过,你一个月前的代码去看下,惨不忍睹是吧.确实,人和代码一样都在成长,都在变好当中.有时候只是实现功能的编程,长进不了呀. 博客提供的好处就可以交流,讨论的学习方法你们应该知道. 在这里,我会陆陆续续的进行对<编写高质量代码改善java程序的151个建议>看法,希望大家点击交流. 正文 看这本书原因   1.项目做的只是实现功能,然而没有好好

编写高质量代码改善C#程序的157个建议——建议90:不要为抽象类提供公开的构造方法

建议90:不要为抽象类提供公开的构造方法 首先,抽象类可以有构造方法.即使没有为抽象类指定构造方法,编译器也会为我们生成一个默认的protected的构造方法.下面是一个标准的最简单的抽象类: abstract class MyAbstractClass { protected MyAbstractClass(){} } 其次,抽象类的方法不应该是public或internal的.抽象类设计的本意是让子类继承,而不是用于生成实例对象的.如果抽象类是public或internal的,它对于其它类型

编写高质量代码改善C#程序的157个建议——建议85:Task中的异常处理

建议85:Task中的异常处理 在任何时候,异常处理都是非常重要的一个环节.多线程与并行编程中尤其是这样.如果不处理这些后台任务中的异常,应用程序将会莫名其妙的退出.处理那些不是主线程(如果是窗体程序,那就是UI主线程)产生的异常,最终的办法都是将其包装到主线程上. 在任务并行库中,如果对任务运行Wait.WaitAny.WaitAll等方法,或者求Result属性,都能捕获到AggregateException异常.可以将AggregateException异常看做是任务并行库编程中最上层的异

编写高质量代码改善C#程序的157个建议——建议89:在并行方法体中谨慎使用锁

建议89:在并行方法体中谨慎使用锁 除了建议88所提到的场合,要谨慎使用并行的情况还包括:某些本身就需要同步运行的场合,或者需要较长时间锁定共享资源的场合. 在对整型数据进行同步操作时,可以使用静态类Interlocked的Add方法,这就极大地避免了由于进行原子操作长时间锁定某个共享资源所带来的同步性能损耗.回顾建议83中的例子. static void Main(string[] args) { int[] nums = new int[] { 1, 2, 3, 4 }; int total