您的位置:首页 > 编程语言 > C语言/C++

《Effective C++》学习笔记(七)

2014-08-30 22:25 190 查看
原创文章,转载请注明出处:/article/8316774.html

前言

接下来的两篇笔记将围绕《Effective C++》第五章《实现》来学习。


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

只要你定义了一个变量,而且它具有一个构造、析构函数,那么它将会要有构造和析构的成本。即使变量最终并未被使用,这个成本也仍会耗费,我们应该也要避免这种情况。

或许你会认为你不可能你不可能定义一个不使用的变量,但是想想有没有把异常的情况考虑进去。比如:

string encryptPassword(const string &password)
{
using namespace std;
string encryted;
if (password.length() < MinimumPasswordLength)
{
...
}
return encryted;
}
对象encryted并没完全使用,但如果有个异常被丢出,它就真的被使用了,但是你必须要付出它的构造析构成本。

所以延后它的定义式,放到if判断之后,确保没有异常抛出再定义。

但是这个代码还是有问题,因为目前它调用的是默认构造函数,但是一般定义出来之后都会给它赋初值。所以可以将构造后赋值改成,直接在构造时指定初值。

我们不只应该延后变量的定义,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造非必要对象,还可以避免无意义的默认构造函数。

可能你也会和我一样,考虑过变量是定义在循环外还是循环内,如下:

// 方法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个析构函数

如果一个赋值的成本低于一组构造+析构的成本,那么A大体而言比较高效,尤其是n比较大的时候。

否则,B或许更好,因为A发明合法中w的作用域比B大,这对程序的可理解性和易维护性造成冲击,因此除非1)你知道赋值成本比“构造+析构”成本低,2)你正在处理代码中效率高度敏感的部分,否则你应该使用做法B。

总结:

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


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

C++规则的设计目标之一是,保证“类型错误”绝不可能发生。理论上如果你的程序通过了编译,就表示它并不企图在任何对象上执行不安全、无意义、荒谬的动作。

不幸的是转型破坏了类型系统。它可能导致任何种类的问题,有些容易发现,有些却很难。

转型分为旧式转型和新式转型,旧式转型指的是C风格的转型,如:

(T) expression

T (expression)

这两种形式没太大差别,只是括号位置不同,C++还提供了四种新式转型:

const_cast< T >(expression) // 将对象的常量性质移除
dynamic_cast< T >(expression) // 执行“安全向下转型”,如pointer-to-base转换成pointer-to-derived
reinterpret_cast< T >(expression) // 执行低级转型,如将point int转换成int,在低级代码以外很少见
static_cast< T >(expression) // 强迫隐式转换,例如将non-const变为const,将pointer-to-base转换成pointer-to-derived,等等多种转换,但是他不能将const转换成non-const,因为那是const_cast才办得到
旧式转型依旧合法,但是新式转型比较受欢迎,因为:

1)它们在代码中容易被辨识出来,以便查错。

2)各种动作目标转型动作的目标愈窄化,编译器愈可能诊断出错误,如去掉常量性,除非用const_cast,否则无法通过编译。

许多程序员认为转型其实什么都没做,只是告诉编译器把某种类型视作另一种类型,这是一个错误的观念。任何一个类型转换,往往真的令编译器编译出运行期间执行的代码。

class Base { ... }
class Derived:public Base { ... }
Derived d;
Base *pb = &d;
这里我们不过是简历一个base class指针指向一个derived class对象,但是有时候上述两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期间被施行于Derived*指针上,用以取得正确的Base*指针值。

上述例子表名一个对象可能拥有一个以上的地址(如”以Base*指向它“时的地址和”以Derived*指向它“时的地址)。

看完这个,我们来看一下dynamic_cast,dynamic_cast的很多实现版本的执行速度比较慢。

之所以需要dynamic_cast通常是因为你想在一个你认定为Derived class 的对象身上执行Derived class操作函数,但是你手上只有一个指向base的pointer或者reference,你只能靠它们来处理对象。有两个一般性做法可以避免这个问题:

1)使用容器,并在其中存储直接执行派生类对象的指针(通常是智能指针),如此便消除额”通过基类接口处理对象“的需要。

class Window
{
...
}
class SpecialWindow: public Window
{
public:
void blink();
...
};
typedef vector<shared_ptr<Window>> VPM;
VPM winPtrs;
for (auto iter : winPtrs)
{
if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get())) psw->blink();
}
应该改成:

class Window
{
...
}
class SpecialWindow: public Window
{
public:
void blink();
...
};
typedef vector<shared_ptr<SpecialWindow>> VPSM;
VPSM winPtrs;
for (auto iter : winPtrs)
{
(*iter)->blink();
}


2)在基类中提供virtual函数做你想对各个派生类做的事。举个例子,虽然只有SpecialWindow可以闪烁,但或许将闪烁函数声明于Base class内并提供一份什么也没做的默认实现码是有意义的。

class Window
{
virtual void blink() { }
...
}
class SpecialWindow: public Window
{
public:
void blink() { ... };
...
};
typedef vector<shared_ptr<Window>> VPM;
VPM winPtrs;
for (auto iter : winPtrs)
{
(*iter)->blink();
}
不论哪一种写法——“使用类型安全容器”或“将virtual函数往继承体系上方移动”,都并非放之四海而皆准,但许多情况下它们提供了一个可行的dynamic_cast替代方案。当它们行得通时,你应该欣然拥抱他们。

优良的C++代码很少使用转型,但要完全摆脱它们又不太切实际。

总结:

1)如果可以,尽量避免转型,贴别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。

2)如果转型是必要的,试着将它隐藏于某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内。

3)宁可使用C++Style(新式)转型,不要使用旧型转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。


条款28:避免返回handles指向对象内部成分

书上给了个矩形的例子,我们来看看:

class Point // 用来表示坐标点
{
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
}

struct RectData // 用点的数据来存储矩形
{
Point ulhc;
point lrhc;
};

class Rectangle
{
public:
Point& upperLeft() const
{
return pData->ulhc;
}
Point& lowerRight() const
{
return pData->lrhc;
}
...
private:
std::shared_ptr< RectData ><rectdata> pData;
...
}</rectdata>

我们可以看到Rectangle有upperLeft、lowerRight两个方法,编译可以通过,但是实际上是有问题的,为什么呢,其实之前提到过,因为它们的返回值可以用来更改pData内部数据。

这给我带来了两个教训:

1)成员变量的封装性最多只等于“返回其reference”的函数的访问级别。

2)如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。(之前有提过bitwise constness)

上面我们返回的是reference,但事实上对于指针和迭代器也是适用的,他们都是handles(号码牌,用来取得某个对象)。

其实我们在返回值Point&之前加上const就可以解决这两个问题。这样用户可以读取Points,但是无法修改。

但即使如此,upperLeft和lowerRight还是返回呢“代表对象内部”的handles,有可能在其他场合带来问题。更明确的说,它可能导致dangling handles(空悬的号码牌)。举个例子:

class GUIobject
{
...
};

const Rectangle boundingBox(const GUIObject &obj);
GUIObject *pgo;
...
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());
upperLeft返回的是一个临时变量,在取地址的表达式结束后,这个变量就会析构了,所以pUpperLeft保存的指针也就变成了空悬、虚吊的了。

这并不意味着你一定不能让成员函数返回handle。有时候你必须那么做,比如operator[]就允许你“采摘”string和vector的个别元素,而这些operator[]s就是返回reference指向“容器内的数据”,那些数据会伴随着容器的销毁而销毁。尽管如此,这样的函数只是例外不是常态。

总结:
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条件可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: