美文网首页
2018-12-23

2018-12-23

作者: 石樊 | 来源:发表于2018-12-23 12:24 被阅读0次

---

toc:

    depth_from: 1

    depth_to: 6

    ordered: false

html:

    embed_local_images: true

    embed_svg: true

    offline: false

    toc: true

print_background: false

export_on_save:

    html: true

---

# 让自己习惯C++

## 确定对象使用前已经初始化

1. 内置类型

    对于内置类型,赋值和初始化成本相同。建议使用前赋初始值。

2. 对于自定义类型

    C++规定,类中成员变量的初始化动作发生在进入构造函数本体之前。如果直接在构造函数中直接赋值,成员变量是经历过初始化和然后在构造函数中赋值两个操作。

    所以建议,在构造函数中使用成员初始化列表(member initialization list)。

    ```C++

    class MyObject

    {

        public:

            Object(Object object, int x)

                :object(object),

                x(x)

            {}

            Object()

                :object(),

                x(0)

            {}

        private:

            Object object;

            int x;

    }

    ```

    注意:如果有构造函数没有入参,记得把成员内置对象初始化。同时,成员初始化列表中变量顺序并不是初始化顺序,而是成员变量在类中定义的顺序决定。为了避免迷惑,建议和定义顺序保持一致。

3. non-local static对象

    C++对定义不同编译单元内的non-local static对象的初始化次序没有明确定义。而相反,对local static对象初始化时机有明确规定,就是对一次调用它的时候。

    注:编译单元是产生单一目标文件的一些源码,基本是它的单一源码文件和include的头文件。

    所以,使用包含local static对象的reference-returning函数来代替non-local static对象。

    ```C++

    Object& Object()

    {

        static Object object;

        return object;

    }

    ```

    但是,在多线程中因为这些reference-returning函数含有static对象,导致行为不确定。应该来说,只要是non-const static对象在多线程都有问题。???

    建议:在程序单线程启动阶段,手工调用所有的reference-returning函数,这样就可消除与初始化有关的竞速问题了。

# 构造/析构/赋值运算

## 请给基类声明virtual析构函数

### 问题

例如,factory函数一般返回基类指针,指针指向heap中的一片内存。但是最后使用完毕后delete对调用基类的析构函数。

但是C++指出,**当derived对象经由base指针delete掉,而该基类带有一个no-virtual析构函数,其行为未定义。一般是调用基类的析构函数,销毁掉基类部分。这样就导致内存泄露的问题。**

### 方法

请给为了**多态用途**的基类声明virtual析构函数。

一般的,一个class带有virtual函数,表示它被当作base class。所以,任何class只要带有virtual函数几乎应该有一个virtual析构函数。

**注意:有时想把一个class声明为抽象类,但手头上没有pure virtual函数,可以把析构函数设为pure virtual。因为抽象类一般是多态用途的base类,而base类几乎都建议析构是virtual的**

并非所有base类都是多态用途,例如uncopyable类,这些基类就用需要virtual析构函数。

### 原理

如果析构函数不是virtual,则调用的函数在编译时已经确定,由于指针是基类指针,所以调用的就是基类的析构函数;

如果析构函数是virtual,则调用的函数在运行期间确定,对象会有一个虚表指针,指向一个虚表数组,元素是函数指针,指向对应的子类virtual函数,所以调用的是子类的析构函数。

## Uncopyable类

### 问题

### 方法

* 方法一

    把拷贝构造函数和赋值操作符声明为私有的。

    优点是实现简单,但是类的成员函数和友元函数可以使用私有函数,解决方法是拷贝构造函数和赋值操作符只声明不定义,这样在链接期会报错。

    ```C++

    class MyClass{

        private:

            MyClass(const MyClass&);            //只声明,不定义

            MyClass& operator=(const MyClass&); //只声明,不定义

        ...

    }

    ```

