Effective C++读书笔记(条款24-29)

Effective C++第四篇,扩展的有点多...

(四).设计与声明

____________________________________________________________________________________________________________________________________

条款24:若所有参数皆需类型转换,请为此采用non-member函数

#1.如果你需要为某个函数的所有参数(包括被 this指针所指的那个隐喻参数)进行

类型转换,那么这个函数必须是个 non-member。

//假设有个有理数类:
class Rational{
public:
    Rational(int numrator=0, int denomination=1);
    int numerator()const;
    int denomination()const;
    ...
};
//如果operator*写成Rational成员函数的话:
class Rational{
public:
    ...
    const Rational operator*(const Rational& rhs)const;
    ...
};

Rational oneEigth(1,8);  //很好
Rational oneHalf(1,2);   //很好
Rational result = oneHalf * oneEighth;  //很好
result = result * oneEighth //很好

//当尝试混合运算时:
result = oneHalf*2;   //result = oneHalf.operator*(2);很好
result = 2*oneHalf;   //result = 2.operator*(oneHalf);出错,继续尝试non-member函数

//当编译器试图调用non-member函数 result = operator*(2, oneHalf);时
//依旧没有找到对应的non-member函数,因此最终导致出错。

//【所以我们应该实现一个这样的non-member函数】:
const Rational operator*(const Rational& lhs,const Rational& rhs)
{
    return Rational(lhs.numerator()*rhs.numerator(),
                lhs.denomination()*rhs.denomination());
}

#2.决定上述non-member函数是non-friend函数还是friend函数的理由是

“是否必须对class内部private成员或protected成员进行访问”,

若需要,则该non-member函数应是friend函数,

若不需要,为满足最大封装性原则,该non-member函数应是non-friend函数。

____________________________________________________________________________________________________________________________________

条款25:考虑写出一个不抛出异常的swap函数

#1.如果swap的缺省实现码对你的class或class template提供了可接受的效率,

请使用该缺省实现版:

namespace std{
    template<typename T>
    void swap(T&a, T&b)        //std:swap的典型实现
    {                           //置换a和b的值
        T temp(a);
        a = b;
        b = temp;
    }
}

反之,如果因class或class template使用了pImp手法(pointer to implementation):

class WidgetImpl{
public:
    ...
private:
    int a, b, c;
    std::vector<double> v;
    ...
};
class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget&rhs)
    {
        ...
        *pImv = *(rhs.pImpl);
        ...
    }
    ...
private:
    WidgetImpl* pImv;
};

从而引起了效率不足,请做以下几件事来提高效率:

(1).提供一个public swap member函数:

