大多数情况i下,提出class和声明函数是最花费时间的地方,但有些实现上的东西还需要注意
条款26:尽可能延后变量定义式的出现时间
尽可能延后变量定义式的出现,这样可以增加程序的清晰度并改善程序效率。
问题A (效率问题):
// 这个函数过早定义变量“encrypted”
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;
}
如果出现异常,那么string encrypted 则不会使用,就会付出构造成本和析构成本
解决(初步)
//
std::string encryptPassword(const std::string& password)
{
if(password.length() < MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
std::string encrypted; // 定义并初始化
// 必要动作
// 将password置入变量encrypted 内
return encrypt;
...
}
这里延后了encrypted的定义式,但不够好,因为encrypted虽然获得定义,但无任何实参,意味着调用的是default构造函数,后面才进行赋值,效率不高
解决(最终)
//定义并初始化encrypted最佳做法
std::string encryptPassword(const std::string& password)
{
... //检查length 如前
std::string encrypted(password); // 定义并初始化
encrypt(encrypted);
...
}
这里在定义时就用password作为实参构造对象,跳过了default构造,做到了效率最大化
由此可以得出
尽可能延后的意义:
你不止应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给他初值实参为止
问题B (清晰度问题):
// 方法A:定义于循环外
Widget w;
for(int i = 0; i < n; i++)
{
w = 取决于i的某个值;
...
}
// 方法B:定义于循环内
for(int i = 0; i < n; i++)
{
Widget w(取决于i的某个值);
...
}
使用 方法一或者方法二 ?
成本分析
一. 1个构造函数 + 1个析构函数 + n 个 赋值操作(Widget对象)
二. n个构造函数 + n个析构函数
结论
一般而言,二的做法比一好,原因是可理解性和易维护好
除非
- 已知赋值成本比构造+析构低
- 对于效率高度敏感
则使用方法一,否则使用方法二
条款 27 :尽量少做转型动作
-------------------------------------------- 转型破坏了类型系统,那可能导致任何种类的麻烦
1. 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计。
2. 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放进他们自己的代码中。
3. 宁可使用C++ -style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
旧式转型
- (类型说明符)表达式
- 类型说明符(表达式)
两个形式并无差别
新式转型
static_cast<类型说明符>(表达式)
用来强迫隐式转换,例如将non-const 转化成const 对象,int -> double 等等
dynamic_cast<类型说明符>(表达式)
- 用于安全向下转型
- 唯一无法由旧式转换执行
- 唯一可能耗费重大成本
const_cast<类型说明符>(表达式)
- 用于去除对象的常量性
- 唯一有此能力发C++操作符
reinterpret_cast<类型说明符>(表达式)
- 执行低级转型,例如pointer to int 的
新式转型的优势
- 容易被辨识
- 各转型的目标比较单一,编译器可以快速找出错误
唯一使用旧式转型的时机是:
当要调用一个explicit构造函数将一个对象传递给一个函数时
后面没太看懂,后期再补吧
条款 28:避免返回handle指向对象内部成分
避免返回handle(句柄:用来取得某个对象,包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助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::shared_ptr<RectData> pData; // 智能指针
};
用户需要能计算Rectangle 的 范围,所以这个class 提供upperLeft函数和lowerRight 函数,根据条例20(尽量用pass-by-reference-to-const(const引用)替换pass-by-value(传值)),可以实现以下函数
class Rectangle
{
public:
...
Point& upperLeft() const {return pData->ulhc;}
Point& lowerRight() const {return pData->lrhc;}
...
};
问题所在 : 这两个函数返回指向对象的引用,那么用户可以使用这个指针修改成员而不被发现,那么就没有封装性了
教训(同样适用指针和迭代器):
- 成员变量的封装性最多只等于“返回其reference”的函数的访问级别。本例中虽然pData的ulhc和lrhc都是private,但实际上是public,由于成员函数传出他们的reference。
- 虽然是const成员函数,但是函数的调用者可以修改这笔数据。
解决 :
class Rectangle
{
public:
...
const Point& upperLeft() const {return pData->ulhc;}
const Point& lowerRight() const {return pData->lrhc;}
};
只要对它们的返回类型上加上const即可,这样用户可以读矩形的Points 但不能修改。
进一步的问题:
但即使如此, 还是返回了"代表对象内部"的handle,有可能在其他场合带来问题。更确切的说,可能导致dangling handle(虚吊号码牌):这种handle 所指的东西不复存在。
class GUIObject {...};
const Rectangle
boundingBox{const GUIOject & obj};
现在可能用户会使用这个函数;
GUIObject * pgo;
...
const Point * p = &(boudningBox(*pgo).upperLeft()); // 取得一个指针指向外框左上角
对boudningBox的调用获得新的Rectangle对象,当这个对象销毁时,间接导致里面的Points析构,最终导致 p 指针指向一个不存在的对象,从而p指针变成dangling虚吊的
这并不意味你绝不可以让成员函数返回handle,有时候你还必须那么做。例如:operator[] 。尽管如此,这样函数毕竟是例外,不是常态
条款29:为“异常安全”而努力是值得的
1. 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
异常安全性函数三大保证 :
- 基本承诺:如果异常被抛出,程序内的任何事务仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)
-
- 强烈保证:如果异常抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
-
- 不抛异常保证:承诺不抛出异常,因为他们总是能够完成他
- 们原先承诺的功能。作用于内置类型(如指针等等)身上的所有操作都提供nothrow保证。
2. "强烈保证"往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
3. 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款 30:透彻了解inline 的里里外外
1. 将大多数inline限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
2. 不要只因为function template出现在头文件,就将它们声明为inline。
inline的思想 :
将“对此函数的每一个调用”都以函数本体替换之
inline的优势 :
- 免除函数调用成本
- 编译器就因此有能力对它执行语境相关最优化。
- 如果inline函数的本体很小,编译器针对”函数本体“所产出的代码可能比函数调用产生的代码更小
inline的缺点:
- 可能增加目标码的大小,过度使用inline会导致程序体积过大,引发换页行为
- 无法随着程序库的升级而升级,如果f是程序库内的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序库设计者决定改变f,所有用到f的客户端程序都必须重新编译。
然而如果f是non-inline函数,一旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少很多。 - 构造函数和析构函数往往是inline的糟糕候选人
inline 提出的特点:
inline只是对编译器的一个申请,不是强制命令。既可以隐喻提出,也可以明确提出。
- 隐喻提出
成员函数和friend函数 在class内定义都被隐喻声明为inline函数。
class Person
{
public:
...
int age() const // 一个隐喻的inline申请
{
return theAge;
}
private:
int theAge;
};
- 显示提出
定义式前加上关键字inline
template<typename T>
inline const T& std::max(const T& a, const T& b) // 明确申请inline
{
return a < b ? b : a;
}
条款31:将文件间的编译依存关系降至最低
支持”编译依存性最小化“的一般构想是 : 相依于声明式,不要相依于定义式,基于此构想的两个手段是Handle classes 和 Interface classes 。
程序库头文件应该以“完全且仅有声明式”形式存在,这种做法不论是否涉及templates都适用。
能力有限,实在没什么经验,看不太懂。。。
有机会回来补完