* 方法二

    继承禁止拷贝类Uncopyable。这样如果使用MyClass类的拷贝构造函数或者赋值操作符,会调用基类对应的函数,由于基类是私有函数,则编译器会报错。

    优点是把链接期报错问题提前到编译期。

    ```C++

    class Uncopyable{

        protected:

            Uncopyable(){}

            ~Uncopyable(){}

        private:

            Uncopyable(const Uncopyable&);

            Uncopyable& operator=(const Uncopyable&);

    }

    class MyClass:public Uncopyable{}

    ```

    **或者你可以使用Boost提供的版本,叫做noncopyable的class。**

# 资源管理

## RAII类管理资源(13 14)

为防止资源泄露,请使用RAII(Resource Acquisition Is Initialization,取得资源就初始化,被销毁就释放资源)对象。

### 智能指针

两个常被使用的RAII类是tr1::shared_ptr(regerence-counting smart pointer引用计数型智能指针)和auto_ptr(智能指针)。

* auto_ptr,是个类指针对象,也就是所谓的智能指针,含义是当指针销毁,会自动delete所指对象。注意,拷贝时会把对象地址传给拷贝者,而原有指针为null。

    ```C++

    void f()

    {

        std::auto_ptr<Resource> pRsc1(resourceFactory());

        /*使用pRsc资源*/

        std::auto_ptr<Resource> pRsc2(pRsc1);//现在pRsc2指向资源,而pRsc1为null

        pRsc1 = pRsc2;//现在pRsc1指向资源,而pRsc2为null

        /*运行到最后销毁,会调用Resource析构函数释放资源*/

    }

    ```

* tr1::shared_ptr,是regerence-counting smart pointer引用计数型智能指针。与auto_ptr不同的是,可以拷贝,只有当所有指针都销毁时,delete所指对象。**通常share_ptr是RAII类的最佳选择**

    ```C++

    void f()

    {

        std::tr1::shared_ptr<Resource> pRsc1(resourceFactory());

        /*使用pRsc资源*/

        std::auto_ptr<Resource> pRsc2(pRsc1);//现在pRsc1、pRsc2都指向资源

        pRsc1 = pRsc2;//同上,无任何改变

        /*运行到最后,当pRsc1、pRsc2都销毁,会调用Resource析构函数释放资源*/

    }

    ```

    当然,如果释放资源不是简单的delete内存,还需要做其他事情,比如文件关闭,数据库关闭,这时智能指针shared_ptr可以指定某一函数为删除器。形式是:`std::tr1::shared_ptr<Resource> pRsc1(resourceFactory(), deleter)`其中deleter是某一函数指针,当没有智能指针指向该资源,会调用deleter函数。**而auto_ptr则没有这个设定**。

注意:这两个智能指针都是delete对象,当对象是个对象数组时,则不会调用delete[],则可以使用boost::scoped_array和boost::shared_array类来代替。

### 自定义RAII类

自定义的好处是可以定义想要的行为。

首先看下RAII类:

```C++

class ResourceManage

{

    public:

        explicit ResourceManage()

        {

            pRsc = resourceFactory();

        }

        ~ResourceManage()

        {

            resouceDestroy(pRsc);

        }

    private:

        Resource *pRsc;

}

```

自定义行为:

1. 禁止复制

    方法是,使用继承禁止拷贝类来实现`class ResourceManage:private Uncopyable{...}`

2. 对底层资源使用引用计数

    方法是,RAII内部的私有资源指针换成share_ptr来实现

3. 转移底部资源的拥有权

    方法是,把RAII内部的私有资源指针换成auto_ptr来实现

4. 复制底部资源

    方法是,重载拷贝构造函数和赋值运算符

## 通过RAII类使用资源

RAII类并不是封装资源,而是为了确保资源释放一定会发生。但是由于把资源或资源指针作为私有成员,这个类也阻碍我们对资源的使用。例如:

```C++

class Resource

{

    ...

    doSomeThing(){...}//资源类有个api会对资源做些事情

    ...

}

void f()

{

    ResourceManage rscMag;//资源获取

    rscMag->doSomeThing()//错误!!!doSomeThing不是ResourceManage的成员方法

}

```

