C++编程规范 2 初始化和类型转换

来源:互联网 发布:阿里云 代码托管 编辑:程序博客网 时间:2024/06/05 06:39

2 初始化和类型转换

  2.1  声明、定义与初始化

规则2.1 禁止用memcpy、memset初始化非POD对象

说明:POD 全称是“Plain Old Data”,是C++ 98标准(ISO/IEC 14882, first edition, 1998-09-01)

中引入的一个概念, POD类型主要包括int, char, float,double,enumeration,void,指针等原始

类型及其集合类型,不能使用封装和面对对象特性(如用户定义的构造/赋值/析构函数、基类、虚函

数等)。
由于非POD类型比如非集合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥

用内存拷贝可能会导致严重的问题。

即使对集合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡

memcpy、memset操作。

示例:×××产品程序异常退出(core dump)。

经过现场环境的模似,程序产生COREDUMP,其原因是:在初始化函数内使用memset(this, 0,

sizeof(*this))进行了类的初始化,将类的虚函数表指针被清空,从而导致使用空指针。

解决方案:使用C++构造函数初始化,不要使用memset函数初始化类对象。

建议2.1 变量使用时才声明并初始化

说明:变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并同时初始化,非常方便

地避免了此类低级错误。

在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:

 程序难以理解和维护:变量的定义与使用分离。

 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的

    空值(比如零)来初始化,这通常是一种浪费,如果变量在被赋于有效值以前使用,还会导致错误。

遵循变量作用域最小化原则与就近声明原则, 使得代码更容易阅读,方便了解变量的类型和初始值。

特别是,应使用初始化的方式替代声明再赋值。

示例:

    //不好的例子:声明与初始化分离
    string  name; //声明时未初始化:调用缺省构造函数
    //…….
    name=”zhangsan”; //再次调用赋值操作符函数;声明与定义在不同的地方,理解相对困难

    //好的例子:声明与初始化一体,理解相对容易
    string  name(”zhangsan”); //调用一次构造函数

建议2.2 避免构造函数做复杂的初始化,可以使用“init”函数

说明:正如函数的变量都在函数内部初始化一样,类数据成员最好的初始化场所就是构造函数,数据

成员都应该尽量在构造函数中初始化。

以下情况可以使用init()函数来初始化:

 需要提供初始化返回信息。

 数据成员初始化可能抛异常。

 数据成员初始化失败会造成该类对象初始化失败,引起不确定状态。

 数据成员初始化依赖this指针:构造函数没结束,对象就没有构造出来,构造函数内不能使用this

    成员;

 数据成员初始化需要调用虚函数。在构造函数和析构函数中调用虚函数,会导致未定义的行为。

示例:数据成员初始化可能抛异常:

    class CPPRule
    {
     public:
       CPPRule():size_(0), res (null) {};   //仅进行值初始化
       long init(int size)
       {
          //根据传入的参数初始化size_, 分配资源res
       }
    private:
       int size_;
       ResourcePtr* res;
    };
    //使用方法:
    CPPRule a;
    a.init(100);

建议2.3 初始化列表要严格按照成员声明顺序来初始化它们

说明:编译器会按照数据成员在类定义中声明的顺序进行初始化,而不是按照初始化列表中的顺序,

如果打乱初始化列表的顺序实际上不起作用,但会造成阅读和理解上的混淆;特别是成员变量之间存

在依赖关系时可能导致BUG。

示例:

    //不好的例子:初始化顺序与声明顺序不一致
    class Employee
    {
    public:
       Employee(const char* firstName, const char* lastName)
          : firstName_(firstName), lastName_(lastName)
          , email_(firstName_ + "." + lastName_ + "@huawei.com") {};
    private:
       string email_, firstName_, lastName_;
    };
类定义email_是在firstName_, lastName_之前声明,它将首先初始化,但使用了未初始化的

firstName_和lastName_,导致错误。

在成员声明时,应按照成员相互依赖关系按顺序声明。

建议2.4 明确有外部依赖关系的全局与静态对象的初始化顺序

说明:如果全局对象A的成员变量有外部依赖,比如依赖另外一个全局变量B,在A的构造函数中访问B,

隐含的规则就是B先于A初始化,然而全局与静态对象的初始化与析构顺序未有严格定义,无法确保B

已经完成初始化,而每次生成可执行程序都可能发生变化,这类BUG难以定位。

通常采用单件(Singleton)模式或者把有依赖关系的全局对象放在一个文件中定义来明确初始化顺序。

同一个文件中,若全局对象a在全局对象b之前定义,则a一定会在b之前初始化;但是不同文件中的全

局对象就没有固定的初始化顺序。可以在main()或 pthread_once() 内初始化一个运行期间不回收的

指针。

  2.2  类型转换

避免使用类型分支来定制行为:类型分支来定制行为容易出错,是企图用C++编写C代码的明显标志。

这是一种很不灵活的技术,要添加新类型时,如果忘记修改所有分支,编译器也不会告知。使用模板

和虚函数,让类型自己而不是调用它们的代码来决定行为。
规则2.2 使用C++风格的类型转换,不要使用C风格的类型转换

说明:C++的类型转换由于采用关键字,更醒目,更容易查找,编程中强迫程序员多停留思考片刻,谨

慎使用强制转换。

C++使用const_cast, dynamic_cast, static_cast, reinterpret_cast等新的类型转换,它们允许用

户选择适当级别的转换符,而不是像C那样全用一个转换符。

dynamic_cast:主要用于下行转换,dynamic_cast具有类型检查的功能。dynamic_cast有一定的开销,

建议在调测代码中使用。

static_cast:和C风格转换相似可做值的强制转换,或上行转换(把派生类的指针或引用转换成基类的

指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。下行转换(把基类的指

针或引用转换成派生类的指针或引用)时,由于没有动态类型检查,所以不安全的,不提倡下行转换。

reinterpret_cast:用于转换不相关的类型。reinterpret_cast强制编译器将某个类型对象的内存重

新解释成另一种类型,相关代码可移植不好。建议对reinterpret_cast<> 的用法进行注释,有助于减

少维护者在看到这种转换时的顾虑。

const_cast:用于移除对象的 const属性,使对象变得可修改。

示例:

    extern void Fun(DerivedClass* pd);
    void Gun(BaseClass* pb)
    {
       //不好的例子: C风格强制转换,转换会导致对象布局不一致,编译不报错,运行时可能会崩溃
       DerivedClass* pd = (DerivedClass *)pb;

       //好的例子: C++风格强制转换,明确知道pb实际指向DerivedClass
       DerivedClass* pd = dynamic_cast< DerivedClass *>(pb);

       if(pd)
          Fun(pd);
    }

建议2.5 避免使用reinterpret_cast

说明:reinterpret_cast用于转换不相关类型。尝试用reinterpret_cast将一种类型强制转换另一种

类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。

建议2.6 避免使用const_cast

说明:const_cast用于移除对象的 const性质。

const属性提供一种安全感,让程序员知道这个定义是固定不变的,从而不需要担心后面的变化。如果

const属性在程序员不知道的地方被消除,会带来很多严重的后果。

示例:不好的例子

    unsigned const int arraySize = 1024;
    int &newArraySize = const_cast<int&>(arraySize);
    newArraySize = 2048;
这里如果不通过引用或者指针访问arraySize,那么arraySize的值始终是1024。可是如果被作为一个

指针或者引用传给其他函数进行取值的话,会发现值变成了2048。

示例:不好的例子:强制去掉入参的const属性,导致函数可以对入参进行修改。
 void setObj(TBase const *obj)
    {
       //m_pObj的定义为:
       TBase *m_pObj;
       m_pObj = const_cast<TBase*>(obj);
       m_pObj->value = 123;
    }

建议2.7 使用虚函数替换dynamic_cast

说明:很多刚从C语言转过了的程序员习惯这样的思路:若对象的类型是T1,则做某种处理;若对象的

类型是T2,则做另外的处理等等。但C++提供了更好的解决方案:虚函数。

虚函数与dynamic_cast类型转换相比:

 虚函数更安全,不会出现强制转换错的情况;

 虚函数效率更高:用函数指针,避免条件判断;

 虚函数不需要在编码时确定对象的真实类型,而dynamic_cast必须告知要转成的类型,运行时若

    类型不当返回空指针或者抛异常;

 虚函数适用性更强:虚函数是真正动态绑定;类型转换当增加或删除一个派生类时,dynamic_cast

    必须增减相应的代码。