//pImv是private成员变量,所以要从内部来swap,
//当然也可用friend,但这样的写法和STL一致
class Widget{
public:
    ...
    void swap(Widget& other)
    {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    ...
};
//该swap函数保证了异常安全性和高效性,因为swap置换的是指针,对于指针
//和内置类型的置换操作绝不会抛出异常,并且还很高效。如果是对自定义类型
//执行swap函数,则会因为copy构造函数和copy assignment操作符而允许抛出异常。

(2).在你的class或template所在的namespace(如果没有,则是globe)内提供一个

non-member swap函数,并令它调用上述的swap函数,例如:

namespace WidgetStuff{
    ...
    template<typename T>
    class Widget {...}
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

(3).如果正在编写的是一个class(而非class template),为你的class特化std::swap,

并让它调用你的swap member函数,例如:

namespace std{
    template<>
    void swap<Widget>(Widget& a, Widget& b)
    {
        a.swap(b);
    }
}
//=======================================================================
// 理由:
// 好处是即使客户不慎直接调用了std::swap,至少也能获得一个全特化版的swap函数,
// 另外,C++只运行class template偏特化,但不允许function template的偏特化,
// 因此无法通过编译(虽然某些编译器错误的接受了它)。
//=======================================================================

(4).如果你调用swap,请使用using声明式,以便让std::swap在函数中曝光可见,从

而在swap直接书写调用时可获得如下的效率查找:

<1>.在与class相同的 namespace 中查找swap函数。

<2>.在globe坏境中查找swap函数。

<3>.在namesapce std中查找全特化swap函数(如果是class而非template,并且予以了实现)

<4>.在namesapce std中查找一般swap函数。

#2.在std内进行std templates全特化是好的,但千万不要在std内加入一些对于std

来说全新的东西,因为C++标准委员会禁止加入那些已经声明好的东西,即便通过了

编译和执行,其行为也不能得到明确。

____________________________________________________________________________________________________________________________________

(五).实现

____________________________________________________________________________________________________________________________________

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

#1.尽可能延后变量定义式的出现,如果可以,要直到初始化为止。

理由1:增加程序的可读性,使程序结构更清晰。

理由2:对于用户(或系统)定义类型,过早的定义变量,可能会导致程序在抛出异常,

函数返回等情况下变量没有使用,因此会带来额外的构造和析构成本。

(内置类型的定义,可以用C风格初始化,也可以延迟变量定义时间,为了统一起见,

最好延迟变量定义的时间。)

例如:

std::string encryptPassword(const std::string& password)
{
    using namespace std;
    string encrypted;
    if(password.length() < MinimumPasswordLength)
        throw logic_error("Password is too short");
    ...

    return encrypted;
}
//若抛出logic_error异常,encrypted完全未被使用。
//所以应改为:
std::string encryptPassword(const std::string& password)
{

    if(password.length() < MinimumPasswordLength)
        throw logic_error("Password is too short");
    using namespace std;
    string encrypted(password);
    ...
    return encrypted;
}

#2.方法A和方法B的定义方式各有千秋,如果你知道赋值的成本比“构造+析构”的成本低,

并且注重效率,那么你应该使用方法A。否则,为了保证程序的可读性和易维护性,

你应该使用方法B。

//方法A:定义于循环外
Widget w;
for(int i=0;i<n;i++){
    w = 取决于i的某个值;
    ...
}
//方法B:定义于循环外
for(int i=0;i<n;i++){
    Widget w(取决于i的某个值);
}

//做法A:1个构造函数+1个析构函数+n个赋值操作
//做法B:n个构造函数+n个析构函数

____________________________________________________________________________________________________________________________________

条款27:尽量少做转型动作

#1.const_cast,dynamic_cast,reinterpret_cast,static_cast转型操作符。

const_cast:常量性转除,唯一有此能力的C++-style转型操作符。

dynamic_cast:主要用来执行”安全向下转型“,也就是用来决定某对象是否归属

于继承体系中的某个类型。它是唯一无法由旧式语句执行的动作,也是唯一可能

耗费重大运行成本的转型动作。

reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,

这也表示它不可移植,例如将一个 pointer to int 转型为一个 int。这一类

转型在低级代码以为很少见。

static_cast:强迫隐式转换,例如

将 non-contst 转型为 const,

将 int 转型为 double,

将 void*指针与 typed指针互转,

将 pointer-to-base 与 pointer-to-derived互转,

但无法将 const 转型为 non-const,因为这个只有const_cast办的到。

#2.如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast,如果

有个设计需要转型动作,试着发展无需转型的替代设计。

理由1:C++规则的设计目标之一是,保证“类型错误”绝不发生,为避免在任何对象

身上执行不安全,荒谬的操作,请尽量避免转型。

理由2:任何一个类型转换往往令编译器编译出运行期间执行的码,例如:

class Base{...};
class Derived:public Base{...};
Derived d;
Base* pb = &d;
//为获取Base*指针值,在运行期间会有个偏移量(offset)添加在Derived*指针身上,
//编译器会为此额外编译出运行期间执行的码。(注意:这里的offset并不明确,
//因为对象的布局方式和地址计算方式随编译器的不同而不同。)

理由3:dynamic_cast会耗费重大的运行期成本,在深度继承中,单单多次的

”class名称之字符串“的strcmp比较就会耗费一定的效率成本。

【两种避免dynamic_cast转型的替代策略】:

(1).在单一继承中,直接用 derived class 指针替代 base class 接口处理的derived

class 对象,例如:

//假设先Window/SpecialWindow继承体系中只有SpecialWindows才支持闪烁效果,
//试着不要这样做:
class Window {...};
class SpecialWindow:public Window{
public:
    void blink();
    ...
};

typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    if(Specialwindow* psw=dynamic_cast<SpecialWindow*>(iter->get()))
        psw->blink();
}
//应该改成这样:
typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    (*iter)->blink();
}

(2).多重继承中,用virtual函数代替dynamic_cast所做的事

//用dynamic_cast时如下所示:
class Window {...};
...
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    if(SpecialWindow* psw1=
        dynamic_cast<SpecialWindow*>(iter->get()))psw1>blink();
    else if(SpecialWindow* psw2=
        dynamic_cast<SpecialWindow*>(iter->get()))psw2->blink();
    else if(SpecialWindow* psw3=
        dynamic_cast<SpecialWindow*>(iter->get()))psw3->blink();
}
//以上的做法中有一连串dynamic_cast,这会使产生出来的代码又长又慢,
//而且基础不稳,因为继承体系一有改变,所有这一类代码都要检查看看
//是否需要修改,例如添加一个新的derived class,则要在一连串判断
//中加入新的条件分支,及其不易维护。
//所以应该改成这样:
class Window {
public:
    virtual void blink();{}   //缺省实现码什么也没做
    ...
};
class SpecialWindow:public Window{
public:
    virtual void blink(){...}; //blink做某些事
    ...
};
...
typedef std::vector<std::tr1::shared_ptr<Window>> VPW;
VPW winPtrs;
...          //容器,内含Window指针,指向所有可能的派生类对象
for(VPW::iterator iter = winPtrs.begin();
    iter != winPtrs.end();++iter){
    (*iter)->blink();
}