方法:

1. 显示转换

    显示的提供get函数,把内部资源或资源指针返回。

    注意,由于ResourceManage类不是为了封装资源,所以通过公有函数把私有成员返回也很正常。这样不仅隐藏客户不需要看到的部分,但同时也为客户全面准备好所有东西。

    ```C++

    rscMag.get()->doSomeThing()

    ```

    相似地,智能指针tr1::shared_ptr和auto_ptr也有get成员函数,把自己转为普通指针。

2. 隐式转换

    RAII类,改良版

    ```C++

    class ResourceManage

    {

        public:

            explicit ResourceManage()

            {

                pRsc = resourceFactory();

            }

            ~ResourceManage()

            {

                resouceDestroy(pRsc);

            }

            operator Resource() const{return pRsc;}//定义隐式转换Resource类型函数

        private:

            Resource *pRsc;

    }

    ```

注意:

    虽然隐式转换更符合书写,但是毕竟是隐私转换,可能增加错误转换的风险。所以一般显式转换比较安全,隐式转换比较方便。

## 以独立语句将newed对象置入智能指针

用RAII类管理资源,或多或少的使用智能指针。而资源对象初始化后赋值给智能指针,需要单独一句。如下:

```C++

//这里资源初始化和使用放到一条语句中。

//C++并没有定义同一条语句,逻辑上没有关联的步骤的执行先后顺序。

//如果在new Resource执行后执行getPara()异常终端,资源指针还没有传给智能指针。这样会导致后面资源始终没有释放。

doSomeThing(std::tr1::shared_ptr<Resource>(getResource()), getPara());

```

应该这样做:

```C++

std::tr1::shared_ptr<Resource> pRsc(getResource());//像这些关键步骤最后单独一个语句

doSomeThing(pRsc, getPara());

```

注意:其中getResource是factory函数,其返回Resource的指针。为了防止用户没有及时把指针传给智能指针,可以把factory函数设计成返回智能指针。

# 设计与声明

## 让接口容易被正确使用,不易被误用

比如一个接口是设置日期,但是参数容易误用。

```C++

void setDate(int month, int day, int year);//setDate的声明

setDate(30, 3, 1999);//错误,应该是3, 30, 1999的

setDate(3, 32,1995);//错误,3月没有32号

```

一种方法是把入参class化或者struct化。

```C++

void setDate(const Month& m, const Day& d, const Year& y);//setDate的声明

struct Day{

    explicit Day(int d):val(d){}

    int val;

}

...

setDate(Day(30), Month(3), Year(1995));

```

但有时候,参数表示的意义不能用基本类型来表示。比如获取日期的其中一个字段。

```C++

int getNumByField(String field);

int year = getNumByField("yeer");//错误,应该是year的

```

大部分人会想到使用宏或者枚举,但是这些都是基本类型,还是容易犯错。可以使用如下:

```C++

int getNumByField(const DateField& dateField);

class DateField{

    public:

        static DateField year(){return DateField("year");}

    ...

    private:

        explicit Month(String field):field(field){};

        String field;

}

getNumByField(DateField.year());

```

## 用pass-by-reference 替换 pass-by-value

### 问题

```c++

//函数声明

void doSomeThing(Base b);

class Base{};

class Drive:public Base{};

//函数使用

void doSomeThing(Drive d);

```

其中Drive类为实参,而Base类为形参。参数传递时会调用Base类的拷贝构造函数,进而会把Drive的特性丢失掉,造成“切割问题”。

### 方法

用pass-by-reference 替换 pass-by-value。其实C++编译器底层就是用指针实现引用的。改为指针也能解决,但会引入指针相关的问题。

注意:内置类型大部分比指针简单,所以内置类型还是推荐使用pass-by-value

相关文章

网友评论

      本文标题:2018-12-23

      本文链接:https://www.haomeiwen.com/subject/putnkqtx.html