码迷,mamicode.com
首页 > 编程语言 > 详细

《Effective C++》资源管理:条款26-条款27

时间:2015-02-11 23:20:31      阅读:267      评论:0      收藏:0      [点我收藏+]

标签:c   高效c   类型转换   变量定义   

条款26:尽可能延后变量定义式的出现时间

在程序中定义一个变量,当控制流(control flow)到达这个变量定义时,程序就要承受变量的构造成本,当控制流离开这个作用域时,程序也要承受析构成本。无论这个变量是否使用,都要承受这些成本。应该尽量避免这种情形。或许你认为自己不会这样使用,但也未必。例如要写一个加密函数,但加密的密码要足够长。如果密码太短,会抛出一个异常logic_error(c++标准库,**条款**54)。

std::string encryptPassword(const std::string& psaaword)
{
    using namespace std;
    string encrypted;
    if(password.length()<MinimumPasswordLength)
    {
        throw logic_error("Password is too short");
    }
    ……//加密密码,把加密结果放到encrypted内
    return encrypted;
}

如果这个函数抛出异常,那么变量encrypted即便是未使用,也会执行构造函数和析构函数。
所以上述函数应该推迟encrypted的定义,直至到真正需要它
std::string encryptPassword(const std::string& psaaword)
{
using namespace std;

    if(password.length()<MinimumPasswordLength)
    {
        throw logic_error("Password is too short");
    }
    string encrypted;
    ……//加密密码,把加密结果放到encrypted内
    return encrypted;
    }

但这段代码任何不够高效。使用变量encrypted时,先定义,之后再给它赋值。在**条款**4曾经提到过,通过default构造函数构造一个对象,然后再用赋值操作符赋值,其效率不如使用一个直接指定初值的构造函数。

    std::string encryptPassword(const std::string& psaaword)
    {
        using namespace std;
        if(password.length()<MinimumPasswordLength)
        {
            throw logic_error("Password is too short");
        }
        string encrypted;//定义
        encrypted=password;//赋值
        encrypt(encrpted);
        ……//加密密码,把加密结果放到encrypted内
        return encrypted;
    }

其效率不如

    std::string encryptPassword(const std::string& psaaword)
    {
        using namespace std;
        if(password.length()<MinimumPasswordLength)
        {
            throw logic_error("Password is too short");
        }
        string encrypted(password);//定义+赋值
        encrypt(encrpted);
        ……//加密密码,把加密结果放到encrypted内
        return encrypted;
    }

这个条款“尽可能延后”的意义,不仅仅是把变量定义延迟到变量的使用前一刻为止,甚至该尝试延后这份定义到能够给它赋初值为止。这样还可以避免无意义的default构造函数。

那么循环时怎么做呢?把变量定义在循环外还是循环内?

代码A

Widget w;//定义在循环外
for(int i=0;i < n;++i)
    w=……;
    ……
}

代码B

for(int i=0;i<n;++i){
    Widget w(……);//定义并赋值
    ……
}

代价
代码A;1个构造函数+1个析构函数+n个赋值操作

代码B:n个构造函数+n个析构函数

如果class赋值成本低于构造成本+析构成本,代码A高效。否则代码B高效。但是代码A中,w的作用域比较代码B大,这样做可能给程序的可理解性和已维护性造成冲突,所以除非(1)你知道赋值成本比“构造+析构”成本低,(2)你正在处理代码中效率高度敏感。否则应该使用代码B

总结尽可能延后变量定义式的出现。这样可以增加程序清晰性并改善效率。

条款27尽量少做转型动作

C++规则的设计目标之一是保证“类型错误”不发生。如果编译时出现与类型转换相关的警告,别轻易放弃它,要确保这个警告是否安全。
C++中的转型(casts)会破坏类型系统(type system)。C++不同于C、Java和C#,这些语言中的转型比较必要,且难以避免,也比较不危险。
转型语法以后下三种
C风格的转型

(T)expression

函数风格的转型

T(expression)

上面两种形式无差别,只是小括号的位置不同而已。
C++中提供四种新式转型

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

这四种各有不同用法
**1.**const_cast通常被用来将对象的常量特性转除(cast away the constness)。它也是唯一由此能力的C++-style转型操作符。
**2.**dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作(后面细讲)。
**3.**reinterpret_cast意图执行低级转型,实际动作(结果)可能 取决于编译器,这表明其不可移植。例如将pointer to int转为int,这类转型常用在低级代码。例如,讨论讨论如何针对原始内存(raw memory)写一个调试用的分配器(debugging allocator),见条款50.
**4.**static_cast执行强迫隐式转换(implicit conversions)。例如将int转为double,non-const转为constant等。它也可以用来执行一些转换的反响转换,但无法将const转为non-const。

为了兼容,旧式的转型仍然合法,但是更提倡用新式的形式。因为1、新式转型很容易被辨识出来,可以很快找到代码中有哪些转型。2、新式转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。

但是当条用explicit构造函数将一个对象传给函数时, 常常使用旧式转型:

class Widget{
public:
    explicit Widget(int size);
    ……
};
void doSomeWord(const Widget& w);
doSomeWork(Widget(15));//函数风格
doSomeWork(static_cast<Widget>(15));//C++风格

使用C++风格转型时,不怎么像生成对象,而函数风格看起来更自然些。但是这只是感觉,只是看起来而已,这可能到时core dump。
转型在表面看起来是把一种类型视为另一种类型,但是编译器做的远远比这个多。编译器在编译器期间会真的编译出运行期间的代码。例如下面代码:

int x,y;
……
double d=static_cast<double>(x)/y;

将x转型为double,和不转型对比,代码肯定不同。因为double和int的底层表示都不相同。下面这个例子更加明显

class Base{……};
class Derived:public Base{……};
Derived d;
Base* b=&d;//隐喻的将Derived*转为Base*

上面代码是实现多态的手段之一,经常看到。Base对象和Derived对象的大小并不相同。这时,通常有个偏移量(offset)在运行期间施与Derived*指针身上,用于取得正确的Base*指针。
上面也说明单一对象,可能有多个地址,例如Derived对象,Derived*指针指向它和Base*指针指向它。在继承中经常发生这样事。通常你不应该假设对象在内存中如何布局,更不该以此假设为基础进行转型。例如把Base*转型为char*进行字符操作。
但是有时候我们需要一个偏移量,知道对象的布局,并以此进行了转型。但是编译器不同可能会导致对象布局不同,这意味着在一个平台编译通过的代码,在另一个平台未必能行。
有时候很容易写出似是而非的代码。比如,在许多应用框架中(application frameworks)都要求Derived classes内的virtual函数代码第一行都是调用base class对应的virtual函数。例如有个Window base class和SpecialWindow derived class。两者都定义了onResize函数,specialWindow中的onResize函数要先调用Window中的onResize函数。下面是一种实现方式,看起来对,但其实是错的。

class Window{
public:
    virtual void onResize(){……};
    ……
};
class specialWindow:public Window{
public:
    virtual void onResize(){
        static_cast<Window>(*this).onResize();//将this转为Window,然后调用。这样其实不行。
        ……
    }
};

这段代码中,使用了转型动作,我们所预期的是将this转为Window,之后调用Window::onResize。但实际上并不是这样,static_cast<Window>(*this).onResize()所调用的是this中“base class成分”的暂时副本身上的onResize()(函数只有一份,调用那个对象身上的函数没有什么关系,关键是函数中隐藏有this指针,会影响函数操作的数据)。要想实现想要的行为,其实很简单,就是使用域操作符::

class specialWindow:public Window{
    public:
        virtual void onResize(){
            Window::onResize();
            ……
        }
    };

这个例子也说明,如果你打算使用转型,那么很可能你正在往错误方向移动。使用dynamic_cast更是如此。
探究dynamic_cast之前,要注意的是dynamic_cast的许多实现版本运行非常慢。有一个很普遍的实现版本基于“class名称之字符串比较”。如果你在四层继承的单继承体系中的某个对象上执行dynamic_cast,刚刚所说的那个实现版本每一次dynamic_cast调用可能会耗用多达四次的strcmp调用,用以比较class名称。如果是深度继承或多重继承,成本更高。当然,某些版本这样实现有些原因(必须只是动态链接)。所以要对一般转型保持猜疑,尤其是对注重效率的代码中使用dynamic_cast。
之所以要使用dynamic_cast是因为我们手头只有一个Base指针或引用,但是要在Derived对象身上执行Derived class操作。通常有两个一般性操作可以避免这个问题。
1、使用容器其中存储指向Derived的指针(通常是智能指针,条款 13)。这样就可以消除“通过base class接口处理”Derived对象的需要。但是这种实现无法再同一个容器内存储指针“指向所有可能的派生类”。通常会需要多个容器,每个容器存储一种类型,它们必须具备类型安全(type-safe)。
2、可以通过base class接口处理“所有可能之各种Window派生类”,就是在base virtual函数做你想对派生类做的事。例如,可以在base class声明一个空的virtual函数,在不同的派生类有不同的实现,通过多态来实现想做的事。
上面不论哪种做法,都不是放之四海皆准的,在许多情况下,都可以提供一个可行的dynamic_cast替代方案。但是绝对要避免的一件事是所谓的“连串(cascading)dynamic_cast”,也就是看起来像这样的东西:

class Window{……};
……//derived classed定义在这里
typedef std::<vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
……
for(VPW::iterator iter=winPtrs.begin(); iter!=winPtrs.end();++iter)
{
    if(SpecialWindow1* psw1=dynamic_cast<SpecialWindow1*>(iter->get())){……)
    else if (SpecialWindow2* psw1=dynamic_cast<SpecialWindow2*>(iter->get())){……)
    else if (SpecialWindow3* psw1=dynamic_cast<SpecialWindow3*>(iter->get())){……)
    ……
}

这样的代码又大又慢,例如如果window继承体系有变动,那么就必须检查看看是否需要修改。如果加入新的Derived class,那么又要添加新的代码,加入新的判断分支。这样的代码应该由基于virtual函数来取代。
优良的C++代码应该很少使用转型,但是完全摆脱转型又不切实际。在一开始的将int转为double是一个通情达理的使用,虽然我们可以一开始声明为double类型来避免转型。像面对众多构造函数一样,通常我们应该隔离转型,把转型隐藏在函数内,通过接口保证调用者不受转型的影响。
总结
1、应该尽量少使用转型,尤其是在注重效率的代码中使用dynamic_cast。如果需要转型,试着设计无需转型代替。
2、如果必须使用转型,把它隐藏在函数内,客户通过接口使用,而不是由客户来实现转型。
3、使用新式的C++ style转型来代替旧的转型,因为新式的转型很容易辨识出来,而且它们有分类。

《Effective C++》资源管理:条款26-条款27

标签:c   高效c   类型转换   变量定义   

原文地址:http://blog.csdn.net/kangroger/article/details/43743531

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!