#3.如果转型无可避免,请将它隐藏在函数背后,这样客户调用该函数时,不需要将

转型动作放入它们的代码里,保证了代码的封装性和易维护性。

#4.尽量用C++-style的转型替换旧式转型(构造函数除外),因为

(1).它们很容易在代码中辨别出来,因此简化了“找出类型系统破坏点”的过程。

(2).转型动作分门别类,因此更有利于编译器找出错误的运用。

____________________________________________________________________________________________________________________________________

条款28:若返回 handles 指向对象内部成分,请保证返回值 const 约束大于等于成员函数

#1.References, 指针和迭代器通通都是 handles,用来取得某个对象。

#2.若返回handles指向内部对象,并且成员函数是const,请保证该返回值也是const 。

class Point{
public:
    Point(int x, int y);
    ...
    void setX(int newVal);
    void setY(int newVal);
    ...
};
struct RectData{
    Point ulhc;
    Point lrhc;
};
class Rectangle{
    ...
private:
    std::tr1::shared_ptr<RectData> pData;
    Point& upperLeft()const{return pData->ulhc;}
    Point& lowerRight()const{return pData->lrhc;}
    ...
};
Point coord1(0,0);Point coord2(100, 100);
const Rectangle rec(coord1, coord2);

rec.upperLeft().setX(50);
//这里返回的handles可以改变,但与upperLeft const()成员函数的本意是相反的。
//所以应改成:
class Rectangle{
    ...
private:
    ...
    const Point& upperLeft()const{return pData->ulhc;}
    const Point& lowerRight()const{return pData->lrhc;}
    ...
};
//这样就可以在保证读写权的同时禁止涂写权。

【本条为个人观点:】

#3.对于返回值为自定义(或系统定义)类型的non-const成员函数,返回内部成员

变量的handles(non-const)表示希望该内部变量是public,而对于返回值为内置

类型的成员变量,则该使用pass-by-valued的getXXX()函数和setXXX(X)来分别

区分读写行为。

例如:

//当upperLeft(),lowerRight()为non-const时:
class Rectangle{
    ...
private:
    ...
    Point& upperLeft(){return pData->ulhc;}
    Point& lowerRight(){return pData->lrhc;}
    ...
};

Point coord1(0,0);Point coord2(100, 100);
Rectangle rec(coord1, coord2);
//我们可以如下这样用函数统一接口设置值和得到值:
rec.upperLeft().setX(50);
int val = rec.lowerRight().getY();

____________________________________________________________________________________________________________________________________

条款29:为”异常安全“而努力是值得的

#1.异常安全函数即使发生异常也不会泄露资源或允许任何数据败坏。

这样的函数区分三种可能的保证:基本型,强烈型,不抛异常性。

(1).基本型:异常抛出后,class一切约束条件满足,程序内事物处于一种有效的状态中。

(2).强烈保证型:异常抛出后,程序状态不改变,函数回到调用前的状态。

(3).不抛异常型:保证不抛出任何异常,对内置类型(int,指针,等)身上的所有操作

都提供nothrow保证。

(任何使用动态内存的东西在无法满足内存需求下,都会抛出bad_alloc异常)

#2.除非调用传统非安全异常安全码,否则应保证异常安全,因为传统非异常安全码

不具备异常安全性,所以任何调用它的代码都不具备异常安全性,因此为其撰写

异常安全码是么有任何意义的。

#3.”强烈保证“往往能够以 copy-and-swap实现出来。

//一个用copy-and-swap的示例:
struct PMImpl{            //PMImpl = PrettyMenu Impl
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges;
};
class PrettyMenu{
    ...
private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pIml;
};
void PrettyMenu::changeBack(std::istream& imgSrc)
{
    using std::swap;
    Lock ml(&mutex);        //获得mutex的副本数据
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc)); //修改副本
    ++pNew->ImageChanges;
    swap(Impl, pNew);        //置换(swap)数据,释放mutex
}
//,但”强烈保证“ 并非对所有函数都具有可实现或具备实现意义:

(1).因为如copy-and-swap这样的强烈保证往往会耗费更多的时间和空间,为了

保证更好的效率,应以”基础保证“来替换,这样才更具现实意义。

(2).当函数对”非局部性数据”有影响时,提供强烈保证就很难了。

void someFunc{
    ...     //对local状态做一份副本
    f1();
    f2();
    ...     //将修改后的状态置换过来
}
//假设someFunc对“局部数据”提供强烈保证,但f1(),f2()对someFunc()来说是
//非局部性的”,因此f1(),f2()会对someFunc()的强烈保证期望造成影响,
//假设f1(),f2()提供一个基本保证,显然someFunc将只会提供基本保证,
//而若f1(),f2()也提供强烈保证,但若f1(),f2()并未抛出异常,在此之后
//却抛出了异常,那么someFunc()也无法提供强烈保证,毕竟程序状态改变了。

//另一方面,在someFunc中像某些数据库之类的非局部数据一旦被改变,想恢复
//就很困难,这时someFunc想提供强烈保证就真的很难了。

#4.函数提供的“异常安全保证”最多只等于其所调用之各个函数的“异常安全保证”

中的最弱者(从上述例子中可以看出)。

____________________________________________________________________________________________________________________________________

时间: 2024-08-09 10:38:49

Effective C++读书笔记(条款24-29)的相关文章

effective C++ 读书笔记 条款06

条款06:若不想使用编译器自动生成的函数,就该明确拒绝: 直接看代码与注释: #include <iostream> using namespace std; class Test { public: Test() { } ~Test() { } /* void TT() { Test t1; Test t2(t1); } */ private: Test(const Test& test); Test& operator = (const Test& test); }

effective C++ 读书笔记 条款11

条款11: 在operator= 中处理"自我赋值" 在实现operator=时考虑自我赋值是必要的就像 x=y .我们不知道变量x与y代表的值是否为同一个值(把x和y说成是一个指针更恰当一点). 例如以下 第一版: #include <iostream> using namespace std; class bitmap { public: bitmap() { cout<<"调用bitmap()无參构造函数"<<endl; }

Effective C++读书笔记(条款1-10)

不得不说,Effective C++确实是一本C++进阶的好书,刚浏览完第二遍, 现对其做一个大体性的总结 ,并进行适当的展开,作为以后C++参考复习之用. (一).让自己习惯C++ ____________________________________________________________________________________________________________________________________ 条款1:视C++为一个语言联邦 #1.将C++ 分

effective C++ 读书笔记 条款20

条款20:宁以 pass-by-reference-to-const 替换 pass -by -value 1:采用后者效率高:看代码: #include <iostream> using namespace std; class Person { public: Person() { cout<<"Person()"<<endl; } Person(const Person& p) { cout<<"Person(co

effective C++ 读书笔记 条款08

条款08  别让异常逃离析构函数: 假设在析构函数其中发生了异常,程序可能会过早结束或者导致不明白行为(异常从析构函数传播出去) 看代码: #include <iostream> using namespace std; class DBConnection { public: void close() { int i = 3; int j = 0; int k = i/j; printf("%d\n",k); } }; class DBConn { public: DBC

Effective C++读书笔记(条款18-23)

(四).设计与声明 ____________________________________________________________________________________________________________________________________ 条款18:让接口容易被使用,而不容易被误用 #1.导入新类型可以让接口不易被误用,以函数替换对象则可以保证类型安全性. 例如: class Date{ public: Date(int month, int day

effective C++ 读书笔记 条款12与条款13

条款12:确定你的public继承塑膜出is-a关系: 这个条款主要将了一些特殊情况:比如企鹅是鸟,企鹅可以继承于鸟,但是鸟会飞,企鹅却不能飞:还有让正方形继承矩形可能也会造成这种尴尬! 这个问题以前想过,但是不知道怎么解决,如果现实生活当中确实要这么使用:比如 猫 狗 鱼  猪等等许多动物继承Animal类,但是猫狗等不会游泳, 假如这里是有很多动物,不能采用鱼里面专门加一个方法!  这个现在还没想出来,条款12也没有讲如果要这么用该怎么处理就是将要避免这样. is - a; 在面向对象程序设

effective C++ 读书笔记 条款10

条款10:  令operator= 返回一个reference to *this; 关于赋值,我们可以这样写: int  x,y,x; x =y = z; 这就是所谓的连续赋值 为了实现"连锁赋值"赋值操作符必须返回一个reference指向操作符的左侧实参.这是我们为class实现赋值操作符时应该遵循的协议: #include <iostream> using namespace std; class Widget { public: Widget() { cout<

Effective C++读书笔记(条款35-40)

(六).继承与面向对象设计 ____________________________________________________________________________________________________________________________________ 条款35:考虑virtual函数以外的其他选择 #1.virual函数的四个替代方案: (1).使用non-virtual interface(NVI)手法,那是 Template Method 设计模式